🌟 Re-add all project files.
This commit is contained in:
@@ -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]
|
||||
@@ -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)
|
||||
@@ -0,0 +1,6 @@
|
||||
class LoginError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MediaUnknown(Exception):
|
||||
pass
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user