From c4824e875fd4d5310a6c7013a7d48d63cc513be7 Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Mon, 13 Sep 2021 22:31:13 -0700 Subject: [PATCH 01/14] Fix `download_videos` key #177 --- streamrip/media.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/streamrip/media.py b/streamrip/media.py index 0a72341..48f161f 100644 --- a/streamrip/media.py +++ b/streamrip/media.py @@ -874,6 +874,10 @@ class Video(Media): :param kwargs: """ + + if not kwargs.get("download_videos", True): + return + import m3u8 import requests @@ -892,7 +896,7 @@ class Video(Media): segment.uri for segment in parsed_m3u.segments ) as pool: bar = get_tqdm_bar( - len(pool), desc="Downloading Video", unit="Chunk" + len(pool), desc=self._progress_desc, unit="Chunk" ) def update_tqdm_bar(): @@ -966,6 +970,10 @@ class Video(Media): """ pass + @property + def _progress_desc(self) -> str: + return style(f"Video {self.tracknumber:02}", fg="blue") + @property def path(self) -> str: """Get path to download the mp4 file. @@ -1497,7 +1505,7 @@ class Album(Tracklist, Media): if not self.get("streamable", False): raise NonStreamable(f"This album is not streamable ({self.id} ID)") - self._load_tracks(resp) + self._load_tracks(resp, kwargs.get("download_videos", True)) self.loaded = True @classmethod @@ -1622,7 +1630,7 @@ class Album(Tracklist, Media): meta.id = resp["id"] return meta - def _load_tracks(self, resp): + def _load_tracks(self, resp, download_videos: bool = True): """Load the tracks into self from an API response. This uses a classmethod to convert an item into a Track object, which @@ -1631,7 +1639,8 @@ class Album(Tracklist, Media): logging.debug("Loading %d tracks to album", self.tracktotal) for track in _get_tracklist(resp, self.client.source): if track.get("type") == "Music Video": - self.append(Video.from_album_meta(track, self.client)) + if download_videos: + self.append(Video.from_album_meta(track, self.client)) else: self.append( Track.from_album_meta( From e8b897d0cea4ce01adf66df6a195724b7654b568 Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Wed, 15 Sep 2021 16:05:27 -0700 Subject: [PATCH 02/14] Use YAML for bug report template --- .github/ISSUE_TEMPLATE/bug_report.md | 58 ----------------------- .github/ISSUE_TEMPLATE/bug_report.yaml | 64 ++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 58 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c813ad3..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -name: "\U0001F41B Bug Report" -about: "If something isn't working as expected \U0001F914." -title: 'Bug: Your Title' -labels: bug -assignees: nathom - ---- - -## Bug Report - -**Current Behavior** -A clear and concise description of the behavior. - -**Command used:** - -```bash -Type the command that caused the bug here -``` - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**`streamrip` Configuration File (find using `rip config -o`):** - - - - - - - - - - -```toml -# Paste the contents of config.toml here -# REMOVE YOUR CREDENTIALS -[your] -config = "file" -``` - -**Environment** - -- `streamrip` version(s): [e.g. v0.5.2] -- Python version: [e.g. 3.8] -- OS: [e.g. OSX 10.13.4, Windows 10] - -**Traceback** -```bash -# Paste the text that Python dumped on the error here -# Delete this section if there was no traceback -``` - -**Possible Solution** - - -**Additional context/Screenshots** -Add any other context about the problem here. If applicable, add screenshots to help explain. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..fcc68dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,64 @@ +name: Bug report +blank_issues_enabled: false +description: Create a report to help us improve streamrip +labels: bug +body: + # types: dropdown, textarea, input + # - type: dropdown + # validations: + # required: true + # attributes: + # label: Are you using "tree-sitter" branch? + # options: + # - 'Yes' + # - 'No' + + - type: textarea + validations: + required: true + attributes: + label: Describe the bug + description: A clear and concise description of the bug. + + - type: textarea + validations: + required: true + attributes: + label: Command Used + description: | + The command that you typed that caused the error + placeholder: | + For example: + `rip url https://example.com` + + - type: textarea + validations: + required: true + attributes: + label: Traceback + description: If there was any error output, paste it here + + - type: textarea + attributes: + label: Screenshots and recordings + description: | + If applicable, add screenshots to help explain your problem. You can also record an asciinema session: https://asciinema.org/ + - type: input + validations: + required: true + attributes: + label: Operating System + placeholder: e.g. Windows 10, Ubuntu 20.04, Arch Linux, macOS 10.15... + + - type: input + validations: + required: true + attributes: + label: streamrip version + description: Run `rip --version` to check. + placeholder: e.g. 1.5 + + - type: textarea + attributes: + label: Additional context + description: Add any other context about the problem here. From f4364eedba99b8a0ab4148a88f5b474973c3dcce Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Wed, 15 Sep 2021 16:06:23 -0700 Subject: [PATCH 03/14] Remove invalid key --- .github/ISSUE_TEMPLATE/bug_report.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index fcc68dc..1d1f2e9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,5 +1,4 @@ name: Bug report -blank_issues_enabled: false description: Create a report to help us improve streamrip labels: bug body: From 839332611972129310b292a3c1ead06cb12336ae Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Wed, 15 Sep 2021 20:58:42 -0700 Subject: [PATCH 04/14] Update issue template --- .github/ISSUE_TEMPLATE/bug_report.yaml | 51 ++++++++++++++++++++++++-- .github/ISSUE_TEMPLATE/config.yaml | 1 + streamrip/utils.py | 17 +++++++-- 3 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 1d1f2e9..8cd7394 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -30,24 +30,59 @@ body: For example: `rip url https://example.com` + - type: markdown + attributes: + value: "```\n" + - type: textarea validations: required: true attributes: - label: Traceback - description: If there was any error output, paste it here + label: Debug Traceback + description: | + Run your command, with `-vvv` appended to it, and paste the output here. + For example, if the problematic command was `rip url https://example.com`, then + you would run `rip url https://example.com -vvv` to get the debug logs. + Make sure to check the logs for any personal information such as emails and remove them. + + - type: markdown + attributes: + value: "```" - type: textarea attributes: label: Screenshots and recordings description: | - If applicable, add screenshots to help explain your problem. You can also record an asciinema session: https://asciinema.org/ + If applicable, add screenshots to help explain your problem. + You can also record an asciinema session: https://asciinema.org/ + + - type: markdown + attributes: + value: "```\n" + + - type: textarea + validations: + required: true + attributes: + label: Config File + description: | + Find the config file using `rip config --open` and paste the contents here. + Make sure you REMOVE YOUR CREDENTIALS! + + - type: markdown + attributes: + value: "```" + - type: input validations: required: true attributes: label: Operating System - placeholder: e.g. Windows 10, Ubuntu 20.04, Arch Linux, macOS 10.15... + placeholder: e.g. Windows, Linux, macOS... + + - type: markdown + attributes: + value: "```\n" - type: input validations: @@ -57,7 +92,15 @@ body: description: Run `rip --version` to check. placeholder: e.g. 1.5 + - type: markdown + attributes: + value: "```" + - type: textarea attributes: label: Additional context description: Add any other context about the problem here. + + - type: markdown + attributes: + value: "Thanks for completing our form!" diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/streamrip/utils.py b/streamrip/utils.py index a64b4f8..5412b78 100644 --- a/streamrip/utils.py +++ b/streamrip/utils.py @@ -342,13 +342,24 @@ def get_cover_urls(resp: dict, source: str) -> dict: } if source == "deezer": + resp_keys = ("cover", "cover_medium", "cover_large", "cover_xl") + resp_keys_fallback = ( + "picture", + "picture_medium", + "picture_large", + "picture_xl", + ) cover_urls = { - sk: resp.get(rk) # size key, resp key - for sk, rk in zip( + sk: resp.get( + rk, resp.get(rkf) + ) # size key, resp key, resp key fallback + for sk, rk, rkf in zip( COVER_SIZES, - ("cover", "cover_medium", "cover_large", "cover_xl"), + resp_keys, + resp_keys_fallback, ) } + print(cover_urls) if cover_urls["large"] is None and resp.get("cover_big") is not None: cover_urls["large"] = resp["cover_big"] From 71b8b0a45b5a47c88f1bc6d87135b9cbb66e1d87 Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Wed, 15 Sep 2021 21:03:52 -0700 Subject: [PATCH 05/14] Update issue template --- .github/ISSUE_TEMPLATE/bug_report.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 8cd7394..2bd07ac 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -49,16 +49,9 @@ body: attributes: value: "```" - - type: textarea - attributes: - label: Screenshots and recordings - description: | - If applicable, add screenshots to help explain your problem. - You can also record an asciinema session: https://asciinema.org/ - - type: markdown attributes: - value: "```\n" + value: "```toml\n" - type: textarea validations: @@ -96,6 +89,13 @@ body: attributes: value: "```" + - type: textarea + attributes: + label: Screenshots and recordings + description: | + If applicable, add screenshots to help explain your problem. + You can also record an asciinema session: https://asciinema.org/ + - type: textarea attributes: label: Additional context From 61079a6c7b630ff2f81376ce7d67d1eb5e18e021 Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Thu, 16 Sep 2021 11:44:07 -0700 Subject: [PATCH 06/14] Formatting --- streamrip/constants.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/streamrip/constants.py b/streamrip/constants.py index e8eb3b0..bce45d7 100644 --- a/streamrip/constants.py +++ b/streamrip/constants.py @@ -4,9 +4,7 @@ import mutagen.id3 as id3 AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0" -TIDAL_COVER_URL = ( - "https://resources.tidal.com/images/{uuid}/{width}x{height}.jpg" -) +TIDAL_COVER_URL = "https://resources.tidal.com/images/{uuid}/{width}x{height}.jpg" # Get this from (base64encoded) # aHR0cHM6Ly9hLXYyLnNuZGNkbi5jb20vYXNzZXRzLzItYWIxYjg1NjguanM= # Don't know if this is a static url yet @@ -142,7 +140,9 @@ ALBUM_KEYS = ( "id", ) # TODO: rename these to DEFAULT_FOLDER_FORMAT etc -FOLDER_FORMAT = "{albumartist} - {title} ({year}) [{container}] [{bit_depth}B-{sampling_rate}kHz]" +FOLDER_FORMAT = ( + "{albumartist} - {title} ({year}) [{container}] [{bit_depth}B-{sampling_rate}kHz]" +) TRACK_FORMAT = "{tracknumber}. {artist} - {title}" From 1f3b24e5b719a1cfffd7555b917063667592084b Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Thu, 16 Sep 2021 18:48:11 -0700 Subject: [PATCH 07/14] Dynamically find soundcloud client ID --- rip/config.py | 6 ++- rip/config.toml | 5 ++- streamrip/clients.py | 103 ++++++++++++++++++++++++++++--------------- 3 files changed, 76 insertions(+), 38 deletions(-) diff --git a/rip/config.py b/rip/config.py index af0f4cc..0542619 100644 --- a/rip/config.py +++ b/rip/config.py @@ -188,7 +188,11 @@ class Config: if source == "deezer": return {"arl": self.file["deezer"]["arl"]} if source == "soundcloud": - return {} + soundcloud = self.file["soundcloud"] + return { + "client_id": soundcloud["client_id"], + "app_version": soundcloud["app_version"], + } raise InvalidSourceError(source) diff --git a/rip/config.toml b/rip/config.toml index c31b00e..ca7451b 100644 --- a/rip/config.toml +++ b/rip/config.toml @@ -63,6 +63,9 @@ deezloader_warnings = true [soundcloud] # Only 0 is available for now quality = 0 +# This changes periodically, so it needs to be updated +client_id = "" +app_version = "" [youtube] # Only 0 is available for now @@ -164,4 +167,4 @@ progress_bar = "dainty" [misc] # Metadata to identify this config file. Do not change. -version = "1.5" +version = "1.6" diff --git a/streamrip/clients.py b/streamrip/clients.py index 9eec840..75ae0bd 100644 --- a/streamrip/clients.py +++ b/streamrip/clients.py @@ -1127,42 +1127,81 @@ class SoundCloudClient(Client): source = "soundcloud" max_quality = 0 - logged_in = True + logged_in = False + + client_id: str = "" + app_version: str = "" def __init__(self): """Create a SoundCloudClient.""" self.session = gen_threadsafe_session( headers={ "User-Agent": AGENT, - "Host": "api-v2.soundcloud.com", - "Origin": "https://soundcloud.com", - "Referer": "https://soundcloud.com/", - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-site", - "Sec-GPC": "1", } ) - def login(self): - """Login is not necessary for SoundCloud.""" - raise NotImplementedError + def login(self, **kwargs): + self.client_id = kwargs.get("client_id") + self.app_version = kwargs.get("app_version") + logger.debug("client_id: %s, app_version: %s", self.client_id, self.app_version) + + # if (not self.client_id) or (not self.app_version) or (not self._announce()): + if not (self.client_id and self.app_version and self._announce()): + logger.debug( + "Refreshing client_id=%s and app_version=%s", + self.client_id, + self.app_version, + ) + self._refresh_tokens() + + self.logged_in = True + + def _announce(self): + return self._get("announcements").status_code == 200 + + def _refresh_tokens(self): + STOCK_URL = "https://soundcloud.com/" + + resp = self.session.get(STOCK_URL) + resp.encoding = "utf-8" + + *_, client_id_url_match = re.finditer( + r"window\.__sc_version="(\d+)"', resp.text + ).group(1) + + resp2 = self.session.get(client_id_url) + self.client_id = re.search(r'client_id:\s*"(\w+)"', resp2.text).group(1) + + def resolve_url(self, url: str) -> dict: + resp = self._get(f"resolve?url={url}").json() + from pprint import pformat + + logger.debug(pformat(resp)) + return resp + + def get_tokens(self): + return self.client_id, self.app_version def get(self, id, media_type="track"): - """Get metadata for a media type given an id. + """Get metadata for a media type given a soundcloud url. :param id: :param media_type: """ - assert media_type in ( + assert media_type in { "track", "playlist", - ), f"{media_type} not supported" + }, f"{media_type} not supported" - if "http" in str(id): - resp, _ = self._get(f"resolve?url={id}") - elif media_type == "track": - resp, _ = self._get(f"{media_type}s/{id}") + if media_type == "track": + resp = self._get(f"{media_type}s/{id}") + resp.raise_for_status() + resp = resp.json() else: raise Exception(id) @@ -1194,16 +1233,13 @@ class SoundCloudClient(Client): url = None for tc in track["media"]["transcodings"]: fmt = tc["format"] - if ( - fmt["protocol"] == "hls" - and fmt["mime_type"] == "audio/mpeg" - ): + if fmt["protocol"] == "hls" and fmt["mime_type"] == "audio/mpeg": url = tc["url"] break assert url is not None - resp, _ = self._get(url, no_base=True) + resp = self._get(url, no_base=True).json() return {"url": resp["url"], "type": "mp3"} def search(self, query: str, media_type="album", limit=50, offset=50): @@ -1222,8 +1258,10 @@ class SoundCloudClient(Client): "offset": offset, "linked_partitioning": "1", } - resp, _ = self._get(f"search/{media_type}s", params=params) - return resp + result = self._get(f"search/{media_type}s", params=params) + + # The response + return result.json() def _get( self, @@ -1232,7 +1270,7 @@ class SoundCloudClient(Client): no_base=False, skip_decode=False, headers=None, - ) -> Optional[Tuple[dict, int]]: + ): """Send a request to the SoundCloud API. :param path: @@ -1244,8 +1282,8 @@ class SoundCloudClient(Client): """ param_arg = params params = { - "client_id": SOUNDCLOUD_CLIENT_ID, - "app_version": SOUNDCLOUD_APP_VERSION, + "client_id": self.client_id, + "app_version": self.app_version, "app_locale": "en", } if param_arg is not None: @@ -1257,11 +1295,4 @@ class SoundCloudClient(Client): url = f"{SOUNDCLOUD_BASE}/{path}" logger.debug("Fetching url %s with params %s", url, params) - r = self.session.get(url, params=params, headers=headers) - - r.raise_for_status() - - if skip_decode: - return None - - return r.json(), r.status_code + return self.session.get(url, params=params, headers=headers) From 35c8932ffb2cd63b94d04c64b126c091b98d0163 Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Thu, 16 Sep 2021 18:48:27 -0700 Subject: [PATCH 08/14] Formatting --- rg | 0 rip/cli.py | 44 ++++--------- rip/config.py | 9 +-- rip/core.py | 119 +++++++++++++++-------------------- rip/db.py | 12 +--- streamrip/clients.py | 88 +++++++------------------- streamrip/converter.py | 20 ++---- streamrip/downloadtools.py | 30 +++------ streamrip/media.py | 123 ++++++++++--------------------------- streamrip/metadata.py | 38 +++--------- streamrip/utils.py | 26 +++----- tests/test_download.py | 5 +- 12 files changed, 151 insertions(+), 363 deletions(-) create mode 100644 rg diff --git a/rg b/rg new file mode 100644 index 0000000..e69de29 diff --git a/rip/cli.py b/rip/cli.py index 6552ea9..95a65c7 100644 --- a/rip/cli.py +++ b/rip/cli.py @@ -86,9 +86,7 @@ class DownloadCommand(Command): if len(core) > 0: core.download() elif not urls and path is None: - self.line( - "Must pass arguments. See rip url -h." - ) + self.line("Must pass arguments. See rip url -h.") update_check.join() if outdated: @@ -115,16 +113,10 @@ class DownloadCommand(Command): "https://api.github.com/repos/nathom/streamrip/releases/latest" ).json()["body"] - release_notes = md_header.sub( - r"
\1
", release_notes - ) - release_notes = bullet_point.sub( - r"• \1", release_notes - ) + release_notes = md_header.sub(r"
\1
", release_notes) + release_notes = bullet_point.sub(r"• \1", release_notes) release_notes = code.sub(r"\1", release_notes) - release_notes = issue_reference.sub( - r"\1", release_notes - ) + release_notes = issue_reference.sub(r"\1", release_notes) self.line(release_notes) @@ -154,9 +146,7 @@ class SearchCommand(Command): def handle(self): query = self.argument("query") - source, type = clean_options( - self.option("source"), self.option("type") - ) + source, type = clean_options(self.option("source"), self.option("type")) config = Config() core = RipCore(config) @@ -219,18 +209,14 @@ class DiscoverCommand(Command): from streamrip.constants import QOBUZ_FEATURED_KEYS if chosen_list not in QOBUZ_FEATURED_KEYS: - self.line( - f'Error: list "{chosen_list}" not available' - ) + self.line(f'Error: list "{chosen_list}" not available') self.line(self.help) return 1 elif source == "deezer": from streamrip.constants import DEEZER_FEATURED_KEYS if chosen_list not in DEEZER_FEATURED_KEYS: - self.line( - f'Error: list "{chosen_list}" not available' - ) + self.line(f'Error: list "{chosen_list}" not available') self.line(self.help) return 1 @@ -318,9 +304,7 @@ class ConfigCommand(Command): self.line(f"{CONFIG_PATH}") if self.option("open"): - self.line( - f"Opening {CONFIG_PATH} in default application" - ) + self.line(f"Opening {CONFIG_PATH} in default application") launch(CONFIG_PATH) if self.option("reset"): @@ -367,9 +351,7 @@ class ConfigCommand(Command): self.line("Sucessfully logged in!") except AuthenticationError: - self.line( - "Could not log in. Double check your ARL" - ) + self.line("Could not log in. Double check your ARL") if self.option("qobuz"): import getpass @@ -377,9 +359,7 @@ class ConfigCommand(Command): self._config.file["qobuz"]["email"] = self.ask("Qobuz email:") self._config.file["qobuz"]["password"] = hashlib.md5( - getpass.getpass( - "Qobuz password (won't show on screen): " - ).encode() + getpass.getpass("Qobuz password (won't show on screen): ").encode() ).hexdigest() self._config.save() @@ -631,9 +611,7 @@ class Application(BaseApplication): formatter.set_style("path", Style("green", options=["bold"])) formatter.set_style("cmd", Style("magenta")) formatter.set_style("title", Style("yellow", options=["bold"])) - formatter.set_style( - "header", Style("yellow", options=["bold", "underline"]) - ) + formatter.set_style("header", Style("yellow", options=["bold", "underline"])) io.output.set_formatter(formatter) io.error_output.set_formatter(formatter) diff --git a/rip/config.py b/rip/config.py index 0542619..10728d1 100644 --- a/rip/config.py +++ b/rip/config.py @@ -31,9 +31,7 @@ class Config: values. """ - default_config_path = os.path.join( - os.path.dirname(__file__), "config.toml" - ) + default_config_path = os.path.join(os.path.dirname(__file__), "config.toml") with open(default_config_path) as cfg: defaults: Dict[str, Any] = tomlkit.parse(cfg.read().strip()) @@ -57,10 +55,7 @@ class Config: if os.path.isfile(self._path): self.load() - if ( - self.file["misc"]["version"] - != self.defaults["misc"]["version"] - ): + if self.file["misc"]["version"] != self.defaults["misc"]["version"]: secho( "Updating config file to new version. Some settings may be lost.", fg="yellow", diff --git a/rip/core.py b/rip/core.py index 54ad588..9c229b6 100644 --- a/rip/core.py +++ b/rip/core.py @@ -112,18 +112,14 @@ class RipCore(list): else: self.config = config - if ( - theme := self.config.file["theme"]["progress_bar"] - ) != TQDM_DEFAULT_THEME: + if (theme := self.config.file["theme"]["progress_bar"]) != TQDM_DEFAULT_THEME: set_progress_bar_theme(theme.lower()) def get_db(db_type: str) -> db.Database: db_settings = self.config.session["database"] db_class = db.CLASS_MAP[db_type] - if db_settings[db_type]["enabled"] and db_settings.get( - "enabled", True - ): + if db_settings[db_type]["enabled"] and db_settings.get("enabled", True): default_db_path = DB_PATH_MAP[db_type] path = db_settings[db_type]["path"] @@ -218,8 +214,7 @@ class RipCore(list): logger.debug(session) # So that the dictionary isn't searched for the same keys multiple times artwork, conversion, filepaths, metadata = ( - session[key] - for key in ("artwork", "conversion", "filepaths", "metadata") + session[key] for key in ("artwork", "conversion", "filepaths", "metadata") ) concurrency = session["downloads"]["concurrency"] return { @@ -265,9 +260,7 @@ class RipCore(list): ) exit() - for counter, (source, media_type, item_id) in enumerate( - self.failed_db - ): + for counter, (source, media_type, item_id) in enumerate(self.failed_db): if counter >= max_items: break @@ -290,9 +283,7 @@ class RipCore(list): logger.debug("Arguments from config: %s", arguments) - source_subdirs = self.config.session["downloads"][ - "source_subdirectories" - ] + source_subdirs = self.config.session["downloads"]["source_subdirectories"] for item in self: # Item already checked in database in handle_urls if source_subdirs: @@ -304,26 +295,20 @@ class RipCore(list): item.download(**arguments) continue - arguments["quality"] = self.config.session[item.client.source][ - "quality" - ] + arguments["quality"] = self.config.session[item.client.source]["quality"] if isinstance(item, Artist): filters_ = tuple( k for k, v in self.config.session["filters"].items() if v ) arguments["filters"] = filters_ - logger.debug( - "Added filter argument for artist/label: %s", filters_ - ) + logger.debug("Added filter argument for artist/label: %s", filters_) if not isinstance(item, Tracklist) or not item.loaded: logger.debug("Loading metadata") try: item.load_meta(**arguments) except NonStreamable: - self.failed_db.add( - (item.client.source, item.type, item.id) - ) + self.failed_db.add((item.client.source, item.type, item.id)) secho(f"{item!s} is not available, skipping.", fg="red") continue @@ -360,9 +345,7 @@ class RipCore(list): :param featured_list: The name of the list. See `rip discover --help`. :type featured_list: str """ - self.extend( - self.search("qobuz", featured_list, "featured", limit=max_items) - ) + self.extend(self.search("qobuz", featured_list, "featured", limit=max_items)) def get_client(self, source: str) -> Client: """Get a client given the source and log in. @@ -427,6 +410,17 @@ class RipCore(list): self.config.file["qobuz"]["secrets"], ) = client.get_tokens() self.config.save() + elif ( + client.source == "soundcloud" + and not creds.get("client_id") + and not creds.get("app_version") + ): + ( + self.config.file["soundcloud"]["client_id"], + self.config.file["soundcloud"]["app_version"], + ) = client.get_tokens() + self.config.save() + elif client.source == "tidal": self.config.file["tidal"].update(client.get_tokens()) self.config.save() # only for the expiry stamp @@ -435,14 +429,14 @@ class RipCore(list): """Return the type of the url and the id. Compatible with urls of the form: - https://www.qobuz.com/us-en/{type}/{name}/{id} - https://open.qobuz.com/{type}/{id} - https://play.qobuz.com/{type}/{id} + https://www.qobuz.com/us-en/type/name/id + https://open.qobuz.com/type/id + https://play.qobuz.com/type/id - https://www.deezer.com/us/{type}/{id} - https://tidal.com/browse/{type}/{id} + https://www.deezer.com/us/type/id + https://tidal.com/browse/type/id - :raises exceptions.ParsingError + :raises exceptions.ParsingError: """ parsed: List[Tuple[str, str, str]] = [] @@ -468,20 +462,25 @@ class RipCore(list): fg="yellow", ) parsed.extend( - ("deezer", *extract_deezer_dynamic_link(url)) - for url in dynamic_urls + ("deezer", *extract_deezer_dynamic_link(url)) for url in dynamic_urls ) - parsed.extend(URL_REGEX.findall(url)) # Qobuz, Tidal, Dezer + parsed.extend(URL_REGEX.findall(url)) # Qobuz, Tidal, Deezer soundcloud_urls = SOUNDCLOUD_URL_REGEX.findall(url) - soundcloud_items = [ - self.clients["soundcloud"].get(u) for u in soundcloud_urls - ] - parsed.extend( - ("soundcloud", item["kind"], url) - for item, url in zip(soundcloud_items, soundcloud_urls) - ) + if soundcloud_urls: + soundcloud_client = self.get_client("soundcloud") + assert isinstance(soundcloud_client, SoundCloudClient) # for typing + + # TODO: Make this async + soundcloud_items = ( + soundcloud_client.resolve_url(u) for u in soundcloud_urls + ) + + parsed.extend( + ("soundcloud", item["kind"], str(item["id"])) + for item in soundcloud_items + ) logger.debug("Parsed urls: %s", parsed) @@ -507,15 +506,11 @@ class RipCore(list): # For testing: # https://www.last.fm/user/nathan3895/playlists/12058911 - user_regex = re.compile( - r"https://www\.last\.fm/user/([^/]+)/playlists/\d+" - ) + user_regex = re.compile(r"https://www\.last\.fm/user/([^/]+)/playlists/\d+") lastfm_urls = LASTFM_URL_REGEX.findall(urls) try: lastfm_source = self.config.session["lastfm"]["source"] - lastfm_fallback_source = self.config.session["lastfm"][ - "fallback_source" - ] + lastfm_fallback_source = self.config.session["lastfm"]["fallback_source"] except KeyError: self._config_updating_message() self.config.update() @@ -549,16 +544,12 @@ class RipCore(list): ) query_is_clean = banned_words_plain.search(query) is None - search_results = self.search( - source, query, media_type="track" - ) + search_results = self.search(source, query, media_type="track") track = next(search_results) if query_is_clean: while banned_words.search(track["title"]) is not None: - logger.debug( - "Track title banned for query=%s", query - ) + logger.debug("Track title banned for query=%s", query) track = next(search_results) # Because the track is searched as a single we need to set @@ -568,9 +559,7 @@ class RipCore(list): except (NoResultsFound, StopIteration): return None - track = try_search(lastfm_source) or try_search( - lastfm_fallback_source - ) + track = try_search(lastfm_source) or try_search(lastfm_fallback_source) if track is None: return False @@ -594,9 +583,7 @@ class RipCore(list): pl.creator = creator_match.group(1) tracks_not_found = 0 - with concurrent.futures.ThreadPoolExecutor( - max_workers=15 - ) as executor: + with concurrent.futures.ThreadPoolExecutor(max_workers=15) as executor: futures = [ executor.submit(search_query, title, artist, pl) for title, artist in queries @@ -725,9 +712,7 @@ class RipCore(list): raise NotImplementedError fields = (fname for _, fname, _, _ in Formatter().parse(fmt) if fname) - ret = fmt.format( - **{k: media.get(k, default="Unknown") for k in fields} - ) + ret = fmt.format(**{k: media.get(k, default="Unknown") for k in fields}) return ret def interactive_search( @@ -865,9 +850,7 @@ class RipCore(list): playlist_title = html.unescape(playlist_title_match.group(1)) if remaining_tracks > 0: - with concurrent.futures.ThreadPoolExecutor( - max_workers=15 - ) as executor: + with concurrent.futures.ThreadPoolExecutor(max_workers=15) as executor: last_page = int(remaining_tracks // 50) + int( remaining_tracks % 50 != 0 ) @@ -922,9 +905,7 @@ class RipCore(list): fg="blue", ) - self.config.file["deezer"]["arl"] = input( - style("ARL: ", fg="green") - ) + self.config.file["deezer"]["arl"] = input(style("ARL: ", fg="green")) self.config.save() secho( f'Credentials saved to config file at "{self.config._path}"', diff --git a/rip/db.py b/rip/db.py index fbbe35c..6125241 100644 --- a/rip/db.py +++ b/rip/db.py @@ -71,15 +71,11 @@ class Database: with sqlite3.connect(self.path) as conn: conditions = " AND ".join(f"{key}=?" for key in items.keys()) - command = ( - f"SELECT EXISTS(SELECT 1 FROM {self.name} WHERE {conditions})" - ) + command = f"SELECT EXISTS(SELECT 1 FROM {self.name} WHERE {conditions})" logger.debug("Executing %s", command) - return bool( - conn.execute(command, tuple(items.values())).fetchone()[0] - ) + return bool(conn.execute(command, tuple(items.values())).fetchone()[0]) def __contains__(self, keys: Union[str, dict]) -> bool: """Check whether a key-value pair exists in the database. @@ -123,9 +119,7 @@ class Database: params = ", ".join(self.structure.keys()) question_marks = ", ".join("?" for _ in items) - command = ( - f"INSERT INTO {self.name} ({params}) VALUES ({question_marks})" - ) + command = f"INSERT INTO {self.name} ({params}) VALUES ({question_marks})" logger.debug("Executing %s", command) logger.debug("Items to add: %s", items) diff --git a/streamrip/clients.py b/streamrip/clients.py index 75ae0bd..490e558 100644 --- a/streamrip/clients.py +++ b/streamrip/clients.py @@ -21,12 +21,9 @@ from .constants import ( DEEZER_BASE, DEEZER_DL, DEEZER_FORMATS, - DEEZER_MAX_Q, QOBUZ_BASE, QOBUZ_FEATURED_KEYS, - SOUNDCLOUD_APP_VERSION, SOUNDCLOUD_BASE, - SOUNDCLOUD_CLIENT_ID, SOUNDCLOUD_USER_ID, TIDAL_AUTH_URL, TIDAL_BASE, @@ -240,9 +237,7 @@ class QobuzClient(Client): :rtype: dict """ page, status_code = self._api_request(epoint, params) - logger.debug( - "Keys returned from _gen_pages: %s", ", ".join(page.keys()) - ) + logger.debug("Keys returned from _gen_pages: %s", ", ".join(page.keys())) key = epoint.split("/")[0] + "s" total = page.get(key, {}) total = total.get("total") or total.get("items") @@ -265,8 +260,7 @@ class QobuzClient(Client): """Check if the secrets are usable.""" with concurrent.futures.ThreadPoolExecutor() as executor: futures = [ - executor.submit(self._test_secret, secret) - for secret in self.secrets + executor.submit(self._test_secret, secret) for secret in self.secrets ] for future in concurrent.futures.as_completed(futures): @@ -309,15 +303,11 @@ class QobuzClient(Client): response, status_code = self._api_request(epoint, params) if status_code != 200: - raise Exception( - f'Error fetching metadata. "{response["message"]}"' - ) + raise Exception(f'Error fetching metadata. "{response["message"]}"') return response - def _api_search( - self, query: str, media_type: str, limit: int = 500 - ) -> Generator: + def _api_search(self, query: str, media_type: str, limit: int = 500) -> Generator: """Send a search request to the API. :param query: @@ -369,9 +359,7 @@ class QobuzClient(Client): resp, status_code = self._api_request(epoint, params) if status_code == 401: - raise AuthenticationError( - f"Invalid credentials from params {params}" - ) + raise AuthenticationError(f"Invalid credentials from params {params}") elif status_code == 400: logger.debug(resp) raise InvalidAppIdError(f"Invalid app id from params {params}") @@ -379,9 +367,7 @@ class QobuzClient(Client): logger.info("Logged in to Qobuz") if not resp["user"]["credential"]["parameters"]: - raise IneligibleError( - "Free accounts are not eligible to download tracks." - ) + raise IneligibleError("Free accounts are not eligible to download tracks.") self.uat = resp["user_auth_token"] self.session.headers.update({"X-User-Auth-Token": self.uat}) @@ -430,9 +416,7 @@ class QobuzClient(Client): } response, status_code = self._api_request("track/getFileUrl", params) if status_code == 400: - raise InvalidAppSecretError( - "Invalid app secret from params %s" % params - ) + raise InvalidAppSecretError("Invalid app secret from params %s" % params) return response @@ -451,9 +435,7 @@ class QobuzClient(Client): logger.debug(r.text) return r.json(), r.status_code except Exception: - logger.error( - "Problem getting JSON. Status code: %s", r.status_code - ) + logger.error("Problem getting JSON. Status code: %s", r.status_code) raise def _test_secret(self, secret: str) -> Optional[str]: @@ -485,9 +467,7 @@ class DeezerClient(Client): # no login required self.logged_in = False - def search( - self, query: str, media_type: str = "album", limit: int = 200 - ) -> dict: + def search(self, query: str, media_type: str = "album", limit: int = 200) -> dict: """Search API for query. :param query: @@ -501,16 +481,12 @@ class DeezerClient(Client): try: if media_type == "featured": if query: - search_function = getattr( - self.client.api, f"get_editorial_{query}" - ) + search_function = getattr(self.client.api, f"get_editorial_{query}") else: search_function = self.client.api.get_editorial_releases else: - search_function = getattr( - self.client.api, f"search_{media_type}" - ) + search_function = getattr(self.client.api, f"search_{media_type}") except AttributeError: raise Exception @@ -584,9 +560,9 @@ class DeezerClient(Client): format_no, format_str = format_info dl_info["size_to_quality"] = { - int( - track_info.get(f"FILESIZE_{format}") - ): self._quality_id_from_filetype(format) + int(track_info.get(f"FILESIZE_{format}")): self._quality_id_from_filetype( + format + ) for format in DEEZER_FORMATS } @@ -627,9 +603,7 @@ class DeezerClient(Client): logger.debug("Info bytes: %s", info_bytes) path = self._gen_url_path(info_bytes) logger.debug(path) - return ( - f"https://e-cdns-proxy-{track_hash[0]}.dzcdn.net/mobile/1/{path}" - ) + return f"https://e-cdns-proxy-{track_hash[0]}.dzcdn.net/mobile/1/{path}" def _gen_url_path(self, data): return binascii.hexlify( @@ -659,9 +633,7 @@ class DeezloaderClient(Client): # no login required self.logged_in = True - def search( - self, query: str, media_type: str = "album", limit: int = 200 - ) -> dict: + def search(self, query: str, media_type: str = "album", limit: int = 200) -> dict: """Search API for query. :param query: @@ -698,9 +670,7 @@ class DeezloaderClient(Client): url = f"{DEEZER_BASE}/{media_type}/{meta_id}" item = self.session.get(url).json() if media_type in ("album", "playlist"): - tracks = self.session.get( - f"{url}/tracks", params={"limit": 1000} - ).json() + tracks = self.session.get(f"{url}/tracks", params={"limit": 1000}).json() item["tracks"] = tracks["data"] item["track_total"] = len(tracks["data"]) elif media_type == "artist": @@ -796,9 +766,7 @@ class TidalClient(Client): logger.debug(resp) return resp - def search( - self, query: str, media_type: str = "album", limit: int = 100 - ) -> dict: + def search(self, query: str, media_type: str = "album", limit: int = 100) -> dict: """Search for a query. :param query: @@ -827,19 +795,13 @@ class TidalClient(Client): return self._get_video_stream_url(track_id) params = { - "audioquality": get_quality( - min(quality, TIDAL_MAX_Q), self.source - ), + "audioquality": get_quality(min(quality, TIDAL_MAX_Q), self.source), "playbackmode": "STREAM", "assetpresentation": "FULL", } - resp = self._api_request( - f"tracks/{track_id}/playbackinfopostpaywall", params - ) + resp = self._api_request(f"tracks/{track_id}/playbackinfopostpaywall", params) try: - manifest = json.loads( - base64.b64decode(resp["manifest"]).decode("utf-8") - ) + manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8")) except KeyError: raise Exception(resp["userMessage"]) @@ -1044,9 +1006,7 @@ class TidalClient(Client): offset += 100 tracks_left -= 100 resp["items"].extend( - self._api_request(f"{url}/items", {"offset": offset})[ - "items" - ] + self._api_request(f"{url}/items", {"offset": offset})["items"] ) item["tracks"] = [item["item"] for item in resp["items"]] @@ -1096,9 +1056,7 @@ class TidalClient(Client): resp = self._api_request( f"videos/{video_id}/playbackinfopostpaywall", params=params ) - manifest = json.loads( - base64.b64decode(resp["manifest"]).decode("utf-8") - ) + manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8")) available_urls = self.session.get(manifest["urls"][0]) available_urls.encoding = "utf-8" diff --git a/streamrip/converter.py b/streamrip/converter.py index e31edfe..f390471 100644 --- a/streamrip/converter.py +++ b/streamrip/converter.py @@ -52,9 +52,7 @@ class Converter: self.filename = filename self.final_fn = f"{os.path.splitext(filename)[0]}.{self.container}" - self.tempfile = os.path.join( - gettempdir(), os.path.basename(self.final_fn) - ) + self.tempfile = os.path.join(gettempdir(), os.path.basename(self.final_fn)) self.remove_source = remove_source self.sampling_rate = sampling_rate self.bit_depth = bit_depth @@ -119,13 +117,9 @@ class Converter: if self.lossless: if isinstance(self.sampling_rate, int): sampling_rates = "|".join( - str(rate) - for rate in SAMPLING_RATES - if rate <= self.sampling_rate - ) - command.extend( - ["-af", f"aformat=sample_rates={sampling_rates}"] + str(rate) for rate in SAMPLING_RATES if rate <= self.sampling_rate ) + command.extend(["-af", f"aformat=sample_rates={sampling_rates}"]) elif self.sampling_rate is not None: raise TypeError( @@ -140,9 +134,7 @@ class Converter: else: raise ValueError("Bit depth must be 16, 24, or 32") elif self.bit_depth is not None: - raise TypeError( - f"Bit depth must be int, not {type(self.bit_depth)}" - ) + raise TypeError(f"Bit depth must be int, not {type(self.bit_depth)}") # automatically overwrite command.extend(["-y", self.tempfile]) @@ -207,9 +199,7 @@ class Vorbis(Converter): codec_name = "vorbis" codec_lib = "libvorbis" container = "ogg" - default_ffmpeg_arg = ( - "-q:a 6" # 160, aka the "high" quality profile from Spotify - ) + default_ffmpeg_arg = "-q:a 6" # 160, aka the "high" quality profile from Spotify class OPUS(Converter): diff --git a/streamrip/downloadtools.py b/streamrip/downloadtools.py index e35917a..24e735f 100644 --- a/streamrip/downloadtools.py +++ b/streamrip/downloadtools.py @@ -74,9 +74,7 @@ class DownloadStream: info = self.request.json() try: # Usually happens with deezloader downloads - raise NonStreamable( - f"{info['error']} -- {info['message']}" - ) + raise NonStreamable(f"{info['error']} - {info['message']}") except KeyError: raise NonStreamable(info) @@ -88,10 +86,7 @@ class DownloadStream: :rtype: Iterator """ - if ( - self.source == "deezer" - and self.is_encrypted.search(self.url) is not None - ): + if self.source == "deezer" and self.is_encrypted.search(self.url) is not None: assert isinstance(self.id, str), self.id blowfish_key = self._generate_blowfish_key(self.id) @@ -99,10 +94,7 @@ class DownloadStream: CHUNK_SIZE = 2048 * 3 return ( # (decryptor.decrypt(chunk[:2048]) + chunk[2048:]) - ( - self._decrypt_chunk(blowfish_key, chunk[:2048]) - + chunk[2048:] - ) + (self._decrypt_chunk(blowfish_key, chunk[:2048]) + chunk[2048:]) if len(chunk) >= 2048 else chunk for chunk in self.request.iter_content(CHUNK_SIZE) @@ -123,9 +115,7 @@ class DownloadStream: return self.file_size def _create_deezer_decryptor(self, key) -> Blowfish: - return Blowfish.new( - key, Blowfish.MODE_CBC, b"\x00\x01\x02\x03\x04\x05\x06\x07" - ) + return Blowfish.new(key, Blowfish.MODE_CBC, b"\x00\x01\x02\x03\x04\x05\x06\x07") @staticmethod def _generate_blowfish_key(track_id: str): @@ -178,9 +168,7 @@ class DownloadPool: self.tempdir = tempdir async def getfn(self, url): - path = os.path.join( - self.tempdir, f"__streamrip_partial_{abs(hash(url))}" - ) + path = os.path.join(self.tempdir, f"__streamrip_partial_{abs(hash(url))}") self._paths[url] = path return path @@ -195,9 +183,7 @@ class DownloadPool: async def _download_url(self, session, url): filename = await self.getfn(url) logger.debug("Downloading %s", url) - async with session.get(url) as response, aiofiles.open( - filename, "wb" - ) as f: + async with session.get(url) as response, aiofiles.open(filename, "wb") as f: # without aiofiles 3.6632679780000004s # with aiofiles 2.504482839s await f.write(await response.content.read()) @@ -215,9 +201,7 @@ class DownloadPool: def files(self): if len(self._paths) != len(self.urls): # Not all of them have downloaded - raise Exception( - "Must run DownloadPool.download() before accessing files" - ) + raise Exception("Must run DownloadPool.download() before accessing files") return [ os.path.join(self.tempdir, self._paths[self.urls[i]]) diff --git a/streamrip/media.py b/streamrip/media.py index 48f161f..f84bfa3 100644 --- a/streamrip/media.py +++ b/streamrip/media.py @@ -68,9 +68,7 @@ logger = logging.getLogger("streamrip") TYPE_REGEXES = { "remaster": re.compile(r"(?i)(re)?master(ed)?"), - "extra": re.compile( - r"(?i)(anniversary|deluxe|live|collector|demo|expanded)" - ), + "extra": re.compile(r"(?i)(anniversary|deluxe|live|collector|demo|expanded)"), } @@ -270,9 +268,7 @@ class Track(Media): except ItemExists as e: logger.debug(e) - self.path = os.path.join( - gettempdir(), f"{hash(self.id)}_{self.quality}.tmp" - ) + self.path = os.path.join(gettempdir(), f"{hash(self.id)}_{self.quality}.tmp") def download( # noqa self, @@ -327,14 +323,9 @@ class Track(Media): except KeyError as e: if restrictions := dl_info["restrictions"]: # Turn CamelCase code into a readable sentence - words = re.findall( - r"([A-Z][a-z]+)", restrictions[0]["code"] - ) + words = re.findall(r"([A-Z][a-z]+)", restrictions[0]["code"]) raise NonStreamable( - words[0] - + " " - + " ".join(map(str.lower, words[1:])) - + "." + words[0] + " " + " ".join(map(str.lower, words[1:])) + "." ) secho(f"Panic: {e} dl_info = {dl_info}", fg="red") @@ -343,9 +334,7 @@ class Track(Media): _quick_download(download_url, self.path, desc=self._progress_desc) elif isinstance(self.client, DeezloaderClient): - _quick_download( - dl_info["url"], self.path, desc=self._progress_desc - ) + _quick_download(dl_info["url"], self.path, desc=self._progress_desc) elif self.client.source == "deezer": # We can only find out if the requested quality is available @@ -457,13 +446,9 @@ class Track(Media): parsed_m3u = m3u8.loads(requests.get(dl_info["url"]).text) self.path += ".mp3" - with DownloadPool( - segment.uri for segment in parsed_m3u.segments - ) as pool: + with DownloadPool(segment.uri for segment in parsed_m3u.segments) as pool: - bar = get_tqdm_bar( - len(pool), desc=self._progress_desc, unit="Chunk" - ) + bar = get_tqdm_bar(len(pool), desc=self._progress_desc, unit="Chunk") def update_tqdm_bar(): bar.update(1) @@ -483,9 +468,7 @@ class Track(Media): ) elif dl_info["type"] == "original": - _quick_download( - dl_info["url"], self.path, desc=self._progress_desc - ) + _quick_download(dl_info["url"], self.path, desc=self._progress_desc) # if a wav is returned, convert to flac engine = converter.FLAC(self.path) @@ -513,9 +496,7 @@ class Track(Media): def download_cover(self, width=999999, height=999999): """Download the cover art, if cover_url is given.""" - self.cover_path = os.path.join( - gettempdir(), f"cover{hash(self.cover_url)}.jpg" - ) + self.cover_path = os.path.join(gettempdir(), f"cover{hash(self.cover_url)}.jpg") logger.debug("Downloading cover from %s", self.cover_url) if not os.path.exists(self.cover_path): @@ -535,9 +516,9 @@ class Track(Media): formatter = self.meta.get_formatter(max_quality=self.quality) logger.debug("Track meta formatter %s", formatter) filename = clean_format(self.file_format, formatter, restrict=restrict) - self.final_path = os.path.join(self.folder, filename)[ - :250 - ].strip() + ext(self.quality, self.client.source) + self.final_path = os.path.join(self.folder, filename)[:250].strip() + ext( + self.quality, self.client.source + ) logger.debug("Formatted path: %s", self.final_path) @@ -550,9 +531,7 @@ class Track(Media): return self.final_path @classmethod - def from_album_meta( - cls, album: TrackMetadata, track: dict, client: Client - ): + def from_album_meta(cls, album: TrackMetadata, track: dict, client: Client): """Return a new Track object initialized with info. :param album: album metadata returned by API @@ -562,9 +541,7 @@ class Track(Media): :raises: IndexError """ meta = TrackMetadata(album=album, track=track, source=client.source) - return cls( - client=client, meta=meta, id=track["id"], part_of_tracklist=True - ) + return cls(client=client, meta=meta, id=track["id"], part_of_tracklist=True) @classmethod def from_api(cls, item: dict, client: Client): @@ -624,9 +601,7 @@ class Track(Media): :param embed_cover: Embed cover art into file :type embed_cover: bool """ - assert isinstance( - self.meta, TrackMetadata - ), "meta must be TrackMetadata" + assert isinstance(self.meta, TrackMetadata), "meta must be TrackMetadata" if not self.downloaded: logger.info( "Track %s not tagged because it was not downloaded", @@ -750,9 +725,7 @@ class Track(Media): self.format_final_path(kwargs.get("restrict_filenames", False)) if not os.path.isfile(self.path): - logger.info( - "File %s does not exist. Skipping conversion.", self.path - ) + logger.info("File %s does not exist. Skipping conversion.", self.path) secho(f"{self!s} does not exist. Skipping conversion.", fg="red") return @@ -892,12 +865,8 @@ class Video(Media): parsed_m3u = m3u8.loads(requests.get(url).text) # Asynchronously download the streams - with DownloadPool( - segment.uri for segment in parsed_m3u.segments - ) as pool: - bar = get_tqdm_bar( - len(pool), desc=self._progress_desc, unit="Chunk" - ) + with DownloadPool(segment.uri for segment in parsed_m3u.segments) as pool: + bar = get_tqdm_bar(len(pool), desc=self._progress_desc, unit="Chunk") def update_tqdm_bar(): bar.update(1) @@ -906,9 +875,7 @@ class Video(Media): # Put the filenames in a tempfile that ffmpeg # can read from - file_list_path = os.path.join( - gettempdir(), "__streamrip_video_files" - ) + file_list_path = os.path.join(gettempdir(), "__streamrip_video_files") with open(file_list_path, "w") as file_list: text = "\n".join(f"file '{path}'" for path in pool.files) file_list.write(text) @@ -1149,9 +1116,7 @@ class Booklet: :type parent_folder: str :param kwargs: """ - fn = clean_filename( - self.description, restrict=kwargs.get("restrict_filenames") - ) + fn = clean_filename(self.description, restrict=kwargs.get("restrict_filenames")) filepath = os.path.join(parent_folder, f"{fn}.pdf") _quick_download(self.url, filepath, "Booklet") @@ -1206,8 +1171,7 @@ class Tracklist(list): kwargs.get("max_connections", 3) ) as executor: future_map = { - executor.submit(target, item, **kwargs): item - for item in self + executor.submit(target, item, **kwargs): item for item in self } try: concurrent.futures.wait(future_map.keys()) @@ -1238,9 +1202,7 @@ class Tracklist(list): secho(f"{item!s} exists. Skipping.", fg="yellow") except NonStreamable as e: e.print(item) - failed_downloads.append( - (item.client.source, item.type, item.id) - ) + failed_downloads.append((item.client.source, item.type, item.id)) self.downloaded = True @@ -1596,9 +1558,7 @@ class Album(Tracklist, Media): and isinstance(item, Track) and kwargs.get("folder_format") ): - disc_folder = os.path.join( - self.folder, f"Disc {item.meta.discnumber}" - ) + disc_folder = os.path.join(self.folder, f"Disc {item.meta.discnumber}") kwargs["parent_folder"] = disc_folder else: kwargs["parent_folder"] = self.folder @@ -1684,9 +1644,7 @@ class Album(Tracklist, Media): logger.debug("Formatter: %s", fmt) return fmt - def _get_formatted_folder( - self, parent_folder: str, restrict: bool = False - ) -> str: + def _get_formatted_folder(self, parent_folder: str, restrict: bool = False) -> str: """Generate the folder name for this album. :param parent_folder: @@ -1818,9 +1776,7 @@ class Playlist(Tracklist, Media): if self.client.source == "qobuz": self.name = self.meta["name"] self.image = self.meta["images"] - self.creator = safe_get( - self.meta, "owner", "name", default="Qobuz" - ) + self.creator = safe_get(self.meta, "owner", "name", default="Qobuz") tracklist = self.meta["tracks"]["items"] @@ -1830,9 +1786,7 @@ class Playlist(Tracklist, Media): elif self.client.source == "tidal": self.name = self.meta["title"] self.image = tidal_cover_url(self.meta["image"], 640) - self.creator = safe_get( - self.meta, "creator", "name", default="TIDAL" - ) + self.creator = safe_get(self.meta, "creator", "name", default="TIDAL") tracklist = self.meta["tracks"] @@ -1845,9 +1799,7 @@ class Playlist(Tracklist, Media): elif self.client.source == "deezer": self.name = self.meta["title"] self.image = self.meta["picture_big"] - self.creator = safe_get( - self.meta, "creator", "name", default="Deezer" - ) + self.creator = safe_get(self.meta, "creator", "name", default="Deezer") tracklist = self.meta["tracks"] @@ -1888,13 +1840,9 @@ class Playlist(Tracklist, Media): logger.debug("Loaded %d tracks from playlist %s", len(self), self.name) - def _prepare_download( - self, parent_folder: str = "StreamripDownloads", **kwargs - ): + def _prepare_download(self, parent_folder: str = "StreamripDownloads", **kwargs): if kwargs.get("folder_format"): - fname = clean_filename( - self.name, kwargs.get("restrict_filenames", False) - ) + fname = clean_filename(self.name, kwargs.get("restrict_filenames", False)) self.folder = os.path.join(parent_folder, fname) else: self.folder = parent_folder @@ -2091,9 +2039,7 @@ class Artist(Tracklist, Media): :rtype: Iterable """ if kwargs.get("folder_format"): - folder = clean_filename( - self.name, kwargs.get("restrict_filenames", False) - ) + folder = clean_filename(self.name, kwargs.get("restrict_filenames", False)) self.folder = os.path.join(parent_folder, folder) else: self.folder = parent_folder @@ -2110,9 +2056,7 @@ class Artist(Tracklist, Media): final = self if isinstance(filters, tuple) and self.client.source == "qobuz": - filter_funcs = ( - getattr(self, f"_{filter_}") for filter_ in filters - ) + filter_funcs = (getattr(self, f"_{filter_}") for filter_ in filters) for func in filter_funcs: final = filter(func, final) @@ -2225,10 +2169,7 @@ class Artist(Tracklist, Media): 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 - ): + if album["bit_depth"] == best_bd and album["sampling_rate"] == best_sr: yield album break diff --git a/streamrip/metadata.py b/streamrip/metadata.py index 900c3b0..86caba5 100644 --- a/streamrip/metadata.py +++ b/streamrip/metadata.py @@ -132,9 +132,7 @@ class TrackMetadata: self.album = resp.get("title", "Unknown Album") self.tracktotal = resp.get("tracks_count", 1) self.genre = resp.get("genres_list") or resp.get("genre") or [] - self.date = resp.get("release_date_original") or resp.get( - "release_date" - ) + self.date = resp.get("release_date_original") or resp.get("release_date") self.copyright = resp.get("copyright") if artists := resp.get("artists"): @@ -148,9 +146,7 @@ class TrackMetadata: self.disctotal = ( max( track.get("media_number", 1) - for track in safe_get( - resp, "tracks", "items", default=[{}] - ) + for track in safe_get(resp, "tracks", "items", default=[{}]) ) or 1 ) @@ -191,22 +187,14 @@ class TrackMetadata: self.streamable = resp.get("allowStreaming", False) self.id = resp.get("id") - if q := resp.get( - "audioQuality" - ): # for album entries in single tracks + if q := resp.get("audioQuality"): # for album entries in single tracks self._get_tidal_quality(q) elif self.__source == "deezer": self.album = resp.get("title", "Unknown Album") - self.tracktotal = resp.get("track_total", 0) or resp.get( - "nb_tracks", 0 - ) + self.tracktotal = resp.get("track_total", 0) or resp.get("nb_tracks", 0) self.disctotal = ( - max( - track.get("disk_number") - for track in resp.get("tracks", [{}]) - ) - or 1 + max(track.get("disk_number") for track in resp.get("tracks", [{}])) or 1 ) self.genre = safe_get(resp, "genres", "data") self.date = resp.get("release_date") @@ -365,9 +353,7 @@ class TrackMetadata: if isinstance(self._genres, list): if self.__source == "qobuz": - genres: Iterable = re.findall( - r"([^\u2192\/]+)", "/".join(self._genres) - ) + genres: Iterable = re.findall(r"([^\u2192\/]+)", "/".join(self._genres)) genres = set(genres) elif self.__source == "deezer": genres = (g["name"] for g in self._genres) @@ -401,9 +387,7 @@ class TrackMetadata: if hasattr(self, "_copyright"): if self._copyright is None: return None - copyright: str = re.sub( - r"(?i)\(P\)", PHON_COPYRIGHT, self._copyright - ) + copyright: str = re.sub(r"(?i)\(P\)", PHON_COPYRIGHT, self._copyright) copyright = re.sub(r"(?i)\(C\)", COPYRIGHT, copyright) return copyright @@ -463,9 +447,7 @@ class TrackMetadata: formatter["sampling_rate"] /= 1000 return formatter - def tags( - self, container: str = "flac", exclude: Optional[set] = None - ) -> Generator: + def tags(self, container: str = "flac", exclude: Optional[set] = None) -> Generator: """Create a generator of key, value pairs for use with mutagen. The *_KEY dicts are organized in the format: @@ -623,9 +605,7 @@ class TrackMetadata: :rtype: int """ - return sum( - hash(v) for v in self.asdict().values() if isinstance(v, Hashable) - ) + return sum(hash(v) for v in self.asdict().values() if isinstance(v, Hashable)) def __repr__(self) -> str: """Return the string representation of the metadata object. diff --git a/streamrip/utils.py b/streamrip/utils.py index 5412b78..874b8ba 100644 --- a/streamrip/utils.py +++ b/streamrip/utils.py @@ -84,9 +84,7 @@ __QUALITY_MAP: Dict[str, Dict[int, Union[int, str, Tuple[int, str]]]] = { } -def get_quality( - quality_id: int, source: str -) -> Union[str, int, Tuple[int, str]]: +def get_quality(quality_id: int, source: str) -> Union[str, int, Tuple[int, str]]: """Get the source-specific quality id. :param quality_id: the universal quality id (0, 1, 2, 4) @@ -156,9 +154,7 @@ def clean_format(formatter: str, format_info, restrict: bool = False): clean_dict = dict() for key in fmt_keys: if isinstance(format_info.get(key), (str, float)): - clean_dict[key] = clean_filename( - str(format_info[key]), restrict=restrict - ) + clean_dict[key] = clean_filename(str(format_info[key]), restrict=restrict) elif isinstance(format_info.get(key), int): # track/discnumber clean_dict[key] = f"{format_info[key]:02}" else: @@ -176,9 +172,7 @@ 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 - ) + return TIDAL_COVER_URL.format(uuid=uuid.replace("-", "/"), height=size, width=size) def init_log(path: Optional[str] = None, level: str = "DEBUG"): @@ -280,9 +274,7 @@ def gen_threadsafe_session( headers = {} session = requests.Session() - adapter = requests.adapters.HTTPAdapter( - pool_connections=100, pool_maxsize=100 - ) + adapter = requests.adapters.HTTPAdapter(pool_connections=100, pool_maxsize=100) session.mount("https://", adapter) session.headers.update(headers) return session @@ -350,9 +342,7 @@ def get_cover_urls(resp: dict, source: str) -> dict: "picture_xl", ) cover_urls = { - sk: resp.get( - rk, resp.get(rkf) - ) # size key, resp key, resp key fallback + sk: resp.get(rk, resp.get(rkf)) # size key, resp key, resp key fallback for sk, rk, rkf in zip( COVER_SIZES, resp_keys, @@ -367,9 +357,9 @@ def get_cover_urls(resp: dict, source: str) -> dict: return cover_urls if source == "soundcloud": - cover_url = ( - resp["artwork_url"] or resp["user"].get("avatar_url") - ).replace("large", "t500x500") + cover_url = (resp["artwork_url"] or resp["user"].get("avatar_url")).replace( + "large", "t500x500" + ) cover_urls = {"large": cover_url} diff --git a/tests/test_download.py b/tests/test_download.py index 7475c1b..becee0f 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -8,10 +8,7 @@ from streamrip.downloadtools import DownloadPool def test_downloadpool(tmpdir): start = time.perf_counter() with DownloadPool( - ( - f"https://pokeapi.co/api/v2/pokemon/{number}" - for number in range(1, 151) - ), + (f"https://pokeapi.co/api/v2/pokemon/{number}" for number in range(1, 151)), tempdir=tmpdir, ) as pool: pool.download() From f222b32f7214de74145166e2272a748a834f7818 Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Fri, 17 Sep 2021 15:13:39 -0700 Subject: [PATCH 09/14] Raise for status in TidalClient --- streamrip/clients.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/streamrip/clients.py b/streamrip/clients.py index 490e558..1b27fa5 100644 --- a/streamrip/clients.py +++ b/streamrip/clients.py @@ -1020,6 +1020,7 @@ class TidalClient(Client): item["albums"] = album_resp["items"] item["albums"].extend(ep_resp["items"]) + logger.debug(item) return item def _api_request(self, path: str, params=None) -> dict: @@ -1036,7 +1037,7 @@ class TidalClient(Client): params["countryCode"] = self.country_code params["limit"] = 100 r = self.session.get(f"{TIDAL_BASE}/{path}", params=params) - # r.raise_for_status() + r.raise_for_status() return r.json() def _get_video_stream_url(self, video_id: str) -> str: From fc9dfe15aa04044d6d099a412cc650f7ba875e67 Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Sat, 18 Sep 2021 18:32:01 -0700 Subject: [PATCH 10/14] Fix lastfm crash when no results found #184 --- rip/core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rip/core.py b/rip/core.py index 9c229b6..134d7c2 100644 --- a/rip/core.py +++ b/rip/core.py @@ -674,8 +674,10 @@ class RipCore(list): or results.get("collection") or results.get("albums", {}).get("data", False) ) - if items is None: + + if not items: raise NoResultsFound(query) + logger.debug("Number of results: %d", len(items)) for i, item in enumerate(items): From 8f722dcd603d1c6f8e4790026925df2328cee732 Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Thu, 23 Sep 2021 13:34:32 -0700 Subject: [PATCH 11/14] Fix #188 --- streamrip/clients.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/streamrip/clients.py b/streamrip/clients.py index 1b27fa5..d7d8287 100644 --- a/streamrip/clients.py +++ b/streamrip/clients.py @@ -461,7 +461,7 @@ class DeezerClient(Client): def __init__(self): """Create a DeezerClient.""" - self.client = deezer.Deezer(accept_language="en-US,en;q=0.5") + self.client = deezer.Deezer() # self.session = gen_threadsafe_session() # no login required From cdacf644217ee7b3457698f29f85c2b242c0153e Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Thu, 23 Sep 2021 13:34:48 -0700 Subject: [PATCH 12/14] Update dependencies --- poetry.lock | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0824a4b..c8d351f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -122,7 +122,7 @@ lxml = ["lxml"] [[package]] name = "black" -version = "21.8b0" +version = "21.9b0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -184,7 +184,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "charset-normalizer" -version = "2.0.4" +version = "2.0.6" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -242,7 +242,7 @@ python-versions = ">=3.5" [[package]] name = "deezer-py" -version = "1.2.2" +version = "1.2.3" description = "A wrapper for all Deezer's APIs" category = "main" optional = false @@ -615,7 +615,7 @@ python-versions = ">=3.6" [[package]] name = "sphinx" -version = "4.1.2" +version = "4.2.0" description = "Python documentation generator" category = "dev" optional = false @@ -741,7 +741,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "tqdm" -version = "4.62.2" +version = "4.62.3" description = "Fast, Extensible Progress Meter" category = "main" optional = false @@ -781,7 +781,7 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.6" +version = "1.26.7" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -938,8 +938,8 @@ beautifulsoup4 = [ {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, ] black = [ - {file = "black-21.8b0-py3-none-any.whl", hash = "sha256:2a0f9a8c2b2a60dbcf1ccb058842fb22bdbbcb2f32c6cc02d9578f90b92ce8b7"}, - {file = "black-21.8b0.tar.gz", hash = "sha256:570608d28aa3af1792b98c4a337dbac6367877b47b12b88ab42095cfc1a627c2"}, + {file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"}, + {file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"}, ] cchardet = [ {file = "cchardet-2.1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6f70139aaf47ffb94d89db603af849b82efdf756f187cdd3e566e30976c519f"}, @@ -1028,8 +1028,8 @@ chardet = [ {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, - {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, + {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"}, + {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"}, ] cleo = [ {file = "cleo-1.0.0a3-py3-none-any.whl", hash = "sha256:46b2f970d06caa311d1e12a1013b0ce2a8149502669ac82cbedafb9e0bfdbccd"}, @@ -1052,8 +1052,7 @@ decorator = [ {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, ] deezer-py = [ - {file = "deezer-py-1.2.2.tar.gz", hash = "sha256:a491af5fcc9e44a2a28be8832169e703a920dae42c78539f45cad59075700ac9"}, - {file = "deezer_py-1.2.2-py3-none-any.whl", hash = "sha256:121d5f7c1bd630ca48ec2d5a497f966aca629a124eb0c7ddda7bbae52a3f0135"}, + {file = "deezer-py-1.2.3.tar.gz", hash = "sha256:f4dd648e5bf251cb13316145e243d3a08d870840e0ac1525309926e640c91ea9"}, ] docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, @@ -1420,8 +1419,8 @@ soupsieve = [ {file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"}, ] sphinx = [ - {file = "Sphinx-4.1.2-py3-none-any.whl", hash = "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544"}, - {file = "Sphinx-4.1.2.tar.gz", hash = "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13"}, + {file = "Sphinx-4.2.0-py3-none-any.whl", hash = "sha256:98a535c62a4fcfcc362528592f69b26f7caec587d32cd55688db580be0287ae0"}, + {file = "Sphinx-4.2.0.tar.gz", hash = "sha256:94078db9184491e15bce0a56d9186e0aec95f16ac20b12d00e06d4e36f1058a6"}, ] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, @@ -1460,8 +1459,8 @@ tomlkit = [ {file = "tomlkit-0.7.2.tar.gz", hash = "sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754"}, ] tqdm = [ - {file = "tqdm-4.62.2-py2.py3-none-any.whl", hash = "sha256:80aead664e6c1672c4ae20dc50e1cdc5e20eeff9b14aa23ecd426375b28be588"}, - {file = "tqdm-4.62.2.tar.gz", hash = "sha256:a4d6d112e507ef98513ac119ead1159d286deab17dffedd96921412c2d236ff5"}, + {file = "tqdm-4.62.3-py2.py3-none-any.whl", hash = "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c"}, + {file = "tqdm-4.62.3.tar.gz", hash = "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"}, ] types-click = [ {file = "types-click-7.1.5.tar.gz", hash = "sha256:ced04123d857f5b05df7c6b10e9710e66cf2ec7d99d4ee48d04a07f4c1a83ad4"}, @@ -1477,8 +1476,8 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] urllib3 = [ - {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, - {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] waitress = [ {file = "waitress-2.0.0-py3-none-any.whl", hash = "sha256:29af5a53e9fb4e158f525367678b50053808ca6c21ba585754c77d790008c746"}, From 5c654ed61206c25ee878f4005dbe10fcd5a0fa6f Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Sat, 25 Sep 2021 16:10:47 -0700 Subject: [PATCH 13/14] Don't index title when searching for artist #189 --- rip/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rip/core.py b/rip/core.py index 134d7c2..0a53807 100644 --- a/rip/core.py +++ b/rip/core.py @@ -681,7 +681,7 @@ class RipCore(list): logger.debug("Number of results: %d", len(items)) for i, item in enumerate(items): - logger.debug(item["title"]) + logger.debug(item) yield MEDIA_CLASS[media_type].from_api(item, client) # type: ignore if i >= limit - 1: return From 35262cb6fab7e5eeef1ef889e075b38f2fb8f4d3 Mon Sep 17 00:00:00 2001 From: Nathan Thomas Date: Sun, 26 Sep 2021 16:39:01 -0700 Subject: [PATCH 14/14] Bump version --- pyproject.toml | 2 +- streamrip/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9b38e69..6c7cd89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "streamrip" -version = "1.5" +version = "1.6" description = "A fast, all-in-one music ripper for Qobuz, Deezer, Tidal, and SoundCloud" authors = ["nathom "] license = "GPL-3.0-only" diff --git a/streamrip/__init__.py b/streamrip/__init__.py index f6cceb2..175a1f7 100644 --- a/streamrip/__init__.py +++ b/streamrip/__init__.py @@ -1,5 +1,5 @@ """streamrip: the all in one music downloader.""" -__version__ = "1.5" +__version__ = "1.6" from . import clients, constants, converter, downloadtools, media