diff --git a/API/Async.pm b/API/Async.pm
index 7415aaf..ff92b48 100644
--- a/API/Async.pm
+++ b/API/Async.pm
@@ -280,9 +280,58 @@ sub albumTracks {
my $tracks = shift;
$tracks = $tracks->{data} if $tracks;
# only missing data in album/tracks is the album itself...
- $tracks = Plugins::Deezer::API->cacheTrackMetadata( $tracks, { album => $album } ) if $tracks;
-
- $cb->($tracks || []);
+ # Deezer sets readable:false for tracks not licensed in the user's region or
+ # subscription. Queuing them causes error 2002 for every track. Filter them
+ # out here, consistent with playlistTracks. This is independent from the
+ # FALLBACK mechanism in getTrackUrl/flowTracks: FALLBACK handles tracks that
+ # are listed as readable but fail at playback time with an alternative version;
+ # readable:false means the track is genuinely unavailable and no FALLBACK is
+ # provided. User-uploaded tracks (negative IDs) bypass licensing entirely.
+ my $total = $tracks ? scalar @$tracks : 0;
+
+ # Step 1: filter readable:false and already-known-norights tracks.
+ # The norights cache is only consulted when check_track_rights is
+ # enabled; disabling the option restores all tracks to visibility.
+ my $checkRights = $prefs->get('check_track_rights');
+ $tracks = [grep {
+ ($_->{readable} || (defined $_->{id} && $_->{id} < 0)) &&
+ (!$checkRights || !$cache->get("deezer_track_norights_$_->{id}"))
+ } @$tracks] if $tracks;
+
+ my $finish = sub {
+ my $filtered = shift;
+ $filtered = Plugins::Deezer::API->cacheTrackMetadata($filtered, { album => $album });
+ my $kept = $filtered ? scalar @$filtered : 0;
+ $log->info("albumTracks id=$id: $total total, $kept kept after filter");
+ $cb->($filtered || []);
+ };
+
+ # Step 2: if check_track_rights is enabled and this album has not been
+ # rights-checked yet, call song.getListData to find remaining unplayable
+ # tracks and cache them (TTL from rights_cache_ttl pref).
+ if ($checkRights && $tracks && @$tracks
+ && !$cache->get("deezer_album_rights_$id")) {
+ my @ids = map { $_->{id} } @$tracks;
+ $self->gwCall(sub {
+ my ($result) = @_;
+ my $ttl = ($prefs->get('rights_cache_ttl') || 24) * 3600;
+ my %unplayable;
+ foreach my $t (@{ $result->{results}->{data} || [] }) {
+ if (!($t->{RIGHTS} && %{$t->{RIGHTS}}) && !$t->{FALLBACK}) {
+ $unplayable{$t->{SNG_ID}} = 1;
+ $cache->set("deezer_track_norights_$t->{SNG_ID}", 1, $ttl);
+ }
+ }
+ $cache->set("deezer_album_rights_$id", 1, $ttl);
+ $finish->([grep { !$unplayable{$_->{id}} } @$tracks]);
+ }, {
+ method => 'song.getListData',
+ }, {
+ sng_ids => \@ids,
+ });
+ } else {
+ $finish->($tracks || []);
+ }
}, {
limit => MAX_LIMIT,
} );
@@ -721,6 +770,18 @@ sub getTrackUrl {
my @trackIds = map { $_->{SNG_ID} } @{ $result->{results}->{data} };
#$log->error(Data::Dump::dump(\@trackTokens), Data::Dump::dump(\@trackIds));
+ # Tracks with no rights AND no fallback cannot be streamed by any means.
+ # Cache their IDs so albumTracks can exclude them from future listings.
+ # Use the user-configured TTL (rights_cache_ttl, in hours).
+ my $norights_ttl = ($prefs->get('rights_cache_ttl') || 24) * 3600;
+ foreach my $track (@{ $result->{results}->{data} || [] }) {
+ if (!($track->{RIGHTS} && %{$track->{RIGHTS}}) && !$track->{FALLBACK}) {
+ $cache->set("deezer_track_norights_$track->{SNG_ID}", 1, $norights_ttl);
+ main::INFOLOG && $log->is_info && $log->info("Track $track->{SNG_ID} has no rights and no fallback, marked unplayable");
+ }
+ }
+
+
return $cb->() unless @trackTokens;
$self->_getProviders( $cb, $context->{license}, $params->{quality}, \@trackTokens, \@trackIds );
diff --git a/HTML/EN/plugins/Deezer/settings.html b/HTML/EN/plugins/Deezer/settings.html
index e1a9db7..03689ba 100644
--- a/HTML/EN/plugins/Deezer/settings.html
+++ b/HTML/EN/plugins/Deezer/settings.html
@@ -49,6 +49,18 @@
[% END %]
+ [% WRAPPER setting title="PLUGIN_DEEZER_CHECK_RIGHTS" desc="PLUGIN_DEEZER_CHECK_RIGHTS_DESC" %]
+
+ [% END %]
+
+ [% WRAPPER setting title="PLUGIN_DEEZER_RIGHTS_CACHE_TTL" desc="PLUGIN_DEEZER_RIGHTS_CACHE_TTL_DESC" %]
+
+ [% END %]
+
[% WRAPPER setting title="PLUGIN_DEEZER_QUALITY" desc="PLUGIN_DEEZER_QUALITY_DESC" %]
diff --git a/Plugin.pm b/Plugin.pm
index df33baf..841914b 100644
--- a/Plugin.pm
+++ b/Plugin.pm
@@ -74,7 +74,9 @@ sub initPlugin {
liveformat => 'mp3',
quality => 'HIGH',
serial => '29436f4b2c5b2b552e4c221b2d7c7a4e7a336c002d7278512e486f1f2c677d432b1c224e29522c0b280e7f42750f7b43794a271c7d652b06744c5454795f6c4e781f51197d742e077b5b344e7b0e694d7e4c271e2c1c7c032c4f794e786060062b4260432f306b40',
- unfold_collection => 1,
+ unfold_collection => 1,
+ check_track_rights => 0,
+ rights_cache_ttl => 24,
});
# reset the API ref when a player changes user
@@ -513,6 +515,8 @@ sub getAlbum {
getAPIHandler($client)->albumTracks(sub {
my $items = _renderTracks(shift);
+ $items = [{ name => cstring($client, 'PLUGIN_DEEZER_NO_PLAYABLE_TRACKS'), type => 'textarea' }]
+ unless @$items;
$cb->( { items => $items } );
}, $params->{id} );
}
diff --git a/Settings.pm b/Settings.pm
index ea2a94a..fb5c7e0 100644
--- a/Settings.pm
+++ b/Settings.pm
@@ -20,7 +20,7 @@ sub name { Slim::Web::HTTP::CSRF->protectName('PLUGIN_DEEZER_NAME') }
sub page { Slim::Web::HTTP::CSRF->protectURI('plugins/Deezer/settings.html') }
-sub prefs { return ($prefs, qw(quality liveformat liverate unfold_collection)) }
+sub prefs { return ($prefs, qw(quality liveformat liverate unfold_collection check_track_rights rights_cache_ttl)) }
sub handler {
my ($class, $client, $params, $callback, @args) = @_;
diff --git a/strings.txt b/strings.txt
index e31e776..9d9d55b 100644
--- a/strings.txt
+++ b/strings.txt
@@ -562,3 +562,23 @@ PLUGIN_DEEZER_UNFOLD_DESC
FR Configure le déroulement du menu de votre collection
HU Állítsa be a gyűjtemény menüjének összehajtási módját
UA Встановіть режим згортання меню вашої колекції
+
+PLUGIN_DEEZER_CHECK_RIGHTS
+ DE Verfügbarkeit beim Öffnen eines Albums prüfen
+ EN Check track availability on album open
+
+PLUGIN_DEEZER_CHECK_RIGHTS_DESC
+ DE Ruft beim Öffnen eines Albums die Streaming-Rechte von Deezer ab. Nicht abspielbare Tracks werden sofort ausgeblendet statt beim Abspielen zu scheitern. Beim ersten Öffnen entsteht eine kurze Verzögerung (~300ms).
+ EN Fetches streaming rights from Deezer for all tracks when an album is opened. Unplayable tracks are hidden immediately instead of failing at playback. Adds a short delay (~300ms) the first time each album is opened.
+
+PLUGIN_DEEZER_RIGHTS_CACHE_TTL
+ DE Cache-Dauer für Verfügbarkeit
+ EN Availability cache duration
+
+PLUGIN_DEEZER_RIGHTS_CACHE_TTL_DESC
+ DE Wie lange die Track-Verfügbarkeit gecacht wird. Nach Ablauf wird erneut geprüft.
+ EN How long track availability is cached. After expiry the check is repeated.
+
+PLUGIN_DEEZER_NO_PLAYABLE_TRACKS
+ DE Keine abspielbaren Tracks verfügbar. Möglicherweise sind diese Titel in deiner Region oder mit deinem Abonnement nicht lizenziert.
+ EN No playable tracks available. These tracks may not be licensed for your region or subscription.