From 8153b8bce6f6d76b64ad92ee5cd4a739f1dabadb Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Sat, 2 Aug 2025 22:16:24 +0300 Subject: [PATCH] init --- backend/.env | 4 +- backend/__pycache__/config.cpython-313.pyc | Bin 1311 -> 1322 bytes .../__pycache__/downloader.cpython-313.pyc | Bin 2461 -> 3716 bytes backend/__pycache__/main.cpython-313.pyc | Bin 1324 -> 2410 bytes backend/config.py | 7 ++- backend/downloader.py | 36 ++++++++++------ backend/main.py | 40 ++++++++++++++---- 7 files changed, 62 insertions(+), 25 deletions(-) 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 53c3e9f94bf5a94fd89e598f84de13d5df0b7f19..85283a313ae6128ca92e4bd38e1c7f8577291418 100644 GIT binary patch delta 168 zcmbQwwTg@HGcPX}0}#Z%?#Ym2+Q_$&k(VbR$luG=IV3*F-#=vX3C81$ypty|T?djG z%sWG~fr{H1Zb(SYXP?PFpMNI*3a0h!E7{lcujJpsaaqIhvV>EE&jjVmEKWB?rEUla zO=q9TKAmSG&y1KA=9dMGZ-^LtWMvj(Yw+wS{>;G4%Jz|iL0)BYKFbUVE=HA(nlB7M S>Z1e$k4U#wBS(=G&^7=zJu>S6 delta 157 zcmZ3*HJ^*`GcPX}0}yOk-j(r@aU3_bpMI|3!Ij_FLYn-ztDe!$7L1U%OZ9S9v#V-S?q3#N-Z#2Zobfb zxy?eG4IDevFDpA;2*|l8n%m&nQGA&t_okfk+@Lf}FIMBZ)^6?*RC9l(jb>&&im z!BW*!YAaHmF67!%xL(>&wNklXO@36z&;02rmrAW&qhf71Dau8a?pH|@o%->oZ)_I0 zG|H(1JMYb#H*em@%)IBPUauQL`;+|c>@^;t&*{Kwj!oq0IY8b=A`+RCh?F5in9|rr zYyfR1?ISE<1v+Ox=@@YmC*bU)>yS-!G@$b=;Y5y(Mdu`ch!tIwcAg>b879PkLAMzT zF(!LjHf8~GIyQ`iI-{G8iy2Jj;>@NZ6xiUi!}s(x=s+n(Lazg+I1#8(#%182v5V}E z8OM$p=Zq`Fjb&@*w2YCN!d-DK)tyetbIF8mWF53WVscxAz)rTeWg|YDQtX|)PKN={9J z_V1&KEr(BRaYb2+c^md^@!l1{N zO%9Ql?4nz&5Iv$-^hplUFII|y?h4f@v0D}dz+Pw6Z|%7aCDxJzJ_D|D6RRZTLt?eW zKws1Acp=X&u{H{xdA>u!rG*1wo{r$G^j>rINjEbbXzrg z6E2Q6|1w!tycDm;LR;uXRyw$4bYh-t9?_*(-$I@K&?yz&WQ~~-n zOJ2)wGVCF?j5DGDQQdK}M=zHBv_nU0iM0hlizFXIs2NS{v!XMqL<4O_lT{B zZK_Z5MZ33GS(x=|eC5vVwQPilKupB;E?XO%8v#C*n?`%kM3c4Ng=DxNs(*(s+9dzZ zlPsVX$cw7K-#^w-hSSV6I%01@(@f};meSJV%i15!3J4xIIL=6 zD;E*^%x2wlMmgIYjASb;yyrk9+uYtM69blqbRjIXcNz&Ji9>oCO^bvY= z^GCu^LQUwiSlQA5w28R0Y!{f9p)cUZt)Kot7?{QJ3slc(kPs0vG<^$?e*B)0p@k8Z zbDV}m&(JJE_Z-snEdg}ahx_1npz@(jUYIkOcX8fo@t-KhFMAEgpOly4r00?8yP)RSu&3Js3q?0`o`quHp?B;YaRaBbW)x% z{pGU|GjIwf3~aifdZUNNbb_lhNyB852^B-lWR*}#taJ~cmMlS&qj6T{6gK(kOfngx zMi3ela0Av&2J4kD5T0uU@;oxVmeiQ8sWapV&}bglRXCJa&^pV#6I|mP^L%50ZCYbn z^K9$#p_R5A#g07=v@Z1(0--A-4_$$UvjtaEu{t=<6@wjDy+yuyAzR@0Y*L!8djHmY zc;VYij^9@Ns^Zt)#qZ~BJwub8kF%Y_ha+-ut+={$|_F%WEwM^DPGp z4L!L&Dc>L!YR_C5-*6&6P~<9$Tz!!XF1ABg;PSGo&~hNxd@$$k zS!H`4d+V;f^^><2hi?zA72~|ZCp6JIJj_ho?EY}y)k@!_{OR0r+yP$+tZuh(|h||q2}m3zaFT*QFp!WM)URN zCB6{YH}3?kbCqpN=axp6@h3xfPUTL=a=qWp`Q*QHQ%`tQwfC6^ag}SXR`}nrIr!tu zdR4=X{qOCk;fUm0B6s`qRYUUwMUQ{YBji28s;70`6UbE^TJ`j8xKUMIv9{xh!|riz zAiKl)uYc@C^_cnRvnEto{}eG)9&pzjyUVQTxvGIR&tTp&_@50so%y#;D}g_B2S+^Y zClxi&f65;n384D{_eih(exq-s%YMJh0eFZR3RRF+;3o990-@ywO*h7DGBH(pM@(m2 zQ>PO%rT2u;imUvDuv40rES+&eUqV9Pzw$NU-biUmCW()dexT9c0D3QUpc=zGK!FE{ me}Md-q2Onzaf5X-!A0>I0`R2r4aT`xy@3EMDPK}h68c|EuSg95 literal 2461 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 delta 941 zcmZuv%WD%s82@Hxce6>FwdupwRGPI^C9PHqRf$GH>Z=WU$TXlN5Mr`PQ}^*!nn$o+9<67iWVNMSn2g{=O%_Z)U%AJPD}lp-=!g zK3%*q{Z6>8_9Er>&fz+h2@o;qn^dTx0q0WEKN+Bb7RSjT4Q7L>Ua#?Q3_v!Jiu@h( zJR%ccT+kqk27oBUA4gQps;TIIDg*lv6k74Dz>9;VjfCRD7$R!6eM}&mA}}M)pnbnO zX*ksoz{3;u&7%FTA8&g zrop5+N2gZ`=no}6&M`mhSGl zKFIUCH-wBgwgn<){2M$iY|JB4T+k8Wl}CjVD#2kKg%XO%XP9i+dE2p=&vM-RQnx!N z^&XI@H;&5OF#qw(1--Qml{_C?HjQB#^0-WO%c6WL~Rpc{Uw{YSmt&`kbgQh4- ziPdobU1ue{-MuMCcPCcWzDM}k_zU#(L_qy`IYJcv$PI+Y1$7Mu*P!n$_?t2yyck>vE}eSS*57FBf7JC7Mn1}+TayhrdJTU- z;$qMBo~6DO)cqzBeSnuoNh31#6(p3nHU7=}tYZ*1HwDg`Du``M^DB7EYcaT(Tu8cU Ke^?ym0)7I9XwV`6 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)