Updated monochrome and votify

This commit is contained in:
2026-08-16 23:29:15 +02:00
parent d136ef26c5
commit 1319121a4f
7 changed files with 130 additions and 64 deletions
+52 -32
View File
@@ -3,22 +3,32 @@ Monochrome - shared utilities for Tidal/Qobuz music downloading via Monochrome A
"""
import json
import re
import ssl
import sys
import urllib.request
import urllib.error
# Hardcoded fallback API instances
# 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.tf",
"https://triton.squid.wtf",
"https://qqdl.site",
"https://monochrome.samidy.com",
"https://api.monochrome.tf",
"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"
UPTIME_URL = "https://tidal-uptime.jiffy-puffs-1j.workers.dev/"
# 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()
@@ -39,36 +49,46 @@ def fetch_json(url, timeout=15, use_ssl_ctx=True):
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 uptime monitor, fall back to hardcoded list."""
"""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:
data = fetch_json(UPTIME_URL, timeout=10)
if isinstance(data, dict):
urls = []
for key, val in data.items():
if isinstance(val, dict) and val.get("url"):
urls.append(val["url"].rstrip("/"))
elif isinstance(val, str) and val.startswith("http"):
urls.append(val.rstrip("/"))
if urls:
log(f"[*] Discovered {len(urls)} instances from uptime monitor")
return urls
elif isinstance(data, list):
urls = []
for item in data:
if isinstance(item, str) and item.startswith("http"):
urls.append(item.rstrip("/"))
elif isinstance(item, dict):
u = item.get("url") or item.get("uri") or ""
if u.startswith("http"):
urls.append(u.rstrip("/"))
if urls:
log(f"[*] Discovered {len(urls)} instances from uptime monitor")
return urls
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"[!] Uptime discovery failed: {e}")
log(f"[!] Instance discovery failed: {e}")
log(f"[*] Using {len(FALLBACK_INSTANCES)} fallback instances")
return list(FALLBACK_INSTANCES)