qobuz-dl/qobuz_dl_rewrite/utils.py

155 lines
4.1 KiB
Python

import logging
import logging.handlers as handlers
import os
from string import Formatter
from typing import Optional
import requests
from pathvalidate import sanitize_filename
from tqdm import tqdm
from .constants import LOG_DIR, TIDAL_COVER_URL
logger = logging.getLogger(__name__)
def safe_get(d: dict, *keys, default=None):
"""A replacement for chained `get()` statements on dicts:
>>> d = {'foo': {'bar': 'baz'}}
>>> _safe_get(d, 'baz')
None
>>> _safe_get(d, 'foo', 'bar')
'baz'
"""
curr = d
res = default
for key in keys:
res = curr.get(key, default)
if res == default or not hasattr(res, "__getitem__"):
return res
else:
curr = res
return res
def quality_id(bit_depth: Optional[int], sampling_rate: Optional[int]):
"""Return a quality id in (5, 6, 7, 27) from bit depth and
sampling rate. If None is provided, mp3/lossy is assumed.
:param bit_depth:
:type bit_depth: Optional[int]
:param sampling_rate:
:type sampling_rate: Optional[int]
"""
if not (bit_depth or sampling_rate): # is lossy
return 5
if bit_depth == 16:
return 6
if bit_depth == 24:
if sampling_rate <= 96:
return 7
return 27
def tqdm_download(url: str, filepath: str):
"""Downloads a file with a progress bar.
:param url: url to direct download
:param filepath: file to write
:type url: str
:type filepath: str
"""
# FIXME: add the conditional to the progress_bar bool
logger.debug(f"Downloading {url} to {filepath}")
r = requests.get(url, allow_redirects=True, stream=True)
total = int(r.headers.get("content-length", 0))
logger.debug(f"File size = {total}")
try:
with open(filepath, "wb") as file, tqdm(
total=total, unit="iB", unit_scale=True, unit_divisor=1024
) as bar:
for data in r.iter_content(chunk_size=1024):
size = file.write(data)
bar.update(size)
except Exception:
try:
os.remove(filepath)
except OSError:
pass
raise
def clean_format(formatter: str, format_info):
"""Formats track or folder names sanitizing every formatter key.
:param formatter:
:type formatter: str
:param kwargs:
"""
fmt_keys = [i[1] for i in Formatter().parse(formatter) if i[1] is not None]
logger.debug("Formatter keys: %s", fmt_keys)
clean_dict = dict()
for key in fmt_keys:
if isinstance(format_info.get(key), (str, int, float)): # int for track numbers
clean_dict[key] = sanitize_filename(str(format_info[key]))
else:
clean_dict[key] = "Unknown"
return formatter.format(**clean_dict)
def tidal_cover_url(uuid, size):
possibles = (80, 160, 320, 640, 1280)
assert size in possibles, f"size must be in {possibles}"
return TIDAL_COVER_URL.format(uuid=uuid.replace("-", "/"), height=size, width=size)
def init_log(
path: Optional[str] = None, level: str = "DEBUG", rotate: str = "midnight"
):
"""
Initialize a log instance with a stream handler and a rotating file handler.
If a path is not set, fallback to the default app log directory.
:param path:
:type path: Optional[str]
:param level:
:type level: str
:param rotate:
:type rotate: str
"""
if not path:
os.makedirs(LOG_DIR, exist_ok=True)
path = os.path.join(LOG_DIR, "qobuz_dl.log")
logger = logging.getLogger()
level = logging.getLevelName(level)
logger.setLevel(level)
formatter = logging.Formatter(
fmt="%(asctime)s - %(module)s.%(funcName)s.%(levelname)s: %(message)s",
datefmt="%H:%M:%S",
)
rotable = handlers.TimedRotatingFileHandler(path, when=rotate)
printable = logging.StreamHandler()
rotable.setFormatter(formatter)
printable.setFormatter(formatter)
logger.addHandler(printable)
logger.addHandler(rotable)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("tidal_api").setLevel(logging.WARNING)
def capitalize(s: str) -> str:
return s[0].upper() + s[1:]