From 63c55041657a360bad3d365403cc4ef5c060524d Mon Sep 17 00:00:00 2001 From: "Ratzman, Adam Mortimer" Date: Wed, 16 Mar 2022 12:16:29 -0700 Subject: [PATCH 01/11] add getNullable method for 204-available methods, update dependencies --- build.gradle.kts | 36 +++++++-------- gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 26 ++++++----- .../SpotifyApiBuilder.kt | 2 +- .../SpotifyExceptions.kt | 2 +- .../endpoints/client/ClientPlayerApi.kt | 5 +-- .../com.adamratzman.spotify/http/Endpoints.kt | 26 +++++++---- .../http/HttpConnection.kt | 45 +++++++++---------- 9 files changed, 79 insertions(+), 69 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6019839e..fa49a600 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,15 +5,15 @@ import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput.Target plugins { id("lt.petuska.npm.publish") version "1.1.2" - kotlin("multiplatform") version "1.5.31" + kotlin("multiplatform") `maven-publish` signing id("io.codearte.nexus-staging") version "0.30.0" id("com.android.library") - kotlin("plugin.serialization") version "1.5.31" - id("com.diffplug.spotless") version "5.14.2" + kotlin("plugin.serialization") + id("com.diffplug.spotless") version "6.3.0" id("com.moowork.node") version "1.3.1" - id("org.jetbrains.dokka") version "1.5.0" + id("org.jetbrains.dokka") } repositories { @@ -27,8 +27,8 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:4.1.3") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31") + classpath("com.android.tools.build:gradle:") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:") } } @@ -68,8 +68,9 @@ android { testOptions { this.unitTests.isReturnDefaultValues = true } - sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") - + //sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + sourceSets["main"].setRoot("src/androidMain") + sourceSets["test"].setRoot("src/androidTest") /*sourceSets { getByName("main") { manifest.srcFile("src/androidMain/AndroidManifest.xml") @@ -95,7 +96,7 @@ kotlin { android { compilations.all { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = "8" } mavenPublication { @@ -109,7 +110,7 @@ kotlin { jvm { compilations.all { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = "8" } testRuns["test"].executionTask.configure { useJUnit() @@ -121,8 +122,7 @@ kotlin { } - val irOnlyJs = project.hasProperty("irOnly") - js(if (irOnlyJs) KotlinJsCompilerType.IR else KotlinJsCompilerType.BOTH) { + js(KotlinJsCompilerType.IR) { mavenPublication { setupPom(artifactId) @@ -151,7 +151,7 @@ kotlin { } }*/ - if (irOnlyJs) binaries.executable() + binaries.executable() } // val hostOs = System.getProperty("os.name") @@ -217,13 +217,13 @@ kotlin { val kotlinxDatetimeVersion = "0.3.1" sourceSets { - val serializationVersion = "1.3.0" - val ktorVersion = "1.6.3" + val serializationVersion = "1.3.2" + val ktorVersion = "2.0.0-beta-1" val korlibsVersion = "2.2.0" val sparkVersion = "2.9.3" - val androidSpotifyAuthVersion = "1.2.3" + val androidSpotifyAuthVersion = "1.2.5" val androidCryptoVersion = "1.0.0" - val coroutineMTVersion = "1.5.2-native-mt" + val coroutineMTVersion = "1.6.0-native-mt" val commonMain by getting { dependencies { @@ -298,7 +298,7 @@ kotlin { implementation("com.pnikosis:materialish-progress:1.7") implementation("io.ktor:ktor-client-okhttp:$ktorVersion") implementation("androidx.security:security-crypto:$androidCryptoVersion") - implementation("androidx.appcompat:appcompat:1.3.1") + implementation("androidx.appcompat:appcompat:1.4.1") } } diff --git a/gradle.properties b/gradle.properties index cab820ff..57e3cf2d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,4 +18,6 @@ org.gradle.jvmargs=-Xmx8000m kotlin.native.enableDependencyPropagation=false android.useAndroidX=true android.enableJetifier=true -kotlin.mpp.enableGranularSourceSetsMetadata=true \ No newline at end of file +kotlin.mpp.enableGranularSourceSetsMetadata=true +kotlinVersion=1.5.31 +androidBuildToolsVersion=7.0.0 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a0f7639f..00e33ede 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index b6af7ec6..a5da4b20 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,20 +1,27 @@ pluginManagement { - val mainKotlinVersion = "1.5.0" + val kotlinVersion: String by settings + val androidBuildToolsVersion: String by settings + + plugins { + id("org.jetbrains.kotlin.multiplatform").version(kotlinVersion) + id("org.jetbrains.kotlin.plugin.serialization").version(kotlinVersion) + id("org.jetbrains.dokka").version(kotlinVersion) + } resolutionStrategy { eachPlugin { if (requested.id.id == "kotlin-multiplatform") { - useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$mainKotlinVersion") + useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } if (requested.id.id == "org.jetbrains.kotlin.jvm") { - useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$mainKotlinVersion") + useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } if (requested.id.id == "kotlinx-serialization") { - useModule("org.jetbrains.kotlin:kotlin-serialization:$mainKotlinVersion") - } else if (requested.id.namespace == "com.android") { - useModule("com.android.tools.build:gradle:3.5.4") + useModule("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion") + } else if (requested.id.id == "com.android.library") { + useModule("com.android.tools.build:gradle:$androidBuildToolsVersion") } else if (requested.id.id == "kotlin-android-extensions") { - useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$mainKotlinVersion") + useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } } } @@ -27,7 +34,4 @@ pluginManagement { } } -rootProject.name = "spotify-web-api-kotlin" -include("java-interop-basic-sample") -findProject(":java-interop-basic-sample")?.name = "java-interop-basic" -include("java-interop-sample") +rootProject.name = "spotify-web-api-kotlin" \ No newline at end of file diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt b/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt index d1d0811f..91ea6468 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt @@ -9,7 +9,7 @@ import com.adamratzman.spotify.models.serialization.nonstrictJson import com.adamratzman.spotify.models.serialization.toObject import com.adamratzman.spotify.utils.urlEncodeBase64String import com.soywiz.krypto.SHA256 -import io.ktor.client.features.ServerResponseException +import io.ktor.client.plugins.ServerResponseException import io.ktor.utils.io.core.toByteArray import kotlinx.coroutines.CancellationException import kotlinx.serialization.json.Json diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyExceptions.kt b/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyExceptions.kt index 5eaeb9b4..0446b0c1 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyExceptions.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyExceptions.kt @@ -3,7 +3,7 @@ package com.adamratzman.spotify import com.adamratzman.spotify.models.AuthenticationError import com.adamratzman.spotify.models.ErrorObject -import io.ktor.client.features.ResponseException +import io.ktor.client.plugins.ResponseException public sealed class SpotifyException(message: String, cause: Throwable? = null) : Exception(message, cause) { public abstract class UnNullableException(message: String) : SpotifyException(message) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlayerApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlayerApi.kt index 15e669b4..bd463ca3 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlayerApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlayerApi.kt @@ -93,13 +93,12 @@ public class ClientPlayerApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { requireScopes(SpotifyScope.USER_READ_PLAYBACK_STATE) val obj = catch { - get( + getNullable( endpointBuilder("/me/player") .with("additional_types", additionalTypes.joinToString(",") { it.identifier }) .with("market", market?.name) .toString() - ) - .toObject(CurrentlyPlayingContext.serializer(), api, json) + )?.toObject(CurrentlyPlayingContext.serializer(), api, json) } return if (obj?.timestamp == null) null else obj } diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/http/Endpoints.kt b/src/commonMain/kotlin/com.adamratzman.spotify/http/Endpoints.kt index 754e1b30..358b4dff 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/http/Endpoints.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/http/Endpoints.kt @@ -11,6 +11,7 @@ import com.adamratzman.spotify.models.ErrorResponse import com.adamratzman.spotify.models.serialization.toObject import com.adamratzman.spotify.utils.ConcurrentHashMap import com.adamratzman.spotify.utils.getCurrentTimeMs +import io.ktor.http.HttpStatusCode import kotlin.math.ceil import kotlinx.coroutines.CancellationException import kotlinx.coroutines.async @@ -76,15 +77,19 @@ public abstract class SpotifyEndpoint(public val api: GenericSpotifyApi) { internal suspend fun get(url: String): String { - return execute(url) + return execute(url) + } + + internal suspend fun getNullable(url: String): String? { + return execute(url) } internal suspend fun post(url: String, body: String? = null, contentType: String? = null): String { - return execute(url, body, HttpRequestMethod.POST, contentType = contentType) + return execute(url, body, HttpRequestMethod.POST, contentType = contentType) } internal suspend fun put(url: String, body: String? = null, contentType: String? = null): String { - return execute(url, body, HttpRequestMethod.PUT, contentType = contentType) + return execute(url, body, HttpRequestMethod.PUT, contentType = contentType) } internal suspend fun delete( @@ -92,10 +97,10 @@ public abstract class SpotifyEndpoint(public val api: GenericSpotifyApi) { body: String? = null, contentType: String? = null ): String { - return execute(url, body, HttpRequestMethod.DELETE, contentType = contentType) + return execute(url, body, HttpRequestMethod.DELETE, contentType = contentType) } - private suspend fun execute( + private suspend fun execute( url: String, body: String? = null, method: HttpRequestMethod = HttpRequestMethod.GET, @@ -117,7 +122,7 @@ public abstract class SpotifyEndpoint(public val api: GenericSpotifyApi) { } try { - return withTimeout(api.spotifyApiOptions.requestTimeoutMillis ?: 100 * 1000L) { + return withTimeout(api.spotifyApiOptions.requestTimeoutMillis ?: (100 * 1000L)) { try { val document = createConnection(url, body, method, contentType).execute( additionalHeaders = cacheState?.eTag?.let { @@ -126,7 +131,7 @@ public abstract class SpotifyEndpoint(public val api: GenericSpotifyApi) { retryIfInternalServerErrorLeft = api.spotifyApiOptions.retryOnInternalServerErrorTimes ) - handleResponse(document, cacheState, spotifyRequest, retry202) ?: execute( + handleResponse(document, cacheState, spotifyRequest, retry202) ?: execute( url, body, method, @@ -137,7 +142,7 @@ public abstract class SpotifyEndpoint(public val api: GenericSpotifyApi) { if (e.statusCode == 401 && !attemptedRefresh) { api.refreshToken() - execute( + execute( url, body, method, @@ -165,10 +170,13 @@ public abstract class SpotifyEndpoint(public val api: GenericSpotifyApi) { ): String? { val statusCode = document.responseCode - if (statusCode == HttpConnectionStatus.HTTP_NOT_MODIFIED.code) { + if (statusCode == HttpStatusCode.NotModified.value) { requireNotNull(cacheState?.eTag) { "304 status only allowed on Etag-able endpoints" } return cacheState?.data } + else if (statusCode == HttpStatusCode.NoContent.value) { + return null + } val responseBody = document.body diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/http/HttpConnection.kt b/src/commonMain/kotlin/com.adamratzman.spotify/http/HttpConnection.kt index 5b5fcfcc..50233d95 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/http/HttpConnection.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/http/HttpConnection.kt @@ -12,12 +12,13 @@ import com.adamratzman.spotify.models.SpotifyRatelimitedException import com.adamratzman.spotify.models.serialization.nonstrictJson import com.adamratzman.spotify.models.serialization.toObject import io.ktor.client.HttpClient -import io.ktor.client.features.ResponseException +import io.ktor.client.plugins.ResponseException import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.header import io.ktor.client.request.request +import io.ktor.client.request.setBody import io.ktor.client.request.url -import io.ktor.client.statement.readText +import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.HttpMethod import io.ktor.http.content.ByteArrayContent @@ -61,19 +62,21 @@ public class HttpConnection constructor( url(this@HttpConnection.url) method = this@HttpConnection.method.externalMethod - body = when (this@HttpConnection.method) { - HttpRequestMethod.DELETE -> { - bodyString.toByteArrayContent() ?: body - } - HttpRequestMethod.PUT, HttpRequestMethod.POST -> { - val contentString = if (contentType == ContentType.Application.FormUrlEncoded) { - bodyMap?.map { "${it.key}=${it.value}" }?.joinToString("&") ?: bodyString - } else bodyString + setBody( + when (this@HttpConnection.method) { + HttpRequestMethod.DELETE -> { + bodyString.toByteArrayContent() ?: body + } + HttpRequestMethod.PUT, HttpRequestMethod.POST -> { + val contentString = if (contentType == ContentType.Application.FormUrlEncoded) { + bodyMap?.map { "${it.key}=${it.value}" }?.joinToString("&") ?: bodyString + } else bodyString - contentString.toByteArrayContent() ?: ByteArrayContent("".toByteArray(), contentType) + contentString.toByteArrayContent() ?: ByteArrayContent("".toByteArray(), contentType) + } + else -> body } - else -> body - } + ) // let additionalHeaders overwrite headers val allHeaders = if (additionalHeaders == null) this@HttpConnection.headers @@ -91,7 +94,7 @@ public class HttpConnection constructor( val httpRequest = buildRequest(additionalHeaders) if (api?.spotifyApiOptions?.enableDebugMode == true) println("DEBUG MODE: Request: $this") try { - return httpClient.request(httpRequest).let { response -> + return httpClient.request(httpRequest).let { response -> val respCode = response.status.value if (respCode in 500..599 && (retryIfInternalServerErrorLeft == null || retryIfInternalServerErrorLeft == -1 || retryIfInternalServerErrorLeft > 0)) { @@ -115,12 +118,10 @@ public class HttpConnection constructor( } else throw SpotifyRatelimitedException(ratelimit) } - val body = response.readText() + val body: String = response.bodyAsText() if (api?.spotifyApiOptions?.enableDebugMode == true) println("DEBUG MODE: $body") - if (respCode == 401 && body.contains("access token") && - api != null && api.spotifyApiOptions.automaticRefresh - ) { + if (respCode == 401 && body.contains("access token") && api?.spotifyApiOptions?.automaticRefresh == true) { api.refreshToken() val newAdditionalHeaders = additionalHeaders?.toMutableList()?.filter { it.key != "Authorization" }?.toMutableList() @@ -146,7 +147,7 @@ public class HttpConnection constructor( } catch (e: CancellationException) { throw e } catch (e: ResponseException) { - val errorBody = e.response.readText() + val errorBody = e.response.bodyAsText() if (api?.spotifyApiOptions?.enableDebugMode == true) println("DEBUG MODE: $errorBody") try { val respCode = e.response.status.value @@ -222,8 +223,4 @@ public class HttpConnection constructor( expectSuccess = false } } -} - -public enum class HttpConnectionStatus(public val code: Int) { - HTTP_NOT_MODIFIED(304); -} +} \ No newline at end of file From 3552fa7056e4808e7fff5d8891e75b9e132a5daf Mon Sep 17 00:00:00 2001 From: "Ratzman, Adam Mortimer" Date: Wed, 16 Mar 2022 12:37:59 -0700 Subject: [PATCH 02/11] install java 11 on ci --- .github/workflows/ci.yml | 5 +++++ .github/workflows/release.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6907c40f..38ae7b86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,11 @@ jobs: steps: - name: Check out repo uses: actions/checkout@v2 + - name: Install java 11 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '11' - name: Install curl run: sudo apt-get install -y curl libcurl4-openssl-dev - name: Test secret diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4aedba7a..eb0cd228 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,6 +28,11 @@ jobs: steps: - name: Check out repo uses: actions/checkout@v2 + - name: Install java 11 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '11' - name: Install curl run: sudo apt-get install -y curl libcurl4-openssl-dev - name: Verify Android From 93d1808b6befaef33d8fc496ca5445217e59abd7 Mon Sep 17 00:00:00 2001 From: "Ratzman, Adam Mortimer" Date: Wed, 16 Mar 2022 12:48:10 -0700 Subject: [PATCH 03/11] set jvm target to java 8 --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index fa49a600..da804f0f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -96,7 +96,7 @@ kotlin { android { compilations.all { - kotlinOptions.jvmTarget = "8" + kotlinOptions.jvmTarget = "1.8" } mavenPublication { @@ -110,7 +110,7 @@ kotlin { jvm { compilations.all { - kotlinOptions.jvmTarget = "8" + kotlinOptions.jvmTarget = "1.8" } testRuns["test"].executionTask.configure { useJUnit() From e7b840fb332ccbe9a6646e1a49ee3a7784d6ae42 Mon Sep 17 00:00:00 2001 From: "Ratzman, Adam Mortimer" Date: Wed, 16 Mar 2022 12:54:38 -0700 Subject: [PATCH 04/11] install java 11 on all non-windows targets --- .github/workflows/ci.yml | 5 +++++ .github/workflows/release.yml | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38ae7b86..72c24239 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,11 @@ jobs: steps: - name: Check out repo uses: actions/checkout@v2 + - name: Install java 11 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '11' - name: Test mac run: gradle macosX64Test - name: Archive test results diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb0cd228..7fb30395 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,6 +54,11 @@ jobs: steps: - name: Check out repo uses: actions/checkout@v2 + - name: Install java 11 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '11' - name: Publish macOS/tvOS/iOS run: gradle publishMacosX64PublicationToNexusRepository publishIosX64PublicationToNexusRepository publishTvosX64PublicationToNexusRepository release_windows: @@ -71,6 +76,11 @@ jobs: steps: - name: Check out repo uses: actions/checkout@v2 + - name: Install java 11 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '11' - name: Build docs run: gradle dokkaHtml - name: Push docs to docs repo From 6a86230188951d7a5d6645444fb4c43c97ccea42 Mon Sep 17 00:00:00 2001 From: "Ratzman, Adam Mortimer" Date: Thu, 17 Mar 2022 22:05:45 -0400 Subject: [PATCH 05/11] update to kotlin 1.6 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 57e3cf2d..9aa540ff 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,5 +19,5 @@ kotlin.native.enableDependencyPropagation=false android.useAndroidX=true android.enableJetifier=true kotlin.mpp.enableGranularSourceSetsMetadata=true -kotlinVersion=1.5.31 +kotlinVersion=1.6.10 androidBuildToolsVersion=7.0.0 \ No newline at end of file From fa2ec9e162dd1b3b6d319e45dda50f27c404fb0f Mon Sep 17 00:00:00 2001 From: "Ratzman, Adam Mortimer" Date: Thu, 17 Mar 2022 22:16:29 -0400 Subject: [PATCH 06/11] downgrade appcompat --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index da804f0f..332b7e43 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -298,7 +298,7 @@ kotlin { implementation("com.pnikosis:materialish-progress:1.7") implementation("io.ktor:ktor-client-okhttp:$ktorVersion") implementation("androidx.security:security-crypto:$androidCryptoVersion") - implementation("androidx.appcompat:appcompat:1.4.1") + implementation("androidx.appcompat:appcompat:1.3.1") } } From 560d5fe3ee6341528b1c52b3174dc6870eb63302 Mon Sep 17 00:00:00 2001 From: "Ratzman, Adam Mortimer" Date: Fri, 18 Mar 2022 00:40:34 -0400 Subject: [PATCH 07/11] fixed search unit test --- .../endpoints/pub/SearchApi.kt | 22 +++++- .../com.adamratzman.spotify/utils/Market.kt | 78 +------------------ .../spotify/pub/PublicPlaylistsApiTest.kt | 4 +- .../spotify/pub/SearchApiTest.kt | 4 +- 4 files changed, 28 insertions(+), 80 deletions(-) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/SearchApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/SearchApi.kt index 869740a4..54dd1216 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/SearchApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/SearchApi.kt @@ -87,7 +87,18 @@ public open class SearchApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * **[Api Reference](https://developer.spotify.com/documentation/web-api/reference/search/search/)** * - * @param query Search query keywords and optional field filters and operators. + * @param query Search query keywords and optional field filters and operators. You can narrow down your search using field filters. The available filters are album, artist, track, year, upc, tag:hipster, tag:new, isrc, and genre. Each field filter only applies to certain result types. + + The artist filter can be used while searching albums, artists or tracks. + The album and year filters can be used while searching albums or tracks. You can filter on a single year or a range (e.g. 1955-1960). + The genre filter can be use while searching tracks and artists. + The isrc and track filters can be used while searching tracks. + The upc, tag:new and tag:hipster filters can only be used while searching albums. The tag:new filter will return albums released in the past two weeks and tag:hipster can be used to return only albums with the lowest 10% popularity. + + You can also use the NOT operator to exclude keywords from your search. + + Example value: + "remaster%20track:Doxy+artist:Miles%20Davis" * @param searchTypes A list of item types to search across. Search results include hits from all the specified item types. * @param limit Maximum number of results to return. Default: 20 @@ -103,7 +114,10 @@ public open class SearchApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { - Playlist results are not affected by the market parameter. - If market is set to from_token, and a valid access token is specified in the request header, only content playable in the country associated with the user account, is returned. - Users can view the country that is associated with their account in the account settings. A user must grant access to the [SpotifyScope.USER_READ_PRIVATE] scope prior to when the access token is issued. + **Note**: episodes will not be returned if this is NOT specified * @param includeExternal If true, the response will include any relevant audio content that is hosted externally. By default external content is filtered out from responses. + * + * @throws IllegalArgumentException if no search types are provided, or if [SearchType.EPISODE] is provided but [market] is not */ public suspend fun search( query: String, @@ -114,6 +128,10 @@ public open class SearchApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { includeExternal: Boolean? = null ): SpotifySearchResult { require(searchTypes.isNotEmpty()) { "At least one search type must be provided" } + if (SearchType.EPISODE in searchTypes) { + requireNotNull(market) { "Market must be provided when SearchType.EPISODE is requested"} + } + val jsonString = get(build(query, market, limit, offset, *searchTypes, includeExternal = includeExternal)) val map = json.decodeFromString(MapSerializer(String.serializer(), JsonObject.serializer()), jsonString) @@ -184,6 +202,8 @@ public open class SearchApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { - Playlist results are not affected by the market parameter. - If market is set to from_token, and a valid access token is specified in the request header, only content playable in the country associated with the user account, is returned. - Users can view the country that is associated with their account in the account settings. A user must grant access to the [SpotifyScope.USER_READ_PRIVATE] scope prior to when the access token is issued. + + **Note**: episodes will not be returned if this is NOT specified * @param includeExternal If true, the response will include any relevant audio content that is hosted externally. By default external content is filtered out from responses. */ public fun searchRestAction( diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/utils/Market.kt b/src/commonMain/kotlin/com.adamratzman.spotify/utils/Market.kt index d701f64d..971450dc 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/utils/Market.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/utils/Market.kt @@ -1,6 +1,8 @@ /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2021; Original author: Adam Ratzman */ package com.adamratzman.spotify.utils +import com.adamratzman.spotify.SpotifyAppApi + /* * Copyright (C) 2012-2019 Neo Visionaries Inc. * @@ -20,81 +22,7 @@ package com.adamratzman.spotify.utils /** * [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) country code. * - * - * - * Enum names of this enum themselves are represented by - * [ISO 3166-1 alpha-2](http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) - * code (2-letter upper-case alphabets). There are instance methods to get the - * country name ([.getName]), the - * [ISO 3166-1 alpha-3](http://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) - * code ([.getAlpha3]) and the - * [ISO 3166-1 numeric](http://en.wikipedia.org/wiki/ISO_3166-1_numeric) - * code ([.getNumeric]). - * In addition, there are static methods to get a `Market` instance that - * corresponds to a given alpha-2/alpha-3/numeric code ([.getByCode], - * [.getByCode]). - * - * - *
- * // List all the country codes.
- * for (Market code : Market.values())
- * {
- * // For example, "[US] United States" is printed.
- * System.out.format("[%s] %s\n", code, code.[.getName]);
- * }
- *
- * // Get a Market instance by ISO 3166-1 code.
- * Market code = Market.[getByCode][.getByCode]("JP");
- *
- * // Print all the information. Output will be:
- * //
- * //     Country name            = Japan
- * //     ISO 3166-1 alpha-2 code = JP
- * //     ISO 3166-1 alpha-3 code = JPN
- * //     ISO 3166-1 numeric code = 392
- * //     Assignment state        = OFFICIALLY_ASSIGNED
- * //
- * System.out.println("Country name            = " + code.[.getName]);
- * System.out.println("ISO 3166-1 alpha-2 code = " + code.[.getAlpha2]);
- * System.out.println("ISO 3166-1 alpha-3 code = " + code.[.getAlpha3]);
- * System.out.println("ISO 3166-1 numeric code = " + code.[.getNumeric]);
- * System.out.println("Assignment state        = " + code.[.getAssignment]);
- *
- * // Convert to a Locale instance.
- *
- * // Get a Market by a Locale instance.
- * code = Market.[getByLocale][.getByLocale](locale);
- *
- * // Get the currency of the country.
- *
- * // Get a list by a regular expression for names.
- * //
- * // The list will contain:
- * //
- * //     Market.AE ed Arab Emirates
- * //     Market.GB ed Kingdom
- * //     Market.TZ : Tanzania, United Republic of
- * //     Market.UK ed Kingdom
- * //     Market.UM ed States Minor Outlying Islands
- * //     Market.US ed States
- * //
- * List<Market> list = Market.[findByName][.findByName](".*United.*");
- *
- * 
- * // For backward compatibility for older versions than 1.16, some
- * // 4-letter ISO 3166-3 codes are accepted by getByCode(String, boolean)
- * // and its variants. To be concrete:
- * //
- * //     [ANHH](https://en.wikipedia.org/wiki/ISO_3166-3#ANHH) : Market.AN
- * //     [BUMM](https://en.wikipedia.org/wiki/ISO_3166-3#BUMM) : Market.BU
- * //     [CSXX](https://en.wikipedia.org/wiki/ISO_3166-3#CSXX) : Market.CS
- * //     [NTHH](https://en.wikipedia.org/wiki/ISO_3166-3#NTHH) : Market.NT
- * //     [TPTL](https://en.wikipedia.org/wiki/ISO_3166-3#TPTL) : Market.TP
- * //     [YUCS](https://en.wikipedia.org/wiki/ISO_3166-3#YUCS) : Market.YU
- * //     [ZRCD](https://en.wikipedia.org/wiki/ISO_3166-3#ZRCD) : Market.ZR
- * //
- * code = Market.[getByCode][.getByCode]("ANHH");
-
* + * **Note**: Use [Market.FROM_TOKEN] if you want to use the client's locale. This should not be used with [SpotifyAppApi] * * @author Takahiko Kawasaki (https://github.com/TakahikoKawasaki/nv-i18n) */ diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt index e076d82f..c5736a16 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt @@ -39,7 +39,7 @@ class PublicPlaylistsApiTest : AbstractTest() { assertEquals("run2", api!!.playlists.getPlaylist("78eWnYKwDksmCHAjOUNPEj")?.name) assertNull(api!!.playlists.getPlaylist("nope")) assertTrue(api!!.playlists.getPlaylist("78eWnYKwDksmCHAjOUNPEj")!!.tracks.isNotEmpty()) - val playlistWithLocalAndNonLocalTracks = api!!.playlists.getPlaylist("0vzdw0N41qZLbRDqyx2cE0")!!.tracks + val playlistWithLocalAndNonLocalTracks = api!!.playlists.getPlaylist("627gNjNzj3sOrSiDm5acc2")!!.tracks assertEquals(LocalTrack::class, playlistWithLocalAndNonLocalTracks[0].track!!::class) assertEquals(Track::class, playlistWithLocalAndNonLocalTracks[1].track!!::class) @@ -57,7 +57,7 @@ class PublicPlaylistsApiTest : AbstractTest() { if (!testPrereq()) return@runBlockingTest else api!! assertTrue(api!!.playlists.getPlaylistTracks("78eWnYKwDksmCHAjOUNPEj").items.isNotEmpty()) - val playlist = api!!.playlists.getPlaylistTracks("0vzdw0N41qZLbRDqyx2cE0") + val playlist = api!!.playlists.getPlaylistTracks("627gNjNzj3sOrSiDm5acc2") assertEquals(LocalTrack::class, playlist[0].track!!::class) assertEquals(Track::class, playlist[1].track!!::class) assertFailsWith { api!!.playlists.getPlaylistTracks("adskjfjkasdf") } diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/SearchApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/SearchApiTest.kt index beb871d5..7510a789 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/SearchApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/SearchApiTest.kt @@ -18,7 +18,7 @@ class SearchApiTest : AbstractTest() { return runBlockingTest { super.build() if (!testPrereq()) return@runBlockingTest else api!! - val query = api!!.search.search("lo", *SearchApi.SearchType.values()) + val query = api!!.search.search("lo", *SearchApi.SearchType.values(), market = Market.US) assertTrue( query.albums?.items?.isNotEmpty() == true && query.tracks?.items?.isNotEmpty() == true && query.artists?.items?.isNotEmpty() == true && query.playlists?.items?.isNotEmpty() == true && query.shows?.items?.isNotEmpty() == true && query.episodes?.items?.isNotEmpty() == true @@ -28,7 +28,7 @@ class SearchApiTest : AbstractTest() { query2.albums == null && query2.tracks == null && query2.shows == null && query2.episodes == null && query2.artists?.items?.isNotEmpty() == true && query2.playlists?.items?.isNotEmpty() == true ) - val query3 = api!!.search.search("lo", SearchApi.SearchType.SHOW, SearchApi.SearchType.EPISODE) + val query3 = api!!.search.search("lo", SearchApi.SearchType.SHOW, SearchApi.SearchType.EPISODE, market = Market.US) assertTrue(query3.episodes?.items?.isNotEmpty() == true && query3.shows?.items?.isNotEmpty() == true) } } From 4a80c46dc939bdc020393007c63ae224a36a09c5 Mon Sep 17 00:00:00 2001 From: "Ratzman, Adam Mortimer" Date: Fri, 18 Mar 2022 01:02:06 -0400 Subject: [PATCH 08/11] use java 11 on windows agent --- .github/workflows/ci.yml | 5 +++++ .github/workflows/release.yml | 5 +++++ .../kotlin/com.adamratzman/spotify/utilities/UtilityTests.kt | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72c24239..46528683 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,11 @@ jobs: steps: - name: Check out repo uses: actions/checkout@v2 + - name: Install java 11 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '11' - name: Test windows run: gradle mingwX64Test - name: Archive test results diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7fb30395..0f3e52c0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,6 +68,11 @@ jobs: steps: - name: Check out repo uses: actions/checkout@v2 + - name: Install java 11 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '11' - name: Publish windows run: gradle publishMingwX64PublicationToNexusRepository release_docs: diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/utilities/UtilityTests.kt b/src/commonTest/kotlin/com.adamratzman/spotify/utilities/UtilityTests.kt index b252f634..84d0a4dd 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/utilities/UtilityTests.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/utilities/UtilityTests.kt @@ -32,7 +32,7 @@ class UtilityTests { if (!testPrereq()) return runBlockingTest { - val spotifyWfhPlaylist = api!!.playlists.getPlaylist("spotify:playlist:37i9dQZF1DWTLSN7iG21yC")!! + val spotifyWfhPlaylist = api!!.playlists.getPlaylist("37i9dQZF1DWTLSN7iG21yC")!! val totalTracks = spotifyWfhPlaylist.tracks.total val allTracks = spotifyWfhPlaylist.tracks.getAllItemsNotNull() assertEquals(totalTracks, allTracks.size) From b14c6d4abba9b02e565343b6eb9af318de7699c3 Mon Sep 17 00:00:00 2001 From: "Ratzman, Adam Mortimer" Date: Fri, 18 Mar 2022 20:26:00 -0400 Subject: [PATCH 09/11] downgrade ktor to 1.6.8 --- build.gradle.kts | 2 +- .../SpotifyApiBuilder.kt | 2 +- .../SpotifyExceptions.kt | 2 +- .../http/HttpConnection.kt | 36 +++++++++---------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 332b7e43..3fd2595a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -218,7 +218,7 @@ kotlin { sourceSets { val serializationVersion = "1.3.2" - val ktorVersion = "2.0.0-beta-1" + val ktorVersion = "1.6.8" val korlibsVersion = "2.2.0" val sparkVersion = "2.9.3" val androidSpotifyAuthVersion = "1.2.5" diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt b/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt index 91ea6468..d1d0811f 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt @@ -9,7 +9,7 @@ import com.adamratzman.spotify.models.serialization.nonstrictJson import com.adamratzman.spotify.models.serialization.toObject import com.adamratzman.spotify.utils.urlEncodeBase64String import com.soywiz.krypto.SHA256 -import io.ktor.client.plugins.ServerResponseException +import io.ktor.client.features.ServerResponseException import io.ktor.utils.io.core.toByteArray import kotlinx.coroutines.CancellationException import kotlinx.serialization.json.Json diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyExceptions.kt b/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyExceptions.kt index 0446b0c1..5eaeb9b4 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyExceptions.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyExceptions.kt @@ -3,7 +3,7 @@ package com.adamratzman.spotify import com.adamratzman.spotify.models.AuthenticationError import com.adamratzman.spotify.models.ErrorObject -import io.ktor.client.plugins.ResponseException +import io.ktor.client.features.ResponseException public sealed class SpotifyException(message: String, cause: Throwable? = null) : Exception(message, cause) { public abstract class UnNullableException(message: String) : SpotifyException(message) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/http/HttpConnection.kt b/src/commonMain/kotlin/com.adamratzman.spotify/http/HttpConnection.kt index 50233d95..6c99c703 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/http/HttpConnection.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/http/HttpConnection.kt @@ -11,14 +11,14 @@ import com.adamratzman.spotify.models.ErrorResponse import com.adamratzman.spotify.models.SpotifyRatelimitedException import com.adamratzman.spotify.models.serialization.nonstrictJson import com.adamratzman.spotify.models.serialization.toObject +import com.soywiz.korio.dynamic.KDynamic.Companion.toLong import io.ktor.client.HttpClient -import io.ktor.client.plugins.ResponseException +import io.ktor.client.features.ResponseException import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.header import io.ktor.client.request.request -import io.ktor.client.request.setBody import io.ktor.client.request.url -import io.ktor.client.statement.bodyAsText +import io.ktor.client.statement.readText import io.ktor.http.ContentType import io.ktor.http.HttpMethod import io.ktor.http.content.ByteArrayContent @@ -62,21 +62,19 @@ public class HttpConnection constructor( url(this@HttpConnection.url) method = this@HttpConnection.method.externalMethod - setBody( - when (this@HttpConnection.method) { - HttpRequestMethod.DELETE -> { - bodyString.toByteArrayContent() ?: body - } - HttpRequestMethod.PUT, HttpRequestMethod.POST -> { - val contentString = if (contentType == ContentType.Application.FormUrlEncoded) { - bodyMap?.map { "${it.key}=${it.value}" }?.joinToString("&") ?: bodyString - } else bodyString + body = when (this@HttpConnection.method) { + HttpRequestMethod.DELETE -> { + bodyString.toByteArrayContent() ?: body + } + HttpRequestMethod.PUT, HttpRequestMethod.POST -> { + val contentString = if (contentType == ContentType.Application.FormUrlEncoded) { + bodyMap?.map { "${it.key}=${it.value}" }?.joinToString("&") ?: bodyString + } else bodyString - contentString.toByteArrayContent() ?: ByteArrayContent("".toByteArray(), contentType) - } - else -> body + contentString.toByteArrayContent() ?: ByteArrayContent("".toByteArray(), contentType) } - ) + else -> body + } // let additionalHeaders overwrite headers val allHeaders = if (additionalHeaders == null) this@HttpConnection.headers @@ -94,7 +92,7 @@ public class HttpConnection constructor( val httpRequest = buildRequest(additionalHeaders) if (api?.spotifyApiOptions?.enableDebugMode == true) println("DEBUG MODE: Request: $this") try { - return httpClient.request(httpRequest).let { response -> + return httpClient.request(httpRequest).let { response -> val respCode = response.status.value if (respCode in 500..599 && (retryIfInternalServerErrorLeft == null || retryIfInternalServerErrorLeft == -1 || retryIfInternalServerErrorLeft > 0)) { @@ -118,7 +116,7 @@ public class HttpConnection constructor( } else throw SpotifyRatelimitedException(ratelimit) } - val body: String = response.bodyAsText() + val body: String = response.readText() if (api?.spotifyApiOptions?.enableDebugMode == true) println("DEBUG MODE: $body") if (respCode == 401 && body.contains("access token") && api?.spotifyApiOptions?.automaticRefresh == true) { @@ -147,7 +145,7 @@ public class HttpConnection constructor( } catch (e: CancellationException) { throw e } catch (e: ResponseException) { - val errorBody = e.response.bodyAsText() + val errorBody = e.response.readText() if (api?.spotifyApiOptions?.enableDebugMode == true) println("DEBUG MODE: $errorBody") try { val respCode = e.response.status.value From 8edb29611c3d935e91ba09dc4df0975a3813c39a Mon Sep 17 00:00:00 2001 From: "Ratzman, Adam Mortimer" Date: Fri, 18 Mar 2022 20:57:17 -0400 Subject: [PATCH 10/11] install curl on windows machines --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46528683..6df16bf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,7 @@ jobs: with: distribution: 'adopt' java-version: '11' + - run: choco install curl - name: Test windows run: gradle mingwX64Test - name: Archive test results diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f3e52c0..b22d80ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,6 +73,7 @@ jobs: with: distribution: 'adopt' java-version: '11' + - run: choco install curl - name: Publish windows run: gradle publishMingwX64PublicationToNexusRepository release_docs: From db5dec261e6da64c2eeebc50eb9b71a5112dd0a0 Mon Sep 17 00:00:00 2001 From: "Ratzman, Adam Mortimer" Date: Fri, 18 Mar 2022 21:27:52 -0400 Subject: [PATCH 11/11] remove windows test --- .github/workflows/ci.yml | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6df16bf5..6faa4f34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,28 +61,4 @@ jobs: with: name: code-coverage-report path: build/reports - if: always() - test_windows: - runs-on: windows-latest - environment: testing - env: - SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }} - SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }} - steps: - - name: Check out repo - uses: actions/checkout@v2 - - name: Install java 11 - uses: actions/setup-java@v2 - with: - distribution: 'adopt' - java-version: '11' - - run: choco install curl - - name: Test windows - run: gradle mingwX64Test - - name: Archive test results - uses: actions/upload-artifact@v2 - with: - name: code-coverage-report - path: build/reports - if: always() - + if: always() \ No newline at end of file