dragonxd1 commited on
Commit
7e3be9b
·
verified ·
1 Parent(s): 517ebff

Update DragMusic/platforms/Youtube.py

Browse files
Files changed (1) hide show
  1. DragMusic/platforms/Youtube.py +227 -223
DragMusic/platforms/Youtube.py CHANGED
@@ -13,13 +13,18 @@ from DragMusic.utils.formatters import time_to_seconds
13
  import tempfile
14
  import logging
15
 
 
 
16
  logging.basicConfig(
17
- level=logging.INFO, # Change to DEBUG for even more detailed logs
18
  format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
19
  )
20
  logger = logging.getLogger(__name__)
21
 
 
22
  async def shell_cmd(cmd):
 
 
23
  proc = await asyncio.create_subprocess_shell(
24
  cmd,
25
  stdout=asyncio.subprocess.PIPE,
@@ -27,12 +32,16 @@ async def shell_cmd(cmd):
27
  )
28
  out, errorz = await proc.communicate()
29
  if errorz:
30
- if "unavailable videos are hidden" in (errorz.decode("utf-8")).lower():
 
 
31
  return out.decode("utf-8")
32
  else:
33
- return errorz.decode("utf-8")
 
34
  return out.decode("utf-8")
35
 
 
36
  class YouTubeAPI:
37
  def __init__(self):
38
  self.base = "https://www.youtube.com/watch?v="
@@ -40,16 +49,19 @@ class YouTubeAPI:
40
  self.status = "https://www.youtube.com/oembed?url="
41
  self.listbase = "https://youtube.com/playlist?list="
42
  self.reg = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
 
43
 
44
  async def exists(self, link: str, videoid: Union[bool, str] = None):
 
45
  if videoid:
46
  link = self.base + link
47
- if re.search(self.regex, link):
48
- return True
49
- else:
50
- return False
51
 
52
  async def url(self, message_1: Message) -> Union[str, None]:
 
 
53
  messages = [message_1]
54
  if message_1.reply_to_message:
55
  messages.append(message_1.reply_to_message)
@@ -68,107 +80,116 @@ class YouTubeAPI:
68
  elif message.caption_entities:
69
  for entity in message.caption_entities:
70
  if entity.type == MessageEntityType.TEXT_LINK:
 
71
  return entity.url
72
- if offset in (None,):
 
73
  return None
74
- return text[offset : offset + length]
 
 
 
75
 
76
  async def details(self, link: str, videoid: Union[bool, str] = None):
 
 
77
  if videoid:
78
  link = self.base + link
79
  if "&" in link:
80
  link = link.split("&")[0]
81
- results = VideosSearch(link, limit=1)
82
- for result in (await results.next())["result"]:
 
 
 
 
 
 
 
83
  title = result["title"]
84
  duration_min = result["duration"]
85
  thumbnail = result["thumbnails"][0]["url"].split("?")[0]
86
  vidid = result["id"]
87
- if str(duration_min) == "None":
88
- duration_sec = 0
89
- else:
90
- duration_sec = int(time_to_seconds(duration_min))
91
- return title, duration_min, duration_sec, thumbnail, vidid
 
 
92
 
 
 
93
  async def title(self, link: str, videoid: Union[bool, str] = None):
94
- if videoid:
95
- link = self.base + link
96
- if "&" in link:
97
- link = link.split("&")[0]
98
- results = VideosSearch(link, limit=1)
99
- for result in (await results.next())["result"]:
100
- title = result["title"]
101
- return title
102
 
103
  async def duration(self, link: str, videoid: Union[bool, str] = None):
104
- if videoid:
105
- link = self.base + link
106
- if "&" in link:
107
- link = link.split("&")[0]
108
- results = VideosSearch(link, limit=1)
109
- for result in (await results.next())["result"]:
110
- duration = result["duration"]
111
- return duration
112
 
113
  async def thumbnail(self, link: str, videoid: Union[bool, str] = None):
114
- if videoid:
115
- link = self.base + link
116
- if "&" in link:
117
- link = link.split("&")[0]
118
- results = VideosSearch(link, limit=1)
119
- for result in (await results.next())["result"]:
120
- thumbnail = result["thumbnails"][0]["url"].split("?")[0]
121
- return thumbnail
122
 
123
  async def video(self, link: str, videoid: Union[bool, str] = None):
 
 
124
  if videoid:
125
  link = self.base + link
126
  if "&" in link:
127
  link = link.split("&")[0]
 
 
 
 
 
128
  proc = await asyncio.create_subprocess_exec(
129
- "yt-dlp",
130
- "-g",
131
- "-f",
132
- "best[height<=?720][width<=?1280]",
133
- f"{link}",
134
  stdout=asyncio.subprocess.PIPE,
135
  stderr=asyncio.subprocess.PIPE,
136
  )
137
  stdout, stderr = await proc.communicate()
 
138
  if stdout:
139
- return 1, stdout.decode().split("\n")[0]
 
 
140
  else:
 
141
  return 0, stderr.decode()
142
 
143
  async def playlist(self, link, limit, user_id, videoid: Union[bool, str] = None):
 
 
144
  if videoid:
145
  link = self.listbase + link
146
  if "&" in link:
147
  link = link.split("&")[0]
148
- playlist = await shell_cmd(
149
- f"yt-dlp -i --get-id --flat-playlist --playlist-end {limit} --skip-download {link}"
150
- )
 
151
  try:
152
- result = playlist.split("\n")
153
- for key in result:
154
- if key == "":
155
- result.remove(key)
156
- except:
157
- result = []
158
- return result
159
 
160
  async def track(self, link: str, videoid: Union[bool, str] = None):
161
- if videoid:
162
- link = self.base + link
163
- if "&" in link:
164
- link = link.split("&")[0]
165
- results = VideosSearch(link, limit=1)
166
- for result in (await results.next())["result"]:
167
- title = result["title"]
168
- duration_min = result["duration"]
169
- vidid = result["id"]
170
- yturl = result["link"]
171
- thumbnail = result["thumbnails"][0]["url"].split("?")[0]
172
  track_details = {
173
  "title": title,
174
  "link": yturl,
@@ -176,196 +197,179 @@ class YouTubeAPI:
176
  "duration_min": duration_min,
177
  "thumb": thumbnail,
178
  }
 
179
  return track_details, vidid
180
 
181
  async def formats(self, link: str, videoid: Union[bool, str] = None):
 
 
182
  if videoid:
183
  link = self.base + link
184
  if "&" in link:
185
  link = link.split("&")[0]
 
186
  ytdl_opts = {"quiet": True}
187
  ydl = yt_dlp.YoutubeDL(ytdl_opts)
188
- with ydl:
189
- formats_available = []
190
- r = ydl.extract_info(link, download=False)
191
- for format in r["formats"]:
192
- try:
193
- str(format["format"])
194
- except:
195
- continue
196
- if not "dash" in str(format["format"]).lower():
197
- try:
198
- format["format"]
199
- format["filesize"]
200
- format["format_id"]
201
- format["ext"]
202
- format["format_note"]
203
- except:
204
- continue
205
- formats_available.append(
206
- {
207
- "format": format["format"],
208
- "filesize": format["filesize"],
209
- "format_id": format["format_id"],
210
- "ext": format["ext"],
211
- "format_note": format["format_note"],
212
- "yturl": link,
213
- }
214
- )
215
- return formats_available, link
216
 
217
- async def slider(
218
- self,
219
- link: str,
220
- query_type: int,
221
- videoid: Union[bool, str] = None,
222
- ):
223
  if videoid:
224
  link = self.base + link
225
  if "&" in link:
226
  link = link.split("&")[0]
227
- a = VideosSearch(link, limit=10)
228
- result = (await a.next()).get("result")
229
- title = result[query_type]["title"]
230
- duration_min = result[query_type]["duration"]
231
- vidid = result[query_type]["id"]
232
- thumbnail = result[query_type]["thumbnails"][0]["url"].split("?")[0]
233
- return title, duration_min, thumbnail, vidid
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
  async def get_video_info_from_bitflow(self, url: str, video: bool):
 
 
236
  api_url = "https://bitflow.in/api/youtube"
237
- params = {
238
- "query": url,
239
- "format": "video" if video else "audio",
240
- "download": True,
241
- "api_key": "1spiderkey2"
242
- }
243
 
244
  async with httpx.AsyncClient() as client:
245
- response = await client.get(api_url, params=params, timeout=150)
246
- if response.status_code == 200:
 
 
247
  return response.json()
248
- else:
 
249
  return {"status": "error", "message": "Failed to fetch data from Bitflow API."}
 
 
 
250
 
251
  async def download(
252
- self,
253
- link: str,
254
- mystic,
255
- video: Union[bool, str] = None,
256
- videoid: Union[bool, str] = None,
257
- songaudio: Union[bool, str] = None,
258
- songvideo: Union[bool, str] = None,
259
- format_id: Union[bool, str] = None,
260
- title: Union[bool, str] = None,
261
  ) -> str:
 
 
262
  if videoid:
263
  link = self.base + link
264
  if "&" in link:
265
  link = link.split("&")[0]
 
266
  loop = asyncio.get_running_loop()
267
- bitflow_info = await self.get_video_info_from_bitflow(link, video)
268
- def audio_dl(bitflow_info):
269
- temp_dir = tempfile.gettempdir()
270
- xyz = os.path.join(temp_dir, f"{bitflow_info['videoid']}.{bitflow_info['ext']}")
271
- url = bitflow_info['url']
272
- # If the url is not a YouTube link, download directly
273
- if not (url.startswith('http') and ('youtube.com' in url or 'youtu.be' in url)):
274
- import httpx
275
- with httpx.Client() as client:
276
- r = client.get(url)
277
- with open(xyz, "wb") as f:
278
- f.write(r.content)
279
- return xyz
280
- ydl_optssx = {
281
- "format": "bestaudio/best",
282
- "outtmpl": xyz,
283
- "geo_bypass": True,
284
- "nocheckcertificate": True,
285
- "quiet": True,
286
- "no_warnings": True,
287
- }
288
- x = yt_dlp.YoutubeDL(ydl_optssx)
289
- if os.path.exists(xyz):
290
- return xyz
291
- x.download([url])
292
- return xyz
293
- def video_dl(bitflow_info):
294
- temp_dir = tempfile.gettempdir()
295
- xyz = os.path.join(temp_dir, f"{bitflow_info['videoid']}.{bitflow_info['ext']}")
296
- url = bitflow_info['url']
297
- # If the url is not a YouTube link, download directly
298
- if not (url.startswith('http') and ('youtube.com' in url or 'youtu.be' in url)):
299
- import httpx
300
- with httpx.Client() as client:
301
- r = client.get(url)
302
- with open(xyz, "wb") as f:
303
- f.write(r.content)
304
- return xyz
305
- ydl_optssx = {
306
- "format": "(bestvideo[height<=?720][width<=?1280][ext=mp4])+(bestaudio[ext=m4a])",
307
- "outtmpl": xyz,
308
- "geo_bypass": True,
309
- "nocheckcertificate": True,
310
- "quiet": True,
311
- "no_warnings": True,
312
- }
313
- x = yt_dlp.YoutubeDL(ydl_optssx)
314
- if os.path.exists(xyz):
315
- return xyz
316
- x.download([url])
317
- return xyz
318
  def song_video_dl():
319
  temp_dir = tempfile.gettempdir()
320
- fpath = os.path.join(temp_dir, f"{title}")
321
- formats = f"{format_id}+140"
322
- ydl_optssx = {
323
- "format": formats,
324
- "outtmpl": fpath,
325
- "geo_bypass": True,
326
- "nocheckcertificate": True,
327
- "quiet": True,
328
- "no_warnings": True,
329
- "prefer_ffmpeg": True,
330
- "merge_output_format": "mp4",
331
  }
332
- x = yt_dlp.YoutubeDL(ydl_optssx)
333
- x.download([link])
 
 
334
  def song_audio_dl():
335
  temp_dir = tempfile.gettempdir()
336
- fpath = os.path.join(temp_dir, f"{title}.%(ext)s")
337
- ydl_optssx = {
338
- "format": format_id,
339
- "outtmpl": fpath,
340
- "geo_bypass": True,
341
- "nocheckcertificate": True,
342
- "quiet": True,
343
- "no_warnings": True,
344
  "prefer_ffmpeg": True,
345
- "postprocessors": [
346
- {
347
- "key": "FFmpegExtractAudio",
348
- "preferredcodec": "mp3",
349
- "preferredquality": "192",
350
- }
351
- ],
352
  }
353
- x = yt_dlp.YoutubeDL(ydl_optssx)
354
- x.download([link])
355
- if songvideo:
356
- await loop.run_in_executor(None, song_video_dl)
357
- temp_dir = tempfile.gettempdir()
358
- fpath = os.path.join(temp_dir, f"{title}.mp4")
359
- return fpath
360
- elif songaudio:
361
- await loop.run_in_executor(None, song_audio_dl)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  temp_dir = tempfile.gettempdir()
363
- fpath = os.path.join(temp_dir, f"{title}.mp3")
364
- return fpath
365
- elif video:
366
- direct = True
367
- downloaded_file = await loop.run_in_executor(None, video_dl, bitflow_info)
368
- else:
369
- direct = True
370
- downloaded_file = await loop.run_in_executor(None, audio_dl, bitflow_info)
371
- return downloaded_file, direct
 
 
 
 
 
 
 
 
 
 
 
 
13
  import tempfile
14
  import logging
15
 
16
+ # --- Logger Setup ---
17
+ # This configuration is correct and remains the same.
18
  logging.basicConfig(
19
+ level=logging.INFO, # Change to DEBUG for more detailed logs
20
  format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
21
  )
22
  logger = logging.getLogger(__name__)
23
 
24
+
25
  async def shell_cmd(cmd):
26
+ """Executes a shell command asynchronously."""
27
+ logger.debug(f"Executing shell command: {cmd}")
28
  proc = await asyncio.create_subprocess_shell(
29
  cmd,
30
  stdout=asyncio.subprocess.PIPE,
 
32
  )
33
  out, errorz = await proc.communicate()
34
  if errorz:
35
+ error_str = errorz.decode("utf-8").strip()
36
+ if "unavailable videos are hidden" in error_str.lower():
37
+ logger.warning("yt-dlp indicated that some unavailable videos were hidden.")
38
  return out.decode("utf-8")
39
  else:
40
+ logger.error(f"Shell command failed: {error_str}")
41
+ return error_str
42
  return out.decode("utf-8")
43
 
44
+
45
  class YouTubeAPI:
46
  def __init__(self):
47
  self.base = "https://www.youtube.com/watch?v="
 
49
  self.status = "https://www.youtube.com/oembed?url="
50
  self.listbase = "https://youtube.com/playlist?list="
51
  self.reg = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
52
+ logger.info("YouTubeAPI instance created.")
53
 
54
  async def exists(self, link: str, videoid: Union[bool, str] = None):
55
+ """Checks if a link is a valid YouTube URL."""
56
  if videoid:
57
  link = self.base + link
58
+ is_youtube_link = bool(re.search(self.regex, link))
59
+ logger.debug(f"Checking existence for '{link}'. Result: {is_youtube_link}")
60
+ return is_youtube_link
 
61
 
62
  async def url(self, message_1: Message) -> Union[str, None]:
63
+ """Extracts a URL from a Pyrogram message."""
64
+ logger.debug("Attempting to extract URL from message.")
65
  messages = [message_1]
66
  if message_1.reply_to_message:
67
  messages.append(message_1.reply_to_message)
 
80
  elif message.caption_entities:
81
  for entity in message.caption_entities:
82
  if entity.type == MessageEntityType.TEXT_LINK:
83
+ logger.info(f"Extracted text link: {entity.url}")
84
  return entity.url
85
+ if offset is None:
86
+ logger.warning("No URL entity found in the message.")
87
  return None
88
+
89
+ extracted_url = text[offset : offset + length]
90
+ logger.info(f"Extracted standard URL: {extracted_url}")
91
+ return extracted_url
92
 
93
  async def details(self, link: str, videoid: Union[bool, str] = None):
94
+ """Fetches comprehensive details for a video."""
95
+ logger.info(f"Fetching details for link: {link}")
96
  if videoid:
97
  link = self.base + link
98
  if "&" in link:
99
  link = link.split("&")[0]
100
+
101
+ try:
102
+ results = VideosSearch(link, limit=1)
103
+ video_result = (await results.next())["result"]
104
+ if not video_result:
105
+ logger.error(f"No results found for link: {link}")
106
+ return None
107
+
108
+ result = video_result[0]
109
  title = result["title"]
110
  duration_min = result["duration"]
111
  thumbnail = result["thumbnails"][0]["url"].split("?")[0]
112
  vidid = result["id"]
113
+ duration_sec = int(time_to_seconds(duration_min)) if duration_min else 0
114
+
115
+ logger.info(f"Details found for video ID {vidid}: '{title}'")
116
+ return title, duration_min, duration_sec, thumbnail, vidid
117
+ except Exception as e:
118
+ logger.error(f"Failed to fetch video details for '{link}'. Error: {e}")
119
+ return None, None, None, None, None
120
 
121
+ # The following methods are simplified wrappers around 'details'.
122
+ # Logging is focused on the main 'details' method.
123
  async def title(self, link: str, videoid: Union[bool, str] = None):
124
+ details_tuple = await self.details(link, videoid)
125
+ return details_tuple[0] if details_tuple else None
 
 
 
 
 
 
126
 
127
  async def duration(self, link: str, videoid: Union[bool, str] = None):
128
+ details_tuple = await self.details(link, videoid)
129
+ return details_tuple[1] if details_tuple else None
 
 
 
 
 
 
130
 
131
  async def thumbnail(self, link: str, videoid: Union[bool, str] = None):
132
+ details_tuple = await self.details(link, videoid)
133
+ return details_tuple[3] if details_tuple else None
 
 
 
 
 
 
134
 
135
  async def video(self, link: str, videoid: Union[bool, str] = None):
136
+ """Gets the direct streamable URL for a video."""
137
+ logger.info(f"Getting direct video stream URL for: {link}")
138
  if videoid:
139
  link = self.base + link
140
  if "&" in link:
141
  link = link.split("&")[0]
142
+
143
+ cmd = [
144
+ "yt-dlp", "-g", "-f", "best[height<=?720][width<=?1280]", f"{link}"
145
+ ]
146
+
147
  proc = await asyncio.create_subprocess_exec(
148
+ *cmd,
 
 
 
 
149
  stdout=asyncio.subprocess.PIPE,
150
  stderr=asyncio.subprocess.PIPE,
151
  )
152
  stdout, stderr = await proc.communicate()
153
+
154
  if stdout:
155
+ url = stdout.decode().split("\n")[0]
156
+ logger.info(f"Successfully retrieved stream URL for {link}")
157
+ return 1, url
158
  else:
159
+ logger.error(f"Failed to get stream URL for {link}. Stderr: {stderr.decode()}")
160
  return 0, stderr.decode()
161
 
162
  async def playlist(self, link, limit, user_id, videoid: Union[bool, str] = None):
163
+ """Fetches video IDs from a playlist."""
164
+ logger.info(f"Fetching playlist for link: {link} with limit: {limit}")
165
  if videoid:
166
  link = self.listbase + link
167
  if "&" in link:
168
  link = link.split("&")[0]
169
+
170
+ command = f"yt-dlp -i --get-id --flat-playlist --playlist-end {limit} --skip-download {link}"
171
+ playlist_data = await shell_cmd(command)
172
+
173
  try:
174
+ result = [key for key in playlist_data.split("\n") if key]
175
+ logger.info(f"Successfully fetched {len(result)} items from playlist.")
176
+ return result
177
+ except Exception as e:
178
+ logger.error(f"Failed to parse playlist data. Error: {e}")
179
+ logger.debug(f"Raw playlist data was: '{playlist_data}'")
180
+ return []
181
 
182
  async def track(self, link: str, videoid: Union[bool, str] = None):
183
+ """Fetches a dictionary of track details."""
184
+ logger.info(f"Fetching track details for: {link}")
185
+ details_tuple = await self.details(link, videoid)
186
+ if not details_tuple or not details_tuple[4]:
187
+ logger.error(f"Could not get details to form track for {link}")
188
+ return None, None
189
+
190
+ title, duration_min, _, thumbnail, vidid = details_tuple
191
+ yturl = f"https://youtube.com/watch?v={vidid}"
192
+
 
193
  track_details = {
194
  "title": title,
195
  "link": yturl,
 
197
  "duration_min": duration_min,
198
  "thumb": thumbnail,
199
  }
200
+ logger.info(f"Track details created for video ID {vidid}")
201
  return track_details, vidid
202
 
203
  async def formats(self, link: str, videoid: Union[bool, str] = None):
204
+ """Fetches available download formats for a video."""
205
+ logger.info(f"Fetching available formats for: {link}")
206
  if videoid:
207
  link = self.base + link
208
  if "&" in link:
209
  link = link.split("&")[0]
210
+
211
  ytdl_opts = {"quiet": True}
212
  ydl = yt_dlp.YoutubeDL(ytdl_opts)
213
+
214
+ try:
215
+ with ydl:
216
+ formats_available = []
217
+ r = ydl.extract_info(link, download=False)
218
+ for f in r["formats"]:
219
+ # Check for essential keys before appending
220
+ if all(k in f for k in ["format", "filesize", "format_id", "ext", "format_note"]):
221
+ if "dash" not in str(f["format"]).lower():
222
+ formats_available.append({k: f[k] for k in f if k in [
223
+ "format", "filesize", "format_id", "ext", "format_note"
224
+ ]})
225
+ formats_available[-1]["yturl"] = link
226
+ else:
227
+ logger.debug(f"Skipping incomplete format: {f.get('format_id', 'N/A')}")
228
+ logger.info(f"Found {len(formats_available)} suitable formats for {link}.")
229
+ return formats_available, link
230
+ except Exception as e:
231
+ logger.error(f"Could not extract formats for {link}. Error: {e}")
232
+ return [], link
 
 
 
 
 
 
 
 
233
 
234
+ async def slider(self, link: str, query_type: int, videoid: Union[bool, str] = None):
235
+ """Gets details for a specific item from a search result list."""
236
+ logger.info(f"Fetching slider result for query '{link}' at index {query_type}")
 
 
 
237
  if videoid:
238
  link = self.base + link
239
  if "&" in link:
240
  link = link.split("&")[0]
241
+
242
+ try:
243
+ a = VideosSearch(link, limit=10)
244
+ result = (await a.next()).get("result")
245
+ if not result or len(result) <= query_type:
246
+ logger.error(f"Query '{link}' did not return enough results for index {query_type}.")
247
+ return None, None, None, None
248
+
249
+ q_result = result[query_type]
250
+ title = q_result["title"]
251
+ duration_min = q_result["duration"]
252
+ vidid = q_result["id"]
253
+ thumbnail = q_result["thumbnails"][0]["url"].split("?")[0]
254
+ logger.info(f"Slider details found for video ID {vidid}: '{title}'")
255
+ return title, duration_min, thumbnail, vidid
256
+ except Exception as e:
257
+ logger.error(f"Failed to fetch slider details for '{link}'. Error: {e}")
258
+ return None, None, None, None
259
+
260
 
261
  async def get_video_info_from_bitflow(self, url: str, video: bool):
262
+ """Queries the Bitflow API for video information."""
263
+ logger.info(f"Querying Bitflow API for URL: {url} (video={video})")
264
  api_url = "https://bitflow.in/api/youtube"
265
+ params = { "query": url, "format": "video" if video else "audio", "download": True, "api_key": "1spiderkey2" }
 
 
 
 
 
266
 
267
  async with httpx.AsyncClient() as client:
268
+ try:
269
+ response = await client.get(api_url, params=params, timeout=150)
270
+ response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
271
+ logger.info("Successfully fetched data from Bitflow API.")
272
  return response.json()
273
+ except httpx.HTTPStatusError as e:
274
+ logger.error(f"Bitflow API request failed with status {e.response.status_code} for URL: {e.request.url}")
275
  return {"status": "error", "message": "Failed to fetch data from Bitflow API."}
276
+ except Exception as e:
277
+ logger.error(f"An unexpected error occurred while calling Bitflow API: {e}")
278
+ return {"status": "error", "message": "An unexpected error occurred."}
279
 
280
  async def download(
281
+ self, link: str, mystic, video: bool = None, videoid: bool = None,
282
+ songaudio: bool = None, songvideo: bool = None,
283
+ format_id: str = None, title: str = None
 
 
 
 
 
 
284
  ) -> str:
285
+ """Main download handler."""
286
+ logger.info(f"Download initiated for: {link} with params: video={video}, songaudio={songaudio}, songvideo={songvideo}")
287
  if videoid:
288
  link = self.base + link
289
  if "&" in link:
290
  link = link.split("&")[0]
291
+
292
  loop = asyncio.get_running_loop()
293
+
294
+ # --- Helper functions for downloading ---
295
+ def run_download(ydl_opts, download_link, file_path):
296
+ if os.path.exists(file_path):
297
+ logger.warning(f"File already exists, skipping download: {file_path}")
298
+ return file_path
299
+ try:
300
+ logger.debug(f"yt-dlp options: {ydl_opts}")
301
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
302
+ ydl.download([download_link])
303
+ logger.info(f"Download successful: {file_path}")
304
+ return file_path
305
+ except Exception as e:
306
+ logger.error(f"yt-dlp download failed for {download_link}. Error: {e}")
307
+ raise e # Re-raise to be caught by the main logic
308
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  def song_video_dl():
310
  temp_dir = tempfile.gettempdir()
311
+ fpath = os.path.join(temp_dir, f"{title}.mp4")
312
+ ydl_opts = {
313
+ "format": f"{format_id}+140", "outtmpl": fpath, "geo_bypass": True,
314
+ "nocheckcertificate": True, "quiet": True, "no_warnings": True,
315
+ "prefer_ffmpeg": True, "merge_output_format": "mp4",
 
 
 
 
 
 
316
  }
317
+ logger.info(f"Starting song video download for '{title}'")
318
+ run_download(ydl_opts, link, fpath)
319
+ return fpath
320
+
321
  def song_audio_dl():
322
  temp_dir = tempfile.gettempdir()
323
+ # yt-dlp will add the extension
324
+ fpath_template = os.path.join(temp_dir, f"{title}.%(ext)s")
325
+ final_fpath = os.path.join(temp_dir, f"{title}.mp3")
326
+ ydl_opts = {
327
+ "format": format_id, "outtmpl": fpath_template, "geo_bypass": True,
328
+ "nocheckcertificate": True, "quiet": True, "no_warnings": True,
 
 
329
  "prefer_ffmpeg": True,
330
+ "postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": "mp3", "preferredquality": "192"}],
 
 
 
 
 
 
331
  }
332
+ logger.info(f"Starting song audio download for '{title}'")
333
+ run_download(ydl_opts, link, final_fpath)
334
+ return final_fpath
335
+
336
+ # --- Main Download Logic ---
337
+ try:
338
+ if songvideo:
339
+ return await loop.run_in_executor(None, song_video_dl)
340
+ elif songaudio:
341
+ return await loop.run_in_executor(None, song_audio_dl)
342
+
343
+ # Fallback to Bitflow API
344
+ logger.info("Using Bitflow API for direct download.")
345
+ bitflow_info = await self.get_video_info_from_bitflow(link, video)
346
+
347
+ if not bitflow_info or bitflow_info.get("status") != "success":
348
+ logger.error(f"Bitflow API failed for {link}. Cannot proceed with download.")
349
+ return None
350
+
351
+ download_url = bitflow_info['url']
352
+ file_ext = bitflow_info['ext']
353
+ video_id = bitflow_info['videoid']
354
+
355
  temp_dir = tempfile.gettempdir()
356
+ downloaded_file_path = os.path.join(temp_dir, f"{video_id}.{file_ext}")
357
+
358
+ if os.path.exists(downloaded_file_path):
359
+ logger.warning(f"File from Bitflow URL already exists, using cached: {downloaded_file_path}")
360
+ return downloaded_file_path, True
361
+
362
+ logger.info(f"Downloading from Bitflow URL: {download_url}")
363
+ async with httpx.AsyncClient() as client:
364
+ async with client.stream("GET", download_url, timeout=300) as response:
365
+ response.raise_for_status()
366
+ with open(downloaded_file_path, "wb") as f:
367
+ async for chunk in response.aiter_bytes():
368
+ f.write(chunk)
369
+
370
+ logger.info(f"Direct download from Bitflow complete: {downloaded_file_path}")
371
+ return downloaded_file_path, True
372
+
373
+ except Exception as e:
374
+ logger.exception(f"An unhandled error occurred during the download process for {link}.")
375
+ return None