Merge branch 'main' of github.com:monochrome-music/monochrome
This commit is contained in:
commit
4ef66d8da9
30 changed files with 317 additions and 92 deletions
BIN
.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite
vendored
Normal file
BIN
.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite
vendored
Normal file
Binary file not shown.
BIN
.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm
vendored
Normal file
BIN
.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm
vendored
Normal file
Binary file not shown.
BIN
.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal
vendored
Normal file
BIN
.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal
vendored
Normal file
Binary file not shown.
BIN
assets/extension-help-1.png
Normal file
BIN
assets/extension-help-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
assets/extension-help-2.png
Normal file
BIN
assets/extension-help-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
16
extension/README.md
Normal file
16
extension/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Monochrome Tidal Origin
|
||||
|
||||
While the website works without the extension with the use of proxies, it is recommended to install it to prevent various annoying issues. The website works best with the extension on.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Click on the **Code** button on the [home page](https://github.com/monochrome-music/monochrome) of the repo and click **Download ZIP**. (or download the [latest main branch ZIP](https://github.com/monochrome-music/monochrome/archive/refs/heads/main.zip))
|
||||
2. Unpack the **.zip** archive with your archive program (like WinRar or 7Zip on Windows)
|
||||
3. Open extensions page in your Chromium-based browser (Or `chrome://extensions/`) and make sure **Developer mode** is turned on.
|
||||
4. Click on the **Load unpacked** and navigate to the directory you just unpacked.
|
||||
|
||||

|
||||
|
||||
5. Inside of it, there's a directory **extension**. **SELECT THAT DIRECTORY INSTEAD OF THE MAIN ONE!** It should contain file "manifest.json".
|
||||
|
||||

|
||||
4
extension/content.js
Normal file
4
extension/content.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
const s = document.createElement('script');
|
||||
s.src = chrome.runtime.getURL('inject.js');
|
||||
(document.head || document.documentElement).appendChild(s);
|
||||
s.remove();
|
||||
BIN
extension/icons/128.png
Normal file
BIN
extension/icons/128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
extension/icons/16.png
Normal file
BIN
extension/icons/16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
extension/icons/48.png
Normal file
BIN
extension/icons/48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
1
extension/inject.js
Normal file
1
extension/inject.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
window.__tidalOriginExtension = true;
|
||||
51
extension/manifest.json
Normal file
51
extension/manifest.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Monochrome Tidal Bypass",
|
||||
"version": "1.0.2",
|
||||
"description": "Adds Origin: https://listen.tidal.com to Tidal CDN requests so audio plays without a proxy",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "monochrome-tidal-bypass@binimum.org",
|
||||
"strict_min_version": "111.0",
|
||||
"data_collection_permissions": {
|
||||
"required": ["none"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
55
extension/rules.json
Normal file
55
extension/rules.json
Normal file
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
14
index.html
14
index.html
|
|
@ -117,6 +117,12 @@
|
|||
<body>
|
||||
<audio id="audio-player" style="display: none"></audio>
|
||||
<video id="video-player" style="display: none"></video>
|
||||
<script>
|
||||
if (window.__tidalOriginExtension) {
|
||||
document.getElementById('audio-player').crossOrigin = 'anonymous';
|
||||
document.getElementById('video-player').crossOrigin = 'anonymous';
|
||||
}
|
||||
</script>
|
||||
<div id="context-menu">
|
||||
<ul>
|
||||
<li data-action="shuffle-play-card" data-type-filter="album,playlist,mix,user-playlist">
|
||||
|
|
@ -1732,8 +1738,11 @@
|
|||
<div id="server-disruption-banner" class="server-disruption-sidebar" style="display: none">
|
||||
<span class="disruption-icon">⚠</span>
|
||||
<span
|
||||
>Services are currently unstable. <br /><br />For Hi-Res streaming, use Chrome or
|
||||
Safari.</span
|
||||
>Services are currently unstable. <br /><br />Use the extension
|
||||
<a href="https://github.com/monochrome-music/monochrome/tree/main/extension"
|
||||
>here</a
|
||||
>
|
||||
for the best experience.</span
|
||||
>
|
||||
<button id="dismiss-disruption-btn" class="disruption-dismiss" title="Dismiss">
|
||||
×
|
||||
|
|
@ -4063,6 +4072,7 @@
|
|||
</div>
|
||||
<select id="streaming-quality-setting">
|
||||
<option value="auto">Auto (Adaptive)</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>
|
||||
<option value="LOW">AAC 96kbps</option>
|
||||
|
|
|
|||
|
|
@ -1590,9 +1590,12 @@ class HiFiClient {
|
|||
const attr = inc.attributes ?? ({} as JsonApiIncludeAttributes);
|
||||
|
||||
let pic_id: string | null = null;
|
||||
const art_data = inc.relationships?.profileArt?.data;
|
||||
if (Array.isArray(art_data) && art_data.length > 0) {
|
||||
const artwork = artworks_map[art_data[0].id];
|
||||
const art_refs_artist = (() => {
|
||||
const d = inc.relationships?.profileArt?.data;
|
||||
return Array.isArray(d) ? d : d ? [d as JsonApiRef] : [];
|
||||
})();
|
||||
if (art_refs_artist.length > 0) {
|
||||
const artwork = artworks_map[art_refs_artist[0].id];
|
||||
const files = artwork?.attributes?.files;
|
||||
if (Array.isArray(files) && files[0]?.href) {
|
||||
pic_id = HiFiClient.#extractUuidFromTidalUrl(files[0].href);
|
||||
|
|
|
|||
|
|
@ -677,7 +677,35 @@ const syncManager = {
|
|||
user_folders: Object.values(userFolders).filter((f) => f && typeof f === 'object'),
|
||||
};
|
||||
|
||||
await database.importData(convertedData);
|
||||
// Safety check: if we had local data but merged result is completely empty, something went wrong.
|
||||
// Do NOT call importData as it would wipe the user's local stores.
|
||||
const hadLocalData =
|
||||
localData.tracks.length > 0 ||
|
||||
localData.albums.length > 0 ||
|
||||
localData.artists.length > 0 ||
|
||||
localData.playlists.length > 0 ||
|
||||
localData.mixes.length > 0 ||
|
||||
localData.history.length > 0 ||
|
||||
localData.userPlaylists.length > 0 ||
|
||||
localData.userFolders.length > 0;
|
||||
|
||||
const isConvertedEmpty =
|
||||
convertedData.favorites_tracks.length === 0 &&
|
||||
convertedData.favorites_albums.length === 0 &&
|
||||
convertedData.favorites_artists.length === 0 &&
|
||||
convertedData.favorites_playlists.length === 0 &&
|
||||
convertedData.favorites_mixes.length === 0 &&
|
||||
convertedData.history_tracks.length === 0 &&
|
||||
convertedData.user_playlists.length === 0 &&
|
||||
convertedData.user_folders.length === 0;
|
||||
|
||||
if (hadLocalData && isConvertedEmpty) {
|
||||
console.warn(
|
||||
'[PocketBase] Sync aborted: local data exists but merged result is empty. Preserving local data to prevent accidental wipe.'
|
||||
);
|
||||
} else {
|
||||
await database.importData(convertedData, true);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('library-changed'));
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -1768,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,
|
||||
|
|
@ -1787,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,
|
||||
});
|
||||
|
|
@ -1812,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,
|
||||
});
|
||||
|
|
|
|||
33
js/app.js
33
js/app.js
|
|
@ -412,6 +412,13 @@ async function uploadCoverImage(file) {
|
|||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await modernSettings.waitPending();
|
||||
|
||||
// Request persistent storage to reduce risk of browser wiping data on updates or cleanup
|
||||
if (navigator.storage && navigator.storage.persist) {
|
||||
navigator.storage.persist().catch(() => {
|
||||
// Ignore errors; persistence is a best-effort request
|
||||
});
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
window.monochrome = {
|
||||
HiFiClient,
|
||||
|
|
@ -467,31 +474,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
|
||||
|
|
|
|||
|
|
@ -483,6 +483,25 @@ class AudioContextManager {
|
|||
this.audioContext = new AudioContext();
|
||||
}
|
||||
|
||||
if (window.__tidalOriginExtension) {
|
||||
if (!this.sources.has(audioElement)) {
|
||||
const src = this.audioContext.createMediaElementSource(audioElement);
|
||||
this.sources.set(audioElement, src);
|
||||
}
|
||||
this.source = this.sources.get(audioElement);
|
||||
|
||||
try {
|
||||
this.audioContext.destination.channelCount = Math.min(this.audioContext.destination.maxChannelCount, 8);
|
||||
this.audioContext.destination.channelCountMode = 'explicit';
|
||||
this.audioContext.destination.channelInterpretation = 'discrete';
|
||||
} catch {
|
||||
// Some browsers may not support changing destination channel count
|
||||
}
|
||||
|
||||
this.binauralDsp = new BinauralDSP(this.audioContext);
|
||||
void this._loadBinauralSettings();
|
||||
}
|
||||
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
this.analyser.fftSize = 1024;
|
||||
this.analyser.smoothingTimeConstant = 0.7;
|
||||
|
|
@ -502,12 +521,16 @@ class AudioContextManager {
|
|||
|
||||
this.monoMergerNode = this.audioContext.createChannelMerger(2);
|
||||
|
||||
if (window.__tidalOriginExtension) {
|
||||
this._connectGraph();
|
||||
}
|
||||
|
||||
// Auto-recover from unexpected suspensions (e.g. background throttling)
|
||||
this.audioContext.addEventListener('statechange', () => {
|
||||
if (this.audioContext.state === 'interrupted' || this.audioContext.state === 'suspended') {
|
||||
console.log(`[AudioContext] State changed to ${this.audioContext.state}, attempting resume`);
|
||||
setTimeout(() => {
|
||||
if (this.audioContext && this.audioContext.state !== 'running') {
|
||||
if (this.audioContext && this.audioContext.state !== 'running' && (window.__tidalOriginExtension ? this.source : true)) {
|
||||
this.audioContext.resume().catch((e) => {
|
||||
console.warn('[AudioContext] Auto-resume failed:', e);
|
||||
});
|
||||
|
|
@ -529,7 +552,32 @@ class AudioContextManager {
|
|||
}
|
||||
if (this.audio === audioElement) return;
|
||||
|
||||
this.audio = audioElement;
|
||||
if (window.__tidalOriginExtension) {
|
||||
try {
|
||||
if (this.source) {
|
||||
try {
|
||||
this.source.disconnect();
|
||||
} catch {
|
||||
// node may already be disconnected
|
||||
}
|
||||
}
|
||||
|
||||
this.audio = audioElement;
|
||||
|
||||
if (!this.sources.has(audioElement)) {
|
||||
this.sources.set(audioElement, this.audioContext.createMediaElementSource(audioElement));
|
||||
}
|
||||
this.source = this.sources.get(audioElement);
|
||||
|
||||
if (this.isInitialized) {
|
||||
this._connectGraph();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('changeSource failed:', e);
|
||||
}
|
||||
} else {
|
||||
this.audio = audioElement;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
35
js/db.js
35
js/db.js
|
|
@ -85,8 +85,15 @@ export class MusicDatabase {
|
|||
const store = transaction.objectStore(storeName);
|
||||
const request = callback(store);
|
||||
|
||||
let result;
|
||||
if (request) {
|
||||
request.onsuccess = () => {
|
||||
result = request.result;
|
||||
};
|
||||
}
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
resolve(request?.result);
|
||||
resolve(result);
|
||||
};
|
||||
transaction.onerror = (event) => {
|
||||
reject(event.target.error);
|
||||
|
|
@ -448,6 +455,25 @@ export class MusicDatabase {
|
|||
async importData(data, clear = false) {
|
||||
const db = await this.open();
|
||||
|
||||
// Safety check: if clear=true but all data is empty, skip to avoid wiping existing data
|
||||
if (clear) {
|
||||
const allEmpty = [
|
||||
data.favorites_tracks,
|
||||
data.favorites_albums,
|
||||
data.favorites_artists,
|
||||
data.favorites_playlists,
|
||||
data.favorites_mixes,
|
||||
data.history_tracks,
|
||||
data.user_playlists,
|
||||
data.user_folders,
|
||||
].every((arr) => !arr || (Array.isArray(arr) ? arr.length === 0 : Object.keys(arr).length === 0));
|
||||
|
||||
if (allEmpty) {
|
||||
console.warn('[importData] Aborting: clear=true but all import data is empty. Existing data preserved.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const importStore = async (storeName, items) => {
|
||||
if (items === undefined) return false;
|
||||
|
||||
|
|
@ -481,9 +507,10 @@ export class MusicDatabase {
|
|||
const transaction = db.transaction(storeName, 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
|
||||
// force clear on first sync
|
||||
console.log(`Clearing ${storeName} to Make Sure Everythings Good`);
|
||||
store.clear();
|
||||
if (clear) {
|
||||
console.log(`[importData] Clearing ${storeName} before import`);
|
||||
store.clear();
|
||||
}
|
||||
|
||||
itemsArray.forEach((item) => {
|
||||
if (item.id && typeof item.id === 'string' && !isNaN(item.id)) {
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
24
js/player.js
24
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();
|
||||
|
|
|
|||
4
js/proxy-utils.js
Normal file
4
js/proxy-utils.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const getProxyUrl = (url) => {
|
||||
if (window.__tidalOriginExtension) return url;
|
||||
return `https://audio-proxy.binimum.org/proxy-audio?url=${url}`;
|
||||
};
|
||||
|
|
@ -6573,7 +6573,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
|
|||
reader.onload = async (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.target.result);
|
||||
await db.importData(data);
|
||||
await db.importData(data, true);
|
||||
alert('Library imported successfully!');
|
||||
window.location.reload(); // Simple way to refresh all state
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
"@svta/common-media-library": "^0.18.1",
|
||||
"@types/wicg-file-system-access": "^2023.10.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
||||
"@uimaxbai/am-lyrics": "^1.2.8",
|
||||
"@uimaxbai/am-lyrics": "^1.2.9",
|
||||
"@vitest/web-worker": "^4.1.2",
|
||||
"appwrite": "^23.0.0",
|
||||
"butterchurn": "^2.6.7",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -7,17 +7,17 @@
|
|||
"types": ["vite/client", "node", "@types/wicg-file-system-access"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"!/*": ["node_modules/*"]
|
||||
"!/*": ["node_modules/*"],
|
||||
},
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"ignoreDeprecations": "6.0",
|
||||
"ignoreDeprecations": "5.0",
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
},
|
||||
"include": ["**/*.ts", "*.ts", "**/*.js", "*.js"],
|
||||
"exclude": ["**/node_modules/*"]
|
||||
"exclude": ["**/node_modules/*"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import authGatePlugin from './vite-plugin-auth-gate.js';
|
||||
import path from 'path';
|
||||
import uploadPlugin from './vite-plugin-upload.js';
|
||||
import blobAssetPlugin from './vite-plugin-blob.js';
|
||||
import svgUse from './vite-plugin-svg-use.js';
|
||||
import uploadPlugin from './vite-plugin-upload.js';
|
||||
// import purgecss from 'vite-plugin-purgecss';
|
||||
import purgecss from 'vite-plugin-purgecss';
|
||||
import { execSync } from 'child_process';
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { execSync } from 'child_process';
|
||||
import purgecss from 'vite-plugin-purgecss';
|
||||
|
||||
function proxyAudioPlugin() {
|
||||
return {
|
||||
name: 'proxy-audio-dev',
|
||||
configureServer(server) {
|
||||
// No longer needed: local proxy-audio middleware replaced by remote proxy
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getGitCommitHash() {
|
||||
try {
|
||||
|
|
@ -80,6 +89,7 @@ export default defineConfig((_options) => {
|
|||
},
|
||||
},
|
||||
plugins: [
|
||||
proxyAudioPlugin(),
|
||||
purgecss({
|
||||
variables: false, // DO NOT REMOVE UNUSED VARIABLES (breaks web components like am-lyrics)
|
||||
safelist: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue