✨ Adapted download code to new tidalapi version.

This commit is contained in:
Robert Honz
2024-04-20 18:11:36 +02:00
parent 94dac6c5b9
commit a205e44c26
+56 -13
View File
@@ -6,11 +6,13 @@ import time
from collections.abc import Callable from collections.abc import Callable
from uuid import uuid4 from uuid import uuid4
import ffmpeg
import m3u8
import requests import requests
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from rich.progress import Progress, TaskID from rich.progress import Progress, TaskID
from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video
from tidalapi.media import StreamManifest from tidalapi.media import AudioExtensions, StreamManifest, VideoExtensions
from tidal_dl_ng.config import Settings from tidal_dl_ng.config import Settings
from tidal_dl_ng.constants import EXTENSION_LYRICS, REQUESTS_TIMEOUT_SEC, MediaType, SkipExisting from tidal_dl_ng.constants import EXTENSION_LYRICS, REQUESTS_TIMEOUT_SEC, MediaType, SkipExisting
@@ -78,10 +80,21 @@ class Download:
def _download( def _download(
self, self,
media: Track | Video, media: Track | Video,
stream_manifest: StreamManifest,
path_file: str, path_file: str,
) -> str: ) -> str:
media_name: str = name_builder_item(media) media_name: str = name_builder_item(media)
urls: [str]
# Get urls for media.
if isinstance(media, Track):
urls = media.get_stream().get_stream_manifest().urls
stream_manifest: StreamManifest = media.get_stream().get_stream_manifest()
elif isinstance(media, Video):
m3u8_variant: m3u8.M3U8 = m3u8.load(media.get_url())
# Find the desired video resolution or the next best one.
m3u8_playlist, codecs = self._extract_video_stream(m3u8_variant, int(self.settings.data.quality_video))
# Populate urls.
urls = m3u8_playlist.files
# Set the correct progress output channel. # Set the correct progress output channel.
if self.progress_gui is None: if self.progress_gui is None:
@@ -93,14 +106,14 @@ class Download:
try: try:
# Compute total iterations for progress # Compute total iterations for progress
urls_count: int = len(stream_manifest.urls) urls_count: int = len(urls)
if urls_count > 1: if urls_count > 1:
progress_total: int = urls_count progress_total: int = urls_count
block_size: int | None = None block_size: int | None = None
else: elif urls_count == 1:
# Compute progress iterations based on the file size. # Compute progress iterations based on the file size.
r = requests.get(stream_manifest.urls[0], stream=True, timeout=REQUESTS_TIMEOUT_SEC) r = requests.get(urls[0], stream=True, timeout=REQUESTS_TIMEOUT_SEC)
r.raise_for_status() r.raise_for_status()
@@ -108,6 +121,8 @@ class Download:
total_size_in_bytes: int = int(r.headers.get("content-length", 0)) total_size_in_bytes: int = int(r.headers.get("content-length", 0))
block_size: int | None = 4096 block_size: int | None = 4096
progress_total: float = total_size_in_bytes / block_size progress_total: float = total_size_in_bytes / block_size
else:
raise ValueError
# Create progress Task # Create progress Task
p_task: TaskID = self.progress.add_task( p_task: TaskID = self.progress.add_task(
@@ -119,7 +134,7 @@ class Download:
# Write content to file until progress is finished. # Write content to file until progress is finished.
while not self.progress.tasks[p_task].finished: while not self.progress.tasks[p_task].finished:
with open(path_file, "wb") as f: with open(path_file, "wb") as f:
for url in stream_manifest.urls: for url in urls:
# Create the request object with stream=True, so the content won't be loaded into memory at once. # Create the request object with stream=True, so the content won't be loaded into memory at once.
r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT_SEC) r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT_SEC)
@@ -138,7 +153,7 @@ class Download:
# TODO: Handle Exception... # TODO: Handle Exception...
self.fn_logger(e) self.fn_logger(e)
if stream_manifest.is_encrypted: if isinstance(media, Track) and stream_manifest.is_encrypted:
key, nonce = decrypt_security_token(stream_manifest.encryption_key) key, nonce = decrypt_security_token(stream_manifest.encryption_key)
tmp_path_file_decrypted = path_file + "_decrypted" tmp_path_file_decrypted = path_file + "_decrypted"
decrypt_file(path_file, tmp_path_file_decrypted, key, nonce) decrypt_file(path_file, tmp_path_file_decrypted, key, nonce)
@@ -187,8 +202,13 @@ class Download:
return False, "" return False, ""
# Populate StreamManifest for further download action. # Get extension.
stream_manifest = media.get_stream().get_stream_manifest() file_extension: str
if isinstance(media, Track):
file_extension = media.get_stream().get_stream_manifest().file_extension
elif isinstance(media, Video):
file_extension = VideoExtensions.TS
# Create file name and path # Create file name and path
file_name_relative = format_path_media(file_template, media) file_name_relative = format_path_media(file_template, media)
@@ -198,7 +218,7 @@ class Download:
# Sanitize final path_file to fit into OS boundaries. # Sanitize final path_file to fit into OS boundaries.
uniquify: bool = self.skip_existing == SkipExisting.Append uniquify: bool = self.skip_existing == SkipExisting.Append
path_file = path_file_sanitize(path_file + stream_manifest.file_extension, adapt=True, uniquify=uniquify) path_file = path_file_sanitize(path_file + file_extension, adapt=True, uniquify=uniquify)
# Compute if and how downloads need to be skipped. # Compute if and how downloads need to be skipped.
if self.skip_existing in (SkipExisting.ExtensionIgnore, SkipExisting.Filename): if self.skip_existing in (SkipExisting.ExtensionIgnore, SkipExisting.Filename):
@@ -210,9 +230,9 @@ class Download:
if not file_exists: if not file_exists:
# Create a temp directory and file. # Create a temp directory and file.
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp_path_dir: with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp_path_dir:
tmp_path_file = os.path.join(tmp_path_dir, str(uuid4()) + stream_manifest.file_extension) tmp_path_file = os.path.join(tmp_path_dir, str(uuid4()) + file_extension)
# Download media. # Download media.
tmp_path_file = self._download(media=media, stream_manifest=stream_manifest, path_file=tmp_path_file) tmp_path_file = self._download(media=media, path_file=tmp_path_file)
if isinstance(media, Video) and self.settings.data.video_convert_mp4: if isinstance(media, Video) and self.settings.data.video_convert_mp4:
# Convert `*.ts` file to `*.mp4` using ffmpeg # Convert `*.ts` file to `*.mp4` using ffmpeg
@@ -303,7 +323,7 @@ class Download:
totaltrack=track.album.num_tracks if track.album and track.album.num_tracks else 1, totaltrack=track.album.num_tracks if track.album and track.album.num_tracks else 1,
totaldisc=track.album.num_volumes if track.album and track.album.num_volumes else 1, totaldisc=track.album.num_volumes if track.album and track.album.num_volumes else 1,
discnumber=track.volume_num if track.volume_num else 1, discnumber=track.volume_num if track.volume_num else 1,
url_cover=track.album.image(self.settings.data.metadata_cover_dimension), url_cover=track.album.image(int(self.settings.data.metadata_cover_dimension)),
) )
m.save() m.save()
@@ -377,3 +397,26 @@ class Download:
self.fn_logger.debug(f"Next download will start in {time_sleep} seconds.") self.fn_logger.debug(f"Next download will start in {time_sleep} seconds.")
time.sleep(time_sleep) time.sleep(time_sleep)
def _video_convert(self, path_file: str) -> str:
path_file_out = os.path.splitext(path_file)[0] + AudioExtensions.MP4
result, _ = ffmpeg.input(path_file).output(path_file_out, map=0, c="copy").run()
return path_file_out
def _extract_video_stream(self, m3u8_variant: m3u8.M3U8, quality: int) -> (m3u8.M3U8 | bool, str):
m3u8_playlist: m3u8.M3U8 | bool = False
resolution_best: int = 0
mime_type: str = ""
if m3u8_variant.is_variant:
for playlist in m3u8_variant.playlists:
if resolution_best < playlist.stream_info.resolution[1]:
resolution_best = playlist.stream_info.resolution[1]
m3u8_playlist = m3u8.load(playlist.uri)
mime_type = playlist.stream_info.codecs
if quality == playlist.stream_info.resolution[1]:
break
return m3u8_playlist, mime_type