diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..349f563 --- /dev/null +++ b/backend/.env @@ -0,0 +1,5 @@ +MUSIC_DIR=./music +NAVIDROME_SCAN_URL=http://navidrome.my-apps.svc.cluster.local:4533/api/rescan +host=0.0.0.0 +port=8000 +reload=true diff --git a/backend/__pycache__/config.cpython-313.pyc b/backend/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..d2aa0d2 Binary files /dev/null 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 new file mode 100644 index 0000000..854caec Binary files /dev/null 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 new file mode 100644 index 0000000..bbb9996 Binary files /dev/null and b/backend/__pycache__/main.cpython-313.pyc differ diff --git a/backend/config.py b/backend/config.py index 15700b7..6f36fd9 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,9 +1,16 @@ from pydantic_settings import BaseSettings from pydantic import Field +from pathlib import Path + +# בסיס הקבצים יהיה תקיית backend +BASE_DIR = Path(__file__).resolve().parent class Settings(BaseSettings): - MUSIC_DIR: str = Field(default="/music", description="Path where songs are saved") + MUSIC_DIR: str = Field(default=str(BASE_DIR / "music"), description="Path where songs are saved") NAVIDROME_SCAN_URL: str = Field(default="", description="URL to trigger Navidrome rescan") + host: str = Field(default="0.0.0.0", description="Host to bind the server") + port: int = Field(default=8000, description="Port to run the server on") + reload: bool = Field(default=True, description="Enable reload for development") class Config: env_file = ".env" diff --git a/backend/downloader.py b/backend/downloader.py index fc0c89e..38d2d2a 100644 --- a/backend/downloader.py +++ b/backend/downloader.py @@ -1,37 +1,49 @@ import subprocess import os -from config import settings +import glob import requests +from config import settings def download_song(query: str): - output_template = os.path.join(settings.MUSIC_DIR, "%(artist)s/%(album)s/%(title)s.%(ext)s") + # ודא שהתיקייה קיימת + os.makedirs(settings.MUSIC_DIR, exist_ok=True) + output_template = os.path.join(settings.MUSIC_DIR, "%(title)s.%(ext)s") + before_files = set(glob.glob(os.path.join(settings.MUSIC_DIR, "**/*.mp3"), recursive=True)) command = [ "yt-dlp", f"ytsearch1:{query}", "--extract-audio", "--audio-format", "mp3", - "--output", output_template, - "--no-playlist", - "--quiet" + "-o", output_template, + "--no-playlist" ] + print("Running command:", " ".join(command)) # Debug + result = subprocess.run(command, capture_output=True, text=True) + after_files = set(glob.glob(os.path.join(settings.MUSIC_DIR, "**/*.mp3"), recursive=True)) + new_files = list(after_files - before_files) + if result.returncode != 0: raise Exception(f"❌ Download failed:\n{result.stderr}") - # Optional: trigger Navidrome rescan + rescan_status = "skipped" if settings.NAVIDROME_SCAN_URL: try: res = requests.get(settings.NAVIDROME_SCAN_URL, timeout=5) res.raise_for_status() + rescan_status = "triggered" except Exception as e: - print(f"⚠️ Failed to trigger Navidrome rescan: {e}") + rescan_status = f"failed: {e}" return { "status": "success", "query": query, - "log": result.stdout + "\n" + result.stderr + "downloaded_files": new_files, + "yt_dlp_stdout": result.stdout, + "yt_dlp_stderr": result.stderr, + "rescan_status": rescan_status } diff --git a/backend/main.py b/backend/main.py index f23a499..3d8541d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,18 +1,45 @@ from fastapi import FastAPI, Query, HTTPException -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, FileResponse +from fastapi.staticfiles import StaticFiles import uvicorn +import os from downloader import download_song from config import settings +from utils.song_list import add_song_to_list, get_song_list -app = FastAPI(title="Tunedrop") +app = FastAPI( + title="Tunedrop", + description="🎵 Search and download songs using yt-dlp. Automatically updates Navidrome library.", + version="1.0.0" +) + +# Serve /songs/filename.mp3 +app.mount("/songs", StaticFiles(directory=settings.MUSIC_DIR), name="songs") @app.get("/download") def download(query: str = Query(..., description="Song name or YouTube search term")): try: result = download_song(query) - return JSONResponse(content={"status": "success", **result}) + if result["downloaded_files"]: + saved_path = os.path.join(settings.MUSIC_DIR, result["downloaded_files"][0]) + add_song_to_list(saved_path, query) + return JSONResponse(content=result) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) +@app.get("/songs") +def list_songs(): + try: + return get_song_list() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/music/{filename}") +def serve_song(filename: str): + file_path = os.path.join(settings.MUSIC_DIR, filename) + if not os.path.isfile(file_path): + raise HTTPException(status_code=404, detail="File not found") + return FileResponse(file_path, media_type="audio/mpeg") + if __name__ == "__main__": - uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) + uvicorn.run("main:app", host=settings.host, port=settings.port, reload=settings.reload) diff --git a/backend/music/Alan Walker - Faded.mp3 b/backend/music/Alan Walker - Faded.mp3 new file mode 100644 index 0000000..b893181 Binary files /dev/null and b/backend/music/Alan Walker - Faded.mp3 differ diff --git a/backend/music/Eminem - Fall (Official Music Video).mp3 b/backend/music/Eminem - Fall (Official Music Video).mp3 new file mode 100644 index 0000000..7d74e6b Binary files /dev/null and b/backend/music/Eminem - Fall (Official Music Video).mp3 differ diff --git a/backend/music/Eminem - Mockingbird [Official Music Video].mp3 b/backend/music/Eminem - Mockingbird [Official Music Video].mp3 new file mode 100644 index 0000000..7e675a8 Binary files /dev/null and b/backend/music/Eminem - Mockingbird [Official Music Video].mp3 differ diff --git a/backend/utils/__pycache__/song_list.cpython-313.pyc b/backend/utils/__pycache__/song_list.cpython-313.pyc new file mode 100644 index 0000000..660d320 Binary files /dev/null and b/backend/utils/__pycache__/song_list.cpython-313.pyc differ diff --git a/backend/utils/song_list.py b/backend/utils/song_list.py new file mode 100644 index 0000000..4cd898b --- /dev/null +++ b/backend/utils/song_list.py @@ -0,0 +1,38 @@ +from pathlib import Path +from datetime import datetime +import json + +SONG_LIST_PATH = Path("/music/songs.json") + +def add_song_to_list(file_path: str, query: str): + file_name = Path(file_path).name + new_song = { + "title": query, + "artist": "Unknown", + "path": file_name, + "cover": "/default-cover.jpg", + "added": datetime.utcnow().isoformat() + } + + songs = [] + if SONG_LIST_PATH.exists(): + with open(SONG_LIST_PATH, "r") as f: + try: + songs = json.load(f) + except json.JSONDecodeError: + songs = [] + + # Avoid duplicates + if not any(song["path"] == file_name for song in songs): + songs.append(new_song) + with open(SONG_LIST_PATH, "w") as f: + json.dump(songs, f, indent=2) + +def get_song_list(): + if SONG_LIST_PATH.exists(): + with open(SONG_LIST_PATH, "r") as f: + try: + return json.load(f) + except json.JSONDecodeError: + return [] + return [] diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..c2058b5 --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:8000 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6f5966b..64e9d01 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,29 +1,8 @@ -import { useState } from "react"; +import Home from "./pages/Home"; import "./App.css"; function App() { - const [query, setQuery] = useState(""); - - const handleSearch = (e) => { - e.preventDefault(); - console.log("Searching for:", query); - // later: call backend here - }; - - return ( -
{song.artist}
-{status}
} + const parsed = data.map((path) => { + const file = path.split("\\").pop().split("/").pop(); + const title = decodeURIComponent(file.replace(".mp3", "")); + return { + title, + artist: "Unknown", + path, + cover: "/default-cover.jpg", + }; + }); + + setSongs(parsed); + } catch (err) { + console.error("❌ Failed to load songs:", err); + } + }; + + + useEffect(() => { + loadSongs(); + }, []); + + return ( +{error}
} + + {result && ( +✅ Success: Downloaded "{result.query}"
+Navidrome rescan: {result.rescan_status}
+