diff --git a/lib/src/endpoints/me.dart b/lib/src/endpoints/me.dart index 818641b..344540d 100644 --- a/lib/src/endpoints/me.dart +++ b/lib/src/endpoints/me.dart @@ -105,23 +105,24 @@ class Me extends _MeEndpointBase { Future player([String? market]) async => _player.playbackState(market); - /// Get the current user's top tracks. - Future> topTracks() async { - final jsonString = await _api._get('$_path/top/tracks'); - final map = json.decode(jsonString); - - final items = map['items'] as Iterable; - return items.map((item) => Track.fromJson(item)); - } - - /// Get the current user's top artists. - Future> topArtists() async { - final jsonString = await _api._get('$_path/top/artists'); - final map = json.decode(jsonString); - - final items = map['items'] as Iterable; - return items.map((item) => Artist.fromJson(item)); - } + /// Get the current user's top tracks, spanning over a [timeRange]. + /// The [timeRange]'s default is [TimeRange.mediumTerm]. + Pages topTracks({TimeRange timeRange = TimeRange.mediumTerm}) => + _top(_TopItemsType.tracks, (item) => Track.fromJson(item), timeRange); + + /// Get the current user's top artists, spanning over a [timeRange]. + /// The [timeRange]'s default is [TimeRange.mediumTerm]. + Pages topArtists({TimeRange timeRange = TimeRange.mediumTerm}) => + _top(_TopItemsType.artists, (item) => Artist.fromJson(item), timeRange); + + Pages _top( + _TopItemsType type, T Function(dynamic) parser, TimeRange range) => + _getPages( + '$_path/top/${type.name}?' + + _buildQuery({ + 'time_range': range._key, + }), + parser); /// Get information about a user’s available devices. @Deprecated('Use [spotify.player.devices()]') @@ -246,3 +247,20 @@ class FollowingType extends ExtendedEnum { static const artist = FollowingType('artist'); static const user = FollowingType('user'); } + +class TimeRange { + final String _key; + + const TimeRange(this._key); + + /// Consists of several years of data and including all new data as it becomes available + static const longTerm = TimeRange('long_term'); + + /// Consists of approximately last 6 months + static const mediumTerm = TimeRange('medium_term'); + + /// Consists of approximately last 4 weeks + static const shortTerm = TimeRange('short_term'); +} + +enum _TopItemsType { artists, tracks } diff --git a/lib/src/models/_models.g.dart b/lib/src/models/_models.g.dart index 91ce0bc..911868e 100644 --- a/lib/src/models/_models.g.dart +++ b/lib/src/models/_models.g.dart @@ -15,7 +15,7 @@ ExternalIds _$ExternalIdsFromJson(Map json) => ExternalIds() ..upc = json['upc'] as String?; Album _$AlbumFromJson(Map json) => Album() - ..albumType = json['album_type'] as String? + ..albumType = AlbumSimple._convertForAlbumType(json['album_type'] as String?) ..artists = (json['artists'] as List?) ?.map((e) => ArtistSimple.fromJson(e as Map)) .toList() @@ -55,7 +55,7 @@ const _$DatePrecisionEnumMap = { }; AlbumSimple _$AlbumSimpleFromJson(Map json) => AlbumSimple() - ..albumType = json['album_type'] as String? + ..albumType = AlbumSimple._convertForAlbumType(json['album_type'] as String?) ..artists = (json['artists'] as List?) ?.map((e) => ArtistSimple.fromJson(e as Map)) .toList() diff --git a/lib/src/models/album.dart b/lib/src/models/album.dart index 499b22b..8064ebd 100644 --- a/lib/src/models/album.dart +++ b/lib/src/models/album.dart @@ -52,9 +52,13 @@ class AlbumSimple extends Object { : items.map((trackJson) => TrackSimple.fromJson(trackJson)); } + static AlbumType? _convertForAlbumType(String? json) { + return AlbumType.values.asNameMap()[json?.toLowerCase()]; + } + /// The type of the album: one of "album", "single", or "compilation". - @JsonKey(name: 'album_type') - String? albumType; + @JsonKey(name: 'album_type', fromJson: _convertForAlbumType) + AlbumType? albumType; /// The artists of the album. Each artist object includes a link in href to /// more detailed information about the artist. @@ -105,3 +109,5 @@ class AlbumSimple extends Object { } enum DatePrecision { day, month, year } + +enum AlbumType { album, single, compilation } diff --git a/test/data/v1/me/top/artists.json b/test/data/v1/me/top/artists.json new file mode 100644 index 0000000..0bd4aeb --- /dev/null +++ b/test/data/v1/me/top/artists.json @@ -0,0 +1,88 @@ +{ + "items": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/5NXHXK6hOCotCF8lvGM1I0" + }, + "followers": { + "href": null, + "total": 0 + }, + "genres": [ + "alternative metal", + "art rock", + "progressive metal", + "progressive rock", + "symphonic rock" + ], + "href": "https://api.spotify.com/v1/artists/5NXHXK6hOCotCF8lvGM1I0", + "id": "5NXHXK6hOCotCF8lvGM1I0", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab6761610000e5ebc4f72407be5d96db73982400", + "width": 640 + }, + { + "height": 320, + "url": "https://i.scdn.co/image/ab67616100005174c4f72407be5d96db73982400", + "width": 320 + }, + { + "height": 160, + "url": "https://i.scdn.co/image/ab6761610000f178c4f72407be5d96db73982400", + "width": 160 + } + ], + "name": "Porcupine Tree", + "popularity": 56, + "type": "artist", + "uri": "spotify:artist:5NXHXK6hOCotCF8lvGM1I0" + }, + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/4X42BfuhWCAZ2swiVze9O0" + }, + "followers": { + "href": null, + "total": 0 + }, + "genres": [ + "art rock", + "neo-progressive", + "progressive metal", + "progressive rock", + "symphonic rock" + ], + "href": "https://api.spotify.com/v1/artists/4X42BfuhWCAZ2swiVze9O0", + "id": "4X42BfuhWCAZ2swiVze9O0", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab6761610000e5ebb0655526ab51d102711937d7", + "width": 640 + }, + { + "height": 320, + "url": "https://i.scdn.co/image/ab67616100005174b0655526ab51d102711937d7", + "width": 320 + }, + { + "height": 160, + "url": "https://i.scdn.co/image/ab6761610000f178b0655526ab51d102711937d7", + "width": 160 + } + ], + "name": "Steven Wilson", + "popularity": 49, + "type": "artist", + "uri": "spotify:artist:4X42BfuhWCAZ2swiVze9O0" + } + ], + "total": 50, + "limit": 2, + "offset": 0, + "href": "https://api.spotify.com/v1/me/top/artists?limit=2&offset=0", + "next": "https://api.spotify.com/v1/me/top/artists?limit=2&offset=2", + "previous": null +} \ No newline at end of file diff --git a/test/data/v1/me/top/tracks.json b/test/data/v1/me/top/tracks.json new file mode 100644 index 0000000..07ddd5f --- /dev/null +++ b/test/data/v1/me/top/tracks.json @@ -0,0 +1,504 @@ +{ + "items": [ + { + "album": { + "album_type": "ALBUM", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/4PmxbsWP1u0TnvqcrIA9ze" + }, + "href": "https://api.spotify.com/v1/artists/4PmxbsWP1u0TnvqcrIA9ze", + "id": "4PmxbsWP1u0TnvqcrIA9ze", + "name": "Vieux Farka Touré", + "type": "artist", + "uri": "spotify:artist:4PmxbsWP1u0TnvqcrIA9ze" + }, + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2mVVjNmdjXZZDvhgQWiakk" + }, + "href": "https://api.spotify.com/v1/artists/2mVVjNmdjXZZDvhgQWiakk", + "id": "2mVVjNmdjXZZDvhgQWiakk", + "name": "Khruangbin", + "type": "artist", + "uri": "spotify:artist:2mVVjNmdjXZZDvhgQWiakk" + } + ], + "available_markets": [ + "AD", + "AE", + "AR", + "AT", + "AU", + "BE", + "BG", + "BH", + "BO", + "BR", + "CA", + "CH", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DE", + "DK", + "DO", + "DZ", + "EC", + "EE", + "EG", + "ES", + "FI", + "FR", + "GB", + "GR", + "GT", + "HK", + "HN", + "HU", + "ID", + "IE", + "IL", + "IN", + "IS", + "IT", + "JO", + "JP", + "KW", + "LB", + "LI", + "LT", + "LU", + "LV", + "MA", + "MC", + "MT", + "MX", + "MY", + "NI", + "NL", + "NO", + "NZ", + "OM", + "PA", + "PE", + "PH", + "PL", + "PS", + "PT", + "PY", + "QA", + "RO", + "SA", + "SE", + "SG", + "SK", + "SV", + "TH", + "TN", + "TR", + "TW", + "US", + "UY", + "VN", + "ZA" + ], + "external_urls": { + "spotify": "https://open.spotify.com/album/4dfAJiDQHQf4dGX0ZdtxPh" + }, + "href": "https://api.spotify.com/v1/albums/4dfAJiDQHQf4dGX0ZdtxPh", + "id": "4dfAJiDQHQf4dGX0ZdtxPh", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab67616d0000b27338c09d1b8121ca97828aceb4", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67616d00001e0238c09d1b8121ca97828aceb4", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab67616d0000485138c09d1b8121ca97828aceb4", + "width": 64 + } + ], + "name": "Ali", + "release_date": "2022-09-23", + "release_date_precision": "day", + "total_tracks": 8, + "type": "album", + "uri": "spotify:album:4dfAJiDQHQf4dGX0ZdtxPh" + }, + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/4PmxbsWP1u0TnvqcrIA9ze" + }, + "href": "https://api.spotify.com/v1/artists/4PmxbsWP1u0TnvqcrIA9ze", + "id": "4PmxbsWP1u0TnvqcrIA9ze", + "name": "Vieux Farka Touré", + "type": "artist", + "uri": "spotify:artist:4PmxbsWP1u0TnvqcrIA9ze" + }, + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2mVVjNmdjXZZDvhgQWiakk" + }, + "href": "https://api.spotify.com/v1/artists/2mVVjNmdjXZZDvhgQWiakk", + "id": "2mVVjNmdjXZZDvhgQWiakk", + "name": "Khruangbin", + "type": "artist", + "uri": "spotify:artist:2mVVjNmdjXZZDvhgQWiakk" + } + ], + "available_markets": [ + "AD", + "AE", + "AR", + "AT", + "AU", + "BE", + "BG", + "BH", + "BO", + "BR", + "CA", + "CH", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DE", + "DK", + "DO", + "DZ", + "EC", + "EE", + "EG", + "ES", + "FI", + "FR", + "GB", + "GR", + "GT", + "HK", + "HN", + "HU", + "ID", + "IE", + "IL", + "IN", + "IS", + "IT", + "JO", + "JP", + "KW", + "LB", + "LI", + "LT", + "LU", + "LV", + "MA", + "MC", + "MT", + "MX", + "MY", + "NI", + "NL", + "NO", + "NZ", + "OM", + "PA", + "PE", + "PH", + "PL", + "PS", + "PT", + "PY", + "QA", + "RO", + "SA", + "SE", + "SG", + "SK", + "SV", + "TH", + "TN", + "TR", + "TW", + "US", + "UY", + "VN", + "ZA" + ], + "disc_number": 1, + "duration_ms": 301242, + "explicit": false, + "external_ids": { + "isrc": "USJ5G2127403" + }, + "external_urls": { + "spotify": "https://open.spotify.com/track/36huM0UOR9A17TYAape2Xg" + }, + "href": "https://api.spotify.com/v1/tracks/36huM0UOR9A17TYAape2Xg", + "id": "36huM0UOR9A17TYAape2Xg", + "is_local": false, + "name": "Diarabi", + "popularity": 59, + "preview_url": "https://p.scdn.co/mp3-preview/d01bbe3a82e87458137fb42f8c5db3c12c5407a6?cid=cfe923b2d660439caf2b557b21f31221", + "track_number": 3, + "type": "track", + "uri": "spotify:track:36huM0UOR9A17TYAape2Xg" + }, + { + "album": { + "album_type": "SINGLE", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/74XFHRwlV6OrjEM0A2NCMF" + }, + "href": "https://api.spotify.com/v1/artists/74XFHRwlV6OrjEM0A2NCMF", + "id": "74XFHRwlV6OrjEM0A2NCMF", + "name": "Paramore", + "type": "artist", + "uri": "spotify:artist:74XFHRwlV6OrjEM0A2NCMF" + } + ], + "available_markets": [ + "AD", + "AE", + "AR", + "AT", + "AU", + "BE", + "BG", + "BH", + "BO", + "BR", + "CA", + "CH", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DE", + "DK", + "DO", + "DZ", + "EC", + "EE", + "EG", + "ES", + "FI", + "FR", + "GB", + "GR", + "GT", + "HK", + "HN", + "HU", + "ID", + "IE", + "IL", + "IN", + "IS", + "IT", + "JO", + "JP", + "KW", + "LB", + "LI", + "LT", + "LU", + "LV", + "MA", + "MC", + "MT", + "MX", + "MY", + "NI", + "NL", + "NO", + "NZ", + "OM", + "PA", + "PE", + "PH", + "PL", + "PS", + "PT", + "PY", + "QA", + "RO", + "SA", + "SE", + "SG", + "SK", + "SV", + "TH", + "TN", + "TR", + "TW", + "US", + "UY", + "VN", + "ZA" + ], + "external_urls": { + "spotify": "https://open.spotify.com/album/70iJhodSPkl7FR1VW4n0KF" + }, + "href": "https://api.spotify.com/v1/albums/70iJhodSPkl7FR1VW4n0KF", + "id": "70iJhodSPkl7FR1VW4n0KF", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab67616d0000b273b236d0eae2659e7e7adb8b56", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67616d00001e02b236d0eae2659e7e7adb8b56", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab67616d00004851b236d0eae2659e7e7adb8b56", + "width": 64 + } + ], + "name": "Decode", + "release_date": "2008-11-04", + "release_date_precision": "day", + "total_tracks": 1, + "type": "album", + "uri": "spotify:album:70iJhodSPkl7FR1VW4n0KF" + }, + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/74XFHRwlV6OrjEM0A2NCMF" + }, + "href": "https://api.spotify.com/v1/artists/74XFHRwlV6OrjEM0A2NCMF", + "id": "74XFHRwlV6OrjEM0A2NCMF", + "name": "Paramore", + "type": "artist", + "uri": "spotify:artist:74XFHRwlV6OrjEM0A2NCMF" + } + ], + "available_markets": [ + "AD", + "AE", + "AR", + "AT", + "AU", + "BE", + "BG", + "BH", + "BO", + "BR", + "CA", + "CH", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DE", + "DK", + "DO", + "DZ", + "EC", + "EE", + "EG", + "ES", + "FI", + "FR", + "GB", + "GR", + "GT", + "HK", + "HN", + "HU", + "ID", + "IE", + "IL", + "IN", + "IS", + "IT", + "JO", + "JP", + "KW", + "LB", + "LI", + "LT", + "LU", + "LV", + "MA", + "MC", + "MT", + "MX", + "MY", + "NI", + "NL", + "NO", + "NZ", + "OM", + "PA", + "PE", + "PH", + "PL", + "PS", + "PT", + "PY", + "QA", + "RO", + "SA", + "SE", + "SG", + "SK", + "SV", + "TH", + "TN", + "TR", + "TW", + "US", + "UY", + "VN", + "ZA" + ], + "disc_number": 1, + "duration_ms": 261959, + "explicit": false, + "external_ids": { + "isrc": "USAT22103220" + }, + "external_urls": { + "spotify": "https://open.spotify.com/track/1ZLtE9tSJdaUiIJ9YoKHQe" + }, + "href": "https://api.spotify.com/v1/tracks/1ZLtE9tSJdaUiIJ9YoKHQe", + "id": "1ZLtE9tSJdaUiIJ9YoKHQe", + "is_local": false, + "name": "Decode", + "popularity": 72, + "preview_url": "https://p.scdn.co/mp3-preview/a553164e09f47b3e66d451d695aa47951fe64582?cid=cfe923b2d660439caf2b557b21f31221", + "track_number": 1, + "type": "track", + "uri": "spotify:track:1ZLtE9tSJdaUiIJ9YoKHQe" + } + ], + "total": 50, + "limit": 2, + "offset": 0, + "href": "https://api.spotify.com/v1/me/top/tracks?limit=2&offset=0", + "next": "https://api.spotify.com/v1/me/top/tracks?limit=2&offset=2", + "previous": null +} \ No newline at end of file diff --git a/test/spotify_test.dart b/test/spotify_test.dart index 7e6b0b9..357e42b 100644 --- a/test/spotify_test.dart +++ b/test/spotify_test.dart @@ -13,7 +13,7 @@ Future main() async { test('get', () async { var album = await spotify.albums.get('4aawyAB9vmqN3uQ7FjRGTy'); - expect(album.albumType, 'album'); + expect(album.albumType, AlbumType.album); expect(album.id, '4aawyAB9vmqN3uQ7FjRGTy'); expect(album.images!.length, 3); expect(album.releaseDatePrecision, DatePrecision.day); @@ -261,6 +261,28 @@ Future main() async { expect(result.last, isTrue); }); + group('me/top', () { + test('tracks', () async { + final result = await spotify.me.topTracks().first(); + expect(result.items?.length, 2); + + var firstItem = result.items?.first; + expect(firstItem?.album?.albumType, AlbumType.album); + expect(firstItem?.album?.name, 'Ali'); + expect(firstItem?.album?.uri, 'spotify:album:4dfAJiDQHQf4dGX0ZdtxPh'); + expect(firstItem?.artists?.length, 2); + expect(firstItem?.artists?.first.name, 'Vieux Farka Touré'); + }); + + test('artists', () async { + final result = await spotify.me.topArtists().first(); + expect(result.items?.length, 2); + + var firstItem = result.items?.first; + expect(firstItem?.name, 'Porcupine Tree'); + }); + }); + group('me/shows', () { test('savedShows', () async { var pages = spotify.me.savedShows();