This commit is contained in:
dvirlabs 2025-08-02 22:16:24 +03:00
parent 3438b1587c
commit 8153b8bce6
7 changed files with 62 additions and 25 deletions

View File

@ -1,4 +1,2 @@
# MUSIC_DIR=/music MUSIC_DIR=/music
# NAVIDROME_SCAN_URL=http://navidrome:4533/api/rescan
MUSIC_DIR=./backend/music
NAVIDROME_SCAN_URL= NAVIDROME_SCAN_URL=

View File

@ -3,8 +3,11 @@ from pydantic import Field
import os import os
class Settings(BaseSettings): class Settings(BaseSettings):
BASE_DIR: str = os.path.dirname(os.path.abspath(__file__)) PROJECT_ROOT: str = os.path.dirname(os.path.abspath(__file__)) # backend/
MUSIC_DIR: str = Field(default=os.path.join(BASE_DIR, "music"), description="Path where songs are saved") MUSIC_DIR: str = Field(
default=os.path.join(PROJECT_ROOT, "music"),
description="Path where songs are saved"
)
NAVIDROME_SCAN_URL: str = Field(default="", description="URL to trigger Navidrome rescan") NAVIDROME_SCAN_URL: str = Field(default="", description="URL to trigger Navidrome rescan")
class Config: class Config:

View File

@ -1,7 +1,8 @@
import subprocess import subprocess
import os import os
from config import settings
import requests import requests
from pathlib import Path
from config import settings
def detect_query_type(query: str): def detect_query_type(query: str):
if "youtube.com/playlist" in query or "list=" in query: if "youtube.com/playlist" in query or "list=" in query:
@ -11,53 +12,64 @@ def detect_query_type(query: str):
else: else:
return "search" return "search"
def download_song(query: str): def download_song(query: str, single: bool = False):
output_template = os.path.join(settings.MUSIC_DIR, "%(uploader)s/%(title)s.%(ext)s") Path(settings.MUSIC_DIR).mkdir(parents=True, exist_ok=True)
query_type = detect_query_type(query) query_type = detect_query_type(query)
if query_type == "video": if query_type == "video":
yt_query = query yt_query = query
playlist_flag = "--no-playlist" playlist_flag = "--no-playlist"
extra_filters = []
elif query_type == "playlist": elif query_type == "playlist":
yt_query = query yt_query = query
playlist_flag = None playlist_flag = None
extra_filters = []
else: else:
yt_query = f"ytsearch20:{query}" yt_query = f"ytsearch1:{query.strip()}" if single else f"ytsearch20:{query.strip()}"
playlist_flag = "--no-playlist" playlist_flag = "--no-playlist"
filter_expr = f"'{query.lower()}' in uploader.lower()" # ✅ valid expression
extra_filters = ["--match-filter", filter_expr] output_template = str(Path(settings.MUSIC_DIR) / "%(artist)s - %(title)s.%(ext)s")
command = [ command = [
"yt-dlp", "yt-dlp",
yt_query, yt_query,
"--extract-audio", "--extract-audio",
"--audio-format", "mp3", "--audio-format", "mp3",
"--add-metadata",
"--output", output_template, "--output", output_template,
"--print", "%(title)s",
"--quiet" "--quiet"
] ]
if playlist_flag: if playlist_flag:
command.append(playlist_flag) command.append(playlist_flag)
if extra_filters:
command.extend(extra_filters) print(f"🎵 Starting download for: {query} (single={single})")
print(f"🛠️ Command: {' '.join(command)}")
result = subprocess.run(command, capture_output=True, text=True) result = subprocess.run(command, capture_output=True, text=True)
if result.returncode != 0: if result.returncode != 0:
print(result.stderr)
raise Exception(f"❌ Download failed:\n{result.stderr}") raise Exception(f"❌ Download failed:\n{result.stderr}")
downloaded_titles = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
print(f"✅ Finished downloading:\n" + "\n".join(downloaded_titles))
for title in downloaded_titles:
filename = f"{title}.mp3"
full_path = os.path.join(settings.MUSIC_DIR, filename)
exists = os.path.exists(full_path)
print(f"📁 Checking file: {filename}{'✅ Exists' if exists else '❌ Not found'}")
if settings.NAVIDROME_SCAN_URL: if settings.NAVIDROME_SCAN_URL:
try: try:
res = requests.get(settings.NAVIDROME_SCAN_URL, timeout=5) res = requests.get(settings.NAVIDROME_SCAN_URL, timeout=5)
res.raise_for_status() res.raise_for_status()
print("🔄 Navidrome rescan triggered.")
except Exception as e: except Exception as e:
print(f"⚠️ Failed to trigger Navidrome rescan: {e}") print(f"⚠️ Failed to trigger Navidrome rescan: {e}")
return { return {
"status": "success",
"query": query, "query": query,
"log": result.stdout + "\n" + result.stderr "downloaded": downloaded_titles
} }

View File

@ -1,22 +1,46 @@
from fastapi import FastAPI, Query, HTTPException from fastapi import FastAPI, Query, HTTPException
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
import uvicorn import uvicorn
from downloader import download_song
from config import settings
import os import os
# Make sure the music directory exists from downloader import download_song
from config import settings
# Ensure the music directory exists
os.makedirs(settings.MUSIC_DIR, exist_ok=True) os.makedirs(settings.MUSIC_DIR, exist_ok=True)
print(f"🎯 Music will be saved to: {os.path.join(settings.MUSIC_DIR, '%(artist)s - %(title)s.%(ext)s')}")
app = FastAPI(title="Tunedrop") # List existing files for debug
existing_files = os.listdir(settings.MUSIC_DIR)
print(f"📂 Existing files: {existing_files}")
app = FastAPI(
title="Tunedrop",
description="🎵 Download music using yt-dlp and stream with Navidrome",
version="1.0.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/download", summary="Download a song or playlist")
def download(
query: str = Query(..., min_length=2, description="Artist name, song title, or YouTube/playlist link"),
single: bool = Query(False, description="Set to true to download only a single matching song (for search queries)")
):
if not query.strip():
raise HTTPException(status_code=400, detail="Query cannot be empty")
@app.get("/download")
def download(query: str = Query(..., description="Song name or YouTube search term")):
try: try:
result = download_song(query) result = download_song(query, single=single)
return JSONResponse(content={"status": "success", **result}) return JSONResponse(content={"status": "success", **result})
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=f"Download failed: {str(e)}")
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)