diff --git a/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite b/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite new file mode 100644 index 0000000..0b58f0d Binary files /dev/null and b/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite differ diff --git a/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm b/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm new file mode 100644 index 0000000..881ccc6 Binary files /dev/null and b/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm differ diff --git a/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal b/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal new file mode 100644 index 0000000..0315217 Binary files /dev/null and b/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal differ diff --git a/extension/_metadata/generated_indexed_rulesets/_ruleset1 b/extension/_metadata/generated_indexed_rulesets/_ruleset1 new file mode 100644 index 0000000..1dba929 Binary files /dev/null and b/extension/_metadata/generated_indexed_rulesets/_ruleset1 differ diff --git a/extension/content.js b/extension/content.js new file mode 100644 index 0000000..eeaed79 --- /dev/null +++ b/extension/content.js @@ -0,0 +1,4 @@ +const s = document.createElement('script'); +s.src = chrome.runtime.getURL('inject.js'); +(document.head || document.documentElement).appendChild(s); +s.remove(); diff --git a/extension/icons/128.png b/extension/icons/128.png new file mode 100644 index 0000000..c8f4d79 Binary files /dev/null and b/extension/icons/128.png differ diff --git a/extension/icons/16.png b/extension/icons/16.png new file mode 100644 index 0000000..91837c5 Binary files /dev/null and b/extension/icons/16.png differ diff --git a/extension/icons/48.png b/extension/icons/48.png new file mode 100644 index 0000000..5e05a0c Binary files /dev/null and b/extension/icons/48.png differ diff --git a/extension/inject.js b/extension/inject.js new file mode 100644 index 0000000..06126f9 --- /dev/null +++ b/extension/inject.js @@ -0,0 +1 @@ +window.__tidalOriginExtension = true; diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000..f7c59d9 --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,47 @@ +{ + "manifest_version": 3, + "name": "Monochrome Tidal Origin", + "version": "1.0.0", + "description": "Adds Origin: https://listen.tidal.com to Tidal CDN requests so audio plays without a proxy", + "permissions": ["declarativeNetRequest", "scripting", "declarativeNetRequestWithHostAccess"], + "host_permissions": [ + "*://*.tidal.com/*", + "*://monochrome.tf/*", + "*://monochrome.samidy.com/*", + "*://lossless.wtf/*", + "*://localhost:*/*" + ], + "declarative_net_request": { + "rule_resources": [ + { + "id": "rules", + "enabled": true, + "path": "rules.json" + } + ] + }, + "content_scripts": [ + { + "matches": [ + "*://monochrome.samidy.com/*", + "*://monochrome.tf/*", + "*://lossless.wtf/*", + "*://localhost:*/*" + ], + "js": ["content.js"], + "run_at": "document_start", + "all_frames": true + } + ], + "icons": { + "16": "icons/16.png", + "48": "icons/48.png", + "128": "icons/128.png" + }, + "web_accessible_resources": [ + { + "resources": ["inject.js"], + "matches": ["*://monochrome.samidy.com/*", "*://monochrome.tf/*", "*://lossless.wtf/*", "*://localhost:*/*"] + } + ] +} diff --git a/extension/rules.json b/extension/rules.json new file mode 100644 index 0000000..8e87651 --- /dev/null +++ b/extension/rules.json @@ -0,0 +1,55 @@ +[ + { + "id": 1, + "priority": 1, + "action": { + "type": "modifyHeaders", + "responseHeaders": [ + { + "header": "Access-Control-Allow-Origin", + "operation": "set", + "value": "*" + }, + { + "header": "Access-Control-Allow-Methods", + "operation": "set", + "value": "GET, POST, PUT, DELETE, PATCH, OPTIONS" + }, + { + "header": "Access-Control-Allow-Headers", + "operation": "set", + "value": "*" + } + ] + }, + "condition": { + "urlFilter": "||tidal.com*", + "initiatorDomains": ["monochrome.tf", "monochrome.samidy.com"], + "resourceTypes": ["xmlhttprequest", "media"] + } + }, + { + "id": 2, + "priority": 1, + "action": { + "type": "modifyHeaders", + "requestHeaders": [ + { + "header": "Origin", + "operation": "set", + "value": "https://listen.tidal.com" + }, + { + "header": "Referer", + "operation": "set", + "value": "https://listen.tidal.com/" + } + ] + }, + "condition": { + "urlFilter": "||tidal.com*", + "initiatorDomains": ["monochrome.tf", "monochrome.samidy.com"], + "resourceTypes": ["xmlhttprequest", "media"] + } + } +] diff --git a/functions/proxy-audio.js b/functions/proxy-audio.js new file mode 100644 index 0000000..3374841 --- /dev/null +++ b/functions/proxy-audio.js @@ -0,0 +1,65 @@ +export async function onRequest(context) { + const { request } = context; + const url = new URL(request.url); + const targetUrl = url.searchParams.get('url'); + + if (!targetUrl) { + return new Response('Missing url parameter', { status: 400 }); + } + + try { + const cacheUrl = new URL(request.url); + try { + const tidalUrl = new URL(targetUrl); + cacheUrl.searchParams.set('cache_key', tidalUrl.pathname); + } catch (e) {} + + const cacheKey = new Request(cacheUrl.toString(), request); + const cache = caches.default; + let response = await cache.match(cacheKey); + + if (!response) { + const headers = new Headers(request.headers); + headers.delete('host'); + headers.delete('referer'); + 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' + ); + + response = await fetch(targetUrl, { + method: request.method, + headers: headers, + redirect: 'follow', + cf: { + cacheTtl: 2592000, + cacheEverything: true, + }, + }); + + if (request.method === 'GET' && response.ok) { + const cacheResponse = new Response(response.body, response); + cacheResponse.headers.set('Access-Control-Allow-Origin', '*'); + cacheResponse.headers.set('Cache-Control', 'public, max-age=2592000'); + + cacheResponse.headers.delete('Set-Cookie'); + + context.waitUntil(cache.put(cacheKey, cacheResponse.clone())); + response = cacheResponse; + } + } else { + } + + const newResponse = new Response(response.body, response); + newResponse.headers.set('Access-Control-Allow-Origin', '*'); + newResponse.headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS'); + newResponse.headers.set('Access-Control-Expose-Headers', '*'); + newResponse.headers.delete('content-security-policy'); + newResponse.headers.delete('x-frame-options'); + + return newResponse; + } catch (error) { + return new Response('Proxy Error: ' + error.message, { status: 500 }); + } +} diff --git a/js/HiFi.ts.rej b/js/HiFi.ts.rej new file mode 100644 index 0000000..107cadd --- /dev/null +++ b/js/HiFi.ts.rej @@ -0,0 +1,53 @@ +@@ -2097,45 +2185,46 @@ + offset, + countryCode: this.#countryCode, + }, + signal, + openApiToken + ); +- return HiFiClient.#jsonResponse({ version: HiFiClient.API_VERSION, data: fallback }); ++ return HiFiClient.#jsonResponse({ version: HiFiClient.API_VERSION, data: parseOpenApiSearch(fallback) }); + } + + const openApiToken = await this.getOpenApiToken(signal); ++ const includeAll = 'albums.artists,albums.coverArt,artists.profileArt,playlists.coverArt,tracks.albums,tracks.albums.coverArt,tracks.artists,videos.artists,videos.image'; + + const mapping: Array<[string | undefined, string, Params]> = [ + [ + q, + `https://openapi.tidal.com/v2/searchResults/${encodeURIComponent(q || '')}`, + { + limit, + offset, +- include: 'albums,artists,tracks,videos,playlists,topHits', ++ include: includeAll, + countryCode: this.#countryCode, + }, + ], + [ + s, + `https://openapi.tidal.com/v2/searchResults/${encodeURIComponent(s || '')}`, +- { limit, offset, include: 'tracks', countryCode: this.#countryCode }, ++ { limit, offset, include: includeAll, countryCode: this.#countryCode }, + ], + [ + a, + `https://openapi.tidal.com/v2/searchResults/${encodeURIComponent(a || '')}`, +- { limit, offset, include: 'artists,tracks', countryCode: this.#countryCode }, ++ { limit, offset, include: includeAll, countryCode: this.#countryCode }, + ], + [ + al, + `https://openapi.tidal.com/v2/searchResults/${encodeURIComponent(al || '')}`, +- { limit, offset, include: 'albums', countryCode: this.#countryCode }, ++ { limit, offset, include: includeAll, countryCode: this.#countryCode }, + ], + [ + v, + `https://openapi.tidal.com/v2/searchResults/${encodeURIComponent(v || '')}`, +- { limit, offset, include: 'videos', countryCode: this.#countryCode }, ++ { limit, offset, include: includeAll, countryCode: this.#countryCode }, + ], + [ + p, + `https://openapi.tidal.com/v2/searchResults/${encodeURIComponent(p || '')}`, diff --git a/js/proxy-utils.js b/js/proxy-utils.js new file mode 100644 index 0000000..8ef1aae --- /dev/null +++ b/js/proxy-utils.js @@ -0,0 +1,4 @@ +export const getProxyUrl = (url) => { + if (window.__tidalOriginExtension) return url; + return `/proxy-audio?url=${encodeURIComponent(url)}`; +}; diff --git a/test-search.js b/test-search.js deleted file mode 100644 index a4f6345..0000000 --- a/test-search.js +++ /dev/null @@ -1,33 +0,0 @@ -import { HiFiClient } from './js/HiFi.ts'; -import { LosslessAPI } from './js/api.js'; - -// mock out modules to make LosslessAPI load in bun -import { mock } from 'bun:test'; -mock.module('./js/icons.ts', () => ({})); -mock.module('./js/settings.js', () => ({ - devModeSettings: { isEnabled: () => false }, - syncManager: {}, - musicProviderSettings: {}, - audioSettings: {}, - apiSettings: {}, -})); - -globalThis.localStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} }; -globalThis.window = { matchMedia: () => ({ matches: false }) }; - -async function test() { - await HiFiClient.initialize(); - const api = new LosslessAPI({ getInstances: () => [] }); - - // mock cache - api.cache = { get: () => null, set: () => {} }; - - api.fetchWithRetry = async function (relativePath, options) { - console.log('fetchWithRetry called:', relativePath); - return HiFiClient.instance.query(relativePath); - }; - - const res = await api.search('coldplay'); - console.log('Returned tracks:', res.tracks?.items?.length); -} -test().catch(console.error);