Working proxy integration with api.zarz.moe and lossless quality enforcement
This commit is contained in:
parent
3355a28789
commit
cf718f3ff2
10 changed files with 580 additions and 1612 deletions
107
.opencode/plans/tidal-web-backend.md
Normal file
107
.opencode/plans/tidal-web-backend.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Replace Tidal OAuth2 Backend with Public Web API + Self-Hosted Proxy
|
||||
|
||||
## Goal
|
||||
Replace Monochrome's Tidal OAuth2 backend (which is blocked in your country) with the SpotiFLAC tidal-web approach that uses:
|
||||
1. **Tidal public web endpoints** (`tidal.com/v1/`) with a public token for metadata/search
|
||||
2. **Self-hosted proxy** (SpotiFLAC Go backend) for audio streaming/downloads
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. `js/HiFi.ts` - Complete rewrite
|
||||
- Remove OAuth2 token flow (`auth.tidal.com/v1/oauth2/token`)
|
||||
- Use public web token (`x-tidal-token` header, default: `49YxDN9a2aFV6RTG`)
|
||||
- Change API base from `api.tidal.com/v1/` to `tidal.com/v1/`
|
||||
- Keep all TypeScript type interfaces (TidalTrack, TidalAlbum, etc.)
|
||||
- Keep `TidalResponse` class
|
||||
- Update `query()` to use `tidal.com/v1/` endpoints
|
||||
- Methods updated: `getInfo`, `getTrack`, `getAlbum`, `getPlaylist`, `getPlaylistItems`, `getArtist`, `search`, `getVideo`, `getLyrics`, `getSimilarArtists`, `getSimilarAlbums`
|
||||
- Constructor accepts: `publicToken`, `countryCode`, `locale`, `deviceType`
|
||||
- Remove: `#fetchAppToken`, `#fetchAuthenticated`, `#fetchJson` (OAuth2-based), `fetchToken`, `getTrackManifest`
|
||||
|
||||
### 2. `js/api.js` - Rewrite LosslessAPI
|
||||
- `fetchWithRetry()` - Remove HiFiClient OAuth2 fallback, remove proxy instance iteration for metadata (use HiFiClient directly). Keep proxy logic only for streaming if needed.
|
||||
- `search()`, `searchTracks()`, `searchArtists()`, `searchAlbums()`, `searchPlaylists()`, `searchVideos()` - Update to use new HiFiClient.search()
|
||||
- `getAlbum()` - Update to handle pages API response structure (`pages/album` returns page modules with `ALBUM_HEADER` and `ALBUM_ITEMS`)
|
||||
- `getPlaylist()` - Update to use new HiFiClient methods
|
||||
- `getArtist()` - Update to handle pages API response (`pages/artist` returns `ARTIST_HEADER` module)
|
||||
- `getTrack()` - Route through self-hosted proxy instead of Tidal OpenAPI
|
||||
- `getStreamUrl()` - Route through self-hosted proxy
|
||||
- `getVideo()` - Update for new endpoints
|
||||
- `downloadTrack()` - Stream URLs come from self-hosted proxy
|
||||
- `enrichTrack()` - Update to use proxy for playback info
|
||||
- `normalizeTrackManifestResponse()` - Adapt to proxy response format
|
||||
- Keep: `getCoverUrl()`, `getCoverSrcset()`, `getArtistPictureUrl()`, `getArtistPictureSrcset()`, `getVideoCoverUrl()`, cache methods, prepare methods
|
||||
|
||||
### 3. `js/storage.js` - Add tidalWebSettings
|
||||
```js
|
||||
export const tidalWebSettings = {
|
||||
STORAGE_KEY: 'tidal-web-settings',
|
||||
DEFAULT_PROXY_URL: '', // User must set their self-hosted proxy
|
||||
DEFAULT_PUBLIC_TOKEN: '49YxDN9a2aFV6RTG',
|
||||
DEFAULT_COUNTRY_CODE: 'US',
|
||||
getProxyUrl() { ... },
|
||||
setProxyUrl(url) { ... },
|
||||
getPublicToken() { ... },
|
||||
setPublicToken(token) { ... },
|
||||
getCountryCode() { ... },
|
||||
setCountryCode(code) { ... },
|
||||
};
|
||||
```
|
||||
|
||||
### 4. `js/proxy-utils.js` - Simplify
|
||||
- Remove tidal CDN URL proxying (no longer needed - streaming goes through self-hosted proxy)
|
||||
- Keep CORS proxy list for any remaining direct stream URLs
|
||||
|
||||
### 5. `js/music-api.js` - Minimal changes
|
||||
- `getTrack()`, `getStreamUrl()`, `downloadTrack()` already delegate to LosslessAPI
|
||||
- No structural changes needed
|
||||
|
||||
### 6. `js/app.js` - Update initialization
|
||||
- Change `HiFiClient.initialize()` to pass `publicToken`, `countryCode` from tidalWebSettings
|
||||
- Remove OAuth2 token/tokenExpiry storage references
|
||||
|
||||
## Self-Hosted Proxy API Contract
|
||||
|
||||
The self-hosted SpotiFLAC Go backend must expose:
|
||||
|
||||
```
|
||||
POST {proxyUrl}/v1/dl/tid2
|
||||
Body: { "id": "123456", "quality": "HI_RES_LOSSLESS" }
|
||||
|
||||
Response:
|
||||
{
|
||||
"data": {
|
||||
"manifest": "base64-encoded-manifest",
|
||||
"manifestMimeType": "application/dash+xml",
|
||||
"audioQuality": "HI_RES_LOSSLESS",
|
||||
"bitDepth": 24,
|
||||
"sampleRate": 96000,
|
||||
"trackPresentation": "FULL"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Alternative response format (from mirror proxies):
|
||||
```json
|
||||
[{ "OriginalTrackUrl": "https://direct-stream-url.flac" }]
|
||||
```
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
The SpotiFLAC Go backend is at `go_backend/` in the SpotiFLAC-Mobile repo:
|
||||
- Build: `cd go_backend && go build`
|
||||
- Run: `./go_backend` (listens on port)
|
||||
- Configure with Tidal credentials or public token
|
||||
|
||||
## Files Modified
|
||||
1. `js/HiFi.ts` - Full rewrite
|
||||
2. `js/api.js` - Major rewrite (LosslessAPI class)
|
||||
3. `js/storage.js` - Add tidalWebSettings
|
||||
4. `js/proxy-utils.js` - Simplify
|
||||
5. `js/music-api.js` - Minimal changes
|
||||
6. `js/app.js` - Update initialization
|
||||
|
||||
## Files NOT Modified
|
||||
- Server-side functions (Cloudflare Workers for SEO) - continue using OAuth2
|
||||
- Frontend UI components
|
||||
- Player, downloads, metadata embedding logic (unchanged interfaces)
|
||||
4
AGENTS.md
Normal file
4
AGENTS.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
## Permissions
|
||||
- Allow edits to: js/*.js, js/*.ts, package.json
|
||||
- Allow edits to: .opencode/plans/*.md
|
||||
- Deny: git commit, git push, .git modifications
|
||||
65
index.html
65
index.html
|
|
@ -5629,6 +5629,71 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Tidal Web Proxy URL</span>
|
||||
<span class="description">URL of your self-hosted SpotiFLAC proxy for streaming and downloads. Leave empty to use default instances.</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="tidal-web-proxy-url"
|
||||
placeholder="http://localhost:8080"
|
||||
style="
|
||||
width: 220px;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--card);
|
||||
color: var(--foreground);
|
||||
font-size: 0.8rem;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Public Token</span>
|
||||
<span class="description">Tidal public web token for API requests</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="tidal-web-public-token"
|
||||
placeholder="49YxDN9a2aFV6RTG"
|
||||
style="
|
||||
width: 220px;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--card);
|
||||
color: var(--foreground);
|
||||
font-size: 0.8rem;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Country Code</span>
|
||||
<span class="description">Country code for Tidal API requests (e.g., US, GB)</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="tidal-web-country-code"
|
||||
placeholder="US"
|
||||
maxlength="2"
|
||||
style="
|
||||
width: 80px;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--card);
|
||||
color: var(--foreground);
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
|
|
|
|||
1707
js/HiFi.ts
1707
js/HiFi.ts
File diff suppressed because it is too large
Load diff
213
js/api.js
213
js/api.js
|
|
@ -8,7 +8,7 @@ import {
|
|||
getTrackDiscNumber,
|
||||
normalizeQualityToken,
|
||||
} from './utils.js';
|
||||
import { preferDolbyAtmosSettings, trackDateSettings, devModeSettings } from './storage.js';
|
||||
import { preferDolbyAtmosSettings, trackDateSettings, devModeSettings, tidalWebSettings } from './storage.js';
|
||||
import { APICache } from './cache.js';
|
||||
import { DashDownloader } from './dash-downloader.ts';
|
||||
import { HlsDownloader } from './hls-downloader.js';
|
||||
|
|
@ -190,32 +190,17 @@ export class LosslessAPI {
|
|||
return response;
|
||||
}
|
||||
|
||||
const shouldTryNative = type !== 'streaming';
|
||||
|
||||
if (shouldTryNative) {
|
||||
// Use HiFiClient directly for metadata requests (no worker fallback needed)
|
||||
if (type !== 'streaming' && !options.userInstancesOnly) {
|
||||
try {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(relativePath);
|
||||
console.log('[hifi]', relativePath);
|
||||
}
|
||||
|
||||
// HiFiClient.query fans out across the native TIDAL endpoints used by the route
|
||||
// implementation, including api.tidal.com and openapi.tidal.com where applicable.
|
||||
return await HiFiClient.instance.query(relativePath);
|
||||
return await HiFiClient.instance.query(relativePath, options.signal);
|
||||
} catch (err) {
|
||||
if (options.directOnly) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV && isSearchRequest) {
|
||||
console.warn(
|
||||
`[search] native TIDAL query failed for ${relativePath}, trying HiFi worker instances`,
|
||||
err
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`Native TIDAL query failed for ${relativePath}. Falling back to configured HiFi API instances...`,
|
||||
err
|
||||
);
|
||||
if (options.directOnly) throw err;
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(`[hifi] HiFiClient query failed for ${relativePath}, falling back to workers`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1504,6 +1489,102 @@ export class LosslessAPI {
|
|||
return null;
|
||||
}
|
||||
|
||||
async #fetchProxy(id, quality, signal) {
|
||||
let proxyUrl = tidalWebSettings.getProxyUrl();
|
||||
if (!proxyUrl) {
|
||||
throw new Error('Proxy URL not configured. Set tidalWebSettings.getProxyUrl() in settings.');
|
||||
}
|
||||
|
||||
let baseUrl;
|
||||
if (import.meta.env.DEV && proxyUrl.startsWith('http://localhost')) {
|
||||
baseUrl = '/tidal-proxy';
|
||||
} else {
|
||||
baseUrl = proxyUrl.replace(/\/+$/g, '');
|
||||
}
|
||||
|
||||
// api.zarz.moe only accepts lossless quality formats; upgrade HIGH/LOW to LOSSLESS
|
||||
const proxyQuality = (quality === 'HIGH' || quality === 'LOW') ? 'LOSSLESS' : quality;
|
||||
|
||||
const response = await fetch(`${baseUrl}/v1/dl/tid2`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'SpotiFLAC-Mobile/4.5.5',
|
||||
},
|
||||
body: JSON.stringify({ id: String(id), quality: proxyQuality }),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => '');
|
||||
throw new Error(`Proxy request failed: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ''}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Validate response quality - reject if proxy downgraded to lossy
|
||||
const inner = data.data ?? data;
|
||||
const responseQuality = inner.audioQuality?.toUpperCase();
|
||||
if (responseQuality === 'LOW' || responseQuality === 'HIGH') {
|
||||
throw new Error(`Proxy returned ${responseQuality} quality; lossless not available for this track`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
#extractStreamUrlFromProxyResponse(data) {
|
||||
if (!data) return null;
|
||||
|
||||
// Handle [{ OriginalTrackUrl: "..." }] format
|
||||
if (Array.isArray(data)) {
|
||||
for (const entry of data) {
|
||||
if (entry?.OriginalTrackUrl) return entry.OriginalTrackUrl;
|
||||
if (entry?.originalTrackUrl) return entry.originalTrackUrl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle { data: { manifest, manifestMimeType, ... } } format
|
||||
const inner = data.data ?? data;
|
||||
|
||||
// Direct stream URL
|
||||
if (inner.url) return inner.url;
|
||||
if (inner.streamUrl) return inner.streamUrl;
|
||||
if (inner.OriginalTrackUrl) return inner.OriginalTrackUrl;
|
||||
|
||||
// Manifest-based
|
||||
if (inner.manifest) {
|
||||
return this.extractStreamUrlFromManifest(inner.manifest);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#buildPlaybackInfoFromProxy(data, quality) {
|
||||
const inner = data.data ?? data;
|
||||
const normalizedQuality = inner.audioQuality || normalizeQualityToken(quality) || 'HIGH';
|
||||
|
||||
const info = {
|
||||
trackId: inner.trackId ? Number(inner.trackId) : null,
|
||||
assetPresentation: inner.trackPresentation || inner.assetPresentation || 'FULL',
|
||||
audioQuality: normalizedQuality,
|
||||
manifestMimeType: inner.manifestMimeType || 'application/dash+xml',
|
||||
manifestHash: inner.manifestHash || '',
|
||||
manifest: inner.manifest || '',
|
||||
bitDepth: inner.bitDepth || (normalizedQuality === 'HI_RES_LOSSLESS' ? 24 : normalizedQuality === 'LOSSLESS' ? 16 : undefined),
|
||||
sampleRate: inner.sampleRate || (normalizedQuality === 'HI_RES_LOSSLESS' ? 96000 : normalizedQuality === 'LOSSLESS' ? 44100 : undefined),
|
||||
replayGain: inner.replayGain || inner.trackReplayGain,
|
||||
trackReplayGain: inner.trackReplayGain,
|
||||
trackPeakAmplitude: inner.trackPeakAmplitude,
|
||||
albumReplayGain: inner.albumReplayGain,
|
||||
albumPeakAmplitude: inner.albumPeakAmplitude,
|
||||
drmData: inner.drmData || null,
|
||||
formats: inner.formats || [],
|
||||
};
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
async normalizeTrackManifestResponse(apiResponse, quality) {
|
||||
if (!apiResponse || typeof apiResponse !== 'object') {
|
||||
return apiResponse;
|
||||
|
|
@ -1613,23 +1694,41 @@ export class LosslessAPI {
|
|||
if (cached) return cached;
|
||||
|
||||
const requestedQuality = normalizeQualityToken(quality) || quality || 'LOSSLESS';
|
||||
const params = new URLSearchParams({
|
||||
id: String(id),
|
||||
quality: requestedQuality,
|
||||
adaptive: String(adaptive),
|
||||
});
|
||||
const formats = adaptive ? this.getAdaptiveTrackManifestFormats() : this.getTrackManifestFormats(quality);
|
||||
for (const format of formats) {
|
||||
params.append('formats', format);
|
||||
|
||||
// Route through self-hosted proxy
|
||||
const proxyData = await this.#fetchProxy(id, requestedQuality);
|
||||
|
||||
// Handle [{ OriginalTrackUrl: "..." }] format (direct stream)
|
||||
if (Array.isArray(proxyData)) {
|
||||
const entry = proxyData.find(e => e.OriginalTrackUrl || e.originalTrackUrl);
|
||||
if (entry) {
|
||||
const result = {
|
||||
track: { id: Number(id), duration: 0 },
|
||||
info: {
|
||||
trackId: Number(id),
|
||||
assetPresentation: 'FULL',
|
||||
audioQuality: requestedQuality,
|
||||
manifestMimeType: 'audio/flac',
|
||||
manifestHash: '',
|
||||
manifest: '',
|
||||
},
|
||||
originalTrackUrl: entry.OriginalTrackUrl || entry.originalTrackUrl,
|
||||
};
|
||||
await this.cache.set('track', cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.fetchWithRetry(`/trackManifests/?${params.toString()}`, { type: 'streaming' });
|
||||
const jsonResponse = await response.json();
|
||||
const result = this.parseTrackLookup(await this.normalizeTrackManifestResponse(jsonResponse, quality));
|
||||
// Handle { data: { manifest, manifestMimeType, ... } } format
|
||||
const playbackInfo = this.#buildPlaybackInfoFromProxy(proxyData, requestedQuality);
|
||||
|
||||
if (!(response instanceof TidalResponse)) {
|
||||
await this.cache.set('track', cacheKey, result);
|
||||
}
|
||||
const result = {
|
||||
track: { id: Number(id), duration: 0 },
|
||||
info: playbackInfo,
|
||||
originalTrackUrl: null,
|
||||
};
|
||||
|
||||
await this.cache.set('track', cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1640,30 +1739,34 @@ export class LosslessAPI {
|
|||
return this.streamCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const requestedQuality = normalizeQualityToken(quality) || quality || 'LOSSLESS';
|
||||
|
||||
// Route through self-hosted proxy
|
||||
const proxyData = await this.#fetchProxy(id, requestedQuality);
|
||||
|
||||
let streamUrl;
|
||||
let manifestRgInfo = null;
|
||||
|
||||
const lookup = await this.getTrack(id, quality, { adaptive: this.shouldUseAdaptiveTrackManifest(download) });
|
||||
|
||||
if (lookup.originalTrackUrl) {
|
||||
streamUrl = lookup.originalTrackUrl;
|
||||
// Handle direct stream URL format
|
||||
if (Array.isArray(proxyData)) {
|
||||
const entry = proxyData.find(e => e.OriginalTrackUrl || e.originalTrackUrl);
|
||||
if (entry) {
|
||||
streamUrl = entry.OriginalTrackUrl || entry.originalTrackUrl;
|
||||
}
|
||||
} else {
|
||||
const manifest = lookup.info?.manifest;
|
||||
if (manifest) {
|
||||
streamUrl = this.extractStreamUrlFromManifest(manifest);
|
||||
}
|
||||
if (!streamUrl) {
|
||||
throw new Error('Could not resolve stream URL');
|
||||
}
|
||||
// Handle manifest-based response
|
||||
streamUrl = this.#extractStreamUrlFromProxyResponse(proxyData);
|
||||
manifestRgInfo = this.#buildPlaybackInfoFromProxy(proxyData, quality);
|
||||
manifestRgInfo = {
|
||||
trackReplayGain: manifestRgInfo.trackReplayGain || manifestRgInfo.replayGain,
|
||||
trackPeakAmplitude: manifestRgInfo.trackPeakAmplitude || manifestRgInfo.peakAmplitude,
|
||||
albumReplayGain: manifestRgInfo.albumReplayGain,
|
||||
albumPeakAmplitude: manifestRgInfo.albumPeakAmplitude,
|
||||
};
|
||||
}
|
||||
|
||||
if (lookup.info) {
|
||||
manifestRgInfo = {
|
||||
trackReplayGain: lookup.info.trackReplayGain || lookup.info.replayGain,
|
||||
trackPeakAmplitude: lookup.info.trackPeakAmplitude || lookup.info.peakAmplitude,
|
||||
albumReplayGain: lookup.info.albumReplayGain,
|
||||
albumPeakAmplitude: lookup.info.albumPeakAmplitude,
|
||||
};
|
||||
if (!streamUrl) {
|
||||
throw new Error('Could not resolve stream URL from proxy');
|
||||
}
|
||||
|
||||
const result = { url: streamUrl, rgInfo: manifestRgInfo };
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
pwaUpdateSettings,
|
||||
modalSettings,
|
||||
keyboardShortcuts,
|
||||
tidalWebSettings,
|
||||
} from './storage.js';
|
||||
import { UIRenderer } from './ui.js';
|
||||
import { Player } from './player.js';
|
||||
|
|
@ -455,6 +456,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
new ThemeStore();
|
||||
|
||||
await HiFiClient.initialize({
|
||||
publicToken: tidalWebSettings.getPublicToken(),
|
||||
countryCode: tidalWebSettings.getCountryCode(),
|
||||
baseUrl: tidalWebSettings.getProxyUrl(),
|
||||
storage: [
|
||||
localStorage,
|
||||
...(import.meta.env.DEV
|
||||
|
|
@ -466,8 +470,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
]
|
||||
: []),
|
||||
],
|
||||
token: localStorage.getItem('hifi_token') || undefined,
|
||||
tokenExpiry: parseInt(localStorage.getItem('hifi_token_expiry') || '0'),
|
||||
});
|
||||
|
||||
await MusicAPI.initialize(apiSettings);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// CORS proxy utilities for streaming audio/video content.
|
||||
// The self-hosted proxy provides stream URLs, but actual segment downloads
|
||||
// may still require CORS proxying for blob://, DASH, and HLS streams.
|
||||
|
||||
const PROXIES = [
|
||||
{ url: 'http://your-nas-ip:8081/', param: 'url=' }, // Local proxy - change to your NAS IP
|
||||
{ url: 'https://audio-proxy.binimum.org/proxy-audio', param: 'url=' },
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
pwaUpdateSettings,
|
||||
contentBlockingSettings,
|
||||
musicProviderSettings,
|
||||
tidalWebSettings,
|
||||
gaplessPlaybackSettings,
|
||||
analyticsSettings,
|
||||
modalSettings,
|
||||
|
|
@ -838,6 +839,43 @@ export async function initializeSettings(scrobbler, player, api, ui) {
|
|||
});
|
||||
}
|
||||
|
||||
// Tidal Web Proxy settings
|
||||
const proxyUrlInput = document.getElementById('tidal-web-proxy-url');
|
||||
if (proxyUrlInput) {
|
||||
proxyUrlInput.value = tidalWebSettings.getProxyUrl();
|
||||
let proxyUrlTimeout;
|
||||
proxyUrlInput.addEventListener('input', (e) => {
|
||||
clearTimeout(proxyUrlTimeout);
|
||||
proxyUrlTimeout = setTimeout(() => {
|
||||
tidalWebSettings.setProxyUrl(e.target.value.trim());
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
const publicTokenInput = document.getElementById('tidal-web-public-token');
|
||||
if (publicTokenInput) {
|
||||
publicTokenInput.value = tidalWebSettings.getPublicToken();
|
||||
let tokenTimeout;
|
||||
publicTokenInput.addEventListener('input', (e) => {
|
||||
clearTimeout(tokenTimeout);
|
||||
tokenTimeout = setTimeout(() => {
|
||||
tidalWebSettings.setPublicToken(e.target.value.trim());
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
const countryCodeInput = document.getElementById('tidal-web-country-code');
|
||||
if (countryCodeInput) {
|
||||
countryCodeInput.value = tidalWebSettings.getCountryCode();
|
||||
let countryTimeout;
|
||||
countryCodeInput.addEventListener('input', (e) => {
|
||||
clearTimeout(countryTimeout);
|
||||
countryTimeout = setTimeout(() => {
|
||||
tidalWebSettings.setCountryCode(e.target.value.trim().toUpperCase());
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Streaming Quality setting
|
||||
const streamingQualitySetting = document.getElementById('streaming-quality-setting');
|
||||
if (streamingQualitySetting) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export const apiSettings = {
|
|||
INSTANCES_URLS: ['https://tidal-uptime.geeked.wtf'],
|
||||
FALLBACK_INSTANCES: {
|
||||
api: [
|
||||
{ url: 'https://api.zarz.moe', version: '2.10' },
|
||||
{ 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' },
|
||||
|
|
@ -22,6 +23,7 @@ export const apiSettings = {
|
|||
{ url: 'https://hifi-two.spotisaver.net', version: '2.5' },
|
||||
],
|
||||
streaming: [
|
||||
{ url: 'https://api.zarz.moe', version: '2.10' },
|
||||
{ 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' },
|
||||
|
|
@ -3034,6 +3036,45 @@ export const pwaUpdateSettings = {
|
|||
},
|
||||
};
|
||||
|
||||
export const tidalWebSettings = {
|
||||
PROXY_URL_KEY: 'tidal-web-proxy-url',
|
||||
PUBLIC_TOKEN_KEY: 'tidal-web-public-token',
|
||||
COUNTRY_CODE_KEY: 'tidal-web-country-code',
|
||||
DEFAULT_PUBLIC_TOKEN: '49YxDN9a2aFV6RTG',
|
||||
DEFAULT_COUNTRY_CODE: 'US',
|
||||
|
||||
getProxyUrl() {
|
||||
try {
|
||||
return localStorage.getItem(this.PROXY_URL_KEY) || 'https://api.zarz.moe';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
setProxyUrl(url) {
|
||||
localStorage.setItem(this.PROXY_URL_KEY, url);
|
||||
},
|
||||
getPublicToken() {
|
||||
try {
|
||||
return localStorage.getItem(this.PUBLIC_TOKEN_KEY) || this.DEFAULT_PUBLIC_TOKEN;
|
||||
} catch {
|
||||
return this.DEFAULT_PUBLIC_TOKEN;
|
||||
}
|
||||
},
|
||||
setPublicToken(token) {
|
||||
localStorage.setItem(this.PUBLIC_TOKEN_KEY, token);
|
||||
},
|
||||
getCountryCode() {
|
||||
try {
|
||||
return localStorage.getItem(this.COUNTRY_CODE_KEY) || this.DEFAULT_COUNTRY_CODE;
|
||||
} catch {
|
||||
return this.DEFAULT_COUNTRY_CODE;
|
||||
}
|
||||
},
|
||||
setCountryCode(code) {
|
||||
localStorage.setItem(this.COUNTRY_CODE_KEY, code);
|
||||
},
|
||||
};
|
||||
|
||||
export const musicProviderSettings = {
|
||||
STORAGE_KEY: 'music-provider',
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,13 @@ export default defineConfig((_options) => {
|
|||
// host: true,
|
||||
// allowedHosts: ['<your_tailscale_hostname>'], // e.g. pi5.tailf5f622.ts.net
|
||||
},
|
||||
proxy: {
|
||||
'/tidal-proxy': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/tidal-proxy/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
// preview: {
|
||||
// host: true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue