Merge pull request 'linux' (#1) from linux into download-playlist

Reviewed-on: #1
This commit is contained in:
dvirlabs 2025-08-03 01:15:23 +00:00
commit 84c738632b
11 changed files with 32 additions and 98 deletions

View File

@ -1,2 +1,2 @@
MUSIC_DIR=/music MUSIC_DIR=/root/tunedrop/backend/music
NAVIDROME_SCAN_URL= NAVIDROME_SCAN_URL=

1
backend/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
music/

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,17 +1,10 @@
from pydantic_settings import BaseSettings
from pydantic import Field
import os import os
from dotenv import load_dotenv
class Settings(BaseSettings): load_dotenv()
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: class Settings:
env_file = ".env" MUSIC_DIR = os.getenv("MUSIC_DIR", "/root/tunedrop/backend/music")
env_file_encoding = "utf-8" NAVIDROME_SCAN_URL = os.getenv("NAVIDROME_SCAN_URL", "")
settings = Settings() settings = Settings()

View File

@ -1,75 +1,30 @@
import subprocess import subprocess
import os
import requests
from pathlib import Path from pathlib import Path
from config import settings from config import settings
def detect_query_type(query: str): def is_playlist(query: str) -> bool:
if "youtube.com/playlist" in query or "list=" in query: return "playlist" in query or "list=" in query
return "playlist"
elif "youtube.com/watch" in query or "youtu.be/" in query:
return "video"
else:
return "search"
def download_song(query: str, single: bool = False): def download_song(query: str):
Path(settings.MUSIC_DIR).mkdir(parents=True, exist_ok=True) Path(settings.MUSIC_DIR).mkdir(parents=True, exist_ok=True)
output_template = f"{settings.MUSIC_DIR}/%(title)s.%(ext)s"
query_type = detect_query_type(query)
if query_type == "video":
yt_query = query
playlist_flag = "--no-playlist"
elif query_type == "playlist":
yt_query = query
playlist_flag = None
else:
yt_query = f"ytsearch1:{query.strip()}" if single else f"ytsearch20:{query.strip()}"
playlist_flag = "--no-playlist"
output_template = str(Path(settings.MUSIC_DIR) / "%(artist)s - %(title)s.%(ext)s")
command = [ command = [
"yt-dlp", "yt-dlp",
yt_query, f"{query if query.startswith('http') else f'ytsearch1:{query}'}",
"--extract-audio", "--extract-audio",
"--audio-format", "mp3", "--audio-format", "mp3",
"--add-metadata", "--add-metadata",
"--output", output_template, "--output", output_template,
"--print", "%(title)s", "--verbose"
"--quiet"
] ]
# Only add --no-playlist for search queries
if not is_playlist(query) and not query.startswith("http"):
command.append("--no-playlist")
if playlist_flag: print("Running command:", " ".join(command))
command.append(playlist_flag)
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)
print("STDOUT:", result.stdout)
print("STDERR:", result.stderr)
if result.returncode != 0: if result.returncode != 0:
print(result.stderr) raise Exception(f"yt-dlp failed: {result.stderr}")
raise Exception(f"❌ Download failed:\n{result.stderr}") return {"downloaded": "Check music folder for downloaded files"}
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 {
"query": query,
"downloaded": downloaded_titles
}

View File

@ -1,25 +1,15 @@
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 from fastapi.middleware.cors import CORSMiddleware
import uvicorn
import os import os
from downloader import download_song from downloader import download_song
from config import settings 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')}") print(f"Using MUSIC_DIR: {settings.MUSIC_DIR}")
# List existing files for debug app = FastAPI(title="Tunedrop", version="1.0.0")
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( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@ -28,19 +18,14 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
@app.get("/download", summary="Download a song or playlist") @app.get("/download")
def download( def download(query: str = Query(..., min_length=2)):
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")
try: try:
result = download_song(query, single=single) result = download_song(query)
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=f"Download failed: {str(e)}") raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, log_level="info", reload=True)