Get genre metadata for Tidal, Qobuz and Spotify

Extract genre information when fetching favorites and search results.
Genre is now populated in the collection and playlists for
tracks from these streaming services.
This commit is contained in:
Rob Stanfield
2025-12-20 09:18:15 -08:00
committed by Jonas Kvinge
parent 610b458196
commit ea629aedd1
5 changed files with 85 additions and 0 deletions

View File

@@ -695,6 +695,16 @@ void QobuzRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_req
} }
album.album = obj_item["title"_L1].toString(); album.album = obj_item["title"_L1].toString();
if (obj_item.contains("genre"_L1)) {
QJsonValue value_genre = obj_item["genre"_L1];
if (value_genre.isObject()) {
QJsonObject obj_genre = value_genre.toObject();
if (obj_genre.contains("name"_L1)) {
album.genre = obj_genre["name"_L1].toString();
}
}
}
if (album_songs_requests_pending_.contains(album.album_id)) continue; if (album_songs_requests_pending_.contains(album.album_id)) continue;
QJsonValue value_artist = obj_item["artist"_L1]; QJsonValue value_artist = obj_item["artist"_L1];
@@ -921,6 +931,17 @@ void QobuzRequest::SongsReceived(QNetworkReply *reply, const Artist &artist_requ
} }
} }
// Extract genre from album/get response if not already set
if (album.genre.isEmpty() && json_object.contains("genre"_L1)) {
QJsonValue value_genre = json_object["genre"_L1];
if (value_genre.isObject()) {
QJsonObject obj_genre = value_genre.toObject();
if (obj_genre.contains("name"_L1)) {
album.genre = obj_genre["name"_L1].toString();
}
}
}
QJsonValue value_tracks = json_object["tracks"_L1]; QJsonValue value_tracks = json_object["tracks"_L1];
if (!value_tracks.isObject()) { if (!value_tracks.isObject()) {
Error(u"Json tracks is not an object."_s, json_object); Error(u"Json tracks is not an object."_s, json_object);
@@ -1053,6 +1074,7 @@ void QobuzRequest::ParseSong(Song &song, const QJsonObject &json_obj, const Arti
// bool streamable = json_obj["streamable"].toBool(); // bool streamable = json_obj["streamable"].toBool();
QString composer; QString composer;
QString performer; QString performer;
QString genre;
if (json_obj.contains("media_number"_L1)) { if (json_obj.contains("media_number"_L1)) {
disc = json_obj["media_number"_L1].toInt(); disc = json_obj["media_number"_L1].toInt();
@@ -1118,6 +1140,21 @@ void QobuzRequest::ParseSong(Song &song, const QJsonObject &json_obj, const Arti
song_album.cover_url.setUrl(album_image); song_album.cover_url.setUrl(album_image);
} }
} }
if (obj_album.contains("genre"_L1)) {
QJsonValue value_genre = obj_album["genre"_L1];
if (value_genre.isObject()) {
QJsonObject obj_genre = value_genre.toObject();
if (obj_genre.contains("name"_L1)) {
genre = obj_genre["name"_L1].toString();
}
}
}
}
// Fall back to genre from the Album struct if not found in the track's album object
if (genre.isEmpty() && !album.genre.isEmpty()) {
genre = album.genre;
} }
if (json_obj.contains("composer"_L1)) { if (json_obj.contains("composer"_L1)) {
@@ -1180,6 +1217,7 @@ void QobuzRequest::ParseSong(Song &song, const QJsonObject &json_obj, const Arti
song.set_performer(performer); song.set_performer(performer);
song.set_composer(composer); song.set_composer(composer);
song.set_comment(copyright); song.set_comment(copyright);
song.set_genre(genre);
song.set_directory_id(0); song.set_directory_id(0);
song.set_filetype(Song::FileType::Stream); song.set_filetype(Song::FileType::Stream);
song.set_filesize(0); song.set_filesize(0);

View File

@@ -65,6 +65,7 @@ class QobuzRequest : public QobuzBaseRequest {
QString album; QString album;
QUrl cover_url; QUrl cover_url;
bool album_explicit; bool album_explicit;
QString genre;
}; };
struct Request { struct Request {
Request() : offset(0), limit(0) {} Request() : offset(0), limit(0) {}

View File

@@ -496,11 +496,20 @@ void SpotifyRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_
const QString artist_id = object_item["id"_L1].toString(); const QString artist_id = object_item["id"_L1].toString();
const QString artist = object_item["name"_L1].toString(); const QString artist = object_item["name"_L1].toString();
QString genre;
if (object_item.contains("genres"_L1) && object_item["genres"_L1].isArray()) {
const QJsonArray array_genres = object_item["genres"_L1].toArray();
if (!array_genres.isEmpty()) {
genre = array_genres.first().toString();
}
}
if (artist_albums_requests_pending_.contains(artist_id)) continue; if (artist_albums_requests_pending_.contains(artist_id)) continue;
ArtistAlbumsRequest request; ArtistAlbumsRequest request;
request.artist.artist_id = artist_id; request.artist.artist_id = artist_id;
request.artist.artist = artist; request.artist.artist = artist;
request.artist.genre = genre;
artist_albums_requests_pending_.insert(artist_id, request); artist_albums_requests_pending_.insert(artist_id, request);
} }
@@ -715,6 +724,12 @@ void SpotifyRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_a
if (artist.artist_id.isEmpty() || artist.artist_id == artist_artist.artist_id) { if (artist.artist_id.isEmpty() || artist.artist_id == artist_artist.artist_id) {
artist.artist_id = obj_artist["id"_L1].toString(); artist.artist_id = obj_artist["id"_L1].toString();
artist.artist = obj_artist["name"_L1].toString(); artist.artist = obj_artist["name"_L1].toString();
if (obj_artist.contains("genres"_L1) && obj_artist["genres"_L1].isArray()) {
const QJsonArray array_genres = obj_artist["genres"_L1].toArray();
if (!array_genres.isEmpty()) {
album.genre = array_genres.first().toString();
}
}
if (artist.artist_id == artist_artist.artist_id) { if (artist.artist_id == artist_artist.artist_id) {
artist_matches = true; artist_matches = true;
break; break;
@@ -730,6 +745,11 @@ void SpotifyRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_a
artist = artist_artist; artist = artist_artist;
} }
// Fall back to artist's genre if no genre found in album's artist data
if (album.genre.isEmpty() && !artist_artist.genre.isEmpty()) {
album.genre = artist_artist.genre;
}
if (object_item.contains("images"_L1) && object_item["images"_L1].isArray()) { if (object_item.contains("images"_L1) && object_item["images"_L1].isArray()) {
const QJsonArray array_images = object_item["images"_L1].toArray(); const QJsonArray array_images = object_item["images"_L1].toArray();
for (const QJsonValue &value : array_images) { for (const QJsonValue &value : array_images) {
@@ -1050,6 +1070,7 @@ void SpotifyRequest::ParseSong(Song &song, const QJsonObject &json_obj, const Ar
QString artist_id; QString artist_id;
QString artist_title; QString artist_title;
QString genre;
if (json_obj.contains("artists"_L1) && json_obj["artists"_L1].isArray()) { if (json_obj.contains("artists"_L1) && json_obj["artists"_L1].isArray()) {
const QJsonArray array_artists = json_obj["artists"_L1].toArray(); const QJsonArray array_artists = json_obj["artists"_L1].toArray();
for (const QJsonValue &value_artist : array_artists) { for (const QJsonValue &value_artist : array_artists) {
@@ -1060,6 +1081,12 @@ void SpotifyRequest::ParseSong(Song &song, const QJsonObject &json_obj, const Ar
} }
artist_id = obj_artist["id"_L1].toString(); artist_id = obj_artist["id"_L1].toString();
artist_title = obj_artist["name"_L1].toString(); artist_title = obj_artist["name"_L1].toString();
if (obj_artist.contains("genres"_L1) && obj_artist["genres"_L1].isArray()) {
const QJsonArray array_genres = obj_artist["genres"_L1].toArray();
if (!array_genres.isEmpty()) {
genre = array_genres.first().toString();
}
}
break; break;
} }
} }
@@ -1102,6 +1129,16 @@ void SpotifyRequest::ParseSong(Song &song, const QJsonObject &json_obj, const Ar
cover_url = album.cover_url; cover_url = album.cover_url;
} }
// Fall back to genre from the Album struct if not found in the track's artist
if (genre.isEmpty() && !album.genre.isEmpty()) {
genre = album.genre;
}
// Fall back to genre from the Artist struct if still not found
if (genre.isEmpty() && !album_artist.genre.isEmpty()) {
genre = album_artist.genre;
}
QString song_id = json_obj["id"_L1].toString(); QString song_id = json_obj["id"_L1].toString();
QString title = json_obj["name"_L1].toString(); QString title = json_obj["name"_L1].toString();
QString uri = json_obj["uri"_L1].toString(); QString uri = json_obj["uri"_L1].toString();
@@ -1130,6 +1167,7 @@ void SpotifyRequest::ParseSong(Song &song, const QJsonObject &json_obj, const Ar
song.set_url(url); song.set_url(url);
song.set_length_nanosec(duration); song.set_length_nanosec(duration);
song.set_art_automatic(cover_url); song.set_art_automatic(cover_url);
song.set_genre(genre);
song.set_directory_id(0); song.set_directory_id(0);
song.set_filetype(Song::FileType::Stream); song.set_filetype(Song::FileType::Stream);
song.set_filesize(0); song.set_filesize(0);

View File

@@ -57,11 +57,13 @@ class SpotifyRequest : public SpotifyBaseRequest {
struct Artist { struct Artist {
QString artist_id; QString artist_id;
QString artist; QString artist;
QString genre;
}; };
struct Album { struct Album {
QString album_id; QString album_id;
QString album; QString album;
QUrl cover_url; QUrl cover_url;
QString genre;
}; };
struct Request { struct Request {
Request() : offset(0), limit(0) {} Request() : offset(0), limit(0) {}

View File

@@ -1000,6 +1000,11 @@ void TidalRequest::ParseSong(Song &song, const QJsonObject &json_obj, const Arti
const bool stream_ready = json_obj["streamReady"_L1].toBool(); const bool stream_ready = json_obj["streamReady"_L1].toBool();
const QString copyright = json_obj["copyright"_L1].toString(); const QString copyright = json_obj["copyright"_L1].toString();
QString genre;
if (json_obj.contains("genre"_L1)) {
genre = json_obj["genre"_L1].toString();
}
if (!value_artist.isObject()) { if (!value_artist.isObject()) {
Error(u"Invalid Json reply, track artist is not a object."_s, value_artist); Error(u"Invalid Json reply, track artist is not a object."_s, value_artist);
return; return;
@@ -1095,6 +1100,7 @@ void TidalRequest::ParseSong(Song &song, const QJsonObject &json_obj, const Arti
song.set_art_automatic(cover_url); song.set_art_automatic(cover_url);
} }
song.set_comment(copyright); song.set_comment(copyright);
song.set_genre(genre);
song.set_directory_id(0); song.set_directory_id(0);
song.set_filetype(Song::FileType::Stream); song.set_filetype(Song::FileType::Stream);
song.set_filesize(0); song.set_filesize(0);