From 07b89e4018aa901076071c7e10ac640328f5f094 Mon Sep 17 00:00:00 2001 From: fchinembiri Date: Fri, 10 Jul 2026 09:17:52 +0200 Subject: [PATCH] feat(musicseerr): implement Last.fm recommendation auto-downloader --- apps/musicseerr/main.py | 118 ++++++++++++++++++++++++++++++++ k8s/family-apps/musicseerr.yaml | 4 ++ 2 files changed, 122 insertions(+) diff --git a/apps/musicseerr/main.py b/apps/musicseerr/main.py index 4e2936d..bd39877 100644 --- a/apps/musicseerr/main.py +++ b/apps/musicseerr/main.py @@ -456,3 +456,121 @@ def index_page(): """ return HTMLResponse(content=html_content) + + +def run_lastfm_auto_download(username: str): + url = f"https://lfm.xiffy.nl/{username}/recommended" + proxies = {"http": PROXY_URL, "https": PROXY_URL} if PROXY_URL else None + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } + + logger.info(f"Fetching Last.fm recommendations feed from {url}") + try: + response = requests.get(url, headers=headers, proxies=proxies, timeout=20) + response.raise_for_status() + except Exception as e: + logger.error(f"Failed to fetch Last.fm recommendations feed: {e}") + return + + try: + import xml.etree.ElementTree as ET + root = ET.fromstring(response.content) + items = root.findall(".//item") + except Exception as e: + logger.error(f"Failed to parse Last.fm recommendations XML: {e}") + return + + logger.info(f"Found {len(items)} items in Last.fm feed.") + + # Import tasks dynamically to avoid circular import + from tasks import run_download_task + import re + + def is_track_already_downloaded(track_query: str) -> bool: + remote_base = "/remote-music/music" + if not os.path.isdir(remote_base): + return False + clean_query = re.sub(r'[^\w\s-]', '', track_query).lower() + parts = track_query.split(" - ", 1) + if len(parts) == 2: + artist, title = parts[0].strip().lower(), parts[1].strip().lower() + artist_clean = re.sub(r'[^\w\s-]', '', artist) + title_clean = re.sub(r'[^\w\s-]', '', title) + else: + artist_clean = clean_query + title_clean = clean_query + + for r, d, files in os.walk(remote_base): + for file in files: + if file.lower().endswith((".mp3", ".flac", ".m4a", ".ogg", ".wav", ".opus")): + file_lower = file.lower() + if len(parts) == 2: + if artist_clean in file_lower and title_clean in file_lower: + return True + else: + if clean_query in file_lower: + return True + return False + + for item in items: + title_el = item.find("title") + if title_el is not None and title_el.text: + track_query = title_el.text.strip() + + # Check if already queued + already_queued = any(job.get("query") == f"[Last.fm Auto] {track_query}" for job in JOBS.values()) + if already_queued: + continue + + if is_track_already_downloaded(track_query): + logger.info(f"Skipping Last.fm auto-download for '{track_query}' (already exists).") + continue + + logger.info(f"Queueing Last.fm recommendation: '{track_query}'") + task_id = str(uuid.uuid4()) + JOBS[task_id] = { + "id": task_id, + "query": f"[Last.fm Auto] {track_query}", + "status": "pending", + "priority_used": None, + "files": [], + "error": None + } + try: + run_download_task(task_id, track_query, PROXY_URL, JOBS) + except Exception as e: + logger.error(f"Failed to process auto-download job {task_id}: {e}") + + +@app.post("/lastfm/trigger") +def trigger_lastfm_sync(background_tasks: BackgroundTasks): + username = os.getenv("LASTFM_AUTO_DOWNLOAD_USER") + if not username: + return JSONResponse(status_code=400, content={"error": "Last.fm auto-download is not enabled (LASTFM_AUTO_DOWNLOAD_USER not configured)"}) + background_tasks.add_task(run_lastfm_auto_download, username) + return {"status": "triggered", "user": username} + + +import asyncio + +async def lastfm_scheduler(): + # Wait a short bit after startup before the first check + await asyncio.sleep(30) + while True: + username = os.getenv("LASTFM_AUTO_DOWNLOAD_USER") + if username: + try: + logger.info(f"Triggering scheduled Last.fm recommendations fetch for '{username}'...") + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, run_lastfm_auto_download, username) + except Exception as e: + logger.error(f"Error in Last.fm scheduler task: {e}") + + interval_hours = float(os.getenv("LASTFM_AUTO_DOWNLOAD_INTERVAL_HOURS", "12")) + await asyncio.sleep(interval_hours * 3600) + + +@app.on_event("startup") +async def startup_event(): + asyncio.create_task(lastfm_scheduler()) diff --git a/k8s/family-apps/musicseerr.yaml b/k8s/family-apps/musicseerr.yaml index 01a61b3..c9fb2d7 100644 --- a/k8s/family-apps/musicseerr.yaml +++ b/k8s/family-apps/musicseerr.yaml @@ -64,6 +64,10 @@ spec: name: musicseerr-secrets key: telegram-bot-token optional: true + - name: LASTFM_AUTO_DOWNLOAD_USER + value: "fchinex" + - name: LASTFM_AUTO_DOWNLOAD_INTERVAL_HOURS + value: "12" ports: - name: http containerPort: 8000