From 03f824175ec68f6a342b01907c20d1d58426af03 Mon Sep 17 00:00:00 2001 From: omnunum <3474285+omnunum@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:28:09 -0400 Subject: [PATCH 1/9] Fallback to lower quality on Deezer when requested quality is not available. --- pyproject.toml | 2 +- streamrip/__init__.py | 2 +- streamrip/client/deezer.py | 14 ++- streamrip/config.py | 4 +- streamrip/config.toml | 5 +- tests/config2.toml | 195 +++++++++++++++++++++++++++++++++++++ tests/test_config.py | 1 + tests/test_config.toml | 4 +- 8 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 tests/config2.toml diff --git a/pyproject.toml b/pyproject.toml index 3f63e8b2..307cf5a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "streamrip" -version = "2.1.0" +version = "2.2.0" description = "A fast, all-in-one music downloader for Qobuz, Deezer, Tidal, and SoundCloud" authors = ["nathom "] license = "GPL-3.0-only" diff --git a/streamrip/__init__.py b/streamrip/__init__.py index c88eefa0..08d9db88 100644 --- a/streamrip/__init__.py +++ b/streamrip/__init__.py @@ -2,4 +2,4 @@ from .config import Config __all__ = ["Config", "converter", "db", "exceptions", "media", "metadata"] -__version__ = "2.1.0" +__version__ = "2.2.0" diff --git a/streamrip/client/deezer.py b/streamrip/client/deezer.py index d2642d96..eceb00fe 100644 --- a/streamrip/client/deezer.py +++ b/streamrip/client/deezer.py @@ -157,12 +157,18 @@ async def get_downloadable( (3, "MP3_320"), # quality 1 (1, "FLAC"), # quality 2 ] - - _, format_str = quality_map[quality] - - dl_info["quality_to_size"] = [ + size_map = [ int(track_info.get(f"FILESIZE_{format}", 0)) for _, format in quality_map ] + dl_info["quality_to_size"] = size_map + if size_map[quality] == 0 and size_map[self.config.fallback_quality] != 0: + quality = self.config.fallback_quality + logger.warning( + "The requested quality is not available. Falling back to quality %s", + quality, + ) + + _, format_str = quality_map[quality] token = track_info["TRACK_TOKEN"] try: diff --git a/streamrip/config.py b/streamrip/config.py index d43a155d..308406e0 100644 --- a/streamrip/config.py +++ b/streamrip/config.py @@ -17,7 +17,7 @@ APP_DIR = click.get_app_dir("streamrip") os.makedirs(APP_DIR, exist_ok=True) DEFAULT_CONFIG_PATH = os.path.join(APP_DIR, "config.toml") -CURRENT_CONFIG_VERSION = "2.0.6" +CURRENT_CONFIG_VERSION = "2.2.0" class OutdatedConfigError(Exception): @@ -66,6 +66,8 @@ class DeezerConfig: # This only applies to paid Deezer subscriptions. Those using deezloader # are automatically limited to quality = 1 quality: int + # If the target quality is not available, fallback to this quality + fallback_quality: int # This allows for free 320kbps MP3 downloads from Deezer # If an arl is provided, deezloader is never used use_deezloader: bool diff --git a/streamrip/config.toml b/streamrip/config.toml index eb109b8e..3728c8fc 100644 --- a/streamrip/config.toml +++ b/streamrip/config.toml @@ -59,6 +59,9 @@ token_expiry = "" # This only applies to paid Deezer subscriptions. Those using deezloader # are automatically limited to quality = 1 quality = 2 +# If the target quality is not available, fallback to this quality +# 0 = MP3_128, 1 = MP3_320, 2 = FLAC +fallback_quality = 1 # An authentication cookie that allows streamrip to use your Deezer account # See https://github.com/nathom/streamrip/wiki/Finding-Your-Deezer-ARL-Cookie # for instructions on how to find this @@ -190,6 +193,6 @@ max_search_results = 100 [misc] # Metadata to identify this config file. Do not change. -version = "2.0.6" +version = "2.1.0" # Print a message if a new version of streamrip is available check_for_updates = true diff --git a/tests/config2.toml b/tests/config2.toml new file mode 100644 index 00000000..fd7c0caa --- /dev/null +++ b/tests/config2.toml @@ -0,0 +1,195 @@ +[downloads] +# Folder where tracks are downloaded to +folder = "test_folder" +# Put Qobuz albums in a 'Qobuz' folder, Tidal albums in 'Tidal' etc. +source_subdirectories = false +disc_subdirectories = true + +# Download (and convert) tracks all at once, instead of sequentially. +# If you are converting the tracks, or have fast internet, this will +# substantially improve processing speed. +concurrency = true +# The maximum number of tracks to download at once +# If you have very fast internet, you will benefit from a higher value, +# A value that is too high for your bandwidth may cause slowdowns +# Set to -1 for no limit +max_connections = 6 +# Max number of API requests per source to handle per minute +# Set to -1 for no limit +requests_per_minute = 60 +# Verify SSL certificates for API connections +# Set to false if you encounter SSL certificate verification errors (not recommended) +verify_ssl = true + +[qobuz] +# 1: 320kbps MP3, 2: 16/44.1, 3: 24/<=96, 4: 24/>=96 +quality = 3 +# This will download booklet pdfs that are included with some albums +download_booklets = true + +# Authenticate to Qobuz using auth token? Value can be true/false only +use_auth_token = false +# Enter your userid if the above use_auth_token is set to true, else enter your email +email_or_userid = "test@gmail.com" +# Enter your auth token if the above use_auth_token is set to true, else enter the md5 hash of your plaintext password +password_or_token = "test_pwd" +# Do not change +app_id = "12345" +# Do not change +secrets = ['secret1', 'secret2'] + +[tidal] +# 0: 256kbps AAC, 1: 320kbps AAC, 2: 16/44.1 "HiFi" FLAC, 3: 24/44.1 "MQA" FLAC +quality = 3 +# This will download videos included in Video Albums. +download_videos = true + +# Do not change any of the fields below +user_id = "userid" +country_code = "countrycode" +access_token = "accesstoken" +refresh_token = "refreshtoken" +# Tokens last 1 week after refresh. This is the Unix timestamp of the expiration +# time. If you haven't used streamrip in more than a week, you may have to log +# in again using `rip config --tidal` +token_expiry = "tokenexpiry" + +[deezer] +# 0, 1, or 2 +# This only applies to paid Deezer subscriptions. Those using deezloader +# are automatically limited to quality = 1 +quality = 2 +# If the target quality is not available, fallback to this quality +fallback_quality = 1 +# An authentication cookie that allows streamrip to use your Deezer account +# See https://github.com/nathom/streamrip/wiki/Finding-Your-Deezer-ARL-Cookie +# for instructions on how to find this +arl = "testarl" +# This allows for free 320kbps MP3 downloads from Deezer +# If an arl is provided, deezloader is never used +use_deezloader = true +# This warns you when the paid deezer account is not logged in and rip falls +# back to deezloader, which is unreliable +deezloader_warnings = true + +[soundcloud] +# Only 0 is available for now +quality = 0 +# This changes periodically, so it needs to be updated +client_id = "clientid" +app_version = "appversion" + +[youtube] +# Only 0 is available for now +quality = 0 +# Download the video along with the audio +download_videos = false +# The path to download the videos to +video_downloads_folder = "videodownloadsfolder" + +[database] +# Create a database that contains all the track IDs downloaded so far +# Any time a track logged in the database is requested, it is skipped +# This can be disabled temporarily with the --no-db flag +downloads_enabled = true +# Path to the downloads database +downloads_path = "downloadspath" +# If a download fails, the item ID is stored here. Then, `rip repair` can be +# called to retry the downloads +failed_downloads_enabled = true +failed_downloads_path = "faileddownloadspath" + +# Convert tracks to a codec after downloading them. +[conversion] +enabled = false +# FLAC, ALAC, OPUS, MP3, VORBIS, or AAC +codec = "ALAC" +# In Hz. Tracks are downsampled if their sampling rate is greater than this. +# Value of 48000 is recommended to maximize quality and minimize space +sampling_rate = 48000 +# Only 16 and 24 are available. It is only applied when the bit depth is higher +# than this value. +bit_depth = 24 +# Only applicable for lossy codecs +lossy_bitrate = 320 + +# Filter a Qobuz artist's discography. Set to 'true' to turn on a filter. +[qobuz_filters] +# Remove Collectors Editions, live recordings, etc. +extras = false +# Picks the highest quality out of albums with identical titles. +repeats = false +# Remove EPs and Singles +non_albums = false +# Remove albums whose artist is not the one requested +features = false +# Skip non studio albums +non_studio_albums = false +# Only download remastered albums +non_remaster = false + +[artwork] +# Write the image to the audio file +embed = true +# The size of the artwork to embed. Options: thumbnail, small, large, original. +# "original" images can be up to 30MB, and may fail embedding. +# Using "large" is recommended. +embed_size = "large" +# If this is set to a value > 0, max(width, height) of the embedded art will be set to this value in pixels +# Proportions of the image will remain the same +embed_max_width = -1 +# Save the cover image at the highest quality as a seperate jpg file +save_artwork = true +# If this is set to a value > 0, max(width, height) of the saved art will be set to this value in pixels +# Proportions of the image will remain the same +saved_max_width = -1 + + +[metadata] +# Sets the value of the 'ALBUM' field in the metadata to the playlist's name. +# This is useful if your music library software organizes tracks based on album name. +set_playlist_to_album = true +# If part of a playlist, sets the `tracknumber` field in the metadata to the track's +# position in the playlist instead of its position in its album +renumber_playlist_tracks = true +# The following metadata tags won't be applied +# See https://github.com/nathom/streamrip/wiki/Metadata-Tag-Names for more info +exclude = [] + +# Changes the folder and file names generated by streamrip. +[filepaths] +# Create folders for single tracks within the downloads directory using the folder_format +# template +add_singles_to_folder = false +# Available keys: "albumartist", "title", "year", "bit_depth", "sampling_rate", +# "id", and "albumcomposer" +folder_format = "{albumartist} - {title} ({year}) [{container}] [{bit_depth}B-{sampling_rate}kHz]" +# Available keys: "tracknumber", "artist", "albumartist", "composer", "title", +# and "albumcomposer", "explicit" +track_format = "{tracknumber}. {artist} - {title}{explicit}" +# Only allow printable ASCII characters in filenames. +restrict_characters = false +# Truncate the filename if it is greater than this number of characters +# Setting this to false may cause downloads to fail on some systems +truncate_to = 120 + +# Last.fm playlists are downloaded by searching for the titles of the tracks +[lastfm] +# The source on which to search for the tracks. +source = "qobuz" +# If no results were found with the primary source, the item is searched for +# on this one. +fallback_source = "" + +[cli] +# Print "Downloading {Album name}" etc. to screen +text_output = true +# Show resolve, download progress bars +progress_bars = true +# The maximum number of search results to show in the interactive menu +max_search_results = 100 + +[misc] +# Metadata to identify this config file. Do not change. +version = "2.2.0" +check_for_updates = true diff --git a/tests/test_config.py b/tests/test_config.py index 0bbc79cc..5ca39be5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -186,6 +186,7 @@ def test_sample_config_data_fields(sample_config_data): deezer=DeezerConfig( arl="testarl", quality=2, + fallback_quality=1, use_deezloader=True, deezloader_warnings=True, ), diff --git a/tests/test_config.toml b/tests/test_config.toml index ada6596b..fd7c0caa 100644 --- a/tests/test_config.toml +++ b/tests/test_config.toml @@ -59,6 +59,8 @@ token_expiry = "tokenexpiry" # This only applies to paid Deezer subscriptions. Those using deezloader # are automatically limited to quality = 1 quality = 2 +# If the target quality is not available, fallback to this quality +fallback_quality = 1 # An authentication cookie that allows streamrip to use your Deezer account # See https://github.com/nathom/streamrip/wiki/Finding-Your-Deezer-ARL-Cookie # for instructions on how to find this @@ -189,5 +191,5 @@ max_search_results = 100 [misc] # Metadata to identify this config file. Do not change. -version = "2.0.6" +version = "2.2.0" check_for_updates = true From 1aa11b8e64108eb915a89796a2a7817df8f537bb Mon Sep 17 00:00:00 2001 From: omnunum <3474285+omnunum@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:31:34 -0400 Subject: [PATCH 2/9] black formatting --- tests/test_parse_url.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_parse_url.py b/tests/test_parse_url.py index 88bea94d..9048cd64 100644 --- a/tests/test_parse_url.py +++ b/tests/test_parse_url.py @@ -152,4 +152,3 @@ async def run_test(): if __name__ == "__main__": unittest.main() - From 8513fadd03e63a9f1c9a11941a1384eca55c4dad Mon Sep 17 00:00:00 2001 From: omnunum <3474285+omnunum@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:44:06 -0400 Subject: [PATCH 3/9] Add tests --- streamrip/config.toml | 2 +- tests/config2.toml | 195 ------------------------------------------ tests/test_config.py | 2 +- tests/test_deezer.py | 73 ++++++++++++++++ 4 files changed, 75 insertions(+), 197 deletions(-) delete mode 100644 tests/config2.toml create mode 100644 tests/test_deezer.py diff --git a/streamrip/config.toml b/streamrip/config.toml index 3728c8fc..227073dd 100644 --- a/streamrip/config.toml +++ b/streamrip/config.toml @@ -193,6 +193,6 @@ max_search_results = 100 [misc] # Metadata to identify this config file. Do not change. -version = "2.1.0" +version = "2.2.0" # Print a message if a new version of streamrip is available check_for_updates = true diff --git a/tests/config2.toml b/tests/config2.toml deleted file mode 100644 index fd7c0caa..00000000 --- a/tests/config2.toml +++ /dev/null @@ -1,195 +0,0 @@ -[downloads] -# Folder where tracks are downloaded to -folder = "test_folder" -# Put Qobuz albums in a 'Qobuz' folder, Tidal albums in 'Tidal' etc. -source_subdirectories = false -disc_subdirectories = true - -# Download (and convert) tracks all at once, instead of sequentially. -# If you are converting the tracks, or have fast internet, this will -# substantially improve processing speed. -concurrency = true -# The maximum number of tracks to download at once -# If you have very fast internet, you will benefit from a higher value, -# A value that is too high for your bandwidth may cause slowdowns -# Set to -1 for no limit -max_connections = 6 -# Max number of API requests per source to handle per minute -# Set to -1 for no limit -requests_per_minute = 60 -# Verify SSL certificates for API connections -# Set to false if you encounter SSL certificate verification errors (not recommended) -verify_ssl = true - -[qobuz] -# 1: 320kbps MP3, 2: 16/44.1, 3: 24/<=96, 4: 24/>=96 -quality = 3 -# This will download booklet pdfs that are included with some albums -download_booklets = true - -# Authenticate to Qobuz using auth token? Value can be true/false only -use_auth_token = false -# Enter your userid if the above use_auth_token is set to true, else enter your email -email_or_userid = "test@gmail.com" -# Enter your auth token if the above use_auth_token is set to true, else enter the md5 hash of your plaintext password -password_or_token = "test_pwd" -# Do not change -app_id = "12345" -# Do not change -secrets = ['secret1', 'secret2'] - -[tidal] -# 0: 256kbps AAC, 1: 320kbps AAC, 2: 16/44.1 "HiFi" FLAC, 3: 24/44.1 "MQA" FLAC -quality = 3 -# This will download videos included in Video Albums. -download_videos = true - -# Do not change any of the fields below -user_id = "userid" -country_code = "countrycode" -access_token = "accesstoken" -refresh_token = "refreshtoken" -# Tokens last 1 week after refresh. This is the Unix timestamp of the expiration -# time. If you haven't used streamrip in more than a week, you may have to log -# in again using `rip config --tidal` -token_expiry = "tokenexpiry" - -[deezer] -# 0, 1, or 2 -# This only applies to paid Deezer subscriptions. Those using deezloader -# are automatically limited to quality = 1 -quality = 2 -# If the target quality is not available, fallback to this quality -fallback_quality = 1 -# An authentication cookie that allows streamrip to use your Deezer account -# See https://github.com/nathom/streamrip/wiki/Finding-Your-Deezer-ARL-Cookie -# for instructions on how to find this -arl = "testarl" -# This allows for free 320kbps MP3 downloads from Deezer -# If an arl is provided, deezloader is never used -use_deezloader = true -# This warns you when the paid deezer account is not logged in and rip falls -# back to deezloader, which is unreliable -deezloader_warnings = true - -[soundcloud] -# Only 0 is available for now -quality = 0 -# This changes periodically, so it needs to be updated -client_id = "clientid" -app_version = "appversion" - -[youtube] -# Only 0 is available for now -quality = 0 -# Download the video along with the audio -download_videos = false -# The path to download the videos to -video_downloads_folder = "videodownloadsfolder" - -[database] -# Create a database that contains all the track IDs downloaded so far -# Any time a track logged in the database is requested, it is skipped -# This can be disabled temporarily with the --no-db flag -downloads_enabled = true -# Path to the downloads database -downloads_path = "downloadspath" -# If a download fails, the item ID is stored here. Then, `rip repair` can be -# called to retry the downloads -failed_downloads_enabled = true -failed_downloads_path = "faileddownloadspath" - -# Convert tracks to a codec after downloading them. -[conversion] -enabled = false -# FLAC, ALAC, OPUS, MP3, VORBIS, or AAC -codec = "ALAC" -# In Hz. Tracks are downsampled if their sampling rate is greater than this. -# Value of 48000 is recommended to maximize quality and minimize space -sampling_rate = 48000 -# Only 16 and 24 are available. It is only applied when the bit depth is higher -# than this value. -bit_depth = 24 -# Only applicable for lossy codecs -lossy_bitrate = 320 - -# Filter a Qobuz artist's discography. Set to 'true' to turn on a filter. -[qobuz_filters] -# Remove Collectors Editions, live recordings, etc. -extras = false -# Picks the highest quality out of albums with identical titles. -repeats = false -# Remove EPs and Singles -non_albums = false -# Remove albums whose artist is not the one requested -features = false -# Skip non studio albums -non_studio_albums = false -# Only download remastered albums -non_remaster = false - -[artwork] -# Write the image to the audio file -embed = true -# The size of the artwork to embed. Options: thumbnail, small, large, original. -# "original" images can be up to 30MB, and may fail embedding. -# Using "large" is recommended. -embed_size = "large" -# If this is set to a value > 0, max(width, height) of the embedded art will be set to this value in pixels -# Proportions of the image will remain the same -embed_max_width = -1 -# Save the cover image at the highest quality as a seperate jpg file -save_artwork = true -# If this is set to a value > 0, max(width, height) of the saved art will be set to this value in pixels -# Proportions of the image will remain the same -saved_max_width = -1 - - -[metadata] -# Sets the value of the 'ALBUM' field in the metadata to the playlist's name. -# This is useful if your music library software organizes tracks based on album name. -set_playlist_to_album = true -# If part of a playlist, sets the `tracknumber` field in the metadata to the track's -# position in the playlist instead of its position in its album -renumber_playlist_tracks = true -# The following metadata tags won't be applied -# See https://github.com/nathom/streamrip/wiki/Metadata-Tag-Names for more info -exclude = [] - -# Changes the folder and file names generated by streamrip. -[filepaths] -# Create folders for single tracks within the downloads directory using the folder_format -# template -add_singles_to_folder = false -# Available keys: "albumartist", "title", "year", "bit_depth", "sampling_rate", -# "id", and "albumcomposer" -folder_format = "{albumartist} - {title} ({year}) [{container}] [{bit_depth}B-{sampling_rate}kHz]" -# Available keys: "tracknumber", "artist", "albumartist", "composer", "title", -# and "albumcomposer", "explicit" -track_format = "{tracknumber}. {artist} - {title}{explicit}" -# Only allow printable ASCII characters in filenames. -restrict_characters = false -# Truncate the filename if it is greater than this number of characters -# Setting this to false may cause downloads to fail on some systems -truncate_to = 120 - -# Last.fm playlists are downloaded by searching for the titles of the tracks -[lastfm] -# The source on which to search for the tracks. -source = "qobuz" -# If no results were found with the primary source, the item is searched for -# on this one. -fallback_source = "" - -[cli] -# Print "Downloading {Album name}" etc. to screen -text_output = true -# Show resolve, download progress bars -progress_bars = true -# The maximum number of search results to show in the interactive menu -max_search_results = 100 - -[misc] -# Metadata to identify this config file. Do not change. -version = "2.2.0" -check_for_updates = true diff --git a/tests/test_config.py b/tests/test_config.py index 5ca39be5..5d34d88f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -136,7 +136,7 @@ def test_config_file_update(): assert toml["cli"]["text_output"] is True # type: ignore assert toml["cli"]["progress_bars"] is True # type: ignore assert toml["cli"]["max_search_results"] == 100 # type: ignore - assert toml["misc"]["version"] == "2.0.6" # type: ignore + assert toml["misc"]["version"] == "2.2.0" # type: ignore assert "YouTubeVideos" in str(toml["youtube"]["video_downloads_folder"]) # type: ignore os.remove("tests/test_config_old2.toml") diff --git a/tests/test_deezer.py b/tests/test_deezer.py new file mode 100644 index 00000000..ed044651 --- /dev/null +++ b/tests/test_deezer.py @@ -0,0 +1,73 @@ +import os +import pytest +from util import arun + +from streamrip.client.downloadable import DeezerDownloadable +from streamrip.client.deezer import DeezerClient +from streamrip.config import Config +from streamrip.exceptions import MissingCredentialsError + +@pytest.fixture(scope="session") +def deezer_client(): + config = Config.defaults() + config.session.deezer.arl = os.environ.get("DEEZER_ARL", "") + config.session.deezer.quality = 2 # FLAC + config.session.deezer.fallback_quality = 1 # MP3_320 + client = DeezerClient(config) + arun(client.login()) + + yield client + + arun(client.session.close()) + +def test_client_raises_missing_credentials(): + c = Config.defaults() + with pytest.raises(MissingCredentialsError): + arun(DeezerClient(c).login()) + +@pytest.mark.skipif( + "DEEZER_ARL" not in os.environ, reason="Deezer ARL not found in env." +) +def test_deezer_fallback_behavior_with_track_77874822(deezer_client): + """Test fallback quality behavior with actual track 77874822""" + # Test with high quality (FLAC) - should fallback if not available + downloadable = arun(deezer_client.get_downloadable("77874822", quality=2)) + + assert isinstance(downloadable, DeezerDownloadable) + assert downloadable.quality in [1, 2] # Either FLAC or fallback MP3_320 + assert isinstance(downloadable.url, str) + assert "https://" in downloadable.url + + # Log what quality we actually got for debugging + quality_names = {0: "MP3_128", 1: "MP3_320", 2: "FLAC"} + print(f"Track 77874822 downloaded at quality {downloadable.quality} ({quality_names[downloadable.quality]})") + +@pytest.mark.skipif( + "DEEZER_ARL" not in os.environ, reason="Deezer ARL not found in env." +) +def test_deezer_metadata_track_77874822(deezer_client): + """Test metadata retrieval for track 77874822""" + metadata = arun(deezer_client.get_metadata("77874822", "track")) + + assert "title" in metadata + assert "artist" in metadata + assert "album" in metadata + assert metadata["id"] == "77874822" + + print(f"Track: {metadata['title']} by {metadata['artist']['name']}") + +@pytest.mark.skipif( + "DEEZER_ARL" not in os.environ, reason="Deezer ARL not found in env." +) +def test_deezer_fallback_config_respected(deezer_client): + """Test that fallback quality config is properly respected""" + # Verify the client has the correct fallback config + assert deezer_client.config.fallback_quality == 1 + assert deezer_client.config.quality == 2 + + # Test that fallback logic works when requesting unavailable quality + # This will test the actual fallback behavior in get_downloadable + downloadable = arun(deezer_client.get_downloadable("77874822", quality=2)) + + # Should either get requested quality or fallback + assert downloadable.quality in [1, 2] From 568adb372214e915cd8fc1724f414b2db29758a7 Mon Sep 17 00:00:00 2001 From: omnunum <3474285+omnunum@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:57:53 -0400 Subject: [PATCH 4/9] Improve tests --- tests/test_deezer.py | 105 ++++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 40 deletions(-) diff --git a/tests/test_deezer.py b/tests/test_deezer.py index ed044651..e67c5aba 100644 --- a/tests/test_deezer.py +++ b/tests/test_deezer.py @@ -1,14 +1,15 @@ import os import pytest +from unittest.mock import Mock, patch from util import arun from streamrip.client.downloadable import DeezerDownloadable from streamrip.client.deezer import DeezerClient from streamrip.config import Config -from streamrip.exceptions import MissingCredentialsError @pytest.fixture(scope="session") def deezer_client(): + """Integration test fixture - requires DEEZER_ARL environment variable""" config = Config.defaults() config.session.deezer.arl = os.environ.get("DEEZER_ARL", "") config.session.deezer.quality = 2 # FLAC @@ -20,54 +21,78 @@ def deezer_client(): arun(client.session.close()) -def test_client_raises_missing_credentials(): - c = Config.defaults() - with pytest.raises(MissingCredentialsError): - arun(DeezerClient(c).login()) +@pytest.fixture +def mock_deezer_client(): + """Unit test fixture - mocked client for fast testing""" + config = Config.defaults() + config.session.deezer.arl = "test_arl" + config.session.deezer.quality = 2 + config.session.deezer.fallback_quality = 1 + + client = DeezerClient(config) + client.client = Mock() + client.session = Mock() + + return client -@pytest.mark.skipif( - "DEEZER_ARL" not in os.environ, reason="Deezer ARL not found in env." -) -def test_deezer_fallback_behavior_with_track_77874822(deezer_client): - """Test fallback quality behavior with actual track 77874822""" - # Test with high quality (FLAC) - should fallback if not available - downloadable = arun(deezer_client.get_downloadable("77874822", quality=2)) +# ===== UNIT TESTS ===== + +def test_deezer_fallback_logic_with_mock_data(mock_deezer_client): + """Unit test: fallback logic works with mocked track data""" + # Mock track info where FLAC is unavailable but MP3_320 is available + mock_track_info = { + "FILESIZE_1": 0, # FLAC unavailable + "FILESIZE_3": 5000000, # MP3_320 available + "FILESIZE_9": 2000000, # MP3_128 available + "TRACK_TOKEN": "test_token" + } - assert isinstance(downloadable, DeezerDownloadable) - assert downloadable.quality in [1, 2] # Either FLAC or fallback MP3_320 - assert isinstance(downloadable.url, str) - assert "https://" in downloadable.url + # Mock the client methods + mock_deezer_client.client.gw.get_track.return_value = mock_track_info + mock_deezer_client.client.get_track_url.return_value = "https://test.mp3" - # Log what quality we actually got for debugging - quality_names = {0: "MP3_128", 1: "MP3_320", 2: "FLAC"} - print(f"Track 77874822 downloaded at quality {downloadable.quality} ({quality_names[downloadable.quality]})") + # Test fallback behavior + with patch.object(mock_deezer_client, 'get_session'): + downloadable = arun(mock_deezer_client.get_downloadable("123", quality=2)) + + # Should have fallen back to quality 1 (MP3_320) + assert downloadable.quality == 1 -@pytest.mark.skipif( - "DEEZER_ARL" not in os.environ, reason="Deezer ARL not found in env." -) -def test_deezer_metadata_track_77874822(deezer_client): - """Test metadata retrieval for track 77874822""" - metadata = arun(deezer_client.get_metadata("77874822", "track")) +def test_deezer_no_fallback_when_quality_available(mock_deezer_client): + """Unit test: no fallback when requested quality is available""" + # Mock track info where FLAC is available + mock_track_info = { + "FILESIZE_1": 25000000, # FLAC available + "FILESIZE_3": 5000000, # MP3_320 available + "FILESIZE_9": 2000000, # MP3_128 available + "TRACK_TOKEN": "test_token" + } - assert "title" in metadata - assert "artist" in metadata - assert "album" in metadata - assert metadata["id"] == "77874822" + mock_deezer_client.client.gw.get_track.return_value = mock_track_info + mock_deezer_client.client.get_track_url.return_value = "https://test.flac" - print(f"Track: {metadata['title']} by {metadata['artist']['name']}") + with patch.object(mock_deezer_client, 'get_session'): + downloadable = arun(mock_deezer_client.get_downloadable("123", quality=2)) + + # Should use requested quality 2 (FLAC) + assert downloadable.quality == 2 + +# ===== INTEGRATION TEST ===== @pytest.mark.skipif( "DEEZER_ARL" not in os.environ, reason="Deezer ARL not found in env." ) -def test_deezer_fallback_config_respected(deezer_client): - """Test that fallback quality config is properly respected""" - # Verify the client has the correct fallback config - assert deezer_client.config.fallback_quality == 1 - assert deezer_client.config.quality == 2 - - # Test that fallback logic works when requesting unavailable quality - # This will test the actual fallback behavior in get_downloadable +def test_deezer_fallback_actually_occurred(deezer_client): + """Integration test: verify fallback works with real track 77874822""" + # We know track 77874822 doesn't have FLAC available, so test fallback scenario downloadable = arun(deezer_client.get_downloadable("77874822", quality=2)) - # Should either get requested quality or fallback - assert downloadable.quality in [1, 2] + # Since we requested FLAC (quality=2) but it's not available, + # we should have fallen back to the configured fallback_quality (1 = MP3_320) + assert downloadable.quality == 1, "Should have fallen back to MP3_320 when FLAC unavailable" + print("Fallback occurred: FLAC unavailable, fell back to MP3_320") + + # Verify the URL is actually accessible and working + assert downloadable.url.startswith("https://") + assert downloadable._size > 0, "Downloadable should have a valid file size" + assert downloadable.extension == "mp3", "MP3_320 should have .mp3 extension" From 43bef532e67eb866761c53ae8b8cf3cda1879a27 Mon Sep 17 00:00:00 2001 From: omnunum <3474285+omnunum@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:42:13 -0400 Subject: [PATCH 5/9] Change approach from static quality to lower_quality_if_not_available --- streamrip/client/deezer.py | 28 ++++++++++++---- streamrip/config.py | 4 +-- streamrip/config.toml | 4 +-- tests/test_config.py | 2 +- tests/test_config.toml | 4 +-- tests/test_deezer.py | 67 ++++++++++++++++++++++++++++++++------ 6 files changed, 85 insertions(+), 24 deletions(-) diff --git a/streamrip/client/deezer.py b/streamrip/client/deezer.py index eceb00fe..4baa458c 100644 --- a/streamrip/client/deezer.py +++ b/streamrip/client/deezer.py @@ -158,15 +158,29 @@ async def get_downloadable( (1, "FLAC"), # quality 2 ] size_map = [ - int(track_info.get(f"FILESIZE_{format}", 0)) for _, format in quality_map + int(track_info.get(f"FILESIZE_{format_num}", 0)) for format_num, _ in quality_map ] dl_info["quality_to_size"] = size_map - if size_map[quality] == 0 and size_map[self.config.fallback_quality] != 0: - quality = self.config.fallback_quality - logger.warning( - "The requested quality is not available. Falling back to quality %s", - quality, - ) + + # Check if requested quality is available + if size_map[quality] == 0: + if self.config.lower_quality_if_not_available: + # Fallback to lower quality + while size_map[quality] == 0 and quality > 0: + logger.warning( + "The requested quality %s is not available. Falling back to quality %s", + quality, + quality - 1, + ) + quality -= 1 + else: + # No fallback - raise error + raise NonStreamableError( + f"The requested quality {quality} is not available and fallback is disabled." + ) + + # Update the quality in dl_info to reflect the final quality used + dl_info["quality"] = quality _, format_str = quality_map[quality] diff --git a/streamrip/config.py b/streamrip/config.py index 308406e0..bafa5687 100644 --- a/streamrip/config.py +++ b/streamrip/config.py @@ -66,8 +66,8 @@ class DeezerConfig: # This only applies to paid Deezer subscriptions. Those using deezloader # are automatically limited to quality = 1 quality: int - # If the target quality is not available, fallback to this quality - fallback_quality: int + # If the target quality is not available, fallback to best quality available + lower_quality_if_not_available: bool # This allows for free 320kbps MP3 downloads from Deezer # If an arl is provided, deezloader is never used use_deezloader: bool diff --git a/streamrip/config.toml b/streamrip/config.toml index 227073dd..dadf7219 100644 --- a/streamrip/config.toml +++ b/streamrip/config.toml @@ -59,9 +59,9 @@ token_expiry = "" # This only applies to paid Deezer subscriptions. Those using deezloader # are automatically limited to quality = 1 quality = 2 -# If the target quality is not available, fallback to this quality +# If the target quality is not available, fallback to best quality available # 0 = MP3_128, 1 = MP3_320, 2 = FLAC -fallback_quality = 1 +lower_quality_if_not_available = true # An authentication cookie that allows streamrip to use your Deezer account # See https://github.com/nathom/streamrip/wiki/Finding-Your-Deezer-ARL-Cookie # for instructions on how to find this diff --git a/tests/test_config.py b/tests/test_config.py index 5d34d88f..44742b95 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -186,7 +186,7 @@ def test_sample_config_data_fields(sample_config_data): deezer=DeezerConfig( arl="testarl", quality=2, - fallback_quality=1, + lower_quality_if_not_available=True, use_deezloader=True, deezloader_warnings=True, ), diff --git a/tests/test_config.toml b/tests/test_config.toml index fd7c0caa..a33a1864 100644 --- a/tests/test_config.toml +++ b/tests/test_config.toml @@ -59,8 +59,8 @@ token_expiry = "tokenexpiry" # This only applies to paid Deezer subscriptions. Those using deezloader # are automatically limited to quality = 1 quality = 2 -# If the target quality is not available, fallback to this quality -fallback_quality = 1 +# If the target quality is not available, fallback to best quality available +lower_quality_if_not_available = true # An authentication cookie that allows streamrip to use your Deezer account # See https://github.com/nathom/streamrip/wiki/Finding-Your-Deezer-ARL-Cookie # for instructions on how to find this diff --git a/tests/test_deezer.py b/tests/test_deezer.py index e67c5aba..935b1671 100644 --- a/tests/test_deezer.py +++ b/tests/test_deezer.py @@ -6,6 +6,7 @@ from streamrip.client.downloadable import DeezerDownloadable from streamrip.client.deezer import DeezerClient from streamrip.config import Config +from streamrip.exceptions import NonStreamableError @pytest.fixture(scope="session") def deezer_client(): @@ -13,7 +14,7 @@ def deezer_client(): config = Config.defaults() config.session.deezer.arl = os.environ.get("DEEZER_ARL", "") config.session.deezer.quality = 2 # FLAC - config.session.deezer.fallback_quality = 1 # MP3_320 + config.session.deezer.lower_quality_if_not_available = True client = DeezerClient(config) arun(client.login()) @@ -27,10 +28,11 @@ def mock_deezer_client(): config = Config.defaults() config.session.deezer.arl = "test_arl" config.session.deezer.quality = 2 - config.session.deezer.fallback_quality = 1 + config.session.deezer.lower_quality_if_not_available = True client = DeezerClient(config) client.client = Mock() + client.client.gw = Mock() client.session = Mock() return client @@ -40,10 +42,12 @@ def mock_deezer_client(): def test_deezer_fallback_logic_with_mock_data(mock_deezer_client): """Unit test: fallback logic works with mocked track data""" # Mock track info where FLAC is unavailable but MP3_320 is available + # quality_map: [(9, "MP3_128"), (3, "MP3_320"), (1, "FLAC")] + # So FILESIZE_9 = quality 0, FILESIZE_3 = quality 1, FILESIZE_1 = quality 2 mock_track_info = { - "FILESIZE_1": 0, # FLAC unavailable - "FILESIZE_3": 5000000, # MP3_320 available - "FILESIZE_9": 2000000, # MP3_128 available + "FILESIZE_1": 0, # FLAC unavailable (quality 2) + "FILESIZE_3": 5000000, # MP3_320 available (quality 1) + "FILESIZE_9": 2000000, # MP3_128 available (quality 0) "TRACK_TOKEN": "test_token" } @@ -55,16 +59,17 @@ def test_deezer_fallback_logic_with_mock_data(mock_deezer_client): with patch.object(mock_deezer_client, 'get_session'): downloadable = arun(mock_deezer_client.get_downloadable("123", quality=2)) - # Should have fallen back to quality 1 (MP3_320) + # Should have fallen back to quality 1 (MP3_320) since FLAC is unavailable assert downloadable.quality == 1 def test_deezer_no_fallback_when_quality_available(mock_deezer_client): """Unit test: no fallback when requested quality is available""" # Mock track info where FLAC is available + # quality_map: [(9, "MP3_128"), (3, "MP3_320"), (1, "FLAC")] mock_track_info = { - "FILESIZE_1": 25000000, # FLAC available - "FILESIZE_3": 5000000, # MP3_320 available - "FILESIZE_9": 2000000, # MP3_128 available + "FILESIZE_1": 25000000, # FLAC available (quality 2) + "FILESIZE_3": 5000000, # MP3_320 available (quality 1) + "FILESIZE_9": 2000000, # MP3_128 available (quality 0) "TRACK_TOKEN": "test_token" } @@ -77,6 +82,48 @@ def test_deezer_no_fallback_when_quality_available(mock_deezer_client): # Should use requested quality 2 (FLAC) assert downloadable.quality == 2 +def test_deezer_fallback_to_lowest_available_quality(mock_deezer_client): + """Unit test: fallback walks down quality list until finding available quality""" + # Mock track info where only MP3_128 is available + # quality_map: [(9, "MP3_128"), (3, "MP3_320"), (1, "FLAC")] + mock_track_info = { + "FILESIZE_1": 0, # FLAC unavailable (quality 2) + "FILESIZE_3": 0, # MP3_320 unavailable (quality 1) + "FILESIZE_9": 2000000, # MP3_128 available (quality 0) + "TRACK_TOKEN": "test_token" + } + + mock_deezer_client.client.gw.get_track.return_value = mock_track_info + mock_deezer_client.client.get_track_url.return_value = "https://test.mp3" + + with patch.object(mock_deezer_client, 'get_session'): + downloadable = arun(mock_deezer_client.get_downloadable("123", quality=2)) + + # Should have fallen back to quality 0 (MP3_128) since higher qualities unavailable + assert downloadable.quality == 0 + +def test_deezer_no_fallback_when_disabled(mock_deezer_client): + """Unit test: no fallback when lower_quality_if_not_available is False""" + # Disable fallback + mock_deezer_client.config.lower_quality_if_not_available = False + + # Mock track info where FLAC is unavailable + # quality_map: [(9, "MP3_128"), (3, "MP3_320"), (1, "FLAC")] + mock_track_info = { + "FILESIZE_1": 0, # FLAC unavailable (quality 2) + "FILESIZE_3": 5000000, # MP3_320 available (quality 1) + "FILESIZE_9": 2000000, # MP3_128 available (quality 0) + "TRACK_TOKEN": "test_url" + } + + mock_deezer_client.client.gw.get_track.return_value = mock_track_info + mock_deezer_client.client.get_track_url.return_value = "https://test.mp3" + + # Should raise an error when requested quality is unavailable and fallback is disabled + with patch.object(mock_deezer_client, 'get_session'): + with pytest.raises(NonStreamableError, match="The requested quality 2 is not available and fallback is disabled"): + arun(mock_deezer_client.get_downloadable("123", quality=2)) + # ===== INTEGRATION TEST ===== @pytest.mark.skipif( @@ -88,7 +135,7 @@ def test_deezer_fallback_actually_occurred(deezer_client): downloadable = arun(deezer_client.get_downloadable("77874822", quality=2)) # Since we requested FLAC (quality=2) but it's not available, - # we should have fallen back to the configured fallback_quality (1 = MP3_320) + # we should have fallen back to the next available quality (1 = MP3_320) assert downloadable.quality == 1, "Should have fallen back to MP3_320 when FLAC unavailable" print("Fallback occurred: FLAC unavailable, fell back to MP3_320") From 7576f03964e62bd6a84e04fa2bf5647b5c7c9feb Mon Sep 17 00:00:00 2001 From: omnunum <3474285+omnunum@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:55:56 -0400 Subject: [PATCH 6/9] Correct size_map creation --- streamrip/client/deezer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/streamrip/client/deezer.py b/streamrip/client/deezer.py index 4baa458c..056463f9 100644 --- a/streamrip/client/deezer.py +++ b/streamrip/client/deezer.py @@ -158,7 +158,7 @@ async def get_downloadable( (1, "FLAC"), # quality 2 ] size_map = [ - int(track_info.get(f"FILESIZE_{format_num}", 0)) for format_num, _ in quality_map + int(track_info.get(f"FILESIZE_{format}", 0)) for _, format in quality_map ] dl_info["quality_to_size"] = size_map From c054a818523cbf55db5a32936deebd7c13b87eb1 Mon Sep 17 00:00:00 2001 From: omnunum <3474285+omnunum@users.noreply.github.com> Date: Wed, 3 Sep 2025 02:12:08 -0400 Subject: [PATCH 7/9] Fix tests --- tests/test_deezer.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/test_deezer.py b/tests/test_deezer.py index 935b1671..5b7b7d77 100644 --- a/tests/test_deezer.py +++ b/tests/test_deezer.py @@ -43,11 +43,11 @@ def test_deezer_fallback_logic_with_mock_data(mock_deezer_client): """Unit test: fallback logic works with mocked track data""" # Mock track info where FLAC is unavailable but MP3_320 is available # quality_map: [(9, "MP3_128"), (3, "MP3_320"), (1, "FLAC")] - # So FILESIZE_9 = quality 0, FILESIZE_3 = quality 1, FILESIZE_1 = quality 2 + # So FILESIZE_MP3_128 = quality 0, FILESIZE_MP3_320 = quality 1, FILESIZE_FLAC = quality 2 mock_track_info = { - "FILESIZE_1": 0, # FLAC unavailable (quality 2) - "FILESIZE_3": 5000000, # MP3_320 available (quality 1) - "FILESIZE_9": 2000000, # MP3_128 available (quality 0) + "FILESIZE_FLAC": 0, # FLAC unavailable (quality 2) + "FILESIZE_MP3_320": 5000000, # MP3_320 available (quality 1) + "FILESIZE_MP3_128": 2000000, # MP3_128 available (quality 0) "TRACK_TOKEN": "test_token" } @@ -67,9 +67,9 @@ def test_deezer_no_fallback_when_quality_available(mock_deezer_client): # Mock track info where FLAC is available # quality_map: [(9, "MP3_128"), (3, "MP3_320"), (1, "FLAC")] mock_track_info = { - "FILESIZE_1": 25000000, # FLAC available (quality 2) - "FILESIZE_3": 5000000, # MP3_320 available (quality 1) - "FILESIZE_9": 2000000, # MP3_128 available (quality 0) + "FILESIZE_FLAC": 25000000, # FLAC available (quality 2) + "FILESIZE_MP3_320": 5000000, # MP3_320 available (quality 1) + "FILESIZE_MP3_128": 2000000, # MP3_128 available (quality 0) "TRACK_TOKEN": "test_token" } @@ -87,9 +87,9 @@ def test_deezer_fallback_to_lowest_available_quality(mock_deezer_client): # Mock track info where only MP3_128 is available # quality_map: [(9, "MP3_128"), (3, "MP3_320"), (1, "FLAC")] mock_track_info = { - "FILESIZE_1": 0, # FLAC unavailable (quality 2) - "FILESIZE_3": 0, # MP3_320 unavailable (quality 1) - "FILESIZE_9": 2000000, # MP3_128 available (quality 0) + "FILESIZE_FLAC": 0, # FLAC unavailable (quality 2) + "FILESIZE_MP3_320": 0, # MP3_320 unavailable (quality 1) + "FILESIZE_MP3_128": 2000000, # MP3_128 available (quality 0) "TRACK_TOKEN": "test_token" } @@ -110,9 +110,9 @@ def test_deezer_no_fallback_when_disabled(mock_deezer_client): # Mock track info where FLAC is unavailable # quality_map: [(9, "MP3_128"), (3, "MP3_320"), (1, "FLAC")] mock_track_info = { - "FILESIZE_1": 0, # FLAC unavailable (quality 2) - "FILESIZE_3": 5000000, # MP3_320 available (quality 1) - "FILESIZE_9": 2000000, # MP3_128 available (quality 0) + "FILESIZE_FLAC": 0, # FLAC unavailable (quality 2) + "FILESIZE_MP3_320": 5000000, # MP3_320 available (quality 1) + "FILESIZE_MP3_128": 2000000, # MP3_128 available (quality 0) "TRACK_TOKEN": "test_url" } From b53f830766ef8bb3b193b972017c305c1e262c04 Mon Sep 17 00:00:00 2001 From: Hank Bond <3474285+omnunum@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:22:14 -0400 Subject: [PATCH 8/9] Update README.md with forked changes --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/README.md b/README.md index 0df40b69..2669c59a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,71 @@ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) A scriptable stream downloader for Qobuz, Tidal, Deezer and SoundCloud. +# Differences Between This Fork (`main`) and Upstream (`dev`) + +## Functionality / User-Facing + +- **Metadata enrichment (Deezer, Tidal, Qobuz)** + - Added: BPM, ReplayGain (track & album), UPC/barcode, record type, artist credits, original release date, media type. + - Normalized casing of release types (e.g., “EP”), consistent container and bit-depth fields. + - Composer and performer credits parsed and applied where supported. + - Deduplication utilities reduce redundant copyright tags. + +- **Multi-artist support** + - Introduced `artists` tag alongside `artist`, preserving compatibility but capturing full credits. + +- **Favorites ripping across services** + - Deezer, Tidal, Qobuz: Profile-based favorites via URL parser. + - Supports artists, albums, tracks, and playlists. + +- **Path templating improvements** + - Extended placeholders: `{releasetype}`, `{upc}`, `{source_artist_id}` with correct casing. + - More accurate container detection (real downloadable format per track). + - Enables richer file/folder naming schemes. + +- **Download performance & reliability** + - Non-blocking download initiation (`asyncio.to_thread`) prevents new downloads from freezing active ones. + - Streamability checks improved (e.g., Deezer’s `readable` field). + +- **Configurable lyrics fetching (Tidal)** + - Added `fetch_lyrics` config option (default: true). + +- **Smarter skipping & release detection** + - Skips only downloaded albums, not entire artists/labels. + - Maintains `DownloadedReleases` tracking table. + - Cleaner summary-style logs for large batches. +## Architecture / Developer-Focused + +- **Metadata model modernization** + - Standardized fields aligned with Vorbis/MusicBrainz conventions. + - Renamed: `upc` → `barcode`, `gain` → `replaygain_*`, `record_type` → `releasetype`, `original_release_date` → `originaldate`. + - Removed legacy mapping dictionaries, replaced with type-safe structures. + +- **Favorites pipeline** + - New `DeezerProfileURL` parser and `PendingUserFavorites`/`UserFavorites` types. + - Unified handling across Deezer, Tidal, and Qobuz. + - Tidal & Qobuz: Consistent favorites pipeline with standardized `{"items": [...]}` response. + +- **Quality handling refactor** + - Unified quality scale (0–3) with fallback to lower qualities when tracks are not available at desired quality. + - Moved quality determination into metadata phase, reducing duplicate API calls. + - Added `streamable` to `TrackInfo`. + - Centralized `NonStreamableError` handling. + +- **Concurrency & rate limiting** + - Deezer client uses `asyncio.Semaphore` for concurrency. + - Rate limiter (`AsyncLimiter`, ~10 req/sec). + - Configurable HTTP connection pooling. + - Centralized `_api_call` wrapper for retry/backoff logic. + - Tidal client: respects 429 `Retry-After` headers, raises `NonStreamableError` on 404. + +- **Testing & utilities** + - Expanded unit tests: Deezer concurrency/limits, favorites URL parsing, path templates, config evolution. + - New helpers: `parse_performers()`, `deduplicate_copyright()`. + +- **Logging improvements** + - Summary logs instead of line-per-item in large runs. + - Unified error/warning messaging across services. ![downloading an album](https://github.com/nathom/streamrip/blob/dev/demo/download_album.png?raw=true) From 42f9f710cf8e2777bbffee12dff059fe91401a8b Mon Sep 17 00:00:00 2001 From: omnunum <3474285+omnunum@users.noreply.github.com> Date: Sat, 6 Sep 2025 16:48:11 -0400 Subject: [PATCH 9/9] Revert "Update README.md with forked changes" This reverts commit b53f830766ef8bb3b193b972017c305c1e262c04. --- README.md | 65 ------------------------------------------------------- 1 file changed, 65 deletions(-) diff --git a/README.md b/README.md index 2669c59a..0df40b69 100644 --- a/README.md +++ b/README.md @@ -4,71 +4,6 @@ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) A scriptable stream downloader for Qobuz, Tidal, Deezer and SoundCloud. -# Differences Between This Fork (`main`) and Upstream (`dev`) - -## Functionality / User-Facing - -- **Metadata enrichment (Deezer, Tidal, Qobuz)** - - Added: BPM, ReplayGain (track & album), UPC/barcode, record type, artist credits, original release date, media type. - - Normalized casing of release types (e.g., “EP”), consistent container and bit-depth fields. - - Composer and performer credits parsed and applied where supported. - - Deduplication utilities reduce redundant copyright tags. - -- **Multi-artist support** - - Introduced `artists` tag alongside `artist`, preserving compatibility but capturing full credits. - -- **Favorites ripping across services** - - Deezer, Tidal, Qobuz: Profile-based favorites via URL parser. - - Supports artists, albums, tracks, and playlists. - -- **Path templating improvements** - - Extended placeholders: `{releasetype}`, `{upc}`, `{source_artist_id}` with correct casing. - - More accurate container detection (real downloadable format per track). - - Enables richer file/folder naming schemes. - -- **Download performance & reliability** - - Non-blocking download initiation (`asyncio.to_thread`) prevents new downloads from freezing active ones. - - Streamability checks improved (e.g., Deezer’s `readable` field). - -- **Configurable lyrics fetching (Tidal)** - - Added `fetch_lyrics` config option (default: true). - -- **Smarter skipping & release detection** - - Skips only downloaded albums, not entire artists/labels. - - Maintains `DownloadedReleases` tracking table. - - Cleaner summary-style logs for large batches. -## Architecture / Developer-Focused - -- **Metadata model modernization** - - Standardized fields aligned with Vorbis/MusicBrainz conventions. - - Renamed: `upc` → `barcode`, `gain` → `replaygain_*`, `record_type` → `releasetype`, `original_release_date` → `originaldate`. - - Removed legacy mapping dictionaries, replaced with type-safe structures. - -- **Favorites pipeline** - - New `DeezerProfileURL` parser and `PendingUserFavorites`/`UserFavorites` types. - - Unified handling across Deezer, Tidal, and Qobuz. - - Tidal & Qobuz: Consistent favorites pipeline with standardized `{"items": [...]}` response. - -- **Quality handling refactor** - - Unified quality scale (0–3) with fallback to lower qualities when tracks are not available at desired quality. - - Moved quality determination into metadata phase, reducing duplicate API calls. - - Added `streamable` to `TrackInfo`. - - Centralized `NonStreamableError` handling. - -- **Concurrency & rate limiting** - - Deezer client uses `asyncio.Semaphore` for concurrency. - - Rate limiter (`AsyncLimiter`, ~10 req/sec). - - Configurable HTTP connection pooling. - - Centralized `_api_call` wrapper for retry/backoff logic. - - Tidal client: respects 429 `Retry-After` headers, raises `NonStreamableError` on 404. - -- **Testing & utilities** - - Expanded unit tests: Deezer concurrency/limits, favorites URL parsing, path templates, config evolution. - - New helpers: `parse_performers()`, `deduplicate_copyright()`. - -- **Logging improvements** - - Summary logs instead of line-per-item in large runs. - - Unified error/warning messaging across services. ![downloading an album](https://github.com/nathom/streamrip/blob/dev/demo/download_album.png?raw=true)