From 8a0a895529aa939bc0ce11f2bc11780d53ba45ac Mon Sep 17 00:00:00 2001 From: Timo Date: Sun, 22 Feb 2026 10:23:08 +0100 Subject: [PATCH 1/4] feat: add check_track_rights and rights_cache_ttl preferences Adds two new settings to the Deezer plugin UI: - check_track_rights (bool, default off): enables GW API prefetch on album open to filter unplayable tracks immediately - rights_cache_ttl (hours, default 24h): controls how long the norights cache entries are kept Includes strings (EN/DE), HTML controls (checkbox + dropdown), and preference defaults/registration in Plugin.pm and Settings.pm. Co-Authored-By: Claude Sonnet 4.6 --- HTML/EN/plugins/Deezer/settings.html | 12 ++++++++++++ Plugin.pm | 4 +++- Settings.pm | 2 +- strings.txt | 16 ++++++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) 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..9576fd9 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 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..3333dc6 100644 --- a/strings.txt +++ b/strings.txt @@ -562,3 +562,19 @@ 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. From 9db4f89520251da7c6f5f5343f014d5b43d7ffc0 Mon Sep 17 00:00:00 2001 From: Timo Date: Sun, 22 Feb 2026 10:24:28 +0100 Subject: [PATCH 2/4] feat: prefetch track rights in albumTracks when enabled When check_track_rights is on, albumTracks calls song.getListData for all remaining tracks after the readable filter. Tracks with RIGHTS:{} and no FALLBACK are cached as norights (configurable TTL via rights_cache_ttl) and filtered from the result immediately. A deezer_album_rights_$id key marks that the check was done for this album, so subsequent opens skip the GW call and serve the result from cache at full speed. Also removes the deezer_album_empty mechanism: albums with 0 playable tracks now always appear in artist listings (just empty), per user preference. The lazy norights caching in getTrackUrl now uses rights_cache_ttl as well, so both paths share the same TTL setting. Co-Authored-By: Claude Sonnet 4.6 --- API/Async.pm | 64 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/API/Async.pm b/API/Async.pm index 7415aaf..b5b351d 100644 --- a/API/Async.pm +++ b/API/Async.pm @@ -280,9 +280,55 @@ 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 + $tracks = [grep { + ($_->{readable} || (defined $_->{id} && $_->{id} < 0)) && + !$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 ($prefs->get('check_track_rights') && $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 +767,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 ); From 9c46cc4d017d9222fca57f73dcf70d5af01148eb Mon Sep 17 00:00:00 2001 From: Timo Date: Sun, 22 Feb 2026 10:45:06 +0100 Subject: [PATCH 3/4] fix: skip norights cache filter when check_track_rights is disabled Disabling the option now immediately restores all tracks to visibility, without waiting for norights cache entries to expire. --- API/Async.pm | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/API/Async.pm b/API/Async.pm index b5b351d..ff92b48 100644 --- a/API/Async.pm +++ b/API/Async.pm @@ -289,10 +289,13 @@ sub albumTracks { # 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 + # 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)) && - !$cache->get("deezer_track_norights_$_->{id}") + (!$checkRights || !$cache->get("deezer_track_norights_$_->{id}")) } @$tracks] if $tracks; my $finish = sub { @@ -306,7 +309,7 @@ sub albumTracks { # 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 ($prefs->get('check_track_rights') && $tracks && @$tracks + if ($checkRights && $tracks && @$tracks && !$cache->get("deezer_album_rights_$id")) { my @ids = map { $_->{id} } @$tracks; $self->gwCall(sub { From ee4f4ec057b8219aa35376a8475e5dc58c1799dd Mon Sep 17 00:00:00 2001 From: Timo Date: Sun, 22 Feb 2026 10:47:27 +0100 Subject: [PATCH 4/4] feat: show message when album has no playable tracks Displays a localised textarea in the browser UI instead of an empty list, hinting at regional/subscription licensing as a likely cause. --- Plugin.pm | 2 ++ strings.txt | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Plugin.pm b/Plugin.pm index 9576fd9..841914b 100644 --- a/Plugin.pm +++ b/Plugin.pm @@ -515,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/strings.txt b/strings.txt index 3333dc6..9d9d55b 100644 --- a/strings.txt +++ b/strings.txt @@ -578,3 +578,7 @@ PLUGIN_DEEZER_RIGHTS_CACHE_TTL 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.