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

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

/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.models

import com.adamratzman.spotify.GenericSpotifyApi
import com.adamratzman.spotify.SpotifyRestAction
import com.adamratzman.spotify.models.PagingTraversalType.BACKWARDS
import com.adamratzman.spotify.models.PagingTraversalType.FORWARDS
import com.adamratzman.spotify.models.serialization.instantiateAllNeedsApiObjects
import com.adamratzman.spotify.models.serialization.instantiateLateinitsForPagingObject
import com.adamratzman.spotify.models.serialization.toCursorBasedPagingObject
import com.adamratzman.spotify.models.serialization.toNonNullablePagingObject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.toList
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.KClass

/*
    Types used in PagingObjects and CursorBasedPagingObjects:

    CursorBasedPagingObject:
       PlayHistory
       Artist

    PagingObject:
       SimpleTrack
       SimpleAlbum
       SpotifyCategory
       SimplePlaylist
       SavedTrack
       SavedAlbum
       Artist
       Track
       PlaylistTrack

 */

public enum class PagingTraversalType {
    BACKWARDS,
    FORWARDS;
}

/**
 * The offset-based nullable paging object is a container for a set of objects. It contains a key called items
 * (whose value is an array of the requested objects) along with other keys like previous, next and
 * limit that can be useful in future calls. Its items are not guaranteed to be not null
 */
@Serializable
public class NullablePagingObject(
    override val href: String,
    override val items: List,
    override val limit: Int,
    override val next: String? = null,
    override val offset: Int,
    override val previous: String? = null,
    override val total: Int = 0
) : AbstractPagingObject>() {
    public fun toPagingObject(): PagingObject {
        val pagingObject = PagingObject(
            href,
            items.filterNotNull(),
            limit,
            next,
            offset,
            previous,
            total
        )
        pagingObject.instantiateLateinitsForPagingObject(itemClass, api)

        return pagingObject
    }

    override fun iterator(): Iterator = items.iterator()
    override fun listIterator(): ListIterator = items.listIterator()
    override fun listIterator(index: Int): ListIterator = items.listIterator(index)
    override fun subList(fromIndex: Int, toIndex: Int): List = items.subList(fromIndex, toIndex)
}

/**
 * The offset-based non-nullable paging object is a container for a set of objects. It contains a key called items
 * (whose value is an array of the requested objects) along with other keys like previous, next and
 * limit that can be useful in future calls.
 */
@Serializable
public data class PagingObject(
    override val href: String,
    override val items: List,
    override val limit: Int,
    override val next: String? = null,
    override val offset: Int,
    override val previous: String? = null,
    override val total: Int = 0
) : AbstractPagingObject>() {
    override fun get(index: Int): T = super.get(index)!!

    override fun iterator(): Iterator = items.iterator()
    override fun listIterator(): ListIterator = items.listIterator()
    override fun listIterator(index: Int): ListIterator = items.listIterator(index)
    override fun subList(fromIndex: Int, toIndex: Int): List = items.subList(fromIndex, toIndex)

    override suspend fun take(n: Int): List {
        return super.take(n).filterNotNull()
    }
}

/**
 * The offset-based paging object is a container for a set of objects. It contains a key called items
 * (whose value is an array of the requested objects) along with other keys like previous, next and
 * limit that can be useful in future calls.
 *
 * @property href A link to the Web API endpoint returning the full result of the request.
 * @property items The requested data.
 * @property limit The maximum number of items in the response (as set in the query or by default).
 * @property next URL to the next page of items. ( null if none)
 * @property previous URL to the previous page of items. ( null if none)
 * @property total The maximum number of items available to return.
 * @property offset The offset of the items returned (as set in the query or by default).
 */
@Serializable
public abstract class AbstractPagingObject> :
    PagingObjectBase(),
    List {
    @Suppress("UNCHECKED_CAST")
    override suspend fun get(type: PagingTraversalType): Z? {
        return (if (type == FORWARDS) next else previous)?.let { api.defaultEndpoint.get(it) }?.let { json ->
            when (itemClass) {
                SimpleTrack::class -> json.toNonNullablePagingObject(
                    SimpleTrack.serializer(),
                    null,
                    api,
                    api.spotifyApiOptions.json,
                    true
                )
                SpotifyCategory::class -> json.toNonNullablePagingObject(
                    SpotifyCategory.serializer(),
                    null,
                    api,
                    api.spotifyApiOptions.json,
                    true
                )
                SimpleAlbum::class -> json.toNonNullablePagingObject(
                    SimpleAlbum.serializer(),
                    null,
                    api,
                    api.spotifyApiOptions.json,
                    true
                )
                SimplePlaylist::class -> json.toNonNullablePagingObject(
                    SimplePlaylist.serializer(),
                    null,
                    api,
                    api.spotifyApiOptions.json,
                    true
                )
                SavedTrack::class -> json.toNonNullablePagingObject(
                    SavedTrack.serializer(),
                    null,
                    api,
                    api.spotifyApiOptions.json,
                    true
                )
                SavedAlbum::class -> json.toNonNullablePagingObject(
                    SavedAlbum.serializer(),
                    null,
                    api,
                    api.spotifyApiOptions.json,
                    true
                )
                Artist::class -> json.toNonNullablePagingObject(
                    Artist.serializer(),
                    null,
                    api,
                    api.spotifyApiOptions.json,
                    true
                )
                Track::class -> json.toNonNullablePagingObject(
                    Track.serializer(),
                    null,
                    api,
                    api.spotifyApiOptions.json,
                    true
                )
                PlaylistTrack::class -> json.toNonNullablePagingObject(
                    PlaylistTrack.serializer(),
                    null,
                    api,
                    api.spotifyApiOptions.json,
                    true
                )
                else -> throw IllegalArgumentException("Unknown type ($itemClass) in $href response")
            } as? Z
        }
    }

    override suspend fun getWithNextTotalPagingObjects(total: Int): List {
        @Suppress("UNCHECKED_CAST")
        val pagingObjects = mutableListOf(this as Z)

        var nxt = next?.let { getNext() }
        while (pagingObjects.size < total && nxt != null) {
            pagingObjects.add(nxt)
            nxt = nxt.next?.let { nxt?.getNext() }
        }

        return pagingObjects.distinctBy { it.href }
    }

    override suspend fun getAllPagingObjects(): List {
        val pagingObjects = mutableListOf()
        var prev = previous?.let { getPrevious() }
        while (prev != null) {
            pagingObjects.add(prev)
            prev = prev.previous?.let { prev?.getPrevious() }
        }
        pagingObjects.reverse() // closer we are to current, the further we are from the start

        @Suppress("UNCHECKED_CAST")
        pagingObjects.add(this as Z)
        var nxt = next?.let { getNext() }
        while (nxt != null) {
            pagingObjects.add(nxt)
            nxt = nxt.next?.let { nxt?.getNext() }
        }

        // we don't need to reverse here, as it's in order
        return pagingObjects
    }

    /**
     * Synchronously retrieve the next [total] paging objects associated with this [AbstractPagingObject], including this [AbstractPagingObject].
     *
     * @param total The total amount of [AbstractPagingObject] to request, which includes this [AbstractPagingObject].
     * @since 3.0.0
     */
    @Suppress("UNCHECKED_CAST")
    public suspend fun getWithNext(total: Int): List = getWithNextTotalPagingObjects(total)

    /**
     * Get all items of type [T] associated with the request
     */
    public override suspend fun getAllItems(): List = getAllPagingObjects().map { it.items }.flatten()
}

/**
 * The cursor-based paging object is a container for a set of objects. It contains a key called
 * items (whose value is an array of the requested objects) along with other keys like next and
 * cursors that can be useful in future calls.
 *
 * @param href A link to the Web API endpoint returning the full result of the request.
 * @param items The requested data.
 * @param limit The maximum number of items in the response (as set in the query or by default).
 * @param next URL to the next page of items. ( null if none)
 * @param total The maximum number of items available to return.
 * @param cursor The cursors used to find the next set of items. If [items] is empty, cursor may be null.
 */
@Serializable
public data class CursorBasedPagingObject(
    override val href: String,
    override val items: List,
    override val limit: Int,
    override val next: String? = null,
    @SerialName("cursors") public val cursor: Cursor? = null,
    override val total: Int = 0,
    override val offset: Int = 0,
    override val previous: String? = null
) : PagingObjectBase>() {
    /**
     * Synchronously retrieve the next [total] paging objects associated with this [CursorBasedPagingObject], including this [CursorBasedPagingObject].
     *
     * @param total The total amount of [CursorBasedPagingObject] to request, which includes this [CursorBasedPagingObject].
     * @since 3.0.0
     */
    @Suppress("UNCHECKED_CAST")
    public suspend fun getWithNext(total: Int): List> = getWithNextTotalPagingObjects(total)

    /**
     * Get all items of type [T] associated with the request
     */
    override suspend fun getAllItems(): List = getAllPagingObjects().map { it.items }.flatten()

    override suspend fun get(type: PagingTraversalType): CursorBasedPagingObject? {
        require(type != BACKWARDS) { "CursorBasedPagingObjects only can go forwards" }
        return next?.let { getCursorBasedPagingObject(it) }
    }

    @Suppress("UNCHECKED_CAST")
    public suspend fun getCursorBasedPagingObject(url: String): CursorBasedPagingObject? {
        val json = api.defaultEndpoint.get(url)
        return when (itemClass) {
            PlayHistory::class -> json.toCursorBasedPagingObject(
                PlayHistory::class,
                PlayHistory.serializer(),
                null,
                api,
                api.spotifyApiOptions.json
            )
            Artist::class -> json.toCursorBasedPagingObject(
                Artist::class,
                Artist.serializer(),
                null,
                api,
                api.spotifyApiOptions.json
            )
            else -> throw IllegalArgumentException("Unknown type in $href")
        } as? CursorBasedPagingObject
    }

    override suspend fun getAllPagingObjects(): List> {
        val pagingObjects = mutableListOf>()
        var currentPagingObject = this@CursorBasedPagingObject
        pagingObjects.add(currentPagingObject)
        while (true) {
            currentPagingObject = currentPagingObject.get(FORWARDS) ?: break
            pagingObjects.add(currentPagingObject)
        }
        return pagingObjects
    }

    override suspend fun getWithNextTotalPagingObjects(total: Int): List> {
        val pagingObjects = mutableListOf(this)

        var nxt = getNext()
        while (pagingObjects.size < total && nxt != null) {
            pagingObjects.add(nxt)
            nxt = nxt.next?.let { nxt?.getNext() }
        }

        return pagingObjects.distinctBy { it.href }
    }

    override fun get(index: Int): T = super.get(index)!!
    override fun iterator(): Iterator = items.iterator()
    override fun listIterator(): ListIterator = items.listIterator()
    override fun listIterator(index: Int): ListIterator = items.listIterator(index)
    override fun subList(fromIndex: Int, toIndex: Int): List = items.subList(fromIndex, toIndex)

    override suspend fun take(n: Int): List {
        return super.take(n).filterNotNull()
    }
}

/**
 * The cursor to use as key to find the next (or previous) page of items.
 *
 * @param before The cursor to use as key to find the previous page of items.
 * @param after The cursor to use as key to find the next page of items.
 */
@Serializable
public data class Cursor(val before: String? = null, val after: String? = null)

/**
 * @property href A link to the Web API endpoint returning the full result of the request.
 * @property items The requested data.
 * @property limit The maximum number of items in the response (as set in the query or by default).
 * @property next URL to the next page of items. ( null if none)
 * @property previous URL to the previous page of items. ( null if none)
 * @property total The maximum number of items available to return.
 * @property offset The offset of the items returned (as set in the query or by default).
 */
@Serializable
public abstract class PagingObjectBase> : List, NeedsApi() {
    public abstract val href: String
    public abstract val items: List
    public abstract val limit: Int
    public abstract val next: String?
    public abstract val offset: Int
    public abstract val previous: String?
    public abstract val total: Int

    @Suppress("UNCHECKED_CAST")
    override fun getMembersThatNeedApiInstantiation(): List {
        return if (items.getOrNull(0) !is NeedsApi) {
            listOf(this)
        } else {
            (items as List) + listOf(this)
        }
    }

    @Transient
    internal var itemClass: KClass? = null

    internal abstract suspend fun get(type: PagingTraversalType): Z?

    /**
     * Retrieve all [PagingObjectBase] associated with this rest action
     */
    public abstract suspend fun getAllPagingObjects(): List

    /**
     * Retrieve all [PagingObjectBase] associated with this rest action
     */
    public fun getAllPagingObjectsRestAction(): SpotifyRestAction> = SpotifyRestAction { getAllPagingObjects() }

    /**
     * Retrieve all [T] associated with this rest action
     */
    public abstract suspend fun getAllItems(): List

    /**
     * Retrieve all [T] associated with this rest action
     */
    public fun getAllItemsRestAction(): SpotifyRestAction> = SpotifyRestAction { getAllItems() }

    /**
     * Synchronously retrieve the next [total] paging objects associated with this [PagingObjectBase], including this [PagingObjectBase].
     *
     * @param total The total amount of [PagingObjectBase] to request, which includes this [PagingObjectBase].
     * @since 3.0.0
     */
    public abstract suspend fun getWithNextTotalPagingObjects(total: Int): List

    /**
     * Synchronously retrieve the next [total] paging objects associated with this [PagingObjectBase], including this [PagingObjectBase].
     *
     * @param total The total amount of [PagingObjectBase] to request, which includes this [PagingObjectBase].
     * @since 3.0.0
     */
    public fun getWithNextTotalPagingObjectsRestAction(total: Int): SpotifyRestAction> =
        SpotifyRestAction { getWithNextTotalPagingObjects(total) }

    public suspend fun getNext(): Z? = get(FORWARDS)

    public fun getNextRestAction(): SpotifyRestAction = SpotifyRestAction { getNext() }

    public suspend fun getPrevious(): Z? = get(BACKWARDS)

    public fun getPreviousRestAction(): SpotifyRestAction = SpotifyRestAction { getPrevious() }

    /**
     * Get all items of type [T] associated with the request. Filters out null objects.
     */
    public suspend fun getAllItemsNotNull(): List = getAllItems().filterNotNull()

    /**
     * Get all items of type [T] associated with the request. Filters out null objects.
     */
    public fun getAllItemsNotNullRestAction(): SpotifyRestAction> = SpotifyRestAction { getAllItemsNotNull() }

    /**
     * Retrieve the items associated with the next [total] paging objects associated with this rest action, including the current one.
     *
     * @param total The total amount of [PagingObjectBase] to request, including the [PagingObjectBase] associated with the current request.
     * @since 3.0.0
     */
    public suspend fun getWithNextItems(total: Int): List =
        getWithNextTotalPagingObjects(total).map { it.items }.flatten()

    /**
     * Retrieve the items associated with the next [total] paging objects associated with this rest action, including the current one.
     *
     * @param total The total amount of [PagingObjectBase] to request, including the [PagingObjectBase] associated with the current request.
     * @since 3.0.0
     */
    public fun getWithNextItemsRestAction(total: Int): SpotifyRestAction> =
        SpotifyRestAction { getWithNextItems(total) }

    /**
     * Flow from current page backwards.
     * */
    public fun flowBackward(): Flow = flow {
        if (previous == null) return@flow
        var next = getPrevious()
        while (next != null) {
            emit(next)
            next = next.getPrevious()
        }
    }.flowOn(Dispatchers.Default)

    /**
     * Flow from current page forwards.
     * */
    @ExperimentalCoroutinesApi
    public fun flowForward(): Flow = flow {
        if (next == null) return@flow
        var next = getNext()
        while (next != null) {
            emit(next)
            next = next.getNext()
        }
    }.flowOn(Dispatchers.Default)

    @ExperimentalCoroutinesApi
    public fun flowStartOrdered(): Flow =
        flow {
            if (previous == null) return@flow
            flowBackward().toList().reversed().also {
                emitAll(it.asFlow())
            }
        }.flowOn(Dispatchers.Default)

    @ExperimentalCoroutinesApi
    public fun flowEndOrdered(): Flow = flowForward()

    /**
     * Flow the paging action ordered. This can be less performant than [flow] if you are in the middle of the pages.
     * */
    @FlowPreview
    @ExperimentalCoroutinesApi
    public fun flowOrdered(context: CoroutineContext = Dispatchers.Default): Flow = flow {
        emitAll(flowPagingObjectsOrdered().flatMapConcat { it.asFlow() })
    }.flowOn(context)

    /**
     * Flow the paging objects ordered. This can be less performant than [flowPagingObjects] if you are in the middle of the pages.
     * */
    @ExperimentalCoroutinesApi
    public fun flowPagingObjectsOrdered(context: CoroutineContext = Dispatchers.Default): Flow =
        flow {
            [email protected] { master ->
                emitAll(master.flowStartOrdered())
                @Suppress("UNCHECKED_CAST")
                emit(master as Z)
                emitAll(master.flowEndOrdered())
            }
        }.flowOn(context)

    /**
     * Flow the Paging action.
     * */
    @FlowPreview
    @ExperimentalCoroutinesApi
    public fun flow(context: CoroutineContext = Dispatchers.Default): Flow = flow {
        emitAll(flowPagingObjects().flatMapConcat { it.asFlow() })
    }.flowOn(context)

    /**
     * Flow the paging objects.
     * */
    @ExperimentalCoroutinesApi
    public fun flowPagingObjects(context: CoroutineContext = Dispatchers.Default): Flow =
        flow {
            [email protected] { master ->
                emitAll(master.flowBackward())
                @Suppress("UNCHECKED_CAST")
                emit(master as Z)
                emitAll(master.flowForward())
            }
        }.flowOn(context)

    override val size: Int get() = items.size
    override fun contains(element: T?): Boolean = items.contains(element)
    override fun containsAll(elements: Collection): Boolean = items.containsAll(elements)
    override fun indexOf(element: T?): Int = items.indexOf(element)
    override fun isEmpty(): Boolean = items.isEmpty()
    override fun lastIndexOf(element: T?): Int = items.lastIndexOf(element)
    override fun get(index: Int): T? = items[index]

    /**
     * Returns a list containing at most first [n] elements. Note that additional requests may be performed.
     * The [limit] used in the request used to produce this [PagingObjectBase] will be respected, so choose [limit] carefully.
     */
    public open suspend fun take(n: Int): List {
        if (n < 0) throw IllegalArgumentException("n must be non-negative.")
        if (n in items.indices) return items.take(n)
        return items + (getNext()?.take(n - size) ?: listOf())
    }
}

internal fun Any.instantiateLateinitsIfPagingObjects(api: GenericSpotifyApi) = when (this) {
    is FeaturedPlaylists -> {
        this.playlists.itemClass = SimplePlaylist::class
        listOf(this.playlists)
    }
    is Show -> {
        this.episodes.itemClass = SimpleEpisode::class
        listOf(this.episodes)
    }
    is Album -> {
        this.tracks.itemClass = SimpleTrack::class
        listOf(this.tracks)
    }
    is Playlist -> {
        this.tracks.itemClass = PlaylistTrack::class
        listOf(this.tracks)
    }
    is SpotifySearchResult -> {
        this.albums?.itemClass = SimpleAlbum::class
        this.artists?.itemClass = Artist::class
        this.episodes?.itemClass = SimpleEpisode::class
        this.playlists?.itemClass = SimplePlaylist::class
        this.shows?.itemClass = SimpleShow::class
        this.tracks?.itemClass = Track::class
        listOfNotNull(albums, artists, episodes, playlists, shows, tracks)
    }
    else -> null
}?.let { objs ->
    objs.forEach { obj ->
        obj.api = api
        obj.getMembersThatNeedApiInstantiation().instantiateAllNeedsApiObjects(api)
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy