Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions streamrip/client/deezer.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,24 @@ async def get_downloadable(
"quality allowed is 1.",
)
except deezer.WrongGeolocation:
# A geolocation error usually only affects the lossless master:
# the MP3 formats of the same track are often still available in
# the user's country. Retry at a lower quality before giving up.
if quality > 0 and self.config.lower_quality_if_not_available:
logger.warning(
"Track %s is not available in your country at quality %s (%s). "
"Falling back to quality %s (%s).",
item_id,
quality,
format_str,
quality - 1,
quality_map[quality - 1][1],
)
return await self.get_downloadable(
item_id,
quality - 1,
is_retry=is_retry,
)
if not is_retry and fallback_id:
return await self.get_downloadable(fallback_id, quality, is_retry=True)
raise NonStreamableError(
Expand Down
99 changes: 99 additions & 0 deletions tests/test_deezer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import deezer
import pytest
from unittest.mock import Mock, patch
from util import arun
Expand Down Expand Up @@ -124,6 +125,104 @@ def test_deezer_no_fallback_when_disabled(mock_deezer_client):
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))

# ===== GEOLOCATION FALLBACK TESTS =====

def _geo_url_side_effect(blocked_formats):
"""Return a get_track_url replacement that raises WrongGeolocation for
blocked formats and a fake URL otherwise."""
def _get_track_url(token, format_str):
if format_str in blocked_formats:
raise deezer.WrongGeolocation("ES")
return f"https://cdn.example.com/{format_str}/file"
return _get_track_url

def _full_track_info(token="test_token", **extra):
# All formats have a non-zero size: the server reports the files exist,
# so only the media endpoint's geolocation check can reject them.
return {
"FILESIZE_FLAC": 25000000,
"FILESIZE_MP3_320": 5000000,
"FILESIZE_MP3_128": 2000000,
"TRACK_TOKEN": token,
**extra,
}

def test_deezer_geolocation_falls_back_to_mp3(mock_deezer_client):
"""Geo error on FLAC -> retry the same track as MP3_320"""
mock_deezer_client.client.gw.get_track.return_value = _full_track_info()
mock_deezer_client.client.get_track_url.side_effect = _geo_url_side_effect({"FLAC"})

downloadable = arun(mock_deezer_client.get_downloadable("123", quality=2))

assert downloadable.quality == 1
assert downloadable.extension == "mp3"
formats_requested = [
c.args[1] for c in mock_deezer_client.client.get_track_url.call_args_list
]
assert formats_requested == ["FLAC", "MP3_320"]

def test_deezer_geolocation_falls_back_to_mp3_128(mock_deezer_client):
"""Geo error on FLAC and MP3_320 -> retry the same track as MP3_128"""
mock_deezer_client.client.gw.get_track.return_value = _full_track_info()
mock_deezer_client.client.get_track_url.side_effect = _geo_url_side_effect(
{"FLAC", "MP3_320"}
)

downloadable = arun(mock_deezer_client.get_downloadable("123", quality=2))

assert downloadable.quality == 0
assert downloadable.extension == "mp3"
formats_requested = [
c.args[1] for c in mock_deezer_client.client.get_track_url.call_args_list
]
assert formats_requested == ["FLAC", "MP3_320", "MP3_128"]

def test_deezer_geolocation_all_qualities_blocked_raises(mock_deezer_client):
"""Geo error on every format and no fallback track -> NonStreamableError"""
mock_deezer_client.client.gw.get_track.return_value = _full_track_info()
mock_deezer_client.client.get_track_url.side_effect = _geo_url_side_effect(
{"FLAC", "MP3_320", "MP3_128"}
)

with pytest.raises(NonStreamableError, match="country/location"):
arun(mock_deezer_client.get_downloadable("123", quality=2))

def test_deezer_geolocation_uses_fallback_track_when_mp3_blocked(mock_deezer_client):
"""Geo error on every format of the track -> try the FALLBACK track id"""
def get_track(item_id):
if item_id == "123":
return _full_track_info(token="token123", FALLBACK={"SNG_ID": "456"})
return _full_track_info(token="token456")

mock_deezer_client.client.gw.get_track.side_effect = get_track
# Original track is geo-blocked at every quality; fallback track works as MP3_128
mock_deezer_client.client.get_track_url.side_effect = lambda token, fmt: (
_geo_url_side_effect({"FLAC", "MP3_320", "MP3_128"})(token, fmt)
if token == "token123"
else f"https://cdn.example.com/fallback/{fmt}"
)

downloadable = arun(mock_deezer_client.get_downloadable("123", quality=2))

assert downloadable.id == "456"
assert downloadable.quality == 0
assert downloadable.extension == "mp3"

def test_deezer_geolocation_no_quality_fallback_when_disabled(mock_deezer_client):
"""lower_quality_if_not_available=False -> geo error raises immediately"""
mock_deezer_client.config.lower_quality_if_not_available = False
mock_deezer_client.client.gw.get_track.return_value = _full_track_info()
mock_deezer_client.client.get_track_url.side_effect = _geo_url_side_effect({"FLAC"})

with pytest.raises(NonStreamableError, match="country/location"):
arun(mock_deezer_client.get_downloadable("123", quality=2))

# MP3 formats must not be requested when fallback is disabled
formats_requested = [
c.args[1] for c in mock_deezer_client.client.get_track_url.call_args_list
]
assert formats_requested == ["FLAC"]

# ===== INTEGRATION TEST =====

@pytest.mark.skipif(
Expand Down