feat: Add rotating proxy with health check, fix CORS for localhost, update extension
This commit is contained in:
parent
24aa18054c
commit
7da10fb2c3
10 changed files with 290 additions and 38 deletions
4
bun.lock
4
bun.lock
|
|
@ -19,7 +19,7 @@
|
||||||
"@svta/common-media-library": "^0.18.1",
|
"@svta/common-media-library": "^0.18.1",
|
||||||
"@types/wicg-file-system-access": "^2023.10.7",
|
"@types/wicg-file-system-access": "^2023.10.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
"@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",
|
"@vitest/web-worker": "^4.1.2",
|
||||||
"appwrite": "^23.0.0",
|
"appwrite": "^23.0.0",
|
||||||
"butterchurn": "^2.6.7",
|
"butterchurn": "^2.6.7",
|
||||||
|
|
@ -678,7 +678,7 @@
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
|
||||||
|
|
||||||
"@uimaxbai/am-lyrics": ["@uimaxbai/am-lyrics@1.2.8", "", { "dependencies": { "@babel/runtime": "^7.27.6", "lit": "^3.1.4" }, "peerDependencies": { "@lit/react": "^1.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@lit/react", "react"] }, "sha512-aR8kxqIYcVlsMCH6bbH8ANG+bN/2OAw66ZFjYD1a25hkMTyxtULWgWwAZlUfreP9V47bFvNgXIKvOqhO5JFpeg=="],
|
"@uimaxbai/am-lyrics": ["@uimaxbai/am-lyrics@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "lit": "^3.1.4" }, "peerDependencies": { "@lit/react": "^1.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@lit/react", "react"] }, "sha512-0IpvmCY6384cIKntIap/N9Rq/JuacSjL7Dq+QXPqRAjewZVVGcxEgka7tXtm1BnhwqIZWYWT/2Uzz6aA71RSmQ=="],
|
||||||
|
|
||||||
"@vitest/browser": ["@vitest/browser@4.1.2", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.2", "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.2" } }, "sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ=="],
|
"@vitest/browser": ["@vitest/browser@4.1.2", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.2", "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.2" } }, "sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ=="],
|
||||||
|
|
||||||
|
|
|
||||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
monochrome:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- '8080:4173'
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./data:/data # For local storage if needed
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
|
@ -31,7 +31,15 @@
|
||||||
},
|
},
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["*://monochrome.samidy.com/*", "*://monochrome.tf/*", "*://lossless.wtf/*", "*://localhost/*"],
|
"matches": [
|
||||||
|
"*://monochrome.samidy.com/*",
|
||||||
|
"*://monochrome.tf/*",
|
||||||
|
"*://lossless.wtf/*",
|
||||||
|
"*://localhost:5173/*",
|
||||||
|
"*://localhost:5174/*",
|
||||||
|
"*://localhost:5175/*",
|
||||||
|
"*://localhost:5176/*"
|
||||||
|
],
|
||||||
"js": ["content.js"],
|
"js": ["content.js"],
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"all_frames": true
|
"all_frames": true
|
||||||
|
|
@ -45,7 +53,15 @@
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["inject.js"],
|
"resources": ["inject.js"],
|
||||||
"matches": ["*://monochrome.samidy.com/*", "*://monochrome.tf/*", "*://lossless.wtf/*", "*://localhost/*"]
|
"matches": [
|
||||||
|
"*://monochrome.samidy.com/*",
|
||||||
|
"*://monochrome.tf/*",
|
||||||
|
"*://lossless.wtf/*",
|
||||||
|
"*://localhost:5173/*",
|
||||||
|
"*://localhost:5174/*",
|
||||||
|
"*://localhost:5175/*",
|
||||||
|
"*://localhost:5176/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
},
|
},
|
||||||
"condition": {
|
"condition": {
|
||||||
"urlFilter": "||tidal.com*",
|
"urlFilter": "||tidal.com*",
|
||||||
"initiatorDomains": ["monochrome.tf", "monochrome.samidy.com"],
|
"initiatorDomains": ["monochrome.tf", "monochrome.samidy.com", "localhost"],
|
||||||
"resourceTypes": ["xmlhttprequest", "media"]
|
"resourceTypes": ["xmlhttprequest", "media"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
},
|
},
|
||||||
"condition": {
|
"condition": {
|
||||||
"urlFilter": "||tidal.com*",
|
"urlFilter": "||tidal.com*",
|
||||||
"initiatorDomains": ["monochrome.tf", "monochrome.samidy.com"],
|
"initiatorDomains": ["monochrome.tf", "monochrome.samidy.com", "localhost"],
|
||||||
"resourceTypes": ["xmlhttprequest", "media"]
|
"resourceTypes": ["xmlhttprequest", "media"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
js/api.js
10
js/api.js
|
|
@ -21,6 +21,7 @@ import { resolveDownloadTotalBytes } from './downloadProgressUtils.js';
|
||||||
import { readableStreamIterator } from './readableStreamIterator.js';
|
import { readableStreamIterator } from './readableStreamIterator.js';
|
||||||
import { HiFiClient, TidalResponse } from './HiFi.ts';
|
import { HiFiClient, TidalResponse } from './HiFi.ts';
|
||||||
import { isIos, isSafari, isChrome } from './platform-detection.js';
|
import { isIos, isSafari, isChrome } from './platform-detection.js';
|
||||||
|
import { instanceHealth } from './instance-health.js';
|
||||||
import {
|
import {
|
||||||
TrackAlbum,
|
TrackAlbum,
|
||||||
EnrichedAlbum,
|
EnrichedAlbum,
|
||||||
|
|
@ -112,9 +113,9 @@ export class LosslessAPI {
|
||||||
};
|
};
|
||||||
|
|
||||||
const tryInstances = async (instances) => {
|
const tryInstances = async (instances) => {
|
||||||
const maxTotalAttempts = instances.length * 2; // Allow some retries across instances
|
const maxTotalAttempts = instances.length * 2;
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
let instanceIndex = Math.floor(Math.random() * instances.length);
|
let instanceIndex = 0;
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxTotalAttempts; attempt++) {
|
for (let attempt = 1; attempt <= maxTotalAttempts; attempt++) {
|
||||||
const instance = instances[instanceIndex % instances.length];
|
const instance = instances[instanceIndex % instances.length];
|
||||||
|
|
@ -128,12 +129,14 @@ export class LosslessAPI {
|
||||||
|
|
||||||
if (response.status === 429) {
|
if (response.status === 429) {
|
||||||
console.warn(`Rate limit hit on ${baseUrl}. Trying next instance...`);
|
console.warn(`Rate limit hit on ${baseUrl}. Trying next instance...`);
|
||||||
|
if (!import.meta?.env?.DEV) instanceHealth.checkInstance(baseUrl, type);
|
||||||
instanceIndex++;
|
instanceIndex++;
|
||||||
await delay(500);
|
await delay(500);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
if (!import.meta?.env?.DEV) instanceHealth.checkInstance(baseUrl, type);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,6 +147,7 @@ export class LosslessAPI {
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
if (errorData?.subStatus === 11002) {
|
if (errorData?.subStatus === 11002) {
|
||||||
console.warn(`Auth failed on ${baseUrl}. Trying next instance...`);
|
console.warn(`Auth failed on ${baseUrl}. Trying next instance...`);
|
||||||
|
if (!import.meta?.env?.DEV) instanceHealth.checkInstance(baseUrl, type);
|
||||||
instanceIndex++;
|
instanceIndex++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +155,7 @@ export class LosslessAPI {
|
||||||
|
|
||||||
if (response.status >= 500) {
|
if (response.status >= 500) {
|
||||||
console.warn(`Server error ${response.status} on ${baseUrl}. Trying next instance...`);
|
console.warn(`Server error ${response.status} on ${baseUrl}. Trying next instance...`);
|
||||||
|
if (!import.meta?.env?.DEV) instanceHealth.checkInstance(baseUrl, type);
|
||||||
instanceIndex++;
|
instanceIndex++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -161,6 +166,7 @@ export class LosslessAPI {
|
||||||
if (error.name === 'AbortError') throw error;
|
if (error.name === 'AbortError') throw error;
|
||||||
lastError = error;
|
lastError = error;
|
||||||
console.warn(`Network error on ${baseUrl}: ${error.message}. Trying next instance...`);
|
console.warn(`Network error on ${baseUrl}: ${error.message}. Trying next instance...`);
|
||||||
|
if (!import.meta?.env?.DEV) instanceHealth.checkInstance(baseUrl, type);
|
||||||
instanceIndex++;
|
instanceIndex++;
|
||||||
await delay(200);
|
await delay(200);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
162
js/instance-health.js
Normal file
162
js/instance-health.js
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
//instance-health.js
|
||||||
|
|
||||||
|
const HEALTH_CHECK_INTERVAL = 5 * 60 * 1000;
|
||||||
|
const HEALTH_CHECK_TIMEOUT = 8000;
|
||||||
|
const MAX_CONSECUTIVE_FAILURES = 3;
|
||||||
|
const INITIAL_DELAY = 10000;
|
||||||
|
const IS_DEV = typeof window !== 'undefined' && window.location?.hostname === 'localhost';
|
||||||
|
|
||||||
|
class InstanceHealthChecker {
|
||||||
|
constructor() {
|
||||||
|
this.healthCache = new Map();
|
||||||
|
this.checkTimers = {};
|
||||||
|
this.listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
onHealthUpdate(callback) {
|
||||||
|
this.listeners.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners(instances) {
|
||||||
|
this.listeners.forEach((cb) => cb(instances));
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkInstance(url, type = 'api') {
|
||||||
|
const testPath = '/';
|
||||||
|
const fullUrl = url.endsWith('/') ? `${url}${testPath.substring(1)}` : `${url}${testPath}`;
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(fullUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
const latency = Date.now() - startTime;
|
||||||
|
|
||||||
|
const existing = this.healthCache.get(url) || { url, failures: 0 };
|
||||||
|
const status = response.ok || response.status === 401 || response.status === 404 ? 'online' : 'degraded';
|
||||||
|
|
||||||
|
this.healthCache.set(url, {
|
||||||
|
...existing,
|
||||||
|
status,
|
||||||
|
latency,
|
||||||
|
lastCheck: Date.now(),
|
||||||
|
failures: response.ok ? 0 : existing.failures,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { url, status, latency, online: response.ok };
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
const existing = this.healthCache.get(url) || { url, failures: 0 };
|
||||||
|
const newFailures = existing.failures + 1;
|
||||||
|
const status = newFailures >= MAX_CONSECUTIVE_FAILURES ? 'offline' : 'degraded';
|
||||||
|
|
||||||
|
this.healthCache.set(url, {
|
||||||
|
url,
|
||||||
|
status,
|
||||||
|
latency: null,
|
||||||
|
lastCheck: Date.now(),
|
||||||
|
failures: newFailures,
|
||||||
|
error: error.message,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { url, status, latency: null, online: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAll(instances, type = 'api') {
|
||||||
|
const checks = instances.map((inst) => {
|
||||||
|
const url = typeof inst === 'string' ? inst : inst.url;
|
||||||
|
return this.checkInstance(url, type);
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(checks);
|
||||||
|
return results.map((r) => r.value || { url: 'unknown', status: 'offline', online: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
getHealthStatus(url) {
|
||||||
|
return this.healthCache.get(url) || { url, status: 'unknown', latency: null, lastCheck: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
getHealthyInstances(instances, type = 'api') {
|
||||||
|
return instances.filter((inst) => {
|
||||||
|
const url = typeof inst === 'string' ? inst : inst.url;
|
||||||
|
const health = this.healthCache.get(url);
|
||||||
|
return !health || health.status === 'online' || health.status === 'degraded';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getHealthyAndSorted(instances, type = 'api') {
|
||||||
|
const withHealth = instances.map((inst) => {
|
||||||
|
const url = typeof inst === 'string' ? inst : inst.url;
|
||||||
|
const health = this.healthCache.get(url);
|
||||||
|
return {
|
||||||
|
...inst,
|
||||||
|
healthStatus: health?.status || 'unknown',
|
||||||
|
latency: health?.latency ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return withHealth.sort((a, b) => {
|
||||||
|
if (a.healthStatus === 'offline' && b.healthStatus !== 'offline') return 1;
|
||||||
|
if (b.healthStatus === 'offline' && a.healthStatus !== 'offline') return -1;
|
||||||
|
if (a.latency !== null && b.latency !== null) return a.latency - b.latency;
|
||||||
|
if (a.latency !== null) return -1;
|
||||||
|
if (b.latency !== null) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startPeriodicCheck(instancesGetter, type = 'api') {
|
||||||
|
if (this.checkTimers[type]) {
|
||||||
|
clearInterval(this.checkTimers[type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IS_DEV) {
|
||||||
|
console.log(`Health check disabled in dev mode for ${type}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const check = async () => {
|
||||||
|
const instances = await instancesGetter(type);
|
||||||
|
if (!instances || instances.length === 0) return;
|
||||||
|
const results = await this.checkAll(instances, type);
|
||||||
|
this.notifyListeners(results);
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(check, INITIAL_DELAY);
|
||||||
|
this.checkTimers[type] = setInterval(check, HEALTH_CHECK_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopPeriodicCheck(type = 'api') {
|
||||||
|
if (this.checkTimers[type]) {
|
||||||
|
clearInterval(this.checkTimers[type]);
|
||||||
|
delete this.checkTimers[type];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAllChecks() {
|
||||||
|
Object.keys(this.checkTimers).forEach((type) => this.stopPeriodicCheck(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
async quickCheck(url, type = 'api') {
|
||||||
|
return this.checkInstance(url, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCachedStatus(url) {
|
||||||
|
return this.healthCache.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCache() {
|
||||||
|
this.healthCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const instanceHealth = new InstanceHealthChecker();
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
import { LosslessAPI } from './api.js';
|
import { LosslessAPI } from './api.js';
|
||||||
import { PodcastsAPI } from './podcasts-api.js';
|
import { PodcastsAPI } from './podcasts-api.js';
|
||||||
import { musicProviderSettings } from './storage.js';
|
import { musicProviderSettings, apiSettings } from './storage.js';
|
||||||
|
import { instanceHealth } from './instance-health.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MusicAPI - Singleton class that provides a unified interface for accessing music streaming services.
|
* MusicAPI - Singleton class that provides a unified interface for accessing music streaming services.
|
||||||
|
|
@ -64,7 +65,14 @@ export class MusicAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = new MusicAPI(settings);
|
const api = new MusicAPI(settings);
|
||||||
return (MusicAPI.#instance = api);
|
MusicAPI.#instance = api;
|
||||||
|
|
||||||
|
if (!window.location?.hostname?.includes('localhost')) {
|
||||||
|
instanceHealth.startPeriodicCheck((type) => apiSettings.getInstances(type), 'api');
|
||||||
|
instanceHealth.startPeriodicCheck((type) => apiSettings.getInstances(type), 'streaming');
|
||||||
|
}
|
||||||
|
|
||||||
|
return MusicAPI.#instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentProvider() {
|
getCurrentProvider() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,20 @@
|
||||||
|
const PROXIES = [
|
||||||
|
{ url: 'https://audio-proxy.binimum.org/proxy-audio', param: 'url=' },
|
||||||
|
{ url: 'https://corsproxy.io/?', param: '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let proxyIndex = 0;
|
||||||
|
|
||||||
export const getProxyUrl = (url) => {
|
export const getProxyUrl = (url) => {
|
||||||
if (!url || typeof url !== 'string') return url;
|
if (!url || typeof url !== 'string') return url;
|
||||||
if (window.__tidalOriginExtension) return url;
|
if (window.__tidalOriginExtension) return url;
|
||||||
if (url.startsWith('blob:')) return url;
|
if (url.startsWith('blob:')) return url;
|
||||||
if (url.startsWith('https://audio-proxy.binimum.org/')) return url;
|
if (url.startsWith('https://audio-proxy.binimum.org/')) return url;
|
||||||
return `https://audio-proxy.binimum.org/proxy-audio?url=${url}`;
|
|
||||||
|
const proxy = PROXIES[proxyIndex % PROXIES.length];
|
||||||
|
return `${proxy.url}${proxy.param}${encodeURIComponent(url)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rotateProxy = () => {
|
||||||
|
proxyIndex = (proxyIndex + 1) % PROXIES.length;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,38 @@ import { SVG_RIGHT_ARROW } from './icons';
|
||||||
|
|
||||||
export const apiSettings = {
|
export const apiSettings = {
|
||||||
STORAGE_KEY: 'monochrome-api-instances-v9',
|
STORAGE_KEY: 'monochrome-api-instances-v9',
|
||||||
INSTANCES_URLS: [
|
INSTANCES_URLS: ['https://tidal-uptime.geeked.wtf'],
|
||||||
'https://tidal-uptime.geeked.wtf',
|
FALLBACK_INSTANCES: {
|
||||||
],
|
api: [
|
||||||
|
{ url: 'https://eu-central.monochrome.tf', version: '2.10' },
|
||||||
|
{ url: 'https://us-west.monochrome.tf', version: '2.10' },
|
||||||
|
{ url: 'https://api.monochrome.tf', version: '2.5' },
|
||||||
|
{ url: 'https://arran.monochrome.tf', version: '2.6' },
|
||||||
|
{ url: 'https://tidal.kinoplus.online', version: '2.2' },
|
||||||
|
{ url: 'https://tidal-api.binimum.org', version: '2.6' },
|
||||||
|
{ url: 'https://wolf.qqdl.site', version: '2.2' },
|
||||||
|
{ url: 'https://maus.qqdl.site', version: '2.6' },
|
||||||
|
{ url: 'https://vogel.qqdl.site', version: '2.6' },
|
||||||
|
{ url: 'https://katze.qqdl.site', version: '2.6' },
|
||||||
|
{ url: 'https://hund.qqdl.site', version: '2.6' },
|
||||||
|
{ url: 'https://hifi-one.spotisaver.net', version: '2.5' },
|
||||||
|
{ url: 'https://hifi-two.spotisaver.net', version: '2.5' },
|
||||||
|
],
|
||||||
|
streaming: [
|
||||||
|
{ url: 'https://eu-central.monochrome.tf', version: '2.10' },
|
||||||
|
{ url: 'https://us-west.monochrome.tf', version: '2.10' },
|
||||||
|
{ url: 'https://arran.monochrome.tf', version: '2.6' },
|
||||||
|
{ url: 'https://tidal.kinoplus.online', version: '2.2' },
|
||||||
|
{ url: 'https://tidal-api.binimum.org', version: '2.6' },
|
||||||
|
{ url: 'https://wolf.qqdl.site', version: '2.2' },
|
||||||
|
{ url: 'https://maus.qqdl.site', version: '2.6' },
|
||||||
|
{ url: 'https://vogel.qqdl.site', version: '2.6' },
|
||||||
|
{ url: 'https://katze.qqdl.site', version: '2.6' },
|
||||||
|
{ url: 'https://hund.qqdl.site', version: '2.6' },
|
||||||
|
{ url: 'https://hifi-one.spotisaver.net', version: '2.5' },
|
||||||
|
{ url: 'https://hifi-two.spotisaver.net', version: '2.5' },
|
||||||
|
],
|
||||||
|
},
|
||||||
defaultInstances: { api: [], streaming: [] },
|
defaultInstances: { api: [], streaming: [] },
|
||||||
userInstances: null,
|
userInstances: null,
|
||||||
instancesLoaded: false,
|
instancesLoaded: false,
|
||||||
|
|
@ -75,27 +104,8 @@ export const apiSettings = {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
console.error('Failed to load instances from all uptime APIs:', fetchError);
|
console.error('Failed to load instances from all uptime APIs:', fetchError);
|
||||||
this.defaultInstances = {
|
this.defaultInstances = {
|
||||||
api: [
|
api: [...this.FALLBACK_INSTANCES.api],
|
||||||
{ url: 'https://hifi.geeked.wtf', version: '2.7' },
|
streaming: [...this.FALLBACK_INSTANCES.streaming],
|
||||||
{ url: 'https://eu-central.monochrome.tf', version: '2.7' },
|
|
||||||
{ url: 'https://us-west.monochrome.tf', version: '2.7' },
|
|
||||||
{ url: 'https://api.monochrome.tf', version: '2.5' },
|
|
||||||
{ url: 'https://monochrome-api.samidy.com', version: '2.3' },
|
|
||||||
{ url: 'https://maus.qqdl.site', version: '2.6' },
|
|
||||||
{ url: 'https://vogel.qqdl.site', version: '2.6' },
|
|
||||||
{ url: 'https://katze.qqdl.site', version: '2.6' },
|
|
||||||
{ url: 'https://hund.qqdl.site', version: '2.6' },
|
|
||||||
{ url: 'https://tidal.kinoplus.online', version: '2.2' },
|
|
||||||
{ url: 'https://wolf.qqdl.site', version: '2.2' },
|
|
||||||
],
|
|
||||||
streaming: [
|
|
||||||
{ url: 'https://hifi.geeked.wtf', version: '2.7' },
|
|
||||||
{ url: 'https://maus.qqdl.site', version: '2.6' },
|
|
||||||
{ url: 'https://vogel.qqdl.site', version: '2.6' },
|
|
||||||
{ url: 'https://katze.qqdl.site', version: '2.6' },
|
|
||||||
{ url: 'https://hund.qqdl.site', version: '2.6' },
|
|
||||||
{ url: 'https://wolf.qqdl.site', version: '2.6' },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
this.instancesLoaded = true;
|
this.instancesLoaded = true;
|
||||||
this._loadPromise = null;
|
this._loadPromise = null;
|
||||||
|
|
@ -113,12 +123,20 @@ export const apiSettings = {
|
||||||
groupedInstances.api = data.api.filter((item) => !isBlockedInstance(item));
|
groupedInstances.api = data.api.filter((item) => !isBlockedInstance(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.streaming && Array.isArray(data.streaming)) {
|
if (data.streaming && Array.isArray(data.streaming) && data.streaming.length > 0) {
|
||||||
groupedInstances.streaming = data.streaming.filter((item) => !isBlockedInstance(item));
|
groupedInstances.streaming = data.streaming.filter((item) => !isBlockedInstance(item));
|
||||||
} else if (groupedInstances.api.length > 0) {
|
} else if (groupedInstances.api.length > 0) {
|
||||||
groupedInstances.streaming = [...groupedInstances.api];
|
groupedInstances.streaming = [...groupedInstances.api];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (groupedInstances.streaming.length === 0) {
|
||||||
|
groupedInstances.streaming = [...this.FALLBACK_INSTANCES.streaming];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupedInstances.api.length === 0) {
|
||||||
|
groupedInstances.api = [...this.FALLBACK_INSTANCES.api];
|
||||||
|
}
|
||||||
|
|
||||||
this.defaultInstances = groupedInstances;
|
this.defaultInstances = groupedInstances;
|
||||||
this.instancesLoaded = true;
|
this.instancesLoaded = true;
|
||||||
|
|
||||||
|
|
|
||||||
21
js/ui.js
21
js/ui.js
|
|
@ -31,6 +31,7 @@ import {
|
||||||
fullscreenCoverNoRoundSettings,
|
fullscreenCoverNoRoundSettings,
|
||||||
artistBannerSettings,
|
artistBannerSettings,
|
||||||
} from './storage.js';
|
} from './storage.js';
|
||||||
|
import { instanceHealth } from './instance-health.js';
|
||||||
import { db } from './db.js';
|
import { db } from './db.js';
|
||||||
import { getVibrantColorFromImage } from './vibrant-color.js';
|
import { getVibrantColorFromImage } from './vibrant-color.js';
|
||||||
import { syncManager } from './accounts/pocketbase.js';
|
import { syncManager } from './accounts/pocketbase.js';
|
||||||
|
|
@ -6103,12 +6104,28 @@ export class UIRenderer {
|
||||||
const safeUrl = escapeHtml(instanceUrl || '');
|
const safeUrl = escapeHtml(instanceUrl || '');
|
||||||
const safeVersion = escapeHtml(instanceVersion);
|
const safeVersion = escapeHtml(instanceVersion);
|
||||||
|
|
||||||
|
const health = instanceHealth.getCachedStatus(instanceUrl);
|
||||||
|
const statusColor =
|
||||||
|
health?.status === 'online'
|
||||||
|
? '#10b981'
|
||||||
|
: health?.status === 'degraded'
|
||||||
|
? '#f59e0b'
|
||||||
|
: health?.status === 'offline'
|
||||||
|
? '#ef4444'
|
||||||
|
: '#6b7280';
|
||||||
|
const latencyDisplay = health?.latency ? `${health.latency}ms` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<li data-index="${index}" data-type="${type}" data-url="${safeUrl}">
|
<li data-index="${index}" data-type="${type}" data-url="${safeUrl}">
|
||||||
<div style="flex: 1; min-width: 0;">
|
<div style="flex: 1; min-width: 0;">
|
||||||
<div class="instance-url">${safeName} ${isUser ? '<span style="font-size: 0.6rem; opacity: 0.7; background: var(--muted); padding: 1px 4px; border-radius: 3px; margin-left: 4px; vertical-align: middle;">U</span>' : ''}</div>
|
<div class="instance-url">
|
||||||
|
<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${statusColor}; margin-right: 6px; vertical-align: middle;"></span>
|
||||||
|
${safeName} ${isUser ? '<span style="font-size: 0.6rem; opacity: 0.7; background: var(--muted); padding: 1px 4px; border-radius: 3px; margin-left: 4px; vertical-align: middle;">U</span>' : ''}
|
||||||
|
</div>
|
||||||
${safeUrl && safeUrl !== safeName ? `<div style="font-size: 0.8rem; color: var(--muted-foreground); margin-top: 0.15rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${safeUrl}</div>` : ''}
|
${safeUrl && safeUrl !== safeName ? `<div style="font-size: 0.8rem; color: var(--muted-foreground); margin-top: 0.15rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${safeUrl}</div>` : ''}
|
||||||
${safeVersion ? `<div style="font-size: 0.75rem; color: var(--muted-foreground); margin-top: 0.1rem;">v${safeVersion}</div>` : ''}
|
<div style="font-size: 0.75rem; color: var(--muted-foreground); margin-top: 0.1rem;">
|
||||||
|
${safeVersion ? `v${safeVersion}` : ''}${safeVersion && latencyDisplay ? ' · ' : ''}${latencyDisplay}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
${
|
${
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue