feat(downloads): add check box for atmos, and fallback to desired quality if unavailable
This commit is contained in:
parent
6e517fcb9b
commit
9c34dd4b9d
4 changed files with 68 additions and 11 deletions
11
index.html
11
index.html
|
|
@ -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>
|
||||
|
|
|
|||
33
js/api.js
33
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue