|
|
|
|
@@ -1367,7 +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 { initAiSectionWithLocalConfig } from './js/ai-ui.mjs';
|
|
|
|
|
import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
|
|
|
|
import { SimplePlayer } from './js/greyscale-player.mjs';
|
|
|
|
|
@@ -1617,6 +1616,21 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
|
|
|
|
return message.includes('authentication required');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function tryGetPubkeyWithoutPrompt({ attempts = 8, delayMs = 350 } = {}) {
|
|
|
|
|
for (let i = 0; i < attempts; i += 1) {
|
|
|
|
|
try {
|
|
|
|
|
const pubkey = await getPubkey();
|
|
|
|
|
if (pubkey) return pubkey;
|
|
|
|
|
} catch {
|
|
|
|
|
// Signer can attach slightly after initial script execution.
|
|
|
|
|
}
|
|
|
|
|
if (i < attempts - 1) {
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isReadOnlyTargetMode() {
|
|
|
|
|
return Boolean(targetPubkey && (!currentPubkey || targetPubkey !== currentPubkey));
|
|
|
|
|
}
|
|
|
|
|
@@ -1695,13 +1709,8 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
currentPubkey = await getPubkey();
|
|
|
|
|
isAuthenticated = Boolean(currentPubkey);
|
|
|
|
|
} catch {
|
|
|
|
|
currentPubkey = null;
|
|
|
|
|
isAuthenticated = false;
|
|
|
|
|
}
|
|
|
|
|
currentPubkey = await tryGetPubkeyWithoutPrompt();
|
|
|
|
|
isAuthenticated = Boolean(currentPubkey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function initializeAuthenticatedPageFeatures() {
|
|
|
|
|
@@ -4332,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 } = {}) {
|
|
|
|
|
|