diff options
| author | Robert Honz <exislow+github@gmail.com> | 2023-12-19 23:58:57 +0100 |
|---|---|---|
| committer | Robert Honz <exislow+github@gmail.com> | 2023-12-19 23:58:57 +0100 |
| commit | 0d4a9d6b8740035346d098f5bc28ebca64f6cd4d (patch) | |
| tree | f52c9569509565e581b03b6e60fe32e611b79ad2 /tidal_dl_ng/helper | |
| parent | 82a8fe96aa08903919a90a76030bacba9f93b8e0 (diff) | |
| download | tidal-dl-ng-0d4a9d6b8740035346d098f5bc28ebca64f6cd4d.tar.gz | |
🌟 Re-add all project files.
Diffstat (limited to 'tidal_dl_ng/helper')
| -rw-r--r-- | tidal_dl_ng/helper/__init__.py | 0 | ||||
| -rw-r--r-- | tidal_dl_ng/helper/decorator.py | 19 | ||||
| -rw-r--r-- | tidal_dl_ng/helper/decryption.py | 53 | ||||
| -rw-r--r-- | tidal_dl_ng/helper/exceptions.py | 6 | ||||
| -rw-r--r-- | tidal_dl_ng/helper/path.py | 177 | ||||
| -rw-r--r-- | tidal_dl_ng/helper/url.py | 15 | ||||
| -rw-r--r-- | tidal_dl_ng/helper/wrapper.py | 23 |
7 files changed, 293 insertions, 0 deletions
diff --git a/tidal_dl_ng/helper/__init__.py b/tidal_dl_ng/helper/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tidal_dl_ng/helper/__init__.py diff --git a/tidal_dl_ng/helper/decorator.py b/tidal_dl_ng/helper/decorator.py new file mode 100644 index 0000000..545f73b --- /dev/null +++ b/tidal_dl_ng/helper/decorator.py @@ -0,0 +1,19 @@ +class SingletonMeta(type): + """ + The Singleton class can be implemented in different ways in Python. Some + possible methods include: base class, decorator, metaclass. We will use the + metaclass because it is best suited for this purpose. + """ + + _instances = {} + + def __call__(cls, *args, **kwargs): + """ + Possible changes to the value of the `__init__` argument do not affect + the returned instance. + """ + if cls not in cls._instances: + instance = super().__call__(*args, **kwargs) + cls._instances[cls] = instance + + return cls._instances[cls] diff --git a/tidal_dl_ng/helper/decryption.py b/tidal_dl_ng/helper/decryption.py new file mode 100644 index 0000000..2fa2ec0 --- /dev/null +++ b/tidal_dl_ng/helper/decryption.py @@ -0,0 +1,53 @@ +import base64 + +from Crypto.Cipher import AES +from Crypto.Util import Counter + + +def decrypt_security_token(security_token: str) -> (str, str): + """ + Decrypts security token into key and nonce pair + + security_token should match the securityToken value from the web response + """ + + # Do not change this + master_key = "UIlTTEMmmLfGowo/UC60x2H45W6MdGgTRfo/umg4754=" + + # Decode the base64 strings to ascii strings + master_key = base64.b64decode(master_key) + security_token = base64.b64decode(security_token) + + # Get the IV from the first 16 bytes of the securityToken + iv = security_token[:16] + encrypted_st = security_token[16:] + + # Initialize decryptor + decryptor = AES.new(master_key, AES.MODE_CBC, iv) + + # Decrypt the security token + decrypted_st = decryptor.decrypt(encrypted_st) + + # Get the audio stream decryption key and nonce from the decrypted security token + key = decrypted_st[:16] + nonce = decrypted_st[16:24] + + return key, nonce + + +def decrypt_file(path_file_encrypted: str, path_file_destination: str, key: str, nonce: str) -> None: + """ + Decrypts an encrypted MQA file given the file, key and nonce + """ + + # Initialize counter and file decryptor + counter = Counter.new(64, prefix=nonce, initial_value=0) + decryptor = AES.new(key, AES.MODE_CTR, counter=counter) + + # Open and decrypt + with open(path_file_encrypted, "rb") as f_src: + audio_decrypted = decryptor.decrypt(f_src.read()) + + # Replace with decrypted file + with open(path_file_destination, "wb") as f_dst: + f_dst.write(audio_decrypted) diff --git a/tidal_dl_ng/helper/exceptions.py b/tidal_dl_ng/helper/exceptions.py new file mode 100644 index 0000000..9ef6ddc --- /dev/null +++ b/tidal_dl_ng/helper/exceptions.py @@ -0,0 +1,6 @@ +class LoginError(Exception): + pass + + +class MediaUnknown(Exception): + pass diff --git a/tidal_dl_ng/helper/path.py b/tidal_dl_ng/helper/path.py new file mode 100644 index 0000000..c5bf988 --- /dev/null +++ b/tidal_dl_ng/helper/path.py @@ -0,0 +1,177 @@ +import os +import pathlib +import platform +import re +from collections.abc import Callable +from pathlib import Path + +from pathvalidate import sanitize_filename +from tidalapi import Album, Playlist, Track, UserPlaylist, Video, Mix + + +def path_base(): + if "XDG_CONFIG_HOME" in os.environ: + return os.environ["XDG_CONFIG_HOME"] + elif "HOME" in os.environ: + return os.environ["HOME"] + elif "HOMEDRIVE" in os.environ and "HOMEPATH" in os.environ: + return os.path.join(os.environ["HOMEDRIVE"], os.environ["HOMEPATH"]) + else: + return os.path.abspath("./") + + +def path_file_log(): + return os.path.join(path_base(), ".tidal-dl-ng.log") + + +def path_file_token(): + return os.path.join(path_base(), ".tidal-dl-ng_token.json") + + +def path_file_settings(): + return os.path.join(path_base(), ".tidal-dl-ng_settings.json") + + +def format_path_media(fmt_template: str, media: Track | Album | Playlist | UserPlaylist | Video | Mix) -> str: + result = fmt_template + + # Search track format template for placeholder. + regex = r"\{(.+?)\}" + matches = re.finditer(regex, fmt_template, re.MULTILINE) + fn_format = get_fn_format(media) + + for _matchNum, match in enumerate(matches, start=1): + template_str = match.group() + result_fmt = fn_format(match.group(1), media) + + if result_fmt: + value = sanitize_filename(result_fmt) + result = result.replace(template_str, value) + + return result + + +def format_str_track(name: str, media: Track) -> str | bool: + result: str | bool = False + + if name == "track_num": + result = str(media.track_num).rjust(2, "0") + elif name == "artist_name": + result = ", ".join(artist.name for artist in media.artists) + elif name == "track_title": + result = media.name + + return result + + +def format_str_album(name: str, media: Album) -> str | bool: + result: str | bool = False + + if name == "album_title": + result = media.name + elif name == "artist_name": + result = media.artist.name + + return result + + +def format_str_playlist(name: str, media: Playlist) -> str | bool: + result: str | bool = False + + if name == "playlist_name": + result = media.name + + return result + + +def format_str_mix(name: str, media: Mix) -> str | bool: + result: str | bool = False + + if name == "mix_name": + result = media.title + + return result + + +def format_str_video(name: str, media: Video) -> str | bool: + result: str | bool = False + + if name == "artist_name": + result = ", ".join(artist.name for artist in media.artists) + elif name == "track_title": + result = media.name + + return result + + +def get_fn_format(media: Track | Album | Playlist | UserPlaylist | Video | Mix) -> Callable: + result = None + + if isinstance(media, Track): + result = format_str_track + elif isinstance(media, Album): + result = format_str_album + elif isinstance(media, (Playlist, UserPlaylist)): + result = format_str_playlist + elif isinstance(media, Mix): + result = format_str_mix + elif isinstance(media, Video): + result = format_str_video + + return result + + +def length_max_name_file() -> int: + system = platform.system() + result: int = 255 + + try: + if system in ['Darwin', 'Linux']: + result: int = os.pathconf('/', 'PC_NAME_MAX') + except Exception as e: + pass + + return result + + +def length_max_name_path() -> int: + system = platform.system() + result: int = 255 + + try: + if system in ['Darwin', 'Linux']: + result: int = os.pathconf('/', 'PC_PATH_MAX') + except Exception as e: + pass + + return result + + +def path_validate(path_file: str, adapt: bool = False) -> (bool, str): + result: bool = False + length_max_path: int = length_max_name_path() + length_max_file: int = length_max_name_file() + path, file = os.path.split(path_file) + filename, extension = os.path.splitext(file) + + if len(path) >= length_max_path: + result = False + + if adapt: + path = Path.home() + + if len(file) >= length_max_file: + result = False + + if adapt: + file = f'{filename[:length_max_file - len(extension + "_")]}_{extension}' + + path_file = os.path.join(path, file) + + return result, path_file + + +def check_file_exists(path_file: str): + result = pathlib.Path(path_file).is_file() + + return result diff --git a/tidal_dl_ng/helper/url.py b/tidal_dl_ng/helper/url.py new file mode 100644 index 0000000..cb52238 --- /dev/null +++ b/tidal_dl_ng/helper/url.py @@ -0,0 +1,15 @@ +def get_tidal_media_id(url_or_id_media: str) -> str: + id_dirty = url_or_id_media.rsplit("/", 1)[-1] + id = id_dirty.rsplit("?", 1)[0] + + return id + + +def get_tidal_media_type(url_media: str) -> str | bool: + result = False + url_split = url_media.split("/")[-2] + + if len(url_split) > 1: + result = url_media.split("/")[-2] + + return result diff --git a/tidal_dl_ng/helper/wrapper.py b/tidal_dl_ng/helper/wrapper.py new file mode 100644 index 0000000..5b88f59 --- /dev/null +++ b/tidal_dl_ng/helper/wrapper.py @@ -0,0 +1,23 @@ +from collections.abc import Callable + + +class WrapperLogger: + fn_print: Callable = None + + def __init__(self, fn_print: Callable): + self.fn_print = fn_print + + def debug(self, value): + self.fn_print(value) + + def warning(self, value): + self.fn_print(value) + + def info(self, value): + self.fn_print(value) + + def error(self, value): + self.fn_print(value) + + def critical(self, value): + self.fn_print(value) |
