diff --git a/src/spotifyaio/models.py b/src/spotifyaio/models.py index 666955f..8f4a8f7 100644 --- a/src/spotifyaio/models.py +++ b/src/spotifyaio/models.py @@ -440,7 +440,7 @@ class PlaylistTracks(DataClassORJSONMixin): @classmethod def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]: """Pre deserialize hook.""" - items = [item for item in d["items"] if not item["is_local"]] + items = [item for item in (d.get("items") or []) if not item.get("is_local")] return {"items": items} @@ -452,6 +452,19 @@ class PlaylistTrack(DataClassORJSONMixin): added_by: AddedBy track: Annotated[Item, Discriminator(field="type", include_subtypes=True)] + @classmethod + def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]: + """Pre deserialize hook. + + Spotify's February 2026 Web API migration renamed the per-entry track + wrapper from "track" to "item". Normalize the new shape back so the + legacy "track" field keeps working; the old shape (still sent during + the deprecation window) passes through unchanged. + """ + if "track" not in d and "item" in d: + return {**d, "track": d["item"]} + return d + @dataclass class PlaylistTrackResponse(DataClassORJSONMixin): diff --git a/tests/__snapshots__/test_spotify.ambr b/tests/__snapshots__/test_spotify.ambr index b016a98..54e3d7c 100644 --- a/tests/__snapshots__/test_spotify.ambr +++ b/tests/__snapshots__/test_spotify.ambr @@ -9656,6 +9656,117 @@ 'uri': 'spotify:playlist:0pM0KTMXM7K5qr3sL2DPw1', }) # --- +# name: test_get_playlist[playlist_4.json] + dict({ + 'collaborative': False, + 'description': '', + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/playlist/1Cp6VQCKf2VL4sP09jN9oX', + }), + 'images': list([ + dict({ + 'height': None, + 'url': 'https://i.scdn.co/image/ab67616d00001e022f41071676f4f50842581fb4', + 'width': None, + }), + ]), + 'items': dict({ + 'items': list([ + dict({ + 'added_at': datetime.datetime(2025, 7, 29, 21, 11, 55, tzinfo=datetime.timezone.utc), + 'added_by': dict({ + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/user/1112264649', + }), + 'href': 'https://api.spotify.com/v1/users/1112264649', + 'uri': 'spotify:user:1112264649', + 'user_id': '1112264649', + }), + 'track': dict({ + 'album': dict({ + 'album_id': '7cynQpUNbqg7ZqYQu11Yng', + 'album_type': , + 'artists': list([ + dict({ + 'artist_id': '7zrkALJ9ayRjzysp4QYoEg', + 'name': 'Maribou State', + 'uri': 'spotify:artist:7zrkALJ9ayRjzysp4QYoEg', + }), + dict({ + 'artist_id': '6SKEuFZYhaTytrhtJjgnO2', + 'name': 'Andreya Triana', + 'uri': 'spotify:artist:6SKEuFZYhaTytrhtJjgnO2', + }), + ]), + 'images': list([ + dict({ + 'height': 640, + 'url': 'https://i.scdn.co/image/ab67616d0000b2732f41071676f4f50842581fb4', + 'width': 640, + }), + dict({ + 'height': 300, + 'url': 'https://i.scdn.co/image/ab67616d00001e022f41071676f4f50842581fb4', + 'width': 300, + }), + dict({ + 'height': 64, + 'url': 'https://i.scdn.co/image/ab67616d000048512f41071676f4f50842581fb4', + 'width': 64, + }), + ]), + 'name': 'All I Need', + 'release_date': '2025-01-13', + 'release_date_precision': , + 'total_tracks': 5, + 'uri': 'spotify:album:7cynQpUNbqg7ZqYQu11Yng', + }), + 'artists': list([ + dict({ + 'artist_id': '7zrkALJ9ayRjzysp4QYoEg', + 'name': 'Maribou State', + 'uri': 'spotify:artist:7zrkALJ9ayRjzysp4QYoEg', + }), + dict({ + 'artist_id': '6SKEuFZYhaTytrhtJjgnO2', + 'name': 'Andreya Triana', + 'uri': 'spotify:artist:6SKEuFZYhaTytrhtJjgnO2', + }), + ]), + 'disc_number': 1, + 'duration_ms': 227934, + 'explicit': False, + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/track/3ZL9gPzeCnKG9l5SB1SlcZ', + }), + 'href': 'https://api.spotify.com/v1/tracks/3ZL9gPzeCnKG9l5SB1SlcZ', + 'is_local': False, + 'name': 'All I Need', + 'track_id': '3ZL9gPzeCnKG9l5SB1SlcZ', + 'track_number': 1, + 'type': , + 'uri': 'spotify:track:3ZL9gPzeCnKG9l5SB1SlcZ', + }), + }), + ]), + }), + 'name': 'To find', + 'object_type': 'playlist', + 'owner': dict({ + 'display_name': 'Joost Lekkerkerker', + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/user/1112264649', + }), + 'href': 'https://api.spotify.com/v1/users/1112264649', + 'object_type': 'user', + 'owner_id': '1112264649', + 'uri': 'spotify:user:1112264649', + }), + 'playlist_id': '1Cp6VQCKf2VL4sP09jN9oX', + 'public': True, + 'uri': 'spotify:playlist:1Cp6VQCKf2VL4sP09jN9oX', + }) +# --- # name: test_get_playlist_cover_image list([ dict({ @@ -9665,7 +9776,87 @@ }), ]) # --- -# name: test_get_playlist_items +# name: test_get_playlist_items[playlist_items.json] + list([ + dict({ + 'added_at': datetime.datetime(2025, 7, 29, 21, 11, 55, tzinfo=datetime.timezone.utc), + 'added_by': dict({ + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/user/1112264649', + }), + 'href': 'https://api.spotify.com/v1/users/1112264649', + 'uri': 'spotify:user:1112264649', + 'user_id': '1112264649', + }), + 'track': dict({ + 'album': dict({ + 'album_id': '7cynQpUNbqg7ZqYQu11Yng', + 'album_type': , + 'artists': list([ + dict({ + 'artist_id': '7zrkALJ9ayRjzysp4QYoEg', + 'name': 'Maribou State', + 'uri': 'spotify:artist:7zrkALJ9ayRjzysp4QYoEg', + }), + dict({ + 'artist_id': '6SKEuFZYhaTytrhtJjgnO2', + 'name': 'Andreya Triana', + 'uri': 'spotify:artist:6SKEuFZYhaTytrhtJjgnO2', + }), + ]), + 'images': list([ + dict({ + 'height': 640, + 'url': 'https://i.scdn.co/image/ab67616d0000b2732f41071676f4f50842581fb4', + 'width': 640, + }), + dict({ + 'height': 300, + 'url': 'https://i.scdn.co/image/ab67616d00001e022f41071676f4f50842581fb4', + 'width': 300, + }), + dict({ + 'height': 64, + 'url': 'https://i.scdn.co/image/ab67616d000048512f41071676f4f50842581fb4', + 'width': 64, + }), + ]), + 'name': 'All I Need', + 'release_date': '2025-01-13', + 'release_date_precision': , + 'total_tracks': 5, + 'uri': 'spotify:album:7cynQpUNbqg7ZqYQu11Yng', + }), + 'artists': list([ + dict({ + 'artist_id': '7zrkALJ9ayRjzysp4QYoEg', + 'name': 'Maribou State', + 'uri': 'spotify:artist:7zrkALJ9ayRjzysp4QYoEg', + }), + dict({ + 'artist_id': '6SKEuFZYhaTytrhtJjgnO2', + 'name': 'Andreya Triana', + 'uri': 'spotify:artist:6SKEuFZYhaTytrhtJjgnO2', + }), + ]), + 'disc_number': 1, + 'duration_ms': 227934, + 'explicit': False, + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/track/3ZL9gPzeCnKG9l5SB1SlcZ', + }), + 'href': 'https://api.spotify.com/v1/tracks/3ZL9gPzeCnKG9l5SB1SlcZ', + 'is_local': False, + 'name': 'All I Need', + 'track_id': '3ZL9gPzeCnKG9l5SB1SlcZ', + 'track_number': 1, + 'type': , + 'uri': 'spotify:track:3ZL9gPzeCnKG9l5SB1SlcZ', + }), + }), + ]) +# --- +# name: test_get_playlist_items[playlist_items_2.json] list([ dict({ 'added_at': datetime.datetime(2025, 7, 29, 21, 11, 55, tzinfo=datetime.timezone.utc), diff --git a/tests/fixtures/playlist_4.json b/tests/fixtures/playlist_4.json new file mode 100644 index 0000000..2943db8 --- /dev/null +++ b/tests/fixtures/playlist_4.json @@ -0,0 +1,1503 @@ +{ + "collaborative": false, + "description": "", + "external_urls": { + "spotify": "https://open.spotify.com/playlist/1Cp6VQCKf2VL4sP09jN9oX" + }, + "followers": { + "href": null, + "total": 0 + }, + "href": "https://api.spotify.com/v1/playlists/1Cp6VQCKf2VL4sP09jN9oX?additional_types=track", + "id": "1Cp6VQCKf2VL4sP09jN9oX", + "images": [ + { + "height": null, + "url": "https://i.scdn.co/image/ab67616d00001e022f41071676f4f50842581fb4", + "width": null + } + ], + "name": "To find", + "owner": { + "display_name": "Joost Lekkerkerker", + "external_urls": { + "spotify": "https://open.spotify.com/user/1112264649" + }, + "href": "https://api.spotify.com/v1/users/1112264649", + "id": "1112264649", + "type": "user", + "uri": "spotify:user:1112264649" + }, + "primary_color": null, + "public": true, + "snapshot_id": "AAAAAqEcxS92ddZ6R83Q8TWFhYshm18s", + "tracks": { + "href": "https://api.spotify.com/v1/playlists/1Cp6VQCKf2VL4sP09jN9oX/tracks?offset=0&limit=100&additional_types=track", + "items": [ + { + "added_at": "2025-07-29T21:11:55Z", + "added_by": { + "external_urls": { + "spotify": "https://open.spotify.com/user/1112264649" + }, + "href": "https://api.spotify.com/v1/users/1112264649", + "id": "1112264649", + "type": "user", + "uri": "spotify:user:1112264649" + }, + "is_local": false, + "primary_color": null, + "item": { + "preview_url": null, + "available_markets": [ + "AR", + "AU", + "AT", + "BE", + "BO", + "BR", + "BG", + "CA", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DK", + "DO", + "DE", + "EC", + "EE", + "SV", + "FI", + "FR", + "GR", + "GT", + "HN", + "HK", + "HU", + "IS", + "IE", + "IT", + "LV", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NI", + "NO", + "PA", + "PY", + "PE", + "PH", + "PL", + "PT", + "SG", + "SK", + "ES", + "SE", + "CH", + "TW", + "TR", + "UY", + "US", + "GB", + "AD", + "LI", + "MC", + "ID", + "JP", + "TH", + "VN", + "RO", + "IL", + "ZA", + "SA", + "AE", + "BH", + "QA", + "OM", + "KW", + "EG", + "MA", + "DZ", + "TN", + "LB", + "JO", + "PS", + "IN", + "BY", + "KZ", + "MD", + "UA", + "AL", + "BA", + "HR", + "ME", + "MK", + "RS", + "SI", + "KR", + "BD", + "PK", + "LK", + "GH", + "KE", + "NG", + "TZ", + "UG", + "AG", + "AM", + "BS", + "BB", + "BZ", + "BT", + "BW", + "BF", + "CV", + "CW", + "DM", + "FJ", + "GM", + "GE", + "GD", + "GW", + "GY", + "HT", + "JM", + "KI", + "LS", + "LR", + "MW", + "MV", + "ML", + "MH", + "FM", + "NA", + "NR", + "NE", + "PW", + "PG", + "PR", + "WS", + "SM", + "ST", + "SN", + "SC", + "SL", + "SB", + "KN", + "LC", + "VC", + "SR", + "TL", + "TO", + "TT", + "TV", + "VU", + "AZ", + "BN", + "BI", + "KH", + "CM", + "TD", + "KM", + "GQ", + "SZ", + "GA", + "GN", + "KG", + "LA", + "MO", + "MR", + "MN", + "NP", + "RW", + "TG", + "UZ", + "ZW", + "BJ", + "MG", + "MU", + "MZ", + "AO", + "CI", + "DJ", + "ZM", + "CD", + "CG", + "IQ", + "LY", + "TJ", + "VE", + "ET", + "XK" + ], + "explicit": false, + "type": "track", + "episode": false, + "track": true, + "album": { + "available_markets": [ + "AR", + "AU", + "AT", + "BE", + "BO", + "BR", + "BG", + "CA", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DK", + "DO", + "DE", + "EC", + "EE", + "SV", + "FI", + "FR", + "GR", + "GT", + "HN", + "HK", + "HU", + "IS", + "IE", + "IT", + "LV", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NI", + "NO", + "PA", + "PY", + "PE", + "PH", + "PL", + "PT", + "SG", + "SK", + "ES", + "SE", + "CH", + "TW", + "TR", + "UY", + "US", + "GB", + "AD", + "LI", + "MC", + "ID", + "JP", + "TH", + "VN", + "RO", + "IL", + "ZA", + "SA", + "AE", + "BH", + "QA", + "OM", + "KW", + "EG", + "MA", + "DZ", + "TN", + "LB", + "JO", + "PS", + "IN", + "BY", + "KZ", + "MD", + "UA", + "AL", + "BA", + "HR", + "ME", + "MK", + "RS", + "SI", + "KR", + "BD", + "PK", + "LK", + "GH", + "KE", + "NG", + "TZ", + "UG", + "AG", + "AM", + "BS", + "BB", + "BZ", + "BT", + "BW", + "BF", + "CV", + "CW", + "DM", + "FJ", + "GM", + "GE", + "GD", + "GW", + "GY", + "HT", + "JM", + "KI", + "LS", + "LR", + "MW", + "MV", + "ML", + "MH", + "FM", + "NA", + "NR", + "NE", + "PW", + "PG", + "PR", + "WS", + "SM", + "ST", + "SN", + "SC", + "SL", + "SB", + "KN", + "LC", + "VC", + "SR", + "TL", + "TO", + "TT", + "TV", + "VU", + "AZ", + "BN", + "BI", + "KH", + "CM", + "TD", + "KM", + "GQ", + "SZ", + "GA", + "GN", + "KG", + "LA", + "MO", + "MR", + "MN", + "NP", + "RW", + "TG", + "UZ", + "ZW", + "BJ", + "MG", + "MU", + "MZ", + "AO", + "CI", + "DJ", + "ZM", + "CD", + "CG", + "IQ", + "LY", + "TJ", + "VE", + "ET", + "XK" + ], + "type": "album", + "album_type": "single", + "href": "https://api.spotify.com/v1/albums/7cynQpUNbqg7ZqYQu11Yng", + "id": "7cynQpUNbqg7ZqYQu11Yng", + "images": [ + { + "url": "https://i.scdn.co/image/ab67616d0000b2732f41071676f4f50842581fb4", + "width": 640, + "height": 640 + }, + { + "url": "https://i.scdn.co/image/ab67616d00001e022f41071676f4f50842581fb4", + "width": 300, + "height": 300 + }, + { + "url": "https://i.scdn.co/image/ab67616d000048512f41071676f4f50842581fb4", + "width": 64, + "height": 64 + } + ], + "name": "All I Need", + "release_date": "2025-01-13", + "release_date_precision": "day", + "uri": "spotify:album:7cynQpUNbqg7ZqYQu11Yng", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/7zrkALJ9ayRjzysp4QYoEg" + }, + "href": "https://api.spotify.com/v1/artists/7zrkALJ9ayRjzysp4QYoEg", + "id": "7zrkALJ9ayRjzysp4QYoEg", + "name": "Maribou State", + "type": "artist", + "uri": "spotify:artist:7zrkALJ9ayRjzysp4QYoEg" + }, + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/6SKEuFZYhaTytrhtJjgnO2" + }, + "href": "https://api.spotify.com/v1/artists/6SKEuFZYhaTytrhtJjgnO2", + "id": "6SKEuFZYhaTytrhtJjgnO2", + "name": "Andreya Triana", + "type": "artist", + "uri": "spotify:artist:6SKEuFZYhaTytrhtJjgnO2" + } + ], + "external_urls": { + "spotify": "https://open.spotify.com/album/7cynQpUNbqg7ZqYQu11Yng" + }, + "total_tracks": 5 + }, + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/7zrkALJ9ayRjzysp4QYoEg" + }, + "href": "https://api.spotify.com/v1/artists/7zrkALJ9ayRjzysp4QYoEg", + "id": "7zrkALJ9ayRjzysp4QYoEg", + "name": "Maribou State", + "type": "artist", + "uri": "spotify:artist:7zrkALJ9ayRjzysp4QYoEg" + }, + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/6SKEuFZYhaTytrhtJjgnO2" + }, + "href": "https://api.spotify.com/v1/artists/6SKEuFZYhaTytrhtJjgnO2", + "id": "6SKEuFZYhaTytrhtJjgnO2", + "name": "Andreya Triana", + "type": "artist", + "uri": "spotify:artist:6SKEuFZYhaTytrhtJjgnO2" + } + ], + "disc_number": 1, + "track_number": 1, + "duration_ms": 227934, + "external_ids": { + "isrc": "GBCFB2300770" + }, + "external_urls": { + "spotify": "https://open.spotify.com/track/3ZL9gPzeCnKG9l5SB1SlcZ" + }, + "href": "https://api.spotify.com/v1/tracks/3ZL9gPzeCnKG9l5SB1SlcZ", + "id": "3ZL9gPzeCnKG9l5SB1SlcZ", + "name": "All I Need", + "popularity": 46, + "uri": "spotify:track:3ZL9gPzeCnKG9l5SB1SlcZ", + "is_local": false + }, + "video_thumbnail": { + "url": null + } + } + ], + "limit": 100, + "next": null, + "offset": 0, + "previous": null, + "total": 1 + }, + "items": { + "href": "https://api.spotify.com/v1/playlists/1Cp6VQCKf2VL4sP09jN9oX/items?offset=0&limit=100&additional_types=track", + "items": [ + { + "added_at": "2025-07-29T21:11:55Z", + "added_by": { + "external_urls": { + "spotify": "https://open.spotify.com/user/1112264649" + }, + "href": "https://api.spotify.com/v1/users/1112264649", + "id": "1112264649", + "type": "user", + "uri": "spotify:user:1112264649" + }, + "is_local": false, + "primary_color": null, + "track": { + "preview_url": null, + "available_markets": [ + "AR", + "AU", + "AT", + "BE", + "BO", + "BR", + "BG", + "CA", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DK", + "DO", + "DE", + "EC", + "EE", + "SV", + "FI", + "FR", + "GR", + "GT", + "HN", + "HK", + "HU", + "IS", + "IE", + "IT", + "LV", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NI", + "NO", + "PA", + "PY", + "PE", + "PH", + "PL", + "PT", + "SG", + "SK", + "ES", + "SE", + "CH", + "TW", + "TR", + "UY", + "US", + "GB", + "AD", + "LI", + "MC", + "ID", + "JP", + "TH", + "VN", + "RO", + "IL", + "ZA", + "SA", + "AE", + "BH", + "QA", + "OM", + "KW", + "EG", + "MA", + "DZ", + "TN", + "LB", + "JO", + "PS", + "IN", + "BY", + "KZ", + "MD", + "UA", + "AL", + "BA", + "HR", + "ME", + "MK", + "RS", + "SI", + "KR", + "BD", + "PK", + "LK", + "GH", + "KE", + "NG", + "TZ", + "UG", + "AG", + "AM", + "BS", + "BB", + "BZ", + "BT", + "BW", + "BF", + "CV", + "CW", + "DM", + "FJ", + "GM", + "GE", + "GD", + "GW", + "GY", + "HT", + "JM", + "KI", + "LS", + "LR", + "MW", + "MV", + "ML", + "MH", + "FM", + "NA", + "NR", + "NE", + "PW", + "PG", + "PR", + "WS", + "SM", + "ST", + "SN", + "SC", + "SL", + "SB", + "KN", + "LC", + "VC", + "SR", + "TL", + "TO", + "TT", + "TV", + "VU", + "AZ", + "BN", + "BI", + "KH", + "CM", + "TD", + "KM", + "GQ", + "SZ", + "GA", + "GN", + "KG", + "LA", + "MO", + "MR", + "MN", + "NP", + "RW", + "TG", + "UZ", + "ZW", + "BJ", + "MG", + "MU", + "MZ", + "AO", + "CI", + "DJ", + "ZM", + "CD", + "CG", + "IQ", + "LY", + "TJ", + "VE", + "ET", + "XK" + ], + "explicit": false, + "type": "track", + "episode": false, + "track": true, + "album": { + "available_markets": [ + "AR", + "AU", + "AT", + "BE", + "BO", + "BR", + "BG", + "CA", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DK", + "DO", + "DE", + "EC", + "EE", + "SV", + "FI", + "FR", + "GR", + "GT", + "HN", + "HK", + "HU", + "IS", + "IE", + "IT", + "LV", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NI", + "NO", + "PA", + "PY", + "PE", + "PH", + "PL", + "PT", + "SG", + "SK", + "ES", + "SE", + "CH", + "TW", + "TR", + "UY", + "US", + "GB", + "AD", + "LI", + "MC", + "ID", + "JP", + "TH", + "VN", + "RO", + "IL", + "ZA", + "SA", + "AE", + "BH", + "QA", + "OM", + "KW", + "EG", + "MA", + "DZ", + "TN", + "LB", + "JO", + "PS", + "IN", + "BY", + "KZ", + "MD", + "UA", + "AL", + "BA", + "HR", + "ME", + "MK", + "RS", + "SI", + "KR", + "BD", + "PK", + "LK", + "GH", + "KE", + "NG", + "TZ", + "UG", + "AG", + "AM", + "BS", + "BB", + "BZ", + "BT", + "BW", + "BF", + "CV", + "CW", + "DM", + "FJ", + "GM", + "GE", + "GD", + "GW", + "GY", + "HT", + "JM", + "KI", + "LS", + "LR", + "MW", + "MV", + "ML", + "MH", + "FM", + "NA", + "NR", + "NE", + "PW", + "PG", + "PR", + "WS", + "SM", + "ST", + "SN", + "SC", + "SL", + "SB", + "KN", + "LC", + "VC", + "SR", + "TL", + "TO", + "TT", + "TV", + "VU", + "AZ", + "BN", + "BI", + "KH", + "CM", + "TD", + "KM", + "GQ", + "SZ", + "GA", + "GN", + "KG", + "LA", + "MO", + "MR", + "MN", + "NP", + "RW", + "TG", + "UZ", + "ZW", + "BJ", + "MG", + "MU", + "MZ", + "AO", + "CI", + "DJ", + "ZM", + "CD", + "CG", + "IQ", + "LY", + "TJ", + "VE", + "ET", + "XK" + ], + "type": "album", + "album_type": "single", + "href": "https://api.spotify.com/v1/albums/7cynQpUNbqg7ZqYQu11Yng", + "id": "7cynQpUNbqg7ZqYQu11Yng", + "images": [ + { + "url": "https://i.scdn.co/image/ab67616d0000b2732f41071676f4f50842581fb4", + "width": 640, + "height": 640 + }, + { + "url": "https://i.scdn.co/image/ab67616d00001e022f41071676f4f50842581fb4", + "width": 300, + "height": 300 + }, + { + "url": "https://i.scdn.co/image/ab67616d000048512f41071676f4f50842581fb4", + "width": 64, + "height": 64 + } + ], + "name": "All I Need", + "release_date": "2025-01-13", + "release_date_precision": "day", + "uri": "spotify:album:7cynQpUNbqg7ZqYQu11Yng", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/7zrkALJ9ayRjzysp4QYoEg" + }, + "href": "https://api.spotify.com/v1/artists/7zrkALJ9ayRjzysp4QYoEg", + "id": "7zrkALJ9ayRjzysp4QYoEg", + "name": "Maribou State", + "type": "artist", + "uri": "spotify:artist:7zrkALJ9ayRjzysp4QYoEg" + }, + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/6SKEuFZYhaTytrhtJjgnO2" + }, + "href": "https://api.spotify.com/v1/artists/6SKEuFZYhaTytrhtJjgnO2", + "id": "6SKEuFZYhaTytrhtJjgnO2", + "name": "Andreya Triana", + "type": "artist", + "uri": "spotify:artist:6SKEuFZYhaTytrhtJjgnO2" + } + ], + "external_urls": { + "spotify": "https://open.spotify.com/album/7cynQpUNbqg7ZqYQu11Yng" + }, + "total_tracks": 5 + }, + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/7zrkALJ9ayRjzysp4QYoEg" + }, + "href": "https://api.spotify.com/v1/artists/7zrkALJ9ayRjzysp4QYoEg", + "id": "7zrkALJ9ayRjzysp4QYoEg", + "name": "Maribou State", + "type": "artist", + "uri": "spotify:artist:7zrkALJ9ayRjzysp4QYoEg" + }, + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/6SKEuFZYhaTytrhtJjgnO2" + }, + "href": "https://api.spotify.com/v1/artists/6SKEuFZYhaTytrhtJjgnO2", + "id": "6SKEuFZYhaTytrhtJjgnO2", + "name": "Andreya Triana", + "type": "artist", + "uri": "spotify:artist:6SKEuFZYhaTytrhtJjgnO2" + } + ], + "disc_number": 1, + "track_number": 1, + "duration_ms": 227934, + "external_ids": { + "isrc": "GBCFB2300770" + }, + "external_urls": { + "spotify": "https://open.spotify.com/track/3ZL9gPzeCnKG9l5SB1SlcZ" + }, + "href": "https://api.spotify.com/v1/tracks/3ZL9gPzeCnKG9l5SB1SlcZ", + "id": "3ZL9gPzeCnKG9l5SB1SlcZ", + "name": "All I Need", + "popularity": 46, + "uri": "spotify:track:3ZL9gPzeCnKG9l5SB1SlcZ", + "is_local": false + }, + "item": { + "preview_url": null, + "available_markets": [ + "AR", + "AU", + "AT", + "BE", + "BO", + "BR", + "BG", + "CA", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DK", + "DO", + "DE", + "EC", + "EE", + "SV", + "FI", + "FR", + "GR", + "GT", + "HN", + "HK", + "HU", + "IS", + "IE", + "IT", + "LV", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NI", + "NO", + "PA", + "PY", + "PE", + "PH", + "PL", + "PT", + "SG", + "SK", + "ES", + "SE", + "CH", + "TW", + "TR", + "UY", + "US", + "GB", + "AD", + "LI", + "MC", + "ID", + "JP", + "TH", + "VN", + "RO", + "IL", + "ZA", + "SA", + "AE", + "BH", + "QA", + "OM", + "KW", + "EG", + "MA", + "DZ", + "TN", + "LB", + "JO", + "PS", + "IN", + "BY", + "KZ", + "MD", + "UA", + "AL", + "BA", + "HR", + "ME", + "MK", + "RS", + "SI", + "KR", + "BD", + "PK", + "LK", + "GH", + "KE", + "NG", + "TZ", + "UG", + "AG", + "AM", + "BS", + "BB", + "BZ", + "BT", + "BW", + "BF", + "CV", + "CW", + "DM", + "FJ", + "GM", + "GE", + "GD", + "GW", + "GY", + "HT", + "JM", + "KI", + "LS", + "LR", + "MW", + "MV", + "ML", + "MH", + "FM", + "NA", + "NR", + "NE", + "PW", + "PG", + "PR", + "WS", + "SM", + "ST", + "SN", + "SC", + "SL", + "SB", + "KN", + "LC", + "VC", + "SR", + "TL", + "TO", + "TT", + "TV", + "VU", + "AZ", + "BN", + "BI", + "KH", + "CM", + "TD", + "KM", + "GQ", + "SZ", + "GA", + "GN", + "KG", + "LA", + "MO", + "MR", + "MN", + "NP", + "RW", + "TG", + "UZ", + "ZW", + "BJ", + "MG", + "MU", + "MZ", + "AO", + "CI", + "DJ", + "ZM", + "CD", + "CG", + "IQ", + "LY", + "TJ", + "VE", + "ET", + "XK" + ], + "explicit": false, + "type": "track", + "episode": false, + "track": true, + "album": { + "available_markets": [ + "AR", + "AU", + "AT", + "BE", + "BO", + "BR", + "BG", + "CA", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DK", + "DO", + "DE", + "EC", + "EE", + "SV", + "FI", + "FR", + "GR", + "GT", + "HN", + "HK", + "HU", + "IS", + "IE", + "IT", + "LV", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NI", + "NO", + "PA", + "PY", + "PE", + "PH", + "PL", + "PT", + "SG", + "SK", + "ES", + "SE", + "CH", + "TW", + "TR", + "UY", + "US", + "GB", + "AD", + "LI", + "MC", + "ID", + "JP", + "TH", + "VN", + "RO", + "IL", + "ZA", + "SA", + "AE", + "BH", + "QA", + "OM", + "KW", + "EG", + "MA", + "DZ", + "TN", + "LB", + "JO", + "PS", + "IN", + "BY", + "KZ", + "MD", + "UA", + "AL", + "BA", + "HR", + "ME", + "MK", + "RS", + "SI", + "KR", + "BD", + "PK", + "LK", + "GH", + "KE", + "NG", + "TZ", + "UG", + "AG", + "AM", + "BS", + "BB", + "BZ", + "BT", + "BW", + "BF", + "CV", + "CW", + "DM", + "FJ", + "GM", + "GE", + "GD", + "GW", + "GY", + "HT", + "JM", + "KI", + "LS", + "LR", + "MW", + "MV", + "ML", + "MH", + "FM", + "NA", + "NR", + "NE", + "PW", + "PG", + "PR", + "WS", + "SM", + "ST", + "SN", + "SC", + "SL", + "SB", + "KN", + "LC", + "VC", + "SR", + "TL", + "TO", + "TT", + "TV", + "VU", + "AZ", + "BN", + "BI", + "KH", + "CM", + "TD", + "KM", + "GQ", + "SZ", + "GA", + "GN", + "KG", + "LA", + "MO", + "MR", + "MN", + "NP", + "RW", + "TG", + "UZ", + "ZW", + "BJ", + "MG", + "MU", + "MZ", + "AO", + "CI", + "DJ", + "ZM", + "CD", + "CG", + "IQ", + "LY", + "TJ", + "VE", + "ET", + "XK" + ], + "type": "album", + "album_type": "single", + "href": "https://api.spotify.com/v1/albums/7cynQpUNbqg7ZqYQu11Yng", + "id": "7cynQpUNbqg7ZqYQu11Yng", + "images": [ + { + "url": "https://i.scdn.co/image/ab67616d0000b2732f41071676f4f50842581fb4", + "width": 640, + "height": 640 + }, + { + "url": "https://i.scdn.co/image/ab67616d00001e022f41071676f4f50842581fb4", + "width": 300, + "height": 300 + }, + { + "url": "https://i.scdn.co/image/ab67616d000048512f41071676f4f50842581fb4", + "width": 64, + "height": 64 + } + ], + "name": "All I Need", + "release_date": "2025-01-13", + "release_date_precision": "day", + "uri": "spotify:album:7cynQpUNbqg7ZqYQu11Yng", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/7zrkALJ9ayRjzysp4QYoEg" + }, + "href": "https://api.spotify.com/v1/artists/7zrkALJ9ayRjzysp4QYoEg", + "id": "7zrkALJ9ayRjzysp4QYoEg", + "name": "Maribou State", + "type": "artist", + "uri": "spotify:artist:7zrkALJ9ayRjzysp4QYoEg" + }, + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/6SKEuFZYhaTytrhtJjgnO2" + }, + "href": "https://api.spotify.com/v1/artists/6SKEuFZYhaTytrhtJjgnO2", + "id": "6SKEuFZYhaTytrhtJjgnO2", + "name": "Andreya Triana", + "type": "artist", + "uri": "spotify:artist:6SKEuFZYhaTytrhtJjgnO2" + } + ], + "external_urls": { + "spotify": "https://open.spotify.com/album/7cynQpUNbqg7ZqYQu11Yng" + }, + "total_tracks": 5 + }, + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/7zrkALJ9ayRjzysp4QYoEg" + }, + "href": "https://api.spotify.com/v1/artists/7zrkALJ9ayRjzysp4QYoEg", + "id": "7zrkALJ9ayRjzysp4QYoEg", + "name": "Maribou State", + "type": "artist", + "uri": "spotify:artist:7zrkALJ9ayRjzysp4QYoEg" + }, + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/6SKEuFZYhaTytrhtJjgnO2" + }, + "href": "https://api.spotify.com/v1/artists/6SKEuFZYhaTytrhtJjgnO2", + "id": "6SKEuFZYhaTytrhtJjgnO2", + "name": "Andreya Triana", + "type": "artist", + "uri": "spotify:artist:6SKEuFZYhaTytrhtJjgnO2" + } + ], + "disc_number": 1, + "track_number": 1, + "duration_ms": 227934, + "external_ids": { + "isrc": "GBCFB2300770" + }, + "external_urls": { + "spotify": "https://open.spotify.com/track/3ZL9gPzeCnKG9l5SB1SlcZ" + }, + "href": "https://api.spotify.com/v1/tracks/3ZL9gPzeCnKG9l5SB1SlcZ", + "id": "3ZL9gPzeCnKG9l5SB1SlcZ", + "name": "All I Need", + "popularity": 46, + "uri": "spotify:track:3ZL9gPzeCnKG9l5SB1SlcZ", + "is_local": false + }, + "video_thumbnail": { + "url": null + } + } + ], + "limit": 100, + "next": null, + "offset": 0, + "previous": null, + "total": 1 + }, + "type": "playlist", + "uri": "spotify:playlist:1Cp6VQCKf2VL4sP09jN9oX" +} \ No newline at end of file diff --git a/tests/fixtures/playlist_items_2.json b/tests/fixtures/playlist_items_2.json new file mode 100644 index 0000000..2bf18aa --- /dev/null +++ b/tests/fixtures/playlist_items_2.json @@ -0,0 +1,498 @@ +{ + "href": "https://api.spotify.com/v1/playlists/1Cp6VQCKf2VL4sP09jN9oX/items?offset=0&limit=48", + "items": [ + { + "added_at": "2025-07-29T21:11:55Z", + "added_by": { + "external_urls": { + "spotify": "https://open.spotify.com/user/1112264649" + }, + "href": "https://api.spotify.com/v1/users/1112264649", + "id": "1112264649", + "type": "user", + "uri": "spotify:user:1112264649" + }, + "is_local": false, + "primary_color": null, + "item": { + "preview_url": null, + "available_markets": [ + "AR", + "AU", + "AT", + "BE", + "BO", + "BR", + "BG", + "CA", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DK", + "DO", + "DE", + "EC", + "EE", + "SV", + "FI", + "FR", + "GR", + "GT", + "HN", + "HK", + "HU", + "IS", + "IE", + "IT", + "LV", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NI", + "NO", + "PA", + "PY", + "PE", + "PH", + "PL", + "PT", + "SG", + "SK", + "ES", + "SE", + "CH", + "TW", + "TR", + "UY", + "US", + "GB", + "AD", + "LI", + "MC", + "ID", + "JP", + "TH", + "VN", + "RO", + "IL", + "ZA", + "SA", + "AE", + "BH", + "QA", + "OM", + "KW", + "EG", + "MA", + "DZ", + "TN", + "LB", + "JO", + "PS", + "IN", + "BY", + "KZ", + "MD", + "UA", + "AL", + "BA", + "HR", + "ME", + "MK", + "RS", + "SI", + "KR", + "BD", + "PK", + "LK", + "GH", + "KE", + "NG", + "TZ", + "UG", + "AG", + "AM", + "BS", + "BB", + "BZ", + "BT", + "BW", + "BF", + "CV", + "CW", + "DM", + "FJ", + "GM", + "GE", + "GD", + "GW", + "GY", + "HT", + "JM", + "KI", + "LS", + "LR", + "MW", + "MV", + "ML", + "MH", + "FM", + "NA", + "NR", + "NE", + "PW", + "PG", + "PR", + "WS", + "SM", + "ST", + "SN", + "SC", + "SL", + "SB", + "KN", + "LC", + "VC", + "SR", + "TL", + "TO", + "TT", + "TV", + "VU", + "AZ", + "BN", + "BI", + "KH", + "CM", + "TD", + "KM", + "GQ", + "SZ", + "GA", + "GN", + "KG", + "LA", + "MO", + "MR", + "MN", + "NP", + "RW", + "TG", + "UZ", + "ZW", + "BJ", + "MG", + "MU", + "MZ", + "AO", + "CI", + "DJ", + "ZM", + "CD", + "CG", + "IQ", + "LY", + "TJ", + "VE", + "ET", + "XK" + ], + "explicit": false, + "type": "track", + "episode": false, + "track": true, + "album": { + "available_markets": [ + "AR", + "AU", + "AT", + "BE", + "BO", + "BR", + "BG", + "CA", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DK", + "DO", + "DE", + "EC", + "EE", + "SV", + "FI", + "FR", + "GR", + "GT", + "HN", + "HK", + "HU", + "IS", + "IE", + "IT", + "LV", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NI", + "NO", + "PA", + "PY", + "PE", + "PH", + "PL", + "PT", + "SG", + "SK", + "ES", + "SE", + "CH", + "TW", + "TR", + "UY", + "US", + "GB", + "AD", + "LI", + "MC", + "ID", + "JP", + "TH", + "VN", + "RO", + "IL", + "ZA", + "SA", + "AE", + "BH", + "QA", + "OM", + "KW", + "EG", + "MA", + "DZ", + "TN", + "LB", + "JO", + "PS", + "IN", + "BY", + "KZ", + "MD", + "UA", + "AL", + "BA", + "HR", + "ME", + "MK", + "RS", + "SI", + "KR", + "BD", + "PK", + "LK", + "GH", + "KE", + "NG", + "TZ", + "UG", + "AG", + "AM", + "BS", + "BB", + "BZ", + "BT", + "BW", + "BF", + "CV", + "CW", + "DM", + "FJ", + "GM", + "GE", + "GD", + "GW", + "GY", + "HT", + "JM", + "KI", + "LS", + "LR", + "MW", + "MV", + "ML", + "MH", + "FM", + "NA", + "NR", + "NE", + "PW", + "PG", + "PR", + "WS", + "SM", + "ST", + "SN", + "SC", + "SL", + "SB", + "KN", + "LC", + "VC", + "SR", + "TL", + "TO", + "TT", + "TV", + "VU", + "AZ", + "BN", + "BI", + "KH", + "CM", + "TD", + "KM", + "GQ", + "SZ", + "GA", + "GN", + "KG", + "LA", + "MO", + "MR", + "MN", + "NP", + "RW", + "TG", + "UZ", + "ZW", + "BJ", + "MG", + "MU", + "MZ", + "AO", + "CI", + "DJ", + "ZM", + "CD", + "CG", + "IQ", + "LY", + "TJ", + "VE", + "ET", + "XK" + ], + "type": "album", + "album_type": "single", + "href": "https://api.spotify.com/v1/albums/7cynQpUNbqg7ZqYQu11Yng", + "id": "7cynQpUNbqg7ZqYQu11Yng", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab67616d0000b2732f41071676f4f50842581fb4", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67616d00001e022f41071676f4f50842581fb4", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab67616d000048512f41071676f4f50842581fb4", + "width": 64 + } + ], + "name": "All I Need", + "release_date": "2025-01-13", + "release_date_precision": "day", + "uri": "spotify:album:7cynQpUNbqg7ZqYQu11Yng", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/7zrkALJ9ayRjzysp4QYoEg" + }, + "href": "https://api.spotify.com/v1/artists/7zrkALJ9ayRjzysp4QYoEg", + "id": "7zrkALJ9ayRjzysp4QYoEg", + "name": "Maribou State", + "type": "artist", + "uri": "spotify:artist:7zrkALJ9ayRjzysp4QYoEg" + }, + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/6SKEuFZYhaTytrhtJjgnO2" + }, + "href": "https://api.spotify.com/v1/artists/6SKEuFZYhaTytrhtJjgnO2", + "id": "6SKEuFZYhaTytrhtJjgnO2", + "name": "Andreya Triana", + "type": "artist", + "uri": "spotify:artist:6SKEuFZYhaTytrhtJjgnO2" + } + ], + "external_urls": { + "spotify": "https://open.spotify.com/album/7cynQpUNbqg7ZqYQu11Yng" + }, + "total_tracks": 5 + }, + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/7zrkALJ9ayRjzysp4QYoEg" + }, + "href": "https://api.spotify.com/v1/artists/7zrkALJ9ayRjzysp4QYoEg", + "id": "7zrkALJ9ayRjzysp4QYoEg", + "name": "Maribou State", + "type": "artist", + "uri": "spotify:artist:7zrkALJ9ayRjzysp4QYoEg" + }, + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/6SKEuFZYhaTytrhtJjgnO2" + }, + "href": "https://api.spotify.com/v1/artists/6SKEuFZYhaTytrhtJjgnO2", + "id": "6SKEuFZYhaTytrhtJjgnO2", + "name": "Andreya Triana", + "type": "artist", + "uri": "spotify:artist:6SKEuFZYhaTytrhtJjgnO2" + } + ], + "disc_number": 1, + "track_number": 1, + "duration_ms": 227934, + "external_ids": { + "isrc": "GBCFB2300770" + }, + "external_urls": { + "spotify": "https://open.spotify.com/track/3ZL9gPzeCnKG9l5SB1SlcZ" + }, + "href": "https://api.spotify.com/v1/tracks/3ZL9gPzeCnKG9l5SB1SlcZ", + "id": "3ZL9gPzeCnKG9l5SB1SlcZ", + "name": "All I Need", + "popularity": 46, + "uri": "spotify:track:3ZL9gPzeCnKG9l5SB1SlcZ", + "is_local": false + }, + "video_thumbnail": { + "url": null + } + } + ], + "limit": 48, + "next": null, + "offset": 0, + "previous": null, + "total": 1 +} \ No newline at end of file diff --git a/tests/test_spotify.py b/tests/test_spotify.py index 965cefa..50118ea 100644 --- a/tests/test_spotify.py +++ b/tests/test_spotify.py @@ -660,6 +660,7 @@ async def test_get_album( "playlist_1.json", "playlist_2.json", "playlist_3.json", + "playlist_4.json", ], ) async def test_get_playlist( @@ -1197,16 +1198,24 @@ async def test_update_playlist_details( ) +@pytest.mark.parametrize( + "fixture", + [ + "playlist_items.json", + "playlist_items_2.json", + ], +) async def test_get_playlist_items( responses: aioresponses, snapshot: SnapshotAssertion, authenticated_client: SpotifyClient, + fixture: str, ) -> None: """Test retrieving playlist items.""" responses.get( f"{SPOTIFY_URL}/v1/playlists/1Cp6VQCKf2VL4sP09jN9oX/items?limit=48", status=200, - body=load_fixture("playlist_items.json"), + body=load_fixture(fixture), ) response = await authenticated_client.get_playlist_items("1Cp6VQCKf2VL4sP09jN9oX") assert response == snapshot