Files
tidal-dl/tidal_dl_ng/config.py
T

201 lines
6.7 KiB
Python
Raw Normal View History

import json
2024-01-13 16:26:33 +01:00
import os
import shutil
2023-12-19 23:58:57 +01:00
from collections.abc import Callable
from json import JSONDecodeError
from pathlib import Path
from threading import Event
from typing import Any
2023-12-19 23:58:57 +01:00
import tidalapi
2024-01-12 08:15:54 +01:00
from requests import HTTPError
2023-12-19 23:58:57 +01:00
from tidal_dl_ng.helper.decorator import SingletonMeta
2024-01-23 09:51:59 +01:00
from tidal_dl_ng.helper.path import path_config_base, path_file_settings, path_file_token
2023-12-19 23:58:57 +01:00
from tidal_dl_ng.model.cfg import Settings as ModelSettings
from tidal_dl_ng.model.cfg import Token as ModelToken
class BaseConfig:
2024-01-23 17:22:10 +01:00
data: ModelSettings | ModelToken
file_path: str
cls_model: ModelSettings | ModelToken
2024-01-23 09:51:59 +01:00
path_base: str = path_config_base()
2023-12-19 23:58:57 +01:00
def save(self, config_to_compare: str = None) -> None:
2023-12-19 23:58:57 +01:00
data_json = self.data.to_json()
# If old and current config is equal, skip the write operation.
if config_to_compare == data_json:
return
2024-01-14 23:09:43 +01:00
# Try to create the base folder.
os.makedirs(self.path_base, exist_ok=True)
2023-12-19 23:58:57 +01:00
with open(self.file_path, encoding="utf-8", mode="w") as f:
# Save it in a pretty format
obj_json_config = json.loads(data_json)
json.dump(obj_json_config, f, indent=4)
2023-12-19 23:58:57 +01:00
def set_option(self, key: str, value: Any) -> None:
value_old: Any = getattr(self.data, key)
if type(value_old) == bool: # noqa: E721
value = True if value.lower() in ("true", "1", "yes", "y") else False # noqa: SIM210
elif type(value_old) == int and type(value) != int: # noqa: E721
value = int(value)
2023-12-19 23:58:57 +01:00
setattr(self.data, key, value)
def read(self, path: str) -> bool:
2024-01-23 09:51:59 +01:00
result: bool = False
settings_json: str = ""
2023-12-19 23:58:57 +01:00
try:
with open(path, encoding="utf-8") as f:
settings_json = f.read()
self.data = self.cls_model.from_json(settings_json)
result = True
2024-01-13 16:26:33 +01:00
except (JSONDecodeError, TypeError, FileNotFoundError, ValueError) as e:
if isinstance(e, ValueError):
path_bak = path + ".bak"
# First check if a backup file already exists. If yes, remove it.
if os.path.exists(path_bak):
os.remove(path_bak)
# Move the invalid config file to the backup location.
shutil.move(path, path_bak)
# TODO: Implement better global logger.
print(
"Something is wrong with your config. Maybe it is not compatible anymore due to a new app version."
f" You can find a backup of your old config here: '{path_bak}'. A new default config was created."
)
2024-01-13 16:26:33 +01:00
2023-12-19 23:58:57 +01:00
self.data = self.cls_model()
# Call save in case of we need to update the saved config, due to changes in code.
self.save(settings_json)
2023-12-19 23:58:57 +01:00
return result
class Settings(BaseConfig, metaclass=SingletonMeta):
def __init__(self):
2024-01-23 17:22:10 +01:00
self.cls_model = ModelSettings
2023-12-19 23:58:57 +01:00
self.file_path = path_file_settings()
self.read(self.file_path)
class Tidal(BaseConfig, metaclass=SingletonMeta):
2024-01-23 17:22:10 +01:00
session: tidalapi.Session
2023-12-19 23:58:57 +01:00
token_from_storage: bool = False
2024-01-23 17:22:10 +01:00
settings: Settings
is_pkce: bool
2023-12-19 23:58:57 +01:00
def __init__(self, settings: Settings = None):
2024-01-23 17:22:10 +01:00
self.cls_model = ModelToken
2024-01-18 22:46:34 +01:00
tidal_config: tidalapi.Config = tidalapi.Config(item_limit=10000)
self.session = tidalapi.Session(tidal_config)
2024-01-14 23:09:43 +01:00
# self.session.config.client_id = "km8T1xS355y7dd3H"
# self.session.config.client_secret = "vcmeGW1OuZ0fWYMCSZ6vNvSLJlT3XEpW0ambgYt5ZuI="
2023-12-19 23:58:57 +01:00
self.file_path = path_file_token()
self.token_from_storage = self.read(self.file_path)
if settings:
self.settings = settings
self.settings_apply()
def settings_apply(self, settings: Settings = None) -> bool:
if settings:
self.settings = settings
self.session.audio_quality = self.settings.data.quality_audio
2024-01-18 22:46:34 +01:00
self.session.video_quality = tidalapi.VideoQuality.high
2023-12-19 23:58:57 +01:00
return True
def login_token(self, do_pkce: bool = False) -> bool:
2023-12-19 23:58:57 +01:00
result = False
2024-05-24 13:41:42 +02:00
self.is_pkce = do_pkce
2023-12-19 23:58:57 +01:00
if self.token_from_storage:
try:
result = self.session.load_oauth_session(
2024-04-21 14:48:48 +02:00
self.data.token_type,
self.data.access_token,
self.data.refresh_token,
self.data.expiry_time,
is_pkce=do_pkce,
2023-12-19 23:58:57 +01:00
)
except (HTTPError, JSONDecodeError):
2023-12-19 23:58:57 +01:00
result = False
# Remove token file. Probably corrupt or invalid.
if os.path.exists(self.file_path):
os.remove(self.file_path)
print(
"Either there is something wrong with your credentials / account or some server problems on TIDALs "
"side. Anyway... Try to login again by re-starting this app."
)
2023-12-19 23:58:57 +01:00
return result
def login_finalize(self) -> bool:
2023-12-19 23:58:57 +01:00
result = self.session.check_login()
if result:
self.token_persist()
return result
def token_persist(self) -> None:
self.set_option("token_type", self.session.token_type)
self.set_option("access_token", self.session.access_token)
self.set_option("refresh_token", self.session.refresh_token)
self.set_option("expiry_time", self.session.expiry_time)
self.save()
def login(self, fn_print: Callable) -> bool:
is_token = self.login_token()
result = False
if is_token:
fn_print("Yep, looks good! You are logged in.")
result = True
elif not is_token:
fn_print("You either do not have a token or your token is invalid.")
fn_print("No worries, we will handle this...")
2024-01-23 09:56:42 +01:00
# Login method: Device linking
self.session.login_oauth_simple(fn_print)
# Login method: PKCE authorization (was necessary for HI_RES_LOSSLESS streaming earlier)
# self.session.login_pkce(fn_print)
2023-12-19 23:58:57 +01:00
is_login = self.login_finalize()
2023-12-19 23:58:57 +01:00
if is_login:
fn_print("The login was successful. I have stored your credentials (token).")
result = True
else:
fn_print("Something went wrong. Did you login using your browser correctly? May try again...")
return result
def logout(self):
Path(self.file_path).unlink(missing_ok=True)
self.token_from_storage = False
del self.session
return True
class HandlingApp(metaclass=SingletonMeta):
event_abort: Event = Event()
event_run: Event = Event()
def __init__(self):
self.event_run.set()