feat(musicseerr): implement Last.fm recommendation auto-downloader
Build and Push Docker Images / build (api) (push) Failing after 2m37s Details
Build and Push Docker Images / build (musicseerr) (push) Successful in 8m32s Details
Build and Push Docker Images / build (nextgen) (push) Successful in 8m23s Details
Build and Push Docker Images / build (web) (push) Successful in 5m19s Details
Build and Push Docker Images / build (worker) (push) Successful in 12m58s Details
Build and Push Docker Images / deploy (push) Has been skipped Details

This commit is contained in:
fchinembiri 2026-07-10 09:17:52 +02:00
parent dff1fd4d04
commit 07b89e4018
2 changed files with 122 additions and 0 deletions

View File

@ -456,3 +456,121 @@ def index_page():
</html> </html>
""" """
return HTMLResponse(content=html_content) 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())

View File

@ -64,6 +64,10 @@ spec:
name: musicseerr-secrets name: musicseerr-secrets
key: telegram-bot-token key: telegram-bot-token
optional: true optional: true
- name: LASTFM_AUTO_DOWNLOAD_USER
value: "fchinex"
- name: LASTFM_AUTO_DOWNLOAD_INTERVAL_HOURS
value: "12"
ports: ports:
- name: http - name: http
containerPort: 8000 containerPort: 8000