taslim19 commited on
Commit
eef327e
·
1 Parent(s): 07027a4

feat: Add downloader utility for advanced downloading

Browse files
Files changed (1) hide show
  1. DragMusic/utils/downloader.py +201 -0
DragMusic/utils/downloader.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import aiohttp
3
+ import aiofiles
4
+ import os
5
+ import re
6
+ from typing import Optional, Union, Dict
7
+ from yt_dlp import YoutubeDL
8
+ from config import API_URL, API_KEY
9
+
10
+ USE_API = bool(API_URL and API_KEY)
11
+ _logged_api_skip = False
12
+ CHUNK_SIZE = 8192
13
+ RETRY_DELAY = 2
14
+ cookies_file = "cookies.txt"
15
+ download_folder = "downloads"
16
+ os.makedirs(download_folder, exist_ok=True)
17
+
18
+
19
+ def extract_video_id(link: str) -> str:
20
+ if "v=" in link:
21
+ return link.split("v=")[-1].split("&")[0]
22
+ return link.split("/")[-1].split("?")[0]
23
+
24
+
25
+ def safe_filename(name: str) -> str:
26
+ return re.sub(r"[\\/*?\"<>|]", "_", name).strip()[:100]
27
+
28
+
29
+ def file_exists(video_id: str) -> Optional[str]:
30
+ for ext in ["mp3", "m4a", "webm"]:
31
+ path = f"{download_folder}/{video_id}.{ext}"
32
+ if os.path.exists(path):
33
+ print(f"[CACHED] Using existing file: {path}")
34
+ return path
35
+ return None
36
+
37
+
38
+ async def api_download_song(link: str) -> Optional[str]:
39
+ global _logged_api_skip
40
+
41
+ if not USE_API:
42
+ if not _logged_api_skip:
43
+ print("[SKIPPED] API config missing — using yt-dlp only.")
44
+ _logged_api_skip = True
45
+ return None
46
+
47
+ video_id = extract_video_id(link)
48
+ song_url = f"{API_URL}/song/{video_id}?api={API_KEY}"
49
+
50
+ try:
51
+ async with aiohttp.ClientSession() as session:
52
+ while True:
53
+ async with session.get(song_url) as response:
54
+ if response.status != 200:
55
+ print(f"[API ERROR] Status {response.status}")
56
+ return None
57
+
58
+ data = await response.json()
59
+ status = data.get("status", "").lower()
60
+
61
+ if status == "downloading":
62
+ await asyncio.sleep(RETRY_DELAY)
63
+ continue
64
+ elif status == "error":
65
+ print(f"[API ERROR] Status=error for {video_id}")
66
+ return None
67
+ elif status == "done":
68
+ download_url = data.get("link")
69
+ break
70
+ else:
71
+ print(f"[API ERROR] Unknown status: {status}")
72
+ return None
73
+
74
+ fmt = data.get("format", "mp3").lower()
75
+ path = f"{download_folder}/{video_id}.{fmt}"
76
+
77
+ async with session.get(download_url) as file_response:
78
+ async with aiofiles.open(path, "wb") as f:
79
+ while True:
80
+ chunk = await file_response.content.read(CHUNK_SIZE)
81
+ if not chunk:
82
+ break
83
+ await f.write(chunk)
84
+
85
+ return path
86
+ except Exception as e:
87
+ print(f"[API Download Error] {e}")
88
+ return None
89
+
90
+
91
+ def _download_ytdlp(link: str, opts: Dict) -> Optional[str]:
92
+ try:
93
+ with YoutubeDL(opts) as ydl:
94
+ info = ydl.extract_info(link, download=False)
95
+ ext = info.get("ext", "webm")
96
+ vid = info.get("id")
97
+ path = f"{download_folder}/{vid}.{ext}"
98
+ if os.path.exists(path):
99
+ return path
100
+ ydl.download([link])
101
+ return path
102
+ except Exception as e:
103
+ print(f"[yt-dlp Error] {e}")
104
+ return None
105
+
106
+
107
+ async def yt_dlp_download(link: str, type: str, format_id: str = None, title: str = None) -> Optional[str]:
108
+ loop = asyncio.get_running_loop()
109
+
110
+ if type == "audio":
111
+ opts = {
112
+ "format": "bestaudio/best",
113
+ "outtmpl": f"{download_folder}/%(id)s.%(ext)s",
114
+ "quiet": True,
115
+ "no_warnings": True,
116
+ "cookiefile": cookies_file,
117
+ "noplaylist": True,
118
+ "concurrent_fragment_downloads": 5,
119
+ }
120
+ return await loop.run_in_executor(None, _download_ytdlp, link, opts)
121
+
122
+ elif type == "video":
123
+ opts = {
124
+ "format": "best[height<=?720][width<=?1280]",
125
+ "outtmpl": f"{download_folder}/%(id)s.%(ext)s",
126
+ "quiet": True,
127
+ "no_warnings": True,
128
+ "cookiefile": cookies_file,
129
+ "noplaylist": True,
130
+ "concurrent_fragment_downloads": 5,
131
+ }
132
+ return await loop.run_in_executor(None, _download_ytdlp, link, opts)
133
+
134
+ elif type == "song_video" and format_id and title:
135
+ safe_title = safe_filename(title)
136
+ opts = {
137
+ "format": f"{format_id}+140",
138
+ "outtmpl": f"{download_folder}/{safe_title}.mp4",
139
+ "quiet": True,
140
+ "no_warnings": True,
141
+ "prefer_ffmpeg": True,
142
+ "merge_output_format": "mp4",
143
+ "cookiefile": cookies_file,
144
+ }
145
+ await loop.run_in_executor(None, lambda: YoutubeDL(opts).download([link]))
146
+ return f"{download_folder}/{safe_title}.mp4"
147
+
148
+ elif type == "song_audio" and format_id and title:
149
+ safe_title = safe_filename(title)
150
+ opts = {
151
+ "format": format_id,
152
+ "outtmpl": f"{download_folder}/{safe_title}.%(ext)s",
153
+ "quiet": True,
154
+ "no_warnings": True,
155
+ "prefer_ffmpeg": True,
156
+ "cookiefile": cookies_file,
157
+ "postprocessors": [{
158
+ "key": "FFmpegExtractAudio",
159
+ "preferredcodec": "mp3",
160
+ "preferredquality": "192",
161
+ }],
162
+ }
163
+ await loop.run_in_executor(None, lambda: YoutubeDL(opts).download([link]))
164
+ return f"{download_folder}/{safe_title}.mp3"
165
+
166
+ return None
167
+
168
+
169
+ async def download_audio_concurrent(link: str) -> Optional[str]:
170
+ video_id = extract_video_id(link)
171
+
172
+ existing = file_exists(video_id)
173
+ if existing:
174
+ return existing
175
+
176
+ if not USE_API:
177
+ return await yt_dlp_download(link, type="audio")
178
+
179
+ yt_task = asyncio.create_task(yt_dlp_download(link, type="audio"))
180
+ api_task = asyncio.create_task(api_download_song(link))
181
+
182
+ done, _ = await asyncio.wait([yt_task, api_task], return_when=asyncio.FIRST_COMPLETED)
183
+
184
+ for task in done:
185
+ try:
186
+ result = task.result()
187
+ if result:
188
+ return result
189
+ except Exception as e:
190
+ print(f"[Download Task Error] {e}")
191
+
192
+ for task in [yt_task, api_task]:
193
+ if not task.done():
194
+ try:
195
+ result = await task
196
+ if result:
197
+ return result
198
+ except Exception as e:
199
+ print(f"[Fallback Task Error] {e}")
200
+
201
+ return None