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

commonMain.models.SearchResult.kt Maven / Gradle / Ivy

The newest version!
package torrentsearch.models

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import torrentsearch.TorrentProvider
import torrentsearch.TorrentProviderCache

/**
 * A container for [ProviderResult]s across multiple [TorrentProvider]s.
 * [SearchResult] eagerly executes the [query] with each [TorrentProvider]
 * in [providers].
 *
 * Provider requests begin executing immediately and run until all are
 * completed or [SearchResult.cancel] is called. Alternatively when
 * collecting results with [torrents], [providerResults], or [errors]
 * `cancelOnComplete` can be set to true which will call [cancel] after
 * the downstream flow is completed.
 */
public class SearchResult internal constructor(
    private val parentScope: CoroutineScope,
    private val providers: List,
    private val providerCache: TorrentProviderCache?,
    private val query: TorrentQuery,
    private val previousResults: List? = emptyList(),
) {
    private val scope = CoroutineScope(parentScope.coroutineContext + SupervisorJob())
    private val resultsFlow = providers
        .map(::createProviderQueryFlow)
        .merge()
        .flowOn(Dispatchers.Default)
        .shareIn(parentScope, SharingStarted.Eagerly, providers.size)

    /**
     * A flow of all [TorrentDescription]s from each [TorrentProvider]
     * selected to handle the [TorrentQuery].
     *
     * @param cancelOnComplete When true, cancel pending provider task
     * when the returned flow is completed.
     */
    public fun torrents(cancelOnComplete: Boolean = false): Flow {
        if (providers.isEmpty() && previousResults.isNullOrEmpty()) {
            return emptyFlow()
        }
        return resultsFlow.take(providers.size)
            .filterIsInstance()
            .map { result -> result.torrents }
            .run {
                if (previousResults == null) {
                    this
                } else {
                    onStart {
                        previousResults
                            .filterIsInstance()
                            .forEach { result -> emit(result.torrents) }
                    }
                }
            }
            .flatMapMerge { it.asFlow() }
            .onCompletion { if (cancelOnComplete) scope.cancel() }
    }

    /**
     * A flow of raw [ProviderResult]s from each [TorrentProvider]
     * selected to handle the [TorrentQuery]
     *
     * @param cancelOnComplete When true, cancel pending provider task
     * when the returned flow is completed.
     */
    public fun providerResults(cancelOnComplete: Boolean = false): Flow {
        if (providers.isEmpty() && previousResults.isNullOrEmpty()) {
            return emptyFlow()
        }
        return resultsFlow.take(providers.size).onStart {
            previousResults?.forEach { result -> emit(result) }
        }.onCompletion { if (cancelOnComplete) scope.cancel() }
    }

    /**
     * A flow of [ProviderResult.Error]s for any failed requests made
     * to any of the selected [TorrentProvider]s.
     *
     * @param cancelOnComplete When true, cancel pending provider task
     * when the returned flow is completed.
     */
    public fun errors(cancelOnComplete: Boolean = false): Flow {
        if (providers.isEmpty() && previousResults.isNullOrEmpty()) {
            return emptyFlow()
        }
        return providerResults(cancelOnComplete).filterIsInstance()
    }

    /**
     * A list of the completed [ProviderResult]s at the current moment,
     * size may be less than [providerCount].
     */
    public fun currentProviderResults(): List {
        return previousResults.orEmpty() + resultsFlow.replayCache.toList()
    }

    /**
     * True when all providers have produced a [ProviderResult].
     */
    public fun isCompleted(): Boolean {
        return resultsFlow.replayCache.size == providers.size
    }

    /**
     * True after [cancel] is called, no further requests will be completed.
     */
    public fun isCancelled(): Boolean {
        return scope.isActive
    }

    /**
     * The number of [TorrentProvider]s that were selected to handle
     * the [TorrentQuery].
     */
    public fun providerCount(): Int {
        return providers.size
    }

    /**
     * The number of [ProviderResult]s that may be contained in this
     * [SearchResult].
     */
    public fun providerResultCount(): Int {
        return providers.size + previousResults.orEmpty().size
    }

    /**
     * Returns true when one or more providers has results that can be
     * retrieved with additional requests.
     */
    public suspend fun hasNextResult(): Boolean {
        return resultsFlow.take(providers.size).toList()
            .filterIsInstance()
            .any(ProviderResult.Success::hasMoreResults)
    }

    private fun hasNextResultSync(): Boolean? {
        return if (isCompleted()) {
            resultsFlow.replayCache
                .filterIsInstance()
                .any(ProviderResult.Success::hasMoreResults)
        } else {
            null
        }
    }

    /**
     * Returns a new [SearchResult] that contains all torrents from the
     * current instance and will produce [ProviderResult]s for any providers
     * that have additional result pages.
     *
     * @return null if [hasNextResult] is false or the [SearchResult] container is cancelled.
     */
    public suspend fun nextResult(): SearchResult? {
        if (isCompleted()) return null
        val nextProviders = resultsFlow.take(providers.size).toList()
            .filterIsInstance()
            .filter(ProviderResult.Success::hasMoreResults)
            .map(ProviderResult::providerName)

        if (nextProviders.isEmpty()) {
            return null
        }

        return SearchResult(
            parentScope = parentScope,
            query = query.copy(page = query.page + 1),
            providers = providers.filter { nextProviders.contains(it.name) },
            providerCache = providerCache,
            previousResults = previousResults.orEmpty() + resultsFlow.replayCache,
        )
    }

    /**
     * Cancel pending provider requests.
     */
    public fun cancel() {
        scope.cancel()
    }

    override fun toString(): String {
        return "SearchResult(" +
            "isCompleted=${isCompleted()}, " +
            "isCancelled=${isCancelled()}, " +
            "hasNextResult=${hasNextResultSync() ?: "(pending)"}, " +
            "providers=${providers.joinToString { it.name }}, " +
            "query=$query, " +
            "completed=${resultsFlow.replayCache.size})"
    }

    private fun createProviderQueryFlow(provider: TorrentProvider): Flow = flow {
        if (!query.skipCache) {
            providerCache?.loadResults(provider, query)?.let { cacheResult ->
                return@flow emit(ProviderResult.Success(provider.name, cacheResult, fromCache = true))
            }
        }
        val result = try {
            provider.search(query)
        } catch (e: Throwable) {
            ProviderResult.Error.UnknownError(provider.name, e.message, e)
        }
        emit(result)

        if (!query.skipCache && result is ProviderResult.Success && result.torrents.isNotEmpty()) {
            providerCache?.saveResults(provider, query, result.torrents)
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy