init
This commit is contained in:
parent
3438b1587c
commit
8153b8bce6
@ -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=
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user