diff --git a/www/js/version.json b/www/js/version.json index 788bb15..b52b07a 100644 --- a/www/js/version.json +++ b/www/js/version.json @@ -1,5 +1,5 @@ { - "VERSION": "v0.7.4", - "VERSION_NUMBER": "0.7.4", - "BUILD_DATE": "2026-04-19T18:00:32.861Z" + "VERSION": "v0.7.5", + "VERSION_NUMBER": "0.7.5", + "BUILD_DATE": "2026-04-19T18:31:26.580Z" } diff --git a/www/music.html b/www/music.html index 007c011..ccd1726 100644 --- a/www/music.html +++ b/www/music.html @@ -932,15 +932,6 @@ min-width: 52px; } - #musicSaveToggleBtn { - min-width: 52px; - } - - #musicSaveToggleBtn[data-enabled='true'] { - border-color: var(--accent-color); - color: var(--accent-color); - } - #musicProgressWrap { display: grid; grid-template-columns: auto 1fr auto; @@ -1226,7 +1217,6 @@
- @@ -1377,8 +1367,6 @@ import { HamburgerMorphing } from "./hamburger_morphing/hamburger.mjs"; import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs'; import { initBlossomSection, updateBlossomSection } from './js/blossom-ui.mjs'; - import { uploadToServer, getDefaultBlossomServer } from './js/blossom-api.mjs'; - import { initAiSectionWithLocalConfig } from './js/ai-ui.mjs'; import { GreyscaleAPI } from './js/greyscale-api.mjs'; import { SimplePlayer } from './js/greyscale-player.mjs'; @@ -1486,7 +1474,6 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs'; queueClearBtn: document.getElementById('musicQueueClearBtn'), queueList: document.getElementById('musicQueueList'), playbackModeBtn: document.getElementById('musicPlaybackModeBtn'), - saveToggleBtn: document.getElementById('musicSaveToggleBtn'), shareModal: document.getElementById('musicShareModal'), shareTarget: document.getElementById('musicShareTarget'), shareRendered: document.getElementById('musicShareRendered'), @@ -1543,7 +1530,6 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs'; }; let isHandlingRoute = false; let playbackMode = 'normal'; - let autoSaveToBlossom = false; let currentPlayingTrackId = ''; let pendingShareUrl = ''; let pendingShareArtUrl = ''; @@ -1850,14 +1836,6 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs'; return `music-queue:${currentPubkey || 'anon'}`; } - function getAutoSaveBlossomStorageKey() { - return `music-auto-save-blossom:${currentPubkey || 'anon'}`; - } - - function getSavedTrackBlossomMapStorageKey() { - return `music-saved-track-blossom-map:${currentPubkey || 'anon'}`; - } - function normalizeQueueTrack(track) { if (!track || typeof track !== 'object') return null; return { @@ -1985,64 +1963,6 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs'; } } - const savedTrackBlossomUrlById = new Map(); - const pendingTrackBlossomSaves = new Set(); - - function updateSaveToggleButton() { - if (!musicEls.saveToggleBtn) return; - musicEls.saveToggleBtn.dataset.enabled = autoSaveToBlossom ? 'true' : 'false'; - musicEls.saveToggleBtn.setAttribute('aria-pressed', autoSaveToBlossom ? 'true' : 'false'); - musicEls.saveToggleBtn.textContent = autoSaveToBlossom ? '💾✓' : '💾'; - musicEls.saveToggleBtn.title = autoSaveToBlossom - ? 'Auto-save now playing track to Blossom: ON' - : 'Auto-save now playing track to Blossom: OFF'; - } - - function loadAutoSaveBlossomSetting() { - try { - autoSaveToBlossom = localStorage.getItem(getAutoSaveBlossomStorageKey()) === 'true'; - } catch { - autoSaveToBlossom = false; - } - updateSaveToggleButton(); - } - - function persistAutoSaveBlossomSetting() { - try { - localStorage.setItem(getAutoSaveBlossomStorageKey(), autoSaveToBlossom ? 'true' : 'false'); - } catch { - // Ignore storage write failures. - } - } - - function loadSavedTrackBlossomMap() { - savedTrackBlossomUrlById.clear(); - try { - const raw = localStorage.getItem(getSavedTrackBlossomMapStorageKey()); - if (!raw) return; - const parsed = JSON.parse(raw); - if (!parsed || typeof parsed !== 'object') return; - Object.entries(parsed).forEach(([trackId, url]) => { - if (!trackId || typeof url !== 'string' || !url.trim()) return; - savedTrackBlossomUrlById.set(String(trackId), url.trim()); - }); - } catch (error) { - console.warn('[music] Could not load saved Blossom track map:', error); - } - } - - function persistSavedTrackBlossomMap() { - try { - const payload = {}; - savedTrackBlossomUrlById.forEach((url, trackId) => { - payload[trackId] = url; - }); - localStorage.setItem(getSavedTrackBlossomMapStorageKey(), JSON.stringify(payload)); - } catch { - // Ignore storage write failures. - } - } - const mountedMusicMenus = { myPlaylists: [], friendPlaylists: [], @@ -3502,7 +3422,6 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs'; updatePlayingResultHighlight(); saveQueueLocal(); setMusicStatus(`Playing queue: ${track.title}`, 'ok'); - void maybeAutoSaveTrackToBlossom(track); } catch (error) { console.error(error); setMusicStatus(`Queue playback failed: ${error.message || 'Unknown error'}`, 'error'); @@ -4422,14 +4341,70 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs'; async function fetchDownloadBlobForTrack(track) { if (!track?.id) throw new Error('Track not available for download.'); - const stream = await musicApi.getTrackStream(track.id, 'LOSSLESS'); - const streamUrl = stream?.streamUrl; - if (!streamUrl) throw new Error('No stream URL for download'); - const res = await fetch(streamUrl); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - const blob = await res.blob(); - const ext = extensionFromMime(blob.type); - return { blob, ext }; + + console.debug('[music][download] start', { + trackId: track.id, + title: track.title || '', + }); + + const qualityCandidates = ['HIGH', 'MP3_320', 'LOSSLESS']; + let lastError = null; + + for (const quality of qualityCandidates) { + try { + console.debug('[music][download] requesting stream', { trackId: track.id, quality }); + + const stream = await musicApi.getTrackStream(track.id, quality); + const streamUrl = stream?.streamUrl; + if (!streamUrl) { + lastError = new Error(`No stream URL for quality ${quality}`); + console.warn('[music][download] missing stream URL', { trackId: track.id, quality }); + continue; + } + + console.debug('[music][download] fetching stream URL', { + trackId: track.id, + quality, + streamUrl, + }); + + const res = await fetch(streamUrl); + if (!res.ok) { + lastError = new Error(`HTTP ${res.status} for quality ${quality}`); + console.warn('[music][download] fetch failed', { + trackId: track.id, + quality, + status: res.status, + statusText: res.statusText, + }); + continue; + } + + const blob = await res.blob(); + const ext = extensionFromMime(blob.type); + console.debug('[music][download] success', { + trackId: track.id, + quality, + mimeType: blob.type, + size: blob.size, + ext, + }); + return { blob, ext }; + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error || 'Unknown error')); + console.warn('[music][download] quality attempt threw', { + trackId: track.id, + quality, + error: lastError.message, + }); + } + } + + console.error('[music][download] all quality attempts failed', { + trackId: track.id, + error: lastError?.message || 'No stream URL for download', + }); + throw (lastError || new Error('No stream URL for download')); } async function downloadTrackFile(track, { suppressStatus = false } = {}) { @@ -4458,65 +4433,6 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs'; } } - async function saveTrackToBlossom(track, { suppressStatus = true } = {}) { - const trackId = String(track?.id || '').trim(); - if (!trackId) return ''; - if (savedTrackBlossomUrlById.has(trackId)) { - return savedTrackBlossomUrlById.get(trackId) || ''; - } - if (pendingTrackBlossomSaves.has(trackId)) return ''; - - const targetServer = getDefaultBlossomServer(); - if (!targetServer) { - if (!suppressStatus) setMusicStatus('No Blossom server configured.', 'error'); - return ''; - } - - pendingTrackBlossomSaves.add(trackId); - try { - if (!suppressStatus) { - setMusicStatus(`Saving to Blossom: ${track.title || 'Track'}...`, 'neutral'); - } - - const { blob, ext } = await fetchDownloadBlobForTrack(track); - const title = sanitizeFilenamePart(track.title || 'Unknown title') || 'Unknown title'; - const artist = sanitizeFilenamePart(track.artist || 'Unknown artist') || 'Unknown artist'; - const fileName = `${artist} - ${title}.${ext}`; - const file = new File([blob], fileName, { - type: blob.type || 'application/octet-stream', - }); - - const result = await uploadToServer(file, targetServer); - const sha256 = String(result?.sha256 || '').trim(); - if (!sha256) throw new Error('Upload returned no SHA-256 hash.'); - - const suffix = ext ? `.${ext.replace(/^\./, '')}` : ''; - const blossomUrl = `${String(result?.serverUrl || targetServer).replace(/\/$/, '')}/${sha256}${suffix}`; - - savedTrackBlossomUrlById.set(trackId, blossomUrl); - persistSavedTrackBlossomMap(); - - if (!suppressStatus) { - setMusicStatus(`Saved to Blossom: ${track.title || 'Track'}`, 'ok'); - } - - return blossomUrl; - } catch (error) { - console.error('[music] saveTrackToBlossom failed:', error); - if (!suppressStatus) { - setMusicStatus(`Save to Blossom failed: ${error.message || 'Unknown error'}`, 'error'); - } - return ''; - } finally { - pendingTrackBlossomSaves.delete(trackId); - } - } - - async function maybeAutoSaveTrackToBlossom(track) { - if (!autoSaveToBlossom) return; - await saveTrackToBlossom(track, { suppressStatus: true }); - } - async function downloadMusicByIndex(index) { if (!Number.isInteger(index) || index < 0 || index >= musicTracks.length) return; const track = musicTracks[index]; @@ -5105,16 +5021,9 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs'; updatePlayingResultHighlight(); }); musicEls.playbackModeBtn?.addEventListener('click', cyclePlaybackMode); - musicEls.saveToggleBtn?.addEventListener('click', () => { - autoSaveToBlossom = !autoSaveToBlossom; - persistAutoSaveBlossomSetting(); - updateSaveToggleButton(); - setMusicStatus(`Auto-save to Blossom ${autoSaveToBlossom ? 'enabled' : 'disabled'}.`, 'ok'); - }); setMusicPlayButtonState(); updateMusicPlayerMeta(null); updatePlaybackModeButton(); - updateSaveToggleButton(); musicEls.queueClearBtn?.addEventListener('click', clearQueue); @@ -5215,8 +5124,6 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs'; myPlaylists.clear(); } loadQueueLocal(); - loadAutoSaveBlossomSetting(); - loadSavedTrackBlossomMap(); updateViewerControls(); renderMyPlaylists(); renderFriendPlaylists();