From 11b4998d1d9bf7cfcec3a7fa63b07fb363af145f Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 16 Jan 2024 15:56:30 +0700 Subject: [PATCH] fix #487, fix #530, fix #531 --- SoraStream/build.gradle.kts | 2 +- .../main/kotlin/com/hexated/SoraExtractor.kt | 24 +++-- .../src/main/kotlin/com/hexated/SoraStream.kt | 5 +- .../src/main/kotlin/com/hexated/SoraUtils.kt | 29 +++--- StremioX/build.gradle.kts | 14 ++- .../src/main/kotlin/com/hexated/StremioC.kt | 89 ++++++++++--------- .../src/main/kotlin/com/hexated/StremioX.kt | 43 ++++----- .../main/kotlin/com/hexated/SubsExtractors.kt | 21 ++--- StremioX/src/main/kotlin/com/hexated/Utils.kt | 51 +++++------ 9 files changed, 137 insertions(+), 141 deletions(-) diff --git a/SoraStream/build.gradle.kts b/SoraStream/build.gradle.kts index cbd92fca..12d64480 100644 --- a/SoraStream/build.gradle.kts +++ b/SoraStream/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.konan.properties.Properties // use an integer for version numbers -version = 217 +version = 218 android { defaultConfig { diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt index cde6e82d..9823c0c2 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt @@ -1275,7 +1275,7 @@ object SoraExtractor : SoraStream() { val selector = if (season == null) "p a:contains(V-Cloud)" else "h4:matches(0?$episode) + p a:contains(V-Cloud)" val server = app.get( - href ?: return@apmap, interceptor = wpredisInterceptor + href ?: return@apmap, interceptor = wpRedisInterceptor ).document.selectFirst("div.entry-content > $selector") ?.attr("href") ?: return@apmap @@ -1717,13 +1717,13 @@ object SoraExtractor : SoraStream() { "$url&apikey=whXgvN4kVyoubGwqXpw26Oy3PVryl8dm", referer = "https://watcha.movie/" ).text - val link = Regex("\"file\":\"(http.*?)\"").find(res)?.groupValues?.getOrNull(1) ?: return + val link = Regex("\"file\":\"(http.*?)\"").find(res)?.groupValues?.getOrNull(1) callback.invoke( ExtractorLink( "RStream", "RStream", - link, + link ?: return, "$rStreamAPI/", Qualities.P1080.value, INFER_TYPE @@ -2053,7 +2053,7 @@ object SoraExtractor : SoraStream() { "$dahmerMoviesAPI/tvs/${title?.replace(":", " -")}/Season $season/" } - val request = app.get(url, timeout = 120L) + val request = app.get(url, interceptor = TimeOutInterceptor()) if (!request.isSuccessful) return val paths = request.document.select("a").map { it.text() to it.attr("href") @@ -2382,12 +2382,18 @@ object SoraExtractor : SoraStream() { callback: (ExtractorLink) -> Unit, referer: String = "https://bflix.gs/" ) { + suspend fun String.isSuccess() : Boolean { + return app.get(this, referer = referer).isSuccessful + } val slug = getEpisodeSlug(season, episode) - var url = - if (season == null) "$nowTvAPI/$tmdbId.mp4" else "$nowTvAPI/tv/$tmdbId/s${season}e${slug.second}.mp4" - if (!app.get(url, referer = referer).isSuccessful) { - url = - if (season == null) "$nowTvAPI/$imdbId.mp4" else "$nowTvAPI/tv/$imdbId/s${season}e${slug.second}.mp4" + var url = if (season == null) "$nowTvAPI/$tmdbId.mp4" else "$nowTvAPI/tv/$tmdbId/s${season}e${slug.second}.mp4" + if (!url.isSuccess()) { + url = if (season == null) { + val temp = "$nowTvAPI/$imdbId.mp4" + if (temp.isSuccess()) temp else "$nowTvAPI/$tmdbId-1.mp4" + } else { + "$nowTvAPI/tv/$imdbId/s${season}e${slug.second}.mp4" + } if (!app.get(url, referer = referer).isSuccessful) return } callback.invoke( diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt index 122fdce8..9fdb1441 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt @@ -68,7 +68,7 @@ open class SoraStream : TmdbProvider() { TvType.Anime, ) - val wpredisInterceptor by lazy { CloudflareKiller() } + val wpRedisInterceptor by lazy { CloudflareKiller() } val multiInterceptor by lazy { CloudflareKiller() } /** AUTHOR : Hexated & Sora */ @@ -250,8 +250,7 @@ open class SoraStream : TmdbProvider() { val recommendations = res.recommendations?.results?.mapNotNull { media -> media.toSearchResponse() } - val trailer = - res.videos?.results?.map { "https://www.youtube.com/watch?v=${it.key}" }?.randomOrNull() + val trailer = res.videos?.results?.map { "https://www.youtube.com/watch?v=${it.key}" } return if (type == TvType.TvSeries) { val lastSeason = res.last_episode_to_air?.season_number diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt index 0b5a2b6c..e85277a1 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt @@ -23,6 +23,7 @@ import com.lagradost.nicehttp.requestCreator import kotlinx.coroutines.delay import okhttp3.FormBody import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request @@ -1285,23 +1286,17 @@ private enum class Symbol(val decimalValue: Int) { } } -suspend fun request( - url: String, - allowRedirects: Boolean = true, - timeout: Long = 60L -): Response { - val client = OkHttpClient().newBuilder() - .connectTimeout(timeout, TimeUnit.SECONDS) - .readTimeout(timeout, TimeUnit.SECONDS) - .writeTimeout(timeout, TimeUnit.SECONDS) - .followRedirects(allowRedirects) - .followSslRedirects(allowRedirects) - .build() - - val request: Request = Request.Builder() - .url(url) - .build() - return client.newCall(request).await() +class TimeOutInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val call = chain + .withConnectTimeout(60, TimeUnit.SECONDS) + .withReadTimeout(60, TimeUnit.SECONDS) + .withWriteTimeout(60, TimeUnit.SECONDS) + .request() + .newBuilder() + .build() + return chain.proceed(call) + } } // steal from https://github.com/aniyomiorg/aniyomi-extensions/blob/master/src/en/aniwave/src/eu/kanade/tachiyomi/animeextension/en/nineanime/AniwaveUtils.kt diff --git a/StremioX/build.gradle.kts b/StremioX/build.gradle.kts index c08ea2b1..6c7bb75e 100644 --- a/StremioX/build.gradle.kts +++ b/StremioX/build.gradle.kts @@ -1,6 +1,16 @@ -// use an integer for version numbers -version = 12 +import org.jetbrains.kotlin.konan.properties.Properties +// use an integer for version numbers +version = 13 + +android { + defaultConfig { + val properties = Properties() + properties.load(project.rootProject.file("local.properties").inputStream()) + + buildConfigField("String", "TMDB_API", "\"${properties.getProperty("TMDB_API")}\"") + } +} cloudstream { language = "en" diff --git a/StremioX/src/main/kotlin/com/hexated/StremioC.kt b/StremioX/src/main/kotlin/com/hexated/StremioC.kt index 4d972d31..b6137816 100644 --- a/StremioX/src/main/kotlin/com/hexated/StremioC.kt +++ b/StremioX/src/main/kotlin/com/hexated/StremioC.kt @@ -10,24 +10,23 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import java.net.URI - -private const val TRACKER_LIST_URL = - "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt" class StremioC : MainAPI() { override var mainUrl = "https://stremio.github.io/stremio-static-addon-example" override var name = "StremioC" override val supportedTypes = setOf(TvType.Others) override val hasMainPage = true - private val cinemataUrl = "https://v3-cinemeta.strem.io" - override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse? { + companion object { + private const val cinemataUrl = "https://v3-cinemeta.strem.io" + private const val TRACKER_LIST_URL = "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt" + } + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { mainUrl = mainUrl.fixSourceUrl() - val res = tryParseJson(request("${mainUrl}/manifest.json").body.string()) ?: return null + val res = app.get("${mainUrl}/manifest.json").parsedSafe() val lists = mutableListOf() - res.catalogs.apmap { catalog -> + res?.catalogs?.apmap { catalog -> catalog.toHomePageList(this).let { if (it.list.isNotEmpty()) lists.add(it) } @@ -38,11 +37,11 @@ class StremioC : MainAPI() { ) } - override suspend fun search(query: String): List? { + override suspend fun search(query: String): List { mainUrl = mainUrl.fixSourceUrl() - val res = tryParseJson(request("${mainUrl}/manifest.json").body.string()) ?: return null + val res = app.get("${mainUrl}/manifest.json").parsedSafe() val list = mutableListOf() - res.catalogs.apmap { catalog -> + res?.catalogs?.apmap { catalog -> list.addAll(catalog.search(query, this)) } return list.distinct() @@ -64,10 +63,13 @@ class StremioC : MainAPI() { callback: (ExtractorLink) -> Unit ): Boolean { val loadData = parseJson(data) - val request = request("${mainUrl}/stream/${loadData.type}/${loadData.id}.json") - if (request.code.isSuccessful()) { - val res = tryParseJson(request.body.string()) ?: return false - res.streams.forEach { stream -> + val request = app.get( + "${mainUrl}/stream/${loadData.type}/${loadData.id}.json", + interceptor = interceptor + ) + if (request.isSuccessful) { + val res = request.parsedSafe() + res?.streams?.forEach { stream -> stream.runCallback(subtitleCallback, callback) } } else { @@ -103,15 +105,14 @@ class StremioC : MainAPI() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val sites = - AcraApplication.getKey>(USER_PROVIDER_API)?.toMutableList() - ?: mutableListOf() + val sites = AcraApplication.getKey>(USER_PROVIDER_API)?.toMutableList() + ?: mutableListOf() sites.filter { it.parentJavaClass == "StremioX" }.apmap { site -> - val request = request("${site.url.fixSourceUrl()}/stream/${type}/${id}.json").body.string() - val res = - tryParseJson(request) - ?: return@apmap - res.streams.forEach { stream -> + val res = app.get( + "${site.url.fixSourceUrl()}/stream/${type}/${id}.json", + interceptor = interceptor + ).parsedSafe() + res?.streams?.forEach { stream -> stream.runCallback(subtitleCallback, callback) } } @@ -151,11 +152,11 @@ class StremioC : MainAPI() { suspend fun search(query: String, provider: StremioC): List { val entries = mutableListOf() types.forEach { type -> - val json = request("${provider.mainUrl}/catalog/${type}/${id}/search=${query}.json").body.string() - val res = - tryParseJson(json) - ?: return@forEach - res.metas?.forEach { entry -> + val res = app.get( + "${provider.mainUrl}/catalog/${type}/${id}/search=${query}.json", + interceptor = interceptor + ).parsedSafe() + res?.metas?.forEach { entry -> entries.add(entry.toSearchResponse(provider)) } } @@ -165,11 +166,11 @@ class StremioC : MainAPI() { suspend fun toHomePageList(provider: StremioC): HomePageList { val entries = mutableListOf() types.forEach { type -> - val json = request("${provider.mainUrl}/catalog/${type}/${id}.json").body.string() - val res = - tryParseJson(json) - ?: return@forEach - res.metas?.forEach { entry -> + val res = app.get( + "${provider.mainUrl}/catalog/${type}/${id}.json", + interceptor = interceptor + ).parsedSafe() + res?.metas?.forEach { entry -> entries.add(entry.toSearchResponse(provider)) } } @@ -186,6 +187,7 @@ class StremioC : MainAPI() { val source: String?, val type: String? ) + private data class CatalogEntry( @JsonProperty("name") val name: String, @JsonProperty("id") val id: String, @@ -226,7 +228,7 @@ class StremioC : MainAPI() { year = yearNum?.toIntOrNull() tags = genre ?: genres addActors(cast) - addTrailer(trailersSources?.map { "https://www.youtube.com/watch?v=${it.source}" }?.randomOrNull()) + addTrailer(trailersSources?.map { "https://www.youtube.com/watch?v=${it.source}" }) addImdbId(imdbId) } } else { @@ -245,7 +247,8 @@ class StremioC : MainAPI() { year = yearNum?.toIntOrNull() tags = genre ?: genres addActors(cast) - addTrailer(trailersSources?.map { "https://www.youtube.com/watch?v=${it.source}" }?.randomOrNull()) + addTrailer(trailersSources?.map { "https://www.youtube.com/watch?v=${it.source}" } + ?.randomOrNull()) addImdbId(imdbId) } } @@ -285,13 +288,14 @@ class StremioC : MainAPI() { ) private data class ProxyHeaders( - val request: Map?, + val request: Map?, ) private data class BehaviorHints( val proxyHeaders: ProxyHeaders?, - val headers: Map?, + val headers: Map?, ) + private data class Stream( val name: String?, val title: String?, @@ -312,12 +316,13 @@ class StremioC : MainAPI() { callback.invoke( ExtractorLink( name ?: "", - fixRDSourceName(name, title), + fixSourceName(name, title), url, "", - getQualityFromName(description), - headers = behaviorHints?.proxyHeaders?.request ?: behaviorHints?.headers ?: mapOf(), - isM3u8 = URI(url).path.endsWith(".m3u8") + getQuality(listOf(description,title,name)), + headers = behaviorHints?.proxyHeaders?.request ?: behaviorHints?.headers + ?: mapOf(), + type = INFER_TYPE ) ) subtitles.map { sub -> diff --git a/StremioX/src/main/kotlin/com/hexated/StremioX.kt b/StremioX/src/main/kotlin/com/hexated/StremioX.kt index 314e124f..995479fa 100644 --- a/StremioX/src/main/kotlin/com/hexated/StremioX.kt +++ b/StremioX/src/main/kotlin/com/hexated/StremioX.kt @@ -10,26 +10,21 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson -import java.net.URI import java.util.ArrayList import kotlin.math.roundToInt import com.lagradost.cloudstream3.metaproviders.TmdbProvider -open class StremioX : TmdbProvider() { +class StremioX : TmdbProvider() { override var mainUrl = "https://torrentio.strem.fun" override var name = "StremioX" override val hasMainPage = true override val hasQuickSearch = true - override val supportedTypes = setOf( - TvType.Others, - ) + override val supportedTypes = setOf(TvType.Others) companion object { - const val TRACKER_LIST_URL = - "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt" + const val TRACKER_LIST_URL = "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt" private const val tmdbAPI = "https://api.themoviedb.org/3" - private val apiKey = - base64DecodeAPI("ZTM=NTg=MjM=MjM=ODc=MzI=OGQ=MmE=Nzk=Nzk=ZjI=NTA=NDY=NDA=MzA=YjA=") // PLEASE DON'T STEAL + private const val apiKey = BuildConfig.TMDB_API fun getType(t: String?): TvType { return when (t) { @@ -44,11 +39,6 @@ open class StremioX : TmdbProvider() { else -> ShowStatus.Completed } } - - private fun base64DecodeAPI(api: String): String { - return api.chunked(4).map { base64Decode(it) }.reversed().joinToString("") - } - } override val mainPage = mainPageOf( @@ -159,7 +149,7 @@ open class StremioX : TmdbProvider() { eps.seasonNumber, eps.episodeNumber ).toJson(), - name = eps.name + if (isUpcoming(eps.airDate)) " - [UPCOMING]" else "", + name = eps.name + if (isUpcoming(eps.airDate)) " • [UPCOMING]" else "", season = eps.seasonNumber, episode = eps.episodeNumber, posterUrl = getImageUrl(eps.stillPath), @@ -177,7 +167,7 @@ open class StremioX : TmdbProvider() { this.backgroundPosterUrl = bgPoster this.year = year this.plot = res.overview - this.tags = if (isAnime) keywords else genres + this.tags = keywords.takeIf { !it.isNullOrEmpty() } ?: genres this.rating = rating this.showStatus = getStatus(res.status) this.recommendations = recommendations @@ -200,7 +190,7 @@ open class StremioX : TmdbProvider() { this.year = year this.plot = res.overview this.duration = res.runtime - this.tags = if (isAnime) keywords else genres + this.tags = keywords.takeIf { !it.isNullOrEmpty() } ?: genres this.rating = rating this.recommendations = recommendations this.actors = actors @@ -243,13 +233,13 @@ open class StremioX : TmdbProvider() { callback: (ExtractorLink) -> Unit ) { val fixMainUrl = mainUrl.fixSourceUrl() - val url = if(season == null) { + val url = if (season == null) { "$fixMainUrl/stream/movie/$imdbId.json" } else { "$fixMainUrl/stream/series/$imdbId:$season:$episode.json" } - val res = AppUtils.tryParseJson(request(url).body.string()) ?: return - res.streams.forEach { stream -> + val res = app.get(url, interceptor = interceptor).parsedSafe() + res?.streams?.forEach { stream -> stream.runCallback(subtitleCallback, callback) } } @@ -262,12 +252,12 @@ open class StremioX : TmdbProvider() { ) private data class ProxyHeaders( - val request: Map?, + val request: Map?, ) private data class BehaviorHints( val proxyHeaders: ProxyHeaders?, - val headers: Map?, + val headers: Map?, ) private data class Stream( @@ -290,12 +280,13 @@ open class StremioX : TmdbProvider() { callback.invoke( ExtractorLink( name ?: "", - fixRDSourceName(name, title), + fixSourceName(name, title), url, "", - getQualityFromName(description), - headers = behaviorHints?.proxyHeaders?.request ?: behaviorHints?.headers ?: mapOf(), - isM3u8 = URI(url).path.endsWith(".m3u8") + getQuality(listOf(description,title,name)), + headers = behaviorHints?.proxyHeaders?.request ?: behaviorHints?.headers + ?: mapOf(), + type = INFER_TYPE ) ) subtitles.map { sub -> diff --git a/StremioX/src/main/kotlin/com/hexated/SubsExtractors.kt b/StremioX/src/main/kotlin/com/hexated/SubsExtractors.kt index 367114bd..57e8b9ae 100644 --- a/StremioX/src/main/kotlin/com/hexated/SubsExtractors.kt +++ b/StremioX/src/main/kotlin/com/hexated/SubsExtractors.kt @@ -3,10 +3,9 @@ package com.hexated import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.base64Encode import com.lagradost.cloudstream3.utils.SubtitleHelper -const val openSubAPI = "https://opensubtitles.strem.io/stremio/v1" +const val openSubAPI = "https://opensubtitles-v3.strem.io" const val watchSomuchAPI = "https://watchsomuch.tv" object SubsExtractors { @@ -16,22 +15,20 @@ object SubsExtractors { episode: Int? = null, subtitleCallback: (SubtitleFile) -> Unit, ) { - val id = if(season == null) { - imdbId + val slug = if(season == null) { + "movie/$imdbId" } else { - "$imdbId $season $episode" + "series/$imdbId:$season:$episode" } - val data = base64Encode("""{"id":1,"jsonrpc":"2.0","method":"subtitles.find","params":[null,{"query":{"itemHash":"$id"}}]}""".toByteArray()) - app.get("${openSubAPI}/q.json?b=$data").parsedSafe()?.result?.all?.map { sub -> + app.get("${openSubAPI}/subtitles/$slug.json").parsedSafe()?.subtitles?.map { sub -> subtitleCallback.invoke( SubtitleFile( SubtitleHelper.fromThreeLettersToLanguage(sub.lang ?: "") ?: sub.lang - ?: "", + ?: return@map, sub.url ?: return@map ) ) } - } suspend fun invokeWatchsomuch( @@ -81,12 +78,8 @@ object SubsExtractors { @JsonProperty("lang") val lang: String? = null, ) - data class OsAll( - @JsonProperty("all") val all: ArrayList? = arrayListOf(), - ) - data class OsResult( - @JsonProperty("result") val result: OsAll? = null, + @JsonProperty("subtitles") val subtitles: ArrayList? = arrayListOf(), ) data class WatchsomuchTorrents( diff --git a/StremioX/src/main/kotlin/com/hexated/Utils.kt b/StremioX/src/main/kotlin/com/hexated/Utils.kt index 30b676de..6640cbc4 100644 --- a/StremioX/src/main/kotlin/com/hexated/Utils.kt +++ b/StremioX/src/main/kotlin/com/hexated/Utils.kt @@ -1,53 +1,50 @@ package com.hexated -import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.nicehttp.Requests.Companion.await -import okhttp3.OkHttpClient -import okhttp3.Request +import com.lagradost.cloudstream3.utils.getQualityFromName +import okhttp3.Interceptor import okhttp3.Response import java.text.SimpleDateFormat import java.util.Locale import java.util.concurrent.TimeUnit -const val defaultTimeOut = 30L -suspend fun request( - url: String, - allowRedirects: Boolean = true, - timeout: Long = defaultTimeOut -): Response { - val client = OkHttpClient().newBuilder() - .connectTimeout(timeout, TimeUnit.SECONDS) - .readTimeout(timeout, TimeUnit.SECONDS) - .writeTimeout(timeout, TimeUnit.SECONDS) - .followRedirects(allowRedirects) - .followSslRedirects(allowRedirects) - .build() +val interceptor = TimeOutInterceptor() - val request: Request = Request.Builder() - .url(url) - .build() - return client.newCall(request).await() -} - -fun Int.isSuccessful() : Boolean { - return this in 200..299 +class TimeOutInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val call = chain + .withConnectTimeout(60, TimeUnit.SECONDS) + .withReadTimeout(60, TimeUnit.SECONDS) + .withWriteTimeout(60, TimeUnit.SECONDS) + .request() + .newBuilder() + .build() + return chain.proceed(call) + } } fun String.fixSourceUrl(): String { return this.replace("/manifest.json", "").replace("stremio://", "https://") } -fun fixRDSourceName(name: String?, title: String?): String { +fun fixSourceName(name: String?, title: String?): String { return when { name?.contains("[RD+]", true) == true -> "[RD+] $title" - name?.contains("[RD download]", true) == true -> "[RD] $title" + name?.contains("[RD download]", true) == true -> "[RD download] $title" !name.isNullOrEmpty() && !title.isNullOrEmpty() -> "$name $title" else -> title ?: name ?: "" } } +fun getQuality(qualities: List): Int { + fun String.getQuality(): String? { + return Regex("(\\d{3,4}[pP])").find(this)?.groupValues?.getOrNull(1) + } + val quality = qualities.firstNotNullOfOrNull { it?.getQuality() } + return getQualityFromName(quality) +} + fun getEpisodeSlug( season: Int? = null, episode: Int? = null,