Skip to content

add getNullable method for 204-available methods to use, update dependencies #307

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 11 commits into from
Mar 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 11 additions & 19 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,29 +49,16 @@ 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
uses: actions/upload-artifact@v2
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: 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()
21 changes: 21 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -49,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:
Expand All @@ -58,6 +68,12 @@ 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'
- run: choco install curl
- name: Publish windows
run: gradle publishMingwX64PublicationToNexusRepository
release_docs:
Expand All @@ -66,6 +82,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
Expand Down
30 changes: 15 additions & 15 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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:")
}
}

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -121,8 +122,7 @@ kotlin {

}

val irOnlyJs = project.hasProperty("irOnly")
js(if (irOnlyJs) KotlinJsCompilerType.IR else KotlinJsCompilerType.BOTH) {
js(KotlinJsCompilerType.IR) {

mavenPublication {
setupPom(artifactId)
Expand Down Expand Up @@ -151,7 +151,7 @@ kotlin {
}
}*/

if (irOnlyJs) binaries.executable()
binaries.executable()
}

// val hostOs = System.getProperty("os.name")
Expand Down Expand Up @@ -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 = "1.6.8"
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 {
Expand Down
4 changes: 3 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ org.gradle.jvmargs=-Xmx8000m
kotlin.native.enableDependencyPropagation=false
android.useAndroidX=true
android.enableJetifier=true
kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlinVersion=1.6.10
androidBuildToolsVersion=7.0.0
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
26 changes: 15 additions & 11 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
Expand All @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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)

Expand Down Expand Up @@ -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(
Expand Down
26 changes: 17 additions & 9 deletions src/commonMain/kotlin/com.adamratzman.spotify/http/Endpoints.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -76,26 +77,30 @@ public abstract class SpotifyEndpoint(public val api: GenericSpotifyApi) {


internal suspend fun get(url: String): String {
return execute(url)
return execute<String>(url)
}

internal suspend fun getNullable(url: String): String? {
return execute<String?>(url)
}

internal suspend fun post(url: String, body: String? = null, contentType: String? = null): String {
return execute(url, body, HttpRequestMethod.POST, contentType = contentType)
return execute<String>(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<String>(url, body, HttpRequestMethod.PUT, contentType = contentType)
}

internal suspend fun delete(
url: String,
body: String? = null,
contentType: String? = null
): String {
return execute(url, body, HttpRequestMethod.DELETE, contentType = contentType)
return execute<String>(url, body, HttpRequestMethod.DELETE, contentType = contentType)
}

private suspend fun execute(
private suspend fun <T: String?> execute(
url: String,
body: String? = null,
method: HttpRequestMethod = HttpRequestMethod.GET,
Expand All @@ -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 {
Expand All @@ -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<T>(
url,
body,
method,
Expand All @@ -137,7 +142,7 @@ public abstract class SpotifyEndpoint(public val api: GenericSpotifyApi) {
if (e.statusCode == 401 && !attemptedRefresh) {
api.refreshToken()

execute(
execute<T>(
url,
body,
method,
Expand Down Expand Up @@ -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

Expand Down
Loading