glm hope you did a good job fixing recommendations
This commit is contained in:
parent
de4871ac69
commit
17c382cb93
5 changed files with 261 additions and 21 deletions
|
|
@ -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;
|
||||
|
|
|
|||
67
js/events.js
67
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);
|
||||
}
|
||||
|
|
|
|||
150
js/player.js
150
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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
||||
|
|
|
|||
29
js/ui.js
29
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();
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue