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