From 89b3af774c3126a3793bede0605b7eb00aee9235 Mon Sep 17 00:00:00 2001 From: Timo Date: Sat, 21 Feb 2026 13:51:01 +0100 Subject: [PATCH 1/2] fix: use FALLBACK track when primary track has no streaming rights Deezer's song.getListData and radio.getUserRadio APIs sometimes return tracks with an empty RIGHTS hash (STATUS=3), meaning the primary track version cannot be streamed on the user's account or region. In these cases the API also provides a FALLBACK object pointing to an alternative version that is fully licensed. Before this fix the plugin passed the primary track token to the media provider and received error 2002 "Track token has no sufficient rights on requested media", causing the track to be silently skipped. Now both getTrackUrl and flowTracks check RIGHTS before using a track token and switch to the FALLBACK version when rights are absent. Co-Authored-By: Claude Sonnet 4.6 --- API/Async.pm | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/API/Async.pm b/API/Async.pm index 7415aaf..2d06683 100644 --- a/API/Async.pm +++ b/API/Async.pm @@ -235,11 +235,16 @@ sub flowTracks { $self->gwCall( sub { my ($result, $context) = @_; - my @trackTokens = map { $_->{TRACK_TOKEN} } @{ $result->{results}->{data} }; - my @trackIds = map { $_->{SNG_ID} } @{ $result->{results}->{data} }; -#$log->error(Data::Dump::dump(\@trackTokens), Data::Dump::dump(\@trackIds)); + my @trackTokens = map { + my $t = ($_->{RIGHTS} && %{$_->{RIGHTS}}) ? $_ : ($_->{FALLBACK} || $_); + $t->{TRACK_TOKEN}; + } @{ $result->{results}->{data} }; + my @trackIds = map { + my $t = ($_->{RIGHTS} && %{$_->{RIGHTS}}) ? $_ : ($_->{FALLBACK} || $_); + $t->{SNG_ID}; + } @{ $result->{results}->{data} }; return $cb->() unless @trackTokens; - + $self->_getProviders( $cb, $context->{license}, $params->{quality}, \@trackTokens, \@trackIds ); }, { method => 'radio.getUserRadio', @@ -717,16 +722,22 @@ sub getTrackUrl { $self->gwCall( sub { my ($result, $context) = @_; - my @trackTokens = map { $_->{TRACK_TOKEN} } @{ $result->{results}->{data} }; - my @trackIds = map { $_->{SNG_ID} } @{ $result->{results}->{data} }; -#$log->error(Data::Dump::dump(\@trackTokens), Data::Dump::dump(\@trackIds)); + my @trackTokens = map { + my $t = ($_->{RIGHTS} && %{$_->{RIGHTS}}) ? $_ : ($_->{FALLBACK} || $_); + $t->{TRACK_TOKEN}; + } @{ $result->{results}->{data} }; + my @trackIds = map { + my $t = ($_->{RIGHTS} && %{$_->{RIGHTS}}) ? $_ : ($_->{FALLBACK} || $_); + $t->{SNG_ID}; + } @{ $result->{results}->{data} }; + main::INFOLOG && $log->is_info && $log->info("Track IDs after fallback resolution: @trackIds"); return $cb->() unless @trackTokens; $self->_getProviders( $cb, $context->{license}, $params->{quality}, \@trackTokens, \@trackIds ); - }, { + }, { method => 'song.getListData', - }, { + }, { sng_ids => $ids } ); } From ca0af671f66220e87e849b744f9db4cc82caf413 Mon Sep 17 00:00:00 2001 From: Timo Date: Sat, 21 Feb 2026 13:58:39 +0100 Subject: [PATCH 2/2] docs: explain Deezer FALLBACK mechanism in track resolution comments Deezer explicitly provides a FALLBACK entry in the API response when a track cannot be streamed, pointing to a licensed alternative version. The comment now makes clear that FALLBACK is Deezers own suggestion, not a workaround. Co-Authored-By: Claude Sonnet 4.6 --- API/Async.pm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/API/Async.pm b/API/Async.pm index 2d06683..bca8809 100644 --- a/API/Async.pm +++ b/API/Async.pm @@ -235,6 +235,9 @@ sub flowTracks { $self->gwCall( sub { my ($result, $context) = @_; + # When a track has an empty RIGHTS hash it cannot be streamed (error 2002). + # Deezer then provides a FALLBACK entry with a licensed alternative version + # (different SNG_ID/TRACK_TOKEN) and populated RIGHTS - use that instead. my @trackTokens = map { my $t = ($_->{RIGHTS} && %{$_->{RIGHTS}}) ? $_ : ($_->{FALLBACK} || $_); $t->{TRACK_TOKEN}; @@ -722,6 +725,9 @@ sub getTrackUrl { $self->gwCall( sub { my ($result, $context) = @_; + # When a track has an empty RIGHTS hash it cannot be streamed (error 2002). + # Deezer then provides a FALLBACK entry with a licensed alternative version + # (different SNG_ID/TRACK_TOKEN) and populated RIGHTS - use that instead. my @trackTokens = map { my $t = ($_->{RIGHTS} && %{$_->{RIGHTS}}) ? $_ : ($_->{FALLBACK} || $_); $t->{TRACK_TOKEN};