diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 4f07930..e4d016c 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -8,21 +8,10 @@ - - - + diff --git a/core/auth/src/main/java/com/example/mymusic/core/auth/AuthorizationManager.kt b/core/auth/src/main/java/com/example/mymusic/core/auth/AuthorizationManager.kt index e455909..7423409 100644 --- a/core/auth/src/main/java/com/example/mymusic/core/auth/AuthorizationManager.kt +++ b/core/auth/src/main/java/com/example/mymusic/core/auth/AuthorizationManager.kt @@ -7,6 +7,7 @@ import android.text.TextUtils import android.util.Base64 import android.util.Log import com.example.mymusic.core.common.ApplicationScope +import com.example.mymusic.core.common.Constants import com.example.mymusic.core.common.IoDispatcher import com.example.mymusic.core.datastore.MyMusicPreferencesDataSource import dagger.hilt.android.internal.Contexts @@ -54,10 +55,10 @@ class AuthorizationManager @Inject constructor( private fun initAuthServiceConfig() { _authServiceConfig = AuthorizationServiceConfiguration( - Uri.parse(com.example.mymusic.core.common.Constants.URL_AUTHORIZATION), - Uri.parse(com.example.mymusic.core.common.Constants.URL_TOKEN_EXCHANGE), + Uri.parse(Constants.URL_AUTHORIZATION), + Uri.parse(Constants.URL_TOKEN_EXCHANGE), null, - Uri.parse(com.example.mymusic.core.common.Constants.URL_AUTH_REDIRECT)) + Uri.parse(Constants.URL_AUTH_REDIRECT)) } private fun initAuthService() { @@ -102,28 +103,29 @@ class AuthorizationManager @Inject constructor( val encoding = Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP val codeVerifier = Base64.encodeToString(bytes, encoding) - val digest = MessageDigest.getInstance(com.example.mymusic.core.common.Constants.MESSAGE_DIGEST_ALGORITHM) + val digest = MessageDigest.getInstance(Constants.MESSAGE_DIGEST_ALGORITHM) val hash = digest.digest(codeVerifier.toByteArray()) val codeChallenge = Base64.encodeToString(hash, encoding) val builder = AuthorizationRequest.Builder( _authServiceConfig, - com.example.mymusic.core.common.Constants.CLIENT_ID, + Constants.CLIENT_ID, ResponseTypeValues.CODE, - Uri.parse(com.example.mymusic.core.common.Constants.URL_AUTH_REDIRECT)) + Uri.parse(Constants.URL_AUTH_REDIRECT)) .setCodeVerifier(codeVerifier, codeChallenge, - com.example.mymusic.core.common.Constants.CODE_VERIFIER_CHALLENGE_METHOD) + Constants.CODE_VERIFIER_CHALLENGE_METHOD) builder.setScopes( - com.example.mymusic.core.common.Constants.SCOPE_STREAMING, - com.example.mymusic.core.common.Constants.SCOPE_EMAIL, - com.example.mymusic.core.common.Constants.SCOPE_APP_REMOTE_CONTROL, - com.example.mymusic.core.common.Constants.SCOPE_USER_MODIFY_PLAYBACK_STATE, - com.example.mymusic.core.common.Constants.SCOPE_USER_READ_PRIVATE, - com.example.mymusic.core.common.Constants.SCOPE_USER_READ_RECENTLY_PLAYED, - com.example.mymusic.core.common.Constants.SCOPE_USER_LIBRARY_READ, - com.example.mymusic.core.common.Constants.SCOPE_PLAYLIST_READ_PRIVATE + Constants.SCOPE_STREAMING, + Constants.SCOPE_EMAIL, + Constants.SCOPE_APP_REMOTE_CONTROL, + Constants.SCOPE_USER_MODIFY_PLAYBACK_STATE, + Constants.SCOPE_USER_READ_PRIVATE, + Constants.SCOPE_USER_READ_RECENTLY_PLAYED, + Constants.SCOPE_USER_LIBRARY_READ, + Constants.SCOPE_PLAYLIST_READ_PRIVATE, + Constants.SCOPE_USER_TOP_READ ) val request = builder.build() diff --git a/core/common/src/main/java/com/example/mymusic/core/common/Constants.kt b/core/common/src/main/java/com/example/mymusic/core/common/Constants.kt index d980635..16f74d3 100644 --- a/core/common/src/main/java/com/example/mymusic/core/common/Constants.kt +++ b/core/common/src/main/java/com/example/mymusic/core/common/Constants.kt @@ -12,6 +12,7 @@ class Constants { const val SCOPE_USER_READ_CURRENTLY_PLAYING = "user-read-currently-playing" const val SCOPE_APP_REMOTE_CONTROL = "app-remote-control" const val SCOPE_STREAMING = "streaming" + const val SCOPE_USER_TOP_READ = "user-top-read" const val SCOPE_PLAYLIST_READ_PRIVATE = "playlist-read-private" const val SCOPE_PLAYLIST_MODIFY_PRIVATE = "playlist-modify-private" const val SCOPE_PLAYLIST_MODIFY_PUBLIC = "playlist-modify-public" diff --git a/core/data/src/main/java/com/example/mymusic/core/data/repository/OfflineFirstMusicRepository.kt b/core/data/src/main/java/com/example/mymusic/core/data/repository/OfflineFirstMusicRepository.kt index d6a0b51..396a3e1 100644 --- a/core/data/src/main/java/com/example/mymusic/core/data/repository/OfflineFirstMusicRepository.kt +++ b/core/data/src/main/java/com/example/mymusic/core/data/repository/OfflineFirstMusicRepository.kt @@ -171,7 +171,10 @@ class OfflineFirstMusicRepository @Inject constructor( override suspend fun refresh() { withContext(dispatcher) { - val remoteMusic = getRecommendations() + // Get recommendations endpoint is deprecated + //val remoteMusic = getRecommendations() + + val remoteMusic = networkDataSource.getTopItems("tracks") if (remoteMusic.isNotEmpty()) { diff --git a/core/database/src/androidTest/java/com/example/mymusic/core/database/MusicDaoTest.kt b/core/database/src/androidTest/java/com/example/mymusic/core/database/MusicDaoTest.kt index 59205a9..974c031 100644 --- a/core/database/src/androidTest/java/com/example/mymusic/core/database/MusicDaoTest.kt +++ b/core/database/src/androidTest/java/com/example/mymusic/core/database/MusicDaoTest.kt @@ -448,3 +448,5 @@ class MusicDaoTest { primaryColor = null ) } + + diff --git a/core/network/src/main/java/com/example/mymusic/core/network/MyMusicAPIService.kt b/core/network/src/main/java/com/example/mymusic/core/network/MyMusicAPIService.kt index cb7723a..b0d3704 100644 --- a/core/network/src/main/java/com/example/mymusic/core/network/MyMusicAPIService.kt +++ b/core/network/src/main/java/com/example/mymusic/core/network/MyMusicAPIService.kt @@ -8,6 +8,7 @@ import com.example.mymusic.core.network.model.RecentlyPlayedTracksResponse import com.example.mymusic.core.network.model.SavedAlbumsResponse import com.example.mymusic.core.network.model.SavedPlaylistResponse import com.example.mymusic.core.network.model.SpotifyTrack +import com.example.mymusic.core.network.model.UsersTopItemsResponse import com.haroldadmin.cnradapter.NetworkResponse import retrofit2.http.GET import retrofit2.http.Path @@ -15,6 +16,11 @@ import retrofit2.http.Query interface MyMusicAPIService { + @GET("https://api.spotify.com/v1/me/top/{type}") + suspend fun getTopItems( + @Path("type") type: String + ): NetworkResponse + @GET("https://api.spotify.com/v1/recommendations?limit=10&seed_genres=pop") suspend fun getRecommendations(): NetworkResponse diff --git a/core/network/src/main/java/com/example/mymusic/core/network/MyMusicNetworkDataSource.kt b/core/network/src/main/java/com/example/mymusic/core/network/MyMusicNetworkDataSource.kt index f05fd31..f395816 100644 --- a/core/network/src/main/java/com/example/mymusic/core/network/MyMusicNetworkDataSource.kt +++ b/core/network/src/main/java/com/example/mymusic/core/network/MyMusicNetworkDataSource.kt @@ -8,6 +8,7 @@ import com.example.mymusic.core.network.model.SpotifySimplifiedTrack import com.example.mymusic.core.network.model.SpotifyTrack interface MyMusicNetworkDataSource { + suspend fun getTopItems(type: String): List suspend fun getRecommendations(): List diff --git a/core/network/src/main/java/com/example/mymusic/core/network/RetrofitNetworkDataSource.kt b/core/network/src/main/java/com/example/mymusic/core/network/RetrofitNetworkDataSource.kt index 3f8a6f8..c22142a 100644 --- a/core/network/src/main/java/com/example/mymusic/core/network/RetrofitNetworkDataSource.kt +++ b/core/network/src/main/java/com/example/mymusic/core/network/RetrofitNetworkDataSource.kt @@ -10,12 +10,19 @@ import com.example.mymusic.core.network.model.SavedAlbumsResponse import com.example.mymusic.core.network.model.SavedPlaylistResponse import com.example.mymusic.core.network.model.SpotifySimplifiedTrack import com.example.mymusic.core.network.model.SpotifyTrack +import com.example.mymusic.core.network.model.UsersTopItemsResponse import com.haroldadmin.cnradapter.NetworkResponse import javax.inject.Inject class RetrofitNetworkDataSource @Inject constructor( private val apiService: MyMusicAPIService ): MyMusicNetworkDataSource { + override suspend fun getTopItems(type: String): List { + val response = apiService.getTopItems(type) + val data = (response as? NetworkResponse.Success?)?.body?.items ?: emptyList() + + return processResponse(response, data, emptyList()) + } override suspend fun getRecommendations(): List { val response = apiService.getRecommendations() diff --git a/core/network/src/main/java/com/example/mymusic/core/network/fake/FakeNetworkDataSource.kt b/core/network/src/main/java/com/example/mymusic/core/network/fake/FakeNetworkDataSource.kt index 3e02e85..ffceabd 100644 --- a/core/network/src/main/java/com/example/mymusic/core/network/fake/FakeNetworkDataSource.kt +++ b/core/network/src/main/java/com/example/mymusic/core/network/fake/FakeNetworkDataSource.kt @@ -13,6 +13,7 @@ import com.example.mymusic.core.network.model.SavedAlbumsResponse import com.example.mymusic.core.network.model.SavedPlaylistResponse import com.example.mymusic.core.network.model.SpotifySimplifiedTrack import com.example.mymusic.core.network.model.SpotifyTrack +import com.example.mymusic.core.network.model.UsersTopItemsResponse import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext @@ -26,6 +27,14 @@ class FakeNetworkDataSource @Inject constructor( @IoDispatcher val dispatcher: CoroutineDispatcher, @ApplicationContext val context: Context ) : MyMusicNetworkDataSource { + override suspend fun getTopItems(type: String): List = + withContext(dispatcher) { + val inputStream = context.resources.openRawResource(R.raw.top_items) + .bufferedReader().use { it.readText() } + + val response = Json.decodeFromString(inputStream) + response.items + } override suspend fun getRecommendations(): List = withContext(dispatcher) { diff --git a/core/network/src/main/java/com/example/mymusic/core/network/model/UsersTopItemsResponse.kt b/core/network/src/main/java/com/example/mymusic/core/network/model/UsersTopItemsResponse.kt new file mode 100644 index 0000000..caa440f --- /dev/null +++ b/core/network/src/main/java/com/example/mymusic/core/network/model/UsersTopItemsResponse.kt @@ -0,0 +1,14 @@ +package com.example.mymusic.core.network.model + +import kotlinx.serialization.Serializable + +@Serializable +data class UsersTopItemsResponse( + val href: String, + val limit: Int, + val next: String?, + val offset: Int, + val previous: String?, + val total: String, + val items: List // OneOf issue https://developer.spotify.com/documentation/web-api/reference/get-users-top-artists-and-tracks +) \ No newline at end of file diff --git a/core/network/src/main/res/raw/top_items.json b/core/network/src/main/res/raw/top_items.json new file mode 100644 index 0000000..f606adf --- /dev/null +++ b/core/network/src/main/res/raw/top_items.json @@ -0,0 +1,1606 @@ +{ + "href": "https://api.spotify.com/v1/playlists/3cEYpjA9oz9GiPac4AsH4n/tracks?offset=0&limit=100&locale=en-US,en;q%3D0.5", + "items": [ + { + "added_at": "2015-01-15T12:39:22Z", + "added_by": { + "external_urls": { + "spotify": "https://open.spotify.com/user/jmperezperez" + }, + "href": "https://api.spotify.com/v1/users/jmperezperez", + "id": "jmperezperez", + "type": "user", + "uri": "spotify:user:jmperezperez" + }, + "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": "compilation", + "href": "https://api.spotify.com/v1/albums/2pANdqPvxInB0YvcDiw4ko", + "id": "2pANdqPvxInB0YvcDiw4ko", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab67616d0000b273ce6d0eef0c1ce77e5f95bbbc", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67616d00001e02ce6d0eef0c1ce77e5f95bbbc", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab67616d00004851ce6d0eef0c1ce77e5f95bbbc", + "width": 64 + } + ], + "name": "Progressive Psy Trance Picks Vol.8", + "release_date": "2012-04-02", + "release_date_precision": "day", + "uri": "spotify:album:2pANdqPvxInB0YvcDiw4ko", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/0LyfQWJT6nXafLPZqxe9Of" + }, + "href": "https://api.spotify.com/v1/artists/0LyfQWJT6nXafLPZqxe9Of", + "id": "0LyfQWJT6nXafLPZqxe9Of", + "name": "Various Artists", + "type": "artist", + "uri": "spotify:artist:0LyfQWJT6nXafLPZqxe9Of" + } + ], + "external_urls": { + "spotify": "https://open.spotify.com/album/2pANdqPvxInB0YvcDiw4ko" + }, + "total_tracks": 20 + }, + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/6eSdhw46riw2OUHgMwR8B5" + }, + "href": "https://api.spotify.com/v1/artists/6eSdhw46riw2OUHgMwR8B5", + "id": "6eSdhw46riw2OUHgMwR8B5", + "name": "Odiseo", + "type": "artist", + "uri": "spotify:artist:6eSdhw46riw2OUHgMwR8B5" + } + ], + "disc_number": 1, + "track_number": 10, + "duration_ms": 376000, + "external_ids": { + "isrc": "DEKC41200989" + }, + "external_urls": { + "spotify": "https://open.spotify.com/track/4rzfv0JLZfVhOhbSQ8o5jZ" + }, + "href": "https://api.spotify.com/v1/tracks/4rzfv0JLZfVhOhbSQ8o5jZ", + "id": "4rzfv0JLZfVhOhbSQ8o5jZ", + "name": "Api", + "popularity": 1, + "uri": "spotify:track:4rzfv0JLZfVhOhbSQ8o5jZ", + "is_local": false + }, + "video_thumbnail": { + "url": null + } + }, + { + "added_at": "2015-01-15T12:40:03Z", + "added_by": { + "external_urls": { + "spotify": "https://open.spotify.com/user/jmperezperez" + }, + "href": "https://api.spotify.com/v1/users/jmperezperez", + "id": "jmperezperez", + "type": "user", + "uri": "spotify:user:jmperezperez" + }, + "is_local": false, + "primary_color": null, + "track": { + "preview_url": null, + "available_markets": [], + "explicit": false, + "type": "track", + "episode": false, + "track": true, + "album": { + "available_markets": [], + "type": "album", + "album_type": "compilation", + "href": "https://api.spotify.com/v1/albums/6nlfkk5GoXRL1nktlATNsy", + "id": "6nlfkk5GoXRL1nktlATNsy", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab67616d0000b273aa2ff29970d9a63a49dfaeb2", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67616d00001e02aa2ff29970d9a63a49dfaeb2", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab67616d00004851aa2ff29970d9a63a49dfaeb2", + "width": 64 + } + ], + "name": "Wellness & Dreaming Source", + "release_date": "2015-01-09", + "release_date_precision": "day", + "uri": "spotify:album:6nlfkk5GoXRL1nktlATNsy", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/0LyfQWJT6nXafLPZqxe9Of" + }, + "href": "https://api.spotify.com/v1/artists/0LyfQWJT6nXafLPZqxe9Of", + "id": "0LyfQWJT6nXafLPZqxe9Of", + "name": "Various Artists", + "type": "artist", + "uri": "spotify:artist:0LyfQWJT6nXafLPZqxe9Of" + } + ], + "external_urls": { + "spotify": "https://open.spotify.com/album/6nlfkk5GoXRL1nktlATNsy" + }, + "total_tracks": 25 + }, + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/5VQE4WOzPu9h3HnGLuBoA6" + }, + "href": "https://api.spotify.com/v1/artists/5VQE4WOzPu9h3HnGLuBoA6", + "id": "5VQE4WOzPu9h3HnGLuBoA6", + "name": "Vlasta Marek", + "type": "artist", + "uri": "spotify:artist:5VQE4WOzPu9h3HnGLuBoA6" + } + ], + "disc_number": 1, + "track_number": 21, + "duration_ms": 730066, + "external_ids": { + "isrc": "FR2X41475057" + }, + "external_urls": { + "spotify": "https://open.spotify.com/track/5o3jMYOSbaVz3tkgwhELSV" + }, + "href": "https://api.spotify.com/v1/tracks/5o3jMYOSbaVz3tkgwhELSV", + "id": "5o3jMYOSbaVz3tkgwhELSV", + "name": "Is", + "popularity": 0, + "uri": "spotify:track:5o3jMYOSbaVz3tkgwhELSV", + "is_local": false + }, + "video_thumbnail": { + "url": null + } + }, + { + "added_at": "2015-01-15T12:22:30Z", + "added_by": { + "external_urls": { + "spotify": "https://open.spotify.com/user/jmperezperez" + }, + "href": "https://api.spotify.com/v1/users/jmperezperez", + "id": "jmperezperez", + "type": "user", + "uri": "spotify:user:jmperezperez" + }, + "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": "album", + "href": "https://api.spotify.com/v1/albums/4hnqM0JK4CM1phwfq1Ldyz", + "id": "4hnqM0JK4CM1phwfq1Ldyz", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab67616d0000b273ee0d0dce888c6c8a70db6e8b", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67616d00001e02ee0d0dce888c6c8a70db6e8b", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab67616d00004851ee0d0dce888c6c8a70db6e8b", + "width": 64 + } + ], + "name": "This Is Happening", + "release_date": "2010-05-17", + "release_date_precision": "day", + "uri": "spotify:album:4hnqM0JK4CM1phwfq1Ldyz", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/066X20Nz7iquqkkCW6Jxy6" + }, + "href": "https://api.spotify.com/v1/artists/066X20Nz7iquqkkCW6Jxy6", + "id": "066X20Nz7iquqkkCW6Jxy6", + "name": "LCD Soundsystem", + "type": "artist", + "uri": "spotify:artist:066X20Nz7iquqkkCW6Jxy6" + } + ], + "external_urls": { + "spotify": "https://open.spotify.com/album/4hnqM0JK4CM1phwfq1Ldyz" + }, + "total_tracks": 9 + }, + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/066X20Nz7iquqkkCW6Jxy6" + }, + "href": "https://api.spotify.com/v1/artists/066X20Nz7iquqkkCW6Jxy6", + "id": "066X20Nz7iquqkkCW6Jxy6", + "name": "LCD Soundsystem", + "type": "artist", + "uri": "spotify:artist:066X20Nz7iquqkkCW6Jxy6" + } + ], + "disc_number": 1, + "track_number": 4, + "duration_ms": 401440, + "external_ids": { + "isrc": "US4GE1000022" + }, + "external_urls": { + "spotify": "https://open.spotify.com/track/4Cy0NHJ8Gh0xMdwyM9RkQm" + }, + "href": "https://api.spotify.com/v1/tracks/4Cy0NHJ8Gh0xMdwyM9RkQm", + "id": "4Cy0NHJ8Gh0xMdwyM9RkQm", + "name": "All I Want", + "popularity": 42, + "uri": "spotify:track:4Cy0NHJ8Gh0xMdwyM9RkQm", + "is_local": false + }, + "video_thumbnail": { + "url": null + } + }, + { + "added_at": "2015-01-15T12:40:35Z", + "added_by": { + "external_urls": { + "spotify": "https://open.spotify.com/user/jmperezperez" + }, + "href": "https://api.spotify.com/v1/users/jmperezperez", + "id": "jmperezperez", + "type": "user", + "uri": "spotify:user:jmperezperez" + }, + "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": "album", + "href": "https://api.spotify.com/v1/albums/2usKFntxa98WHMcyW6xJBz", + "id": "2usKFntxa98WHMcyW6xJBz", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab67616d0000b2738b7447ac3daa1da18811cf7b", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67616d00001e028b7447ac3daa1da18811cf7b", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab67616d000048518b7447ac3daa1da18811cf7b", + "width": 64 + } + ], + "name": "Glenn Horiuchi Trio / Gelenn Horiuchi Quartet: Mercy / Jump Start / Endpoints / Curl Out / Earthworks / Mind Probe / Null Set / Another Space (A)", + "release_date": "2011-04-01", + "release_date_precision": "day", + "uri": "spotify:album:2usKFntxa98WHMcyW6xJBz", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/272ArH9SUAlslQqsSgPJA2" + }, + "href": "https://api.spotify.com/v1/artists/272ArH9SUAlslQqsSgPJA2", + "id": "272ArH9SUAlslQqsSgPJA2", + "name": "Glenn Horiuchi Trio", + "type": "artist", + "uri": "spotify:artist:272ArH9SUAlslQqsSgPJA2" + } + ], + "external_urls": { + "spotify": "https://open.spotify.com/album/2usKFntxa98WHMcyW6xJBz" + }, + "total_tracks": 8 + }, + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/272ArH9SUAlslQqsSgPJA2" + }, + "href": "https://api.spotify.com/v1/artists/272ArH9SUAlslQqsSgPJA2", + "id": "272ArH9SUAlslQqsSgPJA2", + "name": "Glenn Horiuchi Trio", + "type": "artist", + "uri": "spotify:artist:272ArH9SUAlslQqsSgPJA2" + } + ], + "disc_number": 1, + "track_number": 2, + "duration_ms": 358760, + "external_ids": { + "isrc": "USB8U1025969" + }, + "external_urls": { + "spotify": "https://open.spotify.com/track/6hvFrZNocdt2FcKGCSY5NI" + }, + "href": "https://api.spotify.com/v1/tracks/6hvFrZNocdt2FcKGCSY5NI", + "id": "6hvFrZNocdt2FcKGCSY5NI", + "name": "Endpoints", + "popularity": 0, + "uri": "spotify:track:6hvFrZNocdt2FcKGCSY5NI", + "is_local": false + }, + "video_thumbnail": { + "url": null + } + }, + { + "added_at": "2015-01-15T12:41:10Z", + "added_by": { + "external_urls": { + "spotify": "https://open.spotify.com/user/jmperezperez" + }, + "href": "https://api.spotify.com/v1/users/jmperezperez", + "id": "jmperezperez", + "type": "user", + "uri": "spotify:user:jmperezperez" + }, + "is_local": false, + "primary_color": null, + "track": { + "preview_url": null, + "available_markets": [], + "explicit": false, + "type": "track", + "episode": false, + "track": true, + "album": { + "available_markets": [], + "type": "album", + "album_type": "album", + "href": "https://api.spotify.com/v1/albums/0ivM6kSawaug0j3tZVusG2", + "id": "0ivM6kSawaug0j3tZVusG2", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab67616d0000b27304e57d181ff062f8339d6c71", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67616d00001e0204e57d181ff062f8339d6c71", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab67616d0000485104e57d181ff062f8339d6c71", + "width": 64 + } + ], + "name": "All The Best (Spanish Version)", + "release_date": "2007-01-01", + "release_date_precision": "day", + "uri": "spotify:album:0ivM6kSawaug0j3tZVusG2", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2KftmGt9sk1yLjsAoloC3M" + }, + "href": "https://api.spotify.com/v1/artists/2KftmGt9sk1yLjsAoloC3M", + "id": "2KftmGt9sk1yLjsAoloC3M", + "name": "Zucchero", + "type": "artist", + "uri": "spotify:artist:2KftmGt9sk1yLjsAoloC3M" + } + ], + "external_urls": { + "spotify": "https://open.spotify.com/album/0ivM6kSawaug0j3tZVusG2" + }, + "total_tracks": 18 + }, + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2KftmGt9sk1yLjsAoloC3M" + }, + "href": "https://api.spotify.com/v1/artists/2KftmGt9sk1yLjsAoloC3M", + "id": "2KftmGt9sk1yLjsAoloC3M", + "name": "Zucchero", + "type": "artist", + "uri": "spotify:artist:2KftmGt9sk1yLjsAoloC3M" + } + ], + "disc_number": 1, + "track_number": 18, + "duration_ms": 176093, + "external_ids": { + "isrc": "ITUM70701043" + }, + "external_urls": { + "spotify": "https://open.spotify.com/track/2E2znCPaS8anQe21GLxcvJ" + }, + "href": "https://api.spotify.com/v1/tracks/2E2znCPaS8anQe21GLxcvJ", + "id": "2E2znCPaS8anQe21GLxcvJ", + "name": "You Are So Beautiful", + "popularity": 0, + "uri": "spotify:track:2E2znCPaS8anQe21GLxcvJ", + "is_local": false + }, + "video_thumbnail": { + "url": null + } + } + ], + "limit": 100, + "next": null, + "offset": 0, + "previous": null, + "total": 5 +} \ No newline at end of file