chore: added documentation
This commit is contained in:
106
docs/monochrome.md
Normal file
106
docs/monochrome.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Monochrome API Architecture
|
||||
|
||||
## How It Works
|
||||
Monochrome is a web frontend that proxies audio from Tidal/Qobuz through distributed API instances. It does NOT host audio itself.
|
||||
|
||||
## Instance Discovery
|
||||
1. **Uptime monitor**: `https://tidal-uptime.jiffy-puffs-1j.workers.dev/` — returns list of live instances
|
||||
2. **Hardcoded fallbacks** (some may go down over time):
|
||||
- `https://monochrome.tf`
|
||||
- `https://triton.squid.wtf`
|
||||
- `https://qqdl.site`
|
||||
- `https://monochrome.samidy.com`
|
||||
- `https://api.monochrome.tf`
|
||||
3. More instances listed at: https://github.com/monochrome-music/monochrome/blob/main/INSTANCES.md
|
||||
|
||||
## API Endpoints (on any instance)
|
||||
|
||||
### Stream/download: `GET /track/?id={trackId}&quality={quality}`
|
||||
- **Response envelope**: `{"version": "2.x", "data": { ... }}`
|
||||
- **Inside `data`**: `manifest` (base64), `audioQuality`, `trackId`, replay gain fields
|
||||
- **`manifest` decodes to**: JSON `{"mimeType":"audio/flac","codecs":"flac","urls":["https://..."]}`
|
||||
- Alternative: `OriginalTrackUrl` field (direct URL, skip manifest)
|
||||
|
||||
### Metadata: `GET /info/?id={trackId}`
|
||||
- Same envelope wrapping
|
||||
- Returns: title, duration, artist, artists[], album (with cover UUID), trackNumber, volumeNumber, copyright, isrc, streamStartDate, bpm, key, explicit, etc.
|
||||
|
||||
### Search: `GET /search/?s={query}` (tracks), `?a=` (artists), `?al=` (albums), `?p=` (playlists)
|
||||
|
||||
### Album: `GET /album/?id={albumId}&offset={n}&limit=500`
|
||||
|
||||
### Qobuz alternative: `https://qobuz.squid.wtf/api`
|
||||
- Search: `/get-music?q={query}`
|
||||
- Stream: `/download-music?track_id={id}&quality={qobuzQuality}`
|
||||
- Quality mapping: 27=MP3_320, 7=FLAC, 6=HiRes96/24, 5=HiRes192/24
|
||||
- Track IDs prefixed with `q:` in the frontend (e.g. `q:12345`)
|
||||
|
||||
## Quality Levels (Tidal instances)
|
||||
| Quality param | What you get | Works? |
|
||||
|-------------------|-----------------------|--------|
|
||||
| HI_RES_LOSSLESS | Best available (FLAC) | Yes |
|
||||
| HI_RES | FLAC | Yes |
|
||||
| LOSSLESS | 16-bit/44.1kHz FLAC | Yes |
|
||||
| HIGH | AAC 320kbps | Yes |
|
||||
| LOW | AAC 96kbps | Yes |
|
||||
| MP3_320 | N/A | **404** — not a valid API quality |
|
||||
|
||||
## Manifest Decoding (3 types)
|
||||
1. **JSON** (most common): `{"mimeType":"audio/flac","urls":["https://lgf.audio.tidal.com/..."]}` — use `urls[0]`
|
||||
2. **DASH XML**: Contains `<MPD>` — extract `<BaseURL>` if present, otherwise needs dash.js (unsupported in CLI)
|
||||
3. **Raw URL**: Just a URL string in the decoded base64
|
||||
|
||||
## Cover Art
|
||||
- Source: Tidal CDN
|
||||
- URL pattern: `https://resources.tidal.com/images/{cover_uuid_with_slashes}/{size}x{size}.jpg`
|
||||
- The album `cover` field is a UUID like `d8170d28-d09b-400a-ae83-6c9dea002b4d`
|
||||
- Replace `-` with `/` to form the path: `d8170d28/d09b/400a/ae83/6c9dea002b4d`
|
||||
- Common sizes: 80, 160, 320, 640, 1280
|
||||
|
||||
## Search Response Structure
|
||||
- **Endpoint**: `GET /search/?s={query}`
|
||||
- **Response envelope**: `{"version": "2.x", "data": {"limit": 25, "offset": 0, "totalNumberOfItems": N, "items": [...]}}`
|
||||
- **Each item** in `items[]`:
|
||||
- `id` (Tidal track ID), `title`, `duration`, `trackNumber`, `volumeNumber`
|
||||
- `artist`: `{"id": N, "name": "...", "picture": "uuid"}`
|
||||
- `artists`: array of artist objects (same shape)
|
||||
- `album`: `{"id": N, "title": "...", "cover": "uuid"}`
|
||||
- `isrc`, `copyright`, `bpm`, `key`, `explicit`, `audioQuality`, `popularity`
|
||||
- **Important**: results are inside `data.items[]`, not `data` directly
|
||||
|
||||
## Frontend Retry Logic
|
||||
- Randomize instance order
|
||||
- Try each instance up to `instances.length * 2` times
|
||||
- 429 (rate limit): 500ms delay, next instance
|
||||
- 401/5xx: next instance
|
||||
- Network error: 200ms delay, next instance
|
||||
|
||||
## Spotify URL Converter (spotify_to_ids.py)
|
||||
|
||||
### How It Works
|
||||
1. **Parse Spotify URL** — regex extracts type (`track`/`album`/`playlist`) and ID
|
||||
2. **Scrape metadata** — fetches `https://open.spotify.com/embed/{type}/{id}`, extracts `__NEXT_DATA__` JSON from HTML
|
||||
3. **Extract tracks** — navigates `props.pageProps.state.data.entity` for title/artist
|
||||
- Track: `name` + `artists[0].name` (or `subtitle`)
|
||||
- Album/Playlist: `trackList[]` array with `title` + `subtitle` per track
|
||||
4. **Fallback** — if embed scraping fails, uses oEmbed API (`/oembed?url=...`) for single tracks (title only, no artist separation)
|
||||
5. **Search Monochrome** — `GET {instance}/search/?s={artist}+{title}`, unwrap envelope, get `data.items[]`
|
||||
6. **Fuzzy match** — normalize strings (strip feat/remaster/punctuation), Jaccard token overlap, weighted 60% title + 40% artist
|
||||
|
||||
### Code Structure (spotify_to_ids.py)
|
||||
- `parse_spotify_url(url)` — regex URL parsing → `(type, id)`
|
||||
- `fetch_spotify_embed(sp_type, sp_id)` — scrape embed page `__NEXT_DATA__` JSON
|
||||
- `fetch_spotify_oembed(sp_type, sp_id)` — oEmbed API fallback
|
||||
- `extract_tracks(embed_data, sp_type, sp_id)` — navigate JSON → list of `{title, artist}`
|
||||
- `normalize(text)` — lowercase, strip feat/remaster/punctuation
|
||||
- `similarity(a, b)` — Jaccard token overlap ratio
|
||||
- `find_best_match(results, title, artist, threshold)` — weighted scoring of search results
|
||||
- `search_monochrome(instances, query)` — search API with envelope unwrapping
|
||||
- Shared utilities copied from download.py: `fetch()`, `fetch_json()`, `discover_instances()`
|
||||
|
||||
### Key Gotchas
|
||||
1. Spotify embed page structure (`__NEXT_DATA__`) is fragile — may break if Spotify redesigns
|
||||
2. oEmbed fallback only works for single tracks, not albums/playlists
|
||||
3. Remixes and live versions often fail to match (different titles on Spotify vs Tidal)
|
||||
4. 0.5s delay between searches to avoid Monochrome 429 rate limits
|
||||
5. All progress/errors go to stderr; only track IDs go to stdout (for piping)
|
||||
Reference in New Issue
Block a user