From 17c382cb93b371dceedce490d2597fc8875b4743 Mon Sep 17 00:00:00 2001 From: uimaxbai <61615730+uimaxbai@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:57:25 +0100 Subject: [PATCH] glm hope you did a good job fixing recommendations --- js/app.js | 5 +- js/events.js | 67 ++++++++++++++++++---- js/player.js | 150 ++++++++++++++++++++++++++++++++++++++++++++++++-- js/storage.js | 31 +++++++++++ js/ui.js | 29 ++++++++-- 5 files changed, 261 insertions(+), 21 deletions(-) diff --git a/js/app.js b/js/app.js index c60adaf..233ce60 100644 --- a/js/app.js +++ b/js/app.js @@ -660,7 +660,7 @@ document.addEventListener('DOMContentLoaded', async () => { }); }); - initializePlayerEvents(Player.instance, audioPlayer, scrobbler, UIRenderer.instance); + await initializePlayerEvents(Player.instance, audioPlayer, scrobbler, UIRenderer.instance); initializeTrackInteractions( Player.instance, MusicAPI.instance, @@ -1087,6 +1087,7 @@ document.addEventListener('DOMContentLoaded', async () => { }); Player.instance.setQueue(sortedTracks, 0); + Player.instance.enableAutoplay(); const shuffleBtn = document.getElementById('shuffle-btn'); if (shuffleBtn) shuffleBtn.classList.remove('active'); Player.instance.shuffleActive = false; @@ -1118,6 +1119,7 @@ document.addEventListener('DOMContentLoaded', async () => { if (tracks && tracks.length > 0) { const shuffledTracks = [...tracks].sort(() => Math.random() - 0.5); Player.instance.setQueue(shuffledTracks, 0); + Player.instance.enableAutoplay(); const shuffleBtn = document.getElementById('shuffle-btn'); if (shuffleBtn) shuffleBtn.classList.remove('active'); Player.instance.shuffleActive = false; @@ -1186,6 +1188,7 @@ document.addEventListener('DOMContentLoaded', async () => { const shuffledTracks = [...allTracks].sort(() => Math.random() - 0.5); Player.instance.setQueue(shuffledTracks, 0); + Player.instance.enableAutoplay(); const shuffleBtn = document.getElementById('shuffle-btn'); if (shuffleBtn) shuffleBtn.classList.remove('active'); Player.instance.shuffleActive = false; diff --git a/js/events.js b/js/events.js index d173fa3..2aad714 100644 --- a/js/events.js +++ b/js/events.js @@ -375,7 +375,7 @@ async function handleSelectionAction(action) { } } -export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { +export async function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { if (homeStartRadioBtn) { homeStartRadioBtn.addEventListener('click', async () => { await player.enableRadio(); @@ -384,9 +384,13 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { const sleepTimerBtnMobile = document.getElementById('sleep-timer-btn'); - // History tracking let historyLoggedTrackId = null; + const { listeningTracker } = await import('./listening-tracker.js'); + + let _previousTrackId = null; + let _trackPlayStartTime = null; + const setupMediaListeners = (element) => { element.addEventListener('loadstart', () => { if (player.activeElement === element) { @@ -397,14 +401,32 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { element.addEventListener('play', async () => { if (player.activeElement !== element) return; - // Initialize audio context manager for EQ (only once) if (!audioContextManager.isReady()) { audioContextManager.init(element); } await audioContextManager.resume(); if (player.currentTrack) { - // Scrobble + const currentId = player.currentTrack.id; + if (currentId !== _previousTrackId) { + if (_previousTrackId !== null) { + const prevSignal = listeningTracker.getSessionSignals(); + const prevPlayTime = prevSignal.accumulatedPlayTime || 0; + const prevDuration = prevSignal.trackDuration || 0; + listeningTracker.onSkip(); + const prevTrack = + player.getCurrentQueue()[player.currentQueueIndex - 1] || + player.getCurrentQueue().find((t) => t.id === _previousTrackId); + if (prevTrack && prevPlayTime > 0) { + listeningTracker.updateArtistAffinity(prevTrack, prevPlayTime, prevDuration, true); + } + listeningTracker.forceFlush(); + } + _previousTrackId = currentId; + listeningTracker.onTrackStart(player.currentTrack); + _trackPlayStartTime = Date.now(); + } + if (scrobbler.isAuthenticated()) { scrobbler.updateNowPlaying(player.currentTrack); } @@ -433,6 +455,15 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { element.addEventListener('ended', () => { if (player.activeElement !== element) return; + const elapsedPlayTime = listeningTracker.getSessionSignals().accumulatedPlayTime || 0; + const trackDur = listeningTracker.getSessionSignals().trackDuration || 0; + listeningTracker.onTrackEnd(); + if (player.currentTrack) { + const effectivePlayTime = elapsedPlayTime || (Date.now() - _trackPlayStartTime) / 1000; + listeningTracker.updateArtistAffinity(player.currentTrack, effectivePlayTime, trackDur, false); + } + listeningTracker.forceFlush(); + _previousTrackId = null; player.playNext(); }); @@ -446,7 +477,8 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { progressFill.style.width = `${(currentTime / duration) * 100}%`; currentTimeEl.textContent = formatTime(currentTime); - // Log to history after 10 seconds of playback + listeningTracker.onTimeUpdate(currentTime, duration); + if (currentTime >= 10 && player.currentTrack && player.currentTrack.id !== historyLoggedTrackId) { historyLoggedTrackId = player.currentTrack.id; const historyEntry = await db.addToHistory(player.currentTrack); @@ -2145,10 +2177,25 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen player.playVideo(clickedTrack); } else { player.setQueue([clickedTrack], 0); + player.enableAutoplay(); document.getElementById('shuffle-btn').classList.remove('active'); player.playTrackFromQueue(); - api.getTrackRecommendations(clickedTrack.id).then((recs) => { + const { autoplaySettings } = await import('./storage.js'); + const fetchRecs = autoplaySettings.isSmartRecsEnabled() + ? (async () => { + const { smartRecommendations } = await import('./smart-recommendations.js'); + const recs = await api.getTrackRecommendations(clickedTrack.id); + if (recs && recs.length > 0) { + const filtered = smartRecommendations.filterRecommendations(recs); + const ranked = smartRecommendations.rankRecommendations(filtered); + return ranked; + } + return []; + })() + : api.getTrackRecommendations(clickedTrack.id); + + fetchRecs.then((recs) => { if (recs && recs.length > 0) { player.addToQueue(recs); } @@ -2164,13 +2211,8 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen const startIndex = trackList.findIndex((t) => t.id == clickedTrackId); player.setQueue(trackList, startIndex); + player.enableAutoplay(); - // Set artist popular tracks context if on artist page - console.log('[Events] Setting context:', { - page: ui.currentPage, - artistId: ui.currentArtistId, - trackCount: trackList.length, - }); if (ui.currentPage === 'artist' && ui.currentArtistId) { player.setArtistPopularTracksContext(ui.currentArtistId, trackList, trackList.length, true); } @@ -2220,6 +2262,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen if (trackList.length === 0) return; const startIndex = trackList.findIndex((t) => t.id == clickedTrackId); player.setQueue(trackList, startIndex); + player.enableAutoplay(); if (ui.currentPage === 'artist' && ui.currentArtistId) { player.setArtistPopularTracksContext(ui.currentArtistId, trackList, trackList.length, true); } diff --git a/js/player.js b/js/player.js index f24caad..ef09ae2 100644 --- a/js/player.js +++ b/js/player.js @@ -16,6 +16,7 @@ import { exponentialVolumeSettings, audioEffectsSettings, radioSettings, + autoplaySettings, binauralDspSettings, } from './storage.js'; import { audioContextManager } from './audio-context.js'; @@ -162,10 +163,23 @@ export class Player { this.isFetchingRadio = false; this.radioFetchPromise = null; + this.autoplayEnabled = autoplaySettings.isEnabled(); + this.autoplaySeeds = []; + this.isFetchingAutoplay = false; + this.autoplayFetchPromise = null; + this._recentlyPlayedIds = []; + this._maxRecentlyPlayed = 100; + this.playbackSequence = 0; window.addEventListener('beforeunload', async () => { await this.saveQueueState(); + import('./listening-tracker.js') + .then(({ listeningTracker }) => { + listeningTracker.onTrackEnd(); + listeningTracker.forceFlush(); + }) + .catch(() => {}); }); // Handle visibility change - AudioContext can be suspended when backgrounded @@ -898,7 +912,7 @@ export class Player { await this.saveQueueState(); this.currentTrack = track; - + this.addToRecentlyPlayed(track.id); const trackTitle = getTrackTitle(track); const artistName = getTrackArtists(track); const trackArtistsHTML = getTrackArtistsHTML(track); @@ -1336,6 +1350,15 @@ export class Player { }); return; } + if (this.autoplayEnabled && isLastTrack) { + this.fetchAutoplayRecommendations().then(async () => { + const updatedQueue = this.getCurrentQueue(); + if (this.currentQueueIndex < updatedQueue.length - 1) { + await this.playNext(0); + } + }); + return; + } if (this.artistPopularTracksState.artistId && this.artistPopularTracksState.hasMore) { await this.fetchMoreArtistPopularTracks().then(async (newTracks) => { if (newTracks && newTracks.length > 0) { @@ -1376,12 +1399,19 @@ export class Player { } }); return; + } else if (this.autoplayEnabled) { + this.fetchAutoplayRecommendations().then(async () => { + const updatedQueue = this.getCurrentQueue(); + if (this.currentQueueIndex < updatedQueue.length - 1) { + await this.playNext(0); + } + }); + return; } else if (this.artistPopularTracksState.artistId && this.artistPopularTracksState.hasMore) { await this.fetchMoreArtistPopularTracks().then(async (newTracks) => { if (newTracks && newTracks.length > 0) { await this.addToQueue(newTracks); } - // Now play the next track (which is now at currentQueueIndex + 1 if tracks were added) this.currentQueueIndex++; await this.playTrackFromQueue(0, recursiveCount); }); @@ -1467,12 +1497,20 @@ export class Player { ...favorites.map((t) => t.id), ...userPlaylists.flatMap((p) => (p.tracks || []).map((t) => t.id)), ...history.map((t) => t.id), + ...this._recentlyPlayedIds, ]); - const recommendations = await this.api.getRecommendedTracksForPlaylist(seeds, 20, { + let recommendations = await this.api.getRecommendedTracksForPlaylist(seeds, 20, { knownTrackIds: knownTrackIds, }); + const { autoplaySettings: _autoplaySettings } = await import('./storage.js'); + if (_autoplaySettings.isSmartRecsEnabled()) { + const { smartRecommendations } = await import('./smart-recommendations.js'); + recommendations = smartRecommendations.filterRecommendations(recommendations); + recommendations = smartRecommendations.rankRecommendations(recommendations); + } + if (recommendations && recommendations.length > 0) { const currentQueueIds = new Set(this.getCurrentQueue().map((t) => t.id)); @@ -1498,6 +1536,14 @@ export class Player { } async pickRadioSeeds() { + try { + const { smartRecommendations } = await import('./smart-recommendations.js'); + const smartSeeds = await smartRecommendations.getSmartSeeds(50); + if (smartSeeds.length > 0) return smartSeeds; + } catch (e) { + console.warn('Smart seeds failed, falling back to basic seed selection:', e); + } + try { const [history, favorites, userPlaylists] = await Promise.all([ db.getHistory(), @@ -1553,6 +1599,97 @@ export class Player { } } + enableAutoplay() { + this.autoplayEnabled = true; + autoplaySettings.setEnabled(true); + } + + disableAutoplay() { + this.autoplayEnabled = false; + autoplaySettings.setEnabled(false); + } + + addToRecentlyPlayed(trackId) { + if (!trackId) return; + this._recentlyPlayedIds = this._recentlyPlayedIds.filter((id) => id !== trackId); + this._recentlyPlayedIds.push(trackId); + if (this._recentlyPlayedIds.length > this._maxRecentlyPlayed) { + this._recentlyPlayedIds = this._recentlyPlayedIds.slice(-this._maxRecentlyPlayed); + } + } + + fetchAutoplayRecommendations() { + if (this.isFetchingAutoplay) return this.autoplayFetchPromise || Promise.resolve(); + this.isFetchingAutoplay = true; + + this.showRadioLoading(true); + + this.autoplayFetchPromise = (async () => { + try { + const { smartRecommendations } = await import('./smart-recommendations.js'); + const { autoplaySettings: _autoplaySettings } = await import('./storage.js'); + + const currentQueue = this.getCurrentQueue(); + const recentQueueTracks = currentQueue.slice( + Math.max(0, this.currentQueueIndex - 10), + this.currentQueueIndex + 1 + ); + + const seeds = await smartRecommendations.getAdaptiveQueueSeeds( + recentQueueTracks, + this._recentlyPlayedIds, + 5 + ); + + if (seeds.length === 0) { + if (this.currentTrack) seeds.push(this.currentTrack); + else return; + } + + const [favorites, userPlaylists, history] = await Promise.all([ + db.getFavorites('track'), + db.getAll('user_playlists'), + db.getHistory(), + ]); + + const knownTrackIds = new Set([ + ...favorites.map((t) => t.id), + ...userPlaylists.flatMap((p) => (p.tracks || []).map((t) => t.id)), + ...history.map((t) => t.id), + ...this._recentlyPlayedIds, + ...currentQueue.map((t) => t.id), + ]); + + let recommendations = await this.api.getRecommendedTracksForPlaylist(seeds, 20, { + knownTrackIds: knownTrackIds, + }); + + if (_autoplaySettings.isSmartRecsEnabled()) { + recommendations = smartRecommendations.filterRecommendations(recommendations); + recommendations = smartRecommendations.rankRecommendations(recommendations); + } + + if (recommendations && recommendations.length > 0) { + const currentQueueIds = new Set(currentQueue.map((t) => t.id)); + let newTracks = recommendations.filter((t) => !currentQueueIds.has(t.id)); + + if (newTracks.length > 0) { + const tracksToAdd = newTracks.slice(0, 5); + await this.addToQueue(tracksToAdd); + } + } + } catch (error) { + console.error('Failed to fetch autoplay recommendations:', error); + } finally { + this.isFetchingAutoplay = false; + this.autoplayFetchPromise = null; + setTimeout(() => this.showRadioLoading(false), 500); + } + })(); + + return this.autoplayFetchPromise; + } + playPrev(recursiveCount = 0) { const el = this.activeElement; if (el.currentTime > 3) { @@ -1560,7 +1697,6 @@ export class Player { this.updateMediaSessionPositionState(); } else if (this.currentQueueIndex > 0) { this.currentQueueIndex--; - // Skip unavailable and blocked tracks const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; if (recursiveCount > currentQueue.length) { @@ -1575,6 +1711,12 @@ export class Player { if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) { return this.playPrev(recursiveCount + 1); } + import('./listening-tracker.js') + .then(({ listeningTracker }) => { + listeningTracker.onSkip(); + listeningTracker.forceFlush(); + }) + .catch(() => {}); await this.playTrackFromQueue(0, recursiveCount); }) .catch(console.error); diff --git a/js/storage.js b/js/storage.js index 37ad376..185c351 100644 --- a/js/storage.js +++ b/js/storage.js @@ -2371,6 +2371,37 @@ export const radioSettings = { }, }; +export const autoplaySettings = { + ENABLED_KEY: 'autoplay-enabled', + SMART_RECS_KEY: 'smart-recommendations-enabled', + + isEnabled() { + try { + const val = localStorage.getItem(this.ENABLED_KEY); + return val === null ? true : val === 'true'; + } catch { + return true; + } + }, + + setEnabled(enabled) { + localStorage.setItem(this.ENABLED_KEY, enabled ? 'true' : 'false'); + }, + + isSmartRecsEnabled() { + try { + const val = localStorage.getItem(this.SMART_RECS_KEY); + return val === null ? true : val === 'true'; + } catch { + return true; + } + }, + + setSmartRecsEnabled(enabled) { + localStorage.setItem(this.SMART_RECS_KEY, enabled ? 'true' : 'false'); + }, +}; + export const analyticsSettings = { ENABLED_KEY: 'analytics-enabled', diff --git a/js/ui.js b/js/ui.js index d88f6c6..5ad0f22 100644 --- a/js/ui.js +++ b/js/ui.js @@ -2999,13 +2999,22 @@ export class UIRenderer { } async getSeeds() { + try { + const { smartRecommendations } = await import('./smart-recommendations.js'); + const { autoplaySettings } = await import('./storage.js'); + if (autoplaySettings.isSmartRecsEnabled()) { + const smartSeeds = await smartRecommendations.getSmartSeeds(50); + if (smartSeeds.length > 0) return smartSeeds; + } + } catch (e) { + console.warn('Smart seeds failed, using basic seeds:', e); + } + const history = await db.getHistory(); const favorites = await db.getFavorites('track'); const playlists = await db.getPlaylists(true); const playlistTracks = playlists.flatMap((p) => p.tracks || []); - // Prioritize: Playlists > Favorites > History - // Take random samples from each to form seeds const shuffle = (arr) => [...arr].sort(() => Math.random() - 0.5); const combined = [ @@ -3039,7 +3048,7 @@ export class UIRenderer { if (forceRefresh || songsContainer.children.length === 0) { songsContainer.innerHTML = this.createSkeletonTracks(10, true); } else if (!songsContainer.querySelector('.skeleton')) { - return; // Already loaded + return; } try { @@ -3056,11 +3065,22 @@ export class UIRenderer { ...history.map((t) => t.id), ]); - const recommendedTracks = await this.api.getRecommendedTracksForPlaylist(seeds, 20, { + let recommendedTracks = await this.api.getRecommendedTracksForPlaylist(seeds, 20, { skipCache: forceRefresh, knownTrackIds: knownTrackIds, }); + try { + const { smartRecommendations } = await import('./smart-recommendations.js'); + const { autoplaySettings } = await import('./storage.js'); + if (autoplaySettings.isSmartRecsEnabled()) { + recommendedTracks = smartRecommendations.filterRecommendations(recommendedTracks); + recommendedTracks = smartRecommendations.rankRecommendations(recommendedTracks); + } + } catch (e) { + console.warn('Smart filtering failed for home songs:', e); + } + const filteredTracks = await this.filterUserContent(recommendedTracks, 'track'); this.lastRecommendedTracks = filteredTracks; @@ -6230,6 +6250,7 @@ export class UIRenderer { playBtn.onclick = () => { this.player.setQueue([track], 0); + this.player.enableAutoplay(); this.player.playTrackFromQueue(); };