From 01309a9a150ea3d913849f7f45e04c6e253df1be Mon Sep 17 00:00:00 2001 From: binimum Date: Sat, 21 Feb 2026 14:56:55 +0000 Subject: [PATCH 1/2] Change instances.json in favour of Workers which have live API uptime --- CONTRIBUTE.md | 2 +- INSTANCES.md | 7 +- functions/album/[id].js | 55 ++++++++--- functions/artist/[id].js | 55 ++++++++--- functions/playlist/[id].js | 55 ++++++++--- functions/track/[id].js | 55 ++++++++--- js/analytics.js | 14 +-- js/api.js | 23 +++-- js/events.js | 5 - js/storage.js | 190 +++++++++++++++++++------------------ package-lock.json | 15 +++ vite.config.js | 2 +- 12 files changed, 300 insertions(+), 178 deletions(-) diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md index c982115..180f3d5 100644 --- a/CONTRIBUTE.md +++ b/CONTRIBUTE.md @@ -105,7 +105,7 @@ monochrome/ ├── 📁 public/ # Static assets │ ├── assets/ # Images, icons, fonts │ ├── manifest.json # PWA manifest -│ └── instances.json # API instances configuration +│ └── instances.json # API instances configuration (deprecated) ├── 📄 index.html # Application entry point ├── 📄 vite.config.js # Build and PWA configuration ├── 📄 package.json # Dependencies and scripts diff --git a/INSTANCES.md b/INSTANCES.md index 11610ed..7dfd12d 100644 --- a/INSTANCES.md +++ b/INSTANCES.md @@ -31,7 +31,12 @@ These instances provide the tidal-ui web interface, not monochrome: ## API Instances -Monochrome uses the Hi-Fi API under the hood. These are available API endpoints that can be used with Monochrome or other Hi-Fi based applications: +Monochrome uses the Hi-Fi API under the hood. Live, up-to-date status trackers (which return JSON) can be found below: + +- https://tidal-uptime.jiffy-puffs-1j.workers.dev/ +- https://tidal-uptime.props-76styles.workers.dev/ + +These are available API endpoints that can be used with Monochrome or other Hi-Fi based applications: ### Official & Community APIs diff --git a/functions/album/[id].js b/functions/album/[id].js index 5028589..c405cfb 100644 --- a/functions/album/[id].js +++ b/functions/album/[id].js @@ -2,27 +2,52 @@ class ServerAPI { constructor() { - this.INSTANCES_URL = 'https://raw.githubusercontent.com/Monochrome-music/monochrome/main/instances.json'; + this.INSTANCES_URLS = [ + 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', + 'https://tidal-uptime.props-76styles.workers.dev/' + ]; this.apiInstances = null; } async getInstances() { if (this.apiInstances) return this.apiInstances; - try { - const response = await fetch(this.INSTANCES_URL); - if (!response.ok) throw new Error('Failed to fetch instances'); - const data = await response.json(); - this.apiInstances = data.api || []; - return this.apiInstances; - } catch (error) { - console.error('Failed to load instances:', error); - return [ - 'https://triton.squid.wtf', - 'https://wolf.qqdl.site', - 'https://tidal-api.binimum.org', - 'https://monochrome-api.samidy.com', - ]; + + let data = null; + const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); + + for (const url of urls) { + try { + const response = await fetch(url); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + data = await response.json(); + break; + } catch (error) { + console.warn(`Failed to fetch from ${url}:`, error); + } } + + if (data) { + this.apiInstances = (data.api || []) + .map(item => item.url || item) + .filter(url => !url.includes('spotisaver.net')); + return this.apiInstances; + } + + console.error('Failed to load instances from all uptime APIs'); + return [ + "https://eu-central.monochrome.tf", + "https://us-west.monochrome.tf", + "https://arran.monochrome.tf", + "https://triton.squid.wtf", + "https://api.monochrome.tf", + "https://monochrome-api.samidy.com", + "https://maus.qqdl.site", + "https://vogel.qqdl.site", + "https://katze.qqdl.site", + "https://hund.qqdl.site", + "https://tidal.kinoplus.online", + "https://wolf.qqdl.site" + ]; } async fetchWithRetry(relativePath) { diff --git a/functions/artist/[id].js b/functions/artist/[id].js index af2adeb..35f6941 100644 --- a/functions/artist/[id].js +++ b/functions/artist/[id].js @@ -2,27 +2,52 @@ class ServerAPI { constructor() { - this.INSTANCES_URL = 'https://raw.githubusercontent.com/Monochrome-music/monochrome/main/public/instances.json'; + this.INSTANCES_URLS = [ + 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', + 'https://tidal-uptime.props-76styles.workers.dev/' + ]; this.apiInstances = null; } async getInstances() { if (this.apiInstances) return this.apiInstances; - try { - const response = await fetch(this.INSTANCES_URL); - if (!response.ok) throw new Error('Failed to fetch instances'); - const data = await response.json(); - this.apiInstances = data.api || []; - return this.apiInstances; - } catch (error) { - console.error('Failed to load instances:', error); - return [ - 'https://triton.squid.wtf', - 'https://wolf.qqdl.site', - 'https://tidal-api.binimum.org', - 'https://monochrome-api.samidy.com', - ]; + + let data = null; + const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); + + for (const url of urls) { + try { + const response = await fetch(url); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + data = await response.json(); + break; + } catch (error) { + console.warn(`Failed to fetch from ${url}:`, error); + } } + + if (data) { + this.apiInstances = (data.api || []) + .map(item => item.url || item) + .filter(url => !url.includes('spotisaver.net')); + return this.apiInstances; + } + + console.error('Failed to load instances from all uptime APIs'); + return [ + "https://eu-central.monochrome.tf", + "https://us-west.monochrome.tf", + "https://arran.monochrome.tf", + "https://triton.squid.wtf", + "https://api.monochrome.tf", + "https://monochrome-api.samidy.com", + "https://maus.qqdl.site", + "https://vogel.qqdl.site", + "https://katze.qqdl.site", + "https://hund.qqdl.site", + "https://tidal.kinoplus.online", + "https://wolf.qqdl.site" + ]; } async fetchWithRetry(relativePath) { diff --git a/functions/playlist/[id].js b/functions/playlist/[id].js index 24452cf..2567e5b 100644 --- a/functions/playlist/[id].js +++ b/functions/playlist/[id].js @@ -2,27 +2,52 @@ class ServerAPI { constructor() { - this.INSTANCES_URL = 'https://raw.githubusercontent.com/Monochrome-music/monochrome/main/public/instances.json'; + this.INSTANCES_URLS = [ + 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', + 'https://tidal-uptime.props-76styles.workers.dev/' + ]; this.apiInstances = null; } async getInstances() { if (this.apiInstances) return this.apiInstances; - try { - const response = await fetch(this.INSTANCES_URL); - if (!response.ok) throw new Error('Failed to fetch instances'); - const data = await response.json(); - this.apiInstances = data.api || []; - return this.apiInstances; - } catch (error) { - console.error('Failed to load instances:', error); - return [ - 'https://triton.squid.wtf', - 'https://wolf.qqdl.site', - 'https://tidal-api.binimum.org', - 'https://monochrome-api.samidy.com', - ]; + + let data = null; + const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); + + for (const url of urls) { + try { + const response = await fetch(url); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + data = await response.json(); + break; + } catch (error) { + console.warn(`Failed to fetch from ${url}:`, error); + } } + + if (data) { + this.apiInstances = (data.api || []) + .map(item => item.url || item) + .filter(url => !url.includes('spotisaver.net')); + return this.apiInstances; + } + + console.error('Failed to load instances from all uptime APIs'); + return [ + "https://eu-central.monochrome.tf", + "https://us-west.monochrome.tf", + "https://arran.monochrome.tf", + "https://triton.squid.wtf", + "https://api.monochrome.tf", + "https://monochrome-api.samidy.com", + "https://maus.qqdl.site", + "https://vogel.qqdl.site", + "https://katze.qqdl.site", + "https://hund.qqdl.site", + "https://tidal.kinoplus.online", + "https://wolf.qqdl.site" + ]; } async fetchWithRetry(relativePath) { diff --git a/functions/track/[id].js b/functions/track/[id].js index e92eafc..028b6b0 100644 --- a/functions/track/[id].js +++ b/functions/track/[id].js @@ -14,27 +14,52 @@ function getTrackArtists(track = {}, { fallback = 'Unknown Artist' } = {}) { class ServerAPI { constructor() { - this.INSTANCES_URL = 'https://raw.githubusercontent.com/Monochrome-music/monochrome/main/public/instances.json'; + this.INSTANCES_URLS = [ + 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', + 'https://tidal-uptime.props-76styles.workers.dev/' + ]; this.apiInstances = null; } async getInstances() { if (this.apiInstances) return this.apiInstances; - try { - const response = await fetch(this.INSTANCES_URL); - if (!response.ok) throw new Error('Failed to fetch instances'); - const data = await response.json(); - this.apiInstances = data.api || []; - return this.apiInstances; - } catch (error) { - console.error('Failed to load instances from GitHub:', error); - return [ - 'https://triton.squid.wtf', - 'https://wolf.qqdl.site', - 'https://tidal-api.binimum.org', - 'https://monochrome-api.samidy.com', - ]; + + let data = null; + const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); + + for (const url of urls) { + try { + const response = await fetch(url); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + data = await response.json(); + break; + } catch (error) { + console.warn(`Failed to fetch from ${url}:`, error); + } } + + if (data) { + this.apiInstances = (data.api || []) + .map(item => item.url || item) + .filter(url => !url.includes('spotisaver.net')); + return this.apiInstances; + } + + console.error('Failed to load instances from all uptime APIs'); + return [ + "https://eu-central.monochrome.tf", + "https://us-west.monochrome.tf", + "https://arran.monochrome.tf", + "https://triton.squid.wtf", + "https://api.monochrome.tf", + "https://monochrome-api.samidy.com", + "https://maus.qqdl.site", + "https://vogel.qqdl.site", + "https://katze.qqdl.site", + "https://hund.qqdl.site", + "https://tidal.kinoplus.online", + "https://wolf.qqdl.site" + ]; } async fetchWithRetry(relativePath) { diff --git a/js/analytics.js b/js/analytics.js index 75c6a61..43a5ace 100644 --- a/js/analytics.js +++ b/js/analytics.js @@ -106,19 +106,7 @@ export function trackToggleMute(muted) { trackEvent('Toggle Mute', { muted }); } -export function trackSeek(position, duration) { - const progress = duration ? Math.round((position / duration) * 100) : 0; - // Track seek at 25%, 50%, 75% milestones - if (progress >= 25 && progress < 30) { - trackEvent('Seek', { milestone: '25%', position }); - } else if (progress >= 50 && progress < 55) { - trackEvent('Seek', { milestone: '50%', position }); - } else if (progress >= 75 && progress < 80) { - trackEvent('Seek', { milestone: '75%', position }); - } -} - -// Track listening progress milestones (10%, 25%, 50%, 75%, 90%, 100%) +// Track listening progress milestones (10%, 50%, 90%, 100%) export function trackListeningProgress(track, percent) { trackEvent('Listening Progress', { track_id: track?.id || 'unknown', diff --git a/js/api.js b/js/api.js index f02c6cb..1f239f0 100644 --- a/js/api.js +++ b/js/api.js @@ -42,17 +42,28 @@ export class LosslessAPI { async fetchWithRetry(relativePath, options = {}) { const type = options.type || 'api'; - const instances = await this.settings.getInstances(type); + let instances = await this.settings.getInstances(type); if (instances.length === 0) { throw new Error(`No API instances configured for type: ${type}`); } + if (options.minVersion) { + instances = instances.filter(instance => { + if (!instance.version) return false; + return parseFloat(instance.version) >= parseFloat(options.minVersion); + }); + if (instances.length === 0) { + throw new Error(`No API instances configured for type: ${type} with minVersion: ${options.minVersion}`); + } + } + const maxTotalAttempts = instances.length * 2; // Allow some retries across instances let lastError = null; let instanceIndex = Math.floor(Math.random() * instances.length); for (let attempt = 1; attempt <= maxTotalAttempts; attempt++) { - const baseUrl = instances[instanceIndex % instances.length]; + const instance = instances[instanceIndex % instances.length]; + const baseUrl = typeof instance === 'string' ? instance : instance.url; const url = baseUrl.endsWith('/') ? `${baseUrl}${relativePath.substring(1)}` : `${baseUrl}${relativePath}`; try { @@ -644,7 +655,7 @@ export class LosslessAPI { const cached = await this.cache.get('mix', id); if (cached) return cached; - const response = await this.fetchWithRetry(`/mix/?id=${id}`, { type: 'api' }); + const response = await this.fetchWithRetry(`/mix/?id=${id}`, { type: 'api', minVersion: '2.3' }); const data = await response.json(); const mixData = data.mix; @@ -778,7 +789,7 @@ export class LosslessAPI { if (cached) return cached; try { - const response = await this.fetchWithRetry(`/artist/similar/?id=${artistId}`, { type: 'api' }); + const response = await this.fetchWithRetry(`/artist/similar/?id=${artistId}`, { type: 'api', minVersion: '2.3' }); const data = await response.json(); // Handle various response structures @@ -829,7 +840,7 @@ export class LosslessAPI { if (cached) return cached; try { - const response = await this.fetchWithRetry(`/album/similar/?id=${albumId}`, { type: 'api' }); + const response = await this.fetchWithRetry(`/album/similar/?id=${albumId}`, { type: 'api', minVersion: '2.3' }); const data = await response.json(); const items = data.items || data.albums || data.data || (Array.isArray(data) ? data : []); @@ -976,7 +987,7 @@ export class LosslessAPI { if (cached) return cached; try { - const response = await this.fetchWithRetry(`/recommendations/?id=${id}`, { type: 'api' }); + const response = await this.fetchWithRetry(`/recommendations/?id=${id}`, { type: 'api', minVersion: '2.4' }); const json = await response.json(); const data = json.data || json; diff --git a/js/events.js b/js/events.js index ae22ad8..3e207a6 100644 --- a/js/events.js +++ b/js/events.js @@ -27,7 +27,6 @@ import { trackSkipTrack, trackToggleShuffle, trackToggleRepeat, - trackSeek, trackAddToQueue, trackPlayNext, trackLikeTrack, @@ -137,9 +136,6 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { progressFill.style.width = `${(currentTime / duration) * 100}%`; currentTimeEl.textContent = formatTime(currentTime); - // Track seek milestones - trackSeek(currentTime, duration); - // Log to history after 10 seconds of playback if (currentTime >= 10 && player.currentTrack && player.currentTrack.id !== historyLoggedTrackId) { historyLoggedTrackId = player.currentTrack.id; @@ -1668,7 +1664,6 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen api.getTrackRecommendations(clickedTrack.id).then((recs) => { if (recs && recs.length > 0) { player.addToQueue(recs); - showNotification(`Added ${recs.length} recommendations to queue`); } }); } diff --git a/js/storage.js b/js/storage.js index 513d1b5..c79185e 100644 --- a/js/storage.js +++ b/js/storage.js @@ -1,120 +1,128 @@ //storage.js export const apiSettings = { - STORAGE_KEY: 'monochrome-api-instances-v8', - INSTANCES_URL: 'instances.json', + STORAGE_KEY: 'monochrome-api-instances-v9', + INSTANCES_URLS: [ + 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', + 'https://tidal-uptime.props-76styles.workers.dev/' + ], defaultInstances: { api: [], streaming: [] }, instancesLoaded: false, + _loadPromise: null, async loadInstancesFromGitHub() { if (this.instancesLoaded) { return this.defaultInstances; } - try { - const response = await fetch(this.INSTANCES_URL); - if (!response.ok) throw new Error('Failed to fetch instances'); + if (this._loadPromise) { + return this._loadPromise; + } - const data = await response.json(); + this._loadPromise = (async () => { + const cachedData = localStorage.getItem(this.STORAGE_KEY); + if (cachedData) { + try { + const parsed = JSON.parse(cachedData); + const now = Date.now(); + // Check if cached data is less than 15 minutes old + if (parsed.timestamp && now - parsed.timestamp < 15 * 60 * 1000) { + this.defaultInstances = parsed.data; + this.instancesLoaded = true; + this._loadPromise = null; + return this.defaultInstances; + } + } catch (e) { + console.warn('Failed to parse cached instances:', e); + } + } + + let data = null; + let fetchError = null; + + // Shuffle URLs to pick a random one first + const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); + + for (const url of urls) { + try { + const response = await fetch(url); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + data = await response.json(); + break; // Success, exit loop + } catch (error) { + console.warn(`Failed to fetch from ${url}:`, error); + fetchError = error; + } + } + + if (!data) { + console.error('Failed to load instances from all uptime APIs:', fetchError); + this.defaultInstances = { + api: [ + { url: "https://eu-central.monochrome.tf", version: "2.4" }, + { url: "https://us-west.monochrome.tf", version: "2.4" }, + { url: "https://arran.monochrome.tf", version: "2.4" }, + { url: "https://triton.squid.wtf", version: "2.4" }, + { url: "https://api.monochrome.tf", version: "2.3" }, + { url: "https://monochrome-api.samidy.com", version: "2.3" }, + { url: "https://maus.qqdl.site", version: "2.2" }, + { url: "https://vogel.qqdl.site", version: "2.2" }, + { url: "https://katze.qqdl.site", version: "2.2" }, + { url: "https://hund.qqdl.site", version: "2.2" }, + { url: "https://tidal.kinoplus.online", version: "2.2" }, + { url: "https://wolf.qqdl.site", version: "2.2" } + ], + streaming: [ + { url: "https://arran.monochrome.tf", version: "2.4" }, + { url: "https://triton.squid.wtf", version: "2.4" }, + { url: "https://api.monochrome.tf", version: "2.3" }, + { url: "https://monochrome-api.samidy.com", version: "2.3" }, + { url: "https://maus.qqdl.site", version: "2.2" }, + { url: "https://vogel.qqdl.site", version: "2.2" }, + { url: "https://katze.qqdl.site", version: "2.2" }, + { url: "https://hund.qqdl.site", version: "2.2" }, + { url: "https://wolf.qqdl.site", version: "2.2" } + ] + }; + this.instancesLoaded = true; + this._loadPromise = null; + return this.defaultInstances; + } let groupedInstances = { api: [], streaming: [] }; - if (Array.isArray(data)) { - // Legacy array format - groupedInstances.api = [...data]; - groupedInstances.streaming = [...data]; - } else { - // New object format or legacy object format - if (data.api && Array.isArray(data.api)) { - const isSimpleArray = data.api.length > 0 && typeof data.api[0] === 'string'; - if (isSimpleArray) { - groupedInstances.api = [...data.api]; - } else { - for (const [, config] of Object.entries(data.api)) { - if (config.cors === false && Array.isArray(config.urls)) { - groupedInstances.api.push(...config.urls); - } - } - } - } + if (data.api && Array.isArray(data.api)) { + groupedInstances.api = data.api.filter(instance => !instance.url.includes('spotisaver.net')); + } - if (data.streaming && Array.isArray(data.streaming)) { - groupedInstances.streaming = [...data.streaming]; - } else if (groupedInstances.api.length > 0) { - groupedInstances.streaming = [...groupedInstances.api]; - } + if (data.streaming && Array.isArray(data.streaming)) { + groupedInstances.streaming = data.streaming.filter(instance => !instance.url.includes('spotisaver.net')); + } else if (groupedInstances.api.length > 0) { + groupedInstances.streaming = [...groupedInstances.api]; } this.defaultInstances = groupedInstances; this.instancesLoaded = true; + try { + localStorage.setItem(this.STORAGE_KEY, JSON.stringify({ + timestamp: Date.now(), + data: groupedInstances + })); + } catch (e) { + console.warn('Failed to cache instances:', e); + } + + this._loadPromise = null; return groupedInstances; - } catch (error) { - console.error('Failed to load instances from GitHub:', error); - this.defaultInstances = { - api: [ - 'https://eu-central.monochrome.tf', - 'https://us-west.monochrome.tf', - 'https://arran.monochrome.tf', - 'https://api.monochrome.tf', - 'https://triton.squid.wtf', - 'https://wolf.qqdl.site', - 'https://monochrome-api.samidy.com', - 'https://maus.qqdl.site', - 'https://tidal.kinoplus.online', - 'https://hund.qqdl.site', - 'https://vogel.qqdl.site', - ], - streaming: [ - 'https://arran.monochrome.tf', - 'https://triton.squid.wtf', - 'https://wolf.qqdl.site', - 'https://maus.qqdl.site', - 'https://vogel.qqdl.site', - 'https://katze.qqdl.site', - 'https://hund.qqdl.site', - ], - }; - this.instancesLoaded = true; - return this.defaultInstances; - } + })(); + + return this._loadPromise; }, async getInstances(type = 'api', _sortBySpeed = false) { let instancesObj; - const stored = localStorage.getItem(this.STORAGE_KEY); - if (stored) { - instancesObj = JSON.parse(stored); - - // love it when local storage doesnt update - if (instancesObj?.api?.length === 2) { - const hasBinimum = instancesObj.api.some((url) => { - try { - const urlObj = new URL(url); - return urlObj.hostname === 'tidal-api.binimum.org'; - } catch { - return false; - } - }); - const hasSamidy = instancesObj.api.some((url) => { - try { - const urlObj = new URL(url); - return urlObj.hostname === 'monochrome-api.samidy.com'; - } catch { - return false; - } - }); - - if (hasBinimum && hasSamidy) { - localStorage.removeItem(this.STORAGE_KEY); - instancesObj = null; - } - } - } - - if (!instancesObj) { - instancesObj = await this.loadInstancesFromGitHub(); - } + instancesObj = await this.loadInstancesFromGitHub(); const targetUrls = instancesObj[type] || instancesObj.api || []; if (targetUrls.length === 0) return []; diff --git a/package-lock.json b/package-lock.json index 2152da2..088afe7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,6 +81,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1609,6 +1610,7 @@ "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -1650,6 +1652,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1693,6 +1696,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3270,6 +3274,7 @@ "resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.0.1.tgz", "integrity": "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=20" }, @@ -3318,6 +3323,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3341,6 +3347,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3638,6 +3645,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4675,6 +4683,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7296,6 +7305,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7379,6 +7389,7 @@ "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -8484,6 +8495,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-syntax-patches-for-csstree": "^1.0.19", @@ -8934,6 +8946,7 @@ "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -9308,6 +9321,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9744,6 +9758,7 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, diff --git a/vite.config.js b/vite.config.js index f0b53de..87619b1 100644 --- a/vite.config.js +++ b/vite.config.js @@ -62,7 +62,7 @@ export default defineConfig(({ mode }) => { }, ], }, - includeAssets: ['instances.json', 'discord.html'], + includeAssets: ['discord.html'], manifest: false, // Use existing public/manifest.json }), ], From 1188a2dcce52b9344e8c4405ab38f86715aac39c Mon Sep 17 00:00:00 2001 From: binimum <61615730+binimum@users.noreply.github.com> Date: Sat, 21 Feb 2026 14:57:44 +0000 Subject: [PATCH 2/2] style: auto-fix linting issues --- functions/album/[id].js | 34 ++++++++++---------- functions/artist/[id].js | 34 ++++++++++---------- functions/playlist/[id].js | 34 ++++++++++---------- functions/track/[id].js | 34 ++++++++++---------- js/api.js | 17 +++++++--- js/storage.js | 63 ++++++++++++++++++++------------------ 6 files changed, 115 insertions(+), 101 deletions(-) diff --git a/functions/album/[id].js b/functions/album/[id].js index c405cfb..95f6fbf 100644 --- a/functions/album/[id].js +++ b/functions/album/[id].js @@ -4,17 +4,17 @@ class ServerAPI { constructor() { this.INSTANCES_URLS = [ 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', - 'https://tidal-uptime.props-76styles.workers.dev/' + 'https://tidal-uptime.props-76styles.workers.dev/', ]; this.apiInstances = null; } async getInstances() { if (this.apiInstances) return this.apiInstances; - + let data = null; const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); - + for (const url of urls) { try { const response = await fetch(url); @@ -28,25 +28,25 @@ class ServerAPI { if (data) { this.apiInstances = (data.api || []) - .map(item => item.url || item) - .filter(url => !url.includes('spotisaver.net')); + .map((item) => item.url || item) + .filter((url) => !url.includes('spotisaver.net')); return this.apiInstances; } console.error('Failed to load instances from all uptime APIs'); return [ - "https://eu-central.monochrome.tf", - "https://us-west.monochrome.tf", - "https://arran.monochrome.tf", - "https://triton.squid.wtf", - "https://api.monochrome.tf", - "https://monochrome-api.samidy.com", - "https://maus.qqdl.site", - "https://vogel.qqdl.site", - "https://katze.qqdl.site", - "https://hund.qqdl.site", - "https://tidal.kinoplus.online", - "https://wolf.qqdl.site" + 'https://eu-central.monochrome.tf', + 'https://us-west.monochrome.tf', + 'https://arran.monochrome.tf', + 'https://triton.squid.wtf', + 'https://api.monochrome.tf', + 'https://monochrome-api.samidy.com', + 'https://maus.qqdl.site', + 'https://vogel.qqdl.site', + 'https://katze.qqdl.site', + 'https://hund.qqdl.site', + 'https://tidal.kinoplus.online', + 'https://wolf.qqdl.site', ]; } diff --git a/functions/artist/[id].js b/functions/artist/[id].js index 35f6941..985e2f8 100644 --- a/functions/artist/[id].js +++ b/functions/artist/[id].js @@ -4,17 +4,17 @@ class ServerAPI { constructor() { this.INSTANCES_URLS = [ 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', - 'https://tidal-uptime.props-76styles.workers.dev/' + 'https://tidal-uptime.props-76styles.workers.dev/', ]; this.apiInstances = null; } async getInstances() { if (this.apiInstances) return this.apiInstances; - + let data = null; const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); - + for (const url of urls) { try { const response = await fetch(url); @@ -28,25 +28,25 @@ class ServerAPI { if (data) { this.apiInstances = (data.api || []) - .map(item => item.url || item) - .filter(url => !url.includes('spotisaver.net')); + .map((item) => item.url || item) + .filter((url) => !url.includes('spotisaver.net')); return this.apiInstances; } console.error('Failed to load instances from all uptime APIs'); return [ - "https://eu-central.monochrome.tf", - "https://us-west.monochrome.tf", - "https://arran.monochrome.tf", - "https://triton.squid.wtf", - "https://api.monochrome.tf", - "https://monochrome-api.samidy.com", - "https://maus.qqdl.site", - "https://vogel.qqdl.site", - "https://katze.qqdl.site", - "https://hund.qqdl.site", - "https://tidal.kinoplus.online", - "https://wolf.qqdl.site" + 'https://eu-central.monochrome.tf', + 'https://us-west.monochrome.tf', + 'https://arran.monochrome.tf', + 'https://triton.squid.wtf', + 'https://api.monochrome.tf', + 'https://monochrome-api.samidy.com', + 'https://maus.qqdl.site', + 'https://vogel.qqdl.site', + 'https://katze.qqdl.site', + 'https://hund.qqdl.site', + 'https://tidal.kinoplus.online', + 'https://wolf.qqdl.site', ]; } diff --git a/functions/playlist/[id].js b/functions/playlist/[id].js index 2567e5b..ae41d1a 100644 --- a/functions/playlist/[id].js +++ b/functions/playlist/[id].js @@ -4,17 +4,17 @@ class ServerAPI { constructor() { this.INSTANCES_URLS = [ 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', - 'https://tidal-uptime.props-76styles.workers.dev/' + 'https://tidal-uptime.props-76styles.workers.dev/', ]; this.apiInstances = null; } async getInstances() { if (this.apiInstances) return this.apiInstances; - + let data = null; const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); - + for (const url of urls) { try { const response = await fetch(url); @@ -28,25 +28,25 @@ class ServerAPI { if (data) { this.apiInstances = (data.api || []) - .map(item => item.url || item) - .filter(url => !url.includes('spotisaver.net')); + .map((item) => item.url || item) + .filter((url) => !url.includes('spotisaver.net')); return this.apiInstances; } console.error('Failed to load instances from all uptime APIs'); return [ - "https://eu-central.monochrome.tf", - "https://us-west.monochrome.tf", - "https://arran.monochrome.tf", - "https://triton.squid.wtf", - "https://api.monochrome.tf", - "https://monochrome-api.samidy.com", - "https://maus.qqdl.site", - "https://vogel.qqdl.site", - "https://katze.qqdl.site", - "https://hund.qqdl.site", - "https://tidal.kinoplus.online", - "https://wolf.qqdl.site" + 'https://eu-central.monochrome.tf', + 'https://us-west.monochrome.tf', + 'https://arran.monochrome.tf', + 'https://triton.squid.wtf', + 'https://api.monochrome.tf', + 'https://monochrome-api.samidy.com', + 'https://maus.qqdl.site', + 'https://vogel.qqdl.site', + 'https://katze.qqdl.site', + 'https://hund.qqdl.site', + 'https://tidal.kinoplus.online', + 'https://wolf.qqdl.site', ]; } diff --git a/functions/track/[id].js b/functions/track/[id].js index 028b6b0..fad3f21 100644 --- a/functions/track/[id].js +++ b/functions/track/[id].js @@ -16,17 +16,17 @@ class ServerAPI { constructor() { this.INSTANCES_URLS = [ 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', - 'https://tidal-uptime.props-76styles.workers.dev/' + 'https://tidal-uptime.props-76styles.workers.dev/', ]; this.apiInstances = null; } async getInstances() { if (this.apiInstances) return this.apiInstances; - + let data = null; const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); - + for (const url of urls) { try { const response = await fetch(url); @@ -40,25 +40,25 @@ class ServerAPI { if (data) { this.apiInstances = (data.api || []) - .map(item => item.url || item) - .filter(url => !url.includes('spotisaver.net')); + .map((item) => item.url || item) + .filter((url) => !url.includes('spotisaver.net')); return this.apiInstances; } console.error('Failed to load instances from all uptime APIs'); return [ - "https://eu-central.monochrome.tf", - "https://us-west.monochrome.tf", - "https://arran.monochrome.tf", - "https://triton.squid.wtf", - "https://api.monochrome.tf", - "https://monochrome-api.samidy.com", - "https://maus.qqdl.site", - "https://vogel.qqdl.site", - "https://katze.qqdl.site", - "https://hund.qqdl.site", - "https://tidal.kinoplus.online", - "https://wolf.qqdl.site" + 'https://eu-central.monochrome.tf', + 'https://us-west.monochrome.tf', + 'https://arran.monochrome.tf', + 'https://triton.squid.wtf', + 'https://api.monochrome.tf', + 'https://monochrome-api.samidy.com', + 'https://maus.qqdl.site', + 'https://vogel.qqdl.site', + 'https://katze.qqdl.site', + 'https://hund.qqdl.site', + 'https://tidal.kinoplus.online', + 'https://wolf.qqdl.site', ]; } diff --git a/js/api.js b/js/api.js index 1f239f0..31fb62c 100644 --- a/js/api.js +++ b/js/api.js @@ -48,7 +48,7 @@ export class LosslessAPI { } if (options.minVersion) { - instances = instances.filter(instance => { + instances = instances.filter((instance) => { if (!instance.version) return false; return parseFloat(instance.version) >= parseFloat(options.minVersion); }); @@ -789,7 +789,10 @@ export class LosslessAPI { if (cached) return cached; try { - const response = await this.fetchWithRetry(`/artist/similar/?id=${artistId}`, { type: 'api', minVersion: '2.3' }); + const response = await this.fetchWithRetry(`/artist/similar/?id=${artistId}`, { + type: 'api', + minVersion: '2.3', + }); const data = await response.json(); // Handle various response structures @@ -840,7 +843,10 @@ export class LosslessAPI { if (cached) return cached; try { - const response = await this.fetchWithRetry(`/album/similar/?id=${albumId}`, { type: 'api', minVersion: '2.3' }); + const response = await this.fetchWithRetry(`/album/similar/?id=${albumId}`, { + type: 'api', + minVersion: '2.3', + }); const data = await response.json(); const items = data.items || data.albums || data.data || (Array.isArray(data) ? data : []); @@ -987,7 +993,10 @@ export class LosslessAPI { if (cached) return cached; try { - const response = await this.fetchWithRetry(`/recommendations/?id=${id}`, { type: 'api', minVersion: '2.4' }); + const response = await this.fetchWithRetry(`/recommendations/?id=${id}`, { + type: 'api', + minVersion: '2.4', + }); const json = await response.json(); const data = json.data || json; diff --git a/js/storage.js b/js/storage.js index c79185e..536133c 100644 --- a/js/storage.js +++ b/js/storage.js @@ -3,7 +3,7 @@ export const apiSettings = { STORAGE_KEY: 'monochrome-api-instances-v9', INSTANCES_URLS: [ 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', - 'https://tidal-uptime.props-76styles.workers.dev/' + 'https://tidal-uptime.props-76styles.workers.dev/', ], defaultInstances: { api: [], streaming: [] }, instancesLoaded: false, @@ -58,30 +58,30 @@ export const apiSettings = { console.error('Failed to load instances from all uptime APIs:', fetchError); this.defaultInstances = { api: [ - { url: "https://eu-central.monochrome.tf", version: "2.4" }, - { url: "https://us-west.monochrome.tf", version: "2.4" }, - { url: "https://arran.monochrome.tf", version: "2.4" }, - { url: "https://triton.squid.wtf", version: "2.4" }, - { url: "https://api.monochrome.tf", version: "2.3" }, - { url: "https://monochrome-api.samidy.com", version: "2.3" }, - { url: "https://maus.qqdl.site", version: "2.2" }, - { url: "https://vogel.qqdl.site", version: "2.2" }, - { url: "https://katze.qqdl.site", version: "2.2" }, - { url: "https://hund.qqdl.site", version: "2.2" }, - { url: "https://tidal.kinoplus.online", version: "2.2" }, - { url: "https://wolf.qqdl.site", version: "2.2" } + { url: 'https://eu-central.monochrome.tf', version: '2.4' }, + { url: 'https://us-west.monochrome.tf', version: '2.4' }, + { url: 'https://arran.monochrome.tf', version: '2.4' }, + { url: 'https://triton.squid.wtf', version: '2.4' }, + { url: 'https://api.monochrome.tf', version: '2.3' }, + { url: 'https://monochrome-api.samidy.com', version: '2.3' }, + { url: 'https://maus.qqdl.site', version: '2.2' }, + { url: 'https://vogel.qqdl.site', version: '2.2' }, + { url: 'https://katze.qqdl.site', version: '2.2' }, + { url: 'https://hund.qqdl.site', version: '2.2' }, + { url: 'https://tidal.kinoplus.online', version: '2.2' }, + { url: 'https://wolf.qqdl.site', version: '2.2' }, ], streaming: [ - { url: "https://arran.monochrome.tf", version: "2.4" }, - { url: "https://triton.squid.wtf", version: "2.4" }, - { url: "https://api.monochrome.tf", version: "2.3" }, - { url: "https://monochrome-api.samidy.com", version: "2.3" }, - { url: "https://maus.qqdl.site", version: "2.2" }, - { url: "https://vogel.qqdl.site", version: "2.2" }, - { url: "https://katze.qqdl.site", version: "2.2" }, - { url: "https://hund.qqdl.site", version: "2.2" }, - { url: "https://wolf.qqdl.site", version: "2.2" } - ] + { url: 'https://arran.monochrome.tf', version: '2.4' }, + { url: 'https://triton.squid.wtf', version: '2.4' }, + { url: 'https://api.monochrome.tf', version: '2.3' }, + { url: 'https://monochrome-api.samidy.com', version: '2.3' }, + { url: 'https://maus.qqdl.site', version: '2.2' }, + { url: 'https://vogel.qqdl.site', version: '2.2' }, + { url: 'https://katze.qqdl.site', version: '2.2' }, + { url: 'https://hund.qqdl.site', version: '2.2' }, + { url: 'https://wolf.qqdl.site', version: '2.2' }, + ], }; this.instancesLoaded = true; this._loadPromise = null; @@ -91,11 +91,13 @@ export const apiSettings = { let groupedInstances = { api: [], streaming: [] }; if (data.api && Array.isArray(data.api)) { - groupedInstances.api = data.api.filter(instance => !instance.url.includes('spotisaver.net')); + groupedInstances.api = data.api.filter((instance) => !instance.url.includes('spotisaver.net')); } if (data.streaming && Array.isArray(data.streaming)) { - groupedInstances.streaming = data.streaming.filter(instance => !instance.url.includes('spotisaver.net')); + groupedInstances.streaming = data.streaming.filter( + (instance) => !instance.url.includes('spotisaver.net') + ); } else if (groupedInstances.api.length > 0) { groupedInstances.streaming = [...groupedInstances.api]; } @@ -104,10 +106,13 @@ export const apiSettings = { this.instancesLoaded = true; try { - localStorage.setItem(this.STORAGE_KEY, JSON.stringify({ - timestamp: Date.now(), - data: groupedInstances - })); + localStorage.setItem( + this.STORAGE_KEY, + JSON.stringify({ + timestamp: Date.now(), + data: groupedInstances, + }) + ); } catch (e) { console.warn('Failed to cache instances:', e); }