""" Monochrome - shared utilities for Tidal/Qobuz music downloading via Monochrome API instances. """ import json import re import ssl import sys import urllib.request import urllib.error # Hardcoded fallback API instances (hifi-api compatible: /search/, /track/). # Mirrors the "API Instances" section of the official INSTANCES.md; kept in sync # manually as a last resort for when the live INSTANCES.md fetch fails. # NOTE: the official Monochrome app has moved to a Turnstile-gated "Unified # Playback API" (music-api.geeked.wtf) that a headless client cannot use. These # are the remaining open, hifi-api-style instances. See docs for details. FALLBACK_INSTANCES = [ "https://monochrome-api.samidy.com", "https://wolf.qqdl.site", "https://maus.qqdl.site", "https://vogel.qqdl.site", "https://katze.qqdl.site", "https://hund.qqdl.site", "https://tidal.kinoplus.online", ] QOBUZ_API = "https://qobuz.squid.wtf/api" # Live API-instance list, parsed at runtime from the official repo. Replaces the # old uptime-monitor worker, which is dead (404). INSTANCES_MD_URL = "https://raw.githubusercontent.com/monochrome-music/monochrome/main/INSTANCES.md" # SSL context that doesn't verify certs (some instances have bad certs) SSL_CTX = ssl.create_default_context() SSL_CTX.check_hostname = False SSL_CTX.verify_mode = ssl.CERT_NONE def fetch(url, timeout=15, use_ssl_ctx=True): """Fetch a URL, returning the response object.""" req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) ctx = SSL_CTX if use_ssl_ctx else None return urllib.request.urlopen(req, timeout=timeout, context=ctx) def fetch_json(url, timeout=15, use_ssl_ctx=True): """Fetch a URL and parse JSON response.""" with fetch(url, timeout, use_ssl_ctx) as resp: return json.loads(resp.read().decode()) def parse_api_instances(markdown): """Extract API-instance URLs from the INSTANCES.md markdown. API instances live under the "## API Instances" heading and are written as backtick-wrapped URLs (e.g. `https://monochrome-api.samidy.com`). Other URLs in that section (the Notes column, related links) use [text](url) markdown form, so extracting only backtick-wrapped URLs keeps us to real API bases. """ section = markdown heading = re.search(r'^#+\s*API Instances\s*$', markdown, re.MULTILINE) if heading: section = markdown[heading.end():] # Stop at the next top-level (## ...) section. nxt = re.search(r'^##\s+\S', section, re.MULTILINE) if nxt: section = section[:nxt.start()] urls = [] for m in re.finditer(r'`(https?://[^`\s]+)`', section): u = m.group(1).rstrip("/") if u not in urls: urls.append(u) return urls def discover_instances(log=None): """Get live API instances from the official INSTANCES.md, fall back to the hardcoded list. (The old uptime-monitor worker is dead — 404.)""" if log is None: log = print try: with fetch(INSTANCES_MD_URL, timeout=10, use_ssl_ctx=False) as resp: markdown = resp.read().decode("utf-8", errors="replace") urls = parse_api_instances(markdown) if urls: log(f"[*] Discovered {len(urls)} instances from INSTANCES.md") return urls log("[!] INSTANCES.md fetched but no API instances parsed") except Exception as e: log(f"[!] Instance discovery failed: {e}") log(f"[*] Using {len(FALLBACK_INSTANCES)} fallback instances") return list(FALLBACK_INSTANCES)