Import Qobuz favorite tracks
Context
The Qobuz plugin currently imports:
- favorite albums;
- purchased albums;
- favorite artists;
- Qobuz playlists.
However, it does not import tracks returned by the Qobuz favorite/getUserFavorites API with type=tracks.
These tracks are exposed by Qobuz as individual favorite tracks ("Favorite Tracks" / "Titres"), and are distinct from the tracks belonging to favorite albums.
As a result, a user can have a track in Qobuz Favorites → Tracks that is not present in the LMS Qobuz library.
Changes
1. Add myFavoriteTracks() to Plugins::Qobuz::API::Sync
A new method calls:
favorite/getUserFavorites
with:
and handles pagination in the same way as the existing myArtists() and myAlbums() methods.
The method also precaches the album embedded in each favorite track before calling _precacheTracks().
This is required because the album returned by favorite/getUserFavorites&type=tracks contains raw nested structures such as:
album => {
genre => {
id => ...,
name => ...
},
artist => {
id => ...,
name => ...
},
...
}
whereas _prepareTrack() expects the album to have already gone through _precacheAlbum().
Without this step, the importer can fail with:
SQL::Abstract::_SWITCH_refkind Fatal:
no dispatch entry for HASHREF
The implementation is therefore:
sub myFavoriteTracks {
my ($class, $userId) = @_;
my $offset = 0;
my $tracks = [];
my $args = {
type => 'tracks',
limit => QOBUZ_LIMIT,
_ttl => QOBUZ_USER_DATA_EXPIRY,
_user_cache => 1,
_use_token => 1,
};
do {
$args->{offset} = $offset;
my $response = $class->_get(
'favorite/getUserFavorites',
$userId,
$args
);
$offset = 0;
if (
$response &&
ref $response &&
$response->{tracks} &&
ref $response->{tracks} &&
$response->{tracks}->{items} &&
ref $response->{tracks}->{items}
) {
my $items = $response->{tracks}->{items};
foreach my $track (@$items) {
next unless $track && ref $track;
next unless $track->{album} && ref $track->{album};
my ($album) = @{ _precacheAlbum([ $track->{album} ]) };
$track->{album} = $album if $album;
}
push @$tracks, @{ _precacheTracks($items) };
if (
$response->{tracks}->{total} > QOBUZ_LIMIT &&
$response->{tracks}->{offset} < $response->{tracks}->{total}
) {
$offset = $response->{tracks}->{offset} + QOBUZ_LIMIT;
}
}
} while $offset && $offset < QOBUZ_USERDATA_LIMIT;
return $tracks;
}
The method is deliberately named myFavoriteTracks() rather than myTracks(), since it retrieves the user's favorite tracks and does not retrieve purchased tracks.
2. Add scanFavoriteTracks() to Plugins::Qobuz::Importer
The importer now calls myFavoriteTracks() during a normal library scan.
The tracks are filtered through the existing _filterStreamables() mechanism and then passed through the existing _prepareTrack() code, so they use the same metadata and URL generation logic as other Qobuz tracks.
The implementation groups favorite tracks by album before preparing them.
This is important because _prepareTrack() and _checkAlbumArtists() maintain album-artist information across tracks belonging to the same album.
The $albumArtists structure is therefore created once per album and shared by all favorite tracks belonging to that album.
The TIMESTAMP for these tracks is overridden with favorited_at, rather than the album's favorited_at/purchased_at, because the relevant event for an individual favorite track is when that track was favorited.
The scan is integrated into startScan() immediately after scanAlbums():
$class->scanAlbums($accounts);
$class->scanFavoriteTracks($accounts);
$class->scanArtists($accounts);
And code for scanFavoriteTracks() is:
sub scanFavoriteTracks {
my ($class, $accounts) = @_;
my $progress = Slim::Utils::Progress->new({
'type' => 'importer',
'name' => 'plugin_qobuz_favorite_tracks',
'total' => 1,
'every' => 1,
});
foreach my $account (@$accounts) {
my $accountName = $account->[0] || '';
my $userId = $account->[1];
$log->warn(
"Reading favorite tracks... account=$accountName userId=$userId"
);
$progress->update(
string('PLUGIN_QOBUZ_PROGRESS_READ_ALBUMS', $accountName)
);
my $tracks = Plugins::Qobuz::API::Sync->myFavoriteTracks($userId);
$tracks ||= [];
$tracks = _filterStreamables($tracks);
$progress->total(scalar @$tracks);
$log->warn(
"Qobuz favorite tracks: " .
scalar(@$tracks) .
" streamable tracks found for $accountName"
);
# Regrouper les tracks par album.
my %albums;
foreach my $track (@$tracks) {
next unless $track && ref $track;
next unless $track->{album} && ref $track->{album};
next unless $track->{album}->{id};
push @{$albums{$track->{album}->{id}}}, $track;
}
# Traiter chaque album, comme dans scanAlbums().
foreach my $albumId (keys %albums) {
my $albumTracks = $albums{$albumId};
next unless $albumTracks && @$albumTracks;
my $album = $albumTracks->[0]->{album};
next unless $album && ref $album;
my $albumArtists = {
required => 0,
ids => undef,
names => undef,
};
my @attributes;
foreach my $track (@$albumTracks) {
$progress->update(
$track->{title} || $track->{id} || ''
);
my $attribute = _prepareTrack(
$album,
$track,
$albumArtists
);
next unless $attribute;
# Pour un titre favori individuel, le timestamp est celui
# du track et non celui de l'album.
$attribute->{TIMESTAMP} = $track->{favorited_at}
if $track->{favorited_at};
push @attributes, $attribute;
}
next unless @attributes;
_checkAlbumArtists(\@attributes, $albumArtists);
$class->storeTracks(
\@attributes,
undef,
$accountName
);
main::SCANNER && Slim::Schema->forceCommit;
}
}
$progress->final();
main::SCANNER && Slim::Schema->forceCommit;
}
3. Add scan progress support
The new importer uses:
Slim::Utils::Progress->new({
'type' => 'importer',
'name' => 'plugin_qobuz_favorite_tracks',
'total' => 1,
'every' => 1,
});
and reports individual track progress.
Two translation keys are required because LMS uses them for different purposes.
The dynamic retrieval message is:
PLUGIN_QOBUZ_PROGRESS_READ_FAVORITE_TRACKS
DE Lese Favoriten-Titel (%s)...
EN Fetching favorite tracks (%s)...
FR Récupération des titres favoris (%s)...
NL Favoriete nummers ophalen (%s)...
The static importer name required by LMS/Material scan progress is:
PLUGIN_QOBUZ_FAVORITE_TRACKS_PROGRESS
DE Qobuz Favoriten-Titel
EN Qobuz Favorite Tracks
FR Titres favoris Qobuz
NL Qobuz Favoriete nummers
The latter is essential because LMS derives the progress-string key from the importer name:
plugin_qobuz_favorite_tracks
↓
plugin_qobuz_favorite_tracks_PROGRESS
↓
PLUGIN_QOBUZ_FAVORITE_TRACKS_PROGRESS
Without this second translation key, the importer is correctly registered in LMS's progress table and rescanprogress, but the Material Skin does not display the corresponding progress row.
Behaviour
With these changes, a normal Qobuz library scan now includes:
Albums Qobuz
Titres favoris Qobuz
Artistes Qobuz
Listes de lecture Qobuz
The favorite-track importer retrieves the tracks from the Qobuz Favorites → Tracks collection while continuing to rely on the existing Qobuz importer infrastructure for:
- streamability filtering;
- URL generation;
- metadata preparation;
- artist/composer/performer handling;
- album artwork;
- ReplayGain;
- album artist handling;
- LMS database storage.
No changes to the existing album or playlist import behaviour are required.
Import Qobuz favorite tracks
Context
The Qobuz plugin currently imports:
However, it does not import tracks returned by the Qobuz
favorite/getUserFavoritesAPI withtype=tracks.These tracks are exposed by Qobuz as individual favorite tracks ("Favorite Tracks" / "Titres"), and are distinct from the tracks belonging to favorite albums.
As a result, a user can have a track in Qobuz Favorites → Tracks that is not present in the LMS Qobuz library.
Changes
1. Add
myFavoriteTracks()toPlugins::Qobuz::API::SyncA new method calls:
with:
and handles pagination in the same way as the existing
myArtists()andmyAlbums()methods.The method also precaches the album embedded in each favorite track before calling
_precacheTracks().This is required because the album returned by
favorite/getUserFavorites&type=trackscontains raw nested structures such as:whereas
_prepareTrack()expects the album to have already gone through_precacheAlbum().Without this step, the importer can fail with:
The implementation is therefore:
The method is deliberately named
myFavoriteTracks()rather thanmyTracks(), since it retrieves the user's favorite tracks and does not retrieve purchased tracks.2. Add
scanFavoriteTracks()toPlugins::Qobuz::ImporterThe importer now calls
myFavoriteTracks()during a normal library scan.The tracks are filtered through the existing
_filterStreamables()mechanism and then passed through the existing_prepareTrack()code, so they use the same metadata and URL generation logic as other Qobuz tracks.The implementation groups favorite tracks by album before preparing them.
This is important because
_prepareTrack()and_checkAlbumArtists()maintain album-artist information across tracks belonging to the same album.The
$albumArtistsstructure is therefore created once per album and shared by all favorite tracks belonging to that album.The
TIMESTAMPfor these tracks is overridden withfavorited_at, rather than the album'sfavorited_at/purchased_at, because the relevant event for an individual favorite track is when that track was favorited.The scan is integrated into
startScan()immediately afterscanAlbums():And code for scanFavoriteTracks() is:
3. Add scan progress support
The new importer uses:
and reports individual track progress.
Two translation keys are required because LMS uses them for different purposes.
The dynamic retrieval message is:
The static importer name required by LMS/Material scan progress is:
The latter is essential because LMS derives the progress-string key from the importer name:
Without this second translation key, the importer is correctly registered in LMS's
progresstable andrescanprogress, but the Material Skin does not display the corresponding progress row.Behaviour
With these changes, a normal Qobuz library scan now includes:
The favorite-track importer retrieves the tracks from the Qobuz Favorites → Tracks collection while continuing to rely on the existing Qobuz importer infrastructure for:
No changes to the existing album or playlist import behaviour are required.