✨ Adapted download code to new tidalapi version.
This commit is contained in:
+56
-13
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user