diff --git a/static/app,js b/static/app,js
new file mode 100644
index 0000000..bafabbf
--- /dev/null
+++ b/static/app,js
@@ -0,0 +1,2010 @@
+"use strict";
+
+const $ = id => document.getElementById(id);
+
+const searchForm = $("searchForm");
+const searchInput = $("searchInput");
+const statusEl = $("status");
+const resultsBody = $("resultsBody");
+const resultsSection = $("resultsSection");
+const scrollSentinel = $("scrollSentinel");
+const playerBar = $("playerBar");
+const audioPlayer = $("audioPlayer");
+const prevTrackButton = $("prevTrackButton");
+const nextTrackButton = $("nextTrackButton");
+const autoplayButton = $("autoplayButton");
+const shuffleButton = $("shuffleButton");
+const loopButton = $("loopButton");
+const playerTitle = $("playerTitle");
+const playerMeta = $("playerMeta");
+const backButton = $("backButton");
+const favoritesTab = $("favoritesTab");
+const playlistsTab = $("playlistsTab");
+const playPauseButton = $("playPauseButton");
+const seekBar = $("seekBar");
+const currentTimeLabel = $("currentTime");
+const durationLabel = $("duration");
+const volumeSlider = $("volumeSlider");
+const seekThemeButton = $("seekThemeButton");
+const seekThemeMenu = $("seekThemeMenu");
+const searchHero = $("searchHero");
+const pageRoot = $("pageRoot");
+const queuePanel = $("queuePanel");
+const queueList = $("queueList");
+const queueModeSelect = $("queueModeSelect");
+const queueRefreshButton = $("queueRefreshButton");
+
+const PAGE_SIZE = 20;
+const MAX_AUTO_RESULTS = 100;
+const MAX_AUTO_PAGES = Math.ceil(MAX_AUTO_RESULTS / PAGE_SIZE);
+const COLSPAN = 9;
+
+const ANON_ID_KEY = "minimal_yt_anonymous_id";
+const AUTOPLAY_KEY = "minimal_yt_autoplay_enabled";
+const SHUFFLE_KEY = "minimal_yt_shuffle_enabled";
+const LOOP_KEY = "minimal_yt_loop_mode";
+const VOLUME_KEY = "minimal_yt_volume";
+const SEEK_THEME_KEY_PREFIX = "minimal_yt_seek_theme";
+const DEFAULT_PAGE_TITLE = document.title.trim() || "search";
+
+let queueMode = localStorage.getItem("minimal_yt_queue_mode") === "recommendations"
+ ? "recommendations"
+ : "queue";
+let playlistReorderDragId = null;
+let currentView = "search";
+let currentQuery = "";
+let currentPage = 0;
+let hasMore = false;
+let isLoading = false;
+let searchToken = 0;
+let loadedCount = 0;
+let currentPlaylistId = null;
+let currentPlaylistItems = [];
+let favoriteIds = new Set();
+let playlistQueue = [];
+let playlistQueueIndex = -1;
+let shufflePlayedIndexes = new Set();
+let autoplayEnabled = localStorage.getItem(AUTOPLAY_KEY) !== "false";
+let shuffleEnabled = localStorage.getItem(SHUFFLE_KEY) === "true";
+let loopMode = ["off", "all", "one"].includes(localStorage.getItem(LOOP_KEY))
+ ? localStorage.getItem(LOOP_KEY)
+ : "off";
+let activeTrackTitle = "";
+let playbackRequestToken = 0;
+let searchAbortController = null;
+let preloadPromise = null;
+let currentTrackContext = null;
+let restoringPlayerState = false;
+let playerStateSaveTimer = null;
+let playerStateRestorePromise = null;
+let playTrackBusy = false;
+let playlistItemDragId = null;
+let playlistItemDragPlaylistId = null;
+
+function reorderArrayByIds(items, draggedId, targetId) {
+ const list = [...items];
+ const fromIndex = list.findIndex(item => item.playlist_item_id === draggedId);
+ const toIndex = list.findIndex(item => item.playlist_item_id === targetId);
+
+ if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) {
+ return list;
+ }
+
+ const [moved] = list.splice(fromIndex, 1);
+ list.splice(toIndex, 0, moved);
+ return list;
+}
+
+
+const validSeekThemes = new Map(
+ [...document.querySelectorAll(".seek-theme-option")].map(option => [
+ option.dataset.seekTheme,
+ option.dataset.seekColor,
+ ]),
+);
+
+function getAnonymousId() {
+ let id = localStorage.getItem(ANON_ID_KEY);
+ if (!id) {
+ id = globalThis.crypto?.randomUUID?.() || `anon_${Date.now()}_${Math.random().toString(36).slice(2)}`;
+ localStorage.setItem(ANON_ID_KEY, id);
+ }
+ return id;
+}
+
+function apiFetch(url, options = {}) {
+ return fetch(url, {
+ ...options,
+ credentials: "same-origin",
+ headers: {
+ ...(options.headers || {}),
+ "X-Anonymous-Id": getAnonymousId(),
+ },
+ });
+}
+
+async function apiJson(url, options = {}) {
+ const response = await apiFetch(url, options);
+ if (!response.ok) {
+ let detail = `request failed (${response.status})`;
+ try {
+ const data = await response.json();
+ if (typeof data.detail === "string") detail = data.detail;
+ } catch {}
+ const error = new Error(detail);
+ error.status = response.status;
+ throw error;
+ }
+ return response.json();
+}
+
+
+
+function setStatus(text) {
+ if (statusEl) statusEl.textContent = text || "";
+}
+
+function setSentinel(text) {
+ if (!scrollSentinel) return;
+ scrollSentinel.textContent = text || "";
+ scrollSentinel.classList.toggle("loading", text === "loading");
+}
+
+function escapeHtml(value) {
+ return String(value ?? "")
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function sanitizeStoredTrack(item) {
+ if (!item || typeof item !== "object") return null;
+
+ const id = String(item.id || "").trim();
+ if (!/^[A-Za-z0-9_-]{6,32}$/.test(id)) return null;
+
+ const title = sanitizeTitle(item.title || "untitled", 120);
+ const channel = item.channel != null ? sanitizeTitle(item.channel, 80) : undefined;
+ const duration = Number(item.duration);
+ const duration_string = item.duration_string != null ? sanitizeTitle(item.duration_string, 32) : undefined;
+ const thumbnail = item.thumbnail != null ? String(item.thumbnail).trim() : undefined;
+ const view_count = item.view_count != null && Number.isFinite(Number(item.view_count))
+ ? Math.max(0, Number(item.view_count))
+ : undefined;
+
+ return {
+ id,
+ title,
+ ...(channel ? { channel } : {}),
+ ...(Number.isFinite(duration) && duration >= 0 && duration <= 86400 ? { duration } : {}),
+ ...(duration_string ? { duration_string } : {}),
+ ...(thumbnail ? { thumbnail } : {}),
+ ...(view_count != null ? { view_count } : {}),
+ is_live: Boolean(item.is_live),
+ source: String(item.source || "youtube").slice(0, 32),
+ ...(item.url ? { url: String(item.url) } : {}),
+ };
+}
+
+function sanitizeTitle(value, maxLength = 72) {
+ let title = String(value || "")
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
+ .replace(/[\u202a-\u202e\u2066-\u2069]/g, "")
+ .replace(/\s+/g, " ")
+ .trim();
+ if (!title) return "untitled";
+ if (title.length > maxLength) {
+ title = `${title.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
+ }
+ return title;
+}
+
+function formatViewCount(value) {
+ const count = Number(value);
+ if (!Number.isFinite(count)) return "-";
+ if (count >= 1e9) return `${(count / 1e9).toFixed(count >= 1e10 ? 0 : 1)}b`;
+ if (count >= 1e6) return `${(count / 1e6).toFixed(count >= 1e7 ? 0 : 1)}m`;
+ if (count >= 1e3) return `${(count / 1e3).toFixed(count >= 1e4 ? 0 : 1)}k`;
+ return String(count);
+}
+
+function formatTime(seconds) {
+ if (!Number.isFinite(seconds)) return "0:00";
+ return `${Math.floor(seconds / 60)}:${String(Math.floor(seconds % 60)).padStart(2, "0")}`;
+}
+
+function getItemKey(item) {
+ return String(item?.id || item?.url || item?.title || "");
+}
+
+function withLoadingGuard(flagGetter, flagSetter, fn) {
+ return async (...args) => {
+ if (flagGetter()) return;
+ try {
+ flagSetter(true);
+ return await fn(...args);
+ } finally {
+ flagSetter(false);
+ }
+ };
+}
+
+function extractVideoIdFromItem(item) {
+ const direct = String(item?.id || "").trim();
+ if (/^[A-Za-z0-9_-]{6,32}$/.test(direct)) return direct;
+
+ try {
+ const url = new URL(String(item?.url || item?.webpage_url || "").trim());
+ const host = url.hostname.toLowerCase();
+
+ if (host === "youtu.be") {
+ const id = url.pathname.replace(/^\/+/, "").split("/")[0];
+ return /^[A-Za-z0-9_-]{6,32}$/.test(id || "") ? id : null;
+ }
+
+ if (
+ host === "youtube.com" ||
+ host === "www.youtube.com" ||
+ host === "music.youtube.com" ||
+ host === "m.youtube.com" ||
+ host.endsWith(".youtube.com")
+ ) {
+ if (url.pathname === "/watch") {
+ const id = url.searchParams.get("v");
+ return /^[A-Za-z0-9_-]{6,32}$/.test(id || "") ? id : null;
+ }
+
+ if (url.pathname.startsWith("/shorts/") || url.pathname.startsWith("/embed/")) {
+ const parts = url.pathname.split("/").filter(Boolean);
+ const id = parts[1];
+ return /^[A-Za-z0-9_-]{6,32}$/.test(id || "") ? id : null;
+ }
+ }
+ } catch {}
+ return null;
+}
+
+function getExactYouTubeUrl(value) {
+ try {
+ const url = new URL(String(value || "").trim());
+ const host = url.hostname.toLowerCase();
+ const allowed =
+ host === "youtube.com" ||
+ host === "www.youtube.com" ||
+ host === "music.youtube.com" ||
+ host === "m.youtube.com" ||
+ host === "youtu.be" ||
+ host.endsWith(".youtube.com");
+ if (!allowed || !/^https?:$/.test(url.protocol)) return null;
+
+ if (host === "youtu.be" && url.pathname.replace(/^\/+/, "")) return url.href;
+ if (url.pathname === "/watch" && url.searchParams.get("v")) return url.href;
+ } catch {}
+ return null;
+}
+
+function updateBrowserTabTitle() {
+ document.title =
+ audioPlayer?.src && activeTrackTitle ? sanitizeTitle(activeTrackTitle) : DEFAULT_PAGE_TITLE;
+}
+
+function clearCurrentTrackPresentation() {
+ activeTrackTitle = "";
+ currentTrackContext = null;
+ document.title = DEFAULT_PAGE_TITLE;
+ if (playerTitle) {
+ playerTitle.textContent = "nothing playing";
+ playerTitle.removeAttribute("title");
+ }
+ if (playerMeta) playerMeta.textContent = "choose a track";
+}
+
+function isAudioBuffering() {
+ if (!audioPlayer?.src || audioPlayer.ended) return false;
+ if (audioPlayer.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) return false;
+
+ try {
+ if (Number.isFinite(audioPlayer.duration) && audioPlayer.duration > 0) {
+ for (let i = 0; i < audioPlayer.buffered.length; i += 1) {
+ const end = audioPlayer.buffered.end(i);
+ if (end >= audioPlayer.duration - 0.25) return false; // fully buffered
+ }
+ }
+ } catch {}
+
+ return (
+ audioPlayer.paused ||
+ audioPlayer.seeking ||
+ audioPlayer.networkState === HTMLMediaElement.NETWORK_LOADING
+ );
+}
+function hasFullyBuffered() {
+ if (!audioPlayer?.src || !Number.isFinite(audioPlayer.duration) || audioPlayer.duration <= 0) {
+ return false;
+ }
+
+ try {
+ for (let i = 0; i < audioPlayer.buffered.length; i += 1) {
+ const start = audioPlayer.buffered.start(i);
+ const end = audioPlayer.buffered.end(i);
+ if (start <= 0 && end >= audioPlayer.duration - 0.25) {
+ return true;
+ }
+ }
+ } catch {}
+ return false;
+}
+
+function updatePlayPauseButton() {
+ if (!audioPlayer || !playPauseButton) return;
+
+ const playing = !audioPlayer.paused && !audioPlayer.ended && Boolean(audioPlayer.src);
+ const buffering = isAudioBuffering();
+ const seekBuffering = seekBufferingUntil > Date.now();
+
+ playPauseButton.classList.toggle("is-playing", playing);
+ playPauseButton.classList.toggle("is-buffering", buffering || seekBuffering);
+ playPauseButton.disabled = buffering || seekBuffering;
+
+ playPauseButton.setAttribute(
+ "aria-label",
+ buffering || seekBuffering ? "buffering" : playing ? "pause" : "play",
+ );
+ playPauseButton.title = buffering || seekBuffering ? "buffering" : playing ? "pause" : "play";
+}
+
+function updateMediaSessionMetadata(item) {
+ if (!("mediaSession" in navigator) || typeof MediaMetadata !== "function") return;
+ try {
+ navigator.mediaSession.metadata = new MediaMetadata({
+ title: sanitizeTitle(item?.title, 120),
+ artist: sanitizeTitle(item?.channel || "-", 80),
+ });
+ } catch {}
+}
+
+function setModeButtonState(button, enabled, label, title) {
+ if (!button) return;
+ button.classList.toggle("active", enabled);
+ button.classList.toggle("inactive", !enabled);
+ button.setAttribute("aria-pressed", String(enabled));
+ button.setAttribute("aria-label", label);
+ button.title = title;
+}
+
+function queueIsValid() {
+ return (
+ Array.isArray(playlistQueue) &&
+ playlistQueue.length > 0 &&
+ Number.isInteger(playlistQueueIndex) &&
+ playlistQueueIndex >= 0 &&
+ playlistQueueIndex < playlistQueue.length
+ );
+}
+
+
+function updateQueueControls() {
+ const valid = queueIsValid();
+
+ for (const button of [prevTrackButton, nextTrackButton]) {
+ if (!button) continue;
+ button.disabled = !valid;
+ button.setAttribute("aria-disabled", String(!valid));
+ }
+
+ setModeButtonState(
+ autoplayButton,
+ autoplayEnabled,
+ `autoplay ${autoplayEnabled ? "enabled" : "disabled"}`,
+ `autoplay: ${autoplayEnabled ? "on" : "off"}`
+ );
+
+ setModeButtonState(
+ shuffleButton,
+ shuffleEnabled,
+ `shuffle ${shuffleEnabled ? "enabled" : "disabled"}`,
+ `shuffle: ${shuffleEnabled ? "on" : "off"}`
+ );
+
+ if (loopButton) {
+ const enabled = loopMode !== "off";
+ loopButton.classList.toggle("active", enabled);
+ loopButton.classList.toggle("inactive", !enabled);
+ loopButton.classList.toggle("loop-one", loopMode === "one");
+ loopButton.setAttribute("aria-pressed", String(enabled));
+ loopButton.setAttribute("aria-label", loopMode === "off" ? "loop disabled" : `loop ${loopMode} enabled`);
+ loopButton.title = `loop: ${loopMode}`;
+ }
+}
+function renderQueue() {
+ if (!queuePanel || !queueList) return;
+
+ if (!queueIsValid()) {
+ queuePanel.hidden = true;
+ queueList.replaceChildren();
+ return;
+ }
+
+ queuePanel.hidden = false;
+ queueList.replaceChildren();
+
+ const upcoming = [];
+ if (shuffleEnabled) {
+ for (let index = 0; index < playlistQueue.length; index += 1) {
+ if (index !== playlistQueueIndex && !shufflePlayedIndexes.has(index)) upcoming.push(index);
+ }
+ } else {
+ for (let index = playlistQueueIndex + 1; index < playlistQueue.length; index += 1) {
+ upcoming.push(index);
+ }
+ if (loopMode === "all") {
+ for (let index = 0; index < playlistQueueIndex; index += 1) upcoming.push(index);
+ }
+ }
+
+ if (!upcoming.length) {
+ queueList.innerHTML = '
no upcoming tracks
';
+ return;
+ }
+
+ for (const index of upcoming.slice(0, 20)) {
+ const item = playlistQueue[index];
+ const button = document.createElement("button");
+ button.className = "queue-item";
+ button.type = "button";
+ button.innerHTML = `
+ ${index + 1}
+
+ ${escapeHtml(item.title || "untitled")}
+ ${escapeHtml(item.channel || "-")} · ${escapeHtml(item.duration_string || "-")}
+
+ `;
+ button.addEventListener("click", () => {
+ playlistQueueIndex = index;
+ shufflePlayedIndexes.add(index);
+ updateQueueControls();
+ renderQueue();
+ playTrack(item, { fromQueue: true });
+ });
+ queueList.appendChild(button);
+ }
+}
+
+function normalizeQueue() {
+ playlistQueue = Array.isArray(playlistQueue)
+ ? playlistQueue.filter(item => extractVideoIdFromItem(item))
+ : [];
+
+ if (!playlistQueue.length) {
+ playlistQueueIndex = -1;
+ shufflePlayedIndexes.clear();
+ updateQueueControls();
+ renderQueue();
+ return false;
+ }
+
+ if (
+ !Number.isInteger(playlistQueueIndex) ||
+ playlistQueueIndex < 0 ||
+ playlistQueueIndex >= playlistQueue.length
+ ) {
+ playlistQueueIndex = 0;
+ }
+
+ shufflePlayedIndexes = new Set(
+ [...shufflePlayedIndexes].filter(index => index >= 0 && index < playlistQueue.length),
+ );
+ shufflePlayedIndexes.add(playlistQueueIndex);
+ return true;
+}
+
+function enterSearchResultsMode() {
+ pageRoot.classList.remove("page-home");
+ pageRoot.classList.add("page-results");
+ resultsSection.classList.remove("results-section-home");
+ resultsSection.classList.add("results-section-active");
+ searchHero.classList.add("is-compact");
+}
+
+function resetHomeLayout() {
+ pageRoot.classList.add("page-home");
+ pageRoot.classList.remove("page-results");
+ resultsSection.classList.add("results-section-home");
+ resultsSection.classList.remove("results-section-active");
+ searchHero.classList.remove("is-compact");
+}
+
+
+function switchView(view) {
+ currentView = view;
+ favoritesTab.classList.toggle("active", view === "favorites");
+ playlistsTab.classList.toggle("active", view === "playlists" || view === "playlistDetail");
+ backButton.style.display = view === "search" ? "none" : "";
+ scrollSentinel.style.display = view === "search" ? "" : "none";
+
+ if (backButton) {
+ backButton.hidden = view === "search";
+ }
+
+ if (scrollSentinel) {
+ scrollSentinel.style.display = view === "search" ? "" : "none";
+ }
+
+ if (view !== "search") enterSearchResultsMode();
+ else if (currentQuery) enterSearchResultsMode();
+
+ setStatus(
+ view === "favorites"
+ ? "favorites"
+ : view === "playlists"
+ ? "playlists"
+ : view === "playlistDetail"
+ ? "playlist"
+ : view === "sharedPlaylist"
+ ? "shared playlist"
+ : currentQuery
+ ? `searching "${currentQuery}"`
+ : "",
+ );
+}
+
+function resetSearchResults() {
+ resultsBody.innerHTML = `| no results |
`;
+ setSentinel("");
+
+}
+function appendResults(items, forceFavorited = false) {
+ items.forEach((item, index) => {
+ const row = document.createElement("tr");
+ const key = getItemKey(item);
+ const favorited = forceFavorited || favoriteIds.has(key);
+ const rowIndex = item.index ?? (index + 1);
+
+ row.dataset.row = "result";
+ row.dataset.key = key;
+
+ row.innerHTML = `
+ ${escapeHtml(rowIndex)} |
+
+
+ |
+ ${escapeHtml(item.channel || "-")} |
+ ${escapeHtml(item.duration_string || "-")} |
+
+ ${item.is_live ? 'live' : ""}
+ ${
+ item.view_count != null
+ ? `${escapeHtml(formatViewCount(item.view_count))}`
+ : item.score != null
+ ? `${escapeHtml(Number(item.score).toFixed(2))}`
+ : ""
+ }
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+ `;
+
+ row.querySelector(".title-button")?.addEventListener("click", () => playTrack(item));
+ row.querySelector(".play-button")?.addEventListener("click", () => playTrack(item));
+ row.querySelector(".download-button")?.addEventListener("click", () => downloadTrack(item));
+ row.querySelector(".star-button")?.addEventListener("click", (event) => {
+ toggleFavorite(item, row, event.currentTarget);
+ });
+ row.querySelector(".playlist-add-button")?.addEventListener("click", () => addItemToPlaylist(item));
+
+ resultsBody.appendChild(row);
+ });
+}
+
+async function toggleFavorite(item, row, button) {
+ try {
+ const data = await apiJson("/api/favorites/toggle", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ item }),
+ });
+
+ const key = getItemKey(item);
+ if (data.favorited) favoriteIds.add(key);
+ else favoriteIds.delete(key);
+
+ button.textContent = data.favorited ? "★" : "☆";
+ button.classList.toggle("favorited", data.favorited);
+
+ if (currentView === "favorites" && !data.favorited) {
+ row.remove();
+ if (!resultsBody.querySelector("tr[data-row='result']")) {
+ resultsBody.innerHTML = `| no favorites yet |
`;
+ }
+ }
+ } catch {
+ setStatus("retry");
+ }
+}
+
+async function loadFavoriteIds() {
+ try {
+ favoriteIds = new Set((await apiJson("/api/favorites")).results.map(getItemKey));
+ } catch {}
+}
+
+
+async function startSearch(query) {
+ searchAbortController?.abort();
+ enterSearchResultsMode();
+ currentView = "search";
+ currentQuery = query;
+ currentPage = 0;
+ loadedCount = 0;
+ hasMore = true;
+ isLoading = false;
+ searchToken += 1;
+ resultsBody.innerHTML = ` |
`;
+ setSentinel("");
+ await loadNextPage(searchToken);
+}
+
+async function loadNextPage(token = searchToken) {
+ if (
+ currentView !== "search" ||
+ !currentQuery ||
+ !hasMore ||
+ isLoading ||
+ loadedCount >= MAX_AUTO_RESULTS ||
+ currentPage >= MAX_AUTO_PAGES ||
+ token !== searchToken
+ ) {
+ return;
+ }
+
+ const page = currentPage + 1;
+ const controller = new AbortController();
+ searchAbortController = controller;
+ isLoading = true;
+ setSentinel("loading");
+
+ try {
+ const response = await apiFetch(
+ `/api/search?q=${encodeURIComponent(currentQuery)}&limit=${PAGE_SIZE}&page=${page}`,
+ { signal: controller.signal },
+ );
+ if (!response.ok) throw new Error("search failed");
+
+ const data = await response.json();
+ if (token !== searchToken) return;
+
+ if (page === 1) resultsBody.replaceChildren();
+
+ const remaining = MAX_AUTO_RESULTS - loadedCount;
+ const items = Array.isArray(data.results) ? data.results.slice(0, remaining) : [];
+ appendResults(items);
+
+ currentPage = Number.isInteger(data.page) ? data.page : page;
+ loadedCount += items.length;
+ hasMore =
+ Boolean(data.has_more) &&
+ loadedCount < MAX_AUTO_RESULTS &&
+ currentPage < MAX_AUTO_PAGES;
+
+ setStatus(`${loadedCount} loaded`);
+
+ if (!resultsBody.querySelector("tr[data-row='result']")) {
+ resetSearchResults();
+ }
+ setSentinel("");
+ } catch (error) {
+ if (error.name !== "AbortError") {
+ setStatus("search failed");
+ setSentinel("");
+ }
+ } finally {
+ if (searchAbortController === controller) searchAbortController = null;
+ if (token === searchToken) isLoading = false;
+ }
+}
+
+async function startRecommendationsSearch(url) {
+ enterSearchResultsMode();
+ currentView = "search";
+ currentQuery = url;
+ currentPage = 0;
+ loadedCount = 0;
+ hasMore = false;
+ isLoading = true;
+ searchToken += 1;
+ resultsBody.innerHTML = `| loading related tracks |
`;
+
+ try {
+ const data = await apiJson(
+ `/api/recommendations?url=${encodeURIComponent(url)}&total=${PAGE_SIZE}&per_query=8`,
+ );
+ resultsBody.replaceChildren();
+ if (!data.results?.length) {
+ resetSearchResults();
+ setStatus("no related tracks");
+ } else {
+ appendResults(data.results);
+ setStatus(`${data.results.length} related found`);
+ }
+ } catch {
+ resultsBody.innerHTML = `| failed to load related |
`;
+ setStatus("related tracks failed");
+ } finally {
+ isLoading = false;
+ }
+}
+
+async function loadFavoritesView() {
+ enterSearchResultsMode();
+ currentView = "favorites";
+ searchToken += 1;
+ isLoading = true;
+ resultsBody.innerHTML = `| loading favorites |
`;
+
+ try {
+ const data = await apiJson("/api/favorites");
+ resultsBody.replaceChildren();
+ if (data.results.length) appendResults(data.results, true);
+ else resultsBody.innerHTML = `| no favorites yet |
`;
+ } catch {
+ resultsBody.innerHTML = `| error |
`;
+ } finally {
+ isLoading = false;
+ }
+}
+
+async function createPlaylist() {
+ const name = prompt("playlist name");
+ if (!name?.trim()) return null;
+ return apiJson("/api/playlists", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ name: name.trim() }),
+ });
+}
+
+async function movePlaylistItem(index, delta) {
+ if (!currentPlaylistId) return;
+
+ const nextIndex = index + delta;
+ if (nextIndex < 0 || nextIndex >= currentPlaylistItems.length) return;
+
+ const reordered = [...currentPlaylistItems];
+ const [moved] = reordered.splice(index, 1);
+ reordered.splice(nextIndex, 0, moved);
+
+ try {
+ await savePlaylistOrder(currentPlaylistId, reordered);
+ currentPlaylistItems = reordered;
+ playlistQueue = [...reordered];
+ setStatus("playlist reordered");
+ loadPlaylistDetail(currentPlaylistId);
+ } catch (error) {
+ console.error(error);
+ setStatus("reorder failed");
+ }
+}
+async function removePlaylistItem(playlistId, playlistItemId) {
+ try {
+ await apiJson(`/api/playlists/${encodeURIComponent(playlistId)}/items/${encodeURIComponent(playlistItemId)}`, {
+ method: "DELETE",
+ });
+
+ currentPlaylistItems = currentPlaylistItems.filter(
+ item => item.playlist_item_id !== playlistItemId
+ );
+ playlistQueue = [...currentPlaylistItems];
+
+ setStatus("removed from playlist");
+ loadPlaylistDetail(playlistId);
+ } catch (error) {
+ console.error(error);
+ setStatus("remove failed");
+ }
+}
+async function loadPlaylistsView() {
+ enterSearchResultsMode();
+ searchToken += 1;
+ isLoading = true;
+ currentPlaylistId = null;
+ currentPlaylistItems = [];
+
+ resultsBody.innerHTML = `
+
+ | loading playlists |
+
+ `;
+
+ setSentinel("");
+
+ try {
+ const data = await apiJson("/api/playlists");
+ const results = data.results || [];
+
+ resultsBody.innerHTML = "";
+ appendCreatePlaylistRow();
+
+ if (!results.length) {
+ const row = document.createElement("tr");
+ row.innerHTML = `no playlists yet | `;
+ resultsBody.appendChild(row);
+ setStatus("no playlists");
+ return;
+ }
+
+ results.forEach((playlist, index) => {
+ const row = document.createElement("tr");
+ row.dataset.row = "playlist";
+ row.dataset.key = playlist.id;
+ row.draggable = true;
+
+ row.innerHTML = `
+ ${index + 1} |
+
+ ⋮⋮
+
+ |
+ ${escapeHtml(String(playlist.count || 0))} tracks |
+ ${playlist.share_url ? "shared" : "-"} |
+
+
+ |
+
+
+ |
+ |
+
+
+ |
+
+
+ |
+ `;
+
+ row.querySelector(".playlist-open-button")?.addEventListener("click", () => {
+ loadPlaylistDetail(playlist.id);
+ });
+
+ row.querySelector(".playlist-open-button-2")?.addEventListener("click", () => {
+ loadPlaylistDetail(playlist.id);
+ });
+
+ row.querySelector(".playlist-rename-button")?.addEventListener("click", () => {
+ renamePlaylist(playlist);
+ });
+
+ row.querySelector(".playlist-share-button")?.addEventListener("click", () => {
+ sharePlaylist(playlist);
+ });
+
+ row.querySelector(".playlist-delete-button")?.addEventListener("click", () => {
+ deletePlaylist(playlist);
+ });
+
+ row.addEventListener("dragstart", (event) => {
+ playlistReorderDragId = playlist.id;
+ event.dataTransfer.effectAllowed = "move";
+ event.dataTransfer.setData("text/plain", playlist.id);
+ row.classList.add("dragging");
+ });
+
+ row.addEventListener("dragend", () => {
+ playlistReorderDragId = null;
+ row.classList.remove("dragging");
+ resultsBody.querySelectorAll("tr.drag-over").forEach(el => el.classList.remove("drag-over"));
+ });
+
+ row.addEventListener("dragover", (event) => {
+ event.preventDefault();
+ event.dataTransfer.dropEffect = "move";
+ row.classList.add("drag-over");
+ });
+
+ row.addEventListener("dragleave", () => {
+ row.classList.remove("drag-over");
+ });
+
+ row.addEventListener("drop", async (event) => {
+ event.preventDefault();
+ row.classList.remove("drag-over");
+
+ const draggedId =
+ event.dataTransfer.getData("text/plain") || playlistReorderDragId;
+
+ if (!draggedId || draggedId === playlist.id) return;
+
+ const current = [...results];
+ const fromIndex = current.findIndex(p => p.id === draggedId);
+ const toIndex = current.findIndex(p => p.id === playlist.id);
+
+ if (fromIndex < 0 || toIndex < 0) return;
+
+ const [moved] = current.splice(fromIndex, 1);
+ current.splice(toIndex, 0, moved);
+
+ try {
+ await apiJson("/api/playlists/reorder", {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ playlist_ids: current.map(p => p.id),
+ }),
+ });
+
+ setStatus("playlists reordered");
+ loadPlaylistsView();
+ } catch (error) {
+ console.error(error);
+ setStatus("reorder failed");
+ }
+ });
+
+ resultsBody.appendChild(row);
+ });
+
+ setStatus(`${results.length} playlists`);
+ } catch (error) {
+ console.error(error);
+ resultsBody.innerHTML = `
+ | failed to load playlists |
+ `;
+ setStatus("error");
+ } finally {
+ isLoading = false;
+ }
+}
+
+async function savePlaylistOrder(playlistId, items) {
+ const itemIds = (Array.isArray(items) ? items : [])
+ .map(item => item?.playlist_item_id)
+ .filter(Boolean);
+
+ if (!itemIds.length) {
+ throw new Error("no playlist item ids to reorder");
+ }
+
+ return apiJson(`/api/playlists/${encodeURIComponent(playlistId)}/items/reorder`, {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ item_ids: itemIds,
+ }),
+ });
+}
+
+function appendPlaylistItems(playlistId, items, shared = false) {
+ items.forEach((item, index) => {
+ const row = document.createElement("tr");
+ const key = getItemKey(item);
+ const favorited = favoriteIds.has(key);
+
+ row.dataset.row = "result";
+ row.dataset.key = key;
+ row.dataset.playlistItemId = item.playlist_item_id;
+ row.draggable = !shared;
+
+ row.innerHTML = `
+ ${index + 1} |
+
+ ${
+ shared
+ ? ""
+ : '⋮⋮'
+ }
+
+ |
+ ${escapeHtml(item.channel || "-")} |
+ ${escapeHtml(item.duration_string || "-")} |
+
+ ${item.is_live ? 'live' : ""}
+ ${
+ item.view_count != null
+ ? `${escapeHtml(formatViewCount(item.view_count))}`
+ : item.score != null
+ ? `${escapeHtml(Number(item.score).toFixed(2))}`
+ : ""
+ }
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ ${
+ shared
+ ? `
+
+ `
+ : `
+
+ `
+ }
+ |
+ `;
+
+ row.querySelector(".title-button")?.addEventListener("click", () => {
+ playPlaylistItem(items, index);
+ });
+
+ row.querySelector(".play-button")?.addEventListener("click", () => {
+ playPlaylistItem(items, index);
+ });
+
+ row.querySelector(".download-button")?.addEventListener("click", () => {
+ downloadTrack(item);
+ });
+
+ row.querySelector(".star-button")?.addEventListener("click", (event) => {
+ toggleFavorite(item, row, event.currentTarget);
+ });
+
+ if (shared) {
+ row.querySelector(".playlist-add-button")?.addEventListener("click", () => {
+ addItemToPlaylist(item);
+ });
+ } else {
+ row.querySelector(".playlist-remove-button")?.addEventListener("click", () => {
+ removePlaylistItem(playlistId, item.playlist_item_id);
+ });
+
+ const dragHandle = row.querySelector(".drag-handle");
+ if (dragHandle) {
+ dragHandle.addEventListener("dragstart", (event) => {
+ playlistItemDragId = item.playlist_item_id;
+ playlistItemDragPlaylistId = playlistId;
+ event.dataTransfer.effectAllowed = "move";
+ event.dataTransfer.setData("text/plain", item.playlist_item_id);
+ row.classList.add("dragging");
+ });
+ }
+
+ row.addEventListener("dragstart", (event) => {
+ if (event.target?.classList?.contains("drag-handle")) return;
+ playlistItemDragId = item.playlist_item_id;
+ playlistItemDragPlaylistId = playlistId;
+ event.dataTransfer.effectAllowed = "move";
+ event.dataTransfer.setData("text/plain", item.playlist_item_id);
+ row.classList.add("dragging");
+ });
+
+ row.addEventListener("dragend", () => {
+ playlistItemDragId = null;
+ playlistItemDragPlaylistId = null;
+ row.classList.remove("dragging");
+ resultsBody
+ .querySelectorAll("tr.drag-over")
+ .forEach(el => el.classList.remove("drag-over"));
+ });
+
+ row.addEventListener("dragover", (event) => {
+ event.preventDefault();
+ event.dataTransfer.dropEffect = "move";
+ row.classList.add("drag-over");
+ });
+
+ row.addEventListener("dragleave", () => {
+ row.classList.remove("drag-over");
+ });
+
+ row.addEventListener("drop", async (event) => {
+ event.preventDefault();
+ row.classList.remove("drag-over");
+
+ if (playlistItemDragPlaylistId !== playlistId) return;
+
+ const draggedId =
+ event.dataTransfer.getData("text/plain") || playlistItemDragId;
+ const targetId = item.playlist_item_id;
+
+ if (!draggedId || draggedId === targetId) return;
+
+ const reordered = reorderArrayByIds(items, draggedId, targetId);
+
+ try {
+ await savePlaylistOrder(playlistId, reordered);
+ currentPlaylistItems = reordered;
+ playlistQueue = [...reordered];
+ setStatus("playlist reordered");
+ loadPlaylistDetail(playlistId);
+ } catch (error) {
+ console.error(error);
+ setStatus("reorder failed");
+ }
+ });
+ }
+
+ resultsBody.appendChild(row);
+ });
+}
+
+async function loadPlaylistDetail(playlistId, shared = false) {
+ currentView = shared ? "sharedPlaylist" : "playlistDetail";
+ switchView(currentView);
+ currentPlaylistId = shared ? null : playlistId;
+ isLoading = true;
+
+ try {
+ const playlist = await apiJson(
+ shared
+ ? `/api/shared-playlists/${encodeURIComponent(playlistId)}`
+ : `/api/playlists/${encodeURIComponent(playlistId)}`
+ );
+
+ currentPlaylistItems = playlist.items || [];
+ playlistQueue = [...currentPlaylistItems];
+ playlistQueueIndex = playlistQueue.length ? 0 : -1;
+ shufflePlayedIndexes = new Set();
+
+ resultsBody.replaceChildren();
+
+ if (!playlistQueue.length) {
+ resultsBody.innerHTML = `| playlist is empty |
`;
+ updateQueueControls();
+ renderQueue();
+ return;
+ }
+
+ appendPlaylistItems(playlistId, playlistQueue, shared);
+ setStatus(`${playlistQueue.length} tracks`);
+ updateQueueControls();
+ renderQueue();
+ } catch (error) {
+ console.error(error);
+ resultsBody.innerHTML = `| failed to load playlist |
`;
+ } finally {
+ isLoading = false;
+ }
+}
+
+function appendCreatePlaylistRow() {
+ const row = document.createElement("tr");
+ row.innerHTML = ` | `;
+ row.querySelector("button").addEventListener("click", async () => {
+ try {
+ if (await createPlaylist()) loadPlaylistsView();
+ } catch {
+ setStatus("create playlist failed");
+ }
+ });
+ resultsBody.appendChild(row);
+}
+async function savePlaylistOrder(playlistId, items) {
+ const itemIds = (Array.isArray(items) ? items : [])
+ .map(item => item?.playlist_item_id)
+ .filter(Boolean);
+
+ if (!itemIds.length) {
+ throw new Error("no playlist item ids to reorder");
+ }
+
+ return apiJson(`/api/playlists/${encodeURIComponent(playlistId)}/items/reorder`, {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ item_ids: itemIds,
+ }),
+ });
+}
+
+async function choosePlaylistForItem() {
+ const data = await apiJson("/api/playlists");
+ const playlists = data.results || [];
+ if (!playlists.length) {
+ return confirm("no playlists. create new?") ? createPlaylist() : null;
+ }
+
+ const lines = playlists.map(
+ (playlist, index) => `${index + 1}. ${playlist.name} (${playlist.count || 0})`,
+ );
+ const choice = Number(prompt(
+ `choose playlist number:\n\n${lines.join("\n")}\n\n0. create new playlist`,
+ ));
+
+ if (choice === 0) return createPlaylist();
+
+ return Number.isInteger(choice) && choice >= 1 && choice <= playlists.length
+ ? playlists[choice - 1]
+ : null;
+}
+
+async function addItemToPlaylist(item) {
+ try {
+ const playlist = await choosePlaylistForItem();
+ if (!playlist) return;
+ const data = await apiJson(`/api/playlists/${encodeURIComponent(playlist.id)}/items`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ item }),
+ });
+ setStatus(data.duplicate ? "already in playlist" : `added to ${playlist.name}`);
+ } catch {
+ setStatus("add to playlist failed");
+ }
+}
+
+async function sharePlaylist(playlist) {
+ try {
+ const data = await apiJson(`/api/playlists/${encodeURIComponent(playlist.id)}/share`, {
+ method: "POST",
+ });
+ await navigator.clipboard?.writeText(`${window.location.origin}${data.share_url}`);
+ setStatus("share link copied");
+ } catch {
+ setStatus("share failed");
+ }
+}
+
+function setQueue(items, index = 0) {
+ playlistQueue = Array.isArray(items)
+ ? items.filter(item => extractVideoIdFromItem(item))
+ : [];
+ playlistQueueIndex = playlistQueue.length
+ ? Math.min(Math.max(index, 0), playlistQueue.length - 1)
+ : -1;
+ shufflePlayedIndexes = playlistQueueIndex >= 0 ? new Set([playlistQueueIndex]) : new Set();
+ updateQueueControls();
+ renderQueue();
+}
+
+function playPlaylistItem(items, index) {
+ setQueue(items, index);
+ if (queueIsValid()) playTrack(playlistQueue[playlistQueueIndex], { fromQueue: true });
+}
+
+function getNextQueueIndex() {
+ if (!queueIsValid()) return -1;
+
+ if (shuffleEnabled) {
+ const available = [];
+ for (let index = 0; index < playlistQueue.length; index += 1) {
+ if (!shufflePlayedIndexes.has(index)) available.push(index);
+ }
+ if (available.length) {
+ return available[Math.floor(Math.random() * available.length)];
+ }
+ if (loopMode === "all") {
+ shufflePlayedIndexes = new Set([playlistQueueIndex]);
+ const loopAvailable = playlistQueue
+ .map((_, index) => index)
+ .filter(index => index !== playlistQueueIndex);
+ return loopAvailable.length
+ ? loopAvailable[Math.floor(Math.random() * loopAvailable.length)]
+ : playlistQueueIndex;
+ }
+ return -1;
+ }
+
+ if (playlistQueueIndex + 1 < playlistQueue.length) {
+ return playlistQueueIndex + 1;
+ }
+ return loopMode === "all" ? 0 : -1;
+}
+
+function playNextInQueue({ manual = false, fromEnded = false } = {}) {
+ if (!queueIsValid()) {
+ if (manual) setStatus("no active playlist");
+ updateQueueControls();
+ return;
+ }
+
+ if (!manual && !fromEnded && !autoplayEnabled) {
+ setStatus("autoplay off");
+ return;
+ }
+
+ if (!manual && loopMode === "one") {
+ playTrack(playlistQueue[playlistQueueIndex], { fromQueue: true });
+ return;
+ }
+
+ const nextIndex = getNextQueueIndex();
+ if (nextIndex < 0) {
+ setStatus("playlist ended");
+ return;
+ }
+
+ playlistQueueIndex = nextIndex;
+ shufflePlayedIndexes.add(nextIndex);
+ updateQueueControls();
+ renderQueue();
+ playTrack(playlistQueue[nextIndex], { fromQueue: true });
+}
+
+function playPreviousInQueue() {
+ if (!queueIsValid()) {
+ setStatus("no active playlist");
+ updateQueueControls();
+ return;
+ }
+
+ if (audioPlayer.currentTime > 3) {
+ audioPlayer.currentTime = 0;
+ setStatus("track restarted");
+ return;
+ }
+
+ let previousIndex = playlistQueueIndex - 1;
+ if (previousIndex < 0) {
+ if (loopMode !== "all") {
+ setStatus("start of playlist");
+ return;
+ }
+ previousIndex = playlistQueue.length - 1;
+ }
+
+ playlistQueueIndex = previousIndex;
+ shufflePlayedIndexes.add(previousIndex);
+ updateQueueControls();
+ renderQueue();
+ playTrack(playlistQueue[previousIndex], { fromQueue: true });
+}
+
+function preloadNextTrack() {
+ if (preloadPromise || !queueIsValid()) return;
+ const nextIndex = getNextQueueIndex();
+ if (nextIndex < 0) return;
+ const videoId = extractVideoIdFromItem(playlistQueue[nextIndex]);
+ if (!videoId) return;
+
+ preloadPromise = apiJson("/api/audio-sessions", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ video_id: videoId }),
+ })
+ .catch(() => null)
+ .finally(() => {
+ preloadPromise = null;
+ });
+}
+
+function safeDownloadName(title) {
+ return sanitizeTitle(title, 120)
+ .replace(/[^\w.-]+/g, "_")
+ .replace(/^_+|_+$/g, "") || "track";
+}
+
+async function downloadTrack(item) {
+ const videoId = extractVideoIdFromItem(item);
+ if (!videoId) {
+ setStatus("invalid track");
+ return;
+ }
+
+ setStatus("preparing download");
+ try {
+ const response = await apiFetch(
+ `/api/download/${encodeURIComponent(videoId)}?title=${encodeURIComponent(item.title || "untitled")}`,
+ );
+ if (!response.ok) throw new Error("download failed");
+
+ const blob = await response.blob();
+ const objectUrl = URL.createObjectURL(blob);
+ const anchor = document.createElement("a");
+ anchor.href = objectUrl;
+ anchor.download = `${safeDownloadName(item.title)}.mp3`;
+ document.body.appendChild(anchor);
+ anchor.click();
+ anchor.remove();
+ setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
+ setStatus("download ready");
+ } catch {
+ setStatus("download failed");
+ }
+}
+
+function buildPlayerStatePayload() {
+ const queue = Array.isArray(playlistQueue)
+ ? playlistQueue.map(sanitizeStoredTrack).filter(Boolean)
+ : [];
+ const currentItem = sanitizeStoredTrack(currentTrackContext?.item);
+ return {
+ current_item: currentItem,
+ current_time: Number.isFinite(audioPlayer?.currentTime)
+ ? Math.max(0, Math.min(Number(audioPlayer.currentTime.toFixed(3)), 86400))
+ : 0,
+ queue,
+ queue_index:
+ Number.isInteger(playlistQueueIndex) &&
+ playlistQueueIndex >= 0 &&
+ playlistQueueIndex < queue.length
+ ? playlistQueueIndex
+ : -1,
+ autoplay: Boolean(autoplayEnabled),
+ shuffle: Boolean(shuffleEnabled),
+ loop_mode: ["off", "all", "one"].includes(loopMode) ? loopMode : "off",
+ volume: Number.isFinite(audioPlayer?.volume)
+ ? Math.min(Math.max(audioPlayer.volume, 0), 1)
+ : 1,
+ queue_mode: queueMode === "recommendations" ? "recommendations" : "queue",
+ source_url: getExactYouTubeUrl(currentQuery) || null,
+ };
+}
+
+async function savePlayerState() {
+ if (restoringPlayerState) return;
+ if (!audioPlayer) return;
+ const payload = buildPlayerStatePayload();
+ try {
+ await apiJson("/api/player-state", {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(payload),
+ });
+ } catch (error) {
+ console.debug("player state save failed", error);
+ }
+}
+
+function schedulePlayerStateSave() {
+ if (restoringPlayerState) return;
+ clearTimeout(playerStateSaveTimer);
+ playerStateSaveTimer = setTimeout(() => {
+ savePlayerState();
+ }, 1200);
+}
+
+function schedulePlayerStateSaveLowPriority() {
+ if (restoringPlayerState) return;
+ clearTimeout(playerStateSaveTimer);
+ playerStateSaveTimer = setTimeout(() => {
+ savePlayerState();
+ }, 3000);
+}
+
+async function restorePlayerState() {
+ if (playerStateRestorePromise) return playerStateRestorePromise;
+
+ playerStateRestorePromise = (async () => {
+ restoringPlayerState = true;
+ try {
+ const state = await apiJson("/api/player-state");
+
+ autoplayEnabled = Boolean(state?.autoplay ?? autoplayEnabled);
+ shuffleEnabled = Boolean(state?.shuffle ?? shuffleEnabled);
+ loopMode = ["off", "all", "one"].includes(state?.loop_mode)
+ ? state.loop_mode
+ : "off";
+ queueMode = state?.queue_mode === "recommendations" ? "recommendations" : "queue";
+ if (queueModeSelect) queueModeSelect.value = queueMode;
+
+ const storedVolume = Number(state?.volume);
+ const volume = Number.isFinite(storedVolume)
+ ? Math.min(Math.max(storedVolume, 0), 1)
+ : 1;
+ audioPlayer.volume = volume;
+ if (volumeSlider) volumeSlider.value = String(Math.round(volume * 100));
+ localStorage.setItem(VOLUME_KEY, String(volume));
+
+ const restoredQueue = Array.isArray(state?.queue)
+ ? state.queue.map(sanitizeStoredTrack).filter(Boolean)
+ : [];
+ playlistQueue = restoredQueue;
+ playlistQueueIndex = Number.isInteger(state?.queue_index) ? state.queue_index : -1;
+ if (
+ playlistQueue.length &&
+ (playlistQueueIndex < 0 || playlistQueueIndex >= playlistQueue.length)
+ ) {
+ playlistQueueIndex = 0;
+ }
+ shufflePlayedIndexes = new Set(
+ playlistQueueIndex >= 0 ? [playlistQueueIndex] : [],
+ );
+
+ updateQueueControlButtons?.();
+
+ const currentItem = sanitizeStoredTrack(state?.current_item);
+ if (currentItem) {
+ if (!playlistQueue.length) {
+ playlistQueue = [currentItem];
+ playlistQueueIndex = 0;
+ shufflePlayedIndexes = new Set([0]);
+ }
+
+ const played = await playTrack(currentItem, {
+ fromQueue: playlistQueue.length > 0,
+ restoreTime: Number(state?.current_time) || 0,
+ autoplay: false,
+ });
+
+ if (played) {
+ audioPlayer.pause();
+ setStatus("restored");
+ } else {
+ setStatus("restored state available");
+ }
+ }
+ } catch (error) {
+ console.debug("player state restore failed", error);
+ } finally {
+ restoringPlayerState = false;
+ updateQueueControlButtons?.();
+ }
+ })();
+
+ return playerStateRestorePromise;
+}
+
+function updateProgress() {
+ if (!audioPlayer) return;
+ const duration = Number(audioPlayer.duration) || 0;
+ seekBar.max = duration || 100;
+ if (!seekBar.matches(":active")) {
+ seekBar.value = audioPlayer.currentTime;
+ }
+ seekBar.style.setProperty(
+ "--progress",
+ `${duration ? (audioPlayer.currentTime / duration) * 100 : 0}%`,
+ );
+ currentTimeLabel.textContent = formatTime(audioPlayer.currentTime);
+ durationLabel.textContent = formatTime(audioPlayer.duration);
+}
+function queueControlUpdateFallback() {
+ updateQueueControls();
+ renderQueue();
+}
+
+async function playTrack(item, options = {}) {
+ if (playTrackBusy) return false;
+ playTrackBusy = true;
+
+ const requestToken = ++playbackRequestToken;
+ const fromQueue = Boolean(options.fromQueue);
+ const restoreTime = Math.max(0, Number(options.restoreTime) || 0);
+ const autoplay = options.autoplay !== false;
+
+ try {
+ if (!fromQueue) {
+ playlistQueue = [];
+ playlistQueueIndex = -1;
+ shufflePlayedIndexes = new Set();
+ }
+
+ const videoId = extractVideoIdFromItem(item);
+ if (!videoId) {
+ if (requestToken === playbackRequestToken) setStatus("invalid track");
+ return false;
+ }
+
+ if (requestToken !== playbackRequestToken) return false;
+
+ const displayTitle = sanitizeTitle(item?.title, 120);
+ activeTrackTitle = displayTitle;
+ currentTrackContext = {
+ item: { ...item, id: videoId },
+ fromQueue,
+ };
+
+ playerBar?.classList.remove("hidden");
+
+ if (playerTitle) {
+ playerTitle.textContent = displayTitle;
+ playerTitle.title = displayTitle;
+ }
+
+ if (playerMeta) {
+ const parts = [sanitizeTitle(item?.channel || "-", 80)];
+ if (item.duration_string) parts.push(String(item.duration_string));
+ if (fromQueue && playlistQueue.length) {
+ parts.push(`${playlistQueueIndex + 1}/${playlistQueue.length}`);
+ }
+ playerMeta.textContent = parts.join(" • ");
+ }
+
+ setStatus("creating audio session");
+ updateQueueControlButtons?.();
+ updateMediaSessionMetadata(item);
+
+ const session = await apiJson("/api/audio-sessions", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ video_id: videoId }),
+ });
+
+ if (requestToken !== playbackRequestToken) return false;
+
+ const streamUrl = session?.streamUrl || session?.stream_url;
+ if (!streamUrl) throw new Error("missing stream URL");
+
+ audioPlayer.src = streamUrl;
+ audioPlayer.load();
+ updateBrowserTabTitle();
+
+ if (Number.isFinite(restoreTime) && restoreTime > 0) {
+ const applyRestoreTime = () => {
+ if (requestToken !== playbackRequestToken) return;
+ try {
+ if (Number.isFinite(audioPlayer.duration) && audioPlayer.duration > 0) {
+ audioPlayer.currentTime = clampAudioTime(restoreTime);
+ }
+ } catch {}
+ };
+
+ if (audioPlayer.readyState >= 1) {
+ applyRestoreTime();
+ } else {
+ audioPlayer.addEventListener("loadedmetadata", applyRestoreTime, { once: true });
+ }
+ }
+
+ if (!autoplay) {
+ setStatus("ready");
+ schedulePlayerStateSave();
+ return true;
+ }
+
+ await audioPlayer.play();
+ if (requestToken !== playbackRequestToken) return false;
+
+ setStatus("playing");
+ preloadNextTrack();
+ schedulePlayerStateSave();
+ return true;
+ } catch (error) {
+ if (requestToken !== playbackRequestToken) return false;
+
+ console.error(error);
+ audioPlayer.removeAttribute("src");
+ audioPlayer.load();
+ clearCurrentTrackPresentation();
+ updatePlayPauseButton();
+
+ if (error.status === 400) {
+ setStatus("invalid video");
+ } else if (error.status === 404) {
+ setStatus("session expired");
+ } else if (error.status === 429) {
+ setStatus("rate limited");
+ } else if (error.status === 502) {
+ setStatus("audio unavailable");
+ } else if (error.status === 503) {
+ setStatus("audio capacity unavailable");
+ } else {
+ setStatus("playback failed");
+ }
+ return false;
+ } finally {
+ playTrackBusy = false;
+ }
+}
+
+
+function installMediaSessionControls() {
+ if (!("mediaSession" in navigator)) return;
+
+ const handlers = {
+ play: () => audioPlayer.play().catch(() => {}),
+ pause: () => audioPlayer.pause(),
+ previoustrack: () => playPreviousInQueue(),
+ nexttrack: () => playNextInQueue({ manual: true }),
+ seekbackward: details => {
+ audioPlayer.currentTime = Math.max(0, audioPlayer.currentTime - (details.seekOffset || 10));
+ },
+ seekforward: details => {
+ audioPlayer.currentTime += details.seekOffset || 10;
+ },
+ seekto: details => {
+ if (Number.isFinite(details.seekTime)) {
+ audioPlayer.currentTime = details.seekTime;
+ }
+ },
+ };
+
+ for (const [action, handler] of Object.entries(handlers)) {
+ try {
+ navigator.mediaSession.setActionHandler(action, handler);
+ } catch {}
+ }
+}
+
+function loadSeekTheme() {
+ const key = `${SEEK_THEME_KEY_PREFIX}:${getAnonymousId()}`;
+ setSeekTheme(localStorage.getItem(key) || "stone", { persist: false });
+}
+
+function setSeekTheme(themeName, { persist = true } = {}) {
+ const theme = validSeekThemes.has(themeName) ? themeName : "stone";
+ const color = validSeekThemes.get(theme) || "#817567";
+ document.documentElement.style.setProperty("--seek-accent", color);
+
+ document.querySelectorAll(".seek-theme-option").forEach(option => {
+ option.setAttribute("aria-selected", String(option.dataset.seekTheme === theme));
+ });
+
+ if (seekThemeButton) {
+ seekThemeButton.style.color = color;
+ seekThemeButton.title = `seek bar color: ${theme}`;
+ }
+
+ if (persist) {
+ localStorage.setItem(`${SEEK_THEME_KEY_PREFIX}:${getAnonymousId()}`, theme);
+ }
+}
+
+
+searchForm?.addEventListener("submit", event => {
+ event.preventDefault();
+ const query = searchInput.value.trim();
+ if (!query) return;
+ const exactUrl = getExactYouTubeUrl(query);
+ switchView("search");
+ if (exactUrl) startRecommendationsSearch(exactUrl);
+ else startSearch(query);
+});
+
+prevTrackButton?.addEventListener("click", playPreviousInQueue);
+nextTrackButton?.addEventListener("click", () => playNextInQueue({ manual: true }));
+
+autoplayButton?.addEventListener("click", () => {
+ autoplayEnabled = !autoplayEnabled;
+ localStorage.setItem(AUTOPLAY_KEY, String(autoplayEnabled));
+ updateQueueControls();
+ savePlayerState();
+});
+
+shuffleButton?.addEventListener("click", () => {
+ shuffleEnabled = !shuffleEnabled;
+ localStorage.setItem(SHUFFLE_KEY, String(shuffleEnabled));
+ shufflePlayedIndexes = playlistQueueIndex >= 0 ? new Set([playlistQueueIndex]) : new Set();
+ updateQueueControls();
+ renderQueue();
+ savePlayerState();
+});
+
+loopButton?.addEventListener("click", () => {
+ loopMode = loopMode === "off" ? "all" : loopMode === "all" ? "one" : "off";
+ localStorage.setItem(LOOP_KEY, loopMode);
+ updateQueueControls();
+ renderQueue();
+ savePlayerState();
+});
+
+playPauseButton?.addEventListener("click", () => {
+ if (!audioPlayer.src) {
+ setStatus("choose a track");
+ return;
+ }
+ if (audioPlayer.paused) audioPlayer.play().catch(() => setStatus("retry"));
+ else audioPlayer.pause();
+});
+
+seekBar?.addEventListener("input", () => {
+ audioPlayer.currentTime = Number(seekBar.value);
+ updateProgress();
+});
+
+volumeSlider?.addEventListener("input", () => {
+ audioPlayer.volume = Number(volumeSlider.value) / 100;
+ localStorage.setItem(VOLUME_KEY, String(audioPlayer.volume));
+ savePlayerState();
+});
+
+favoritesTab?.addEventListener("click", () => {
+ switchView("favorites");
+ loadFavoritesView();
+});
+
+playlistsTab?.addEventListener("click", () => {
+ switchView("playlists");
+ loadPlaylistsView();
+});
+
+backButton?.addEventListener("click", () => {
+ if (currentView === "playlistDetail") {
+ switchView("playlists");
+ loadPlaylistsView();
+ return;
+ }
+ if (currentView === "sharedPlaylist") {
+ window.location.href = "/";
+ return;
+ }
+ switchView("search");
+ if (currentQuery) startSearch(currentQuery);
+ else {
+ resetHomeLayout();
+ resetSearchResults();
+ setStatus("");
+ }
+});
+
+resultsSection?.addEventListener(
+ "scroll",
+ () => {
+ if (
+ currentView === "search" &&
+ hasMore &&
+ !isLoading &&
+ resultsSection.scrollHeight - resultsSection.scrollTop - resultsSection.clientHeight < 100
+ ) {
+ loadNextPage(searchToken);
+ }
+ },
+ { passive: true },
+);
+
+queueRefreshButton?.addEventListener("click", () => {
+ renderQueue();
+ preloadNextTrack();
+});
+
+queueModeSelect?.addEventListener("change", () => {
+ queueMode = queueModeSelect.value === "recommendations" ? "recommendations" : "queue";
+ localStorage.setItem("minimal_yt_queue_mode", queueMode);
+ savePlayerState();
+});
+
+document.querySelectorAll(".seek-theme-option").forEach(option => {
+ option.addEventListener("click", () => {
+ setSeekTheme(option.dataset.seekTheme);
+ seekThemeMenu.hidden = true;
+ });
+});
+
+seekThemeButton?.addEventListener("click", () => {
+ seekThemeMenu.hidden = !seekThemeMenu.hidden;
+});
+
+document.addEventListener("click", event => {
+ if (seekThemeMenu && !seekThemeMenu.hidden && !event.target.closest("#seekThemePicker")) {
+ seekThemeMenu.hidden = true;
+ }
+});
+
+document.addEventListener("keydown", event => {
+ if (
+ event.defaultPrevented ||
+ event.ctrlKey ||
+ event.metaKey ||
+ event.altKey ||
+ event.target.matches("input,textarea,select,[contenteditable='true']")
+ ) {
+ return;
+ }
+ const key = event.key.toLowerCase();
+ if (key === " " || key === "k") {
+ event.preventDefault();
+ playPauseButton.click();
+ } else if (key === "arrowleft") {
+ event.preventDefault();
+ audioPlayer.currentTime = Math.max(0, audioPlayer.currentTime - 5);
+ } else if (key === "arrowright") {
+ event.preventDefault();
+ audioPlayer.currentTime += 5;
+ } else if (event.shiftKey && key === "n") {
+ event.preventDefault();
+ playNextInQueue({ manual: true });
+ } else if (event.shiftKey && key === "p") {
+ event.preventDefault();
+ playPreviousInQueue();
+ }
+});
+
+audioPlayer?.addEventListener("play", () => {
+ updatePlayPauseButton();
+ updateBrowserTabTitle();
+ if ("mediaSession" in navigator) navigator.mediaSession.playbackState = "playing";
+ savePlayerState();
+});
+
+audioPlayer?.addEventListener("pause", () => {
+ updatePlayPauseButton();
+ if ("mediaSession" in navigator) navigator.mediaSession.playbackState = "paused";
+ savePlayerState();
+});
+
+let seekBufferingUntil = 0;
+
+seekBar?.addEventListener("input", () => {
+ if (!audioPlayer?.src) return;
+ seekBufferingUntil = Date.now() + 1200;
+ audioPlayer.currentTime = Number(seekBar.value);
+ updateProgress();
+ updatePlayPauseButton();
+});
+
+audioPlayer?.addEventListener("seeking", () => {
+ seekBufferingUntil = Date.now() + 1200;
+ updatePlayPauseButton();
+});
+
+audioPlayer?.addEventListener("seeked", () => {
+ seekBufferingUntil = 0;
+ updatePlayPauseButton();
+});
+
+audioPlayer?.addEventListener("waiting", updatePlayPauseButton);
+audioPlayer?.addEventListener("stalled", updatePlayPauseButton);
+audioPlayer?.addEventListener("canplay", updatePlayPauseButton);
+audioPlayer?.addEventListener("canplaythrough", updatePlayPauseButton);
+audioPlayer?.addEventListener("playing", updatePlayPauseButton);
+audioPlayer?.addEventListener("pause", updatePlayPauseButton);
+audioPlayer?.addEventListener("loadedmetadata", updatePlayPauseButton);
+
+audioPlayer?.addEventListener("timeupdate", () => {
+ updateProgress();
+ savePlayerState();
+});
+
+audioPlayer?.addEventListener("loadedmetadata", updateProgress);
+
+
+audioPlayer?.addEventListener("ended", () => {
+ if (seekBar) {
+ seekBar.value = 0;
+ seekBar.style.setProperty("--progress", "0%");
+ }
+ if (currentTimeLabel) currentTimeLabel.textContent = "0:00";
+ updatePlayPauseButton();
+ if ("mediaSession" in navigator) navigator.mediaSession.playbackState = "none";
+ schedulePlayerStateSaveLowPriority();
+ playNextInQueue({ fromEnded: true });
+});
+
+audioPlayer?.addEventListener("error", () => {
+ if (audioPlayer.src) setStatus("audio unavailable");
+});
+
+function clampAudioTime(value) {
+ const duration = Number.isFinite(audioPlayer?.duration) ? audioPlayer.duration : 0;
+ return Math.min(Math.max(Number(value) || 0, 0), duration);
+}
+
+function updateQueueControlButtons() {
+ updateQueueControls();
+ renderQueue();
+}
+
+getAnonymousId();
+loadSeekTheme();
+audioPlayer.volume = Number(localStorage.getItem(VOLUME_KEY) ?? 1);
+if (volumeSlider) volumeSlider.value = String(Math.round(audioPlayer.volume * 100));
+updateQueueControls();
+updatePlayPauseButton();
+installMediaSessionControls();
+
+loadFavoriteIds().finally(() => {
+ const shareMatch = window.location.pathname.match(/^\/share\/([a-f0-9]{32})$/);
+ if (shareMatch) loadPlaylistDetail(shareMatch[1], true);
+ else restorePlayerState();
+});