Skip to content

About

simple api for tidal (custom rewrite)

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

hifi-api-rust

Rust (Axum) API for Tidal catalog data and playback, with multiple accounts, a playback queue, a separate catalog account, and an admin panel.

This repository is a fork of itsmeadarsh2008/hifi-api. That project is a fork of binimum/hifi-api, which is a fork of sachinsenal0x64/hifi.

Use your own valid Tidal account. Catalog availability and playback formats depend on the account, track, and client credentials.

What this fork adds

Compared with itsmeadarsh2008/hifi-api, this fork adds:

  • Bounded playback queue: concurrent work is limited by the playback accounts that can serve right now. Waiting requests can be polled or cancelled, abandoned jobs are removed, and a full queue returns 503.
  • Account-aware rate limiting: a Tidal 429 pauses the affected account for Retry-After and tries at most one other eligible account. If none succeeds, the caller receives 429 with Retry-After.
  • Per-account proxy routing: each account keeps a working proxy assignment across restarts. Accounts spread over spare proxies; repeated connection failures move only the affected account. Direct connection fallback and rotation on token refresh are opt-in.
  • Access protection: repeated failed admin authentication and requests to common scanner paths trigger temporary, per-IP lockouts.

The fork also retains upstream features useful for operating the service: a metadata cache with request coalescing, separate error and latency metrics, automatic and manual FULL/PREVIEW account checks, API keys, and optional Redis sharing across instances. The queue, cache, proxy bindings, and rate-limit pauses remain local to each instance.

Quick start

Docker Compose

cp .env.example .env
# Set a strong ADMIN_KEY in .env.
docker compose up -d --build
docker compose ps

Open http://127.0.0.1:8000/admin, sign in with ADMIN_KEY, and use Add via OAuth to add a Tidal account. You can also import an existing credentials.json in the admin panel. API liveness is available at /health.

The base Compose file binds to 127.0.0.1:8000 by default and stores SQLite data in the hifi_data volume. Container logs are rotated at 10 MB with three files kept, so they never exceed about 30 MB. Set HIFI_BIND and HIFI_PORT in .env to change the host binding. token.json is not mounted into the container automatically.

To connect the container to a reverse proxy, create the external Docker network proxy once, then start with the override:

docker network create proxy
docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d --build

The proxy override removes the host port mapping. Set your reverse proxy's upstream to hifi-api-rust:8000 on the proxy network and serve it over HTTPS. Use the base Compose command when you want the local port binding.

Run from source

Requires the Rust toolchain. From the repository root:

cp .env.example .env
# Set ADMIN_KEY in .env.
cargo run --release

The server listens on 0.0.0.0:8000 by default; use HOST and PORT to change this. With no accounts configured, add one at /admin. To start the device authorization flow automatically on first boot, set AUTO_SETUP=true before starting the server. Its verification URL is printed in the logs.

You may instead provide CLIENT_ID, REFRESH_TOKEN, and optionally CLIENT_SECRET and USER_ID in .env. When the database has no playback accounts, these values create a default account. Keep refresh tokens and ADMIN_KEY out of version control.

Each account card shows availability over the last seven days. Green means an enabled account has a nonempty, unexpired token that has not been rejected; yellow means an enabled account has no usable token; red means the account is disabled. Time without a usable token is shown separately and lowers the availability percentage. Token expiration is recorded in history so idle accounts stop accruing uptime at expiration. This is not a continuous upstream connectivity check. History starts when monitoring is introduced, and earlier time is shown as unknown. Existing pool-only history is preserved. SQLite preserves history across restarts and backups; ephemeral mode retains it only until restart. Status changes adopted from Redis are recorded when observed locally. To measure stability from a clean slate, for example after an update, use Reset uptime in Admin → System: it deletes the history of every account on that server and restarts tracking from the current state.

The overview separates playback requests running now, queued requests, and the concurrent request limit from account readiness. The limit is the number of enabled playback accounts with a usable token and no 429 cooldown (at least one), so accounts waiting for a token do not add capacity; the FULL check does not affect it. Ready accounts require a usable token and no 429 cooldown; playback accounts additionally need a successful FULL check within the last eight hours. Account cards use the same readiness rules and show why an enabled account is not ready. Metadata only needs a usable token, so its available account count can be higher than the FULL playback count.

Request totals and account attempt/error counters are local to the server process and start over on restart. Requests per second count only requests sent on to Tidal (playback, search, and other catalog routes that picked an account, plus queued playback jobs); responses served from the cache are shown beside them, and queue polling, unknown paths, and requests answered locally are left out. The card charts the last 60 seconds per second and shows the peak, the average load per serving account, and the number of 429 responses. The overview refreshes every 2 seconds and the request log every 5. p95 and HTTP error percentage use the bounded recent log (up to 5000 requests), with the actual sample count shown. An empty sample has no error rate or p95. Cache counters also count requests since process start; clearing cached data does not reset these counters. UI rendering checks run with node --test tests/admin_cards.test.cjs, alongside cargo test.

Changing the version

Set the version in Cargo.toml under [package]. Cargo updates the generated Cargo.lock when you build or run cargo check:

cargo check

Commit Cargo.toml and the updated Cargo.lock together. The API response, startup log, and admin panel read the package version from Cargo.toml at compile time. Docker also updates the lockfile during its build, but that copy stays inside the build container.

Configuration

Token renewal and auto-heal are always enabled. Legacy AUTO_HEAL and persisted auto_heal settings are ignored. Recovery retries use backoff and respect Retry-After; manually disabled accounts stay off. SQLite preserves the token-refresh retry deadline, failure count, failure cause, and rejected-token marker across restarts and backups. Restarting waits out the remaining cooldown; an expired deadline allows the next retry without resetting backoff. Successful renewal or an explicit account toggle clears the cooldown; changing OAuth credentials also clears the rejected-token marker. These recovery fields remain local to each instance and are not persisted in database-free mode.

Refresh uses the account's assigned proxy and a separate HTTP/1.1 auth client. API connections negotiate HTTP normally. Requests have connect/read/total deadlines of 5/15/25 seconds; each refresh operation has a 45-second deadline, and at most two account refreshes contact auth concurrently. The first recovery cycle makes up to four attempts for transient 403, 408, 425, 429, selected 5xx, and network failures, using roughly 1.5 → 3 → 6 second delays with jitter and honoring Retry-After. Later recovery cycles send one probe per pause. Temporary auth failures then back off from roughly 30 seconds to one hour; explicit OAuth credential errors start at five minutes. Revoked credentials can still require reauthorization.

Legacy URL-encoded client secrets are normalized when loaded/imported. Store raw secrets (= rather than %3D); request builders perform the required wire encoding. Rotated refresh tokens are persisted. Auth starts with client credentials in the form body and alternates body/Basic only after a 403; successful form choice is reused. Four consecutive auth 403 responses keep the current account-to-IP affinity for the complete fast cycle, then move that account to a different proxy before its next recovery probe. 429, 5xx, and network failures do not change the OAuth form.

New device-authorized accounts keep the same client ID and secret that issued their refresh token. The access token returned by device authorization is cached immediately. Existing accounts are not rewritten automatically because imported credentials may legitimately belong to another client; reauthorize an old setup-created account to adopt the corrected pairing.

Copy .env.example for the full list. Values saved in the admin panel can override initial environment defaults where noted.

Variable Default Purpose
DATABASE_URL hifi.db locally; /data/hifi.db in Compose SQLite file. Empty or ephemeral disables the database and persistence.
ADMIN_KEY Empty Protects the admin panel and admin API. Set a strong value before exposing the service.
HOST, PORT 0.0.0.0, 8000 Server listener.
HIFI_BIND, HIFI_PORT 127.0.0.1, 8000 Host binding for the base Compose file.
AUTO_SETUP false Start device authorization in the background when no account exists.
CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, USER_ID Empty Initial playback account credentials.
TOKEN_FILE token.json Import an existing upstream credential file on startup when present; entries marked role: "catalog" stay out of playback.
CATALOG_CLIENT_ID, CATALOG_CLIENT_SECRET, CATALOG_REFRESH_TOKEN, CATALOG_USER_ID Empty Initial dedicated metadata account.
CATALOG_TOKEN Empty Static metadata bearer token; cannot refresh itself. CATALOG_ACCESS_TOKEN is also accepted.
COUNTRY_CODE US Default Tidal catalog region.
ATMOS_MODE prefer Default playback mode: prefer, off, or high. The admin setting and ?atmos= can override it.
USE_PROXIES, PROXIES_FILE false, proxies.txt Initial outbound proxy setting and proxy list. Editable in admin and persisted with SQLite.
FALLBACK_TO_DIRECT_CONNECTION false Use the host connection when no proxy works. This exposes the host IP to Tidal.
ROTATE_PROXIES_ON_REFRESH false Whether to rotate an account's proxy on token refresh.
TRUST_PROXY_HEADERS true Use X-Forwarded-For and X-Real-IP to identify clients. Enable only behind a trusted reverse proxy that replaces client-supplied forwarding headers; set false for direct access.
DISCORD_WEBHOOK_URL Empty Initial Discord webhook for account and outage alerts. Admin → System → Notifications can replace or disable it without a restart; the saved value persists in SQLite and shared Redis when enabled. The panel never returns the saved URL.
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN Empty Optional shared state across instances using Upstash REST.
REDIS_POOL Empty Alternative native Redis/Valkey URL. Takes precedence over the Upstash pair.
RUST_LOG info Log filter.

USER_AGENT and DEV_MODE configure upstream HTTP requests and diagnostics. See .env.example for their defaults. PUBLIC_POOL_REDIS_URL is a deprecated fallback for REDIS_POOL.

The maximum number of upstream requests made on one account is configured live in Admin → System, independently for track playback and catalog metadata. Both default to 1 and persist in SQLite (and shared Redis when enabled). A separate setting caps how many metadata requests one catalog account serves at the same time: 0 (the default) means no cap, and when every catalog account is at the cap the overflow goes to the playback pool. With no playback pool the overflow gets 503 with Retry-After: 1. The cap does not apply to the static catalog token or to metadata served by the playback pool.

Proxies

proxies.txt accepts one http:// or https:// proxy URL per line, optionally with credentials. Each Tidal account is assigned a working proxy, with unused proxies preferred so accounts spread across the pool. A proxy counts as working when Tidal answers an API request through it; one whose address Tidal's edge refuses (a non-JSON 403) is used only when no accepted proxy is left. Assignments persist in local SQLite and are reused after restart. With shared Redis, instances publish a fingerprint of each assignment (never the proxy URL) and follow one another, so an account leaves through the same proxy on every instance that lists it. When working proxies are added, accounts sharing a proxy are moved to the new spare proxies until the pool is balanced; unaffected bindings stay in place. After three consecutive connection or timeout errors, only the affected account moves to another working proxy. If no proxy is usable, Tidal requests fail unless FALLBACK_TO_DIRECT_CONNECTION=true. The admin panel can change the list and switch proxy mode without restarting the server. ROTATE_PROXIES_ON_REFRESH=true rotates only the account being refreshed.

Multiple instances

Configure either the Upstash pair or REDIS_POOL on every instance in a fleet. Shared state includes app settings, Tidal access tokens, account credentials, and API key definitions and usage. SQLite still belongs to each instance. Playback jobs, request logs, metadata cache, and proxy settings remain local. Protect Redis credentials: the shared state contains Tidal credentials and the Discord webhook token. Database backups also contain the saved webhook.

Without Redis, each instance operates independently. If Redis becomes unavailable, the service continues with local state until sync recovers.

Admin and API authentication

The admin panel is at /admin. With ADMIN_KEY set, sign-in creates an HttpOnly session cookie; admin API clients can use X-Admin-Key. Five incorrect admin keys from one IP within five minutes lock admin authentication for 15 minutes (429 with Retry-After). The same limit applies when X-Admin-Key is used on public API routes. Missing credentials and expired session cookies are not counted. Successful authentication clears earlier failures. Lockouts are local to each instance and reset on restart. Leaving ADMIN_KEY empty leaves the admin panel open.

The panel manages OAuth accounts, catalog flags, credentials import/export, API keys, proxy settings, Atmos and auto-heal settings, alerts, cache, backups, and request statistics. Its interface defaults to English; choose English or Russian in System → Panel settings. The choice is saved in a browser cookie for one year. Overview shows the total completed API requests since the instance started, completed API requests per second averaged over the last 60 seconds, and p95 response time from up to 5000 recent logged requests. These are observed per-instance metrics, not the server's maximum capacity. Working now counts enabled accounts with a current, non-rejected token and no active 429 pause; playback accounts must also have a successful FULL probe no more than eight hours old, while catalog accounts need only a current token. The enabled count remains visible separately. The existing total request and error cards count account selections and account errors. The live log records recent API requests with status, latency, client IP, endpoint counts, and top tracks; More fields reveals the account-selection and failover chain without exposing credentials. Admin, health, and favicon requests are excluded. Three requests to scanner paths such as wp-includes, .env, or .git/config within one minute temporarily block the client IP from all routes for 15 minutes (403 with Retry-After). The ban is local to each instance and resets on restart.

The request log also separates 4xx responses (except 429) from 429/5xx, shows slow routes, and labels cache hits, stale responses, and negative-cache responses. The always-on account maintenance pass automatically probes active playback accounts for FULL or PREVIEW about every six hours, staggering the first checks over five minutes and later checks over an additional fifteen-minute window. At most four accounts are checked concurrently. Check FULL runs the same probe immediately. A probe may try up to four Tidal tracks; Three automatic checks in a row that return PREVIEW move the account to catalog-only, so it keeps serving metadata but leaves playback; a FULL result resets the count, and unknown results neither count nor reset it. The count survives restarts. Manual checks never move an account, and a catalog account returns to playback only when you unflag it. Catalog, manually disabled, and rate-limited accounts are skipped. A subscription flag alone is not used as a playback verdict. The System page shows cache counters and can clear queued playback jobs. These diagnostics are local to the running instance and do not guarantee that Tidal will keep an account active.

Public API routes are open until you create the first API key. After that, send X-API-Key with requests, or use X-Admin-Key as the owner. /, /health, and /admin are exempt from API key checks. API keys can have usage quotas.

API

All routes below are registered by the Rust server. The catalog routes return JSON derived from Tidal; available fields vary with the upstream response. Paths shown with a trailing slash require that slash.

Method and path Query parameters Result
GET / — API version and repository field.
GET /health — Liveness and Redis connection status.
GET /info/ id Track metadata.
GET /search/ One of s (track), a (artist), al (album), v (video), p (playlist), i (ISRC); optional limit, offset Search results. Default limit=25, offset=0.
GET /album/ id; optional limit, offset Album and tracks. Default limit=100, offset=0.
GET /artist/ id or f; optional skip_tracks Artist details or a related artist view.
GET /album/similar/, GET /artist/similar/ id; optional cursor Similar items.
GET /mix/ id Mix and its tracks.
GET /playlist/ id; optional offset Playlist and a page of items (100 per request).
GET /recommendations/ id Up to 20 recommended tracks.
GET /cover/ id or q Cover URLs.
GET /lyrics/ id Track lyrics.
GET /topvideos/ Optional countryCode, locale, deviceType, limit, offset Recommended videos.
GET /track/{id}/{quality}, GET /track/{id}, or GET /track/?id=... Optional quality in query for older paths; immersiveaudio Playback info as JSON. Default HIGH uses v1; other supported qualities use v2.
GET /trackManifests/{id} or GET /trackManifests/?id=... Optional formats, adaptive, manifestType, uriScheme, usage, countryCode, atmos Track manifest data. formats accepts commas or repeated parameters.
GET /dash/{id} Optional atmos Redirects to a fresh Tidal DASH manifest, or a direct AAC file when HIGH is selected.
GET /video/ id; optional quality, mode, presentation Video playback info. Defaults: HIGH, STREAM, FULL.
GET or POST /widevine Challenge in request body when required Proxies the Widevine license request.
GET, DELETE /playback/requests/{request_id} — Poll or cancel a queued playback job.

Examples:

curl 'http://127.0.0.1:8000/health'
curl 'http://127.0.0.1:8000/search/?s=Billie%20Jean'
curl 'http://127.0.0.1:8000/trackManifests/1781887?formats=FLAC_HIRES,FLAC&atmos=off'
curl -s -D - -o /dev/null 'http://127.0.0.1:8000/dash/1781887?atmos=off'

Add -H 'X-API-Key: YOUR_KEY' to the curl commands after enabling API keys.

Playback queue and formats

/track/, /trackManifests, /dash, /widevine, and /video/ use playback accounts. The queue allows as many simultaneous requests as there are playback accounts able to serve right now (enabled, usable token, no 429 cooldown; at least one). Each request goes to the account with the fewest requests in flight, so parallel requests take one account each before any account takes a second; usage, recency and error rate order equally busy accounts. An account that joins or returns to the pool starts level with the others instead of catching up on the requests it missed. If all slots are busy, the API returns 202 Accepted with a Location header pointing to /playback/requests/{request_id} and a Retry-After header. Poll that URL with GET until it returns the original result; DELETE cancels the job. Finished jobs expire after five minutes. Catalog accounts do not serve playback.

The pending queue is capped at the playback pool size. Once full, new requests receive 503 with Retry-After: 5 instead of accumulating. Pending jobs without a poll for 60 seconds are cancelled, and a background reaper removes finished jobs after five minutes. Each playback operation times out after 120 seconds. Metadata GET responses use a one-hour fresh cache plus a one-hour stale window with background refresh; repeatable 404/429/5xx responses have short negative-cache lifetimes. Playback and Widevine responses are excluded from this shared cache.

When Tidal responds with 429, the API pauses that credential for the upstream Retry-After interval (seconds or HTTP date; 30 seconds when absent) and tries one other eligible credential for user-facing playback, Widevine, video, or catalog requests. If that backup also receives 429 or none is available, the caller receives 429 with Retry-After. Token refreshes and manual account probes also pause the affected credential; account-specific probes do not switch accounts. The pause is stored in SQLite and survives a restart (ephemeral mode keeps it in memory only); instances do not share it through Redis.

/trackManifests accepts formats=FLAC_HIRES,FLAC,AACLC or repeated formats parameters. It also accepts atmos=prefer, atmos=only, or atmos=off. Explicit formats take precedence except when atmos=prefer adds Atmos first or atmos=only selects only Atmos. /dash/{id} supports the same Atmos preference, but uses its own fixed format list. In the admin panel, HIGH · AAC 320 kbps makes /dash/{id} redirect to a direct AAC file from v1 without calling v2; atmos=prefer or atmos=only still requests v2. /trackManifests always exposes the raw v2 endpoint. atmos=off requests non-Atmos formats for a conventional DASH player when the FLAC or Atmos preference is selected. Atmos playback requires a compatible player and may require Widevine.

Admin → System → Playback selects the v1 playback method: playbackinfopostpaywall (default) or the legacy playbackinfo. The choice applies immediately to v1 track playback, the HIGH /dash path, and automatic/manual FULL checks. It persists in SQLite and syncs through Redis. Changing it uses a separate playback cache entry. v2 formats continue to use /trackManifests.

/track/{id}/{quality} puts quality in the path, for example /track/1781887/HIGH. /track/{id} defaults to HIGH, and the older ?quality= and ?id= query forms still work. HIGH uses v1; LOW/HEAACV1, AACLC, LOSSLESS/FLAC, HI_RES_LOSSLESS/FLAC_HIRES, and DOLBY_ATMOS/EAC3_JOC each request one format through v2. Unsupported values return 200 OK with status=unsupported_quality and a supportedQualities list, without calling Tidal. The v1 and v2 JSON responses have different shapes; see USAGE.md.

The FLAC and Atmos /dash/{id} response points to a DASH .mpd manifest (application/dash+xml). A browser may download that text manifest; play the URL in a DASH-capable player. The HIGH setting redirects to an AAC/MP4 file instead.

If every available playback account receives only a preview for a track, the API returns 503 instead of presenting the preview as a full track. Quality and format availability ultimately depend on Tidal.

For examples of extracting and playing media URLs, see USAGE.md.

License

See LICENSE.

About

simple api for tidal (custom rewrite)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages