diff --git a/backend/.env b/backend/.env index 35d78bc..77a8ece 100644 --- a/backend/.env +++ b/backend/.env @@ -1,4 +1,2 @@ -# MUSIC_DIR=/music -# NAVIDROME_SCAN_URL=http://navidrome:4533/api/rescan -MUSIC_DIR=./backend/music +MUSIC_DIR=/music NAVIDROME_SCAN_URL= diff --git a/backend/__pycache__/config.cpython-313.pyc b/backend/__pycache__/config.cpython-313.pyc index 53c3e9f..85283a3 100644 Binary files a/backend/__pycache__/config.cpython-313.pyc and b/backend/__pycache__/config.cpython-313.pyc differ diff --git a/backend/__pycache__/downloader.cpython-313.pyc b/backend/__pycache__/downloader.cpython-313.pyc index 969dfa5..4c4a84d 100644 Binary files a/backend/__pycache__/downloader.cpython-313.pyc and b/backend/__pycache__/downloader.cpython-313.pyc differ diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index aa7b6c9..0aff2ff 100644 Binary files a/backend/__pycache__/main.cpython-313.pyc and b/backend/__pycache__/main.cpython-313.pyc differ diff --git a/backend/config.py b/backend/config.py index 0cf9645..78f3299 100644 --- a/backend/config.py +++ b/backend/config.py @@ -3,8 +3,11 @@ from pydantic import Field import os class Settings(BaseSettings): - BASE_DIR: str = os.path.dirname(os.path.abspath(__file__)) - MUSIC_DIR: str = Field(default=os.path.join(BASE_DIR, "music"), description="Path where songs are saved") + PROJECT_ROOT: str = os.path.dirname(os.path.abspath(__file__)) # backend/ + 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") class Config: diff --git a/backend/downloader.py b/backend/downloader.py index f00e972..68bff29 100644 --- a/backend/downloader.py +++ b/backend/downloader.py @@ -1,7 +1,8 @@ import subprocess import os -from config import settings import requests +from pathlib import Path +from config import settings def detect_query_type(query: str): if "youtube.com/playlist" in query or "list=" in query: @@ -11,53 +12,64 @@ def detect_query_type(query: str): else: return "search" -def download_song(query: str): - output_template = os.path.join(settings.MUSIC_DIR, "%(uploader)s/%(title)s.%(ext)s") +def download_song(query: str, single: bool = False): + Path(settings.MUSIC_DIR).mkdir(parents=True, exist_ok=True) query_type = detect_query_type(query) if query_type == "video": yt_query = query playlist_flag = "--no-playlist" - extra_filters = [] elif query_type == "playlist": yt_query = query playlist_flag = None - extra_filters = [] else: - yt_query = f"ytsearch20:{query}" + yt_query = f"ytsearch1:{query.strip()}" if single else f"ytsearch20:{query.strip()}" 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 = [ "yt-dlp", yt_query, "--extract-audio", "--audio-format", "mp3", + "--add-metadata", "--output", output_template, + "--print", "%(title)s", "--quiet" ] if 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) if result.returncode != 0: + print(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: try: res = requests.get(settings.NAVIDROME_SCAN_URL, timeout=5) res.raise_for_status() + print("🔄 Navidrome rescan triggered.") except Exception as e: print(f"⚠️ Failed to trigger Navidrome rescan: {e}") return { - "status": "success", "query": query, - "log": result.stdout + "\n" + result.stderr + "downloaded": downloaded_titles } diff --git a/backend/main.py b/backend/main.py index e29107f..18fe763 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,22 +1,46 @@ from fastapi import FastAPI, Query, HTTPException from fastapi.responses import JSONResponse +from fastapi.middleware.cors import CORSMiddleware import uvicorn -from downloader import download_song -from config import settings 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) +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: - result = download_song(query) + result = download_song(query, single=single) return JSONResponse(content={"status": "success", **result}) 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__": uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)