feat(downloads): add check box for atmos, and fallback to desired quality if unavailable

This commit is contained in:
Daniel 2026-03-26 12:21:26 -05:00
parent 6e517fcb9b
commit 9c34dd4b9d
4 changed files with 68 additions and 11 deletions

View file

@ -4166,7 +4166,6 @@
<span class="description">Quality for track downloads</span>
</div>
<select id="download-quality-setting">
<option value="DOLBY_ATMOS">Dolby Atmos (MP4)</option>
<option value="HI_RES_LOSSLESS">Hi-Res Lossless (24-bit)</option>
<option value="LOSSLESS">Lossless (16-bit)</option>
<option value="HIGH">AAC 320kbps</option>
@ -4182,6 +4181,16 @@
</div>
</div>
</div>
<div class="setting-item">
<div class="info">
<span class="label">Dolby Atmos</span>
<span class="description">Prefer Dolby Atmos tracks when available</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="dolby-atmos-toggle" />
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<div class="info">
<span class="label">Lossless Container</span>

View file

@ -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');
}
}
}

View file

@ -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');

View file

@ -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() {