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(
|
initializeTrackInteractions(
|
||||||
Player.instance,
|
Player.instance,
|
||||||
MusicAPI.instance,
|
MusicAPI.instance,
|
||||||
|
|
@ -1087,6 +1087,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Player.instance.setQueue(sortedTracks, 0);
|
Player.instance.setQueue(sortedTracks, 0);
|
||||||
|
Player.instance.enableAutoplay();
|
||||||
const shuffleBtn = document.getElementById('shuffle-btn');
|
const shuffleBtn = document.getElementById('shuffle-btn');
|
||||||
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
||||||
Player.instance.shuffleActive = false;
|
Player.instance.shuffleActive = false;
|
||||||
|
|
@ -1118,6 +1119,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (tracks && tracks.length > 0) {
|
if (tracks && tracks.length > 0) {
|
||||||
const shuffledTracks = [...tracks].sort(() => Math.random() - 0.5);
|
const shuffledTracks = [...tracks].sort(() => Math.random() - 0.5);
|
||||||
Player.instance.setQueue(shuffledTracks, 0);
|
Player.instance.setQueue(shuffledTracks, 0);
|
||||||
|
Player.instance.enableAutoplay();
|
||||||
const shuffleBtn = document.getElementById('shuffle-btn');
|
const shuffleBtn = document.getElementById('shuffle-btn');
|
||||||
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
||||||
Player.instance.shuffleActive = false;
|
Player.instance.shuffleActive = false;
|
||||||
|
|
@ -1186,6 +1188,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
const shuffledTracks = [...allTracks].sort(() => Math.random() - 0.5);
|
const shuffledTracks = [...allTracks].sort(() => Math.random() - 0.5);
|
||||||
Player.instance.setQueue(shuffledTracks, 0);
|
Player.instance.setQueue(shuffledTracks, 0);
|
||||||
|
Player.instance.enableAutoplay();
|
||||||
const shuffleBtn = document.getElementById('shuffle-btn');
|
const shuffleBtn = document.getElementById('shuffle-btn');
|
||||||
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
||||||
Player.instance.shuffleActive = false;
|
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) {
|
if (homeStartRadioBtn) {
|
||||||
homeStartRadioBtn.addEventListener('click', async () => {
|
homeStartRadioBtn.addEventListener('click', async () => {
|
||||||
await player.enableRadio();
|
await player.enableRadio();
|
||||||
|
|
@ -384,9 +384,13 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
||||||
|
|
||||||
const sleepTimerBtnMobile = document.getElementById('sleep-timer-btn');
|
const sleepTimerBtnMobile = document.getElementById('sleep-timer-btn');
|
||||||
|
|
||||||
// History tracking
|
|
||||||
let historyLoggedTrackId = null;
|
let historyLoggedTrackId = null;
|
||||||
|
|
||||||
|
const { listeningTracker } = await import('./listening-tracker.js');
|
||||||
|
|
||||||
|
let _previousTrackId = null;
|
||||||
|
let _trackPlayStartTime = null;
|
||||||
|
|
||||||
const setupMediaListeners = (element) => {
|
const setupMediaListeners = (element) => {
|
||||||
element.addEventListener('loadstart', () => {
|
element.addEventListener('loadstart', () => {
|
||||||
if (player.activeElement === element) {
|
if (player.activeElement === element) {
|
||||||
|
|
@ -397,14 +401,32 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
||||||
element.addEventListener('play', async () => {
|
element.addEventListener('play', async () => {
|
||||||
if (player.activeElement !== element) return;
|
if (player.activeElement !== element) return;
|
||||||
|
|
||||||
// Initialize audio context manager for EQ (only once)
|
|
||||||
if (!audioContextManager.isReady()) {
|
if (!audioContextManager.isReady()) {
|
||||||
audioContextManager.init(element);
|
audioContextManager.init(element);
|
||||||
}
|
}
|
||||||
await audioContextManager.resume();
|
await audioContextManager.resume();
|
||||||
|
|
||||||
if (player.currentTrack) {
|
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()) {
|
if (scrobbler.isAuthenticated()) {
|
||||||
scrobbler.updateNowPlaying(player.currentTrack);
|
scrobbler.updateNowPlaying(player.currentTrack);
|
||||||
}
|
}
|
||||||
|
|
@ -433,6 +455,15 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
||||||
|
|
||||||
element.addEventListener('ended', () => {
|
element.addEventListener('ended', () => {
|
||||||
if (player.activeElement !== element) return;
|
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();
|
player.playNext();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -446,7 +477,8 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
||||||
progressFill.style.width = `${(currentTime / duration) * 100}%`;
|
progressFill.style.width = `${(currentTime / duration) * 100}%`;
|
||||||
currentTimeEl.textContent = formatTime(currentTime);
|
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) {
|
if (currentTime >= 10 && player.currentTrack && player.currentTrack.id !== historyLoggedTrackId) {
|
||||||
historyLoggedTrackId = player.currentTrack.id;
|
historyLoggedTrackId = player.currentTrack.id;
|
||||||
const historyEntry = await db.addToHistory(player.currentTrack);
|
const historyEntry = await db.addToHistory(player.currentTrack);
|
||||||
|
|
@ -2145,10 +2177,25 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
||||||
player.playVideo(clickedTrack);
|
player.playVideo(clickedTrack);
|
||||||
} else {
|
} else {
|
||||||
player.setQueue([clickedTrack], 0);
|
player.setQueue([clickedTrack], 0);
|
||||||
|
player.enableAutoplay();
|
||||||
document.getElementById('shuffle-btn').classList.remove('active');
|
document.getElementById('shuffle-btn').classList.remove('active');
|
||||||
player.playTrackFromQueue();
|
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) {
|
if (recs && recs.length > 0) {
|
||||||
player.addToQueue(recs);
|
player.addToQueue(recs);
|
||||||
}
|
}
|
||||||
|
|
@ -2164,13 +2211,8 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
||||||
const startIndex = trackList.findIndex((t) => t.id == clickedTrackId);
|
const startIndex = trackList.findIndex((t) => t.id == clickedTrackId);
|
||||||
|
|
||||||
player.setQueue(trackList, startIndex);
|
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) {
|
if (ui.currentPage === 'artist' && ui.currentArtistId) {
|
||||||
player.setArtistPopularTracksContext(ui.currentArtistId, trackList, trackList.length, true);
|
player.setArtistPopularTracksContext(ui.currentArtistId, trackList, trackList.length, true);
|
||||||
}
|
}
|
||||||
|
|
@ -2220,6 +2262,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
||||||
if (trackList.length === 0) return;
|
if (trackList.length === 0) return;
|
||||||
const startIndex = trackList.findIndex((t) => t.id == clickedTrackId);
|
const startIndex = trackList.findIndex((t) => t.id == clickedTrackId);
|
||||||
player.setQueue(trackList, startIndex);
|
player.setQueue(trackList, startIndex);
|
||||||
|
player.enableAutoplay();
|
||||||
if (ui.currentPage === 'artist' && ui.currentArtistId) {
|
if (ui.currentPage === 'artist' && ui.currentArtistId) {
|
||||||
player.setArtistPopularTracksContext(ui.currentArtistId, trackList, trackList.length, true);
|
player.setArtistPopularTracksContext(ui.currentArtistId, trackList, trackList.length, true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
150
js/player.js
150
js/player.js
|
|
@ -16,6 +16,7 @@ import {
|
||||||
exponentialVolumeSettings,
|
exponentialVolumeSettings,
|
||||||
audioEffectsSettings,
|
audioEffectsSettings,
|
||||||
radioSettings,
|
radioSettings,
|
||||||
|
autoplaySettings,
|
||||||
binauralDspSettings,
|
binauralDspSettings,
|
||||||
} from './storage.js';
|
} from './storage.js';
|
||||||
import { audioContextManager } from './audio-context.js';
|
import { audioContextManager } from './audio-context.js';
|
||||||
|
|
@ -162,10 +163,23 @@ export class Player {
|
||||||
this.isFetchingRadio = false;
|
this.isFetchingRadio = false;
|
||||||
this.radioFetchPromise = null;
|
this.radioFetchPromise = null;
|
||||||
|
|
||||||
|
this.autoplayEnabled = autoplaySettings.isEnabled();
|
||||||
|
this.autoplaySeeds = [];
|
||||||
|
this.isFetchingAutoplay = false;
|
||||||
|
this.autoplayFetchPromise = null;
|
||||||
|
this._recentlyPlayedIds = [];
|
||||||
|
this._maxRecentlyPlayed = 100;
|
||||||
|
|
||||||
this.playbackSequence = 0;
|
this.playbackSequence = 0;
|
||||||
|
|
||||||
window.addEventListener('beforeunload', async () => {
|
window.addEventListener('beforeunload', async () => {
|
||||||
await this.saveQueueState();
|
await this.saveQueueState();
|
||||||
|
import('./listening-tracker.js')
|
||||||
|
.then(({ listeningTracker }) => {
|
||||||
|
listeningTracker.onTrackEnd();
|
||||||
|
listeningTracker.forceFlush();
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle visibility change - AudioContext can be suspended when backgrounded
|
// Handle visibility change - AudioContext can be suspended when backgrounded
|
||||||
|
|
@ -898,7 +912,7 @@ export class Player {
|
||||||
await this.saveQueueState();
|
await this.saveQueueState();
|
||||||
|
|
||||||
this.currentTrack = track;
|
this.currentTrack = track;
|
||||||
|
this.addToRecentlyPlayed(track.id);
|
||||||
const trackTitle = getTrackTitle(track);
|
const trackTitle = getTrackTitle(track);
|
||||||
const artistName = getTrackArtists(track);
|
const artistName = getTrackArtists(track);
|
||||||
const trackArtistsHTML = getTrackArtistsHTML(track);
|
const trackArtistsHTML = getTrackArtistsHTML(track);
|
||||||
|
|
@ -1336,6 +1350,15 @@ export class Player {
|
||||||
});
|
});
|
||||||
return;
|
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) {
|
if (this.artistPopularTracksState.artistId && this.artistPopularTracksState.hasMore) {
|
||||||
await this.fetchMoreArtistPopularTracks().then(async (newTracks) => {
|
await this.fetchMoreArtistPopularTracks().then(async (newTracks) => {
|
||||||
if (newTracks && newTracks.length > 0) {
|
if (newTracks && newTracks.length > 0) {
|
||||||
|
|
@ -1376,12 +1399,19 @@ export class Player {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
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) {
|
} else if (this.artistPopularTracksState.artistId && this.artistPopularTracksState.hasMore) {
|
||||||
await this.fetchMoreArtistPopularTracks().then(async (newTracks) => {
|
await this.fetchMoreArtistPopularTracks().then(async (newTracks) => {
|
||||||
if (newTracks && newTracks.length > 0) {
|
if (newTracks && newTracks.length > 0) {
|
||||||
await this.addToQueue(newTracks);
|
await this.addToQueue(newTracks);
|
||||||
}
|
}
|
||||||
// Now play the next track (which is now at currentQueueIndex + 1 if tracks were added)
|
|
||||||
this.currentQueueIndex++;
|
this.currentQueueIndex++;
|
||||||
await this.playTrackFromQueue(0, recursiveCount);
|
await this.playTrackFromQueue(0, recursiveCount);
|
||||||
});
|
});
|
||||||
|
|
@ -1467,12 +1497,20 @@ export class Player {
|
||||||
...favorites.map((t) => t.id),
|
...favorites.map((t) => t.id),
|
||||||
...userPlaylists.flatMap((p) => (p.tracks || []).map((t) => t.id)),
|
...userPlaylists.flatMap((p) => (p.tracks || []).map((t) => t.id)),
|
||||||
...history.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,
|
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) {
|
if (recommendations && recommendations.length > 0) {
|
||||||
const currentQueueIds = new Set(this.getCurrentQueue().map((t) => t.id));
|
const currentQueueIds = new Set(this.getCurrentQueue().map((t) => t.id));
|
||||||
|
|
||||||
|
|
@ -1498,6 +1536,14 @@ export class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
async pickRadioSeeds() {
|
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 {
|
try {
|
||||||
const [history, favorites, userPlaylists] = await Promise.all([
|
const [history, favorites, userPlaylists] = await Promise.all([
|
||||||
db.getHistory(),
|
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) {
|
playPrev(recursiveCount = 0) {
|
||||||
const el = this.activeElement;
|
const el = this.activeElement;
|
||||||
if (el.currentTime > 3) {
|
if (el.currentTime > 3) {
|
||||||
|
|
@ -1560,7 +1697,6 @@ export class Player {
|
||||||
this.updateMediaSessionPositionState();
|
this.updateMediaSessionPositionState();
|
||||||
} else if (this.currentQueueIndex > 0) {
|
} else if (this.currentQueueIndex > 0) {
|
||||||
this.currentQueueIndex--;
|
this.currentQueueIndex--;
|
||||||
// Skip unavailable and blocked tracks
|
|
||||||
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
|
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
|
||||||
|
|
||||||
if (recursiveCount > currentQueue.length) {
|
if (recursiveCount > currentQueue.length) {
|
||||||
|
|
@ -1575,6 +1711,12 @@ export class Player {
|
||||||
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
|
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
|
||||||
return this.playPrev(recursiveCount + 1);
|
return this.playPrev(recursiveCount + 1);
|
||||||
}
|
}
|
||||||
|
import('./listening-tracker.js')
|
||||||
|
.then(({ listeningTracker }) => {
|
||||||
|
listeningTracker.onSkip();
|
||||||
|
listeningTracker.forceFlush();
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
await this.playTrackFromQueue(0, recursiveCount);
|
await this.playTrackFromQueue(0, recursiveCount);
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.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 = {
|
export const analyticsSettings = {
|
||||||
ENABLED_KEY: 'analytics-enabled',
|
ENABLED_KEY: 'analytics-enabled',
|
||||||
|
|
||||||
|
|
|
||||||
29
js/ui.js
29
js/ui.js
|
|
@ -2999,13 +2999,22 @@ export class UIRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSeeds() {
|
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 history = await db.getHistory();
|
||||||
const favorites = await db.getFavorites('track');
|
const favorites = await db.getFavorites('track');
|
||||||
const playlists = await db.getPlaylists(true);
|
const playlists = await db.getPlaylists(true);
|
||||||
const playlistTracks = playlists.flatMap((p) => p.tracks || []);
|
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 shuffle = (arr) => [...arr].sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
const combined = [
|
const combined = [
|
||||||
|
|
@ -3039,7 +3048,7 @@ export class UIRenderer {
|
||||||
if (forceRefresh || songsContainer.children.length === 0) {
|
if (forceRefresh || songsContainer.children.length === 0) {
|
||||||
songsContainer.innerHTML = this.createSkeletonTracks(10, true);
|
songsContainer.innerHTML = this.createSkeletonTracks(10, true);
|
||||||
} else if (!songsContainer.querySelector('.skeleton')) {
|
} else if (!songsContainer.querySelector('.skeleton')) {
|
||||||
return; // Already loaded
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -3056,11 +3065,22 @@ export class UIRenderer {
|
||||||
...history.map((t) => t.id),
|
...history.map((t) => t.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const recommendedTracks = await this.api.getRecommendedTracksForPlaylist(seeds, 20, {
|
let recommendedTracks = await this.api.getRecommendedTracksForPlaylist(seeds, 20, {
|
||||||
skipCache: forceRefresh,
|
skipCache: forceRefresh,
|
||||||
knownTrackIds: knownTrackIds,
|
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');
|
const filteredTracks = await this.filterUserContent(recommendedTracks, 'track');
|
||||||
this.lastRecommendedTracks = filteredTracks;
|
this.lastRecommendedTracks = filteredTracks;
|
||||||
|
|
||||||
|
|
@ -6230,6 +6250,7 @@ export class UIRenderer {
|
||||||
|
|
||||||
playBtn.onclick = () => {
|
playBtn.onclick = () => {
|
||||||
this.player.setQueue([track], 0);
|
this.player.setQueue([track], 0);
|
||||||
|
this.player.enableAutoplay();
|
||||||
this.player.playTrackFromQueue();
|
this.player.playTrackFromQueue();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue