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 \ beautifulsoup4 \
mutagen \ mutagen \
spotdl \ spotdl \
yt-dlp yt-dlp \
curl_cffi
# Copy application files # Copy application files
COPY main.py tasks.py /app/ 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 fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel from pydantic import BaseModel
import requests import requests
from curl_cffi import requests as cf_requests
from tasks import run_download_task from tasks import run_download_task
@ -460,18 +461,28 @@ def index_page():
def run_lastfm_auto_download(username: str): def run_lastfm_auto_download(username: str):
url = f"https://lfm.xiffy.nl/{username}/recommended" url = f"https://lfm.xiffy.nl/{username}/recommended"
proxies = {"http": PROXY_URL, "https": PROXY_URL} if PROXY_URL else None
headers = { 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" "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}") logger.info(f"Fetching Last.fm recommendations feed from {url}")
response = None
try: 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() response.raise_for_status()
except Exception as e: except Exception as e:
logger.error(f"Failed to fetch Last.fm recommendations feed: {e}") logger.warning(f"Failed to fetch Last.fm feed directly: {e}. Retrying with proxy...")
return 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: try:
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET

View File

@ -9,11 +9,35 @@ from bs4 import BeautifulSoup
from urllib.parse import urljoin from urllib.parse import urljoin
from mutagen.mp3 import EasyMP3 from mutagen.mp3 import EasyMP3
from mutagen.id3 import ID3 from mutagen.id3 import ID3
from curl_cffi import requests as cf_requests
# Configure Logger # Configure Logger
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("musicseerr-tasks") 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: def scrape_tubidy_link(url: str, proxy_url: str) -> str:
""" """
Scrapes a Tubidy page to find the direct MP3 download URL. 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}") 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() response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser") 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"]) sub_url = urljoin(url, a["href"])
logger.info(f"Following nested Tubidy download link: {sub_url}") logger.info(f"Following nested Tubidy download link: {sub_url}")
try: 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: if sub_resp.status_code == 200:
sub_soup = BeautifulSoup(sub_resp.text, "html.parser") sub_soup = BeautifulSoup(sub_resp.text, "html.parser")
for sub_a in sub_soup.find_all("a", href=True): 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}") logger.info(f"Searching Tubidy for '{query}' via proxy: {proxy_url}")
try: 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() response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser") 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}'") logger.info(f"Fetching watch.php for content ID '{content_id}'")
try: 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() response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser") 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}") 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() response.raise_for_status()
with open(dest_path, "wb") as f: 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}") logger.info(f"Scraping Spotify track via embed URL: {embed_url}")
try: 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: if response.status_code == 200:
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', response.text) next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', response.text)
if next_data_match: if next_data_match:
@ -287,7 +311,7 @@ def scrape_spotify_playlist(url: str, proxy_url: str) -> list:
try: try:
# Short jitter to avoid hitting Spotify exactly at the same time # Short jitter to avoid hitting Spotify exactly at the same time
time.sleep(1) 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: if response.status_code == 200:
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', response.text) next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', response.text)
if next_data_match: 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: if track_success:
download_count += 1 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: if download_count > 0:
download_successful = True 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: if not download_successful:
raise Exception("Ingestion finished but no download was successful.") raise Exception("Ingestion finished but no download was successful.")
downloaded_files = os.listdir(temp_dir) if download_successful and not is_playlist:
actual_files = [f for f in downloaded_files if not os.path.islink(os.path.join(temp_dir, f))] 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"]:
if not actual_files:
if downloaded_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]["files"] = []
jobs_dict[task_id]["status"] = "completed" jobs_dict[task_id]["files"].extend(new_moved)
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.")
os.makedirs("/remote-music/music", exist_ok=True) if "files" not in jobs_dict[task_id] or not jobs_dict[task_id]["files"]:
moved_files = [] logger.info(f"[{task_id}] All tracks in this query were already downloaded (skipped duplicates).")
jobs_dict[task_id]["files"] = []
for file_name in actual_files: jobs_dict[task_id]["status"] = "completed"
src_file = os.path.join(temp_dir, file_name) if chat_id:
dest_file = os.path.join("/remote-music/music", file_name) 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" jobs_dict[task_id]["status"] = "completed"
logger.info(f"[{task_id}] Music Ingestion task fully completed.") logger.info(f"[{task_id}] Music Ingestion task fully completed.")
if chat_id: 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}") send_telegram_notification(chat_id, f"✅ Successfully ingested request: '{query}'\n\nFiles imported to Navidrome:\n{files_str}")
except Exception as e: except Exception as e: