fix(musicseerr): move downloaded files immediately to seedbox, bypass Cloudflare with curl_cffi, and fix Last.fm recommendations feed fetching
Build and Push Docker Images / deploy (push) Blocked by required conditions Details
Build and Push Docker Images / build (api) (push) Successful in 2m29s Details
Build and Push Docker Images / build (musicseerr) (push) Successful in 6m55s Details
Build and Push Docker Images / build (web) (push) Successful in 1m44s Details
Build and Push Docker Images / build (nextgen) (push) Successful in 7m41s Details
Build and Push Docker Images / build (worker) (push) Has been cancelled Details

This commit is contained in:
fchinembiri 2026-07-10 20:19:05 +02:00
parent c900287dae
commit 732f71700c
3 changed files with 65 additions and 37 deletions

View File

@ -22,7 +22,8 @@ RUN pip install --no-cache-dir \
beautifulsoup4 \
mutagen \
spotdl \
yt-dlp
yt-dlp \
curl_cffi
# Copy application files
COPY main.py tasks.py /app/

View File

@ -5,6 +5,7 @@ from fastapi import FastAPI, BackgroundTasks, Request, Form
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel
import requests
from curl_cffi import requests as cf_requests
from tasks import run_download_task
@ -460,18 +461,28 @@ def index_page():
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}")
response = None
try:
response = requests.get(url, headers=headers, proxies=proxies, timeout=20)
logger.info("Fetching Last.fm feed directly (no proxy)")
response = cf_requests.get(url, headers=headers, impersonate="chrome", timeout=20)
response.raise_for_status()
except Exception as e:
logger.error(f"Failed to fetch Last.fm recommendations feed: {e}")
return
logger.warning(f"Failed to fetch Last.fm feed directly: {e}. Retrying with proxy...")
if PROXY_URL:
proxies = {"http": PROXY_URL, "https": PROXY_URL}
try:
response = cf_requests.get(url, headers=headers, proxies=proxies, impersonate="chrome", timeout=20)
response.raise_for_status()
except Exception as e2:
logger.error(f"Failed to fetch Last.fm feed with proxy: {e2}")
return
else:
return
try:
import xml.etree.ElementTree as ET

View File

@ -9,11 +9,35 @@ from bs4 import BeautifulSoup
from urllib.parse import urljoin
from mutagen.mp3 import EasyMP3
from mutagen.id3 import ID3
from curl_cffi import requests as cf_requests
# Configure Logger
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("musicseerr-tasks")
def move_new_files_to_dest(temp_dir: str, dest_dir: str) -> list:
moved = []
if not os.path.exists(temp_dir):
return moved
os.makedirs(dest_dir, exist_ok=True)
for file_name in os.listdir(temp_dir):
src_path = os.path.join(temp_dir, file_name)
# Check if it is a regular file and not a symlink
if os.path.isfile(src_path) and not os.path.islink(src_path):
dest_path = os.path.join(dest_dir, file_name)
logger.info(f"Moving new file '{file_name}' to remote Seedbox directory: {dest_path}")
try:
shutil.move(src_path, dest_path)
moved.append(file_name)
# Recreate as symlink in temp_dir so subsequent tools can find it if needed
try:
os.symlink(dest_path, src_path)
except Exception as sym_err:
logger.warning(f"Failed to recreate symlink for {file_name}: {sym_err}")
except Exception as move_err:
logger.error(f"Failed to move file {file_name} to {dest_dir}: {move_err}")
return moved
def scrape_tubidy_link(url: str, proxy_url: str) -> str:
"""
Scrapes a Tubidy page to find the direct MP3 download URL.
@ -24,7 +48,7 @@ def scrape_tubidy_link(url: str, proxy_url: str) -> str:
}
logger.info(f"Fetching Tubidy page: {url} via proxy: {proxy_url}")
response = requests.get(url, headers=headers, proxies=proxies, timeout=20)
response = cf_requests.get(url, headers=headers, proxies=proxies, impersonate="chrome", timeout=20)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
@ -51,7 +75,7 @@ def scrape_tubidy_link(url: str, proxy_url: str) -> str:
sub_url = urljoin(url, a["href"])
logger.info(f"Following nested Tubidy download link: {sub_url}")
try:
sub_resp = requests.get(sub_url, headers=headers, proxies=proxies, timeout=15)
sub_resp = cf_requests.get(sub_url, headers=headers, proxies=proxies, impersonate="chrome", timeout=15)
if sub_resp.status_code == 200:
sub_soup = BeautifulSoup(sub_resp.text, "html.parser")
for sub_a in sub_soup.find_all("a", href=True):
@ -88,7 +112,7 @@ def search_tubidy(query: str, proxy_url: str) -> list:
logger.info(f"Searching Tubidy for '{query}' via proxy: {proxy_url}")
try:
response = requests.get(search_url, headers=headers, params=params, proxies=proxies, timeout=15)
response = cf_requests.get(search_url, headers=headers, params=params, proxies=proxies, impersonate="chrome", timeout=15)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
@ -142,7 +166,7 @@ def get_tubidy_download_link(content_id: str, proxy_url: str) -> str:
logger.info(f"Fetching watch.php for content ID '{content_id}'")
try:
response = requests.get(watch_url, headers=headers, params=params, proxies=proxies, timeout=15)
response = cf_requests.get(watch_url, headers=headers, params=params, proxies=proxies, impersonate="chrome", timeout=15)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
@ -175,7 +199,7 @@ def download_file_stream(url: str, dest_path: str, proxy_url: str):
}
logger.info(f"Streaming download from {url} to {dest_path}")
response = requests.get(url, headers=headers, proxies=proxies, stream=True, timeout=60)
response = cf_requests.get(url, headers=headers, proxies=proxies, impersonate="chrome", stream=True, timeout=60)
response.raise_for_status()
with open(dest_path, "wb") as f:
@ -246,7 +270,7 @@ def extract_spotify_track_title(url: str, proxy_url: str) -> str:
logger.info(f"Scraping Spotify track via embed URL: {embed_url}")
try:
response = requests.get(embed_url, headers=headers, proxies=proxies, timeout=15)
response = cf_requests.get(embed_url, headers=headers, proxies=proxies, impersonate="chrome", timeout=15)
if response.status_code == 200:
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', response.text)
if next_data_match:
@ -287,7 +311,7 @@ def scrape_spotify_playlist(url: str, proxy_url: str) -> list:
try:
# Short jitter to avoid hitting Spotify exactly at the same time
time.sleep(1)
response = requests.get(embed_url, headers=headers, proxies=current_proxies, timeout=15)
response = cf_requests.get(embed_url, headers=headers, proxies=current_proxies, impersonate="chrome", timeout=15)
if response.status_code == 200:
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', response.text)
if next_data_match:
@ -488,6 +512,11 @@ def run_download_task(task_id: str, query: str, proxy_url: str, jobs_dict: dict,
if track_success:
download_count += 1
new_moved = move_new_files_to_dest(temp_dir, "/remote-music/music")
if "files" not in jobs_dict[task_id] or not jobs_dict[task_id]["files"]:
jobs_dict[task_id]["files"] = []
jobs_dict[task_id]["files"].extend(new_moved)
logger.info(f"[{task_id}] Successfully downloaded and moved {len(new_moved)} files for track '{track_query}'")
if download_count > 0:
download_successful = True
@ -638,38 +667,25 @@ def run_download_task(task_id: str, query: str, proxy_url: str, jobs_dict: dict,
if not download_successful:
raise Exception("Ingestion finished but no download was successful.")
downloaded_files = os.listdir(temp_dir)
actual_files = [f for f in downloaded_files if not os.path.islink(os.path.join(temp_dir, f))]
if not actual_files:
if downloaded_files:
logger.info(f"[{task_id}] All tracks in this query were already downloaded (skipped duplicates).")
if download_successful and not is_playlist:
new_moved = move_new_files_to_dest(temp_dir, "/remote-music/music")
if "files" not in jobs_dict[task_id] or not jobs_dict[task_id]["files"]:
jobs_dict[task_id]["files"] = []
jobs_dict[task_id]["status"] = "completed"
if chat_id:
send_telegram_notification(chat_id, f"✅ Successfully ingested request: '{query}'\n\n(All tracks already existed - skipped duplicates)")
return
else:
raise Exception("Ingestion finished successfully but no files were found in local temp folder.")
jobs_dict[task_id]["files"].extend(new_moved)
os.makedirs("/remote-music/music", exist_ok=True)
moved_files = []
for file_name in actual_files:
src_file = os.path.join(temp_dir, file_name)
dest_file = os.path.join("/remote-music/music", file_name)
if "files" not in jobs_dict[task_id] or not jobs_dict[task_id]["files"]:
logger.info(f"[{task_id}] All tracks in this query were already downloaded (skipped duplicates).")
jobs_dict[task_id]["files"] = []
jobs_dict[task_id]["status"] = "completed"
if chat_id:
send_telegram_notification(chat_id, f"✅ Successfully ingested request: '{query}'\n\n(All tracks already existed - skipped duplicates)")
return
logger.info(f"[{task_id}] Moving new file '{file_name}' to remote Seedbox directory")
shutil.move(src_file, dest_file)
moved_files.append(file_name)
jobs_dict[task_id]["files"] = moved_files
jobs_dict[task_id]["status"] = "completed"
logger.info(f"[{task_id}] Music Ingestion task fully completed.")
if chat_id:
files_str = "\n".join(moved_files)
files_str = "\n".join(jobs_dict[task_id]["files"])
send_telegram_notification(chat_id, f"✅ Successfully ingested request: '{query}'\n\nFiles imported to Navidrome:\n{files_str}")
except Exception as e: