Skip to content

Import Qobuz favorite tracks #133

Description

@wawa79

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:

type => 'tracks'

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions