├── webpack.config.js
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── webpack.config.d
└── patch.js
├── src
├── androidMain
│ ├── AndroidManifest.xml
│ ├── res
│ │ └── layout
│ │ │ └── spotify_pkce_auth_layout.xml
│ └── kotlin
│ │ └── com
│ │ └── adamratzman
│ │ └── spotify
│ │ ├── auth
│ │ ├── pkce
│ │ │ └── PkceAuthUtils.kt
│ │ └── implicit
│ │ │ ├── AbstractSpotifyAppImplicitLoginActivity.kt
│ │ │ ├── AbstractSpotifyAppCompatImplicitLoginActivity.kt
│ │ │ ├── ImplicitAuthUtils.kt
│ │ │ └── SpotifyImplicitLoginActivity.kt
│ │ ├── notifications
│ │ ├── SpotifyBroadcastReceiverUtils.kt
│ │ └── AbstractSpotifyBroadcastReceiver.kt
│ │ └── utils
│ │ └── PlatformUtils.kt
├── iosMain
│ └── kotlin
│ │ └── com.adamratzman.spotify.utils
│ │ └── PlatformUtils.kt
├── tvosMain
│ └── kotlin
│ │ └── com.adamratzman.spotify.utils
│ │ └── PlatformUtils.kt
├── linuxX64Main
│ └── kotlin
│ │ └── com.adamratzman.spotify.utils
│ │ └── PlatformUtils.kt
├── macosX64Main
│ └── kotlin
│ │ └── com.adamratzman.spotify.utils
│ │ └── PlatformUtils.kt
├── mingwX64Main
│ └── kotlin
│ │ └── com.adamratzman.spotify.utils
│ │ └── PlatformUtils.kt
├── commonMain
│ └── kotlin
│ │ ├── com.adamratzman.spotify
│ │ ├── utils
│ │ │ ├── Platform.kt
│ │ │ ├── TimeUnit.kt
│ │ │ ├── ConcurrentHashMap.kt
│ │ │ ├── Encoding.kt
│ │ │ ├── IO.kt
│ │ │ ├── Utils.kt
│ │ │ └── ExternalUrls.kt
│ │ ├── annotations
│ │ │ └── ExperimentalAnnotations.kt
│ │ ├── models
│ │ │ ├── Misc.kt
│ │ │ ├── SpotifySearchResult.kt
│ │ │ ├── Library.kt
│ │ │ ├── Authentication.kt
│ │ │ ├── Browse.kt
│ │ │ ├── Playable.kt
│ │ │ ├── Artists.kt
│ │ │ ├── LocalTracks.kt
│ │ │ ├── ResultObjects.kt
│ │ │ ├── Show.kt
│ │ │ └── Users.kt
│ │ ├── endpoints
│ │ │ ├── pub
│ │ │ │ ├── MarketsApi.kt
│ │ │ │ ├── UserApi.kt
│ │ │ │ ├── FollowingApi.kt
│ │ │ │ ├── EpisodeApi.kt
│ │ │ │ ├── AlbumApi.kt
│ │ │ │ ├── TrackApi.kt
│ │ │ │ └── ShowApi.kt
│ │ │ └── client
│ │ │ │ ├── ClientProfileApi.kt
│ │ │ │ ├── ClientEpisodeApi.kt
│ │ │ │ ├── ClientShowApi.kt
│ │ │ │ └── ClientPersonalizationApi.kt
│ │ ├── SpotifyException.kt
│ │ ├── SpotifyRestAction.kt
│ │ └── SpotifyScope.kt
│ │ └── com.soywiz.korim.format.jpg
│ │ └── JPEG.kt
├── commonJvmLikeMain
│ └── kotlin
│ │ └── com
│ │ └── adamratzman
│ │ └── spotify
│ │ ├── utils
│ │ └── DateTimeUtils.kt
│ │ └── javainterop
│ │ └── SpotifyContinuation.kt
├── commonNonJvmTargetsTest
│ └── kotlin
│ │ └── com.adamratzman.spotify
│ │ └── CommonImpl.kt
├── jvmMain
│ └── kotlin
│ │ └── com
│ │ └── adamratzman
│ │ └── spotify
│ │ └── utils
│ │ └── PlatformUtils.kt
├── commonTest
│ └── kotlin
│ │ └── com.adamratzman
│ │ └── spotify
│ │ ├── pub
│ │ ├── MarketsApiTest.kt
│ │ ├── PublicUserApiTest.kt
│ │ ├── EpisodeApiTest.kt
│ │ ├── PublicFollowingApiTest.kt
│ │ ├── ShowApiTest.kt
│ │ ├── PublicTracksApiTest.kt
│ │ ├── PublicAlbumsApiTest.kt
│ │ ├── PublicArtistsApiTest.kt
│ │ ├── PublicPlaylistsApiTest.kt
│ │ └── SearchApiTest.kt
│ │ ├── priv
│ │ ├── ClientUserApiTest.kt
│ │ ├── ClientPersonalizationApiTest.kt
│ │ ├── ClientEpisodeApiTest.kt
│ │ └── ClientFollowingApiTest.kt
│ │ ├── utilities
│ │ ├── RestTests.kt
│ │ ├── JsonTests.kt
│ │ └── UtilityTests.kt
│ │ ├── AbstractTest.kt
│ │ └── Common.kt
├── desktopMain
│ └── kotlin
│ │ └── com
│ │ └── adamratzman
│ │ └── spotify
│ │ └── utils
│ │ └── PlatformUtils.kt
├── nativeDarwinMain
│ └── kotlin
│ │ └── com
│ │ └── adamratzman
│ │ └── spotify
│ │ └── utils
│ │ └── PlatformUtils.kt
├── jsMain
│ └── kotlin
│ │ ├── com
│ │ └── adamratzman
│ │ │ └── spotify
│ │ │ └── utils
│ │ │ ├── ImplicitGrant.kt
│ │ │ └── PlatformUtils.kt
│ │ └── co.scdn.sdk
│ │ └── SpotifyPlayerJs.kt
├── jvmTest
│ └── kotlin
│ │ └── com
│ │ └── adamratzman
│ │ └── spotify
│ │ └── PkceTest.kt
└── commonJvmLikeTest
│ └── kotlin
│ └── com
│ └── adamratzman
│ └── spotify
│ └── CommonImpl.kt
├── publish_all.sh
├── CONTRIBUTING.md
├── gradle.properties
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── FUNDING.yml
├── .github
│ └── FUNDING.yml
└── workflows
│ ├── ci.yml
│ ├── ci-client.yml
│ └── release.yml
├── LICENSE
├── settings.gradle.kts
├── TESTING.md
└── gradlew.bat
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | output: {
3 | globalObject: 'this'
4 | }
5 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamint/spotify-web-api-kotlin/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/webpack.config.d/patch.js:
--------------------------------------------------------------------------------
1 | config.resolve.alias = {
2 | "crypto": false,
3 | }
4 |
5 | output: {
6 | globalObject: 'this'
7 | }
--------------------------------------------------------------------------------
/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/src/iosMain/kotlin/com.adamratzman.spotify.utils/PlatformUtils.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | import kotlinx.coroutines.runBlocking
5 |
6 | public actual fun runBlockingOnJvmAndNative(block: suspend () -> T): T {
7 | return runBlocking { block() }
8 | }
9 |
--------------------------------------------------------------------------------
/src/tvosMain/kotlin/com.adamratzman.spotify.utils/PlatformUtils.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | import kotlinx.coroutines.runBlocking
5 |
6 | public actual fun runBlockingOnJvmAndNative(block: suspend () -> T): T {
7 | return runBlocking { block() }
8 | }
9 |
--------------------------------------------------------------------------------
/src/linuxX64Main/kotlin/com.adamratzman.spotify.utils/PlatformUtils.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | import kotlinx.coroutines.runBlocking
5 |
6 | public actual fun runBlockingOnJvmAndNative(block: suspend () -> T): T {
7 | return runBlocking { block() }
8 | }
9 |
--------------------------------------------------------------------------------
/src/macosX64Main/kotlin/com.adamratzman.spotify.utils/PlatformUtils.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | import kotlinx.coroutines.runBlocking
5 |
6 | public actual fun runBlockingOnJvmAndNative(block: suspend () -> T): T {
7 | return runBlocking { block() }
8 | }
9 |
--------------------------------------------------------------------------------
/src/mingwX64Main/kotlin/com.adamratzman.spotify.utils/PlatformUtils.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | import kotlinx.coroutines.runBlocking
5 |
6 | public actual fun runBlockingOnJvmAndNative(block: suspend () -> T): T {
7 | return runBlocking { block() }
8 | }
9 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com.adamratzman.spotify/utils/Platform.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | /**
5 | * Actual platforms that this program can be run on.
6 | */
7 | public enum class Platform {
8 | Jvm,
9 | Android,
10 | Js,
11 | Native
12 | }
13 |
14 | public expect val currentApiPlatform: Platform
15 |
--------------------------------------------------------------------------------
/publish_all.sh:
--------------------------------------------------------------------------------
1 | gradle publishMacosX64PublicationToNexusRepository publishLinuxX64PublicationToNexusRepository publishKotlinMultiplatformPublicationToNexusRepository publishTvosX64PublicationToNexusRepository publishTvosArm64PublicationToNexusRepository publishIosX64PublicationToNexusRepository publishIosArm64PublicationToNexusRepository publishJsPublicationToNexusRepository publishJvmPublicationToNexusRepository publishAndroidPublicationToNexusRepository
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com.adamratzman.spotify/utils/TimeUnit.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | /**
5 | * Represents a unit of time
6 | */
7 | public enum class TimeUnit(private val multiplier: Long) {
8 | Milliseconds(1),
9 | Seconds(1000),
10 | Minutes(60000);
11 |
12 | public fun toMillis(duration: Long): Long = duration * multiplier
13 | }
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, feel free to first discuss the change you wish to make via issue,
4 | email, or any other method with the owners of this repository before making a change.
5 |
6 | However, any library additions are always welcome. I am especially looking for the addition of new Kotlin/Native
7 | targets.
8 |
9 | ## Testing
10 | Please see [testing.md](TESTING.md) for full testing instructions. Your contributions should be able to pass every test.
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com.adamratzman.spotify/annotations/ExperimentalAnnotations.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.annotations
3 |
4 | @Retention(AnnotationRetention.BINARY)
5 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
6 | public annotation class SpotifyExperimentalHttpApi
7 |
8 | @Retention(AnnotationRetention.BINARY)
9 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
10 | public annotation class SpotifyExperimentalFunctionApi
11 |
--------------------------------------------------------------------------------
/src/commonJvmLikeMain/kotlin/com/adamratzman/spotify/utils/DateTimeUtils.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | import kotlinx.datetime.Instant
5 |
6 | /**
7 | * The current time in milliseconds since UNIX epoch.
8 | */
9 | public actual fun getCurrentTimeMs(): Long = System.currentTimeMillis()
10 |
11 | /**
12 | * Format date to ISO 8601 format
13 | */
14 | internal actual fun formatDate(date: Long): String {
15 | return Instant.fromEpochMilliseconds(date).toString()
16 | }
17 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com.adamratzman.spotify/utils/ConcurrentHashMap.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | public expect class ConcurrentHashMap() {
5 | public operator fun get(key: K): V?
6 | public fun put(key: K, value: V): V?
7 | public fun remove(key: K): V?
8 | public fun clear()
9 |
10 | public val size: Int
11 | public val entries: MutableSet>
12 | }
13 |
14 | public expect fun ConcurrentHashMap.asList(): List>
15 |
--------------------------------------------------------------------------------
/src/androidMain/res/layout/spotify_pkce_auth_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com.adamratzman.spotify/utils/Encoding.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | import com.soywiz.krypto.encoding.Base64
5 | import io.ktor.utils.io.core.toByteArray
6 |
7 | internal fun String.base64ByteEncode() = Base64.encode(toByteArray())
8 |
9 | public fun String.urlEncodeBase64String(): String {
10 | var result = this
11 | while (result.endsWith("=")) result = result.removeSuffix("=")
12 |
13 | return result.replace("/", "_").replace("+", "-")
14 | }
15 |
16 | internal expect fun String.encodeUrl(): String
17 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | systemProp.org.gradle.internal.publish.checksums.insecure=true
2 |
3 | org.gradle.daemon=true
4 | org.gradle.jvmargs=-Xmx8000m
5 | org.gradle.caching=true
6 |
7 | # android target settings
8 | android.useAndroidX=true
9 | android.enableJetifier=true
10 |
11 | # language dependencies
12 | kotlinVersion=1.9.22
13 |
14 | # library dependencies
15 | kotlinxDatetimeVersion=0.5.0
16 | kotlinxSerializationVersion=1.6.2
17 | ktorVersion=2.3.8
18 | korlibsVersion=3.4.0
19 | kotlinxCoroutinesVersion=1.7.3
20 |
21 | androidBuildToolsVersion=8.2.2
22 | androidSpotifyAuthVersion=2.1.1
23 | androidCryptoVersion=1.1.0-alpha06
24 | androidxCompatVersion=1.7.0-alpha03
25 |
26 | sparkVersion=2.9.4
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/commonNonJvmTargetsTest/kotlin/com.adamratzman.spotify/CommonImpl.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify
3 |
4 | actual fun areLivePkceTestsEnabled(): Boolean = false
5 | actual fun arePlayerTestsEnabled(): Boolean = false
6 | actual fun isHttpLoggingEnabled(): Boolean = false
7 | actual fun getTestTokenString(): String? = null
8 | actual fun getTestRedirectUri(): String? = null
9 | actual fun getTestClientId(): String? = null
10 | actual fun getTestClientSecret(): String? = null
11 | actual fun getResponseCacher(): ResponseCacher? = null
12 |
13 | actual suspend fun buildSpotifyApi(testClassQualifiedName: String, testName: String): GenericSpotifyApi? = null
14 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: adamint # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: adamratzman # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: adamint # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: adamratzman # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/com/adamratzman/spotify/utils/PlatformUtils.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | import kotlinx.coroutines.runBlocking
5 | import java.net.URLEncoder
6 |
7 | internal actual fun String.encodeUrl() = URLEncoder.encode(this, "UTF-8")!!
8 |
9 | /**
10 | * The actual platform that this program is running on.
11 | */
12 | public actual val currentApiPlatform: Platform = Platform.Jvm
13 |
14 | public actual typealias ConcurrentHashMap = java.util.concurrent.ConcurrentHashMap
15 |
16 | public actual fun ConcurrentHashMap.asList(): List> = toList()
17 |
18 | public actual fun runBlockingOnJvmAndNative(block: suspend () -> T): T {
19 | return runBlocking { block() }
20 | }
21 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com.adamratzman.spotify/models/Misc.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.models
3 |
4 | import kotlinx.serialization.Serializable
5 |
6 | /**
7 | * A Spotify image
8 | *
9 | * @param height The image height in pixels. If unknown: null or not returned.
10 | * @param url The source URL of the image.
11 | * @param width The image width in pixels. If unknown: null or not returned.
12 | */
13 | @Serializable
14 | public data class SpotifyImage(
15 | val height: Double? = null,
16 | val url: String,
17 | val width: Double? = null
18 | )
19 |
20 | /**
21 | * Contains an explanation of why a track is not available
22 | *
23 | * @param reason why the track is not available
24 | */
25 | @Serializable
26 | public data class Restrictions(val reason: String)
27 |
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com.adamratzman/spotify/pub/MarketsApiTest.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | @file:OptIn(ExperimentalCoroutinesApi::class)
3 |
4 | package com.adamratzman.spotify.pub
5 |
6 | import com.adamratzman.spotify.AbstractTest
7 | import com.adamratzman.spotify.GenericSpotifyApi
8 | import com.adamratzman.spotify.runTestOnDefaultDispatcher
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import kotlinx.coroutines.test.TestResult
11 | import kotlin.test.Test
12 | import kotlin.test.assertTrue
13 |
14 | class MarketsApiTest : AbstractTest() {
15 | @Test
16 | fun testGetAvailableMarkets(): TestResult = runTestOnDefaultDispatcher {
17 | buildApi(::testGetAvailableMarkets.name)
18 | assertTrue(api.markets.getAvailableMarkets().isNotEmpty())
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientUserApiTest.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | @file:OptIn(ExperimentalCoroutinesApi::class)
3 |
4 | package com.adamratzman.spotify.priv
5 |
6 | import com.adamratzman.spotify.AbstractTest
7 | import com.adamratzman.spotify.SpotifyClientApi
8 | import com.adamratzman.spotify.runTestOnDefaultDispatcher
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import kotlinx.coroutines.test.TestResult
11 | import kotlin.test.Test
12 |
13 | class ClientUserApiTest : AbstractTest() {
14 | @Test
15 | fun testClientProfile(): TestResult = runTestOnDefaultDispatcher {
16 | buildApi(::testClientProfile.name)
17 | if (!isApiInitialized()) return@runTestOnDefaultDispatcher
18 |
19 | api.users.getClientProfile().displayName
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/src/desktopMain/kotlin/com/adamratzman/spotify/utils/PlatformUtils.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | import io.ktor.http.encodeURLQueryComponent
5 | import kotlinx.datetime.Clock
6 | import kotlinx.datetime.Instant
7 |
8 | internal actual fun String.encodeUrl() = encodeURLQueryComponent()
9 |
10 | /**
11 | * Actual platform that this program is run on.
12 | */
13 | public actual val currentApiPlatform: Platform = Platform.Native
14 |
15 | public actual typealias ConcurrentHashMap = HashMap
16 |
17 | public actual fun ConcurrentHashMap.asList(): List> = toList()
18 |
19 | /**
20 | * The current time in milliseconds since UNIX epoch.
21 | */
22 | public actual fun getCurrentTimeMs(): Long = Clock.System.now().toEpochMilliseconds()
23 |
24 | /**
25 | * Format date to ISO 8601 format
26 | */
27 | internal actual fun formatDate(date: Long): String {
28 | return Instant.fromEpochMilliseconds(date).toString()
29 | }
30 |
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicUserApiTest.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | @file:OptIn(ExperimentalCoroutinesApi::class)
3 |
4 | package com.adamratzman.spotify.pub
5 |
6 | import com.adamratzman.spotify.AbstractTest
7 | import com.adamratzman.spotify.GenericSpotifyApi
8 | import com.adamratzman.spotify.runTestOnDefaultDispatcher
9 | import com.adamratzman.spotify.utils.catch
10 | import kotlinx.coroutines.ExperimentalCoroutinesApi
11 | import kotlinx.coroutines.test.TestResult
12 | import kotlin.test.Test
13 | import kotlin.test.assertNull
14 | import kotlin.test.assertTrue
15 |
16 | class PublicUserApiTest : AbstractTest() {
17 | @Test
18 | fun testPublicUser(): TestResult = runTestOnDefaultDispatcher {
19 | buildApi(::testPublicUser.name)
20 |
21 | assertTrue(catch { api.users.getProfile("adamratzman1")!!.followers.total } != null)
22 | assertNull(api.users.getProfile("ejwkfjwkerfjkwerjkfjkwerfjkjksdfjkasdf"))
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/nativeDarwinMain/kotlin/com/adamratzman/spotify/utils/PlatformUtils.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | import io.ktor.http.encodeURLQueryComponent
5 | import kotlinx.datetime.Clock
6 | import kotlinx.datetime.Instant
7 |
8 | internal actual fun String.encodeUrl() = encodeURLQueryComponent()
9 |
10 | /**
11 | * Actual platform that this program is run on.
12 | */
13 | public actual val currentApiPlatform: Platform = Platform.Native
14 |
15 | public actual typealias ConcurrentHashMap = HashMap
16 |
17 | public actual fun ConcurrentHashMap.asList(): List> = toList()
18 |
19 | /**
20 | * The current time in milliseconds since UNIX epoch.
21 | */
22 | public actual fun getCurrentTimeMs(): Long = Clock.System.now().toEpochMilliseconds()
23 |
24 | /**
25 | * Format date to ISO 8601 format
26 | */
27 | internal actual fun formatDate(date: Long): String {
28 | return Instant.fromEpochMilliseconds(date).toString()
29 | }
30 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI Test Workflow
2 |
3 | on:
4 | push:
5 | branches: [ main, dev, dev/** ]
6 | pull_request:
7 | branches: [ main, dev, dev/** ]
8 |
9 | jobs:
10 | test_android_jvm_linux_trusted:
11 | runs-on: ubuntu-latest
12 | environment: testing
13 | env:
14 | SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }}
15 | SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
16 | steps:
17 | - name: Check out repo
18 | uses: actions/checkout@v2
19 | - name: Install java 11
20 | uses: actions/setup-java@v2
21 | with:
22 | distribution: 'adopt'
23 | java-version: '17'
24 | - name: Install curl
25 | run: sudo apt-get install -y curl libcurl4-openssl-dev
26 | - name: Test android
27 | run: ./gradlew testDebugUnitTest
28 | - name: Test jvm
29 | run: ./gradlew jvmTest
30 | - name: Archive test results
31 | uses: actions/upload-artifact@v2
32 | if: always()
33 | with:
34 | name: code-coverage-report
35 | path: build/reports
36 |
--------------------------------------------------------------------------------
/src/androidMain/kotlin/com/adamratzman/spotify/auth/pkce/PkceAuthUtils.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.auth.pkce
3 |
4 | import android.app.Activity
5 | import android.content.Intent
6 | import android.os.Build
7 | import androidx.annotation.RequiresApi
8 |
9 | public fun Intent?.isSpotifyPkceAuthIntent(redirectUri: String): Boolean {
10 | return this != null &&
11 | (dataString?.startsWith("$redirectUri/?code=") == true || dataString?.startsWith("$redirectUri/?error=") == true)
12 | }
13 |
14 | /**
15 | * Start Spotify PKCE login activity within an existing activity.
16 | *
17 | * @param spotifyLoginImplementationClass Your implementation of [AbstractSpotifyPkceLoginActivity], defining what to do on Spotify PKCE login
18 | */
19 | @RequiresApi(Build.VERSION_CODES.M)
20 | public fun Activity.startSpotifyClientPkceLoginActivity(spotifyLoginImplementationClass: Class) {
21 | val intent = Intent(this, spotifyLoginImplementationClass)
22 | startActivity(intent)
23 | }
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Adam Ratzman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com.adamratzman.spotify/utils/IO.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | import com.soywiz.korim.bitmap.Bitmap
5 | import com.soywiz.korim.format.jpg.JPEG
6 | import com.soywiz.korio.file.VfsFile
7 | import com.soywiz.korio.file.std.UrlVfs
8 | import com.soywiz.korio.file.std.localVfs
9 | import com.soywiz.krypto.encoding.Base64
10 |
11 | /**
12 | * Represents an image. Please use convertXToBufferedImage and convertBufferedImageToX methods to read and write [BufferedImage]
13 | */
14 | public typealias BufferedImage = Bitmap
15 |
16 | public fun convertBufferedImageToBase64JpegString(image: BufferedImage): String {
17 | return Base64.encode(JPEG.encode(image))
18 | }
19 |
20 | public suspend fun convertUrlPathToBufferedImage(url: String): BufferedImage {
21 | return JPEG.decode(UrlVfs(url))
22 | }
23 |
24 | public suspend fun convertLocalImagePathToBufferedImage(path: String): BufferedImage {
25 | return JPEG.decode(localVfs(path))
26 | }
27 |
28 | public suspend fun convertFileToBufferedImage(file: VfsFile): BufferedImage = JPEG.decode(file.readBytes())
29 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/MarketsApi.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.endpoints.pub
3 |
4 | import com.adamratzman.spotify.GenericSpotifyApi
5 | import com.adamratzman.spotify.http.SpotifyEndpoint
6 | import com.adamratzman.spotify.models.serialization.toInnerArray
7 | import com.adamratzman.spotify.utils.Market
8 | import kotlinx.serialization.builtins.ListSerializer
9 | import kotlinx.serialization.builtins.serializer
10 |
11 | public class MarketsApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) {
12 | /**
13 | * Get the list of markets where Spotify is available.
14 | *
15 | * **[Api Reference](https://developer.spotify.com/documentation/web-api/reference/#category-markets)**
16 | *
17 | * @return List of [Market]
18 | */
19 | public suspend fun getAvailableMarkets(): List {
20 | return get(endpointBuilder("/markets").toString()).toInnerArray(
21 | ListSerializer(String.serializer()),
22 | "markets",
23 | json
24 | ).map { Market.valueOf(it.uppercase()) }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/androidMain/kotlin/com/adamratzman/spotify/notifications/SpotifyBroadcastReceiverUtils.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.notifications
3 |
4 | import android.app.Activity
5 | import android.content.Context
6 | import android.content.IntentFilter
7 | import androidx.fragment.app.Fragment
8 |
9 | /**
10 | * Register a Spotify broadcast receiver (receiving notifications from the Spotify app) for the specified notification types.
11 | *
12 | * This should be used in a [Fragment] or [Activity].
13 | *
14 | * Note that "Device Broadcast Status" must be enabled in the Spotify app and the active Spotify device must be the Android
15 | * device that your app is on to receive notifications.
16 | *
17 | * @param receiver An instance of your implementation of [AbstractSpotifyBroadcastReceiver]
18 | * @param notificationTypes The notification types that you would like to subscribe to.
19 | */
20 | public fun Context.registerSpotifyBroadcastReceiver(
21 | receiver: AbstractSpotifyBroadcastReceiver,
22 | vararg notificationTypes: SpotifyBroadcastType
23 | ) {
24 | val filter = IntentFilter()
25 | notificationTypes.forEach { filter.addAction(it.id) }
26 |
27 | registerReceiver(receiver, filter)
28 | }
29 |
--------------------------------------------------------------------------------
/src/commonJvmLikeMain/kotlin/com/adamratzman/spotify/javainterop/SpotifyContinuation.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | @file:JvmName("SpotifyContinuation")
3 |
4 | package com.adamratzman.spotify.javainterop
5 |
6 | import kotlin.coroutines.Continuation
7 | import kotlin.coroutines.CoroutineContext
8 | import kotlin.coroutines.EmptyCoroutineContext
9 |
10 | /**
11 | * A [Continuation] wrapper to allow you to directly implement [onSuccess] and [onFailure], when exceptions are hidden
12 | * on JVM via traditional continuations. **Please use this class as a callback anytime you are using Java code with this library.**
13 | *
14 | */
15 | public abstract class SpotifyContinuation : Continuation {
16 | /**
17 | * Invoke a function with the callback [value]
18 | *
19 | * @param value The value retrieved from the Spotify API.
20 | */
21 | public abstract fun onSuccess(value: T)
22 |
23 | /**
24 | * Handle exceptions during this API call.
25 | *
26 | * @param exception The exception that was thrown during the call.
27 | */
28 | public abstract fun onFailure(exception: Throwable)
29 |
30 | override fun resumeWith(result: Result) {
31 | result.fold(::onSuccess, ::onFailure)
32 | }
33 |
34 | override val context: CoroutineContext = EmptyCoroutineContext
35 | }
36 |
--------------------------------------------------------------------------------
/src/jsMain/kotlin/com/adamratzman/spotify/utils/ImplicitGrant.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | @file:Suppress("unused")
3 |
4 | package com.adamratzman.spotify.utils
5 |
6 | import com.adamratzman.spotify.SpotifyImplicitGrantApi
7 | import com.adamratzman.spotify.models.Token
8 | import kotlinx.browser.window
9 | import org.w3c.dom.url.URLSearchParams
10 |
11 | /**
12 | * Parse the current url into a valid [Token] to be used when instantiating a new [SpotifyImplicitGrantApi]
13 | */
14 | public fun parseSpotifyCallbackHashToToken(): Token = parseSpotifyCallbackHashToToken(window.location.hash.substring(1))
15 |
16 | /**
17 | * Parse the hash string into a valid [Token] to be used when instantiating a new [SpotifyImplicitGrantApi]
18 | *
19 | * @param hashString The Spotify hash string containing access_token, token_type, and expires_in.
20 | */
21 | public fun parseSpotifyCallbackHashToToken(hashString: String): Token {
22 | val hash = URLSearchParams(hashString)
23 |
24 | return Token(
25 | hash.get("access_token") ?: throw IllegalStateException("access_token is not part of the hash!"),
26 | hash.get("token_type") ?: throw IllegalStateException("token_type is not part of the hash!"),
27 | hash.get("expires_in")?.toIntOrNull() ?: throw IllegalStateException("expires_in is not part of the hash!")
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/jsMain/kotlin/com/adamratzman/spotify/utils/PlatformUtils.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.utils
3 |
4 | import com.adamratzman.spotify.SpotifyRestAction
5 | import io.ktor.http.encodeURLQueryComponent
6 | import kotlinx.coroutines.GlobalScope
7 | import kotlinx.coroutines.promise
8 | import kotlin.js.Date
9 | import kotlin.js.Promise
10 |
11 | internal actual fun String.encodeUrl() = encodeURLQueryComponent()
12 |
13 | /**
14 | * Actual platform that this program is run on.
15 | */
16 | public actual val currentApiPlatform: Platform = Platform.Js
17 |
18 | public actual typealias ConcurrentHashMap = HashMap
19 |
20 | public actual fun ConcurrentHashMap.asList(): List> = toList()
21 |
22 | public actual fun runBlockingOnJvmAndNative(block: suspend () -> T): T {
23 | throw IllegalStateException("JS does not have runBlocking")
24 | }
25 |
26 | public fun SpotifyRestAction.asPromise(): Promise = GlobalScope.promise {
27 | supplier()
28 | }
29 |
30 | /**
31 | * The current time in milliseconds since UNIX epoch.
32 | */
33 | public actual fun getCurrentTimeMs(): Long = Date.now().toLong()
34 |
35 | /**
36 | * Format date to ISO 8601 format
37 | */
38 | internal actual fun formatDate(date: Long): String {
39 | return Date(date).toISOString()
40 | }
41 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com.adamratzman.spotify/models/SpotifySearchResult.kt:
--------------------------------------------------------------------------------
1 | /* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
2 | package com.adamratzman.spotify.models
3 |
4 | import kotlinx.serialization.Serializable
5 |
6 | /**
7 | * Available filters that Spotify allows in search, in addition to filtering by object type.
8 | */
9 | public enum class SearchFilterType(public val id: String) {
10 | Album("album"),
11 | Artist("artist"),
12 | Track("track"),
13 | Year("year"),
14 | Upc("upc"),
15 | Hipster("tag:hipster"),
16 | New("tag:new"),
17 | Isrc("isrc"),
18 | Genre("genre")
19 | }
20 |
21 | /**
22 | * A filter of type [SearchFilterType]. Should be unique by type.
23 | *
24 | * @param filterValue A string to match, or in the case of [SearchFilterType.Year] can be a range of years in the form
25 | * A-B. Example: 2000-2010
26 | */
27 | public data class SearchFilter(val filterType: SearchFilterType, val filterValue: String)
28 |
29 | @Serializable
30 | public data class SpotifySearchResult(
31 | val albums: PagingObject? = null,
32 | val artists: PagingObject? = null,
33 | val playlists: PagingObject? = null,
34 | val tracks: PagingObject