Skip to content

Commit 98a28a3

Browse files
committed
add common upload methods for playlists (fixes #254, #255)
Signed-off-by: Adam Ratzman <[email protected]>
1 parent e14271a commit 98a28a3

File tree

16 files changed

+1926
-138
lines changed

16 files changed

+1926
-138
lines changed

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,6 @@ If you have a question, you can:
9090
## Unsupported features on each platform:
9191
| Feature | JVM | Android | JS | Native (Mac/Windows/Linux) |
9292
|-----------------------------|--------------------|--------------------|--------------------|----------------------------|
93-
| Images (Playlist covers) | :heavy_check_mark: | :heavy_check_mark: | Unsupported | Unsupported |
94-
| getSpotifyPkceCodeChallenge | :heavy_check_mark: | :heavy_check_mark: | Unsupported | Unsupported |
9593
| Edit client playlist | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | Unsupported |
9694
| Remove playlist tracks | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | Unsupported |
9795

@@ -182,7 +180,7 @@ hash the code verifier using the SHA256 algorithm. Then, base64url encode the ha
182180
the code challenge used to authorize the user.
183181

184182
This library contains helpful methods that can be used to simplify the PKCE authorization process.
185-
This includes `getSpotifyPkceCodeChallenge` (not available in the Kotlin/JS target), which SHA256 hashes and base64url encodes the code
183+
This includes `getSpotifyPkceCodeChallenge`, which SHA256 hashes and base64url encodes the code
186184
challenge, and `getPkceAuthorizationUrl`, which allows you to generate an easy authorization url for PKCE flow.
187185

188186
Please see the [spotifyClientPkceApi builder docs](https://adamint.github.io/spotify-web-api-kotlin-docs/spotify-web-api-kotlin/com.adamratzman.spotify/spotify-client-pkce-api.html) for a full list of available builders.

build.gradle.kts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,16 +175,22 @@ kotlin {
175175
sourceSets {
176176
val coroutineVersion = "1.4.2-native-mt"
177177
val serializationVersion = "1.0.1"
178-
val ktorVersion = "1.5.0"
179-
val klockVersion = "2.0.3"
178+
val ktorVersion = "1.5.1"
179+
val korlibsVersion = "2.0.6"
180180

181181
val commonMain by getting {
182182
dependencies {
183-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
183+
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"){
184+
version {
185+
strictly(coroutineVersion)
186+
}
187+
}
184188
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion")
185189
implementation("io.ktor:ktor-client-core:$ktorVersion")
186-
implementation("com.soywiz.korlibs.klock:klock:$klockVersion")
187-
implementation("com.soywiz.korlibs.krypto:krypto:2.0.6")
190+
implementation("com.soywiz.korlibs.klock:klock:$korlibsVersion")
191+
implementation("com.soywiz.korlibs.krypto:krypto:$korlibsVersion")
192+
implementation("com.soywiz.korlibs.korim:korim:$korlibsVersion")
193+
188194
}
189195
}
190196

src/androidMain/kotlin/com/adamratzman/spotify/utils/PlatformUtils.kt

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@
22
package com.adamratzman.spotify.utils
33

44
import android.annotation.SuppressLint
5-
import android.graphics.Bitmap
6-
import android.graphics.BitmapFactory
7-
import android.util.Base64
8-
import java.io.ByteArrayOutputStream
9-
import java.net.URL
105
import java.net.URLEncoder
116
import java.text.SimpleDateFormat
127
import java.util.Date
@@ -18,32 +13,11 @@ internal actual fun formatDate(format: String, date: Long): String {
1813
return SimpleDateFormat(format).format(Date(date))
1914
}
2015

21-
internal actual fun encodeBufferedImageToBase64String(image: BufferedImage): String {
22-
val byteArrayOutputStream = ByteArrayOutputStream()
23-
image.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
24-
val byteArray: ByteArray = byteArrayOutputStream.toByteArray()
25-
return Base64.encodeToString(byteArray, Base64.DEFAULT)
26-
}
27-
28-
internal actual fun convertFileToBufferedImage(file: File): BufferedImage = BitmapFactory.decodeFile(file.absolutePath)
29-
30-
internal actual fun convertLocalImagePathToBufferedImage(path: String): BufferedImage = BitmapFactory.decodeFile(path)
31-
32-
internal actual fun convertUrlPathToBufferedImage(url: String): BufferedImage {
33-
return URL(url).openConnection().getInputStream().use { inputStream ->
34-
BitmapFactory.decodeStream(inputStream)
35-
}
36-
}
37-
3816
/**
3917
* Actual platform that this program is run on.
4018
*/
4119
public actual val currentApiPlatform: Platform = Platform.ANDROID
4220

4321
public actual typealias ConcurrentHashMap<K, V> = java.util.concurrent.ConcurrentHashMap<K, V>
4422

45-
public actual typealias BufferedImage = Bitmap // TODO
46-
47-
public actual typealias File = java.io.File
48-
4923
public actual fun <K, V> ConcurrentHashMap<K, V>.asList(): List<Pair<K, V>> = toList()

src/commonMain/kotlin/com.adamratzman.spotify/Builder.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.adamratzman.spotify.http.HttpRequestMethod
77
import com.adamratzman.spotify.models.Token
88
import com.adamratzman.spotify.models.serialization.nonstrictJson
99
import com.adamratzman.spotify.models.serialization.toObject
10+
import com.adamratzman.spotify.utils.urlEncodeBase64String
1011
import com.soywiz.krypto.SHA256
1112
import io.ktor.client.features.ServerResponseException
1213
import io.ktor.utils.io.core.toByteArray
@@ -80,11 +81,8 @@ public fun getPkceAuthorizationUrl(
8081
*/
8182
public fun getSpotifyPkceCodeChallenge(codeVerifier: String): String {
8283
if (codeVerifier.length !in 43..128) throw IllegalArgumentException("Code verifier must be between 43 and 128 characters long")
83-
84-
var sha256 = SHA256.digest(codeVerifier.toByteArray()).base64
85-
while (sha256.endsWith("=")) sha256 = sha256.removeSuffix("=")
86-
87-
return sha256.replace("/", "_").replace("+", "-")
84+
val sha256 = SHA256.digest(codeVerifier.toByteArray()).base64
85+
return sha256.urlEncodeBase64String()
8886
}
8987

9088
// ==============================================

src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlaylistApi.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ import com.adamratzman.spotify.models.serialization.mapToJsonString
1717
import com.adamratzman.spotify.models.serialization.toNonNullablePagingObject
1818
import com.adamratzman.spotify.models.serialization.toObject
1919
import com.adamratzman.spotify.utils.BufferedImage
20-
import com.adamratzman.spotify.utils.File
20+
import com.adamratzman.spotify.utils.convertBufferedImageToBase64JpegString
2121
import com.adamratzman.spotify.utils.convertFileToBufferedImage
2222
import com.adamratzman.spotify.utils.convertLocalImagePathToBufferedImage
2323
import com.adamratzman.spotify.utils.convertUrlPathToBufferedImage
24-
import com.adamratzman.spotify.utils.encodeBufferedImageToBase64String
2524
import com.adamratzman.spotify.utils.encodeUrl
2625
import com.adamratzman.spotify.utils.jsonMap
26+
import com.soywiz.korio.file.VfsFile
2727
import kotlinx.serialization.SerialName
2828
import kotlinx.serialization.Serializable
2929
import kotlinx.serialization.json.JsonArray
@@ -334,7 +334,8 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) {
334334
* @param playlist the id or uri for the playlist.
335335
* @param imagePath Optionally specify the full local path to the image
336336
* @param imageUrl Optionally specify a URL to the image
337-
* @param imageFile Optionally specify the image [File]
337+
* @param imageFile Optionally specify the image [VfsFile]. Note that in each platform, there is a [toVfs] method to convert
338+
* the platform's file type to a [VfsFile]. For example, `java.util.File.toVfs()` will return a [VfsFile].
338339
* @param image Optionally specify the image's [BufferedImage] object
339340
* @param imageData Optionally specify the Base64-encoded image data yourself
340341
*
@@ -343,16 +344,16 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) {
343344
public suspend fun uploadClientPlaylistCover(
344345
playlist: String,
345346
imagePath: String? = null,
346-
imageFile: File? = null,
347+
imageFile: VfsFile? = null,
347348
image: BufferedImage? = null,
348349
imageData: String? = null,
349350
imageUrl: String? = null
350351
) {
351352
val data = imageData ?: when {
352-
image != null -> encodeBufferedImageToBase64String(image)
353-
imageFile != null -> encodeBufferedImageToBase64String(convertFileToBufferedImage(imageFile))
354-
imageUrl != null -> encodeBufferedImageToBase64String(convertUrlPathToBufferedImage(imageUrl))
355-
imagePath != null -> encodeBufferedImageToBase64String(convertLocalImagePathToBufferedImage(imagePath))
353+
image != null -> convertBufferedImageToBase64JpegString(image)
354+
imageFile != null -> convertBufferedImageToBase64JpegString(convertFileToBufferedImage(imageFile))
355+
imageUrl != null -> convertBufferedImageToBase64JpegString(convertUrlPathToBufferedImage(imageUrl))
356+
imagePath != null -> convertBufferedImageToBase64JpegString(convertLocalImagePathToBufferedImage(imagePath))
356357
else -> throw IllegalArgumentException("No cover image was specified")
357358
}
358359
put(

src/commonMain/kotlin/com.adamratzman.spotify/models/Users.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,12 @@ public data class SpotifyPublicUser(
7373
* @param href Will always be null, per the Spotify documentation,
7474
* until the Web API is updated to support this.
7575
*
76-
* @param total -1 if the user object does not contain followers, otherwise the amount of followers the user has
76+
* @param total Null or -1 if the user object does not contain followers, otherwise the amount of followers the user has
7777
*/
7878
@Serializable
7979
public data class Followers(
8080
val href: String? = null,
81-
@SerialName("total") val total: Int
81+
@SerialName("total") val total: Int? = null
8282
)
8383

8484
@Serializable

src/commonMain/kotlin/com.adamratzman.spotify/utils/Encoding.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,12 @@ import com.soywiz.krypto.encoding.Base64
55
import io.ktor.utils.io.core.toByteArray
66

77
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+
816
internal expect fun String.encodeUrl(): String
Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
11
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2021; Original author: Adam Ratzman */
22
package com.adamratzman.spotify.utils
33

4-
internal expect fun encodeBufferedImageToBase64String(image: BufferedImage): String
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+
import io.ktor.utils.io.core.String
511

6-
internal expect fun convertFileToBufferedImage(file: File): BufferedImage
7-
internal expect fun convertUrlPathToBufferedImage(url: String): BufferedImage
8-
internal expect fun convertLocalImagePathToBufferedImage(path: String): BufferedImage
12+
/**
13+
* Represents an image. Please use convertXToBufferedImage and convertBufferedImageToX methods to read and write [BufferedImage]
14+
*/
15+
public typealias BufferedImage = Bitmap
16+
17+
public fun convertBufferedImageToBase64JpegString(image: BufferedImage): String {
18+
return Base64.encode(JPEG.encode(image))
19+
}
20+
21+
public suspend fun convertUrlPathToBufferedImage(url: String): BufferedImage {
22+
return JPEG.decode(UrlVfs(url))
23+
}
24+
25+
public suspend fun convertLocalImagePathToBufferedImage(path: String): BufferedImage {
26+
return JPEG.decode(localVfs(path))
27+
}
28+
29+
public suspend fun convertFileToBufferedImage(file: VfsFile): BufferedImage = JPEG.decode(file.readBytes())

src/commonMain/kotlin/com.adamratzman.spotify/utils/Types.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,3 @@ public expect class ConcurrentHashMap<K, V>() {
1212
}
1313

1414
public expect fun <K, V> ConcurrentHashMap<K, V>.asList(): List<Pair<K, V>>
15-
16-
public expect class BufferedImage
17-
18-
public expect class File
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2021; Original author: Adam Ratzman */
2+
package com.soywiz.korim.format.jpg
3+
4+
import com.soywiz.korim.format.ImageData
5+
import com.soywiz.korim.format.ImageDecodingProps
6+
import com.soywiz.korim.format.ImageEncodingProps
7+
import com.soywiz.korim.format.ImageFormat
8+
import com.soywiz.korim.format.ImageFrame
9+
import com.soywiz.korim.format.ImageInfo
10+
import com.soywiz.korio.stream.SyncStream
11+
import com.soywiz.korio.stream.readAll
12+
import com.soywiz.korio.stream.writeBytes
13+
14+
public object JPEG : ImageFormat("jpg", "jpeg") {
15+
override fun decodeHeader(s: SyncStream, props: ImageDecodingProps): ImageInfo? = try {
16+
val info = JPEGDecoder.decodeInfo(s.readAll())
17+
ImageInfo().apply {
18+
this.width = info.width
19+
this.height = info.height
20+
this.bitsPerPixel = 24
21+
}
22+
} catch (e: Throwable) {
23+
null
24+
}
25+
26+
override fun readImage(s: SyncStream, props: ImageDecodingProps): ImageData {
27+
return ImageData(listOf(ImageFrame(JPEGDecoder.decode(s.readAll()))))
28+
}
29+
30+
override fun writeImage(image: ImageData, s: SyncStream, props: ImageEncodingProps) {
31+
s.writeBytes(JPEGEncoder.encode(image.mainBitmap.toBMP32(), quality = (props.quality * 100).toInt()))
32+
}
33+
}

0 commit comments

Comments
 (0)