diff --git a/index.html b/index.html index 7a7f1a9..e6fde5e 100644 --- a/index.html +++ b/index.html @@ -4166,7 +4166,6 @@ Quality for track downloads + + +
Lossless Container diff --git a/js/api.js b/js/api.js index 08b6829..8e968f3 100644 --- a/js/api.js +++ b/js/api.js @@ -10,7 +10,7 @@ import { getTrackDiscNumber, getMimeType, } from './utils.js'; -import { trackDateSettings } from './storage.js'; +import { preferDolbyAtmosSettings, trackDateSettings } from './storage.js'; import { APICache } from './cache.js'; import { DashDownloader } from './dash-downloader.ts'; import { HlsDownloader } from './hls-downloader.js'; @@ -1467,7 +1467,7 @@ export class LosslessAPI { return result; } - async getStreamUrl(id, quality = 'HI_RES_LOSSLESS') { + async getStreamUrl(id, quality = 'HI_RES_LOSSLESS', download = false) { const cacheKey = `stream_info_${id}_${quality}`; if (this.streamCache.has(cacheKey)) { @@ -1514,7 +1514,7 @@ export class LosslessAPI { paramsArray.push(['formats', 'AACLC']); paramsArray.push(['formats', 'FLAC_HIRES']); paramsArray.push(['formats', 'FLAC']); - } else if (quality === 'DOLBY_ATMOS' && canPlayAtmos) { + } else if (quality === 'DOLBY_ATMOS' && (canPlayAtmos || download)) { paramsArray.push(['formats', 'EAC3_JOC']); } else { // Default fallback or "auto" behavior @@ -1522,7 +1522,7 @@ export class LosslessAPI { paramsArray.push(['formats', 'AACLC']); paramsArray.push(['formats', 'FLAC']); paramsArray.push(['formats', 'FLAC_HIRES']); - if (canPlayAtmos) { + if (canPlayAtmos || download) { paramsArray.push(['formats', 'EAC3_JOC']); } } @@ -1635,6 +1635,10 @@ export class LosslessAPI { } async enrichTrack(input, { downloadQuality = 'HI_RES_LOSSLESS' }) { + if (downloadQuality == 'DOLBY_ATMOS' && !input?.audioModes?.includes('DOLBY_ATMOS')) { + downloadQuality = 'LOSSLESS'; + } + const id = input?.id || input; const track = typeof input === 'object' ? input : await this.getTrack(id, downloadQuality); const isVideo = track?.type?.toLowerCase().includes('video'); @@ -1725,7 +1729,7 @@ export class LosslessAPI { * * @throws {Error} If stream URL cannot be resolved, manifest is missing, or download fails * @throws {AbortError} If the download is aborted via the signal - * @throws {MP3EncodingError|FfmpegError} If audio transcoding fails + * @throws {FfmpegError} If audio transcoding fails */ async downloadTrack(id, quality = 'HI_RES_LOSSLESS', filename, options = {}) { // Load ffmpeg in the background. @@ -1738,7 +1742,7 @@ export class LosslessAPI { try { // Custom FFMPEG formats are not native TIDAL qualities; download LOSSLESS and transcode - const downloadQuality = isCustomFormat(quality) ? 'LOSSLESS' : quality; + let downloadQuality = isCustomFormat(quality) ? 'LOSSLESS' : quality; const { lookup, enrichedTrack, isVideo } = await this.enrichTrack(track, { downloadQuality }); @@ -1768,9 +1772,22 @@ export class LosslessAPI { throw new Error('Could not resolve manifest'); } - streamUrl = this.extractStreamUrlFromManifest(manifest); + if (preferDolbyAtmosSettings.isEnabled() && track.audioModes?.includes('DOLBY_ATMOS')) { + try { + const stream = await this.getStreamUrl(id, 'DOLBY_ATMOS', true); + const manifest = await fetch(stream.url); + const manifestText = await manifest.text(); + streamUrl = this.extractStreamUrlFromManifest(btoa(manifestText)); + } catch (err) { + console.error('Failed to extract Dolby Atmos stream URL:', err); + } + } + if (!streamUrl) { - throw new Error('Could not resolve stream URL'); + streamUrl = this.extractStreamUrlFromManifest(manifest); + if (!streamUrl) { + throw new Error('Could not resolve stream URL'); + } } } diff --git a/js/settings.js b/js/settings.js index 741fbb9..6519098 100644 --- a/js/settings.js +++ b/js/settings.js @@ -34,6 +34,7 @@ import { gaplessPlaybackSettings, analyticsSettings, modalSettings, + preferDolbyAtmosSettings, } from './storage.js'; import { audioContextManager, EQ_PRESETS } from './audio-context.js'; import { db } from './db.js'; @@ -830,7 +831,6 @@ export async function initializeSettings(scrobbler, player, api, ui) { if (downloadQualitySetting) { // Assign categories to the static (native) options already in the HTML const staticCategories = { - DOLBY_ATMOS: 'Spatial', HI_RES_LOSSLESS: 'Lossless', LOSSLESS: 'Lossless', HIGH: 'AAC', @@ -857,7 +857,7 @@ export async function initializeSettings(scrobbler, player, api, ui) { const m = text.match(/(\d+)\s*kbps/i); return m ? parseInt(m[1], 10) : Infinity; }; - const categoryOrder = ['Spatial', 'Lossless', 'AAC', 'MP3', 'OGG']; + const categoryOrder = ['Lossless', 'AAC', 'MP3', 'OGG']; allOptions.sort((a, b) => { if (a.category == b.category && a.category === 'Lossless') return 0; // Preserve original order for lossless options const ai = categoryOrder.indexOf(a.category); @@ -895,6 +895,14 @@ export async function initializeSettings(scrobbler, player, api, ui) { }); } + const prefersAtmosSetting = document.getElementById('dolby-atmos-toggle'); + if (prefersAtmosSetting) { + prefersAtmosSetting.checked = preferDolbyAtmosSettings.isEnabled(); + prefersAtmosSetting.addEventListener('change', (e) => { + preferDolbyAtmosSettings.setEnabled(e.target.checked); + }); + } + const losslessContainerSetting = document.getElementById('lossless-container-setting'); const losslessContainerSettingItem = losslessContainerSetting?.closest('.setting-item'); diff --git a/js/storage.js b/js/storage.js index 6f8a0a4..69780c8 100644 --- a/js/storage.js +++ b/js/storage.js @@ -648,6 +648,14 @@ export const downloadQualitySettings = { this.setQuality('FFMPEG_MP3_320'); return 'FFMPEG_MP3_320'; } + + // Migrate legacy atmos value + if (stored === 'DOLBY_ATMOS') { + this.setQuality('HI_RES_LOSSLESS'); + preferDolbyAtmosSettings.setEnabled(true); + return 'HI_RES_LOSSLESS'; + } + return stored; } catch { return 'HI_RES_LOSSLESS'; @@ -658,6 +666,21 @@ export const downloadQualitySettings = { }, }; +export const preferDolbyAtmosSettings = { + STORAGE_KEY: 'prefer-dolby-atmos', + isEnabled() { + try { + const stored = localStorage.getItem(this.STORAGE_KEY) || 'false'; + return stored === 'true'; + } catch { + return false; + } + }, + setEnabled(enabled) { + localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false'); + }, +}; + export const losslessContainerSettings = { STORAGE_KEY: 'lossless-container', getContainer() {