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.