summaryrefslogtreecommitdiff
path: root/tidal_dl_ng/helper
diff options
context:
space:
mode:
authorRobert Honz <exislow+github@gmail.com>2023-12-19 23:58:57 +0100
committerRobert Honz <exislow+github@gmail.com>2023-12-19 23:58:57 +0100
commit0d4a9d6b8740035346d098f5bc28ebca64f6cd4d (patch)
treef52c9569509565e581b03b6e60fe32e611b79ad2 /tidal_dl_ng/helper
parent82a8fe96aa08903919a90a76030bacba9f93b8e0 (diff)
downloadtidal-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__.py0
-rw-r--r--tidal_dl_ng/helper/decorator.py19
-rw-r--r--tidal_dl_ng/helper/decryption.py53
-rw-r--r--tidal_dl_ng/helper/exceptions.py6
-rw-r--r--tidal_dl_ng/helper/path.py177
-rw-r--r--tidal_dl_ng/helper/url.py15
-rw-r--r--tidal_dl_ng/helper/wrapper.py23
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)