tecuts commited on
Commit
773c8ee
·
verified ·
1 Parent(s): 3aeeb19

Upload 3 files

Browse files
Files changed (1) hide show
  1. app.py +108 -52
app.py CHANGED
@@ -370,14 +370,33 @@ def download_aac_segments(stream_data, track_title, artist, quality):
370
  print(f"Error downloading AAC: {e}")
371
  return None
372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  def download_flac_segments(stream_data, track_title, artist, quality):
374
  """Download DASH segments and convert to FLAC using FFmpeg"""
375
  try:
376
- dash_info = parse_dash_xml(stream_data.get("manifest"))
 
377
  if not dash_info:
378
  return None
379
 
380
- print(f"Downloading {dash_info['segment_count']} segments for {track_title}...")
 
 
 
 
381
 
382
  temp_dir = tempfile.mkdtemp()
383
  segments = []
@@ -421,8 +440,7 @@ def download_flac_segments(stream_data, track_title, artist, quality):
421
 
422
  safe_title = re.sub(r'[^\w\s-]', '', track_title).strip()
423
  safe_artist = re.sub(r'[^\w\s-]', '', artist).strip()
424
- filename = f"{safe_artist} - {safe_title}.flac"
425
- filename = re.sub(r'\s+', '_', filename)
426
  output_path = os.path.join(DOWNLOAD_DIR, filename)
427
  temp_mp4 = os.path.join(temp_dir, "temp_container.mp4")
428
 
@@ -437,27 +455,39 @@ def download_flac_segments(stream_data, track_title, artist, quality):
437
  outfile.write(infile.read())
438
 
439
  try:
440
- result = subprocess.run([
 
 
441
  'ffmpeg', '-y', '-i', temp_mp4,
442
- '-acodec', 'flac',
443
- '-compression_level', '0',
444
  output_path
445
- ], capture_output=True, text=True, timeout=120)
 
 
 
 
 
446
 
447
  if result.returncode != 0:
448
- print(f"FFmpeg error: {result.stderr}")
449
- shutil.copy(temp_mp4, output_path)
 
 
 
 
 
 
450
  except Exception as e:
451
  print(f"FFmpeg conversion error: {e}")
452
  shutil.copy(temp_mp4, output_path)
453
 
454
  shutil.rmtree(temp_dir)
455
 
456
- print(f"✓ FLAC file saved: {output_path}")
457
  return output_path
458
 
459
  except Exception as e:
460
- print(f"Error downloading FLAC: {e}")
461
  return None
462
 
463
  def load_tidal_session(client_id=None, client_secret=None):
@@ -565,39 +595,60 @@ async def get_tidal_track_info(
565
  result["message"] = "Failed to download AAC file"
566
  elif stream_data and stream_data.get("manifestMimeType") == "application/vnd.tidal.bts":
567
  bts_urls = []
568
- if "urls" in stream_data and stream_data["urls"]:
569
- bts_urls = stream_data["urls"]
570
- elif "manifest" in stream_data:
571
  try:
572
  manifest_json = json.loads(base64.b64decode(stream_data["manifest"]))
573
  print(f"DEBUG [H/L]: Decoded bts manifest keys: {list(manifest_json.keys())}")
574
  bts_urls = manifest_json.get("urls", [])
 
575
  except Exception as e:
576
  print(f"DEBUG [H/L]: Error decoding bts manifest: {e}")
577
 
 
 
 
578
  if bts_urls:
579
  direct_url = bts_urls[0]
580
- print(f"DEBUG [H/L]: Found bts URL: {direct_url[:50]}...")
 
581
  try:
582
  safe_title = re.sub(r'[^\w\s-]', '', title).strip()
583
  safe_artist = re.sub(r'[^\w\s-]', '', artist).strip()
584
- ext = ".m4a"
585
- filename = f"{safe_artist} - {safe_title}{ext}"
586
- filename = re.sub(r'\s+', '_', filename)
587
  output_path = os.path.join(DOWNLOAD_DIR, filename)
588
 
589
- response = r_session.get(direct_url, verify=False, stream=True, timeout=60)
590
- if response.status_code == 200:
591
- with open(output_path, 'wb') as f:
592
- for chunk in response.iter_content(chunk_size=8192):
593
- if chunk: f.write(chunk)
594
- result["download_url"] = f"/api/tidal/download/{filename}"
595
- result["file_path"] = output_path
596
- result["file_size"] = os.path.getsize(output_path)
 
 
 
 
 
597
  else:
598
- result["download_url"] = None
599
- result["message"] = f"Failed to download bts stream: {response.status_code}"
 
 
 
 
 
 
 
 
 
 
 
 
 
600
  except Exception as e:
 
601
  result["download_url"] = None
602
  result["message"] = f"Error: {e}"
603
  else:
@@ -642,55 +693,60 @@ async def get_tidal_track_info(
642
  elif stream_data and stream_data.get("manifestMimeType") == "application/vnd.tidal.bts":
643
  # For bts, the URLs are usually inside the base64 encoded manifest JSON
644
  bts_urls = []
645
- if "urls" in stream_data and stream_data["urls"]:
646
- bts_urls = stream_data["urls"]
647
- elif "manifest" in stream_data:
648
  try:
649
  manifest_json = json.loads(base64.b64decode(stream_data["manifest"]))
650
  print(f"DEBUG: Decoded bts manifest keys: {list(manifest_json.keys())}")
651
  bts_urls = manifest_json.get("urls", [])
 
652
  except Exception as e:
653
  print(f"DEBUG: Error decoding bts manifest: {e}")
654
 
 
 
 
655
  if bts_urls:
656
  direct_url = bts_urls[0]
657
- print(f"DEBUG: Found bts URL: {direct_url[:50]}...")
 
658
  try:
659
  safe_title = re.sub(r'[^\w\s-]', '', title).strip()
660
  safe_artist = re.sub(r'[^\w\s-]', '', artist).strip()
661
- filename = f"{safe_artist}_{safe_title}.flac".replace(" ", "_")
662
  output_path = os.path.join(DOWNLOAD_DIR, filename)
663
 
664
- print(f"Processing bts stream via FFmpeg to ensure FLAC: {direct_url[:50]}...")
665
- # Use FFmpeg to stream from the URL and convert/extract to FLAC
666
  ffmpeg_res = subprocess.run([
667
  'ffmpeg', '-y', '-i', direct_url,
668
- '-vn', '-acodec', 'flac',
669
- '-compression_level', '0',
670
  output_path
671
  ], capture_output=True, text=True, timeout=180)
672
 
673
  if ffmpeg_res.returncode == 0 and os.path.exists(output_path):
674
- print(f"✓ FLAC file saved via FFmpeg: {output_path}")
675
  result.update({
676
  "download_url": f"/api/tidal/download/{filename}",
677
  "file_path": output_path,
678
  "file_size": os.path.getsize(output_path)
679
  })
680
  else:
681
- print(f"FAILED to convert bts stream with FFmpeg: {ffmpeg_res.stderr}")
682
- # Fallback: try direct download if FFmpeg fails
683
- response = r_session.get(direct_url, verify=False, stream=True, timeout=60)
684
- if response.status_code == 200:
685
- with open(output_path, 'wb') as f:
686
- for chunk in response.iter_content(chunk_size=8192):
687
- if chunk: f.write(chunk)
688
- result["download_url"] = f"/api/tidal/download/{filename}"
689
- result["file_path"] = output_path
690
- result["file_size"] = os.path.getsize(output_path)
691
- else:
692
- result["download_url"] = None
693
- result["message"] = f"Failed to download bts stream: {response.status_code}"
 
 
694
  except Exception as e:
695
  print(f"Error processing bts stream: {e}")
696
  result["download_url"] = None
 
370
  print(f"Error downloading AAC: {e}")
371
  return None
372
 
373
+ def get_extension(codec):
374
+ codec = codec.lower()
375
+ if 'flac' in codec or 'mqa' in codec:
376
+ return '.flac'
377
+ elif 'mp4a' in codec or 'aac' in codec:
378
+ return '.m4a'
379
+ elif 'ec-3' in codec or 'ac3' in codec or 'eac3' in codec:
380
+ return '.m4a'
381
+ elif 'ac-4' in codec or 'ac4' in codec:
382
+ return '.m4a'
383
+ elif 'mha1' in codec:
384
+ return '.m4a'
385
+ return '.flac' # Default
386
+
387
  def download_flac_segments(stream_data, track_title, artist, quality):
388
  """Download DASH segments and convert to FLAC using FFmpeg"""
389
  try:
390
+ manifest_b64 = stream_data.get("manifest")
391
+ dash_info = parse_dash_xml(manifest_b64)
392
  if not dash_info:
393
  return None
394
 
395
+ # Detect codec from dash_info
396
+ codec = dash_info.get('codecs', 'flac')
397
+ ext = get_extension(codec)
398
+
399
+ print(f"Downloading {dash_info['segment_count']} segments for {track_title} (Codec: {codec})...")
400
 
401
  temp_dir = tempfile.mkdtemp()
402
  segments = []
 
440
 
441
  safe_title = re.sub(r'[^\w\s-]', '', track_title).strip()
442
  safe_artist = re.sub(r'[^\w\s-]', '', artist).strip()
443
+ filename = f"{safe_artist}_{safe_title}{ext}".replace(" ", "_")
 
444
  output_path = os.path.join(DOWNLOAD_DIR, filename)
445
  temp_mp4 = os.path.join(temp_dir, "temp_container.mp4")
446
 
 
455
  outfile.write(infile.read())
456
 
457
  try:
458
+ # Use acodec copy to avoid quality loss and preserve raw stream
459
+ # If output is .flac, FFmpeg will remux to FLAC container
460
+ ffmpeg_cmd = [
461
  'ffmpeg', '-y', '-i', temp_mp4,
462
+ '-vn', '-acodec', 'copy',
 
463
  output_path
464
+ ]
465
+
466
+ # If codec is FLAC but output is .flac, FFmpeg usually handles it well with 'copy'
467
+ # If it fails, we might need to specify the format or re-encode as a fallback
468
+
469
+ result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True, timeout=120)
470
 
471
  if result.returncode != 0:
472
+ print(f"FFmpeg error with acodec copy: {result.stderr}")
473
+ # Fallback to re-encoding only if copy fails
474
+ print("Attempting fallback with explicit flac encoding...")
475
+ subprocess.run([
476
+ 'ffmpeg', '-y', '-i', temp_mp4,
477
+ '-vn', '-acodec', 'flac', '-compression_level', '0',
478
+ output_path
479
+ ], capture_output=True, text=True, timeout=120)
480
  except Exception as e:
481
  print(f"FFmpeg conversion error: {e}")
482
  shutil.copy(temp_mp4, output_path)
483
 
484
  shutil.rmtree(temp_dir)
485
 
486
+ print(f"✓ Audio file saved: {output_path}")
487
  return output_path
488
 
489
  except Exception as e:
490
+ print(f"Error downloading audio segments: {e}")
491
  return None
492
 
493
  def load_tidal_session(client_id=None, client_secret=None):
 
595
  result["message"] = "Failed to download AAC file"
596
  elif stream_data and stream_data.get("manifestMimeType") == "application/vnd.tidal.bts":
597
  bts_urls = []
598
+ codec = 'aac' # Default for H/L
599
+ if "manifest" in stream_data:
 
600
  try:
601
  manifest_json = json.loads(base64.b64decode(stream_data["manifest"]))
602
  print(f"DEBUG [H/L]: Decoded bts manifest keys: {list(manifest_json.keys())}")
603
  bts_urls = manifest_json.get("urls", [])
604
+ codec = manifest_json.get("codecs", "aac")
605
  except Exception as e:
606
  print(f"DEBUG [H/L]: Error decoding bts manifest: {e}")
607
 
608
+ if not bts_urls and "urls" in stream_data:
609
+ bts_urls = stream_data["urls"]
610
+
611
  if bts_urls:
612
  direct_url = bts_urls[0]
613
+ ext = get_extension(codec)
614
+ print(f"DEBUG [H/L]: Found bts URL: {direct_url[:50]}... Codec: {codec}")
615
  try:
616
  safe_title = re.sub(r'[^\w\s-]', '', title).strip()
617
  safe_artist = re.sub(r'[^\w\s-]', '', artist).strip()
618
+ filename = f"{safe_artist}_{safe_title}{ext}".replace(" ", "_")
 
 
619
  output_path = os.path.join(DOWNLOAD_DIR, filename)
620
 
621
+ print(f"Processing H/L bts stream via FFmpeg (acodec copy): {direct_url[:50]}...")
622
+ ffmpeg_res = subprocess.run([
623
+ 'ffmpeg', '-y', '-i', direct_url,
624
+ '-vn', '-acodec', 'copy',
625
+ output_path
626
+ ], capture_output=True, text=True, timeout=180)
627
+
628
+ if ffmpeg_res.returncode == 0 and os.path.exists(output_path):
629
+ result.update({
630
+ "download_url": f"/api/tidal/download/{filename}",
631
+ "file_path": output_path,
632
+ "file_size": os.path.getsize(output_path)
633
+ })
634
  else:
635
+ print(f"FAILED to copy H/L bts stream: {ffmpeg_res.stderr}")
636
+ # Try re-encoding if copy fails
637
+ if ext == '.m4a':
638
+ print("Attempting fallback aac encoding...")
639
+ subprocess.run([
640
+ 'ffmpeg', '-y', '-i', direct_url,
641
+ '-vn', '-acodec', 'aac', '-b:a', '320k',
642
+ output_path
643
+ ], capture_output=True, timeout=180)
644
+ if os.path.exists(output_path):
645
+ result.update({
646
+ "download_url": f"/api/tidal/download/{filename}",
647
+ "file_path": output_path,
648
+ "file_size": os.path.getsize(output_path)
649
+ })
650
  except Exception as e:
651
+ print(f"Error processing H/L bts stream: {e}")
652
  result["download_url"] = None
653
  result["message"] = f"Error: {e}"
654
  else:
 
693
  elif stream_data and stream_data.get("manifestMimeType") == "application/vnd.tidal.bts":
694
  # For bts, the URLs are usually inside the base64 encoded manifest JSON
695
  bts_urls = []
696
+ codec = 'flac' # Default
697
+ if "manifest" in stream_data:
 
698
  try:
699
  manifest_json = json.loads(base64.b64decode(stream_data["manifest"]))
700
  print(f"DEBUG: Decoded bts manifest keys: {list(manifest_json.keys())}")
701
  bts_urls = manifest_json.get("urls", [])
702
+ codec = manifest_json.get("codecs", "flac")
703
  except Exception as e:
704
  print(f"DEBUG: Error decoding bts manifest: {e}")
705
 
706
+ if not bts_urls and "urls" in stream_data:
707
+ bts_urls = stream_data["urls"]
708
+
709
  if bts_urls:
710
  direct_url = bts_urls[0]
711
+ ext = get_extension(codec)
712
+ print(f"DEBUG: Found bts URL: {direct_url[:50]}... Codec: {codec}")
713
  try:
714
  safe_title = re.sub(r'[^\w\s-]', '', title).strip()
715
  safe_artist = re.sub(r'[^\w\s-]', '', artist).strip()
716
+ filename = f"{safe_artist}_{safe_title}{ext}".replace(" ", "_")
717
  output_path = os.path.join(DOWNLOAD_DIR, filename)
718
 
719
+ print(f"Processing bts stream via FFmpeg (acodec copy): {direct_url[:50]}...")
720
+ # Use acodec copy to preserve original quality
721
  ffmpeg_res = subprocess.run([
722
  'ffmpeg', '-y', '-i', direct_url,
723
+ '-vn', '-acodec', 'copy',
 
724
  output_path
725
  ], capture_output=True, text=True, timeout=180)
726
 
727
  if ffmpeg_res.returncode == 0 and os.path.exists(output_path):
728
+ print(f"✓ Audio file saved via FFmpeg: {output_path}")
729
  result.update({
730
  "download_url": f"/api/tidal/download/{filename}",
731
  "file_path": output_path,
732
  "file_size": os.path.getsize(output_path)
733
  })
734
  else:
735
+ print(f"FAILED to copy bts stream with FFmpeg: {ffmpeg_res.stderr}")
736
+ # If it's FLAC and copy failed, try re-encoding as last resort
737
+ if ext == '.flac':
738
+ print("Attempting fallback flac encoding...")
739
+ subprocess.run([
740
+ 'ffmpeg', '-y', '-i', direct_url,
741
+ '-vn', '-acodec', 'flac', '-compression_level', '0',
742
+ output_path
743
+ ], capture_output=True, timeout=180)
744
+ if os.path.exists(output_path):
745
+ result.update({
746
+ "download_url": f"/api/tidal/download/{filename}",
747
+ "file_path": output_path,
748
+ "file_size": os.path.getsize(output_path)
749
+ })
750
  except Exception as e:
751
  print(f"Error processing bts stream: {e}")
752
  result["download_url"] = None