(function () { const style = ` #spotui-tui { position: fixed; top: 0; left: 0; right: 0; bottom: 90px; width: 100vw; background: #000; color: #ddd; font-family: "JetBrains Mono", "Fira Code", monospace; font-size: 15px; padding: 40px; box-sizing: border-box; z-index: 9999; display: flex; flex-direction: column; overflow-x: hidden; user-select: text; cursor: text; } #spotui-logo { position: absolute; left: 50%; top: 41%; transform: translate(-50%, -50%); color: #ff8c42; opacity: 0.28; white-space: pre; text-align: center; font-family: "JetBrains Mono", "Fira Code", monospace; font-size: 28px; line-height: 1.0; pointer-events: none; user-select: none; z-index: 0; } #spotui-output { flex: 1 1 auto; display: flex; flex-direction: column-reverse; white-space: pre-wrap; line-height: 1.6; user-select: text; overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none; position: relative; z-index: 1; transition: opacity 260ms ease, transform 260ms ease; } body.spotui-command-mode #spotui-output { display: none !important; } body.spotui-cli-mode #spotui-output { display: flex !important; } #spotui-output::-webkit-scrollbar { width: 0; height: 0; } #spotui-footer { display: flex; align-items: center; gap: 12px; padding-top: 12px; margin-top: auto; border-top: 1px solid rgba(255, 140, 66, 0.18); position: relative; z-index: 1; transition: opacity 260ms ease, transform 260ms ease; } #spotui-input { background: transparent; border: none; outline: none; color: #ff8c42; font-family: inherit; font-size: inherit; flex: 1 1 auto; min-width: 0; } .prompt { color: #ff8c42; } .cl-line, .result { margin-bottom: 8px; user-select: text; } .result { padding: 5px; } .selected { background: #ff8c42; color: #000; } #spotui-controls { display: flex; gap: 8px; align-items: center; margin-left: auto; } .spotui-control-btn { background: #ff8c42; color: #000; border: none; padding: 6px 12px; font-family: "JetBrains Mono", monospace; font-size: 13px; cursor: pointer; border-radius: 4px; } .spotui-control-btn:hover { background: #e07b39; } body.spotui-tui-hidden #spotui-tui { display: none !important; } body.spotui-tui-hidden #spotui-popup { display: none !important; } #spotui-popup { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: min(480px, calc(100vw - 32px)); max-height: 60vh; background: #0a0a0a; border: 2px solid #ff8c42; border-radius: 6px; color: #ddd; font-family: "JetBrains Mono", "Fira Code", monospace; font-size: 14px; z-index: 10002; padding: 16px; overflow-y: auto; outline: none; box-shadow: 0 0 24px rgba(0,0,0,0.6); scrollbar-width: none; -ms-overflow-style: none; } #spotui-popup::-webkit-scrollbar { width: 0; height: 0; } .popup-title { color: #ff8c42; margin-bottom: 10px; font-size: 12px; border-bottom: 1px solid #333; padding-bottom: 8px; } .popup-row { padding: 4px 6px; } .popup-row.selected { background: #ff8c42; color: #000; } .popup-input { width: 100%; background: #111; border: 1px solid #ff8c42; color: #ff8c42; font-family: inherit; font-size: inherit; padding: 6px; outline: none; box-sizing: border-box; } `; const SPOTUI_ASCII_ART = [ " ██████ ██▓███ ▒█████ ▄▄▄█████▓ █ ██ ██▓", "▒██ ▒ ▓██░ ██▒▒██▒ ██▒▓ ██▒ ▓▒ ██ ▓██▒▓██▒", "░ ▓██▄ ▓██░ ██▓▒▒██░ ██▒▒ ▓██░ ▒░▓██ ▒██░▒██▒", " ▒ ██▒▒██▄█▓▒ ▒▒██ ██░░ ▓██▓ ░ ▓▓█ ░██░░██░", "▒██████▒▒▒██▒ ░ ░░ ████▓▒░ ▒██▒ ░ ▒▒█████▓ ░██░", "▒ ▒▓▒ ▒ ░▒▓▒░ ░ ░░ ▒░▒░▒░ ▒ ░░ ░▒▓▒ ▒ ▒ ░▓ ", "░ ░▒ ░ ░░▒ ░ ░ ▒ ▒░ ░ ░░▒░ ░ ░ ▒ ░", "░ ░ ░ ░░ ░ ░ ░ ▒ ░ ░░░ ░ ░ ▒ ░", " ░ ░ ░ ░ ░ ", " ", ]; let tuiMode = "command"; let results = []; let selected = 0; let lyricsObserver = null; function injectStyle() { const s = document.createElement("style"); s.textContent = style; document.head.appendChild(s); } function setTuiMode(mode) { tuiMode = mode === "cli" ? "cli" : "command"; document.body.classList.toggle("spotui-cli-mode", tuiMode === "cli"); document.body.classList.toggle("spotui-command-mode", tuiMode !== "cli"); } function createCopyButton() { const controls = document.createElement("div"); controls.id = "spotui-controls"; const copyBtn = document.createElement("button"); copyBtn.id = "copy-log-btn"; copyBtn.className = "spotui-control-btn"; copyBtn.textContent = "Copy log"; copyBtn.addEventListener("click", () => { const output = document.getElementById("spotui-output"); if (output) { const text = output.innerText; navigator.clipboard.writeText(text).then(() => { copyBtn.textContent = "Copied!"; setTimeout(() => { copyBtn.textContent = "Copy log"; }, 1500); }).catch(() => { alert("Copy failed. Please manually select and copy."); }); } }); const hideBtn = document.createElement("button"); hideBtn.id = "hide-tui-btn"; hideBtn.className = "spotui-control-btn"; hideBtn.textContent = "Hide TUI"; hideBtn.addEventListener("click", () => { const hidden = document.body.classList.toggle("spotui-tui-hidden"); hideBtn.textContent = hidden ? "Show TUI" : "Hide TUI"; }); const spotifyBtn = document.createElement("button"); spotifyBtn.id = "enable-spotify-btn"; spotifyBtn.className = "spotui-control-btn"; spotifyBtn.textContent = "Enable Spotify"; spotifyBtn.addEventListener("click", () => { const enabled = document.body.classList.toggle("spotui-spotify-enabled"); if (enabled) { document.body.classList.add("spotui-tui-hidden"); hideBtn.textContent = "Show TUI"; spotifyBtn.textContent = "Disable Spotify"; } else { spotifyBtn.textContent = "Enable Spotify"; document.body.classList.remove("spotui-tui-hidden"); document.body.classList.remove("spotui-search-mode"); hideBtn.textContent = "Hide TUI"; } }); controls.appendChild(copyBtn); controls.appendChild(hideBtn); controls.appendChild(spotifyBtn); const footer = document.getElementById("spotui-footer"); (footer || document.body).appendChild(controls); const backBtn = document.createElement("button"); backBtn.id = "spotui-back-btn"; backBtn.className = "spotui-control-btn"; backBtn.textContent = "Back"; backBtn.addEventListener("click", () => { document.body.classList.remove("spotui-search-mode"); document.body.classList.remove("spotui-spotify-enabled"); document.body.classList.remove("spotui-tui-hidden"); spotifyBtn.textContent = "Enable Spotify"; hideBtn.textContent = "Hide TUI"; syncLyricsState(); }); document.body.appendChild(backBtn); } function detectLyricsSurface() { return Boolean( document.querySelector( ".main-nowPlayingView-lyricsContent, .main-lyricsCinema-container, .lyrics-lyricsContainer-LyricsContainer" ) ); } function syncLyricsState() { if (!document.body) return; document.body.classList.toggle("spotui-lyrics-open", detectLyricsSurface()); } function hookLyricsButton() { const button = document.querySelector(".main-nowPlayingBar-lyricsButton"); if (!button || button.dataset.spotuiTuiLyricsHooked === "1") return; button.dataset.spotuiTuiLyricsHooked = "1"; button.addEventListener( "click", () => { setTimeout(syncLyricsState, 50); setTimeout(syncLyricsState, 250); setTimeout(syncLyricsState, 1000); }, true ); } function initLyricsBridge() { if (!document.body) { setTimeout(initLyricsBridge, 250); return; } const refresh = () => { hookLyricsButton(); syncLyricsState(); }; refresh(); if (!lyricsObserver) { lyricsObserver = new MutationObserver(refresh); lyricsObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style"], }); window.addEventListener( "beforeunload", () => { lyricsObserver?.disconnect(); }, { once: true } ); } } function createTerminal() { const box = document.createElement("div"); box.id = "spotui-tui"; setTuiMode("command"); box.innerHTML = `
SpoTUI v1.0 Type /help
`; document.body.appendChild(box); const input = document.getElementById("spotui-input"); input.addEventListener("keydown", async (e) => { if (e.key === "Enter") { const cmd = input.value.trim(); input.value = ""; print("> " + cmd); await execute(cmd); } if (e.key === "ArrowDown" && results.length) { selected = Math.min(selected + 1, results.length - 1); renderResults(); } if (e.key === "ArrowUp" && results.length) { selected = Math.max(selected - 1, 0); renderResults(); } }); } function print(text) { if (tuiMode !== "cli") return; const output = document.getElementById("spotui-output"); const line = document.createElement("div"); line.className = "cl-line"; line.textContent = text; output.prepend(line); output.scrollTop = 0; } async function execute(cmd) { const rawCmd = cmd.trim(); const cleanedCmd = rawCmd.startsWith("/") || rawCmd.startsWith(".") ? rawCmd.slice(1).trim() : rawCmd; const [command, ...args] = cleanedCmd.split(/\s+/); const argText = args.join(" ").trim(); if (command === "tui") { if (args[0] === "-m") { const mode = args[1]; if (!mode || (mode !== "cli" && mode !== "command")) { print("Usage: tui -m [command|cli]"); return; } setTuiMode(mode); print(`TUI mode: ${mode}`); return; } print("Usage: tui -m [command|cli]"); return; } if (command === "help") { popupMode = "help"; renderPopup(); return; } if (command === "clear") { document.getElementById("spotui-output").textContent = ""; return; } if (command === "playlist") { await openPlaylistPopup(); return; } if (command === "list") { await openPlaylistTracksPopup(); return; } if (command === "queue") { await openQueuePopup(); return; } if (command === "play") { try { const wasPlaying = Spicetify.Player.isPlaying(); if (!wasPlaying) { Spicetify.Player.togglePlay(); } print("Playing"); } catch (err) { print("Play error: " + err.message); } return; } if (command === "pause") { try { const wasPlaying = Spicetify.Player.isPlaying(); if (wasPlaying) { Spicetify.Player.togglePlay(); } print("Paused"); } catch (err) { print("Pause error: " + err.message); } return; } if (command === "p") { try { const wasPlaying = Spicetify.Player.isPlaying(); Spicetify.Player.togglePlay(); print(wasPlaying ? "Paused" : "Playing"); } catch (err) { print("Play/pause error: " + err.message); } return; } if (command === "search") { document.body.classList.add("spotui-search-mode"); document.body.classList.add("spotui-tui-hidden"); syncLyricsState(); return; } if (command === "skip") { try { Spicetify.Player.next(); print("Skipped to next track"); } catch (err) { print("Skip error: " + err.message); } return; } if (command === "back") { try { Spicetify.Player.back(); print("Went back to previous track"); } catch (err) { print("Back error: " + err.message); } return; } if (command === "volume" || command === "v") { try { if (!argText) { print("Usage: volume <0-100>"); return; } const percent = Number(argText); if (!Number.isFinite(percent) || percent < 0 || percent > 100) { print("Volume must be between 0 and 100."); return; } Spicetify.Player.setVolume(percent / 100); print(`Volume is ${Math.round(percent)}%`); } catch (err) { print("Volume error: " + err.message); } return; } if (command === "shuffle") { try { const current = Spicetify.Player.getShuffle(); Spicetify.Player.setShuffle(!current); print("Shuffle: " + (!current ? "ON" : "OFF")); } catch (err) { print("Shuffle error: " + err.message); } return; } if (command === "loop") { handleRepeatCommand("loop", argText); return; } if (command === "superloop") { handleRepeatCommand("superloop", argText); return; } print("Unknown command. Type /help"); } function renderResults() { const output = document.getElementById("spotui-output"); output.textContent = ""; results.forEach((item, idx) => { const line = document.createElement("div"); line.className = "result" + (idx === selected ? " selected" : ""); line.textContent = `${idx + 1}. ${item.name}${item.artist ? " - " + item.artist : ""}`; output.appendChild(line); }); } let popup = null; let popupMode = "list"; let playlists = []; let popupSelected = 0; let popupTracks = []; let popupTrackSelected = 0; let popupTrackTitle = ""; let popupTrackContext = null; let lastPlaylistContextUri = null; let lastQueueSnapshot = null; function getTrackTitle(track, index = 0) { const meta = track?.metadata || track?.contextTrack?.metadata || {}; return track?.name || track?.title || meta.title || meta.name || `Track ${index + 1}`; } function getTrackArtist(track) { const meta = track?.metadata || track?.contextTrack?.metadata || {}; if (track?.artist) return track.artist; if (Array.isArray(track?.artists) && track.artists.length) { return track.artists.map((artist) => artist?.name).filter(Boolean).join(", "); } if (meta.artist_name) return meta.artist_name; if (meta["artist_name:1"]) return meta["artist_name:1"]; return ""; } function normalizeTrackItem(track, index = 0) { const uri = track?.uri || track?.contextTrack?.uri || ""; return { uri, name: getTrackTitle(track, index), artist: getTrackArtist(track), }; } function dedupeTracks(tracks) { const seen = new Set(); const out = []; for (const track of tracks) { if (!track?.uri || seen.has(track.uri)) continue; seen.add(track.uri); out.push(track); } return out; } function findTrackIndexByUri(tracks, uri) { if (!uri) return -1; return tracks.findIndex((track) => track?.uri === uri); } function getCurrentPlaylistContextUri() { const current = Spicetify.Player.data?.context?.uri; if (current && Spicetify.URI.isPlaylistV1OrV2(current)) return current; return lastPlaylistContextUri; } function getPlaylistId(uri) { try { return Spicetify.URI.fromString(uri)?.id ?? uri.split(":").pop(); } catch { return uri.split(":").pop(); } } function handleRepeatCommand(kind, arg) { try { const current = Spicetify.Player.getRepeat(); const targetMode = kind === "loop" ? 1 : 2; let nextMode = targetMode; if (arg === "on") { nextMode = targetMode; } else if (arg === "off") { nextMode = 0; } else if (arg === "") { nextMode = current === targetMode ? 0 : targetMode; } else { print(`Usage: /${kind} [on|off]`); return; } Spicetify.Player.setRepeat(nextMode); print(`${kind === "loop" ? "Loop" : "Superloop"}: ${nextMode === 0 ? "OFF" : "ON"}`); } catch (err) { print(`${kind === "loop" ? "Loop" : "Superloop"} error: ${err.message}`); } } async function getPlaylists() { const rootlist = await Spicetify.Platform.RootlistAPI.getContents(); const list = []; function flatten(items) { for (const item of items) { if (item.type === "playlist") { list.push({ name: item.name, uri: item.uri }); } else if (item.type === "folder" && item.items) { flatten(item.items); } } } flatten(rootlist.items); return list; } async function openPlaylistPopup() { try { playlists = await getPlaylists(); } catch (err) { print("Playlist error: " + err.message); return; } popupSelected = 0; popupMode = "list"; renderPopup(); } async function openPlaylistTracksPopup() { try { const contextUri = getCurrentPlaylistContextUri(); if (!contextUri || !Spicetify.URI.isPlaylistV1OrV2(contextUri)) { print("List error: not currently playing a playlist."); return; } const res = await Spicetify.Platform.PlaylistAPI.getContents(contextUri, { limit: 9999999, }); popupTracks = (res.items || []) .filter((item) => item && item.uri && item.isPlayable !== false) .map((item, index) => normalizeTrackItem(item, index)); popupTracks = dedupeTracks(popupTracks); popupTrackTitle = "Playlist tracks"; popupTrackContext = Spicetify.Player.data?.context || { uri: contextUri }; lastPlaylistContextUri = contextUri; const currentTrackUri = Spicetify.Player.data?.item?.uri; popupTrackSelected = Math.max(findTrackIndexByUri(popupTracks, currentTrackUri), 0); popupMode = "tracks"; renderPopup(); } catch (err) { print("List error: " + err.message); console.error("Playlist tracks popup error:", err); } } function getQueueTracks() { const queue = Spicetify.Queue || {}; const tracks = []; if (queue.track && queue.track.uri) { tracks.push(queue.track); } else if (Spicetify.Player.data?.item?.uri) { tracks.push(Spicetify.Player.data.item); } for (const item of queue.nextTracks || []) { const track = item?.contextTrack || item?.track || item; if (!track?.uri || track.uri === "spotify:delimiter") continue; tracks.push(track); } const currentTrackContextUri = Spicetify.Player.data?.context_uri || Spicetify.Player.data?.context?.uri || queue.track?.contextTrack?.metadata?.context_uri || queue.track?.metadata?.context_uri || null; popupTrackContext = currentTrackContextUri ? { uri: currentTrackContextUri } : Spicetify.Player.data?.context || null; return dedupeTracks(tracks.map((track, index) => normalizeTrackItem(track, index))); } function openQueuePopup() { const currentQueueTracks = getQueueTracks(); if (!lastQueueSnapshot || lastQueueSnapshot.length === 0) { lastQueueSnapshot = currentQueueTracks; } popupTracks = lastQueueSnapshot.length ? lastQueueSnapshot : currentQueueTracks; popupTrackTitle = "Queue"; const currentTrackUri = Spicetify.Player.data?.item?.uri; popupTrackSelected = Math.max(findTrackIndexByUri(popupTracks, currentTrackUri), 0); popupMode = "tracks"; renderPopup(); } function closePopup() { if (popup) { popup.remove(); popup = null; } popupMode = "list"; popupTracks = []; popupTrackSelected = 0; popupTrackTitle = ""; popupTrackContext = null; const input = document.getElementById("spotui-input"); if (input) input.focus(); } function renderPopup() { if (popup) popup.remove(); popup = document.createElement("div"); popup.id = "spotui-popup"; popup.tabIndex = 0; if (popupMode === "list") { popup.innerHTML = ``; document.body.appendChild(popup); const listEl = popup.querySelector("#popup-list"); if (!playlists.length) { const row = document.createElement("div"); row.className = "popup-row"; row.textContent = "No playlists found."; listEl.appendChild(row); } else { playlists.forEach((p, idx) => { const row = document.createElement("div"); row.className = "popup-row" + (idx === popupSelected ? " selected" : ""); row.textContent = `${idx + 1}. ${p.name}`; listEl.appendChild(row); }); } popup.addEventListener("keydown", handleListKeydown); popup.focus(); } else if (popupMode === "new") { popup.innerHTML = ``; document.body.appendChild(popup); const inputEl = popup.querySelector("#popup-input"); inputEl.focus(); inputEl.addEventListener("keydown", handleNewKeydown); } else if (popupMode === "tracks") { popup.innerHTML = ``; document.body.appendChild(popup); const listEl = popup.querySelector("#popup-list"); if (!popupTracks.length) { const row = document.createElement("div"); row.className = "popup-row"; row.textContent = "No tracks found."; listEl.appendChild(row); } else { popupTracks.forEach((track, idx) => { const row = document.createElement("div"); row.className = "popup-row" + (idx === popupTrackSelected ? " selected" : ""); row.textContent = `${idx + 1}. ${track.name}${track.artist ? " - " + track.artist : ""}`; listEl.appendChild(row); }); } popup.addEventListener("keydown", handleTrackListKeydown); popup.focus(); scrollSelectedPopupRowIntoView(); } else if (popupMode === "help") { popup.innerHTML = ``; document.body.appendChild(popup); const listEl = popup.querySelector("#popup-list"); [ "tui -m [command|cli]", "playlist", "list", "queue", "play", "pause", "p", "skip", "back", "v ", "volume ", "shuffle", "loop [on|off]", "superloop [on|off]", "search", "clear", ].forEach((item) => { const row = document.createElement("div"); row.className = "popup-row"; row.textContent = item; listEl.appendChild(row); }); popup.addEventListener("keydown", handleHelpKeydown); popup.focus(); } } function scrollSelectedPopupRowIntoView() { requestAnimationFrame(() => { const selectedRow = popup?.querySelector(".popup-row.selected"); if (selectedRow?.scrollIntoView) { selectedRow.scrollIntoView({ block: "nearest" }); } }); } function handleListKeydown(e) { if (e.key === "ArrowDown") { e.preventDefault(); if (playlists.length) popupSelected = (popupSelected + 1) % playlists.length; renderPopup(); } else if (e.key === "ArrowUp") { e.preventDefault(); if (playlists.length) popupSelected = (popupSelected - 1 + playlists.length) % playlists.length; renderPopup(); } else if (e.key === "Enter") { e.preventDefault(); const p = playlists[popupSelected]; if (p) { lastPlaylistContextUri = p.uri; Spicetify.Player.playUri(p.uri); print("Playing playlist: " + p.name); } closePopup(); } else if (e.key === "n" || e.key === "N") { e.preventDefault(); popupMode = "new"; renderPopup(); } else if (e.key === "Escape") { e.preventDefault(); closePopup(); } scrollSelectedPopupRowIntoView(); } function handleTrackListKeydown(e) { if (e.key === "ArrowDown") { e.preventDefault(); if (popupTracks.length) popupTrackSelected = (popupTrackSelected + 1) % popupTracks.length; renderPopup(); } else if (e.key === "ArrowUp") { e.preventDefault(); if (popupTracks.length) popupTrackSelected = (popupTrackSelected - 1 + popupTracks.length) % popupTracks.length; renderPopup(); } else if (e.key === "Enter") { e.preventDefault(); const track = popupTracks[popupTrackSelected]; if (track?.uri) { Spicetify.Player.playUri(track.uri, popupTrackContext || undefined); print("Playing: " + track.name); } if (popupTrackTitle === "Queue") { lastQueueSnapshot = popupTracks.slice(); } closePopup(); } else if (e.key === "Escape") { e.preventDefault(); closePopup(); } scrollSelectedPopupRowIntoView(); } function handleHelpKeydown(e) { if (e.key === "Escape" || e.key === "Enter") { e.preventDefault(); closePopup(); } } async function handleNewKeydown(e) { if (e.key === "Enter") { e.preventDefault(); const name = e.target.value.trim(); if (!name) return; try { await Spicetify.Platform.RootlistAPI.createPlaylist(name, {}); print("Created playlist: " + name); } catch (err) { print("Create failed: " + err.message + " (check console)"); console.error("Create playlist error:", err); } playlists = await getPlaylists(); popupSelected = 0; popupMode = "list"; renderPopup(); } else if (e.key === "Escape") { e.preventDefault(); popupMode = "list"; renderPopup(); } } injectStyle(); setTimeout(createCopyButton, 500); setTimeout(initLyricsBridge, 1000); if (Spicetify?.Platform) { createTerminal(); } else { setTimeout(createTerminal, 1500); } })();