glm hope you did a good job fixing recommendations

This commit is contained in:
uimaxbai 2026-04-13 21:57:25 +01:00
parent de4871ac69
commit 17c382cb93
5 changed files with 261 additions and 21 deletions

View file

@ -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;

View file

@ -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);
}

View file

@ -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);

View file

@ -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',

View file

@ -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();
};