Added filtering to Artist

This commit is contained in:
nathannathant
2021-03-17 14:57:41 -07:00
parent 3f1642f9cd
commit 16d262f3d3
4 changed files with 95 additions and 18 deletions
+2 -2
View File
@@ -193,7 +193,7 @@ class QobuzClient(SecureClientInterface):
@property
def source(self):
return 'qobuz'
return "qobuz"
# ---------- Private Methods ---------------
@@ -387,7 +387,7 @@ class DeezerClient(ClientInterface):
@staticmethod
def get_file_url(meta_id: Union[str, int], quality: int = 6):
quality = min(DEEZER_MAX_Q, quality)
url = f'{DEEZER_DL}/{DEEZER_Q_IDS[quality]}/{DEEZER_BASE}/track/{meta_id}'
url = f"{DEEZER_DL}/{DEEZER_Q_IDS[quality]}/{DEEZER_BASE}/track/{meta_id}"
logger.debug(f"Download url {url}")
return url
+2 -2
View File
@@ -31,7 +31,7 @@ class QobuzDL:
):
self.client = QobuzClient()
self.client.login(creds[0], creds[1])
self.qobuz_url_parse = re.compile(QOBUZ_URL_REGEX)
self.url_parse = re.compile(QOBUZ_URL_REGEX)
def handle_url(self, url: str):
url_type, item_id = self.parse_url(url)
@@ -53,7 +53,7 @@ class QobuzDL:
:raises exceptions.ParsingError
"""
parsed = self.qobuz_url_parse.search(url)
parsed = self.url_parse.search(url)
if parsed is not None:
parsed = parsed.groups()
+90 -13
View File
@@ -1,8 +1,9 @@
import logging
import os
import re
import shutil
from abc import ABC, abstractmethod
from pprint import pformat, pprint
from pprint import pprint
from tempfile import gettempdir
from typing import Any, Optional, Union
@@ -33,6 +34,11 @@ TIDAL_Q_MAP = {
COVER_SIZES = ("thumbnail", "small", "large")
TYPE_REGEXES = {
"remaster": re.compile(r"(?i)(re)?master(ed)?"),
"extra": re.compile(r"(?i)(anniversary|deluxe|live|collector|demo|expanded)"),
}
class Track:
"""Represents a downloadable track.
@@ -130,17 +136,17 @@ class Track:
self.temp_file = os.path.join(gettempdir(), "~qdl_track.tmp")
logger.debug("Temporary file path: %s", self.temp_file)
if self.client.source == 'qobuz':
if self.client.source == "qobuz":
if not (dl_info.get("url") or dl_info.get("sampling_rate")) or dl_info.get(
"sample"
):
logger.debug("Track is a sample: %s", dl_info)
return
if self.client.source in ('qobuz', 'tidal'):
if self.client.source in ("qobuz", "tidal"):
logger.debug("Downloadable URL found: %s", dl_info.get("url"))
tqdm_download(dl_info["url"], self.temp_file) # downloads file
elif self.client.source == 'deezer':
elif self.client.source == "deezer":
logger.debug("Downloadable URL found: %s", dl_info)
tqdm_download(dl_info, self.temp_file) # downloads file
else:
@@ -419,6 +425,15 @@ class Tracklist(list, ABC):
def download(self, **kwargs):
pass
@staticmethod
def essence(album: dict) -> str:
"""Ignore text in parens/brackets, return all lowercase.
Used to group two albums that may be named similarly, but not exactly
the same.
"""
r = re.match(r"([^\(]+)(?:\s*[\(\[][^\)][\)\]])*", album)
return r.group(1).strip().lower()
class Album(Tracklist):
"""Represents a downloadable Qobuz album."""
@@ -466,13 +481,15 @@ class Album(Tracklist):
"id": resp.get("id"),
"title": resp.get("title"),
"_artist": resp.get("artist") or resp.get("performer"),
"albumartist": resp.get("name"),
"albumartist": resp.get("artist", {}).get("name"),
"version": resp.get("version"),
"cover_urls": resp.get("image"),
"streamable": resp.get("streamable"),
"quality": quality_id(
resp.get("maximum_bit_depth"), resp.get("maximum_sampling_rate")
),
"bit_depth": resp.get("maximum_bit_depth"),
"sampling_rate": resp.get("maximum_sampling_rate") * 1000,
"tracktotal": resp.get("tracks_count"),
}
elif client.source == "tidal":
@@ -490,6 +507,8 @@ class Album(Tracklist):
},
"streamable": resp.get("allowStreaming"),
"quality": TIDAL_Q_MAP[resp.get("audioQuality")],
"bit_depth": 16,
"sampling_rate": 44100,
"tracktotal": resp.get("numberOfTracks"),
}
elif client.source == "deezer":
@@ -508,6 +527,8 @@ class Album(Tracklist):
"url": resp.get("link"),
"streamable": True, # api only returns streamables
"quality": 6, # all tracks are 16/44.1 streamable
"bit_depth": 16,
"sampling_rate": 44100,
"tracktotal": resp.get("track_total"),
}
else:
@@ -523,7 +544,6 @@ class Album(Tracklist):
stores the metadata inside a TrackMetadata object.
"""
logging.debug("Loading tracks to album")
logging.debug(pformat(self.meta))
for i in range(self.tracktotal):
# append method inherited from superclass list
self.append(
@@ -594,13 +614,13 @@ class Album(Tracklist):
# create a single cover object and use them for all tracks
# TODO: avoid this method if embeded covers are not requested
if self.client.source != 'deezer':
if self.client.source != "deezer":
cover = self.get_cover_obj(cover_path, quality)
for track in self:
logger.debug("Downloading track to %s with quality %s", folder, quality)
track.download(quality, folder, progress_bar)
if tag_tracks and self.client.source != 'deezer':
if tag_tracks and self.client.source != "deezer":
logger.debug("Tagging track")
track.tag(cover=cover)
@@ -699,17 +719,47 @@ class Artist(Tracklist):
self.load_meta()
def load_meta(self):
response = self.client.get(self.id, media_type="artist")
for page in response: # hacky solution, fix later
self.meta = page
self._load_albums()
self.meta = self.client.get(self.id, media_type="artist")
self._load_albums()
self.name = self.meta.get("name")
def _load_albums(self):
for album in self.meta.get("albums", {}).get("items", []):
logger.debug("Appending album: %s", album.get("title"))
self.append(Album(self.client, **album))
self.append(Album(self.client, **Album._parse_get_resp(album, self.client)))
def download(self, filters=None, no_repeats=False, quality=7):
logger.debug(f"Length of tracklist {len(self)}")
if no_repeats:
final = self._remove_repeats(bit_depth=max, sampling_rate=min)
else:
final = self
if filters is not None and self.client.source == "qobuz":
if type(filters) in (tuple, list):
for f in filters:
def inter(album):
"""Intermediate function to pass self into f"""
return f(self, album)
final = filter(inter, final)
else:
def inter(album):
return filters(self, album)
final = filter(inter, final)
i = 0
for album in final:
i += 1
logger.debug(f"Downloading album {album}")
album.load_meta()
album.download(quality=quality)
logger.debug(f"{i} albums downloaded")
@classmethod
def from_api(cls, item: dict, client: ClientInterface, source: str = "qobuz"):
@@ -739,6 +789,33 @@ class Artist(Tracklist):
# equivalent to Artist(client=client, **info)
return cls(client=client, **info)
def _remove_repeats(self, bit_depth=max, sampling_rate=max):
groups = dict()
for album in self:
if (t := self.essence(album.title)) not in groups:
groups[t] = []
groups[t].append(album)
for group in groups.values():
assert bit_depth in (min, max) and sampling_rate in (min, max)
best_bd = bit_depth(a["bit_depth"] for a in group)
best_sr = sampling_rate(a["sampling_rate"] for a in group)
for album in group:
if album["bit_depth"] == best_bd and album["sampling_rate"] == best_sr:
yield album
break
@staticmethod
def studio_albums(artist, album):
return (
album["albumartist"] != "Various Artists"
and TYPE_REGEXES['extra'].search(album.title) is None
)
@staticmethod
def no_features(artist, album):
return artist["name"] == album["albumartist"]
def __repr__(self) -> str:
"""Return a string representation of this Artist object.
Useful for pprint and json.dumps.
+1 -1
View File
@@ -1,7 +1,7 @@
import sys
import logging
import logging.handlers as handlers
import os
import sys
from typing import Optional, Union
import requests