From 78a48348a8986f0044fbce2097ad3e099258d32d Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Tue, 12 Nov 2024 22:15:11 +0100 Subject: [PATCH 01/29] =?UTF-8?q?=E2=9C=A8=20Track=20segments=20are=20down?= =?UTF-8?q?loaded=20simultaneously,=20which=20results=20in=20a=20huge=20sp?= =?UTF-8?q?eed=20increase.=20Some=20refactoring=20work=20has=20been=20done?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/constants.py | 3 + tidal_dl_ng/download.py | 173 ++++++++++++++++++++----------- tidal_dl_ng/helper/decryption.py | 7 +- tidal_dl_ng/helper/path.py | 21 ++++ tidal_dl_ng/model/cfg.py | 2 + tidal_dl_ng/model/downloader.py | 9 ++ 6 files changed, 152 insertions(+), 63 deletions(-) create mode 100644 tidal_dl_ng/model/downloader.py diff --git a/tidal_dl_ng/constants.py b/tidal_dl_ng/constants.py index d3ea2b8..ad8d2ee 100644 --- a/tidal_dl_ng/constants.py +++ b/tidal_dl_ng/constants.py @@ -6,6 +6,9 @@ EXTENSION_LYRICS: str = ".lrc" UNIQUIFY_THRESHOLD: int = 99 FILENAME_SANITIZE_PLACEHOLDER: str = "_" COVER_NAME: str = "cover.jpg" +BLOCK_SIZE: int = 4096 +BLOCKS: int = 1024 +CHUNK_SIZE: int = BLOCK_SIZE * BLOCKS class QualityVideo(StrEnum): diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index b6d5f13..7876368 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -5,12 +5,13 @@ import shutil import tempfile import time from collections.abc import Callable +from concurrent import futures from uuid import uuid4 import ffmpeg import m3u8 import requests -from constants import COVER_NAME +from constants import CHUNK_SIZE, COVER_NAME from requests.exceptions import HTTPError from rich.progress import Progress, TaskID from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video @@ -20,7 +21,7 @@ from tidal_dl_ng.config import Settings from tidal_dl_ng.constants import EXTENSION_LYRICS, REQUESTS_TIMEOUT_SEC, MediaType, QualityVideo, SkipExisting from tidal_dl_ng.helper.decryption import decrypt_file, decrypt_security_token from tidal_dl_ng.helper.exceptions import MediaMissing -from tidal_dl_ng.helper.path import check_file_exists, format_path_media, path_file_sanitize +from tidal_dl_ng.helper.path import check_file_exists, format_path_media, path_file_sanitize, url_to_filename from tidal_dl_ng.helper.tidal import ( instantiate_media, items_results_all, @@ -30,6 +31,7 @@ from tidal_dl_ng.helper.tidal import ( name_builder_title, ) from tidal_dl_ng.metadata import Metadata +from tidal_dl_ng.model.downloader import DownloadSegmentResult from tidal_dl_ng.model.gui_data import ProgressBars @@ -88,10 +90,13 @@ class Download: def _download( self, media: Track | Video, - path_file: str, - ) -> str: + path_file: pathlib.Path, + ) -> pathlib.Path: media_name: str = name_builder_item(media) urls: [str] + path_base: pathlib.Path = path_file.parent + result_segments: bool = True + dl_segment_results: [DownloadSegmentResult] = [] # Get urls for media. if isinstance(media, Track): @@ -106,9 +111,9 @@ class Download: # Set the correct progress output channel. if self.progress_gui is None: - progress_stdout: bool = True + progress_to_stdout: bool = True else: - progress_stdout: bool = False + progress_to_stdout: bool = False # Send signal to GUI with media name self.progress_gui.item_name.emit(media_name[:30]) @@ -119,8 +124,11 @@ class Download: progress_total: int = urls_count block_size: int | None = None elif urls_count == 1: - # Will be computed later. - progress_total: float = None + # Get file size and compute progress steps + r = requests.head(urls[0], timeout=REQUESTS_TIMEOUT_SEC) + total_size_in_bytes: int = int(r.headers.get("content-length", 0)) + block_size: int | None = 1048576 + progress_total: float = total_size_in_bytes / block_size else: raise ValueError @@ -128,58 +136,100 @@ class Download: p_task: TaskID = self.progress.add_task( f"[blue]Item '{media_name[:30]}'", total=progress_total, - visible=progress_stdout, + visible=progress_to_stdout, ) - # Write content to file until progress is finished. + # Download segments until progress is finished. while not self.progress.tasks[p_task].finished: - with open(path_file, "wb") as f: - for url in urls: - try: - # 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) + with futures.ThreadPoolExecutor( + max_workers=self.settings.data.downloads_simultaneous_per_track_max + ) as executor: + # Dispatch all download tasks to worker threads + l_futures: [any] = [ + executor.submit(self._download_segment, url, path_base, block_size, p_task, progress_to_stdout) + for url in urls + ] + # Report results as they become available + for future in futures.as_completed(l_futures): + # Retrieve result + result_dl_segment: DownloadSegmentResult = future.result() - r.raise_for_status() + dl_segment_results.append(result_dl_segment) - # Compute progress iterations based on the file size and update task details. - if not progress_total: - # Get file size and compute progress steps - total_size_in_bytes: int = int(r.headers.get("content-length", 0)) - block_size: int | None = 1048576 - progress_total: float = total_size_in_bytes / block_size + # check for a link that was skipped + if not result_dl_segment.result and (result_dl_segment.url is not urls[-1]): + # Sometimes it happens, if a track is very short (< 8 seconds or so), that the last URL in `urls` is + # invalid (HTTP Error 500) and not necessary. File won't be corrupt. + # If this is NOT the case, but any other URL has resulted in an error, + # mark the whole thing as corrupt. + result_segments = False - self.progress.update(p_task, total=progress_total) + # TODO: Implement error handling on corrupt segments (also in the following method. + reult_merge: bool = self._segments_merge(path_file, dl_segment_results) + tmp_path_file_decrypted: pathlib.Path = pathlib.Path("") - # Write the content to disk. If `chunk_size` is set to `None` the whole file will be written at once. - for data in r.iter_content(chunk_size=block_size): - f.write(data) - # Advance progress bar. - self.progress.advance(p_task) - except HTTPError as e: - if url is urls[-1]: - # It happens, if a track is very short (< 8 seconds or so), that the last URL in `urls` is - # invalid (HTTP Error 500) and not necessary. File won't be corrupt. - # Thus, advance progress bar to avoid infinity loops. - self.progress.advance(p_task) - else: - # Finish downloading early and report error. - # TODO: The track should somehow be marked as corrupt. - self.progress.update(p_task, completed=progress_total) - self.fn_logger.error(e) - finally: - # To send the progress to the GUI, we need to emit the percentage. - if not progress_stdout: - self.progress_gui.item.emit(self.progress.tasks[p_task].percentage) - - if isinstance(media, Track) and stream_manifest.is_encrypted: - key, nonce = decrypt_security_token(stream_manifest.encryption_key) - tmp_path_file_decrypted = path_file + "_decrypted" - decrypt_file(path_file, tmp_path_file_decrypted, key, nonce) - else: - tmp_path_file_decrypted = path_file + if reult_merge: + if isinstance(media, Track) and stream_manifest.is_encrypted: + key, nonce = decrypt_security_token(stream_manifest.encryption_key) + tmp_path_file_decrypted = path_file + "_decrypted" + decrypt_file(path_file, tmp_path_file_decrypted, key, nonce) + else: + tmp_path_file_decrypted = path_file return tmp_path_file_decrypted + def _segments_merge(self, path_file, dl_segment_results) -> bool: + result: bool + + # Copy the content of all segments into one file. + try: + with path_file.open("wb") as f_target: + for dl_segment_result in dl_segment_results: + if dl_segment_result.result: + with dl_segment_result.path_segment.open("rb") as f_segment: + # Read and write junks, which gives better HDD write performance + while segment := f_segment.read(CHUNK_SIZE): + f_target.write(segment) + + # Delete segment from HDD + dl_segment_result.path_segment.unlink() + + result = True + except: + result = False + + return result + + def _download_segment( + self, url: str, path_base: pathlib.Path, block_size: int | None, p_task: TaskID, progress_to_stdout: bool + ) -> DownloadSegmentResult: + result: bool = False + path_segment: pathlib.Path = path_base / url_to_filename(url) + + try: + # 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.raise_for_status() + + # Write the content to disk. If `chunk_size` is set to `None` the whole file will be written at once. + with path_segment.open("wb") as f: + for data in r.iter_content(chunk_size=block_size): + f.write(data) + # Advance progress bar. + self.progress.advance(p_task) + + result = True + except HTTPError: + # TODO: Maybe return e as well? + self.progress.advance(p_task) + finally: + # To send the progress to the GUI, we need to emit the percentage. + if not progress_to_stdout: + self.progress_gui.item.emit(self.progress.tasks[p_task].percentage) + + return DownloadSegmentResult(result, url, path_segment) + def item( self, file_template: str, @@ -221,6 +271,7 @@ class Download: # Get extension. file_extension: str + do_flac_extract = False if isinstance(media, Track): # If a quality is explicitly set, change it. @@ -232,8 +283,6 @@ class Download: file_extension = AudioExtensions.M4A if file_extension == AudioExtensions.MP4 else file_extension if self.settings.data.extract_flac: - do_flac_extract = False - if ( media.get_stream().get_stream_manifest().codecs.upper() == Codec.FLAC and file_extension != AudioExtensions.FLAC @@ -266,7 +315,11 @@ class Download: if not file_exists: # Create a temp directory and file. with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp_path_dir: - tmp_path_file = os.path.join(tmp_path_dir, str(uuid4())) + tmp_path_file: pathlib.Path = pathlib.Path(tmp_path_dir) / str(uuid4()) + + # Create empty file + tmp_path_file.touch() + # Download media. tmp_path_file = self._download(media=media, path_file=tmp_path_file) @@ -403,7 +456,7 @@ class Download: return result def metadata_write( - self, track: Track, path_media: str, is_parent_album: str + self, track: Track, path_media: pathlib.Path, is_parent_album: bool ) -> (bool, pathlib.Path | None, pathlib.Path | None): result: bool = False path_lyrics: pathlib.Path | None = None @@ -433,14 +486,14 @@ class Download: print(f"Could not retrieve lyrics for `{name_builder_item(track)}`.") if lyrics and self.settings.data.lyrics_file: - path_lyrics = self.lyrics_to_file(pathlib.Path(path_media).parent, lyrics) + path_lyrics = self.lyrics_to_file(path_media.parent, lyrics) if self.settings.data.metadata_cover_embed or (self.settings.data.cover_album_file and is_parent_album): url_cover = track.album.image(int(self.settings.data.metadata_cover_dimension)) cover_data = self.cover_data(url=url_cover) if cover_data and self.settings.data.cover_album_file and is_parent_album: - path_cover = self.cover_to_file(pathlib.Path(path_media).parent, cover_data) + path_cover = self.cover_to_file(path_media.parent, cover_data) # `None` values are not allowed. m: Metadata = Metadata( @@ -533,8 +586,8 @@ class Download: if not progress_stdout: self.progress_gui.list_item.emit(self.progress.tasks[p_task1].percentage) - def _video_convert(self, path_file: str) -> str: - path_file_out = path_file + AudioExtensions.MP4 + def _video_convert(self, path_file: pathlib.Path) -> pathlib.Path: + path_file_out: pathlib.Path = path_file.with_suffix(AudioExtensions.MP4) result, _ = ( ffmpeg.input(path_file) .output(path_file_out, map=0, c="copy", loglevel="quiet") @@ -543,8 +596,8 @@ class Download: return path_file_out - def _extract_flac(self, path_media_src: str) -> str: - path_media_out = path_media_src + AudioExtensions.FLAC + def _extract_flac(self, path_media_src: pathlib.Path) -> pathlib.Path: + path_media_out = path_media_src.with_suffix(AudioExtensions.FLAC) result, _ = ( ffmpeg.input(path_media_src) .output( diff --git a/tidal_dl_ng/helper/decryption.py b/tidal_dl_ng/helper/decryption.py index ebb946a..17ea19b 100644 --- a/tidal_dl_ng/helper/decryption.py +++ b/tidal_dl_ng/helper/decryption.py @@ -1,4 +1,5 @@ import base64 +import pathlib from Crypto.Cipher import AES from Crypto.Util import Counter @@ -35,7 +36,7 @@ def decrypt_security_token(security_token: str) -> (str, str): return key, nonce -def decrypt_file(path_file_encrypted: str, path_file_destination: str, key: str, nonce: str) -> None: +def decrypt_file(path_file_encrypted: pathlib.Path, path_file_destination: pathlib.Path, key: str, nonce: str) -> None: """ Decrypts an encrypted MQA file given the file, key and nonce. TODO: Is it really only necessary for MQA of for all other formats, too? @@ -46,9 +47,9 @@ def decrypt_file(path_file_encrypted: str, path_file_destination: str, key: str, decryptor = AES.new(key, AES.MODE_CTR, counter=counter) # Open and decrypt - with open(path_file_encrypted, "rb") as f_src: + with path_file_encrypted.open("rb") as f_src: audio_decrypted = decryptor.decrypt(f_src.read()) # Replace with decrypted file - with open(path_file_destination, "wb") as f_dst: + with path_file_destination.open("wb") as f_dst: f_dst.write(audio_decrypted) diff --git a/tidal_dl_ng/helper/path.py b/tidal_dl_ng/helper/path.py index cc8d5b6..e6f5b14 100644 --- a/tidal_dl_ng/helper/path.py +++ b/tidal_dl_ng/helper/path.py @@ -1,8 +1,10 @@ import math import os +import posixpath import re import sys from pathlib import Path, PosixPath +from urllib.parse import unquote, urlsplit from pathvalidate import sanitize_filename, sanitize_filepath from pathvalidate.error import ValidationError @@ -295,3 +297,22 @@ def resource_path(relative_path): base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) + + +def url_to_filename(url: str) -> str: + """Return basename corresponding to url. + >>> print(url_to_filename('http://example.com/path/to/file%C3%80?opt=1')) + fileƀ + >>> print(url_to_filename('http://example.com/slash%2fname')) # '/' in name + Taken from https://gist.github.com/zed/c2168b9c52b032b5fb7d + Traceback (most recent call last): + ... + ValueError + """ + urlpath: str = urlsplit(url).path + basename: str = posixpath.basename(unquote(urlpath)) + + if os.path.basename(basename) != basename or unquote(posixpath.basename(urlpath)) != basename: + raise ValueError # reject '%2f' or 'dir%5Cbasename.ext' on Windows + + return basename diff --git a/tidal_dl_ng/model/cfg.py b/tidal_dl_ng/model/cfg.py index fae3590..6e9b991 100644 --- a/tidal_dl_ng/model/cfg.py +++ b/tidal_dl_ng/model/cfg.py @@ -37,6 +37,7 @@ class Settings: metadata_cover_embed: bool = True cover_album_file: bool = True extract_flac: bool = True + downloads_simultaneous_per_track_max: int = 20 @dataclass_json @@ -82,6 +83,7 @@ class HelpSettings: metadata_cover_embed: str = "Embed album cover into file." cover_album_file: str = "Save cover to 'cover.jpg', if an album is downloaded." extract_flac: str = "Extract FLAC audio tracks from MP4 containers and save them as `*.flac` (uses FFmpeg)." + downloads_simultaneous_per_track_max: str = "Maximum number of simultaneous chunk downloads per track." @dataclass_json diff --git a/tidal_dl_ng/model/downloader.py b/tidal_dl_ng/model/downloader.py new file mode 100644 index 0000000..b8c5348 --- /dev/null +++ b/tidal_dl_ng/model/downloader.py @@ -0,0 +1,9 @@ +import pathlib +from dataclasses import dataclass + + +@dataclass +class DownloadSegmentResult: + result: bool + url: str + path_segment: pathlib.Path From beea8f1fe75b56ec743b17076d65756bc4e9ec30 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Tue, 12 Nov 2024 22:36:16 +0100 Subject: [PATCH 02/29] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Fixed=20segment?= =?UTF-8?q?=20order=20after=20download.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/download.py | 6 +++++- tidal_dl_ng/model/downloader.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index 7876368..b1ff2c6 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -164,6 +164,8 @@ class Download: # mark the whole thing as corrupt. result_segments = False + # Bring list into right order, so segments can be easily merged. + dl_segment_results.sort(key=lambda x: x.id_segment) # TODO: Implement error handling on corrupt segments (also in the following method. reult_merge: bool = self._segments_merge(path_file, dl_segment_results) tmp_path_file_decrypted: pathlib.Path = pathlib.Path("") @@ -205,6 +207,8 @@ class Download: ) -> DownloadSegmentResult: result: bool = False path_segment: pathlib.Path = path_base / url_to_filename(url) + # Calculate the segment ID based on the file name within the URL. + id_segment: int = int(path_segment.stem) try: # Create the request object with stream=True, so the content won't be loaded into memory at once. @@ -228,7 +232,7 @@ class Download: if not progress_to_stdout: self.progress_gui.item.emit(self.progress.tasks[p_task].percentage) - return DownloadSegmentResult(result, url, path_segment) + return DownloadSegmentResult(result=result, url=url, path_segment=path_segment, id_segment=id_segment) def item( self, diff --git a/tidal_dl_ng/model/downloader.py b/tidal_dl_ng/model/downloader.py index b8c5348..52c8e30 100644 --- a/tidal_dl_ng/model/downloader.py +++ b/tidal_dl_ng/model/downloader.py @@ -7,3 +7,4 @@ class DownloadSegmentResult: result: bool url: str path_segment: pathlib.Path + id_segment: int From c1a102f646665d3570fe2f40e6d43f845c9ec55a Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 13 Nov 2024 09:09:00 +0100 Subject: [PATCH 03/29] =?UTF-8?q?=E2=9C=A8=20Removed=20complex=20skipping?= =?UTF-8?q?=20options=20in=20favor=20of=20a=20faster=20duplicates=20check?= =?UTF-8?q?=20/=20skipping=20of=20already=20present=20items=20without=20do?= =?UTF-8?q?wnload=20delay.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/constants.py | 7 -- tidal_dl_ng/dialog.py | 4 +- tidal_dl_ng/download.py | 119 ++++++++++++++++-------------- tidal_dl_ng/helper/wrapper.py | 3 + tidal_dl_ng/model/cfg.py | 10 +-- tidal_dl_ng/model/downloader.py | 3 + tidal_dl_ng/ui/dialog_settings.py | 52 ++++++------- tidal_dl_ng/ui/dialog_settings.ui | 51 +++++-------- 8 files changed, 116 insertions(+), 133 deletions(-) diff --git a/tidal_dl_ng/constants.py b/tidal_dl_ng/constants.py index ad8d2ee..10fb93d 100644 --- a/tidal_dl_ng/constants.py +++ b/tidal_dl_ng/constants.py @@ -27,13 +27,6 @@ class MediaType(StrEnum): ARTIST: str = "artist" -class SkipExisting(StrEnum): - Disabled: str = "False" - Filename: str = "exact" - ExtensionIgnore: str = "extension_ignore" - Append: str = "append" - - class CoverDimensions(StrEnum): Px80: str = "80" Px160: str = "160" diff --git a/tidal_dl_ng/dialog.py b/tidal_dl_ng/dialog.py index c035867..92656dc 100644 --- a/tidal_dl_ng/dialog.py +++ b/tidal_dl_ng/dialog.py @@ -9,7 +9,7 @@ from tidalapi import Quality as QualityAudio from tidal_dl_ng import __version__ from tidal_dl_ng.config import Settings -from tidal_dl_ng.constants import CoverDimensions, QualityVideo, SkipExisting +from tidal_dl_ng.constants import CoverDimensions, QualityVideo from tidal_dl_ng.model.cfg import HelpSettings from tidal_dl_ng.model.cfg import Settings as ModelSettings from tidal_dl_ng.model.meta import ReleaseLatest @@ -157,7 +157,6 @@ class DialogPreferences(QtWidgets.QDialog): def _init_comboboxes(self): self.parameters_combo = [ - ("skip_existing", SkipExisting), ("quality_audio", QualityAudio), ("quality_video", QualityVideo), ("metadata_cover_dimension", CoverDimensions), @@ -173,6 +172,7 @@ class DialogPreferences(QtWidgets.QDialog): "extract_flac", "metadata_cover_embed", "cover_album_file", + "skip_existing", ] def gui_populate(self): diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index b1ff2c6..a9348f3 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -15,10 +15,11 @@ from constants import CHUNK_SIZE, COVER_NAME from requests.exceptions import HTTPError from rich.progress import Progress, TaskID from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video +from tidalapi.exceptions import TooManyRequests from tidalapi.media import AudioExtensions, Codec, Quality, StreamManifest, VideoExtensions from tidal_dl_ng.config import Settings -from tidal_dl_ng.constants import EXTENSION_LYRICS, REQUESTS_TIMEOUT_SEC, MediaType, QualityVideo, SkipExisting +from tidal_dl_ng.constants import EXTENSION_LYRICS, REQUESTS_TIMEOUT_SEC, MediaType, QualityVideo from tidal_dl_ng.helper.decryption import decrypt_file, decrypt_security_token from tidal_dl_ng.helper.exceptions import MediaMissing from tidal_dl_ng.helper.path import check_file_exists, format_path_media, path_file_sanitize, url_to_filename @@ -53,7 +54,7 @@ class RequestsClient: class Download: settings: Settings session: Session - skip_existing: SkipExisting = SkipExisting.Disabled + skip_existing: bool = False fn_logger: Callable progress_gui: ProgressBars progress: Progress @@ -63,7 +64,7 @@ class Download: session: Session, path_base: str, fn_logger: Callable, - skip_existing: SkipExisting = SkipExisting.Disabled, + skip_existing: bool = False, progress_gui: ProgressBars = None, progress: Progress = None, ): @@ -209,6 +210,7 @@ class Download: path_segment: pathlib.Path = path_base / url_to_filename(url) # Calculate the segment ID based on the file name within the URL. id_segment: int = int(path_segment.stem) + error: HTTPError | None = None try: # Create the request object with stream=True, so the content won't be loaded into memory at once. @@ -224,15 +226,18 @@ class Download: self.progress.advance(p_task) result = True - except HTTPError: - # TODO: Maybe return e as well? + except HTTPError as e: + error = e self.progress.advance(p_task) - finally: - # To send the progress to the GUI, we need to emit the percentage. - if not progress_to_stdout: - self.progress_gui.item.emit(self.progress.tasks[p_task].percentage) + self.fn_logger.exception(e.charachter_written) - return DownloadSegmentResult(result=result, url=url, path_segment=path_segment, id_segment=id_segment) + # To send the progress to the GUI, we need to emit the percentage. + if not progress_to_stdout: + self.progress_gui.item.emit(self.progress.tasks[p_task].percentage) + + return DownloadSegmentResult( + result=result, url=url, path_segment=path_segment, id_segment=id_segment, error=error + ) def item( self, @@ -273,50 +278,56 @@ class Download: return False, "" - # Get extension. - file_extension: str - do_flac_extract = False - - if isinstance(media, Track): - # If a quality is explicitly set, change it. - if quality_audio: - quality_audio_old: Quality = self.adjust_quality_audio(quality_audio) - - file_extension = media.get_stream().get_stream_manifest().file_extension - # Use M4A extension for MP4 audio tracks, because it looks better and is completely interchangeable. - file_extension = AudioExtensions.M4A if file_extension == AudioExtensions.MP4 else file_extension - - if self.settings.data.extract_flac: - if ( - media.get_stream().get_stream_manifest().codecs.upper() == Codec.FLAC - and file_extension != AudioExtensions.FLAC - ): - file_extension = AudioExtensions.FLAC - do_flac_extract = True - elif isinstance(media, Video): - if quality_video: - quality_video_old: QualityVideo = self.adjust_quality_video(quality_video) - - file_extension = AudioExtensions.MP4 if self.settings.data.video_convert_mp4 else VideoExtensions.TS - # Create file name and path + file_extension_dummy: str = AudioExtensions.FLAC file_name_relative = format_path_media(file_template, media) path_media_dst = os.path.abspath( os.path.normpath(os.path.join(os.path.expanduser(self.path_base), file_name_relative)) ) # Sanitize final path_file to fit into OS boundaries. - uniquify: bool = self.skip_existing == SkipExisting.Append - path_media_dst = path_file_sanitize(path_media_dst + file_extension, adapt=True, uniquify=uniquify) + path_media_dst = path_file_sanitize(path_media_dst + file_extension_dummy, adapt=True) # Compute if and how downloads need to be skipped. - if self.skip_existing in (SkipExisting.ExtensionIgnore, SkipExisting.Filename): - extension_ignore: bool = self.skip_existing == SkipExisting.ExtensionIgnore - file_exists: bool = check_file_exists(path_media_dst, extension_ignore=extension_ignore) + if self.skip_existing: + skip_file: bool = check_file_exists(path_media_dst, extension_ignore=True) else: - file_exists: bool = False + skip_file: bool = False + + if not skip_file: + # Get extension. + file_extension: str + do_flac_extract = False + # If a quality is explicitly set, change it and remember the previously set quality. + quality_audio_old: Quality = self.adjust_quality_audio(quality_audio) + quality_video_old: QualityVideo = self.adjust_quality_video(quality_video) + + if isinstance(media, Track): + try: + media_stream = media.get_stream() + except TooManyRequests: + self.fn_logger.exception( + f"Too many requests against TIDAL backend. Skipping '{name_builder_item(media)}'. " + f"Consider to activate delay between downloads." + ) + + return False, "" + + file_extension = media_stream.get_stream_manifest().file_extension + # Use M4A extension for MP4 audio tracks, because it looks better and is completely interchangeable. + file_extension = AudioExtensions.M4A if file_extension == AudioExtensions.MP4 else file_extension + + if self.settings.data.extract_flac and ( + media_stream.get_stream_manifest().codecs.upper() == Codec.FLAC + and file_extension != AudioExtensions.FLAC + ): + file_extension = AudioExtensions.FLAC + do_flac_extract = True + elif isinstance(media, Video): + file_extension = AudioExtensions.MP4 if self.settings.data.video_convert_mp4 else VideoExtensions.TS + + # TODO: correct extension - if not file_exists: # Create a temp directory and file. with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp_path_dir: tmp_path_file: pathlib.Path = pathlib.Path(tmp_path_dir) / str(uuid4()) @@ -356,22 +367,22 @@ class Download: # Move cover file if self.settings.data.cover_album_file: self._move_cover(tmp_path_cover, path_media_dst) + + if quality_audio: + # Set quality back to the global user value + self.adjust_quality_audio(quality_audio_old) + + if quality_video: + # Set quality back to the global user value + self.adjust_quality_video(quality_video_old) else: self.fn_logger.debug(f"Download skipped, since file exists: '{path_media_dst}'") - status_download: bool = not file_exists - - if quality_audio: - # Set quality back to the global user value - self.adjust_quality_audio(quality_audio_old) - - if quality_video: - # Set quality back to the global user value - self.adjust_quality_video(quality_video_old) + status_download: bool = not skip_file # Whether a file was downloaded or skipped and the download delay is enabled, wait until the next download. # Only use this, if you have a list of several Track items. - if download_delay: + if download_delay and not skip_file: time_sleep: float = round(random.SystemRandom().uniform(2, 5), 1) self.fn_logger.debug(f"Next download will start in {time_sleep} seconds.") @@ -575,7 +586,7 @@ class Download: while not self.progress.finished: for item_media in items: # Download the item. - status_download, result_path_file = self.item( + status, result_path_file = self.item( media=item_media, file_template=file_name_relative, quality_audio=quality_audio, diff --git a/tidal_dl_ng/helper/wrapper.py b/tidal_dl_ng/helper/wrapper.py index d8ffb09..cd8e6ba 100644 --- a/tidal_dl_ng/helper/wrapper.py +++ b/tidal_dl_ng/helper/wrapper.py @@ -21,3 +21,6 @@ class LoggerWrapped: def critical(self, value): self.fn_print(value) + + def exception(self, value): + self.fn_print(value) diff --git a/tidal_dl_ng/model/cfg.py b/tidal_dl_ng/model/cfg.py index 6e9b991..631a2f2 100644 --- a/tidal_dl_ng/model/cfg.py +++ b/tidal_dl_ng/model/cfg.py @@ -3,13 +3,13 @@ from dataclasses import dataclass from dataclasses_json import dataclass_json from tidalapi import Quality -from tidal_dl_ng.constants import CoverDimensions, QualityVideo, SkipExisting +from tidal_dl_ng.constants import CoverDimensions, QualityVideo @dataclass_json @dataclass class Settings: - skip_existing: SkipExisting = SkipExisting.Disabled + skip_existing: bool = True lyrics_embed: bool = False lyrics_file: bool = False # TODO: Implement API KEY selection. @@ -43,11 +43,7 @@ class Settings: @dataclass_json @dataclass class HelpSettings: - skip_existing: str = ( - "Do not download, if file already exists. Possible option false = do not skip, " - "'exact' = if filename already exists, 'extension_ignore' = skip even if a file with a " - "different file extension exists." - ) + skip_existing: str = "Skip download if file already exists." album_cover_save: str = "Safe cover to album folder." lyrics_embed: str = "Embed lyrics in audio file, if lyrics are available." lyrics_file: str = "Save lyrics to separate *.lrc file, if lyrics are available." diff --git a/tidal_dl_ng/model/downloader.py b/tidal_dl_ng/model/downloader.py index 52c8e30..3f6151f 100644 --- a/tidal_dl_ng/model/downloader.py +++ b/tidal_dl_ng/model/downloader.py @@ -1,6 +1,8 @@ import pathlib from dataclasses import dataclass +from requests import HTTPError + @dataclass class DownloadSegmentResult: @@ -8,3 +10,4 @@ class DownloadSegmentResult: url: str path_segment: pathlib.Path id_segment: int + error: HTTPError | None = None diff --git a/tidal_dl_ng/ui/dialog_settings.py b/tidal_dl_ng/ui/dialog_settings.py index ad45f1d..a624e34 100644 --- a/tidal_dl_ng/ui/dialog_settings.py +++ b/tidal_dl_ng/ui/dialog_settings.py @@ -153,6 +153,24 @@ class Ui_DialogSettings: self.lv_flags.addLayout(self.lh_flags_4) + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.lv_flag_skip_existing = QVBoxLayout() + self.lv_flag_skip_existing.setObjectName("lv_flag_skip_existing") + self.cb_skip_existing = QCheckBox(self.gb_flags) + self.cb_skip_existing.setObjectName("cb_skip_existing") + + self.lv_flag_skip_existing.addWidget(self.cb_skip_existing) + + self.horizontalLayout.addLayout(self.lv_flag_skip_existing) + + self.verticalLayout_3 = QVBoxLayout() + self.verticalLayout_3.setObjectName("verticalLayout_3") + + self.horizontalLayout.addLayout(self.verticalLayout_3) + + self.lv_flags.addLayout(self.horizontalLayout) + self.lv_main.addWidget(self.gb_flags) self.gb_choices = QGroupBox(DialogSettings) @@ -164,38 +182,13 @@ class Ui_DialogSettings: self.gb_choices.setSizePolicy(sizePolicy3) self.lv_choices = QVBoxLayout(self.gb_choices) self.lv_choices.setObjectName("lv_choices") - self.lh_choices_skip_existing = QHBoxLayout() - self.lh_choices_skip_existing.setObjectName("lh_choices_skip_existing") - self.l_icon_skip_existing = QLabel(self.gb_choices) - self.l_icon_skip_existing.setObjectName("l_icon_skip_existing") - sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) - sizePolicy4.setHorizontalStretch(0) - sizePolicy4.setVerticalStretch(0) - sizePolicy4.setHeightForWidth(self.l_icon_skip_existing.sizePolicy().hasHeightForWidth()) - self.l_icon_skip_existing.setSizePolicy(sizePolicy4) - - self.lh_choices_skip_existing.addWidget(self.l_icon_skip_existing) - - self.l_skip_existing = QLabel(self.gb_choices) - self.l_skip_existing.setObjectName("l_skip_existing") - sizePolicy4.setHeightForWidth(self.l_skip_existing.sizePolicy().hasHeightForWidth()) - self.l_skip_existing.setSizePolicy(sizePolicy4) - - self.lh_choices_skip_existing.addWidget(self.l_skip_existing) - - self.c_skip_existing = QComboBox(self.gb_choices) - self.c_skip_existing.setObjectName("c_skip_existing") - - self.lh_choices_skip_existing.addWidget(self.c_skip_existing) - - self.lh_choices_skip_existing.setStretch(2, 50) - - self.lv_choices.addLayout(self.lh_choices_skip_existing) - self.lh_choices_quality_audio = QHBoxLayout() self.lh_choices_quality_audio.setObjectName("lh_choices_quality_audio") self.l_icon_quality_audio = QLabel(self.gb_choices) self.l_icon_quality_audio.setObjectName("l_icon_quality_audio") + sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + sizePolicy4.setHorizontalStretch(0) + sizePolicy4.setVerticalStretch(0) sizePolicy4.setHeightForWidth(self.l_icon_quality_audio.sizePolicy().hasHeightForWidth()) self.l_icon_quality_audio.setSizePolicy(sizePolicy4) @@ -532,9 +525,8 @@ class Ui_DialogSettings: self.cb_extract_flac.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) self.cb_metadata_cover_embed.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) self.cb_cover_album_file.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) + self.cb_skip_existing.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) self.gb_choices.setTitle(QCoreApplication.translate("DialogSettings", "Choices", None)) - self.l_icon_skip_existing.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) - self.l_skip_existing.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) self.l_icon_quality_audio.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) self.l_quality_audio.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) self.l_icon_quality_video.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) diff --git a/tidal_dl_ng/ui/dialog_settings.ui b/tidal_dl_ng/ui/dialog_settings.ui index 9bc894d..4fa07cf 100644 --- a/tidal_dl_ng/ui/dialog_settings.ui +++ b/tidal_dl_ng/ui/dialog_settings.ui @@ -210,6 +210,24 @@ + + + + + + + + CheckBox + + + + + + + + + + @@ -225,39 +243,6 @@ Choices - - - - - - - 0 - 0 - - - - TextLabel - - - - - - - - 0 - 0 - - - - TextLabel - - - - - - - - From 3171b1570be4fe5a520bda45049a50a34037fbbb Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 13 Nov 2024 09:49:33 +0100 Subject: [PATCH 04/29] =?UTF-8?q?=E2=9C=A8=20Do=20not=20process=20tracks?= =?UTF-8?q?=20with=20missing=20segments.=20Refactored=20some=20method=20to?= =?UTF-8?q?=20`pathlib.Path`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/download.py | 90 +++++++++++++++++++------------------- tidal_dl_ng/helper/path.py | 12 ++--- 2 files changed, 52 insertions(+), 50 deletions(-) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index a9348f3..98a7c75 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -92,7 +92,7 @@ class Download: self, media: Track | Video, path_file: pathlib.Path, - ) -> pathlib.Path: + ) -> (bool, pathlib.Path): media_name: str = name_builder_item(media) urls: [str] path_base: pathlib.Path = path_file.parent @@ -165,21 +165,20 @@ class Download: # mark the whole thing as corrupt. result_segments = False - # Bring list into right order, so segments can be easily merged. - dl_segment_results.sort(key=lambda x: x.id_segment) - # TODO: Implement error handling on corrupt segments (also in the following method. - reult_merge: bool = self._segments_merge(path_file, dl_segment_results) - tmp_path_file_decrypted: pathlib.Path = pathlib.Path("") + tmp_path_file_decrypted: pathlib.Path = path_file - if reult_merge: - if isinstance(media, Track) and stream_manifest.is_encrypted: + # Only if no error happened while downloading. + if result_segments: + # Bring list into right order, so segments can be easily merged. + dl_segment_results.sort(key=lambda x: x.id_segment) + reult_merge: bool = self._segments_merge(path_file, dl_segment_results) + + if reult_merge and isinstance(media, Track) and stream_manifest.is_encrypted: key, nonce = decrypt_security_token(stream_manifest.encryption_key) - tmp_path_file_decrypted = path_file + "_decrypted" + tmp_path_file_decrypted = path_file.with_suffix("_decrypted") decrypt_file(path_file, tmp_path_file_decrypted, key, nonce) - else: - tmp_path_file_decrypted = path_file - return tmp_path_file_decrypted + return result_segments, tmp_path_file_decrypted def _segments_merge(self, path_file, dl_segment_results) -> bool: result: bool @@ -229,7 +228,7 @@ class Download: except HTTPError as e: error = e self.progress.advance(p_task) - self.fn_logger.exception(e.charachter_written) + self.fn_logger.exception(e) # To send the progress to the GUI, we need to emit the percentage. if not progress_to_stdout: @@ -281,12 +280,14 @@ class Download: # Create file name and path file_extension_dummy: str = AudioExtensions.FLAC file_name_relative = format_path_media(file_template, media) - path_media_dst = os.path.abspath( - os.path.normpath(os.path.join(os.path.expanduser(self.path_base), file_name_relative)) + path_media_dst: pathlib.Path = ( + (pathlib.Path(self.path_base).expanduser() / file_name_relative).resolve().absolute() ) # Sanitize final path_file to fit into OS boundaries. - path_media_dst = path_file_sanitize(path_media_dst + file_extension_dummy, adapt=True) + path_media_dst = pathlib.Path( + path_file_sanitize(str(path_media_dst.with_suffix(file_extension_dummy)), adapt=True) + ) # Compute if and how downloads need to be skipped. if self.skip_existing: @@ -336,38 +337,39 @@ class Download: tmp_path_file.touch() # Download media. - tmp_path_file = self._download(media=media, path_file=tmp_path_file) + result_download, tmp_path_file = self._download(media=media, path_file=tmp_path_file) - # Convert video from TS to MP4 - if isinstance(media, Video) and self.settings.data.video_convert_mp4: - # Convert `*.ts` file to `*.mp4` using ffmpeg - tmp_path_file = self._video_convert(tmp_path_file) + if result_download: + # Convert video from TS to MP4 + if isinstance(media, Video) and self.settings.data.video_convert_mp4: + # Convert `*.ts` file to `*.mp4` using ffmpeg + tmp_path_file = self._video_convert(tmp_path_file) - # Extract FLAC from MP4 container using ffmpeg - if isinstance(media, Track) and self.settings.data.extract_flac and do_flac_extract: - tmp_path_file = self._extract_flac(tmp_path_file) + # Extract FLAC from MP4 container using ffmpeg + if isinstance(media, Track) and self.settings.data.extract_flac and do_flac_extract: + tmp_path_file = self._extract_flac(tmp_path_file) - tmp_path_lyrics: pathlib.Path | None = None - tmp_path_cover: pathlib.Path | None = None + tmp_path_lyrics: pathlib.Path | None = None + tmp_path_cover: pathlib.Path | None = None - # Write metadata to file. - if not isinstance(media, Video): - result_metadata, tmp_path_lyrics, tmp_path_cover = self.metadata_write( - media, tmp_path_file, is_parent_album - ) + # Write metadata to file. + if not isinstance(media, Video): + result_metadata, tmp_path_lyrics, tmp_path_cover = self.metadata_write( + media, tmp_path_file, is_parent_album + ) + + # Move lyrics file + if self.settings.data.lyrics_file and not isinstance(media, Video): + self._move_lyrics(tmp_path_lyrics, path_media_dst) + + # Move cover file + if self.settings.data.cover_album_file: + self._move_cover(tmp_path_cover, path_media_dst) # Move final file to the configured destination directory. - os.makedirs(os.path.dirname(path_media_dst), exist_ok=True) + os.makedirs(path_media_dst.parent, exist_ok=True) shutil.move(tmp_path_file, path_media_dst) - # Move lyrics file - if self.settings.data.lyrics_file and not isinstance(media, Video): - self._move_lyrics(tmp_path_lyrics, path_media_dst) - - # Move cover file - if self.settings.data.cover_album_file: - self._move_cover(tmp_path_cover, path_media_dst) - if quality_audio: # Set quality back to the global user value self.adjust_quality_audio(quality_audio_old) @@ -418,16 +420,16 @@ class Download: return result - def _move_lyrics(self, path_lyrics: pathlib.Path, file_media_dst: str) -> bool: + def _move_lyrics(self, path_lyrics: pathlib.Path, file_media_dst: pathlib.Path) -> bool: # Build tmp lyrics filename - path_file_lyrics: str = os.path.splitext(file_media_dst)[0] + EXTENSION_LYRICS + path_file_lyrics: pathlib.Path = file_media_dst.with_suffix(EXTENSION_LYRICS) result: bool = self._move_file(path_lyrics, path_file_lyrics) return result - def _move_cover(self, path_cover: pathlib.Path, file_media_dst: str) -> bool: + def _move_cover(self, path_cover: pathlib.Path, file_media_dst: pathlib.Path) -> bool: # Build tmp lyrics filename - path_file_cover: pathlib.Path = pathlib.Path(file_media_dst).parent.absolute() / COVER_NAME + path_file_cover: pathlib.Path = file_media_dst.parent / COVER_NAME result: bool = self._move_file(path_cover, path_file_cover) return result diff --git a/tidal_dl_ng/helper/path.py b/tidal_dl_ng/helper/path.py index e6f5b14..627f7c1 100644 --- a/tidal_dl_ng/helper/path.py +++ b/tidal_dl_ng/helper/path.py @@ -1,9 +1,9 @@ import math import os +import pathlib import posixpath import re import sys -from pathlib import Path, PosixPath from urllib.parse import unquote, urlsplit from pathvalidate import sanitize_filename, sanitize_filepath @@ -197,7 +197,7 @@ def get_format_template( def path_file_sanitize(path_file: str, adapt: bool = False, uniquify: bool = False) -> (bool, str): # Split into path and filename pathname, filename = os.path.split(path_file) - file_extension: str = Path(path_file).suffix + file_extension: str = pathlib.Path(path_file).suffix # Sanitize path try: @@ -207,7 +207,7 @@ def path_file_sanitize(path_file: str, adapt: bool = False, uniquify: bool = Fal except ValidationError: # If adaption of path is allowed in case of an error set path to HOME. if adapt: - pathname_sanitized: str = Path.home() + pathname_sanitized: str = str(pathlib.Path.home()) else: raise @@ -274,10 +274,10 @@ def file_unique_suffix(path_file: str, seperator: str = "_") -> str: return unique_suffix -def check_file_exists(path_file: str, extension_ignore: bool = False) -> bool: +def check_file_exists(path_file: pathlib.Path, extension_ignore: bool = False) -> bool: if extension_ignore: - path_file_stem: str = Path(path_file).stem - path_parent: PosixPath = Path(path_file).parent + path_file_stem: str = pathlib.Path(path_file).stem + path_parent: pathlib.Path = pathlib.Path(path_file).parent path_files: [str] = [] for extension in AudioExtensions: From 0eea0f35484786a549eaf77574f4f4742464af5a Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 13 Nov 2024 10:37:29 +0100 Subject: [PATCH 05/29] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Fixed=20suffix=20?= =?UTF-8?q?handling=20with=20`pathlib.Path`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/download.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index 98a7c75..2329381 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -281,13 +281,13 @@ class Download: file_extension_dummy: str = AudioExtensions.FLAC file_name_relative = format_path_media(file_template, media) path_media_dst: pathlib.Path = ( - (pathlib.Path(self.path_base).expanduser() / file_name_relative).resolve().absolute() + (pathlib.Path(self.path_base).expanduser() / (file_name_relative + file_extension_dummy)) + .resolve() + .absolute() ) # Sanitize final path_file to fit into OS boundaries. - path_media_dst = pathlib.Path( - path_file_sanitize(str(path_media_dst.with_suffix(file_extension_dummy)), adapt=True) - ) + path_media_dst = pathlib.Path(path_file_sanitize(str(path_media_dst), adapt=True)) # Compute if and how downloads need to be skipped. if self.skip_existing: @@ -327,7 +327,9 @@ class Download: elif isinstance(media, Video): file_extension = AudioExtensions.MP4 if self.settings.data.video_convert_mp4 else VideoExtensions.TS - # TODO: correct extension + # Compute file name and create destination directory + path_media_dst = path_media_dst.with_suffix(file_extension) + os.makedirs(path_media_dst.parent, exist_ok=True) # Create a temp directory and file. with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp_path_dir: @@ -359,15 +361,14 @@ class Download: ) # Move lyrics file - if self.settings.data.lyrics_file and not isinstance(media, Video): + if self.settings.data.lyrics_file and not isinstance(media, Video) and tmp_path_lyrics: self._move_lyrics(tmp_path_lyrics, path_media_dst) # Move cover file - if self.settings.data.cover_album_file: + if self.settings.data.cover_album_file and tmp_path_cover: self._move_cover(tmp_path_cover, path_media_dst) # Move final file to the configured destination directory. - os.makedirs(path_media_dst.parent, exist_ok=True) shutil.move(tmp_path_file, path_media_dst) if quality_audio: @@ -410,7 +411,7 @@ class Download: result: bool # Check if the file was downloaded - if path_file_source and os.path.isfile(path_file_source): + if path_file_source and path_file_source.is_file(): # Move it. shutil.move(path_file_source, path_file_destination) From f3be5f30140b5604460e06f09abd2405771cf682 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 13 Nov 2024 10:58:52 +0100 Subject: [PATCH 06/29] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Fixed=20ffmpeg=20?= =?UTF-8?q?output=20file=20to=20`str`.=20Fixed=20segment=20parsing=20for?= =?UTF-8?q?=20videos=20while=20downloading=20segments.=20Lowered=20time=20?= =?UTF-8?q?delay=20span.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/download.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index 2329381..26d6f84 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -208,7 +208,7 @@ class Download: result: bool = False path_segment: pathlib.Path = path_base / url_to_filename(url) # Calculate the segment ID based on the file name within the URL. - id_segment: int = int(path_segment.stem) + id_segment: int = int(str(path_segment.stem).split("_")[-1]) error: HTTPError | None = None try: @@ -386,7 +386,7 @@ class Download: # Whether a file was downloaded or skipped and the download delay is enabled, wait until the next download. # Only use this, if you have a list of several Track items. if download_delay and not skip_file: - time_sleep: float = round(random.SystemRandom().uniform(2, 5), 1) + time_sleep: float = round(random.SystemRandom().uniform(1.5, 4), 1) self.fn_logger.debug(f"Next download will start in {time_sleep} seconds.") time.sleep(time_sleep) @@ -608,7 +608,7 @@ class Download: path_file_out: pathlib.Path = path_file.with_suffix(AudioExtensions.MP4) result, _ = ( ffmpeg.input(path_file) - .output(path_file_out, map=0, c="copy", loglevel="quiet") + .output(str(path_file_out), map=0, c="copy", loglevel="quiet") .run(cmd=self.settings.data.path_binary_ffmpeg) ) @@ -619,7 +619,12 @@ class Download: result, _ = ( ffmpeg.input(path_media_src) .output( - path_media_out, map=0, movflags="use_metadata_tags", acodec="copy", map_metadata="0:g", loglevel="quiet" + str(path_media_out), + map=0, + movflags="use_metadata_tags", + acodec="copy", + map_metadata="0:g", + loglevel="quiet", ) .run(cmd=self.settings.data.path_binary_ffmpeg) ) From ae2dd4fb2b4738cf73d2a0d1b0d94e7410b5ca65 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 13 Nov 2024 12:04:24 +0100 Subject: [PATCH 07/29] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Do=20not=20attemp?= =?UTF-8?q?t=20to=20set=20quality=20if=20it=20is=20set=20to=20`None`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/download.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index 26d6f84..6c637f9 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -300,8 +300,10 @@ class Download: file_extension: str do_flac_extract = False # If a quality is explicitly set, change it and remember the previously set quality. - quality_audio_old: Quality = self.adjust_quality_audio(quality_audio) - quality_video_old: QualityVideo = self.adjust_quality_video(quality_video) + quality_audio_old: Quality = self.adjust_quality_audio(quality_audio) if quality_audio else quality_audio + quality_video_old: QualityVideo = ( + self.adjust_quality_video(quality_video) if quality_video else quality_video + ) if isinstance(media, Track): try: @@ -312,6 +314,10 @@ class Download: f"Consider to activate delay between downloads." ) + return False, "" + except: + self.fn_logger.exception(f"Something went wrong. Skipping '{name_builder_item(media)}'.") + return False, "" file_extension = media_stream.get_stream_manifest().file_extension From f5ed1c6554dbe5e7dda8ce662ac7ee49919cece3 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 13 Nov 2024 12:36:01 +0100 Subject: [PATCH 08/29] =?UTF-8?q?=E2=9C=A8=20Time=20exponential=20retries?= =?UTF-8?q?=20on=20segment=20download=20errors.=20Fixes=20#196?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/download.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index 6c637f9..6f039a8 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -12,6 +12,7 @@ import ffmpeg import m3u8 import requests from constants import CHUNK_SIZE, COVER_NAME +from requests.adapters import HTTPAdapter, Retry from requests.exceptions import HTTPError from rich.progress import Progress, TaskID from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video @@ -164,6 +165,7 @@ class Download: # If this is NOT the case, but any other URL has resulted in an error, # mark the whole thing as corrupt. result_segments = False + self.fn_logger.error(f"Something went wrong while downloading {media_name}. File is corrupt!") tmp_path_file_decrypted: pathlib.Path = path_file @@ -211,9 +213,15 @@ class Download: id_segment: int = int(str(path_segment.stem).split("_")[-1]) error: HTTPError | None = None + # Retry download on failed segments, with an exponential delay between retries + s = requests.Session() + retries = Retry(total=5, backoff_factor=1) # , status_forcelist=[ 502, 503, 504 ]) + + s.mount("https://", HTTPAdapter(max_retries=retries)) + try: # 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 = s.get(url, stream=True, timeout=REQUESTS_TIMEOUT_SEC) r.raise_for_status() @@ -225,7 +233,7 @@ class Download: self.progress.advance(p_task) result = True - except HTTPError as e: + except Exception as e: error = e self.progress.advance(p_task) self.fn_logger.exception(e) From 7134e0e51b3653ed91a30f973898a2476d06f57b Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 13 Nov 2024 13:09:58 +0100 Subject: [PATCH 09/29] =?UTF-8?q?=E2=9C=A8=20`tidalapi`=20and=20dependent?= =?UTF-8?q?=20code=20was=20updated.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 274 ++++++++++++++++++------------------ pyproject.toml | 4 +- tidal_dl_ng/download.py | 2 +- tidal_dl_ng/helper/tidal.py | 2 +- 4 files changed, 141 insertions(+), 141 deletions(-) diff --git a/poetry.lock b/poetry.lock index f66f37a..515cdd4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -636,13 +636,13 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "identify" -version = "2.6.1" +version = "2.6.2" description = "File identification library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, - {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, + {file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"}, + {file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"}, ] [package.extras] @@ -1150,13 +1150,13 @@ files = [ [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -1438,23 +1438,23 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyinstaller" -version = "6.11.0" +version = "6.11.1" description = "PyInstaller bundles a Python application and all its dependencies into a single package." optional = false python-versions = "<3.14,>=3.8" files = [ - {file = "pyinstaller-6.11.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:6fd68a3c1207635c49326c54881b89d5c3bd9ba061bbc9daa58c0902db1be39e"}, - {file = "pyinstaller-6.11.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:eddd53f231e51adc65088eac4f40057ca803a990239828d4a9229407fb866239"}, - {file = "pyinstaller-6.11.0-py3-none-manylinux2014_i686.whl", hash = "sha256:e6d229009e815542833fe00332b589aa6984a06f794dc16f2ce1acab1c567590"}, - {file = "pyinstaller-6.11.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:7d2cd2ebdcd6860f8a4abe2977264a7b6d260a7147047008971c7cfc66a656a4"}, - {file = "pyinstaller-6.11.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:d9ec6d4398b4eebc1d4c00437716264ba8406bc2746f594e253070a82378a584"}, - {file = "pyinstaller-6.11.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:04f71828aa9531ab18c9656985c1f09b83d10332c73a1f4a113a48b491906955"}, - {file = "pyinstaller-6.11.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:a843d470768d68b05684ccf4860c45b2eb13727f41667c0b2cd8f57ae231bd18"}, - {file = "pyinstaller-6.11.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:963dedc1f37144a4385f58f7f65f1c69c004a67faae522a2085b5ddb230c908b"}, - {file = "pyinstaller-6.11.0-py3-none-win32.whl", hash = "sha256:c71024c8a19c7b221b9152b2baff4c3ba849cada68dcdd34382ba09f0107451f"}, - {file = "pyinstaller-6.11.0-py3-none-win_amd64.whl", hash = "sha256:0e229610c22b96d741d905706f9496af472c1a9216a118988f393c98ecc3f51f"}, - {file = "pyinstaller-6.11.0-py3-none-win_arm64.whl", hash = "sha256:a5f716bb507517912fda39d109dead91fc0dd2e7b2859562522b63c61aa21676"}, - {file = "pyinstaller-6.11.0.tar.gz", hash = "sha256:cb4d433a3db30d9d17cf5f2cf7bb4df80a788d493c1d67dd822dc5791d9864af"}, + {file = "pyinstaller-6.11.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:44e36172de326af6d4e7663b12f71dbd34e2e3e02233e181e457394423daaf03"}, + {file = "pyinstaller-6.11.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6d12c45a29add78039066a53fb05967afaa09a672426072b13816fe7676abfc4"}, + {file = "pyinstaller-6.11.1-py3-none-manylinux2014_i686.whl", hash = "sha256:ddc0fddd75f07f7e423da1f0822e389a42af011f9589e0269b87e0d89aa48c1f"}, + {file = "pyinstaller-6.11.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:0d6475559c4939f0735122989611d7f739ed3bf02f666ce31022928f7a7e4fda"}, + {file = "pyinstaller-6.11.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:e21c7806e34f40181e7606926a14579f848bfb1dc52cbca7eea66eccccbfe977"}, + {file = "pyinstaller-6.11.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:32c742a24fe65d0702958fadf4040f76de85859c26bec0008766e5dbabc5b68f"}, + {file = "pyinstaller-6.11.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:208c0ef6dab0837a0a273ea32d1a3619a208e3d1fe3fec3785eea71a77fd00ce"}, + {file = "pyinstaller-6.11.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ad84abf465bcda363c1d54eafa76745d77b6a8a713778348377dc98d12a452f7"}, + {file = "pyinstaller-6.11.1-py3-none-win32.whl", hash = "sha256:2e8365276c5131c9bef98e358fbc305e4022db8bedc9df479629d6414021956a"}, + {file = "pyinstaller-6.11.1-py3-none-win_amd64.whl", hash = "sha256:7ac83c0dc0e04357dab98c487e74ad2adb30e7eb186b58157a8faf46f1fa796f"}, + {file = "pyinstaller-6.11.1-py3-none-win_arm64.whl", hash = "sha256:35e6b8077d240600bb309ed68bb0b1453fd2b7ab740b66d000db7abae6244423"}, + {file = "pyinstaller-6.11.1.tar.gz", hash = "sha256:491dfb4d9d5d1d9650d9507daec1ff6829527a254d8e396badd60a0affcb72ef"}, ] [package.dependencies] @@ -1462,7 +1462,7 @@ altgraph = "*" macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} packaging = ">=22.0" pefile = {version = ">=2022.5.30,<2024.8.26 || >2024.8.26", markers = "sys_platform == \"win32\""} -pyinstaller-hooks-contrib = ">=2024.8" +pyinstaller-hooks-contrib = ">=2024.9" pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} setuptools = ">=42.0.0" @@ -1472,13 +1472,13 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2024.9" +version = "2024.10" description = "Community maintained hooks for PyInstaller" optional = false python-versions = ">=3.8" files = [ - {file = "pyinstaller_hooks_contrib-2024.9-py3-none-any.whl", hash = "sha256:1ddf9ba21d586afa84e505bb5c65fca10b22500bf3fdb89ee2965b99da53b891"}, - {file = "pyinstaller_hooks_contrib-2024.9.tar.gz", hash = "sha256:4793869f370d1dc4806c101efd2890e3c3e703467d8d27bb5a3db005ebfb008d"}, + {file = "pyinstaller_hooks_contrib-2024.10-py3-none-any.whl", hash = "sha256:ad47db0e153683b4151e10d231cb91f2d93c85079e78d76d9e0f57ac6c8a5e10"}, + {file = "pyinstaller_hooks_contrib-2024.10.tar.gz", hash = "sha256:8a46655e5c5b0186b5e527399118a9b342f10513eb1425c483fa4f6d02e8800c"}, ] [package.dependencies] @@ -1797,105 +1797,105 @@ files = [ [[package]] name = "regex" -version = "2024.9.11" +version = "2024.11.6" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" files = [ - {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408"}, - {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d"}, - {file = "regex-2024.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a"}, - {file = "regex-2024.9.11-cp310-cp310-win32.whl", hash = "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0"}, - {file = "regex-2024.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623"}, - {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df"}, - {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268"}, - {file = "regex-2024.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1"}, - {file = "regex-2024.9.11-cp311-cp311-win32.whl", hash = "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9"}, - {file = "regex-2024.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf"}, - {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7"}, - {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231"}, - {file = "regex-2024.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a"}, - {file = "regex-2024.9.11-cp312-cp312-win32.whl", hash = "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776"}, - {file = "regex-2024.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009"}, - {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784"}, - {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36"}, - {file = "regex-2024.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8"}, - {file = "regex-2024.9.11-cp313-cp313-win32.whl", hash = "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8"}, - {file = "regex-2024.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f"}, - {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4"}, - {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e"}, - {file = "regex-2024.9.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd"}, - {file = "regex-2024.9.11-cp38-cp38-win32.whl", hash = "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771"}, - {file = "regex-2024.9.11-cp38-cp38-win_amd64.whl", hash = "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508"}, - {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066"}, - {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62"}, - {file = "regex-2024.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35"}, - {file = "regex-2024.9.11-cp39-cp39-win32.whl", hash = "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142"}, - {file = "regex-2024.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919"}, - {file = "regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, + {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, + {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, + {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, + {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, + {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, + {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, + {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, + {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, + {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, + {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, + {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, + {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, ] [[package]] @@ -1940,23 +1940,23 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "75.3.0" +version = "75.5.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, - {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, + {file = "setuptools-75.5.0-py3-none-any.whl", hash = "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829"}, + {file = "setuptools-75.5.0.tar.gz", hash = "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] +core = ["importlib-metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.12.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] [[package]] name = "shellingham" @@ -2020,13 +2020,13 @@ pbr = ">=2.0.0" [[package]] name = "tidalapi" -version = "0.8.0" +version = "0.8.1" description = "Unofficial API for TIDAL music streaming service." optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "tidalapi-0.8.0-py3-none-any.whl", hash = "sha256:114b8cf127bb9d6f02796b446f9202571989fbd5c3c62b69b58c43d9ada77e38"}, - {file = "tidalapi-0.8.0.tar.gz", hash = "sha256:da0fca3cb790db6475526d724d868d079554203eb9b0636af7ff60f7e023a690"}, + {file = "tidalapi-0.8.1-py3-none-any.whl", hash = "sha256:933ee71bc081d6538460d762ce70d0490de52ce027398112218037f7bf2f1cf2"}, + {file = "tidalapi-0.8.1.tar.gz", hash = "sha256:eb48e42600533513161fd9fc9b1ee06c9788dbe1e111bb8e91b9aa9603ab3a6b"}, ] [package.dependencies] @@ -2061,13 +2061,13 @@ files = [ [[package]] name = "tomli" -version = "2.0.2" +version = "2.1.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, - {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, + {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, + {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, ] [[package]] @@ -2110,13 +2110,13 @@ test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"] [[package]] name = "typer" -version = "0.12.5" +version = "0.13.0" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" files = [ - {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, - {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, + {file = "typer-0.13.0-py3-none-any.whl", hash = "sha256:d85fe0b777b2517cc99c8055ed735452f2659cd45e451507c76f48ce5c1d00e2"}, + {file = "typer-0.13.0.tar.gz", hash = "sha256:f1c7198347939361eec90139ffa0fd8b3df3a2259d5852a0f7400e476d95985c"}, ] [package.dependencies] @@ -2236,4 +2236,4 @@ gui = ["pyqtdarktheme-fork", "pyside6"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "92af866c0916091be26c50790b79c210f70a939fc8c89c9e44857d30b76bc7bd" +content-hash = "7320eb9a300445a91002452fa6961cc177c6f81f72bc71acedf0e243ffa318d9" diff --git a/pyproject.toml b/pyproject.toml index dc89604..3a213b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,9 +38,9 @@ pyqtdarktheme-fork = {version = "^2.3.2", optional = true} mpegdash = "^0.4.0" rich = "^13.9.4" toml = "^0.10.2" -typer = "^0.12.5" +typer = "^0.13.0" ffmpeg-python = "^0.2.0" -tidalapi = "^0.8.0" +tidalapi = "^0.8.1" [tool.poetry.extras] gui = ["pyside6", "pyqtdarktheme-fork"] diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index 6f039a8..10e8f68 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -102,7 +102,7 @@ class Download: # Get urls for media. if isinstance(media, Track): - urls = media.get_stream().get_stream_manifest().urls + urls = media.get_stream().get_stream_manifest().get_urls() stream_manifest: StreamManifest = media.get_stream().get_stream_manifest() elif isinstance(media, Video): m3u8_variant: m3u8.M3U8 = m3u8.load(media.get_url()) diff --git a/tidal_dl_ng/helper/tidal.py b/tidal_dl_ng/helper/tidal.py index d679d5e..ebea4cd 100644 --- a/tidal_dl_ng/helper/tidal.py +++ b/tidal_dl_ng/helper/tidal.py @@ -186,7 +186,7 @@ def instantiate_media( def quality_audio_highest(media: Track | Album) -> Quality: quality: Quality - if MediaMetadataTags.hires_lossless in media.media_metadata_tags: + if MediaMetadataTags.hi_res_lossless in media.media_metadata_tags: quality = Quality.hi_res_lossless elif MediaMetadataTags.lossless in media.media_metadata_tags: quality = Quality.high_lossless From 9e0b9748cb5a53926d4b0a2cd9f56f271c0ec473 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 13 Nov 2024 13:30:50 +0100 Subject: [PATCH 10/29] =?UTF-8?q?=F0=9F=8C=9F=20Version=20bumped=20to=20v0?= =?UTF-8?q?.20.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3a213b9..5cd1554 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "tidal-dl-ng" authors = ["Robert Honz "] -version = "0.15.6" +version = "0.20.0" description = "TIDAL Medial Downloader Next Generation!" repository = "https://github.com/exislow/tidal-dl-ng" documentation = "https://exislow.github.io/tidal-dl-ng/" From 65dbe8524154a9c36413c40185b91ab0b10f82aa Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 13 Nov 2024 13:45:38 +0100 Subject: [PATCH 11/29] =?UTF-8?q?=E2=9C=A8=20Bug=20fix=20for=20import=20co?= =?UTF-8?q?nstants.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- tidal_dl_ng/download.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5cd1554..0e78e05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -171,7 +171,7 @@ profile = "black" multi_line_output = 3 indent = 4 color_output = true -known_third_party = ["Crypto", "PySide6", "coloredlogs", "constants", "dataclasses_json", "ffmpeg", "m3u8", "mutagen", "pathvalidate", "requests", "rich", "tidalapi", "toml", "typer"] +known_third_party = ["Crypto", "PySide6", "coloredlogs", "dataclasses_json", "ffmpeg", "m3u8", "mutagen", "pathvalidate", "requests", "rich", "tidalapi", "toml", "typer"] [tool.mypy] # https://mypy.readthedocs.io/en/latest/config_file.html#using-a-pyproject-toml-file diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index 10e8f68..f5b7404 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -11,7 +11,6 @@ from uuid import uuid4 import ffmpeg import m3u8 import requests -from constants import CHUNK_SIZE, COVER_NAME from requests.adapters import HTTPAdapter, Retry from requests.exceptions import HTTPError from rich.progress import Progress, TaskID @@ -20,7 +19,14 @@ from tidalapi.exceptions import TooManyRequests from tidalapi.media import AudioExtensions, Codec, Quality, StreamManifest, VideoExtensions from tidal_dl_ng.config import Settings -from tidal_dl_ng.constants import EXTENSION_LYRICS, REQUESTS_TIMEOUT_SEC, MediaType, QualityVideo +from tidal_dl_ng.constants import ( + CHUNK_SIZE, + COVER_NAME, + EXTENSION_LYRICS, + REQUESTS_TIMEOUT_SEC, + MediaType, + QualityVideo, +) from tidal_dl_ng.helper.decryption import decrypt_file, decrypt_security_token from tidal_dl_ng.helper.exceptions import MediaMissing from tidal_dl_ng.helper.path import check_file_exists, format_path_media, path_file_sanitize, url_to_filename From 182e67e7c3858851604d84d28a75b9bba1b45313 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 13 Nov 2024 13:45:55 +0100 Subject: [PATCH 12/29] =?UTF-8?q?=F0=9F=8C=9F=20Version=20bumped=20to=20v0?= =?UTF-8?q?.20.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0e78e05..c00683a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "tidal-dl-ng" authors = ["Robert Honz "] -version = "0.20.0" +version = "0.20.1" description = "TIDAL Medial Downloader Next Generation!" repository = "https://github.com/exislow/tidal-dl-ng" documentation = "https://exislow.github.io/tidal-dl-ng/" From c3c0b90de240e4029a3edd9edfebc8e561eb9d99 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 13 Nov 2024 14:04:25 +0100 Subject: [PATCH 13/29] =?UTF-8?q?=E2=9C=A8=20MKDocs=20update.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 29 +++++++++++++++-------------- pyproject.toml | 7 ++++--- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/poetry.lock b/poetry.lock index 515cdd4..72b0f3c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1011,24 +1011,24 @@ files = [ [[package]] name = "mkdocstrings" -version = "0.24.3" +version = "0.27.0" description = "Automatic documentation from sources, for MkDocs." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "mkdocstrings-0.24.3-py3-none-any.whl", hash = "sha256:5c9cf2a32958cd161d5428699b79c8b0988856b0d4a8c5baf8395fc1bf4087c3"}, - {file = "mkdocstrings-0.24.3.tar.gz", hash = "sha256:f327b234eb8d2551a306735436e157d0a22d45f79963c60a8b585d5f7a94c1d2"}, + {file = "mkdocstrings-0.27.0-py3-none-any.whl", hash = "sha256:6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332"}, + {file = "mkdocstrings-0.27.0.tar.gz", hash = "sha256:16adca6d6b0a1f9e0c07ff0b02ced8e16f228a9d65a37c063ec4c14d7b76a657"}, ] [package.dependencies] click = ">=7.0" Jinja2 = ">=2.11.1" -Markdown = ">=3.3" +Markdown = ">=3.6" MarkupSafe = ">=1.1" mkdocs = ">=1.4" -mkdocs-autorefs = ">=0.3.1" +mkdocs-autorefs = ">=1.2" mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} -platformdirs = ">=2.2.0" +platformdirs = ">=2.2" pymdown-extensions = ">=6.3" [package.extras] @@ -1038,18 +1038,19 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "1.10.0" +version = "1.12.2" description = "A Python handler for mkdocstrings." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "mkdocstrings_python-1.10.0-py3-none-any.whl", hash = "sha256:ba833fbd9d178a4b9d5cb2553a4df06e51dc1f51e41559a4d2398c16a6f69ecc"}, - {file = "mkdocstrings_python-1.10.0.tar.gz", hash = "sha256:71678fac657d4d2bb301eed4e4d2d91499c095fd1f8a90fa76422a87a5693828"}, + {file = "mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a"}, + {file = "mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3"}, ] [package.dependencies] -griffe = ">=0.44" -mkdocstrings = ">=0.24.2" +griffe = ">=0.49" +mkdocs-autorefs = ">=1.2" +mkdocstrings = ">=0.26" [[package]] name = "mpegdash" @@ -2236,4 +2237,4 @@ gui = ["pyqtdarktheme-fork", "pyside6"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "7320eb9a300445a91002452fa6961cc177c6f81f72bc71acedf0e243ffa318d9" +content-hash = "6a5d6313d6690d1d6c8ab1940903888bd880b97718d7157c7124be087e902f6a" diff --git a/pyproject.toml b/pyproject.toml index c00683a..56fd4db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,9 +70,10 @@ black = "^24.10.0" optional = true [tool.poetry.group.docs.dependencies] -mkdocs = "^1.5.3" -mkdocs-material = "^9.4.14" -mkdocstrings = { extras = ["python"], version = "^0.24.0" } +mkdocs = "^1.6.1" +mkdocs-material = "^9.5.44" +mkdocstrings = { extras = ["python"], version = "^0.27.0" } +griffe = "^1.5.1" [build-system] requires = ["poetry-core>=1.0.0"] From 84b777ca0a3e40656f1bfd1434eabe82358d7918 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 13 Nov 2024 14:05:10 +0100 Subject: [PATCH 14/29] =?UTF-8?q?=E2=9C=A8=20Lock=20file=20update.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 72b0f3c..9a5c670 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2237,4 +2237,4 @@ gui = ["pyqtdarktheme-fork", "pyside6"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "6a5d6313d6690d1d6c8ab1940903888bd880b97718d7157c7124be087e902f6a" +content-hash = "db6f9c26830d69dd01e4ca4522970fdd0baa9711211ebe1411c617f42bdfbb34" From d3dedc0b6f146f09e0afb41d543a46c98d8c1c3b Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Thu, 14 Nov 2024 08:07:54 +0100 Subject: [PATCH 15/29] =?UTF-8?q?=E2=9C=A8=20Added=20some=20error=20handli?= =?UTF-8?q?ng=20for=20file=20merge.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/download.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index f5b7404..29ca284 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -105,6 +105,7 @@ class Download: path_base: pathlib.Path = path_file.parent result_segments: bool = True dl_segment_results: [DownloadSegmentResult] = [] + result_merge: bool = False # Get urls for media. if isinstance(media, Track): @@ -179,14 +180,16 @@ class Download: if result_segments: # Bring list into right order, so segments can be easily merged. dl_segment_results.sort(key=lambda x: x.id_segment) - reult_merge: bool = self._segments_merge(path_file, dl_segment_results) + result_merge: bool = self._segments_merge(path_file, dl_segment_results) - if reult_merge and isinstance(media, Track) and stream_manifest.is_encrypted: + if not result_merge: + self.fn_logger.error(f"Something went wrong while writing to {media_name}. File is corrupt!") + elif result_merge and isinstance(media, Track) and stream_manifest.is_encrypted: key, nonce = decrypt_security_token(stream_manifest.encryption_key) tmp_path_file_decrypted = path_file.with_suffix("_decrypted") decrypt_file(path_file, tmp_path_file_decrypted, key, nonce) - return result_segments, tmp_path_file_decrypted + return result_merge, tmp_path_file_decrypted def _segments_merge(self, path_file, dl_segment_results) -> bool: result: bool @@ -195,17 +198,16 @@ class Download: try: with path_file.open("wb") as f_target: for dl_segment_result in dl_segment_results: - if dl_segment_result.result: - with dl_segment_result.path_segment.open("rb") as f_segment: - # Read and write junks, which gives better HDD write performance - while segment := f_segment.read(CHUNK_SIZE): - f_target.write(segment) + with dl_segment_result.path_segment.open("rb") as f_segment: + # Read and write junks, which gives better HDD write performance + while segment := f_segment.read(CHUNK_SIZE): + f_target.write(segment) - # Delete segment from HDD - dl_segment_result.path_segment.unlink() + # Delete segment from HDD + dl_segment_result.path_segment.unlink() result = True - except: + except Exception: result = False return result From 94382446121458e8b4dd46ebc7133e902f5f2b32 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Thu, 14 Nov 2024 13:11:06 +0100 Subject: [PATCH 16/29] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Workaround=20for?= =?UTF-8?q?=20wrong=20extension=20detection=20of=20`tidalapi`.=20Fixes=20#?= =?UTF-8?q?232?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/download.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index 29ca284..46c0beb 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -336,9 +336,16 @@ class Download: return False, "" - file_extension = media_stream.get_stream_manifest().file_extension - # Use M4A extension for MP4 audio tracks, because it looks better and is completely interchangeable. - file_extension = AudioExtensions.M4A if file_extension == AudioExtensions.MP4 else file_extension + # TODO: HOTFIX! Go back to + # file_extension = media_stream.get_stream_manifest().file_extension + # After tidalapi has fixed #304 + stream_manifest: StreamManifest = media_stream.get_stream_manifest() + if stream_manifest.file_extension is VideoExtensions.TS: + file_extension = stream_manifest.file_extension + elif AudioExtensions.FLAC in stream_manifest.dash_info.first_url: + file_extension = AudioExtensions.FLAC + else: + file_extension = AudioExtensions.M4A if self.settings.data.extract_flac and ( media_stream.get_stream_manifest().codecs.upper() == Codec.FLAC From 8cb907f908170cf0e46f757c1bd35150e056e36d Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Thu, 14 Nov 2024 13:13:06 +0100 Subject: [PATCH 17/29] =?UTF-8?q?=F0=9F=8C=9F=20Version=20bumped=20to=20v0?= =?UTF-8?q?.20.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 56fd4db..fa62f4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "tidal-dl-ng" authors = ["Robert Honz "] -version = "0.20.1" +version = "0.20.2" description = "TIDAL Medial Downloader Next Generation!" repository = "https://github.com/exislow/tidal-dl-ng" documentation = "https://exislow.github.io/tidal-dl-ng/" From a9ff6dc558637452e635f5ba44f5842715b41b2d Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Thu, 14 Nov 2024 15:10:14 +0100 Subject: [PATCH 18/29] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Fixed=20segment?= =?UTF-8?q?=20id=20calculation=20for=20BTS=20tracks.=20Fixes=20#232=20Fixe?= =?UTF-8?q?d=20GUI=20crash=20on=20trailing=20/=20leading=20spaces=20in=20s?= =?UTF-8?q?earch=20terms.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/download.py | 6 ++++-- tidal_dl_ng/gui.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index 46c0beb..11ee8da 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -218,7 +218,9 @@ class Download: result: bool = False path_segment: pathlib.Path = path_base / url_to_filename(url) # Calculate the segment ID based on the file name within the URL. - id_segment: int = int(str(path_segment.stem).split("_")[-1]) + filename_stem: str = str(path_segment.stem).split("_")[-1] + # CAUTION: This is a workaround, so BTS (LOW quality) track will work. They usually have only ONE link. + id_segment: int = int(filename_stem) if filename_stem.isdecimal() else 0 error: HTTPError | None = None # Retry download on failed segments, with an exponential delay between retries @@ -342,7 +344,7 @@ class Download: stream_manifest: StreamManifest = media_stream.get_stream_manifest() if stream_manifest.file_extension is VideoExtensions.TS: file_extension = stream_manifest.file_extension - elif AudioExtensions.FLAC in stream_manifest.dash_info.first_url: + elif stream_manifest.dash_info and AudioExtensions.FLAC in stream_manifest.dash_info.first_url: file_extension = AudioExtensions.FLAC else: file_extension = AudioExtensions.M4A diff --git a/tidal_dl_ng/gui.py b/tidal_dl_ng/gui.py index 7aa2150..d85c8a0 100644 --- a/tidal_dl_ng/gui.py +++ b/tidal_dl_ng/gui.py @@ -511,6 +511,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self._init_dl() def search(self, query: str, types_media: SearchTypes) -> [ResultItem]: + query = query.strip() + # If a direct link was searched for, skip search and create the object from the link directly. if "http" in query: media_type = get_tidal_media_type(query) From 6f112546a515b67b3964d4164fca703d68e38610 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Thu, 14 Nov 2024 15:10:54 +0100 Subject: [PATCH 19/29] =?UTF-8?q?=F0=9F=8C=9F=20Version=20bumped=20to=20v0?= =?UTF-8?q?.20.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fa62f4c..1c8a102 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "tidal-dl-ng" authors = ["Robert Honz "] -version = "0.20.2" +version = "0.20.3" description = "TIDAL Medial Downloader Next Generation!" repository = "https://github.com/exislow/tidal-dl-ng" documentation = "https://exislow.github.io/tidal-dl-ng/" From f28d07b651a0d69229bfb6f1a1bcd3525ed99933 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Fri, 15 Nov 2024 09:14:00 +0100 Subject: [PATCH 20/29] =?UTF-8?q?=E2=9C=A8=20Todo=20added=20to=20implement?= =?UTF-8?q?=20download=20speed.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/download.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index 11ee8da..bdc2481 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -149,6 +149,7 @@ class Download: ) # Download segments until progress is finished. + # TODO: Compute download speed (https://github.com/Textualize/rich/blob/master/examples/downloader.py) while not self.progress.tasks[p_task].finished: with futures.ThreadPoolExecutor( max_workers=self.settings.data.downloads_simultaneous_per_track_max From 037e6f14a20dc32055755382592e47bde94f073f Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Fri, 15 Nov 2024 09:53:21 +0100 Subject: [PATCH 21/29] =?UTF-8?q?=E2=9C=A8=20Finished=20items=20will=20be?= =?UTF-8?q?=20written=20to=20log=20output.=20Minor=20refactoring.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/download.py | 27 ++++++++++++--------------- tidal_dl_ng/helper/tidal.py | 6 ++++-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index bdc2481..735ea51 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -244,10 +244,8 @@ class Download: self.progress.advance(p_task) result = True - except Exception as e: - error = e + except Exception: self.progress.advance(p_task) - self.fn_logger.exception(e) # To send the progress to the GUI, we need to emit the percentage. if not progress_to_stdout: @@ -397,9 +395,13 @@ class Download: self._move_lyrics(tmp_path_lyrics, path_media_dst) # Move cover file + # TODO: Cover is downloaded with every track of the album. Needs refactoring, so cover is only + # dowloaded for an album once. if self.settings.data.cover_album_file and tmp_path_cover: self._move_cover(tmp_path_cover, path_media_dst) + self.fn_logger.info(f"Downloaded item '{name_builder_item(media)}'.") + # Move final file to the configured destination directory. shutil.move(tmp_path_file, path_media_dst) @@ -590,29 +592,22 @@ class Download: file_name_relative = format_path_media(file_template, media) # Get the name of the list and check, if videos should be included. - videos_include: bool = True - - if isinstance(media, Mix): - list_media_name = media.title[:30] - elif video_download: - list_media_name = name_builder_title(media)[:30] - else: - videos_include = False - list_media_name = name_builder_title(media)[:30] + list_media_name: str = name_builder_title(media) + list_media_name_short: str = list_media_name[:30] # Get all items of the list. - items = items_results_all(media, videos_include=videos_include) + items = items_results_all(media, videos_include=video_download) # Determine where to redirect the progress information. if self.progress_gui is None: progress_stdout: bool = True else: progress_stdout: bool = False - self.progress_gui.list_name.emit(list_media_name[:30]) + self.progress_gui.list_name.emit(list_media_name_short[:30]) # Create the list progress task. p_task1: TaskID = self.progress.add_task( - f"[green]List '{list_media_name}'", total=len(items), visible=progress_stdout + f"[green]List '{list_media_name_short}'", total=len(items), visible=progress_stdout ) is_album: bool = isinstance(media, Album) @@ -636,6 +631,8 @@ class Download: if not progress_stdout: self.progress_gui.list_item.emit(self.progress.tasks[p_task1].percentage) + self.fn_logger.info(f"Finished list '{list_media_name}'.") + def _video_convert(self, path_file: pathlib.Path) -> pathlib.Path: path_file_out: pathlib.Path = path_file.with_suffix(AudioExtensions.MP4) result, _ = ( diff --git a/tidal_dl_ng/helper/tidal.py b/tidal_dl_ng/helper/tidal.py index ebea4cd..e568cc6 100644 --- a/tidal_dl_ng/helper/tidal.py +++ b/tidal_dl_ng/helper/tidal.py @@ -25,8 +25,10 @@ def name_builder_album_artist(media: Track | Album) -> str: return ", ".join(artists_tmp) -def name_builder_title(media: Track | Video) -> str: - result: str = media.full_name if hasattr(media, "full_name") else media.name +def name_builder_title(media: Track | Video | Mix | Playlist | Album | Video) -> str: + result: str = ( + media.title if isinstance(media, Mix) else media.full_name if hasattr(media, "full_name") else media.name + ) return result From fba23aee41bd11c0a25a9351d970d69270bc02df Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Fri, 15 Nov 2024 10:01:17 +0100 Subject: [PATCH 22/29] =?UTF-8?q?=E2=9C=A8=20Default=20formats=20changed.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/model/cfg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tidal_dl_ng/model/cfg.py b/tidal_dl_ng/model/cfg.py index 631a2f2..7e0d079 100644 --- a/tidal_dl_ng/model/cfg.py +++ b/tidal_dl_ng/model/cfg.py @@ -25,10 +25,10 @@ class Settings: quality_video: QualityVideo = QualityVideo.P480 format_album: str = ( "Albums/{album_artist} - {album_title}{album_explicit}/{track_volume_num_optional}" - "{album_track_num}. {artist_name} - {track_title}" + "{album_track_num}. {artist_name} - {track_title}{track_explicit}" ) - format_playlist: str = "Playlists/{playlist_name}/{artist_name} - {track_title}" - format_mix: str = "Mix/{mix_name}/{artist_name} - {track_title}" + format_playlist: str = "Playlists/{playlist_name}/{artist_name} - {track_title}{track_explicit}" + format_mix: str = "Mix/{mix_name}/{artist_name} - {track_title}{track_explicit}" format_track: str = "Tracks/{artist_name} - {track_title}{track_explicit}" format_video: str = "Videos/{artist_name} - {track_title}{track_explicit}" video_convert_mp4: bool = True From 3c776c132678c8af920669b3c62d3126840047c2 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Sun, 17 Nov 2024 12:05:44 +0100 Subject: [PATCH 23/29] =?UTF-8?q?=E2=9C=A8=20Downgraded=20PySide6=20to=20f?= =?UTF-8?q?ormer=20version=206.7.0=20to=20support=20Win7=20again.=20Fixes?= =?UTF-8?q?=20#236?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 128 ++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9a5c670..0b0837e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -367,73 +367,73 @@ cron = ["capturer (>=2.4)"] [[package]] name = "coverage" -version = "7.6.4" +version = "7.6.7" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, - {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, - {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, - {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, - {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, - {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, - {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, - {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, - {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, - {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, - {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, - {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, - {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, - {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, - {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, - {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, - {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, - {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, - {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, - {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, - {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, - {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, - {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, - {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, - {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, - {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, - {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, - {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, - {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, - {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, - {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, - {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, - {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, - {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, - {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, - {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, - {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, - {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, - {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, - {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, - {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, - {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, - {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, - {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, - {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, - {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, - {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, - {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, - {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, - {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, - {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"}, - {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"}, - {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"}, - {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"}, - {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"}, - {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"}, - {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"}, - {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"}, - {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"}, - {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"}, - {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, - {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, + {file = "coverage-7.6.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:108bb458827765d538abcbf8288599fee07d2743357bdd9b9dad456c287e121e"}, + {file = "coverage-7.6.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c973b2fe4dc445cb865ab369df7521df9c27bf40715c837a113edaa2aa9faf45"}, + {file = "coverage-7.6.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c6b24007c4bcd0b19fac25763a7cac5035c735ae017e9a349b927cfc88f31c1"}, + {file = "coverage-7.6.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acbb8af78f8f91b3b51f58f288c0994ba63c646bc1a8a22ad072e4e7e0a49f1c"}, + {file = "coverage-7.6.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad32a981bcdedb8d2ace03b05e4fd8dace8901eec64a532b00b15217d3677dd2"}, + {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:34d23e28ccb26236718a3a78ba72744212aa383141961dd6825f6595005c8b06"}, + {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e25bacb53a8c7325e34d45dddd2f2fbae0dbc230d0e2642e264a64e17322a777"}, + {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af05bbba896c4472a29408455fe31b3797b4d8648ed0a2ccac03e074a77e2314"}, + {file = "coverage-7.6.7-cp310-cp310-win32.whl", hash = "sha256:796c9b107d11d2d69e1849b2dfe41730134b526a49d3acb98ca02f4985eeff7a"}, + {file = "coverage-7.6.7-cp310-cp310-win_amd64.whl", hash = "sha256:987a8e3da7da4eed10a20491cf790589a8e5e07656b6dc22d3814c4d88faf163"}, + {file = "coverage-7.6.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e61b0e77ff4dddebb35a0e8bb5a68bf0f8b872407d8d9f0c726b65dfabe2469"}, + {file = "coverage-7.6.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a5407a75ca4abc20d6252efeb238377a71ce7bda849c26c7a9bece8680a5d99"}, + {file = "coverage-7.6.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df002e59f2d29e889c37abd0b9ee0d0e6e38c24f5f55d71ff0e09e3412a340ec"}, + {file = "coverage-7.6.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:673184b3156cba06154825f25af33baa2671ddae6343f23175764e65a8c4c30b"}, + {file = "coverage-7.6.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e69ad502f1a2243f739f5bd60565d14a278be58be4c137d90799f2c263e7049a"}, + {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60dcf7605c50ea72a14490d0756daffef77a5be15ed1b9fea468b1c7bda1bc3b"}, + {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9c2eb378bebb2c8f65befcb5147877fc1c9fbc640fc0aad3add759b5df79d55d"}, + {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c0317288f032221d35fa4cbc35d9f4923ff0dfd176c79c9b356e8ef8ef2dff4"}, + {file = "coverage-7.6.7-cp311-cp311-win32.whl", hash = "sha256:951aade8297358f3618a6e0660dc74f6b52233c42089d28525749fc8267dccd2"}, + {file = "coverage-7.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:5e444b8e88339a2a67ce07d41faabb1d60d1004820cee5a2c2b54e2d8e429a0f"}, + {file = "coverage-7.6.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f07ff574986bc3edb80e2c36391678a271d555f91fd1d332a1e0f4b5ea4b6ea9"}, + {file = "coverage-7.6.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:49ed5ee4109258973630c1f9d099c7e72c5c36605029f3a91fe9982c6076c82b"}, + {file = "coverage-7.6.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3e8796434a8106b3ac025fd15417315d7a58ee3e600ad4dbcfddc3f4b14342c"}, + {file = "coverage-7.6.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3b925300484a3294d1c70f6b2b810d6526f2929de954e5b6be2bf8caa1f12c1"}, + {file = "coverage-7.6.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c42ec2c522e3ddd683dec5cdce8e62817afb648caedad9da725001fa530d354"}, + {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0266b62cbea568bd5e93a4da364d05de422110cbed5056d69339bd5af5685433"}, + {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e5f2a0f161d126ccc7038f1f3029184dbdf8f018230af17ef6fd6a707a5b881f"}, + {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c132b5a22821f9b143f87446805e13580b67c670a548b96da945a8f6b4f2efbb"}, + {file = "coverage-7.6.7-cp312-cp312-win32.whl", hash = "sha256:7c07de0d2a110f02af30883cd7dddbe704887617d5c27cf373362667445a4c76"}, + {file = "coverage-7.6.7-cp312-cp312-win_amd64.whl", hash = "sha256:fd49c01e5057a451c30c9b892948976f5d38f2cbd04dc556a82743ba8e27ed8c"}, + {file = "coverage-7.6.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3"}, + {file = "coverage-7.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab"}, + {file = "coverage-7.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808"}, + {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc"}, + {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8"}, + {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a"}, + {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55"}, + {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384"}, + {file = "coverage-7.6.7-cp313-cp313-win32.whl", hash = "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30"}, + {file = "coverage-7.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42"}, + {file = "coverage-7.6.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413"}, + {file = "coverage-7.6.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd"}, + {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37"}, + {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b"}, + {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d"}, + {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529"}, + {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b"}, + {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3"}, + {file = "coverage-7.6.7-cp313-cp313t-win32.whl", hash = "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8"}, + {file = "coverage-7.6.7-cp313-cp313t-win_amd64.whl", hash = "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56"}, + {file = "coverage-7.6.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37a15573f988b67f7348916077c6d8ad43adb75e478d0910957394df397d2874"}, + {file = "coverage-7.6.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b6cce5c76985f81da3769c52203ee94722cd5d5889731cd70d31fee939b74bf0"}, + {file = "coverage-7.6.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ab9763d291a17b527ac6fd11d1a9a9c358280adb320e9c2672a97af346ac2c"}, + {file = "coverage-7.6.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cf96ceaa275f071f1bea3067f8fd43bec184a25a962c754024c973af871e1b7"}, + {file = "coverage-7.6.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aee9cf6b0134d6f932d219ce253ef0e624f4fa588ee64830fcba193269e4daa3"}, + {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2bc3e45c16564cc72de09e37413262b9f99167803e5e48c6156bccdfb22c8327"}, + {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:623e6965dcf4e28a3debaa6fcf4b99ee06d27218f46d43befe4db1c70841551c"}, + {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:850cfd2d6fc26f8346f422920ac204e1d28814e32e3a58c19c91980fa74d8289"}, + {file = "coverage-7.6.7-cp39-cp39-win32.whl", hash = "sha256:c296263093f099da4f51b3dff1eff5d4959b527d4f2f419e16508c5da9e15e8c"}, + {file = "coverage-7.6.7-cp39-cp39-win_amd64.whl", hash = "sha256:90746521206c88bdb305a4bf3342b1b7316ab80f804d40c536fc7d329301ee13"}, + {file = "coverage-7.6.7-pp39.pp310-none-any.whl", hash = "sha256:0ddcb70b3a3a57581b450571b31cb774f23eb9519c2aaa6176d3a84c9fc57671"}, + {file = "coverage-7.6.7.tar.gz", hash = "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24"}, ] [package.dependencies] @@ -2237,4 +2237,4 @@ gui = ["pyqtdarktheme-fork", "pyside6"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "db6f9c26830d69dd01e4ca4522970fdd0baa9711211ebe1411c617f42bdfbb34" +content-hash = "2761e7f9379c3e4444fdfcf9b01a0d86fb56cb1e1ec5c5a63cf32e5331884726" diff --git a/pyproject.toml b/pyproject.toml index 1c8a102..e1de1bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dataclasses-json = "^0.6.7" pathvalidate = "^3.2.1" m3u8 = "^6.0.0" coloredlogs = "^15.0.1" -pyside6 = {version = "^6.8.0.2", optional = true} +pyside6 = {version = "^6.7.0", optional = true} pyqtdarktheme-fork = {version = "^2.3.2", optional = true} mpegdash = "^0.4.0" rich = "^13.9.4" From 86829bce0497d8ea71a7fa500055e097b53a8c0e Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Sun, 17 Nov 2024 22:57:37 +0100 Subject: [PATCH 24/29] =?UTF-8?q?=E2=9C=A8=20Added=20ID3=20tags=20overview?= =?UTF-8?q?=20as=20comment.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/metadata.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tidal_dl_ng/metadata.py b/tidal_dl_ng/metadata.py index d9ecb8c..e1fb44b 100644 --- a/tidal_dl_ng/metadata.py +++ b/tidal_dl_ng/metadata.py @@ -111,6 +111,8 @@ class Metadata: self.m.tags["LYRICS"] = self.lyrics def set_mp3(self): + # ID3 Frame (tags) overview: https://exiftool.org/TagNames/ID3.html / https://id3.org/id3v2.3.0 + # Mapping overview: https://docs.mp3tag.de/mapping/ self.m.tags.add(TIT2(encoding=3, text=self.title)) self.m.tags.add(TALB(encoding=3, text=self.album)) self.m.tags.add(TOPE(encoding=3, text=self.albumartist)) From d4cc677bd96e477fba4cbc4384331030f6d04519 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Sun, 17 Nov 2024 23:04:50 +0100 Subject: [PATCH 25/29] =?UTF-8?q?=E2=9C=A8=20Fixed=20default=20format=20st?= =?UTF-8?q?rings=20explicit=20tag.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/model/cfg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tidal_dl_ng/model/cfg.py b/tidal_dl_ng/model/cfg.py index 7e0d079..1cd4b5f 100644 --- a/tidal_dl_ng/model/cfg.py +++ b/tidal_dl_ng/model/cfg.py @@ -25,10 +25,10 @@ class Settings: quality_video: QualityVideo = QualityVideo.P480 format_album: str = ( "Albums/{album_artist} - {album_title}{album_explicit}/{track_volume_num_optional}" - "{album_track_num}. {artist_name} - {track_title}{track_explicit}" + "{album_track_num}. {artist_name} - {track_title}{album_explicit}" ) - format_playlist: str = "Playlists/{playlist_name}/{artist_name} - {track_title}{track_explicit}" - format_mix: str = "Mix/{mix_name}/{artist_name} - {track_title}{track_explicit}" + format_playlist: str = "Playlists/{playlist_name}/{artist_name} - {track_title}" + format_mix: str = "Mix/{mix_name}/{artist_name} - {track_title}" format_track: str = "Tracks/{artist_name} - {track_title}{track_explicit}" format_video: str = "Videos/{artist_name} - {track_title}{track_explicit}" video_convert_mp4: bool = True From 56f4522f2b3fd8558e11a97bef5e9893c7e1b3ea Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Sun, 17 Nov 2024 23:06:36 +0100 Subject: [PATCH 26/29] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Fixed=20`{album?= =?UTF-8?q?=5Fartist}`=20tag=20replacement=20on=20track=20level.=20Fixes?= =?UTF-8?q?=20#239?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/helper/path.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tidal_dl_ng/helper/path.py b/tidal_dl_ng/helper/path.py index 627f7c1..fb7bb87 100644 --- a/tidal_dl_ng/helper/path.py +++ b/tidal_dl_ng/helper/path.py @@ -79,8 +79,7 @@ def format_str_media(name: str, media: Track | Album | Playlist | UserPlaylist | elif hasattr(media, "artist"): result = media.artist.name case "album_artist": - if isinstance(media, Album): - result = name_builder_album_artist(media) + result = name_builder_album_artist(media) case "track_title": if isinstance(media, Track | Video): result = name_builder_title(media) From 9fa01271163625968b375ea667d348bb0e492dd2 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 20 Nov 2024 12:28:18 +0100 Subject: [PATCH 27/29] =?UTF-8?q?=F0=9F=91=B7=20Refactored=20download=20de?= =?UTF-8?q?lay=20limits=20to=20config.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/model/cfg.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tidal_dl_ng/model/cfg.py b/tidal_dl_ng/model/cfg.py index 1cd4b5f..06614f4 100644 --- a/tidal_dl_ng/model/cfg.py +++ b/tidal_dl_ng/model/cfg.py @@ -38,6 +38,8 @@ class Settings: cover_album_file: bool = True extract_flac: bool = True downloads_simultaneous_per_track_max: int = 20 + download_delay_sec_min: float = 3.0 + download_delay_sec_max: float = 5.0 @dataclass_json @@ -80,6 +82,8 @@ class HelpSettings: cover_album_file: str = "Save cover to 'cover.jpg', if an album is downloaded." extract_flac: str = "Extract FLAC audio tracks from MP4 containers and save them as `*.flac` (uses FFmpeg)." downloads_simultaneous_per_track_max: str = "Maximum number of simultaneous chunk downloads per track." + download_delay_sec_min: float = "Lower boundary for the calculation of the download delay in seconds." + download_delay_sec_max: float = "Upper boundary for the calculation of the download delay in seconds." @dataclass_json From dda52eb05e6beae476fff9c15208677bd0a07629 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Wed, 20 Nov 2024 12:30:14 +0100 Subject: [PATCH 28/29] =?UTF-8?q?=F0=9F=91=B7=20Stream=20manifest=20will?= =?UTF-8?q?=20only=20be=20retrieved=20once=20per=20media=20item.=20Downloa?= =?UTF-8?q?d=20delay=20will=20be=20read=20from=20config.=20Exception=20han?= =?UTF-8?q?dling=20improved.=20Fixes=20#248?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tidal_dl_ng/download.py | 69 +++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py index 735ea51..c755411 100644 --- a/tidal_dl_ng/download.py +++ b/tidal_dl_ng/download.py @@ -98,6 +98,7 @@ class Download: def _download( self, media: Track | Video, + stream_manifest: StreamManifest, path_file: pathlib.Path, ) -> (bool, pathlib.Path): media_name: str = name_builder_item(media) @@ -108,15 +109,17 @@ class Download: result_merge: bool = False # Get urls for media. - if isinstance(media, Track): - urls = media.get_stream().get_stream_manifest().get_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 + try: + if isinstance(media, Track): + urls = stream_manifest.get_urls() + 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 + except Exception: + return False, path_file # Set the correct progress output channel. if self.progress_gui is None: @@ -187,7 +190,7 @@ class Download: self.fn_logger.error(f"Something went wrong while writing to {media_name}. File is corrupt!") elif result_merge and isinstance(media, Track) and stream_manifest.is_encrypted: key, nonce = decrypt_security_token(stream_manifest.encryption_key) - tmp_path_file_decrypted = path_file.with_suffix("_decrypted") + tmp_path_file_decrypted = path_file.with_suffix(".decrypted") decrypt_file(path_file, tmp_path_file_decrypted, key, nonce) return result_merge, tmp_path_file_decrypted @@ -322,25 +325,25 @@ class Download: self.adjust_quality_video(quality_video) if quality_video else quality_video ) + try: + media_stream = media.get_stream() + stream_manifest: StreamManifest = media_stream.get_stream_manifest() + except TooManyRequests: + self.fn_logger.exception( + f"Too many requests against TIDAL backend. Skipping '{name_builder_item(media)}'. " + f"Consider to activate delay between downloads." + ) + + return False, "" + except Exception: + self.fn_logger.exception(f"Something went wrong. Skipping '{name_builder_item(media)}'.") + + return False, "" + if isinstance(media, Track): - try: - media_stream = media.get_stream() - except TooManyRequests: - self.fn_logger.exception( - f"Too many requests against TIDAL backend. Skipping '{name_builder_item(media)}'. " - f"Consider to activate delay between downloads." - ) - - return False, "" - except: - self.fn_logger.exception(f"Something went wrong. Skipping '{name_builder_item(media)}'.") - - return False, "" - # TODO: HOTFIX! Go back to # file_extension = media_stream.get_stream_manifest().file_extension # After tidalapi has fixed #304 - stream_manifest: StreamManifest = media_stream.get_stream_manifest() if stream_manifest.file_extension is VideoExtensions.TS: file_extension = stream_manifest.file_extension elif stream_manifest.dash_info and AudioExtensions.FLAC in stream_manifest.dash_info.first_url: @@ -349,8 +352,7 @@ class Download: file_extension = AudioExtensions.M4A if self.settings.data.extract_flac and ( - media_stream.get_stream_manifest().codecs.upper() == Codec.FLAC - and file_extension != AudioExtensions.FLAC + stream_manifest.codecs.upper() == Codec.FLAC and file_extension != AudioExtensions.FLAC ): file_extension = AudioExtensions.FLAC do_flac_extract = True @@ -369,7 +371,9 @@ class Download: tmp_path_file.touch() # Download media. - result_download, tmp_path_file = self._download(media=media, path_file=tmp_path_file) + result_download, tmp_path_file = self._download( + media=media, stream_manifest=stream_manifest, path_file=tmp_path_file + ) if result_download: # Convert video from TS to MP4 @@ -396,7 +400,7 @@ class Download: # Move cover file # TODO: Cover is downloaded with every track of the album. Needs refactoring, so cover is only - # dowloaded for an album once. + # downloaded for an album once. if self.settings.data.cover_album_file and tmp_path_cover: self._move_cover(tmp_path_cover, path_media_dst) @@ -420,7 +424,12 @@ class Download: # Whether a file was downloaded or skipped and the download delay is enabled, wait until the next download. # Only use this, if you have a list of several Track items. if download_delay and not skip_file: - time_sleep: float = round(random.SystemRandom().uniform(1.5, 4), 1) + time_sleep: float = round( + random.SystemRandom().uniform( + self.settings.data.download_delay_sec_min, self.settings.data.download_delay_sec_max + ), + 1, + ) self.fn_logger.debug(f"Next download will start in {time_sleep} seconds.") time.sleep(time_sleep) From ba3a1b65c701e35537c87340da216fddfcc70425 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Fri, 29 Nov 2024 17:33:26 +0100 Subject: [PATCH 29/29] =?UTF-8?q?=E2=9C=A8=20Dependency=20updates.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 220 ++++++++++++++++++++++++++++--------------------- pyproject.toml | 4 +- 2 files changed, 127 insertions(+), 97 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0b0837e..b6302bf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -97,13 +97,13 @@ files = [ [[package]] name = "bandit" -version = "1.7.10" +version = "1.8.0" description = "Security oriented static analyser for python code." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "bandit-1.7.10-py3-none-any.whl", hash = "sha256:665721d7bebbb4485a339c55161ac0eedde27d51e638000d91c8c2d68343ad02"}, - {file = "bandit-1.7.10.tar.gz", hash = "sha256:59ed5caf5d92b6ada4bf65bc6437feea4a9da1093384445fed4d472acc6cff7b"}, + {file = "bandit-1.8.0-py3-none-any.whl", hash = "sha256:b1a61d829c0968aed625381e426aa378904b996529d048f8d908fa28f6b13e38"}, + {file = "bandit-1.8.0.tar.gz", hash = "sha256:b5bfe55a095abd9fe20099178a7c6c060f844bfd4fe4c76d28e35e4c52b9d31e"}, ] [package.dependencies] @@ -367,73 +367,73 @@ cron = ["capturer (>=2.4)"] [[package]] name = "coverage" -version = "7.6.7" +version = "7.6.8" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:108bb458827765d538abcbf8288599fee07d2743357bdd9b9dad456c287e121e"}, - {file = "coverage-7.6.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c973b2fe4dc445cb865ab369df7521df9c27bf40715c837a113edaa2aa9faf45"}, - {file = "coverage-7.6.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c6b24007c4bcd0b19fac25763a7cac5035c735ae017e9a349b927cfc88f31c1"}, - {file = "coverage-7.6.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acbb8af78f8f91b3b51f58f288c0994ba63c646bc1a8a22ad072e4e7e0a49f1c"}, - {file = "coverage-7.6.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad32a981bcdedb8d2ace03b05e4fd8dace8901eec64a532b00b15217d3677dd2"}, - {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:34d23e28ccb26236718a3a78ba72744212aa383141961dd6825f6595005c8b06"}, - {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e25bacb53a8c7325e34d45dddd2f2fbae0dbc230d0e2642e264a64e17322a777"}, - {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af05bbba896c4472a29408455fe31b3797b4d8648ed0a2ccac03e074a77e2314"}, - {file = "coverage-7.6.7-cp310-cp310-win32.whl", hash = "sha256:796c9b107d11d2d69e1849b2dfe41730134b526a49d3acb98ca02f4985eeff7a"}, - {file = "coverage-7.6.7-cp310-cp310-win_amd64.whl", hash = "sha256:987a8e3da7da4eed10a20491cf790589a8e5e07656b6dc22d3814c4d88faf163"}, - {file = "coverage-7.6.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e61b0e77ff4dddebb35a0e8bb5a68bf0f8b872407d8d9f0c726b65dfabe2469"}, - {file = "coverage-7.6.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a5407a75ca4abc20d6252efeb238377a71ce7bda849c26c7a9bece8680a5d99"}, - {file = "coverage-7.6.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df002e59f2d29e889c37abd0b9ee0d0e6e38c24f5f55d71ff0e09e3412a340ec"}, - {file = "coverage-7.6.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:673184b3156cba06154825f25af33baa2671ddae6343f23175764e65a8c4c30b"}, - {file = "coverage-7.6.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e69ad502f1a2243f739f5bd60565d14a278be58be4c137d90799f2c263e7049a"}, - {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60dcf7605c50ea72a14490d0756daffef77a5be15ed1b9fea468b1c7bda1bc3b"}, - {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9c2eb378bebb2c8f65befcb5147877fc1c9fbc640fc0aad3add759b5df79d55d"}, - {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c0317288f032221d35fa4cbc35d9f4923ff0dfd176c79c9b356e8ef8ef2dff4"}, - {file = "coverage-7.6.7-cp311-cp311-win32.whl", hash = "sha256:951aade8297358f3618a6e0660dc74f6b52233c42089d28525749fc8267dccd2"}, - {file = "coverage-7.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:5e444b8e88339a2a67ce07d41faabb1d60d1004820cee5a2c2b54e2d8e429a0f"}, - {file = "coverage-7.6.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f07ff574986bc3edb80e2c36391678a271d555f91fd1d332a1e0f4b5ea4b6ea9"}, - {file = "coverage-7.6.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:49ed5ee4109258973630c1f9d099c7e72c5c36605029f3a91fe9982c6076c82b"}, - {file = "coverage-7.6.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3e8796434a8106b3ac025fd15417315d7a58ee3e600ad4dbcfddc3f4b14342c"}, - {file = "coverage-7.6.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3b925300484a3294d1c70f6b2b810d6526f2929de954e5b6be2bf8caa1f12c1"}, - {file = "coverage-7.6.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c42ec2c522e3ddd683dec5cdce8e62817afb648caedad9da725001fa530d354"}, - {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0266b62cbea568bd5e93a4da364d05de422110cbed5056d69339bd5af5685433"}, - {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e5f2a0f161d126ccc7038f1f3029184dbdf8f018230af17ef6fd6a707a5b881f"}, - {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c132b5a22821f9b143f87446805e13580b67c670a548b96da945a8f6b4f2efbb"}, - {file = "coverage-7.6.7-cp312-cp312-win32.whl", hash = "sha256:7c07de0d2a110f02af30883cd7dddbe704887617d5c27cf373362667445a4c76"}, - {file = "coverage-7.6.7-cp312-cp312-win_amd64.whl", hash = "sha256:fd49c01e5057a451c30c9b892948976f5d38f2cbd04dc556a82743ba8e27ed8c"}, - {file = "coverage-7.6.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3"}, - {file = "coverage-7.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384"}, - {file = "coverage-7.6.7-cp313-cp313-win32.whl", hash = "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30"}, - {file = "coverage-7.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42"}, - {file = "coverage-7.6.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413"}, - {file = "coverage-7.6.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3"}, - {file = "coverage-7.6.7-cp313-cp313t-win32.whl", hash = "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8"}, - {file = "coverage-7.6.7-cp313-cp313t-win_amd64.whl", hash = "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56"}, - {file = "coverage-7.6.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37a15573f988b67f7348916077c6d8ad43adb75e478d0910957394df397d2874"}, - {file = "coverage-7.6.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b6cce5c76985f81da3769c52203ee94722cd5d5889731cd70d31fee939b74bf0"}, - {file = "coverage-7.6.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ab9763d291a17b527ac6fd11d1a9a9c358280adb320e9c2672a97af346ac2c"}, - {file = "coverage-7.6.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cf96ceaa275f071f1bea3067f8fd43bec184a25a962c754024c973af871e1b7"}, - {file = "coverage-7.6.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aee9cf6b0134d6f932d219ce253ef0e624f4fa588ee64830fcba193269e4daa3"}, - {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2bc3e45c16564cc72de09e37413262b9f99167803e5e48c6156bccdfb22c8327"}, - {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:623e6965dcf4e28a3debaa6fcf4b99ee06d27218f46d43befe4db1c70841551c"}, - {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:850cfd2d6fc26f8346f422920ac204e1d28814e32e3a58c19c91980fa74d8289"}, - {file = "coverage-7.6.7-cp39-cp39-win32.whl", hash = "sha256:c296263093f099da4f51b3dff1eff5d4959b527d4f2f419e16508c5da9e15e8c"}, - {file = "coverage-7.6.7-cp39-cp39-win_amd64.whl", hash = "sha256:90746521206c88bdb305a4bf3342b1b7316ab80f804d40c536fc7d329301ee13"}, - {file = "coverage-7.6.7-pp39.pp310-none-any.whl", hash = "sha256:0ddcb70b3a3a57581b450571b31cb774f23eb9519c2aaa6176d3a84c9fc57671"}, - {file = "coverage-7.6.7.tar.gz", hash = "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24"}, + {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, + {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, + {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, + {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, + {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, + {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, + {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, + {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, + {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, + {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, + {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, + {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, + {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, + {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, + {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, + {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, ] [package.dependencies] @@ -636,13 +636,13 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "identify" -version = "2.6.2" +version = "2.6.3" description = "File identification library for Python" optional = false python-versions = ">=3.9" files = [ - {file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"}, - {file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"}, + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, ] [package.extras] @@ -971,13 +971,13 @@ pyyaml = ">=5.1" [[package]] name = "mkdocs-material" -version = "9.5.44" +version = "9.5.46" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca"}, - {file = "mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0"}, + {file = "mkdocs_material-9.5.46-py3-none-any.whl", hash = "sha256:98f0a2039c62e551a68aad0791a8d41324ff90c03a6e6cea381a384b84908b83"}, + {file = "mkdocs_material-9.5.46.tar.gz", hash = "sha256:ae2043f4238e572f9a40e0b577f50400d6fc31e2fef8ea141800aebf3bd273d7"}, ] [package.dependencies] @@ -1941,23 +1941,23 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "75.5.0" +version = "75.6.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" files = [ - {file = "setuptools-75.5.0-py3-none-any.whl", hash = "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829"}, - {file = "setuptools-75.5.0.tar.gz", hash = "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef"}, + {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, + {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] -core = ["importlib-metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] [[package]] name = "shellingham" @@ -2007,13 +2007,13 @@ files = [ [[package]] name = "stevedore" -version = "5.3.0" +version = "5.4.0" description = "Manage dynamic plugins for Python applications" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"}, - {file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"}, + {file = "stevedore-5.4.0-py3-none-any.whl", hash = "sha256:b0be3c4748b3ea7b854b265dcb4caa891015e442416422be16f8b31756107857"}, + {file = "stevedore-5.4.0.tar.gz", hash = "sha256:79e92235ecb828fe952b6b8b0c6c87863248631922c8e8e0fa5b17b232c4514d"}, ] [package.dependencies] @@ -2021,13 +2021,13 @@ pbr = ">=2.0.0" [[package]] name = "tidalapi" -version = "0.8.1" +version = "0.8.2" description = "Unofficial API for TIDAL music streaming service." optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "tidalapi-0.8.1-py3-none-any.whl", hash = "sha256:933ee71bc081d6538460d762ce70d0490de52ce027398112218037f7bf2f1cf2"}, - {file = "tidalapi-0.8.1.tar.gz", hash = "sha256:eb48e42600533513161fd9fc9b1ee06c9788dbe1e111bb8e91b9aa9603ab3a6b"}, + {file = "tidalapi-0.8.2-py3-none-any.whl", hash = "sha256:46998ecb9103aee6d1346ad2974df05fe9bc03a429a89d5a701571fe4d9c4525"}, + {file = "tidalapi-0.8.2.tar.gz", hash = "sha256:9eab07291f92f506694b6d326dd1e6651349eaa71597cef634a62915312db38c"}, ] [package.dependencies] @@ -2062,13 +2062,43 @@ files = [ [[package]] name = "tomli" -version = "2.1.0" +version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, - {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -2111,13 +2141,13 @@ test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"] [[package]] name = "typer" -version = "0.13.0" +version = "0.14.0" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" files = [ - {file = "typer-0.13.0-py3-none-any.whl", hash = "sha256:d85fe0b777b2517cc99c8055ed735452f2659cd45e451507c76f48ce5c1d00e2"}, - {file = "typer-0.13.0.tar.gz", hash = "sha256:f1c7198347939361eec90139ffa0fd8b3df3a2259d5852a0f7400e476d95985c"}, + {file = "typer-0.14.0-py3-none-any.whl", hash = "sha256:f476233a25770ab3e7b2eebf7c68f3bc702031681a008b20167573a4b7018f09"}, + {file = "typer-0.14.0.tar.gz", hash = "sha256:af58f737f8d0c0c37b9f955a6d39000b9ff97813afcbeef56af5e37cf743b45a"}, ] [package.dependencies] @@ -2171,13 +2201,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.27.1" +version = "20.28.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, - {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, + {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, + {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, ] [package.dependencies] @@ -2237,4 +2267,4 @@ gui = ["pyqtdarktheme-fork", "pyside6"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "2761e7f9379c3e4444fdfcf9b01a0d86fb56cb1e1ec5c5a63cf32e5331884726" +content-hash = "5d06aa39f3567f397125c1d081fbde9f3ca9eb7f454385cd02bdaeac3dbfd8e1" diff --git a/pyproject.toml b/pyproject.toml index e1de1bd..23ae7ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,9 +38,9 @@ pyqtdarktheme-fork = {version = "^2.3.2", optional = true} mpegdash = "^0.4.0" rich = "^13.9.4" toml = "^0.10.2" -typer = "^0.13.0" +typer = "^0.14.0" ffmpeg-python = "^0.2.0" -tidalapi = "^0.8.1" +tidalapi = "^0.8.2" [tool.poetry.extras] gui = ["pyside6", "pyqtdarktheme-fork"]