f0ck algos
This commit is contained in:
+200
-3
@@ -4739,6 +4739,7 @@ window.cancelAnimFrame = (function () {
|
||||
// Special check for random
|
||||
if (link.id === 'random' || link.id === 'nav-random') {
|
||||
e.preventDefault();
|
||||
document.dispatchEvent(new CustomEvent('f0ck:randomTriggered'));
|
||||
|
||||
const outEls = document.querySelectorAll('.content, ._204863, .item_title');
|
||||
outEls.forEach(el => {
|
||||
@@ -4970,9 +4971,9 @@ window.cancelAnimFrame = (function () {
|
||||
const parts = pathname.split('/').filter(Boolean);
|
||||
const isItemLink = isItemPath(pathname);
|
||||
if (isItemLink) {
|
||||
// Links inside comment bodies or MOTD should not inherit tag/hall/user context
|
||||
const _inComment = anyLink.closest(".comment, .comment-content, .comment-body, .motd-content");
|
||||
loadItemAjax(targetUrl, !_inComment);
|
||||
// Links inside comment bodies, MOTD, or sidebar suggestions/recommendations should not inherit tag/hall/user context
|
||||
const _skipInherit = anyLink.dataset.inheritContext === 'false' || anyLink.closest(".comment, .comment-content, .comment-body, .motd-content, .sidebar-video-card, .sidebar-recommendations-list, #sidebar-recommendations-container");
|
||||
loadItemAjax(targetUrl, !_skipInherit);
|
||||
} else {
|
||||
const isBrand = anyLink.classList.contains('navbar-brand');
|
||||
const fromModal = document.body.classList.contains('onara-modal-open') || !!anyLink.closest('#onara-modal');
|
||||
@@ -13192,3 +13193,199 @@ document.addEventListener('keydown', (e) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── User Interest & Behavioral Recommendation Engine (Client Tracking) ──
|
||||
(function() {
|
||||
const STORAGE_TAGS = 'f0ck_guest_tag_affinity';
|
||||
const STORAGE_CREATORS = 'f0ck_guest_creator_affinity';
|
||||
|
||||
let currentTrackingItem = null;
|
||||
|
||||
const loadStoredMap = (key) => {
|
||||
try {
|
||||
const data = localStorage.getItem(key);
|
||||
return data ? JSON.parse(data) : {};
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const saveStoredMap = (key, map) => {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(map));
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
const updateLocalAffinity = (key, name, delta) => {
|
||||
if (!name || typeof name !== 'string') return;
|
||||
const cleanName = name.trim().toLowerCase();
|
||||
if (!cleanName) return;
|
||||
|
||||
const map = loadStoredMap(key);
|
||||
const existing = map[cleanName] || { score: 0, count: 0, ts: Date.now() };
|
||||
existing.score = Math.max(-10, Math.min(500, (existing.score || 0) + delta));
|
||||
existing.count = (existing.count || 0) + 1;
|
||||
existing.ts = Date.now();
|
||||
map[cleanName] = existing;
|
||||
|
||||
// Prune to top 60 to prevent storage bloat
|
||||
const entries = Object.entries(map);
|
||||
if (entries.length > 60) {
|
||||
entries.sort((a, b) => b[1].score - a[1].score);
|
||||
const pruned = Object.fromEntries(entries.slice(0, 50));
|
||||
saveStoredMap(key, pruned);
|
||||
} else {
|
||||
saveStoredMap(key, map);
|
||||
}
|
||||
};
|
||||
|
||||
const f0ckInterestEngine = {
|
||||
getTopAffinities: () => {
|
||||
const tagsMap = loadStoredMap(STORAGE_TAGS);
|
||||
const creatorsMap = loadStoredMap(STORAGE_CREATORS);
|
||||
|
||||
const topTags = Object.entries(tagsMap)
|
||||
.filter(([, v]) => v.score > 0)
|
||||
.sort((a, b) => b[1].score - a[1].score)
|
||||
.slice(0, 15)
|
||||
.map(([k]) => k);
|
||||
|
||||
const topCreators = Object.entries(creatorsMap)
|
||||
.filter(([, v]) => v.score > 0)
|
||||
.sort((a, b) => b[1].score - a[1].score)
|
||||
.slice(0, 10)
|
||||
.map(([k]) => k);
|
||||
|
||||
return { topTags, topCreators };
|
||||
},
|
||||
|
||||
recordInteraction: ({ itemId, tags = [], creator = '', duration = 0, percent = 0, type = 'dwell' }) => {
|
||||
if (!itemId) return;
|
||||
|
||||
// Behavioral weighting: time spent is a primary signal
|
||||
let delta = 1.0;
|
||||
if (type === 'skip' || (duration < 2.5 && percent < 15)) {
|
||||
delta = -1.5;
|
||||
} else if (type === 'click_suggestion') {
|
||||
delta = 2.5;
|
||||
} else if (type === 'finish' || percent >= 75 || duration >= 20) {
|
||||
delta = 3.0;
|
||||
} else if (duration >= 8 || percent >= 35) {
|
||||
delta = 1.5;
|
||||
}
|
||||
|
||||
// Update local storage (for persistent guest learning across browser restarts)
|
||||
if (Array.isArray(tags)) {
|
||||
tags.forEach(t => updateLocalAffinity(STORAGE_TAGS, t, delta));
|
||||
}
|
||||
if (creator) {
|
||||
updateLocalAffinity(STORAGE_CREATORS, creator, delta);
|
||||
}
|
||||
|
||||
// Send beacon to backend
|
||||
const payload = {
|
||||
item_id: itemId,
|
||||
duration: Math.round(duration * 10) / 10,
|
||||
percent: Math.round(percent),
|
||||
type
|
||||
};
|
||||
|
||||
try {
|
||||
if (navigator.sendBeacon) {
|
||||
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
|
||||
navigator.sendBeacon('/api/v2/track/interaction', blob);
|
||||
} else {
|
||||
fetch('/api/v2/track/interaction', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
keepalive: true
|
||||
}).catch(() => {});
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
recordSuggestionClick: (itemId, tags = [], creator = '') => {
|
||||
if (!itemId) return;
|
||||
f0ckInterestEngine.recordInteraction({ itemId, tags, creator, type: 'click_suggestion' });
|
||||
},
|
||||
|
||||
flushCurrentTracking: () => {
|
||||
if (!currentTrackingItem) return;
|
||||
const { itemId, startTime, maxPercent, tags, creator } = currentTrackingItem;
|
||||
currentTrackingItem = null;
|
||||
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
let type = 'dwell';
|
||||
if (duration < 2.5 && maxPercent < 15) {
|
||||
type = 'skip';
|
||||
} else if (maxPercent >= 75 || duration >= 20) {
|
||||
type = 'finish';
|
||||
}
|
||||
|
||||
f0ckInterestEngine.recordInteraction({
|
||||
itemId,
|
||||
tags,
|
||||
creator,
|
||||
duration,
|
||||
percent: maxPercent,
|
||||
type
|
||||
});
|
||||
},
|
||||
|
||||
startTrackingItem: () => {
|
||||
f0ckInterestEngine.flushCurrentTracking();
|
||||
|
||||
// Only track if on an item page
|
||||
const itemIdEl = document.querySelector('[data-item-id]');
|
||||
const itemId = itemIdEl ? parseInt(itemIdEl.dataset.itemId, 10) : null;
|
||||
if (!itemId) return;
|
||||
|
||||
const tags = Array.from(document.querySelectorAll('#tags .tag-text, #tags .tag-badge, .tag-link'))
|
||||
.map(el => el.textContent.trim().replace(/^#/, ''))
|
||||
.filter(Boolean);
|
||||
|
||||
const creatorEl = document.querySelector('#a_username, [data-username]');
|
||||
const creator = creatorEl ? (creatorEl.dataset.username || creatorEl.textContent.trim()) : '';
|
||||
|
||||
currentTrackingItem = {
|
||||
itemId,
|
||||
startTime: Date.now(),
|
||||
maxPercent: 0,
|
||||
tags,
|
||||
creator
|
||||
};
|
||||
|
||||
// Attach media playback listeners
|
||||
const media = document.querySelector('video, audio');
|
||||
if (media) {
|
||||
const updatePercent = () => {
|
||||
if (!currentTrackingItem || !media.duration) return;
|
||||
const p = (media.currentTime / media.duration) * 100;
|
||||
if (p > currentTrackingItem.maxPercent) {
|
||||
currentTrackingItem.maxPercent = p;
|
||||
}
|
||||
};
|
||||
|
||||
media.addEventListener('timeupdate', updatePercent, { passive: true });
|
||||
media.addEventListener('ended', () => {
|
||||
if (currentTrackingItem) currentTrackingItem.maxPercent = 100;
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.f0ckInterestEngine = f0ckInterestEngine;
|
||||
|
||||
// Listen to item content loads and page teardown
|
||||
document.addEventListener('f0ck:contentLoaded', f0ckInterestEngine.startTrackingItem);
|
||||
window.addEventListener('beforeunload', f0ckInterestEngine.flushCurrentTracking);
|
||||
window.addEventListener('pagehide', f0ckInterestEngine.flushCurrentTracking);
|
||||
|
||||
// Initial check on load
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
setTimeout(f0ckInterestEngine.startTrackingItem, 200);
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => setTimeout(f0ckInterestEngine.startTrackingItem, 200));
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user