From 3438b1587c50519916ba652675fb68a92bca3ec0 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Thu, 31 Jul 2025 08:01:51 +0300 Subject: [PATCH] init --- backend/.env | 4 ++ backend/__pycache__/config.cpython-313.pyc | Bin 0 -> 1311 bytes .../__pycache__/downloader.cpython-313.pyc | Bin 0 -> 2461 bytes backend/__pycache__/main.cpython-313.pyc | Bin 0 -> 1324 bytes backend/config.py | 4 +- backend/downloader.py | 36 +++++++++++++++--- backend/main.py | 4 ++ 7 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 backend/.env create mode 100644 backend/__pycache__/config.cpython-313.pyc create mode 100644 backend/__pycache__/downloader.cpython-313.pyc create mode 100644 backend/__pycache__/main.cpython-313.pyc diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..35d78bc --- /dev/null +++ b/backend/.env @@ -0,0 +1,4 @@ +# MUSIC_DIR=/music +# NAVIDROME_SCAN_URL=http://navidrome:4533/api/rescan +MUSIC_DIR=./backend/music +NAVIDROME_SCAN_URL= diff --git a/backend/__pycache__/config.cpython-313.pyc b/backend/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53c3e9f94bf5a94fd89e598f84de13d5df0b7f19 GIT binary patch literal 1311 zcmZ`(%}*Og6rcUF*I*zJK*dc{3#bxTDu+raX+%*mq(Erw$Rl#vWVGxKW*d9gd9!vE z)I)k{QG3cEN{`{>_Lf5r{WFMsArs})6Sqc2>NRiHc2!HwNPhF)oA>p5?|HjpV8VnWuyjs_l?l8fbaN>pS`_1B`?>c1$ooAoGNpIYKQ*TQ+*W zU^&)2S?&jdRMjY?&XiQpWVYxEPI(*$VO5}PeI4+cU-J6K$(8C0U4q3uB@O|N640pz z#wWxvD@pPHbs(fNHY_XYJ(d&yV`(pACUpikWSkLalk!8VQ#eBlDk!en)LP-brL((Uym z3OW3ExAILsj`A@C)hdU4$!!HbM0K7Aba6vDRa(2dy-_LOzRBqNT8W|k(8T)PBi&0_ z;PAw#4kk6gq9!haRY=K+0h*=tDBKUKXKI{><`NHEhgLJ*|Lpqjq*O>to3YTXbH?Nd zWA(^y))b#*>|xWb^XDFU1LunK3qZk@X?IT~8tjaMh^5=g)DSU9?1USJk%AY63YDpz%x|M0gsh4>u1J!0 zPH_r!uN1Yzp#E|RQ5zDgzSPd1;GpOl{$+ndLm?ecN?#a3tW%g*%b8 zG4KCl>ek{pLDDNg<;HnWHXiwIsHP+Op9fIi6KN0h2_*OkbxEbt{?~38jI>vJ>#_=0 s(OB)KSU~c(p=sJ1^649rdrdOuW?EZ3`u?2Y^>&J6r+>NegPj!g4!BJ`82|tP literal 0 HcmV?d00001 diff --git a/backend/__pycache__/downloader.cpython-313.pyc b/backend/__pycache__/downloader.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..969dfa5004a67cb7e75bb1f6a43d573ffe08ce63 GIT binary patch literal 2461 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 literal 0 HcmV?d00001 diff --git a/backend/config.py b/backend/config.py index 15700b7..0cf9645 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,8 +1,10 @@ from pydantic_settings import BaseSettings from pydantic import Field +import os class Settings(BaseSettings): - MUSIC_DIR: str = Field(default="/music", description="Path where songs are saved") + 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") 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 fc0c89e..f00e972 100644 --- a/backend/downloader.py +++ b/backend/downloader.py @@ -3,26 +3,52 @@ import os from config import settings import requests -def download_song(query: str): - output_template = os.path.join(settings.MUSIC_DIR, "%(artist)s/%(album)s/%(title)s.%(ext)s") +def detect_query_type(query: str): + if "youtube.com/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): + output_template = os.path.join(settings.MUSIC_DIR, "%(uploader)s/%(title)s.%(ext)s") + + 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}" + playlist_flag = "--no-playlist" + filter_expr = f"'{query.lower()}' in uploader.lower()" # ✅ valid expression + extra_filters = ["--match-filter", filter_expr] command = [ "yt-dlp", - f"ytsearch1:{query}", + yt_query, "--extract-audio", "--audio-format", "mp3", "--output", output_template, - "--no-playlist", "--quiet" ] + if playlist_flag: + command.append(playlist_flag) + if extra_filters: + command.extend(extra_filters) + result = subprocess.run(command, capture_output=True, text=True) if result.returncode != 0: raise Exception(f"❌ Download failed:\n{result.stderr}") - # Optional: trigger Navidrome rescan if settings.NAVIDROME_SCAN_URL: try: res = requests.get(settings.NAVIDROME_SCAN_URL, timeout=5) diff --git a/backend/main.py b/backend/main.py index f23a499..e29107f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,6 +3,10 @@ from fastapi.responses import JSONResponse import uvicorn from downloader import download_song from config import settings +import os + +# Make sure the music directory exists +os.makedirs(settings.MUSIC_DIR, exist_ok=True) app = FastAPI(title="Tunedrop")