95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
"""
|
|
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)
|