Skip to content
 
 

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 active account pool. 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. 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.

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.

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).

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. Assignments persist in local SQLite and are reused after restart. 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; PREVIEW and unknown results update diagnostics but do not change account availability. 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 up to the number of active playback accounts to run simultaneously. 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 held in memory on each server instance and resets on restart; 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.

/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