// player state management // fixed resolution for cover art, favicon, and media session // why? the media session cover art should be a pretty decent resolution, and if there were a bunch of separate sizes then we are just downloading more images for no good reason const FIXED_ART_SIZE = 512; // update favicon function updateFavicon(artId) { const link = document.querySelector('link[rel="icon"]'); if (!link) return; if (state.settings.dynamicFavicon && artId) { const url = state.api.getCoverArtUrl(artId, FIXED_ART_SIZE); link.href = url; link.type = "image/jpeg"; } else { link.href = ICONS.TINYSUB; link.type = "image/svg+xml"; } } // load and play a song function playTrack(song, autoplay = true) { if (!song) { clearPlayerUI(); return; } ui.trackTitle.textContent = song.title || STRINGS.UNKNOWN_TRACK; ui.trackArtist.textContent = song.artist || STRINGS.UNKNOWN_ARTIST; document.title = `${song.artist || STRINGS.UNKNOWN_ARTIST} - ${song.title || STRINGS.UNKNOWN_TRACK} - tinysub`; // Prefer album art (albumId) over individual song art (coverArt) const artId = song.albumId || song.coverArt; updateFavicon(artId); if (artId) { ui.coverArt.src = state.api.getCoverArtUrl(artId, FIXED_ART_SIZE); } else { ui.coverArt.src = ""; } ui.player.src = state.api.getStreamUrl(song.id); if (autoplay) { ui.player.play(); } loadLyricsForSong(song); highlightCurrentTrack(); updateMediaSession(song); if (navigator.mediaSession) { navigator.mediaSession.playbackState = autoplay ? "playing" : "paused"; } } // update play button icon based on playback state const updatePlayIcon = () => { const isPaused = ui.player.paused; ui.playBtn.innerHTML = `${isPaused ? `; if (ui.player.src) { if (navigator.mediaSession) { navigator.mediaSession.playbackState = isPaused ? "paused" : "playing"; } } }; // skip tracks by offset (can use negative too) function skipTrack(offset) { let newIndex = state.queueIndex + offset; // loop back to start, if at end, and moving forward with loop enabled if (offset > 0 && newIndex >= state.queue.length && state.loop) { newIndex = 0; } if (newIndex >= 0 && newIndex < state.queue.length) { state.queueIndex = newIndex; saveQueue(); playTrack(state.queue[state.queueIndex]); } else if (offset > 0) { state.queueIndex = -1; saveQueue(); clearPlayerUI(); } } // toggle playback or start queue if nothing playing const togglePlayback = () => { if (state.queueIndex >= 0 && state.queueIndex < state.queue.length) { if (ui.player.src) { ui.player.paused ? ui.player.play() : ui.player.pause(); } else { playTrack(state.queue[state.queueIndex]); } } else if (state.queue.length > 0) { state.queueIndex = 0; playTrack(state.queue[0]); updateQueueDisplay(); saveQueue(); } }; // toggle favorite status of a song const setFavoriteSong = async (song, force) => { if (!song || !state.api) return; const isFavorited = state.favorites.has(song.id); const shouldAdd = force === undefined ? !isFavorited : force; if (shouldAdd === isFavorited) return; if (shouldAdd) { await state.api.star(song.id); state.favorites.add(song.id); } else { await state.api.unstar(song.id); state.favorites.delete(song.id); } }; // set song rating (0-5) const setSongRating = async (song, rating) => { if (!song || !state.api) return; if (rating < 0 || rating > 5) return; await state.api.setRating(song.id, rating); if (rating === 0) { state.ratings.delete(song.id); } else { state.ratings.set(song.id, rating); } }; // highlight currently playing song in queue function highlightCurrentTrack() { clearRowClasses(ui.queueList, ["currently-playing"]); if (state.queueIndex >= 0) { const row = ui.queueList.querySelector( `tr[data-index="${state.queueIndex}"]`, ); if (row) row.classList.add(CLASSES.CURRENTLY_PLAYING); } } // clear player UI and reset to default state function clearPlayerUI() { ui.player.pause(); ui.player.src = ""; ui.trackTitle.textContent = STRINGS.NO_TRACK_PLAYING; ui.trackArtist.textContent = ""; document.title = "tinysub"; updateFavicon(); currentLyrics = []; lyricIndex = 0; ui.trackLyric.textContent = ""; ui.coverArt.src = ICONS.TINYSUB; ui.coverArt.srcset = ""; ui.progress.value = 0; ui.timeDisplay.textContent = "0:00 / 0:00"; updatePlayIcon(); highlightCurrentTrack(); if (navigator.mediaSession) { navigator.mediaSession.metadata = null; navigator.mediaSession.playbackState = "none"; } } // handler for when song ends function handleTrackEnd() { if ( state.queueIndex >= 0 && state.queueIndex < state.queue.length && state.settings.scrobbling ) { state.api.scrobble(state.queue[state.queueIndex].id).catch(() => {}); } state.queueIndex++; if (state.queueIndex < state.queue.length) { saveQueue(); playTrack(state.queue[state.queueIndex]); } else if (state.loop) { state.queueIndex = 0; saveQueue(); playTrack(state.queue[0]); } else { state.queueIndex = -1; saveQueue(); clearPlayerUI(); } } // update media session metadata with song const updateMediaSession = (song) => { if (!song) return; // prefer album art to save bandwidth const artId = song.albumId || song.coverArt; if (navigator.mediaSession) { navigator.mediaSession.metadata = new MediaMetadata({ title: song.title || STRINGS.UNKNOWN_TRACK, artist: song.artist || STRINGS.UNKNOWN_ARTIST, album: song.album || "", artwork: artId ? [ { src: state.api.getCoverArtUrl(artId, FIXED_ART_SIZE), sizes: "512x512", type: "image/jpeg", }, ] : [], }); } };