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();