tecuts commited on
Commit
ca08db2
Β·
verified Β·
1 Parent(s): 79960bc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +162 -38
app.py CHANGED
@@ -114,45 +114,65 @@ def decrypt_file(input_path, output_path, key, nonce):
114
  outfile.write(decrypted_data)
115
 
116
  def parse_dash_manifest(manifest_data):
117
- """Parse DASH manifest XML and extract segment URLs"""
118
  manifest_str = manifest_data.decode('utf-8')
119
-
120
  base_url = ""
121
  segments = []
122
  media_template = ""
123
  start_number = 1
124
-
 
 
125
  try:
126
  root = ET.fromstring(manifest_str)
127
-
128
  for elem in root.iter():
129
  tag = elem.tag
130
  if '}' in tag:
131
  tag = tag.split('}')[1]
132
-
133
  if tag == 'BaseURL' or tag.lower() == 'baseurl':
134
  base_url = elem.text
135
-
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  if tag == 'SegmentTemplate' or tag.lower() == 'segmenttemplate':
137
  media_template = elem.get('media', '')
 
138
  start_number = int(elem.get('startNumber', '1'))
139
 
 
 
 
 
140
  for timeline in elem.iter():
141
  tl_tag = timeline.tag
142
  if '}' in tl_tag:
143
  tl_tag = tl_tag.split('}')[1]
144
-
145
  if tl_tag == 'SegmentTimeline' or tl_tag.lower() == 'segmenttimeline':
146
  total_segments = 0
147
  for s_elem in timeline.iter():
148
  s_tag = s_elem.tag
149
  if '}' in s_tag:
150
  s_tag = s_tag.split('}')[1]
151
-
152
  if s_tag == 'S' or s_tag.lower() == 's':
153
  repeat = int(s_elem.get('r', '0'))
154
  total_segments += repeat + 1
155
-
156
  if media_template and total_segments > 0:
157
  for i in range(start_number, start_number + total_segments):
158
  segment_url = media_template.replace('$Number$', str(i))
@@ -160,34 +180,63 @@ def parse_dash_manifest(manifest_data):
160
  print(f"βœ… Generated {len(segments)} segment URLs from template")
161
  except Exception as e:
162
  print(f"⚠️ Error parsing DASH manifest: {e}")
163
-
164
  if not segments:
165
  print("⚠️ Trying regex parsing...")
166
- import re
167
-
 
 
 
 
 
168
  template_match = re.search(r'media="([^"]+\$Number\$[^"]+)"', manifest_str)
169
  if template_match:
170
  media_template = template_match.group(1)
171
-
172
  start_num_match = re.search(r'startNumber="(\d+)"', manifest_str)
173
  if start_num_match:
174
  start_number = int(start_num_match.group(1))
175
-
176
  timeline_matches = re.findall(r'<S[^>]*d="(\d+)"[^>]*r="(\d+)"', manifest_str)
177
  total_segments = 0
178
  for d, r in timeline_matches:
179
  total_segments += int(r) + 1
180
-
181
  single_segments = re.findall(r'<S[^>]*d="(\d+)"[^>]*[^r][^>]*>', manifest_str)
182
  total_segments += len(single_segments)
183
-
184
  if total_segments > 0:
185
  for i in range(start_number, start_number + total_segments):
186
  segment_url = media_template.replace('$Number$', str(i))
187
  segments.append(segment_url)
188
  print(f"βœ… Generated {len(segments)} segment URLs with regex")
189
-
190
- return base_url, segments
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  def parse_bt_manifest(manifest_data):
193
  """Parse BT manifest (JSON format)"""
@@ -283,6 +332,7 @@ async def download_track(track_id: int, quality: str = "LOSSLESS"):
283
 
284
  if is_lossless:
285
  security_token = data.get('securityToken')
 
286
  key = None
287
  nonce = None
288
 
@@ -292,18 +342,25 @@ async def download_track(track_id: int, quality: str = "LOSSLESS"):
292
 
293
  download_dir = "./downloads"
294
  os.makedirs(download_dir, exist_ok=True)
295
-
296
- tmpdir = tempfile.mkdtemp(prefix="tidal-api-")
297
 
298
  try:
299
  print("πŸ“¦ Parsing DASH manifest...")
300
- base_url, segments = parse_dash_manifest(manifest_data)
301
- print(f"βœ… Found {len(segments)} segments")
302
 
303
  print("⬇️ Downloading segments...")
 
304
  for i, seg in enumerate(segments):
305
- seg_url = seg
306
- seg_path = os.path.join(tmpdir, f"segment_{i:04d}.enc")
 
 
 
 
 
 
307
 
308
  max_retries = 3
309
  success = False
@@ -326,27 +383,94 @@ async def download_track(track_id: int, quality: str = "LOSSLESS"):
326
 
327
  print(f" Segment {i+1}/{len(segments)} downloaded")
328
 
329
- print("πŸ” Decrypting and merging segments...")
330
- output_filename = f"{artist} - {title}.flac"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  output_path = os.path.join(download_dir, output_filename)
 
332
 
333
- with open(output_path, 'wb') as out:
334
- for i in range(len(segments)):
335
- seg_path = os.path.join(tmpdir, f"segment_{i:04d}.enc")
336
- decrypted_path = os.path.join(tmpdir, f"decrypted_{i:04d}.flac")
 
 
 
 
 
 
 
 
 
 
337
 
338
- if key and nonce:
339
- decrypt_file(seg_path, decrypted_path, key, nonce)
 
 
 
 
 
 
340
  else:
341
- with open(seg_path, 'rb') as f:
342
- with open(decrypted_path, 'wb') as df:
343
- df.write(f.read())
344
 
345
- with open(decrypted_path, 'rb') as f:
346
- out.write(f.read())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
348
  print(f"βœ… Download complete: {output_path}")
349
 
 
 
 
 
 
350
  file_size = os.path.getsize(output_path)
351
 
352
  return {
@@ -362,7 +486,7 @@ async def download_track(track_id: int, quality: str = "LOSSLESS"):
362
  "isrc": isrc,
363
  "quality": audio_quality,
364
  "fileSize": file_size,
365
- "format": "flac"
366
  },
367
  "downloadUrl": f"/download_file/{output_filename}",
368
  "message": "File downloaded and ready for download"
 
114
  outfile.write(decrypted_data)
115
 
116
  def parse_dash_manifest(manifest_data):
117
+ """Parse DASH manifest XML and extract segment URLs and audio format"""
118
  manifest_str = manifest_data.decode('utf-8')
119
+
120
  base_url = ""
121
  segments = []
122
  media_template = ""
123
  start_number = 1
124
+ audio_format = "flac"
125
+ codecs_detected = ""
126
+
127
  try:
128
  root = ET.fromstring(manifest_str)
129
+
130
  for elem in root.iter():
131
  tag = elem.tag
132
  if '}' in tag:
133
  tag = tag.split('}')[1]
134
+
135
  if tag == 'BaseURL' or tag.lower() == 'baseurl':
136
  base_url = elem.text
137
+
138
+ if tag == 'Representation' or tag.lower() == 'representation':
139
+ codec = elem.get('codecs', '').upper()
140
+ codecs_detected = codec
141
+ if codec:
142
+ print(f" Found Representation with codecs: '{codec}'")
143
+ if 'MP4A' in codec or 'AAC' in codec:
144
+ audio_format = "m4a"
145
+ elif 'FLAC' in codec:
146
+ audio_format = "flac"
147
+ elif 'EC3' in codec or 'EAC3' in codec:
148
+ audio_format = "eac3"
149
+ print(f" Detected audio format: {audio_format}")
150
+
151
  if tag == 'SegmentTemplate' or tag.lower() == 'segmenttemplate':
152
  media_template = elem.get('media', '')
153
+ initialization = elem.get('initialization', '')
154
  start_number = int(elem.get('startNumber', '1'))
155
 
156
+ if initialization:
157
+ segments.append(initialization)
158
+ print(f"βœ… Added initialization segment: {initialization}")
159
+
160
  for timeline in elem.iter():
161
  tl_tag = timeline.tag
162
  if '}' in tl_tag:
163
  tl_tag = tl_tag.split('}')[1]
164
+
165
  if tl_tag == 'SegmentTimeline' or tl_tag.lower() == 'segmenttimeline':
166
  total_segments = 0
167
  for s_elem in timeline.iter():
168
  s_tag = s_elem.tag
169
  if '}' in s_tag:
170
  s_tag = s_tag.split('}')[1]
171
+
172
  if s_tag == 'S' or s_tag.lower() == 's':
173
  repeat = int(s_elem.get('r', '0'))
174
  total_segments += repeat + 1
175
+
176
  if media_template and total_segments > 0:
177
  for i in range(start_number, start_number + total_segments):
178
  segment_url = media_template.replace('$Number$', str(i))
 
180
  print(f"βœ… Generated {len(segments)} segment URLs from template")
181
  except Exception as e:
182
  print(f"⚠️ Error parsing DASH manifest: {e}")
183
+
184
  if not segments:
185
  print("⚠️ Trying regex parsing...")
186
+
187
+ init_match = re.search(r'initialization="([^"]+)"', manifest_str)
188
+ if init_match:
189
+ initialization = init_match.group(1)
190
+ segments.append(initialization)
191
+ print(f"βœ… Added initialization segment with regex: {initialization}")
192
+
193
  template_match = re.search(r'media="([^"]+\$Number\$[^"]+)"', manifest_str)
194
  if template_match:
195
  media_template = template_match.group(1)
196
+
197
  start_num_match = re.search(r'startNumber="(\d+)"', manifest_str)
198
  if start_num_match:
199
  start_number = int(start_num_match.group(1))
200
+
201
  timeline_matches = re.findall(r'<S[^>]*d="(\d+)"[^>]*r="(\d+)"', manifest_str)
202
  total_segments = 0
203
  for d, r in timeline_matches:
204
  total_segments += int(r) + 1
205
+
206
  single_segments = re.findall(r'<S[^>]*d="(\d+)"[^>]*[^r][^>]*>', manifest_str)
207
  total_segments += len(single_segments)
208
+
209
  if total_segments > 0:
210
  for i in range(start_number, start_number + total_segments):
211
  segment_url = media_template.replace('$Number$', str(i))
212
  segments.append(segment_url)
213
  print(f"βœ… Generated {len(segments)} segment URLs with regex")
214
+
215
+ codecs_match = re.search(r'codecs="([^"]+)"', manifest_str)
216
+ if codecs_match:
217
+ codec = codecs_match.group(1).upper()
218
+ codecs_detected = codec
219
+ if 'MP4A' in codec or 'AAC' in codec:
220
+ audio_format = "m4a"
221
+ elif 'FLAC' in codec:
222
+ audio_format = "flac"
223
+ elif 'EC3' in codec or 'EAC3' in codec:
224
+ audio_format = "eac3"
225
+
226
+ mime_match = re.search(r'mimeType="([^"]+)"', manifest_str)
227
+ if mime_match:
228
+ mime = mime_match.group(1).lower()
229
+ if 'mp4' in mime or 'aac' in mime or 'm4a' in mime:
230
+ audio_format = "m4a"
231
+ elif 'flac' in mime:
232
+ audio_format = "flac"
233
+
234
+ base_match = re.search(r'<[^>]*BaseURL[^>]*>([^<]+)</', manifest_str)
235
+ if base_match:
236
+ base_url = base_match.group(1)
237
+
238
+ print(f" Final detected codec: {codecs_detected}, format: {audio_format}")
239
+ return base_url, segments, audio_format
240
 
241
  def parse_bt_manifest(manifest_data):
242
  """Parse BT manifest (JSON format)"""
 
332
 
333
  if is_lossless:
334
  security_token = data.get('securityToken')
335
+ print(f" securityToken present: {bool(security_token)}, length: {len(security_token) if security_token else 0}")
336
  key = None
337
  nonce = None
338
 
 
342
 
343
  download_dir = "./downloads"
344
  os.makedirs(download_dir, exist_ok=True)
345
+ tmpdir = tempfile.mkdtemp(prefix="tidal-segments-")
346
+ temp_files = []
347
 
348
  try:
349
  print("πŸ“¦ Parsing DASH manifest...")
350
+ base_url, segments, audio_format = parse_dash_manifest(manifest_data)
351
+ print(f"βœ… Found {len(segments)} segments, format: {audio_format}")
352
 
353
  print("⬇️ Downloading segments...")
354
+
355
  for i, seg in enumerate(segments):
356
+ if seg.startswith('http://') or seg.startswith('https://'):
357
+ seg_url = seg
358
+ elif base_url:
359
+ seg_url = base_url + seg
360
+ else:
361
+ seg_url = seg
362
+ seg_path = os.path.join(tmpdir, f"segment_{i:04d}.mp4")
363
+ temp_files.append(seg_path)
364
 
365
  max_retries = 3
366
  success = False
 
383
 
384
  print(f" Segment {i+1}/{len(segments)} downloaded")
385
 
386
+ print(f"πŸ”„ Merging {len(segments)} segments into {audio_format}...")
387
+ print(f" Temp files count: {len(temp_files)}")
388
+ print(f" Temp directory: {tmpdir}")
389
+
390
+ # Check first segment header
391
+ if temp_files:
392
+ with open(temp_files[0], 'rb') as f:
393
+ header = f.read(16).hex()
394
+ print(f" First segment header (hex): {header}")
395
+ if header.startswith('0000001866747970'):
396
+ print(" βœ… First segment is MP4 (ftyp header)")
397
+ elif header.startswith('0000010c6d6f6f66'):
398
+ print(" ⚠️ First segment is fragmented MP4 (moof header)")
399
+ else:
400
+ print(f" ❓ Unknown header: {header[:20]}...")
401
+
402
+ output_filename = f"{artist} - {title}.{audio_format}"
403
  output_path = os.path.join(download_dir, output_filename)
404
+ print(f" Output path: {output_path}")
405
 
406
+ if audio_format == "flac":
407
+ merged_path = os.path.join(tmpdir, "merged.mp4")
408
+ print(f" Merged path: {merged_path}")
409
+
410
+ with open(merged_path, 'wb') as out:
411
+ print(" Starting merge...")
412
+ for idx, temp_file in enumerate(temp_files):
413
+ with open(temp_file, 'rb') as f:
414
+ out.write(f.read())
415
+ print(" Merge complete")
416
+
417
+ print(" Starting FFmpeg conversion...")
418
+ try:
419
+ import subprocess
420
 
421
+ # First check if the merged file is valid with ffprobe
422
+ probe_result = subprocess.run([
423
+ 'ffprobe', '-v', 'error', '-show_streams', '-of', 'json', merged_path
424
+ ], capture_output=True, text=True, timeout=30)
425
+ print(f"ffprobe return code: {probe_result.returncode}")
426
+ if probe_result.returncode == 0:
427
+ print(f"βœ… Merged file is valid MP4")
428
+ print(f"ffprobe output: {probe_result.stdout[:500]}")
429
  else:
430
+ print(f"❌ Merged file analysis failed: {probe_result.stderr}")
 
 
431
 
432
+ # Use stream copy mode which is what OrpheusDL does
433
+ result = subprocess.run([
434
+ 'ffmpeg', '-y', '-i', merged_path,
435
+ '-c', 'copy',
436
+ '-loglevel', 'error',
437
+ output_path
438
+ ], capture_output=True, text=True, timeout=120)
439
+
440
+ print(f"FFmpeg return code: {result.returncode}")
441
+ print(f"FFmpeg stdout: {result.stdout[:2000] if result.stdout else 'None'}")
442
+ print(f"FFmpeg stderr: {result.stderr[:2000] if result.stderr else 'None'}")
443
+
444
+ if result.returncode != 0:
445
+ print(f"⚠️ FFmpeg conversion failed with code {result.returncode}: {result.stderr}")
446
+ raise HTTPException(status_code=500, detail=f"FFmpeg conversion failed: {result.stderr}")
447
+
448
+ print(f"βœ… Converted to FLAC: {output_path}")
449
+ except FileNotFoundError:
450
+ print("⚠️ FFmpeg not found, using raw merge")
451
+ with open(output_path, 'wb') as out:
452
+ for temp_file in temp_files:
453
+ with open(temp_file, 'rb') as f:
454
+ out.write(f.read())
455
+ except subprocess.TimeoutExpired:
456
+ print("⚠️ FFmpeg timeout")
457
+ raise HTTPException(status_code=500, detail="FFmpeg timeout")
458
+ except Exception as e:
459
+ print(f"⚠️ Exception during FFmpeg: {e}")
460
+ raise
461
+ else:
462
+ with open(output_path, 'wb') as out:
463
+ for temp_file in temp_files:
464
+ with open(temp_file, 'rb') as f:
465
+ out.write(f.read())
466
 
467
  print(f"βœ… Download complete: {output_path}")
468
 
469
+ print(" Cleaning up temp files...")
470
+ import shutil
471
+ shutil.rmtree(tmpdir, ignore_errors=True)
472
+ print(" Cleanup complete")
473
+
474
  file_size = os.path.getsize(output_path)
475
 
476
  return {
 
486
  "isrc": isrc,
487
  "quality": audio_quality,
488
  "fileSize": file_size,
489
+ "format": audio_format
490
  },
491
  "downloadUrl": f"/download_file/{output_filename}",
492
  "message": "File downloaded and ready for download"