diff --git a/index.html b/index.html index 0d46417..028c808 100644 --- a/index.html +++ b/index.html @@ -4063,6 +4063,7 @@ Auto (Adaptive) + Hi-Res Lossless (24-bit) Lossless (16-bit) AAC 320kbps AAC 96kbps diff --git a/js/api.js b/js/api.js index 15f2bad..75b0c8a 100644 --- a/js/api.js +++ b/js/api.js @@ -11,6 +11,7 @@ import { preferDolbyAtmosSettings, trackDateSettings, devModeSettings } from './ import { APICache } from './cache.js'; import { DashDownloader } from './dash-downloader.ts'; import { HlsDownloader } from './hls-downloader.js'; +import { getProxyUrl } from './proxy-utils.js'; import { loadFfmpeg, FfmpegError, ffmpeg } from './ffmpeg.js'; import { triggerDownload, applyAudioPostProcessing } from './download-utils.ts'; import { isCustomFormat } from './ffmpegFormats.ts'; @@ -117,7 +118,9 @@ export class LosslessAPI { for (let attempt = 1; attempt <= maxTotalAttempts; attempt++) { const instance = instances[instanceIndex % instances.length]; const baseUrl = typeof instance === 'string' ? instance : instance.url; - const url = baseUrl.endsWith('/') ? `${baseUrl}${relativePath.substring(1)}` : `${baseUrl}${relativePath}`; + const url = baseUrl.endsWith('/') + ? `${baseUrl}${relativePath.substring(1)}` + : `${baseUrl}${relativePath}`; try { const response = await fetch(url, { signal: options.signal }); @@ -134,7 +137,10 @@ export class LosslessAPI { } if (response.status === 401) { - const errorData = await response.clone().json().catch(() => null); + const errorData = await response + .clone() + .json() + .catch(() => null); if (errorData?.subStatus === 11002) { console.warn(`Auth failed on ${baseUrl}. Trying next instance...`); instanceIndex++; @@ -1763,7 +1769,7 @@ export class LosslessAPI { if (streamUrl.startsWith('blob:')) { try { const downloader = new DashDownloader(); - blob = await downloader.downloadDashStream(streamUrl, { + blob = await downloader.downloadDashStream(getProxyUrl(streamUrl), { signal: options.signal, onProgress, calculateDashBytes: calculateDashBytes ?? true, @@ -1782,7 +1788,7 @@ export class LosslessAPI { } else if (streamUrl.includes('.m3u8') || streamUrl.includes('application/vnd.apple.mpegurl')) { try { const downloader = new HlsDownloader(); - blob = await downloader.downloadHlsStream(streamUrl, { + blob = await downloader.downloadHlsStream(getProxyUrl(streamUrl), { signal: options.signal, onProgress, }); @@ -1807,7 +1813,7 @@ export class LosslessAPI { /* ignore HEAD failure; proceed with GET */ } - const response = await fetch(streamUrl, { + const response = await fetch(getProxyUrl(streamUrl), { cache: 'no-store', signal: options.signal, }); diff --git a/js/app.js b/js/app.js index 3b25c86..817722c 100644 --- a/js/app.js +++ b/js/app.js @@ -467,31 +467,7 @@ document.addEventListener('DOMContentLoaded', async () => { const audioPlayer = document.getElementById('audio-player'); // i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only playback - // Use isIos from platform-detection (set before UA spoof in index.html) so detection works on real iOS. - if (isIos || isSafari) { - const qualitySelect = document.getElementById('streaming-quality-setting'); - const downloadQualitySelect = document.getElementById('download-quality-setting'); - - const removeHiRes = (select) => { - if (!select) return; - const option = select.querySelector('option[value="HI_RES_LOSSLESS"]'); - if (option) option.remove(); - }; - - removeHiRes(qualitySelect); - removeHiRes(downloadQualitySelect); - - if (isIos) { - document.querySelector('#hi-res-download-warning').style.display = ''; - } - - const currentQualitySetting = localStorage.getItem('playback-quality'); - if (!currentQualitySetting || currentQualitySetting === 'HI_RES_LOSSLESS') { - localStorage.setItem('playback-quality', 'LOSSLESS'); - } - } - - const currentQuality = localStorage.getItem('playback-quality') || 'LOSSLESS'; + const currentQuality = localStorage.getItem('playback-quality') || 'HI_RES_LOSSLESS'; await Player.initialize(audioPlayer, MusicAPI.instance, currentQuality); // Initialize tracker diff --git a/js/dash-downloader.ts b/js/dash-downloader.ts index 7ac3a93..d328a1b 100644 --- a/js/dash-downloader.ts +++ b/js/dash-downloader.ts @@ -30,7 +30,7 @@ export class DashDownloader { await Promise.all( urls.map(async (url) => { - const result = await fetch(url, { method: 'HEAD', signal }); + const result = await fetch(getProxyUrl(url), { method: 'HEAD', signal }); if (result.ok) { const contentLength = result.headers.get('Content-Length'); @@ -75,7 +75,7 @@ export class DashDownloader { onProgress?.(new SegmentedDownloadProgress(downloadedBytes, totalSize ?? undefined, i, totalSegments)); - const url = urls[i]; + const url = getProxyUrl(urls[i]); const segmentResponse = await fetch(url, { signal }); if (!segmentResponse.ok) { diff --git a/js/hls-downloader.js b/js/hls-downloader.js index 19ab45f..3355e5c 100644 --- a/js/hls-downloader.js +++ b/js/hls-downloader.js @@ -1,4 +1,5 @@ import { SegmentedDownloadProgress } from './progressEvents'; +import { getProxyUrl } from './proxy-utils'; export class HlsDownloader { constructor() {} @@ -6,12 +7,12 @@ export class HlsDownloader { async downloadHlsStream(masterUrl, options = {}) { const { onProgress, signal } = options; - const response = await fetch(masterUrl, { signal }); + const response = await fetch(getProxyUrl(masterUrl), { signal }); const masterText = await response.text(); const variantUrl = this.getBestVariantUrl(masterUrl, masterText); - const mediaResponse = await fetch(variantUrl, { signal }); + const mediaResponse = await fetch(getProxyUrl(variantUrl), { signal }); const mediaText = await mediaResponse.text(); const segments = this.parseMediaPlaylist(variantUrl, mediaText); @@ -29,7 +30,7 @@ export class HlsDownloader { onProgress?.(new SegmentedDownloadProgress(downloadedBytes, undefined, i, totalSegments)); const segmentUrl = segments[i]; - const segmentResponse = await fetch(segmentUrl, { signal }); + const segmentResponse = await fetch(getProxyUrl(segmentUrl), { signal }); if (!segmentResponse.ok) { throw new Error(`Failed to fetch segment ${i}: ${segmentResponse.status}`); diff --git a/js/player.js b/js/player.js index 7952b98..16fdf09 100644 --- a/js/player.js +++ b/js/player.js @@ -22,6 +22,7 @@ import { import { audioContextManager } from './audio-context.js'; import { isIos, isSafari } from './platform-detection.js'; import { db } from './db.js'; +import { getProxyUrl } from './proxy-utils.js'; import { SVG_CLOCK, SVG_ATMOS } from './icons.js'; import { UIRenderer } from './ui.js'; @@ -133,17 +134,25 @@ export class Player { }, abr: { enabled: true, - // Start with a low bandwidth estimate (200kbps) so it plays instantly - // on slow connections and smoothly scales UP to Hi-Fi if the connection allows. defaultBandwidthEstimate: 100000, - switchInterval: 1, // Check more frequently - bandwidthDowngradeTarget: 0.8, // Downgrade more aggressively if bandwidth drops + switchInterval: 1, + bandwidthDowngradeTarget: 0.8, restrictToElementSize: false, }, mediaSource: { codecSwitchingStrategy: 'smooth', }, }); + this.shakaPlayer.getNetworkingEngine().registerRequestFilter((type, request) => { + if (type === shaka.net.NetworkingEngine.RequestType.SEGMENT) { + const uris = request.uris; + for (let i = 0; i < uris.length; i++) { + if (uris[i].includes('tidal.com')) { + uris[i] = getProxyUrl(uris[i]); + } + } + } + }); this.shakaPlayer.addEventListener('adaptation', this.updateAdaptiveQualityBadge.bind(this)); this.shakaPlayer.addEventListener('variantchanged', this.updateAdaptiveQualityBadge.bind(this)); @@ -1266,6 +1275,13 @@ export class Player { // which delays the event loop and natively adds gap/latency await this.safePlay(activeElement); } else { + if (this.shakaInitialized) { + try { + this.shakaPlayer.unload(); + this.shakaPlayer.detach(); + } catch {} + this.shakaInitialized = false; + } activeElement.src = streamUrl; this.applyAudioEffects(); this.updateAdaptiveQualityBadge(); diff --git a/vite.config.ts b/vite.config.ts index a440ea2..8b0758f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,6 +10,53 @@ import purgecss from 'vite-plugin-purgecss'; import { execSync } from 'child_process'; import { playwright } from '@vitest/browser-playwright'; +function proxyAudioPlugin() { + return { + name: 'proxy-audio-dev', + configureServer(server) { + server.middlewares.use('/proxy-audio', async (req, res) => { + const url = new URL(req.url, 'http://localhost'); + const targetUrl = url.searchParams.get('url'); + + if (!targetUrl) { + res.writeHead(400); + res.end('Missing url parameter'); + return; + } + + try { + const headers = new Headers(); + headers.set('Origin', 'https://listen.tidal.com'); + headers.set( + 'User-Agent', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + ); + + const upstream = await fetch(targetUrl, { + method: req.method, + headers, + redirect: 'follow', + }); + + const body = Buffer.from(await upstream.arrayBuffer()); + + res.writeHead(upstream.status, { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS', + 'Access-Control-Expose-Headers': '*', + 'content-type': upstream.headers.get('content-type') || 'application/octet-stream', + 'content-length': body.length, + }); + res.end(body); + } catch (error) { + res.writeHead(500); + res.end('Proxy Error: ' + error.message); + } + }); + }, + }; +} + function getGitCommitHash() { try { return execSync('git rev-parse --short HEAD').toString().trim(); @@ -80,6 +127,7 @@ export default defineConfig((_options) => { }, }, plugins: [ + proxyAudioPlugin(), purgecss({ variables: false, // DO NOT REMOVE UNUSED VARIABLES (breaks web components like am-lyrics) safelist: {