All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.adamratzman.spotify.models.SpotifyUris.kt Maven / Gradle / Ivy

/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
@file:Suppress("EXPERIMENTAL_API_USAGE")

package com.adamratzman.spotify.models

import com.adamratzman.spotify.SpotifyException
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
 * Exception instantiating or deserializing a uri perceived as invalid
 */
public class SpotifyUriException(message: String) : SpotifyException.BadRequestException(message)

private fun String.matchType(type: String, allowColon: Boolean): String? {
    val uriContent = "[^:]".takeUnless { allowColon } ?: "."
    val typeRegex = "^spotify:(?:.*:)?$type:($uriContent*)(?::.*)*$|^([^:]+)\$".toRegex()
    val match = typeRegex.matchEntire(this)?.groupValues ?: return null
    return match[1].takeIf { it.isNotBlank() || match[2].isEmpty() } ?: match[2].takeIf { it.isNotEmpty() }
}

private fun String.matchesUserCollectionUri() = this.matches("^spotify:user:([^:]+):collection".toRegex())

private fun String.add(type: String, allowColon: Boolean): String {
    if (type == UserCollectionUriType && matchesUserCollectionUri()) {
        return this
    } else {
        this.matchType(type, allowColon)?.let {
            return "spotify:$type:${it.trim()}"
        }
    }
    throw SpotifyUriException("Illegal Spotify ID/URI: '$this' isn't convertible to '$type' uri")
}

private fun String.remove(type: String, allowColon: Boolean): String {
    if (type == UserCollectionUriType && matchesUserCollectionUri()) {
        return "collection"
    } else {
        this.matchType(type, allowColon)?.let {
            return it.trim()
        }
    }
    throw SpotifyUriException("Illegal Spotify ID/URI: '$this' isn't convertible to '$type' id")
}

private class SimpleUriSerializer(val ctor: (String) -> T) : KSerializer {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("SimpleUri", PrimitiveKind.STRING)
    override fun deserialize(decoder: Decoder): T {
        val str = decoder.decodeString()
        return ctor(str)
    }

    override fun serialize(encoder: Encoder, value: T) = encoder.encodeString(value.uri)
}

/**
 * @property uri retrieve this URI as a string
 * @property id representation of this uri as an id
 */
public interface ISpotifyUri {
    public val uri: String
    public val id: String
}

/**
 * Represents any Spotify **URI** (one of [ArtistUri], [PlayableUri], [ImmutableCollectionUri], [UserUri], [PlaylistUri]),
 * parsed from either a Spotify ID or taken from an endpoint.
 *
 * @param type The type (per Spotify) corresponding to the Uri.
 *
 */
@Serializable(with = SpotifyUriSerializer::class)
public sealed class SpotifyUri(input: String, public val type: String, allowColon: Boolean = false) : ISpotifyUri {
    public final override val uri: String
    public final override val id: String

    init {
        input.replace(" ", "").also {
            this.uri = it.add(type, allowColon)
            this.id = it.remove(type, allowColon)
        }
    }

    override fun equals(other: Any?): Boolean {
        val spotifyUri = other as? SpotifyUri ?: return false
        return spotifyUri.uri == this.uri
    }

    override fun hashCode(): Int {
        var result = uri.hashCode()
        result = 31 * result + id.hashCode()
        return result
    }

    override fun toString(): String {
        return "SpotifyUri(type=$type, uri=$uri)"
    }

    public companion object {
        /**
         * This function safely instantiates a SpotifyUri from given constructor.
         * */
        public inline fun  safeInitiate(uri: String, ctor: (String) -> T): T? {
            return try {
                ctor(uri)
            } catch (e: SpotifyUriException) {
                null
            }
        }

        /**
         * Creates a abstract SpotifyUri of given input. Doesn't allow ambiguity by disallowing creation by id.
         * */
        public operator fun invoke(input: String): SpotifyUri {
            val inputUriModified = input.removeSuffix(":recommended")

            val constructors = listOf(
                ::ArtistUri,
                PlayableUri.Companion::invoke,
                CollectionUri.Companion::invoke,
                ::UserUri,
                ::PlaylistUri
            )
            for (ctor in constructors) {
                safeInitiate(inputUriModified, ctor)?.takeIf { it.uri == inputUriModified }?.also { return it }
            }

            throw SpotifyUriException("Illegal Spotify ID/URI: '$inputUriModified' isn't convertible to any arbitrary id")
        }

        /**
         * This function returns whether or not the given input IS a given type.
         *
         * @example ```Kotlin
         *     SpotifyUri.isType("abc") // returns: false
         *     SpotifyUri.isType("spotify:user:abc") // returns: true
         *     SpotifyUri.isType("spotify:track:abc") // returns: false
         * ```
         * */
        public inline fun  isType(input: String): Boolean {
            return safeInitiate(input, ::invoke)?.let { it is T } ?: false
        }

        /**
         * This function returns whether ot not the given input CAN be a given type.
         *
         * @example ```Kotlin
         *     SpotifyUri.canBeType("abc") // returns: true
         *     SpotifyUri.canBeType("spotify:user:abc") // returns: true
         *     SpotifyUri.canBeType("spotify:track:abc") // returns: false
         * ```
         * */
        public inline fun  canBeType(input: String): Boolean {
            return isType(input) || !input.contains(':')
        }
    }
}

/**
 * Convert any (artist, [PlayableUri], [ImmutableCollectionUri], user, playlist) uri string to a [SpotifyUri] object.
 * Ambiguity is not allowed.
 *
 * *Note*: it is preferable to use a more specific function ([toArtistUri], [toPlayableUri], [toImmutableCollectionUri], [toUserUri], [toPlaylistUri]) if possible.
 */
public fun String.toSpotifyUri(): SpotifyUri = SpotifyUri(this)

// TODO replace serialization with JSON specific code
public object SpotifyUriSerializer : KSerializer {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("SpotifyUri", PrimitiveKind.STRING)
    override fun deserialize(decoder: Decoder): SpotifyUri = SpotifyUri(decoder.decodeString())
    override fun serialize(encoder: Encoder, value: SpotifyUri): Unit = encoder.encodeString(value.uri)
}

/**
 * Represents a Spotify **Collection** URI (one of [PlaylistUri] or [ImmutableCollectionUri]),
 * parsed from either a Spotify ID or taken from an endpoint.
 */
@Serializable(with = CollectionUriSerializer::class)
public sealed class CollectionUri(input: String, type: String, allowColon: Boolean = false) :
    SpotifyUri(input, type, allowColon) {
    public companion object {
        /**
         * Creates an abstract [CollectionUri] of given input. Prefers [PlaylistUri] if the input is ambiguous.
         */
        public operator fun invoke(input: String): CollectionUri {
            val constructors = listOf(::PlaylistUri, ::UserCollectionUri, ImmutableCollectionUri.Companion::invoke)
            for (ctor in constructors) {
                safeInitiate(input, ctor)?.also { return it }
            }
            throw SpotifyUriException("Illegal Spotify ID/URI: '$input' isn't convertible to 'playlist' or 'predefinedCollection' id")
        }
    }
}

/**
 * Convert a collection (playlist/[ImmutableCollectionUri]) id or uri string to an [ImmutableCollectionUri] object.
 * If an id is provided or the input is ambiguous, [PlaylistUri] is preferred.
 *
 * *Note*: it is preferable to use a more specific function ([toPlaylistUri], [toImmutableCollectionUri]) if possible.
 */
public fun String.toCollectionUri(): CollectionUri = CollectionUri(this)

public object CollectionUriSerializer : KSerializer {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CollectionUri", PrimitiveKind.STRING)
    override fun deserialize(decoder: Decoder): CollectionUri {
        return CollectionUri(decoder.decodeString())
    }

    override fun serialize(encoder: Encoder, value: CollectionUri): Unit = encoder.encodeString(value.uri)
}

/**
 * Represents a Spotify **Immutable Collection** URI (one of [AlbumUri] or [ShowUri]),
 * parsed from either a Spotify ID or taken from an endpoint.
 */
@Serializable(with = ImmutableCollectionUriSerializer::class)
public sealed class ImmutableCollectionUri(input: String, type: String, allowColon: Boolean = false) :
    CollectionUri(input, type, allowColon) {
    public companion object {
        /**
         * Creates an abstract [ImmutableCollectionUri] of given input. Prefers [AlbumUri] if the input is ambiguous.
         */
        public operator fun invoke(input: String): ImmutableCollectionUri {
            val constructors = listOf(::AlbumUri, ::ShowUri)
            for (ctor in constructors) {
                safeInitiate(input, ctor)?.also { return it }
            }
            throw SpotifyUriException("Illegal Spotify ID/URI: '$input' isn't convertible to 'album' or 'show' id")
        }
    }
}

/**
 * Convert an immutable collection (album/show) id or uri string to an [ImmutableCollectionUri] object.
 * If an id is provided or the input is ambiguous, [AlbumUri] is preferred.
 *
 * *Note*: it is preferable to use a more specific function ([toAlbumUri], [toShowUri]) if possible.
 */
public fun String.toImmutableCollectionUri(): ImmutableCollectionUri = ImmutableCollectionUri(this)

public object ImmutableCollectionUriSerializer : KSerializer {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ImmutableCollection", PrimitiveKind.STRING)
    override fun deserialize(decoder: Decoder): ImmutableCollectionUri =
        ImmutableCollectionUri(decoder.decodeString())

    override fun serialize(encoder: Encoder, value: ImmutableCollectionUri): Unit = encoder.encodeString(value.uri)
}

/**
 * Represents a Spotify **Playable** URI (one of [SpotifyTrackUri], [LocalTrackUri], or [EpisodeUri]),
 * parsed from either a Spotify ID or taken from an endpoint.
 */
@Serializable(with = PlayableUriSerializer::class)
public sealed class PlayableUri(input: String, type: String, allowColon: Boolean = false) :
    SpotifyUri(input, type, allowColon) {
    public companion object {
        /**
         * Creates an abstract [PlayableUri] of given input. Prefers [SpotifyTrackUri] if the input is ambiguous.
         */
        public operator fun invoke(input: String): PlayableUri {
            val constructors = listOf(::SpotifyTrackUri, ::LocalTrackUri, ::EpisodeUri)
            for (ctor in constructors) {
                safeInitiate(input, ctor)?.also { return it }
            }
            throw SpotifyUriException("Illegal Spotify ID/URI: '$input' isn't convertible to 'track' or 'localTrack' or 'episode' id")
        }
    }
}

/**
 * Convert a playable (track/local track/episode) id or uri string to a [PlayableUri] object.
 * If an id is provided or the input is ambiguous, [SpotifyTrackUri] is preferred.
 *
 * *Note*: it is preferable to use a more specific function ([toTrackUri], [toLocalTrackUri], [toEpisodeUri]) if possible.
 */
public fun String.toPlayableUri(): PlayableUri = PlayableUri(this)

public object PlayableUriSerializer : KSerializer {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PlayableUri", PrimitiveKind.STRING)
    override fun deserialize(decoder: Decoder): PlayableUri = PlayableUri(decoder.decodeString())
    override fun serialize(encoder: Encoder, value: PlayableUri): Unit = encoder.encodeString(value.uri)
}

/**
 * Represents a Spotify **Album** URI (spotify:album:XXXXXXXXXX), parsed from either a Spotify ID or taken from an endpoint.
 */
@Serializable(with = AlbumUriSerializer::class)
public class AlbumUri(input: String) : ImmutableCollectionUri(input, "album"), ContextUri
public object AlbumUriSerializer : KSerializer by SimpleUriSerializer(::AlbumUri)

/**
 * Convert an album id or uri string to an [AlbumUri] object
 */
public fun String.toAlbumUri(): AlbumUri = AlbumUri(this)

/**
 * Represents a Spotify **Artist** URI (spotify:artist:XXXXXXXXXX), parsed from either a Spotify ID or taken from an endpoint.
 */
@Serializable(with = ArtistUriSerializer::class)
public class ArtistUri(input: String) : SpotifyUri(input, "artist"), ContextUri
public object ArtistUriSerializer : KSerializer by SimpleUriSerializer(::ArtistUri)

/**
 * Convert an artist id or uri string to an [ArtistUri] object
 */
public fun String.toArtistUri(): ArtistUri = ArtistUri(this)

/**
 * Represents a Spotify **User** URI (spotify:user:XXXXXXXXXX), parsed from either a Spotify ID or taken from an endpoint.
 */
@Serializable(with = UserUriSerializer::class)
public class UserUri(input: String) : SpotifyUri(input, "user")
public object UserUriSerializer : KSerializer by SimpleUriSerializer(::UserUri)

/**
 * Convert a user id or uri string to a [UserUri] object
 */
public fun String.toUserUri(): UserUri = UserUri(this)

/**
 * Represents a Spotify **Playlist** URI (spotify:playlist:XXXXXXXXXX), parsed from either a Spotify ID or taken from an endpoint.
 */
@Serializable(with = PlaylistUriSerializer::class)
public class PlaylistUri(input: String) : CollectionUri(input, "playlist"), ContextUri
public object PlaylistUriSerializer : KSerializer by SimpleUriSerializer(::PlaylistUri)

/**
 * Convert a playlist id or uri string to a [PlaylistUri] object
 */
public fun String.toPlaylistUri(): PlaylistUri = PlaylistUri(this)

/**
 * Represents a Spotify **Track** URI (spotify:track:XXXXXXXXXX), parsed from either a Spotify ID or taken from an endpoint.
 */
@Serializable(with = SpotifyTrackUriSerializer::class)
public class SpotifyTrackUri(input: String) : PlayableUri(input, "track")
public object SpotifyTrackUriSerializer : KSerializer by SimpleUriSerializer(::SpotifyTrackUri)

/**
 * Convert a track (non-local) id or uri string to a [SpotifyTrackUri] object
 */
public fun String.toTrackUri(): SpotifyTrackUri = SpotifyTrackUri(this)

/**
 * Represents a Spotify **Local Track** URI (spotify:local:XXXXXXXXXX), parsed from either a Spotify ID or taken from an endpoint.
 */
@Serializable(with = LocalTrackUriSerializer::class)
public class LocalTrackUri(input: String) : PlayableUri(input, "local", allowColon = true)
public object LocalTrackUriSerializer : KSerializer by SimpleUriSerializer(::LocalTrackUri)

/**
 * Convert a local track id or uri string to a [LocalTrackUri] object
 */
public fun String.toLocalTrackUri(): LocalTrackUri = LocalTrackUri(this)

/**
 * Represents a Spotify **Episode** URI (spotify:episode:XXXXXXXXXX), parsed from either a Spotify ID or taken from an endpoint.
 */
@Serializable(with = EpisodeUriSerializer::class)
public class EpisodeUri(input: String) : PlayableUri(input, "episode")
public object EpisodeUriSerializer : KSerializer by SimpleUriSerializer(::EpisodeUri)

/**
 * Convert an episode id or uri string to an [EpisodeUri] object
 */
public fun String.toEpisodeUri(): EpisodeUri = EpisodeUri(this)

/**
 * Represents a Spotify **Show** URI (spotify:show:XXXXXXXXXX), parsed from either a Spotify ID or taken from an endpoint.
 */
@Serializable(with = ShowUriSerializer::class)
public class ShowUri(input: String) : ImmutableCollectionUri(input, "show"), ContextUri
public object ShowUriSerializer : KSerializer by SimpleUriSerializer(::ShowUri)

/**
 * Convert a show id or uri string to a [ShowUri] object
 */
public fun String.toShowUri(): ShowUri = ShowUri(this)

private const val UserCollectionUriType = "UserCollectionUri"

/**
 * Represents a Spotify **User Collection URI** URI (spotify:user:XXXX:collection), parsed from either a Spotify ID or taken from an endpoint.
 * It appears that this URI corresponds to the user's saved tracks collection in their library.
 */
@Serializable(with = UserCollectionUriSerializer::class)
public class UserCollectionUri(input: String) : CollectionUri(input, UserCollectionUriType), ContextUri
public object UserCollectionUriSerializer : KSerializer by SimpleUriSerializer(::UserCollectionUri)

/**
 * Convert a show id or uri string to a [ShowUri] object
 */
public fun String.toUserCollectionUri(): UserCollectionUri = UserCollectionUri(this)

/**
 * Represents a Spotify **Context** URI (one of [AlbumUri], [ArtistUri], [PlaylistUri], [UserCollectionUri], or [ShowUri]),
 */
@Serializable(with = ContextUriSerializer::class)
public interface ContextUri : ISpotifyUri {
    public companion object {
        /**
         * Creates an abstract [ContextUri] of given input. Prefers [PlaylistUri] if the input is ambiguous.
         */
        public operator fun invoke(input: String): ContextUri {
            val constructors = listOf(::PlaylistUri, ::AlbumUri, ::ArtistUri, ::ShowUri)
            for (ctor in constructors) {
                SpotifyUri.safeInitiate(input, ctor)?.also { return it }
            }
            throw SpotifyUriException("Illegal Spotify ID/URI: '$input' isn't convertible to 'playlist' or 'album' or 'artist' or 'show' id")
        }
    }
}

/**
 * Convert any (artist, album, playlist, or show) uri string to a [ContextUri] object.
 *
 * *Note*: it is preferable to use a more specific function ([toPlaylistUri], [toAlbumUri], [toArtistUri], [toShowUri]) if possible.
 */
public fun String.toContextUri(): ContextUri = ContextUri(this)

public object ContextUriSerializer : KSerializer {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PlayableUri", PrimitiveKind.STRING)
    override fun deserialize(decoder: Decoder): ContextUri = ContextUri(decoder.decodeString())
    override fun serialize(encoder: Encoder, value: ContextUri): Unit = encoder.encodeString(value.uri)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy