Skip to content

Commit 312e0ad

Browse files
committed
feat(vite): re-implement /proxy route
This re-adds the proxy, but only for local development. If not dev, it uses the remote proxy.
1 parent b6b0748 commit 312e0ad

8 files changed

Lines changed: 108 additions & 14 deletions

File tree

js/api.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,7 +1846,7 @@ export class LosslessAPI {
18461846
} else if (streamUrl.includes('.m3u8') || streamUrl.includes('application/vnd.apple.mpegurl')) {
18471847
try {
18481848
const downloader = new HlsDownloader();
1849-
blob = await downloader.downloadHlsStream(getProxyUrl(streamUrl), {
1849+
blob = await downloader.downloadHlsStream(streamUrl, {
18501850
signal: options.signal,
18511851
onProgress,
18521852
});
@@ -1871,7 +1871,7 @@ export class LosslessAPI {
18711871
/* ignore HEAD failure; proceed with GET */
18721872
}
18731873

1874-
const response = await fetch(getProxyUrl(streamUrl), {
1874+
const response = await fetch(streamUrl, {
18751875
cache: 'no-store',
18761876
signal: options.signal,
18771877
});

js/app.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ import {
5656
SVG_RESET,
5757
} from './icons.js';
5858
import { HiFiClient } from './HiFi.js';
59+
import { patchFetch } from './proxy-utils';
60+
61+
patchFetch();
5962

6063
// Capture real iOS state before spoofing (needed for background audio)
6164
if (typeof window !== 'undefined') {

js/dash-downloader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class DashDownloader {
3131

3232
await Promise.all(
3333
urls.map(async (url) => {
34-
const result = await fetch(getProxyUrl(url), { method: 'HEAD', signal });
34+
const result = await fetch(url, { method: 'HEAD', signal });
3535

3636
if (result.ok) {
3737
const contentLength = result.headers.get('Content-Length');
@@ -76,7 +76,7 @@ export class DashDownloader {
7676

7777
onProgress?.(new SegmentedDownloadProgress(downloadedBytes, totalSize ?? undefined, i, totalSegments));
7878

79-
const url = getProxyUrl(urls[i]);
79+
const url = urls[i];
8080
const segmentResponse = await fetch(url, { signal });
8181

8282
if (!segmentResponse.ok) {

js/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ type WithRequiredKeys<T> = {
3434

3535
declare global {
3636
const __COMMIT_HASH__: string | undefined;
37+
const __VITE_PROXY__: string | undefined;
3738
}

js/hls-downloader.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ export class HlsDownloader {
77
async downloadHlsStream(masterUrl, options = {}) {
88
const { onProgress, signal } = options;
99

10-
const response = await fetch(getProxyUrl(masterUrl), { signal });
10+
const response = await fetch(masterUrl, { signal });
1111
const masterText = await response.text();
1212

1313
const variantUrl = this.getBestVariantUrl(masterUrl, masterText);
1414

15-
const mediaResponse = await fetch(getProxyUrl(variantUrl), { signal });
15+
const mediaResponse = await fetch(variantUrl, { signal });
1616
const mediaText = await mediaResponse.text();
1717

1818
const segments = this.parseMediaPlaylist(variantUrl, mediaText);
@@ -30,7 +30,7 @@ export class HlsDownloader {
3030
onProgress?.(new SegmentedDownloadProgress(downloadedBytes, undefined, i, totalSegments));
3131

3232
const segmentUrl = segments[i];
33-
const segmentResponse = await fetch(getProxyUrl(segmentUrl), { signal });
33+
const segmentResponse = await fetch(segmentUrl, { signal });
3434

3535
if (!segmentResponse.ok) {
3636
throw new Error(`Failed to fetch segment ${i}: ${segmentResponse.status}`);

js/player.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export class Player {
148148
const uris = request.uris;
149149
for (let i = 0; i < uris.length; i++) {
150150
if (uris[i].includes('tidal.com')) {
151-
uris[i] = getProxyUrl(uris[i]);
151+
uris[i] = uris[i];
152152
}
153153
}
154154
}

js/proxy-utils.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,45 @@
1+
/* eslint-disable no-undef */
12
export const getProxyUrl = (url) => {
23
if (window.__tidalOriginExtension) return url;
3-
return `https://audio-proxy.binimum.org/proxy-audio?url=${url}`;
4+
return `${__VITE_PROXY__}?url=${encodeURIComponent(url)}`;
45
};
6+
7+
export function patchFetch() {
8+
if (__VITE_PROXY__ && !window.__tidalOriginExtension) {
9+
const originalSrcDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'src');
10+
11+
Object.defineProperty(HTMLMediaElement.prototype, 'src', {
12+
set(value) {
13+
console.log(value);
14+
const alreadyProxied = value.includes(__VITE_PROXY__);
15+
16+
if (!alreadyProxied) {
17+
const realUrl = new URL(value, window.location.href).href;
18+
if (originalSrcDescriptor && originalSrcDescriptor.set) {
19+
originalSrcDescriptor.set.call(this, getProxyUrl(realUrl));
20+
} else {
21+
this.setAttribute('src', getProxyUrl(realUrl));
22+
}
23+
}
24+
},
25+
get() {
26+
return originalSrcDescriptor ? originalSrcDescriptor.get.call(this) : this.getAttribute('src');
27+
},
28+
});
29+
30+
const ogFetch = window.fetch;
31+
window.fetch = async function (input, init, ...rest) {
32+
let url = typeof input === 'string' ? new URL(input, window.location.href) : input.url;
33+
if (url.href.includes(__VITE_PROXY__)) {
34+
return await ogFetch(input, init, ...rest);
35+
} else if (
36+
url.hostname.toLowerCase().endsWith('tidal.com') &&
37+
!url.hostname.toLowerCase().endsWith('api.tidal.com')
38+
) {
39+
return await ogFetch(getProxyUrl(url.href), init, ...rest);
40+
} else {
41+
return await ogFetch(input, init, ...rest);
42+
}
43+
};
44+
}
45+
}

vite.config.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,66 @@
1-
import path from 'path';
21
import { defineConfig } from 'vite';
32
import { VitePWA } from 'vite-plugin-pwa';
43
import authGatePlugin from './vite-plugin-auth-gate.js';
4+
import path from 'path';
5+
import uploadPlugin from './vite-plugin-upload.js';
56
import blobAssetPlugin from './vite-plugin-blob.js';
67
import svgUse from './vite-plugin-svg-use.js';
7-
import uploadPlugin from './vite-plugin-upload.js';
88
// import purgecss from 'vite-plugin-purgecss';
9-
import { playwright } from '@vitest/browser-playwright';
10-
import { execSync } from 'child_process';
119
import purgecss from 'vite-plugin-purgecss';
10+
import { execSync } from 'child_process';
11+
import { playwright } from '@vitest/browser-playwright';
12+
import type { IncomingMessage, ServerResponse } from 'http';
1213

1314
function proxyAudioPlugin() {
1415
return {
1516
name: 'proxy-audio-dev',
1617
configureServer(server) {
17-
// No longer needed: local proxy-audio middleware replaced by remote proxy
18+
server.middlewares.use('/proxy', async (req: IncomingMessage, res: ServerResponse) => {
19+
const url = new URL(req.url ?? '', 'http://localhost');
20+
const targetUrl = url.searchParams.get('url');
21+
22+
if (!targetUrl) {
23+
res.writeHead(400);
24+
res.end('Missing url parameter');
25+
return;
26+
}
27+
28+
try {
29+
const headers = new Headers();
30+
headers.set('Origin', 'https://listen.tidal.com');
31+
headers.set('User-Agent', req.headers['user-agent']);
32+
33+
const upstream = await fetch(targetUrl, {
34+
method: req.method,
35+
headers,
36+
redirect: 'follow',
37+
});
38+
39+
const resHead = new Headers(upstream.headers);
40+
Object.entries({
41+
'Access-Control-Allow-Origin': '*',
42+
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
43+
'Access-Control-Expose-Headers': '*',
44+
}).forEach(([key, value]) => resHead.set(key, value));
45+
46+
res.writeHead(upstream.status, { ...Object.fromEntries(resHead.entries()) });
47+
48+
const reader = upstream.body?.getReader();
49+
50+
if (reader) {
51+
while (true) {
52+
const { done, value } = await reader.read();
53+
if (done) break;
54+
await res.write(value);
55+
}
56+
}
57+
58+
res.end();
59+
} catch (error) {
60+
res.writeHead(500);
61+
res.end('Proxy Error: ' + error.message);
62+
}
63+
});
1864
},
1965
};
2066
}
@@ -44,6 +90,9 @@ export default defineConfig((_options) => {
4490
define: {
4591
__COMMIT_HASH__: JSON.stringify(commitHash),
4692
__VITEST__: !!process.env.VITEST,
93+
__VITE_PROXY__: JSON.stringify(
94+
_options.mode == 'development' ? '/proxy' : 'https://audio-proxy.binimum.org/proxy-audio'
95+
),
4796
},
4897
worker: {
4998
format: 'es',

0 commit comments

Comments
 (0)