f0ck algos

This commit is contained in:
2026-09-12 04:25:49 +02:00
parent b55a17dc85
commit 2913b03d50
12 changed files with 1756 additions and 22 deletions
+467
View File
@@ -1044,6 +1044,13 @@
currentPage = 1;
hasMore = true;
loadActivity(false); // Force reload with loading state
recommendationsLoaded = false;
let savedTab = 'comments';
try { savedTab = localStorage.getItem('sidebar_active_tab'); } catch (_) {}
if (savedTab === 'videos') {
loadVideoRecommendations(false);
}
} else {
// Immediately render from cache to avoid flicker
renderFromCache();
@@ -1057,6 +1064,456 @@
syncSidebarAndComments();
}
// Video / Media Recommendations Logic
let recommendationsLoading = false;
let recommendationsLoadingMore = false;
let recommendationsLoaded = false;
let currentRecommendations = [];
const seenRecommendationIds = new Set();
const RECOMMENDATIONS_LIMIT = SIDEBAR_MAX_COMMENTS;
let recIoSentinel = null;
let recObserver = null;
const renderVideoCard = (video) => {
const videoKey = (window.f0ckSession?.enable_item_slugs && video.slug) ? video.slug : video.id;
const rClass = video.rating_class || 'untagged';
const blurNsfw = localStorage.getItem('blurNsfw') === 'true';
const blurNsfl = localStorage.getItem('blurNsfl') === 'true';
const blurSfw = localStorage.getItem('blurSfw') === 'true';
const blurUntagged = localStorage.getItem('blurUntagged') === 'true';
let isBlurred = false;
if (rClass === 'nsfw' && blurNsfw) isBlurred = true;
else if (rClass === 'nsfl' && blurNsfl) isBlurred = true;
else if (rClass === 'sfw' && blurSfw) isBlurred = true;
else if (rClass === 'untagged' && blurUntagged) isBlurred = true;
const mime = video.mime || '';
const isAudio = mime.startsWith('audio/');
const isVideo = mime.startsWith('video/');
const isImage = mime.startsWith('image/');
let thumbUrl = `/t/${video.id}.webp`;
if (isBlurred) {
thumbUrl = `/t/${video.id}_blur.webp`;
}
if (window.applyThumbCacheBust) thumbUrl = window.applyThumbCacheBust(thumbUrl);
let displayTitle = '';
if (video.title && video.title.trim()) {
displayTitle = video.title.trim();
} else if (video.tags && video.tags.length > 0) {
displayTitle = video.tags.map(t => '#' + t).join(' ');
} else {
const typeLabel = isAudio ? 'Audio' : (isVideo ? 'Video' : 'Image');
displayTitle = `${typeLabel} #${video.id}`;
}
const isAnonGuest = window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in;
const authorName = isAnonGuest ? 'anonymous' : (video.display_name || video.username);
const userColorStyle = (!isAnonGuest && video.username_color) ? `style="color: ${escapeHtml(video.username_color)}"` : '';
const timeStr = video.stamp
? (window.f0ckTimeAgo ? window.f0ckTimeAgo(new Date(video.stamp * 1000).toISOString()) : '')
: '';
const fullDate = video.stamp ? new Date(video.stamp * 1000).toLocaleString() : '';
const xdBadge = (video.xd_score && video.xd_score > 0)
? `<span class="sidebar-video-xd xd-tier-${video.xd_tier || 0}">xD ${video.xd_score}</span>`
: '';
const ext = (video.mime ? video.mime.split('/')[1] : '')
.replace('jpeg', 'jpg')
.replace('x-shockwave-flash', 'flash')
.replace('x-flac', 'flac')
.replace('mpeg', 'mp3')
.toUpperCase();
const formatBadge = ext ? `<span class="sidebar-media-format-badge">${escapeHtml(ext)}</span>` : '';
const overlayIcon = isAudio ? 'fa-solid fa-music' : (isVideo ? 'fa-solid fa-play' : 'fa-solid fa-image');
let thumbContentHtml = '';
if (isAudio && !video.has_coverart) {
thumbContentHtml = `
<div class="sidebar-media-placeholder audio">
<i class="fa-solid fa-music"></i>
</div>
`;
} else {
thumbContentHtml = `
<img src="${thumbUrl}" class="sidebar-video-thumb" alt="${escapeHtml(displayTitle)}" loading="lazy" onerror="this.style.display='none'; if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');" />
<div class="sidebar-media-placeholder ${isAudio ? 'audio' : ''} hidden">
<i class="${overlayIcon}"></i>
</div>
`;
}
const forYouBadge = video.personalized
? `<span class="sidebar-personalized-pill" title="Personalized recommendation based on your interests"><i class="fa-solid fa-wand-magic-sparkles"></i> For You</span>`
: '';
return `
<div class="sidebar-video-card" data-id="${video.id}" data-slug="${escapeHtml(video.slug || '')}" data-personalized="${video.personalized ? 'true' : 'false'}">
<a href="/${videoKey}" class="sidebar-video-link" data-mode="${rClass}" data-inherit-context="false">
<div class="sidebar-video-thumb-wrap">
${thumbContentHtml}
<div class="sidebar-video-play-overlay"><i class="${overlayIcon}"></i></div>
${formatBadge}
<span class="sidebar-video-badge rating-${rClass}">${rClass.toUpperCase()}</span>
</div>
<div class="sidebar-video-details">
<div class="sidebar-video-info">
<div class="sidebar-video-title" title="${escapeHtml(displayTitle)}">${escapeHtml(displayTitle)}</div>
<div class="sidebar-video-channel">
<span class="sidebar-video-user" ${userColorStyle}>${escapeHtml(authorName)}</span>
</div>
<div class="sidebar-video-meta">
${timeStr ? `<span class="sidebar-video-time" title="${escapeHtml(fullDate)}">${escapeHtml(timeStr)}</span>` : ''}
${(timeStr && xdBadge) ? `&bull;` : ''}
${xdBadge}
${((timeStr || xdBadge) && forYouBadge) ? `&bull;` : ''}
${forYouBadge}
</div>
</div>
</div>
</a>
</div>
`;
};
const getSessionAffinityParams = () => {
try {
const aff = window.f0ckInterestEngine ? window.f0ckInterestEngine.getTopAffinities() : null;
if (!aff) return '';
let q = '';
if (aff.topTags && aff.topTags.length > 0) {
q += `&session_tags=${encodeURIComponent(aff.topTags.join(','))}`;
}
if (aff.topCreators && aff.topCreators.length > 0) {
q += `&session_creators=${encodeURIComponent(aff.topCreators.join(','))}`;
}
return q;
} catch (_) {
return '';
}
};
const loadRecommendations = async (silent = false) => {
const container = document.getElementById('sidebar-recommendations-container');
if (!container || recommendationsLoading) return;
recommendationsLoading = true;
if (!silent && !recommendationsLoaded) {
container.innerHTML = `
<div class="sidebar-loading-state">
<i class="fa-solid fa-circle-notch fa-spin"></i>
<span>${window.f0ckI18n?.sidebar_loading_recommendations || 'Loading recommendations...'}</span>
</div>
`;
}
try {
seenRecommendationIds.clear();
const mode = typeof window.activeMode !== 'undefined' ? window.activeMode : '';
const affParams = getSessionAffinityParams();
const res = await fetch(`/api/v2/recommendations?limit=${RECOMMENDATIONS_LIMIT}&mode=${mode}${affParams}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
const items = data.items || data.videos || [];
if (data.success && items.length > 0) {
currentRecommendations = items;
recommendationsLoaded = true;
let html = '';
items.forEach(v => {
seenRecommendationIds.add(v.id);
html += renderVideoCard(v);
});
container.innerHTML = html;
if (silent) {
container.scrollTop = 0;
}
attachRecommendationSentinel();
bindRecommendationEvents();
} else {
container.innerHTML = `
<div style="text-align:center;padding:20px;color:#888;">
${window.f0ckI18n?.sidebar_no_recommendations || 'No recommendations found.'}
</div>
`;
}
} catch (e) {
console.error("Sidebar Recommendations: Failed to load", e);
if (!recommendationsLoaded) {
container.innerHTML = `<div style="text-align:center;padding:20px;color:#888;">${window.f0ckI18n?.sidebar_failed_to_load || 'Failed to load.'}</div>`;
}
} finally {
recommendationsLoading = false;
}
};
const loadMoreRecommendations = async () => {
const container = document.getElementById('sidebar-recommendations-container');
if (!container || recommendationsLoading || recommendationsLoadingMore || !recommendationsLoaded) return;
recommendationsLoadingMore = true;
let indicator = document.getElementById('sidebar-recommendations-load-more');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'sidebar-recommendations-load-more';
indicator.style.cssText = 'text-align:center;padding:12px 0;font-size:0.85em;color:#888;';
indicator.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i>';
}
container.appendChild(indicator);
try {
const mode = typeof window.activeMode !== 'undefined' ? window.activeMode : '';
const excludeArr = Array.from(seenRecommendationIds).slice(-100);
const affParams = getSessionAffinityParams();
const res = await fetch(`/api/v2/recommendations?limit=15&mode=${mode}&exclude_ids=${excludeArr.join(',')}${affParams}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
const items = data.items || data.videos || [];
if (indicator && indicator.parentNode) {
indicator.remove();
}
if (data.success && items.length > 0) {
const freshItems = items.filter(v => !seenRecommendationIds.has(v.id));
if (freshItems.length > 0) {
const temp = document.createElement('div');
let html = '';
freshItems.forEach(v => {
seenRecommendationIds.add(v.id);
html += renderVideoCard(v);
});
temp.innerHTML = html;
while (temp.firstChild) {
container.appendChild(temp.firstChild);
}
attachRecommendationSentinel();
}
}
} catch (e) {
console.error("Sidebar Recommendations: Failed to load more", e);
if (indicator && indicator.parentNode) {
indicator.remove();
}
} finally {
recommendationsLoadingMore = false;
}
};
const attachRecommendationSentinel = () => {
const container = document.getElementById('sidebar-recommendations-container');
if (!container) return;
if (!recIoSentinel) {
recIoSentinel = document.createElement('div');
recIoSentinel.id = 'sidebar-recommendations-io-sentinel';
recIoSentinel.style.height = '1px';
}
container.appendChild(recIoSentinel);
if (typeof IntersectionObserver !== 'undefined') {
if (!recObserver) {
recObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && recommendationsLoaded && !recommendationsLoading && !recommendationsLoadingMore) {
loadMoreRecommendations();
}
}, { root: container, rootMargin: '0px 0px 250px 0px', threshold: 0 });
}
recObserver.disconnect();
recObserver.observe(recIoSentinel);
}
};
const bindRecommendationScrollListener = () => {
const container = document.getElementById('sidebar-recommendations-container');
if (!container) return;
// Fallback for environments without IntersectionObserver
if (typeof IntersectionObserver === 'undefined') {
container.addEventListener('scroll', () => {
if (recommendationsLoading || recommendationsLoadingMore || !recommendationsLoaded) return;
const nearBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 150;
if (nearBottom) loadMoreRecommendations();
}, { passive: true });
}
};
const loadVideoRecommendations = loadRecommendations;
const switchSidebarTab = (tabName) => {
const normalizedTab = (tabName === 'videos' || tabName === 'recommendations') ? 'recommendations' : tabName;
const tabs = document.querySelectorAll('.sidebar-tab');
const contents = document.querySelectorAll('.sidebar-tab-content');
tabs.forEach(t => {
const tTab = (t.dataset.tab === 'videos' || t.dataset.tab === 'recommendations') ? 'recommendations' : t.dataset.tab;
if (tTab === normalizedTab) {
t.classList.add('active');
} else {
t.classList.remove('active');
}
});
contents.forEach(c => {
const cTab = (c.dataset.tabContent === 'videos' || c.dataset.tabContent === 'recommendations') ? 'recommendations' : c.dataset.tabContent;
if (cTab === normalizedTab) {
c.classList.add('active');
c.style.display = 'block';
} else {
c.classList.remove('active');
c.style.display = 'none';
}
});
try {
localStorage.setItem('sidebar_active_tab', normalizedTab);
} catch (_) {}
if (normalizedTab === 'recommendations') {
if (!recommendationsLoaded) {
loadRecommendations();
}
}
};
const initSidebarTabs = () => {
const tabsContainer = document.querySelector('.sidebar-tabs');
if (tabsContainer) {
tabsContainer.addEventListener('click', (e) => {
const btn = e.target.closest('.sidebar-tab');
if (btn && btn.dataset.tab) {
e.preventDefault();
switchSidebarTab(btn.dataset.tab);
}
});
}
// Restore persisted active tab if any
let savedTab = 'comments';
try {
savedTab = localStorage.getItem('sidebar_active_tab') || 'comments';
} catch (_) {}
if (savedTab === 'videos' || savedTab === 'recommendations') {
switchSidebarTab('recommendations');
}
};
const replaceCardWithNewRandom = async (card) => {
if (!card || card.dataset.swapping === 'true') return;
card.dataset.swapping = 'true';
card.classList.add('sidebar-card-swapping');
const clickedId = parseInt(card.dataset.id, 10);
try {
const mode = typeof window.activeMode !== 'undefined' ? window.activeMode : '';
const container = document.getElementById('sidebar-recommendations-container');
const visibleIds = [];
if (container) {
container.querySelectorAll('.sidebar-video-card').forEach(c => {
const cid = parseInt(c.dataset.id, 10);
if (cid) visibleIds.push(cid);
});
}
const excludeSet = new Set([...seenRecommendationIds, ...visibleIds]);
if (clickedId) {
excludeSet.add(clickedId);
if (window.f0ckInterestEngine) {
window.f0ckInterestEngine.recordSuggestionClick(clickedId);
}
}
const excludeArr = Array.from(excludeSet).slice(-120);
const affParams = getSessionAffinityParams();
const res = await fetch(`/api/v2/recommendations?limit=1&mode=${mode}&exclude_ids=${excludeArr.join(',')}${affParams}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
const items = data.items || data.videos || [];
if (data.success && items.length > 0) {
const newItem = items[0];
seenRecommendationIds.add(newItem.id);
const idx = currentRecommendations.findIndex(v => v.id === clickedId);
if (idx !== -1) {
currentRecommendations[idx] = newItem;
} else {
currentRecommendations.push(newItem);
}
const temp = document.createElement('div');
temp.innerHTML = renderVideoCard(newItem).trim();
const newCard = temp.firstElementChild;
if (newCard && card.parentNode) {
newCard.classList.add('sidebar-card-entering');
card.replaceWith(newCard);
setTimeout(() => {
newCard.classList.remove('sidebar-card-entering');
}, 2200);
}
} else {
card.classList.remove('sidebar-card-swapping');
delete card.dataset.swapping;
}
} catch (e) {
console.error("Sidebar Recommendations: Failed to swap clicked card", e);
if (card) {
card.classList.remove('sidebar-card-swapping');
delete card.dataset.swapping;
}
}
};
const bindRecommendationEvents = () => {
const container = document.getElementById('sidebar-recommendations-container');
if (!container || container.dataset.eventsBound === 'true') return;
container.dataset.eventsBound = 'true';
const handleCardAction = (e) => {
// Only respond to primary click (0) or middle-click (1). Ignore right click (2).
if (e.button !== 0 && e.button !== 1) return;
const card = e.target.closest('.sidebar-video-card');
if (card) {
replaceCardWithNewRandom(card);
}
};
container.addEventListener('click', handleCardAction);
container.addEventListener('auxclick', handleCardAction);
};
// Reload recommendations when user triggers Random (#random, #nav-random, or 'r' key)
const handleRandomAction = () => {
recommendationsLoaded = false;
let savedTab = 'comments';
try { savedTab = localStorage.getItem('sidebar_active_tab'); } catch (_) {}
if (savedTab === 'videos' || savedTab === 'recommendations') {
loadRecommendations(false);
}
};
document.addEventListener('f0ck:randomTriggered', handleRandomAction);
document.addEventListener('click', (e) => {
const btn = e.target.closest('#random, #nav-random, a[href="/random"], a[href$="/random"]');
if (btn) {
handleRandomAction();
}
}, true);
// Handle explicit mode changes (e.g. from item page where full transition doesn't occur)
document.addEventListener('f0ck:modeChanged', (e) => {
window.f0ckDebug("Sidebar Activity: Mode change detected", e.detail.mode);
@@ -1066,6 +1523,13 @@
currentPage = 1;
hasMore = true;
loadActivity(false);
recommendationsLoaded = false;
let savedTab = 'comments';
try { savedTab = localStorage.getItem('sidebar_active_tab'); } catch (_) {}
if (savedTab === 'videos' || savedTab === 'recommendations') {
loadRecommendations(false);
}
});
// When the current user posts a comment, silently refresh sidebar to show it
@@ -1108,6 +1572,9 @@
// Initial load
const _origInit = init;
const initWithScroll = async () => {
initSidebarTabs();
bindRecommendationEvents();
bindRecommendationScrollListener();
await _origInit();
bindScrollListener();
};