From c66b0eadd62f5a781ae1c07c6780eb4028cbfd5b Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:29:29 +0000 Subject: [PATCH 01/19] Fix failed cast in ContextHelper --- app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt index a9cd9c01e..466ef5a00 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt @@ -113,7 +113,7 @@ class CloudStreamApp : Application(), SingletonImageLoader.Factory { get() = _context?.get() private set(value) { _context = WeakReference(value) - setContext(WeakReference(value)) + setContext(value) } fun getKeyClass(path: String, valueType: Class): T? { From 36502213342b98ab297f1ab7c68736a4b1b5bb86 Mon Sep 17 00:00:00 2001 From: Kraptor123 <89366989+Kraptor123@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:05:15 +0300 Subject: [PATCH 02/19] [skip ci] ui: Hide pinned NSFW-only extensions from non-NSFW categories (#2923) --- .../com/lagradost/cloudstream3/ui/home/HomeFragment.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index ab0715c6d..96c5f9f56 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -451,7 +451,14 @@ class HomeFragment : BaseFragment( arrayAdapter.clear() val sortedApis = validAPIs .filter { - it.hasMainPage && (pinnedphashset.contains(it.name) || it.supportedTypes.any( + val isPinned = pinnedphashset.contains(it.name) + + // Hide pinned NSFW when NSFW not selected. NSFW is distracting when not chosen. + if (isPinned && !preSelectedTypes.contains(TvType.NSFW)) { + if (it.supportedTypes.all { type -> type == TvType.NSFW }) return@filter false + } + + it.hasMainPage && (isPinned || it.supportedTypes.any( preSelectedTypes::contains )) } From c09c06820dc3b47d237893c22202f5687434ca0c Mon Sep 17 00:00:00 2001 From: Leandre Chamberland-Dozois Date: Tue, 14 Jul 2026 11:19:39 -0400 Subject: [PATCH 03/19] fix(download): remove deleted/cancelled items from the Downloads list in real time (#1227) (#2598) --- .../ui/download/DownloadViewModel.kt | 27 ++++++++++++++++++- .../ui/download/button/BaseFetchButton.kt | 12 +++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 0d35d5670..19c006b28 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.api.Log +import com.lagradost.cloudstream3.CloudStreamApp import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.isEpisodeBased import com.lagradost.cloudstream3.mvvm.Resource @@ -36,6 +37,7 @@ import com.lagradost.cloudstream3.utils.ResourceLiveData import com.lagradost.cloudstream3.utils.downloader.DownloadObjects import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.deleteFilesAndUpdateSettings +import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.downloadDeleteEvent import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getDownloadFileInfo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -67,6 +69,17 @@ class DownloadViewModel : ViewModel() { private val _selectedItemIds = ConsistentLiveData?>(null) val selectedItemIds: LiveData?> = _selectedItemIds + init { + // Keep the Downloads list in sync when a download is deleted/cancelled from + // anywhere in the app (result page button, queue, notification, etc.). See + // onDownloadDeleted for the rationale (issue #1227). + downloadDeleteEvent += ::onDownloadDeleted + } + + override fun onCleared() { + downloadDeleteEvent -= ::onDownloadDeleted + super.onCleared() + } fun cancelSelection() { updateSelectedItems { null } @@ -389,6 +402,18 @@ class DownloadViewModel : ViewModel() { postChildren(_childCards.success?.filter { it.data.id !in idsToRemove }) } + /** + * Refreshes the Downloads screen in real time when a download is deleted/cancelled. + */ + private fun onDownloadDeleted(id: Int) { + // Keep multi-select state consistent: forget the removed id if it was selected. + updateSelectedItems { it?.minus(id) } + + val context = CloudStreamApp.context ?: return + updateHeaderList(context) + postChildren(_childCards.success?.filterNot { it.data.id == id }) + } + private fun updateStorageStats(visual: List) { try { val stat = StatFs(Environment.getExternalStorageDirectory().path) @@ -584,4 +609,4 @@ class DownloadViewModel : ViewModel() { val names: List, val parentName: String? ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index 382a770cd..450dc9a61 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -164,9 +164,11 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } } - /*fun downloadDeleteEvent(data: Int) { - - }*/ + fun downloadDeleteEvent(data: Int) { + if (data == persistentId) { + resetView() + } + } /*fun downloadEvent(data: Pair) { val (id, action) = data @@ -185,7 +187,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : override fun onAttachedToWindow() { VideoDownloadManager.downloadStatusEvent += ::downloadStatusEvent - // VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent + VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent // VideoDownloadManager.downloadEvent += ::downloadEvent VideoDownloadManager.downloadProgressEvent += ::downloadProgressEvent @@ -200,7 +202,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : override fun onDetachedFromWindow() { VideoDownloadManager.downloadStatusEvent -= ::downloadStatusEvent - // VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent + VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent // VideoDownloadManager.downloadEvent -= ::downloadEvent VideoDownloadManager.downloadProgressEvent -= ::downloadProgressEvent From fb8bcb4ac70e4277ce29c540c6a2ea386de5268b Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:23:52 +0000 Subject: [PATCH 04/19] more robust date parsing (#3068) --- .../com/lagradost/cloudstream3/MainAPI.kt | 12 +++++++++-- .../lagradost/cloudstream3/EpisodeDateTest.kt | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index 3e8774db2..ca024b463 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -2576,13 +2576,21 @@ constructor( @OptIn(FormatStringsInDatetimeFormats::class) fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") { - if (date == null) return + if (date.isNullOrBlank()) return this.date = runCatching { // First try standard ISO 8601 (e.g. "2026-01-01T12:30:00.000Z", "2026-05-17T14:35+02:00") runCatching { Instant.parse(date).toEpochMilliseconds() } .getOrElse { val fmt = DateTimeComponents.Format { byUnicodePattern(format) } - val components = DateTimeComponents.parse(date, fmt) + + // Try parsing the full date first then only parse the beginning of the string + // May lose time, but better than failing. + val components = runCatching { + DateTimeComponents.parse(date, fmt) + }.recoverCatching { + DateTimeComponents.parse(date.trimStart().take(format.length), fmt) + }.getOrThrow() + /** * Try multiple conversions in order of precision for non-ISO-8601 formats, * since the date string may or may not include time and/or timezone offset: diff --git a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt index 4dc56978e..422b4cd93 100644 --- a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt +++ b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt @@ -31,6 +31,26 @@ class EpisodeDateTest { assertEquals(expected, ep.date) } + @Test + fun addDateDefaultFormatParsesExtraTime() { + val ep = episode() + ep.addDate("2026-05-17 12:30:45") + val expected = LocalDate(2026, 5, 17) + .atStartOfDayIn(TimeZone.currentSystemDefault()) + .toEpochMilliseconds() + assertEquals(expected, ep.date) + } + + @Test + fun addDateDefaultFormatParsesExtraJunk() { + val ep = episode() + ep.addDate(" 2026-05-17 random data") + val expected = LocalDate(2026, 5, 17) + .atStartOfDayIn(TimeZone.currentSystemDefault()) + .toEpochMilliseconds() + assertEquals(expected, ep.date) + } + @Test fun addDateNullDoesNotSetDate() { val ep = episode() From fef17079a8263e9afd20124ddbd13334f5c4835c Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:05:44 -0700 Subject: [PATCH 05/19] [skip ci] Coroutines KMP API (#2836) --- library/api/jvm/library.api | 8 ++ .../cloudstream3/mvvm/ArchComponentExt.kt | 87 ++++++------------- .../cloudstream3/utils/Coroutines.kt | 7 +- .../cloudstream3/mvvm/ArchComponentExtTest.kt | 31 +++++++ .../mvvm/ArchComponentExt.jvmCommon.kt | 37 ++++++++ .../utils/Coroutines.jvmCommon.kt | 4 + 6 files changed, 112 insertions(+), 62 deletions(-) create mode 100644 library/src/commonTest/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExtTest.kt create mode 100644 library/src/jvmCommonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.jvmCommon.kt diff --git a/library/api/jvm/library.api b/library/api/jvm/library.api index 1b39b3906..0b0996f94 100644 --- a/library/api/jvm/library.api +++ b/library/api/jvm/library.api @@ -7235,6 +7235,10 @@ public final class com/lagradost/cloudstream3/mvvm/ArchComponentExtKt { public static final fun throwAbleToResource (Ljava/lang/Throwable;)Lcom/lagradost/cloudstream3/mvvm/Resource; } +public final class com/lagradost/cloudstream3/mvvm/ArchComponentExt_jvmCommonKt { + public static final fun platformThrowAbleToResource (Ljava/lang/Throwable;)Lcom/lagradost/cloudstream3/mvvm/Resource; +} + public final class com/lagradost/cloudstream3/mvvm/DebugException : java/lang/Exception { public fun (Ljava/lang/String;)V } @@ -7438,6 +7442,10 @@ public final class com/lagradost/cloudstream3/utils/Coroutines { public final fun threadSafeListOf ([Ljava/lang/Object;)Ljava/util/List; } +public final class com/lagradost/cloudstream3/utils/Coroutines_jvmCommonKt { + public static final fun getWorkerDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; +} + public final class com/lagradost/cloudstream3/utils/Coroutines_jvmKt { public static final fun runOnMainThreadNative (Lkotlin/jvm/functions/Function0;)V } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index 4cd8eefa6..73de880db 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -4,15 +4,11 @@ import androidx.annotation.AnyThread import com.lagradost.api.Log import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.utils.AppDebug +import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.WorkerThread import kotlinx.coroutines.* -import java.io.InterruptedIOException -import java.net.SocketTimeoutException -import java.net.UnknownHostException -import javax.net.ssl.SSLHandshakeException import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext -import kotlin.reflect.full.NoSuchPropertyException const val DEBUG_EXCEPTION = "THIS IS A DEBUG EXCEPTION!" const val DEBUG_PRINT = "DEBUG PRINT" @@ -61,7 +57,7 @@ sealed class Resource { companion object { fun fromResult(result: Result) : Resource { val value = result.getOrNull() - return if(value != null) { + return if (value != null) { Success(value) } else { throwAbleToResource(result.exceptionOrNull() ?: Exception("this should not be possible")) @@ -72,7 +68,6 @@ sealed class Resource { fun logError(throwable: Throwable) { Log.d("ApiError", "-------------------------------------------------------------------") - Log.d("ApiError", "safeApiCall: " + throwable.localizedMessage) Log.d("ApiError", "safeApiCall: " + throwable.message) throwable.printStackTrace() Log.d("ApiError", "-------------------------------------------------------------------") @@ -129,16 +124,21 @@ suspend fun suspendSafeApiCall(apiCall: suspend () -> T): T? { } fun Throwable.getAllMessages(): String { - return (this.localizedMessage ?: "") + (this.cause?.getAllMessages()?.let { "\n$it" } ?: "") + return (this.message ?: "") + (this.cause?.getAllMessages()?.let { "\n$it" } ?: "") } fun Throwable.getStackTracePretty(showMessage: Boolean = true): String { - val prefix = if (showMessage) this.localizedMessage?.let { "\n$it" } ?: "" else "" - return prefix + this.stackTrace.joinToString( - separator = "\n" - ) { - "${it.fileName} ${it.lineNumber}" - } + val prefix = if (showMessage) this.message?.let { "\n$it" } ?: "" else "" + return prefix + this.stackTraceToString() + .lines() + .mapNotNull { line -> + val trimmed = line.trim() + if (!trimmed.startsWith("at ")) return@mapNotNull null + // pull "File.kt:42" out of "at com.pkg.Class.method(File.kt:42)" + Regex("""\(([^)]+)\)$""").find(trimmed)?.groupValues?.get(1) + ?.replace(":", " ") + } + .joinToString("\n") } fun safeFail(throwable: Throwable): Resource { @@ -162,50 +162,26 @@ fun CoroutineScope.launchSafe( return this.launch(context, start, obj) } +expect fun platformThrowAbleToResource(throwable: Throwable): Resource + fun throwAbleToResource( throwable: Throwable ): Resource { return when (throwable) { - is NoSuchMethodException, is NoSuchFieldException, is NoSuchMethodError, is NoSuchFieldError, is NoSuchPropertyException -> { - Resource.Failure( - false, - "App or extension is outdated, update the app or try pre-release.\n${throwable.message}" // todo add exact version? - ) - } - is NullPointerException -> { - for (line in throwable.stackTrace) { - if (line?.fileName?.endsWith("provider.kt", ignoreCase = true) == true) { - return Resource.Failure( - false, - "NullPointerException at ${line.fileName} ${line.lineNumber}\nSite might have updated or added Cloudflare/DDOS protection" - ) - } + val traceLine = throwable.stackTraceToString() + .lines() + .firstOrNull { it.contains("provider.kt", ignoreCase = true) } + ?.let { Regex("""\(([^)]+)\)$""").find(it)?.groupValues?.get(1) } + if (traceLine != null) { + return Resource.Failure( + false, + "NullPointerException at $traceLine\nSite might have updated or added Cloudflare/DDOS protection" + ) } safeFail(throwable) } - is SocketTimeoutException, is InterruptedIOException -> { - Resource.Failure( - true, - "Connection Timeout\nPlease try again later." - ) - } -// is HttpException -> { -// Resource.Failure( -// false, -// throwable.statusCode, -// null, -// throwable.message ?: "HttpException" -// ) -// } - is UnknownHostException -> { - Resource.Failure( - true, - "Cannot connect to server, try again later.\n${throwable.message}" - ) - } - is ErrorLoadingException -> { Resource.Failure( true, @@ -217,20 +193,13 @@ fun throwAbleToResource( Resource.Failure(false, "This operation is not implemented.") } - is SSLHandshakeException -> { - Resource.Failure( - true, - (throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS." - ) - } - is CancellationException -> { throwable.cause?.let { throwAbleToResource(it) } ?: safeFail(throwable) } - else -> safeFail(throwable) + else -> platformThrowAbleToResource(throwable) } } @@ -238,9 +207,9 @@ fun throwAbleToResource( suspend fun safeApiCall( @WorkerThread apiCall: suspend () -> T, ): Resource { - return withContext(Dispatchers.IO) { + return apiCall.ioWork { work -> try { - Resource.Success(apiCall.invoke()) + Resource.Success(work.invoke()) } catch (throwable: Throwable) { logError(throwable) throwAbleToResource(throwable) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt index 4536eb88b..4a269c525 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.* @AnyThread expect fun runOnMainThreadNative(@MainThread work: (() -> Unit)) +expect val workerDispatcher: CoroutineDispatcher internal expect annotation class WorkerThread() object Coroutines { @@ -24,7 +25,7 @@ object Coroutines { @AnyThread fun T.ioSafe(@WorkerThread work: suspend (CoroutineScope.(T) -> Unit)): Job { val value = this - return CoroutineScope(Dispatchers.IO).launchSafe { + return CoroutineScope(workerDispatcher).launchSafe { work(value) } } @@ -32,7 +33,7 @@ object Coroutines { @AnyThread suspend fun V.ioWorkSafe(@WorkerThread work: suspend (CoroutineScope.(V) -> T)): T? { val value = this - return withContext(Dispatchers.IO) { + return withContext(workerDispatcher) { try { work(value) } catch (e: Exception) { @@ -45,7 +46,7 @@ object Coroutines { @AnyThread suspend fun V.ioWork(@WorkerThread work: suspend (CoroutineScope.(V) -> T)): T { val value = this - return withContext(Dispatchers.IO) { + return withContext(workerDispatcher) { work(value) } } diff --git a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExtTest.kt b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExtTest.kt new file mode 100644 index 000000000..970fe728d --- /dev/null +++ b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExtTest.kt @@ -0,0 +1,31 @@ +package com.lagradost.cloudstream3.mvvm + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ArchComponentExtTest { + + @Test + fun getStackTracePrettyStripsFullyQualifiedClassNames() { + val throwable = try { + throw IllegalStateException("Exception") + } catch (t: Throwable) { + t + } + + val pretty = throwable.getStackTracePretty() + + // Should not contain the fully qualified package/class prefix + assertFalse( + pretty.contains("com.lagradost.cloudstream3.mvvm.ArchComponentExtTest"), + "Expected no fully qualified class names, got:\n$pretty" + ) + + // Should still reference the source file itself + assertTrue( + pretty.contains("ArchComponentExtTest.kt"), + "Expected pretty stack trace to reference a .kt file, got:\n$pretty" + ) + } +} diff --git a/library/src/jvmCommonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.jvmCommon.kt b/library/src/jvmCommonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.jvmCommon.kt new file mode 100644 index 000000000..f2ff5e3ac --- /dev/null +++ b/library/src/jvmCommonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.jvmCommon.kt @@ -0,0 +1,37 @@ +package com.lagradost.cloudstream3.mvvm + +import java.io.InterruptedIOException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import javax.net.ssl.SSLHandshakeException +import kotlin.reflect.full.NoSuchPropertyException + +actual fun platformThrowAbleToResource(throwable: Throwable): Resource { + return when (throwable) { + is NoSuchMethodException, is NoSuchFieldException, is NoSuchMethodError, is NoSuchFieldError, is NoSuchPropertyException -> { + Resource.Failure( + false, + "App or extension is outdated, update the app or try pre-release.\n${throwable.message}" // todo add exact version? + ) + } + is SocketTimeoutException, is InterruptedIOException -> { + Resource.Failure( + true, + "Connection Timeout\nPlease try again later." + ) + } + is UnknownHostException -> { + Resource.Failure( + true, + "Cannot connect to server, try again later.\n${throwable.message}" + ) + } + is SSLHandshakeException -> { + Resource.Failure( + true, + (throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS." + ) + } + else -> safeFail(throwable) + } +} diff --git a/library/src/jvmCommonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvmCommon.kt b/library/src/jvmCommonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvmCommon.kt index 2ae886f59..03a5be6c2 100644 --- a/library/src/jvmCommonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvmCommon.kt +++ b/library/src/jvmCommonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvmCommon.kt @@ -1,3 +1,7 @@ package com.lagradost.cloudstream3.utils +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +actual val workerDispatcher: CoroutineDispatcher = Dispatchers.IO internal actual typealias WorkerThread = androidx.annotation.WorkerThread From 91e533686eddbecc1d0a895f25ee5d592a6528b3 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:31:41 -0700 Subject: [PATCH 06/19] [skip ci] Rewrite TmdbProvider to remove external lib dependency (#2800) --- app/build.gradle.kts | 2 + gradle/libs.versions.toml | 2 - library/api/jvm/library.api | 694 ++++++++++++++++++ library/build.gradle.kts | 1 - .../metaproviders/TmdbProvider.kt | 661 ++++++++++------- 5 files changed, 1076 insertions(+), 284 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0feb866be..2ad8451a3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -276,6 +276,8 @@ dependencies { implementation(libs.jackson.module.kotlin) // JSON Parser implementation(libs.zipline) + // Temp/deprecated; will be removed once extensions have time to migrate from using it + implementation("com.google.code.gson:gson:2.11.0") // Deprecated; will be removed once extensions have time to migrate from using it implementation("me.xdrop:fuzzywuzzy:1.4.0") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fdfaee7b2..898529d9a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,7 +49,6 @@ qrcodeKotlin = "4.5.0" rhino = { strictly = "1.8.1" } # Requires minSdk 26 or later beginning at version 1.9.0 safefile = "0.0.8" shimmer = "0.5.0" -tmdbJava = "2.13.0" torrentserver = "7861970" tvprovider = "1.1.0" video = "1.0.0" @@ -129,7 +128,6 @@ qrcode-kotlin = { module = "io.github.g0dkar:qrcode-kotlin", version.ref = "qrco rhino = { module = "org.mozilla:rhino", version.ref = "rhino" } safefile = { module = "com.github.LagradOst:SafeFile", version.ref = "safefile" } shimmer = { module = "com.facebook.shimmer:shimmer", version.ref = "shimmer" } -tmdb-java = { module = "com.uwetrottmann.tmdb2:tmdb-java", version.ref = "tmdbJava" } torrentserver = { module = "com.github.recloudstream:torrentserver", version.ref = "torrentserver" } tvprovider = { module = "androidx.tvprovider:tvprovider", version.ref = "tvprovider" } video = { module = "com.google.android.mediahome:video", version.ref = "video" } diff --git a/library/api/jvm/library.api b/library/api/jvm/library.api index 0b0996f94..0d9b8f76e 100644 --- a/library/api/jvm/library.api +++ b/library/api/jvm/library.api @@ -6613,6 +6613,7 @@ public final class com/lagradost/cloudstream3/metaproviders/SyncRedirector { } public final class com/lagradost/cloudstream3/metaproviders/TmdbLink { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbLink$Companion; public fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;)V public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; @@ -6632,6 +6633,21 @@ public final class com/lagradost/cloudstream3/metaproviders/TmdbLink { public fun toString ()Ljava/lang/String; } +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbLink$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbLink$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbLink; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbLink;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbLink$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public class com/lagradost/cloudstream3/metaproviders/TmdbProvider : com/lagradost/cloudstream3/MainAPI { public fun ()V public fun fetchContentRating (Ljava/lang/Integer;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -6650,6 +6666,684 @@ public class com/lagradost/cloudstream3/metaproviders/TmdbProvider : com/lagrado public fun search (Ljava/lang/String;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCastMember { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCastMember$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCastMember; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCastMember;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCastMember; + public fun equals (Ljava/lang/Object;)Z + public final fun getCharacter ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getProfilePath ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCastMember$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCastMember$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCastMember; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCastMember;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCastMember$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRating { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRating$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRating; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRating;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRating; + public fun equals (Ljava/lang/Object;)Z + public final fun getCountry ()Ljava/lang/String; + public final fun getRating ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRating$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRating$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRating; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRating;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRating$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings$Companion; + public fun ()V + public fun (Ljava/util/List;)V + public synthetic fun (Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings;Ljava/util/List;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings; + public fun equals (Ljava/lang/Object;)Z + public final fun getResults ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits$Companion; + public fun ()V + public fun (Ljava/util/List;)V + public synthetic fun (Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits;Ljava/util/List;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits; + public fun equals (Ljava/lang/Object;)Z + public final fun getCast ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbEpisode { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbEpisode$Companion; + public fun ()V + public fun (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;)V + public synthetic fun (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Integer; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Ljava/lang/Integer; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Ljava/lang/Double; + public final fun component9 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds; + public final fun copy (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbEpisode; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbEpisode;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbEpisode; + public fun equals (Ljava/lang/Object;)Z + public final fun getAirDate ()Ljava/lang/String; + public final fun getEpisodeNumber ()Ljava/lang/Integer; + public final fun getExternalIds ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds; + public final fun getId ()Ljava/lang/Integer; + public final fun getName ()Ljava/lang/String; + public final fun getOverview ()Ljava/lang/String; + public final fun getSeasonNumber ()Ljava/lang/Integer; + public final fun getStillPath ()Ljava/lang/String; + public final fun getVoteAverage ()Ljava/lang/Double; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbEpisode$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbEpisode$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbEpisode; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbEpisode;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbEpisode$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbGenre { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbGenre$Companion; + public fun ()V + public fun (Ljava/lang/Integer;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/Integer;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Integer; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/Integer;Ljava/lang/String;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbGenre; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbGenre;Ljava/lang/Integer;Ljava/lang/String;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbGenre; + public fun equals (Ljava/lang/Object;)Z + public final fun getId ()Ljava/lang/Integer; + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbGenre$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbGenre$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbGenre; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbGenre;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbGenre$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/Integer;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/String;Ljava/lang/Integer;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds; + public fun equals (Ljava/lang/Object;)Z + public final fun getImdbId ()Ljava/lang/String; + public final fun getTvdbId ()Ljava/lang/Integer; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMovieDetail { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMovieDetail$Companion; + public fun ()V + public fun (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates;)V + public synthetic fun (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Integer; + public final fun component10 ()Ljava/lang/String; + public final fun component11 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds; + public final fun component12 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos; + public final fun component13 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits; + public final fun component14 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult; + public final fun component15 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult; + public final fun component16 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/Double; + public final fun component8 ()Ljava/util/List; + public final fun component9 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMovieDetail; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMovieDetail;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMovieDetail; + public fun equals (Ljava/lang/Object;)Z + public final fun getCredits ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits; + public final fun getDisplayTitle ()Ljava/lang/String; + public final fun getExternalIds ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds; + public final fun getGenres ()Ljava/util/List; + public final fun getId ()Ljava/lang/Integer; + public final fun getImdbId ()Ljava/lang/String; + public final fun getOriginalTitle ()Ljava/lang/String; + public final fun getOverview ()Ljava/lang/String; + public final fun getPosterPath ()Ljava/lang/String; + public final fun getRecommendations ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult; + public final fun getReleaseDate ()Ljava/lang/String; + public final fun getReleaseDates ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates; + public final fun getRuntime ()Ljava/lang/Integer; + public final fun getSimilar ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult; + public final fun getTitle ()Ljava/lang/String; + public final fun getVideos ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos; + public final fun getVoteAverage ()Ljava/lang/Double; + public final fun getYear ()Ljava/lang/Integer; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMovieDetail$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMovieDetail$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMovieDetail; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMovieDetail;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMovieDetail$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMultiResult { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMultiResult$Companion; + public fun ()V + public fun (Ljava/util/List;)V + public synthetic fun (Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMultiResult; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMultiResult;Ljava/util/List;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMultiResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getResults ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMultiResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMultiResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMultiResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMultiResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbMultiResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult$Companion; + public fun ()V + public fun (Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;)V + public synthetic fun (Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/Integer; + public final fun component3 ()Ljava/lang/Integer; + public final fun copy (Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getResults ()Ljava/util/List; + public final fun getTotalPages ()Ljava/lang/Integer; + public final fun getTotalResults ()Ljava/lang/Integer; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateEntry { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateEntry$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/Integer;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/String;Ljava/lang/Integer;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateEntry; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateEntry;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateEntry; + public fun equals (Ljava/lang/Object;)Z + public final fun getCertification ()Ljava/lang/String; + public final fun getType ()Ljava/lang/Integer; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateEntry$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateEntry$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateEntry; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateEntry;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateEntry$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateResult { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateResult$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/util/List;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateResult; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateResult;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getCountry ()Ljava/lang/String; + public final fun getReleaseDates ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDateResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates$Companion; + public fun ()V + public fun (Ljava/util/List;)V + public synthetic fun (Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates;Ljava/util/List;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates; + public fun equals (Ljava/lang/Object;)Z + public final fun getResults ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbReleaseDates$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSearchResult { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSearchResult$Companion; + public fun ()V + public fun (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Integer; + public final fun component10 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/Double; + public final fun component8 ()Ljava/lang/String; + public final fun component9 ()Ljava/lang/String; + public final fun copy (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSearchResult; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSearchResult;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSearchResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getDisplayTitle ()Ljava/lang/String; + public final fun getFirstAirDate ()Ljava/lang/String; + public final fun getId ()Ljava/lang/Integer; + public final fun getMediaType ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getOriginalName ()Ljava/lang/String; + public final fun getOriginalTitle ()Ljava/lang/String; + public final fun getPosterPath ()Ljava/lang/String; + public final fun getReleaseDate ()Ljava/lang/String; + public final fun getTitle ()Ljava/lang/String; + public final fun getVoteAverage ()Ljava/lang/Double; + public final fun getYear ()Ljava/lang/Integer; + public fun hashCode ()I + public final fun isTv ()Z + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSearchResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSearchResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSearchResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSearchResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSearchResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonDetail { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonDetail$Companion; + public fun ()V + public fun (Ljava/lang/Integer;Ljava/util/List;)V + public synthetic fun (Ljava/lang/Integer;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Integer; + public final fun component2 ()Ljava/util/List; + public final fun copy (Ljava/lang/Integer;Ljava/util/List;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonDetail; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonDetail;Ljava/lang/Integer;Ljava/util/List;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonDetail; + public fun equals (Ljava/lang/Object;)Z + public final fun getEpisodes ()Ljava/util/List; + public final fun getSeasonNumber ()Ljava/lang/Integer; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonDetail$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonDetail$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonDetail; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonDetail;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonDetail$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonSummary { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonSummary$Companion; + public fun ()V + public fun (Ljava/lang/Integer;Ljava/lang/Integer;)V + public synthetic fun (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Integer; + public final fun component2 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/Integer;Ljava/lang/Integer;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonSummary; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonSummary;Ljava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonSummary; + public fun equals (Ljava/lang/Object;)Z + public final fun getEpisodeCount ()Ljava/lang/Integer; + public final fun getSeasonNumber ()Ljava/lang/Integer; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonSummary$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonSummary$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonSummary; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonSummary;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbSeasonSummary$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbTvDetail { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbTvDetail$Companion; + public fun ()V + public fun (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings;)V + public synthetic fun (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Integer; + public final fun component10 ()Ljava/util/List; + public final fun component11 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds; + public final fun component12 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos; + public final fun component13 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits; + public final fun component14 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult; + public final fun component15 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult; + public final fun component16 ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/Double; + public final fun component8 ()Ljava/util/List; + public final fun component9 ()Ljava/util/List; + public final fun copy (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbTvDetail; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbTvDetail;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbTvDetail; + public fun equals (Ljava/lang/Object;)Z + public final fun getContentRatings ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbContentRatings; + public final fun getCredits ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbCredits; + public final fun getDisplayTitle ()Ljava/lang/String; + public final fun getEpisodeRunTime ()Ljava/util/List; + public final fun getExternalIds ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbIds; + public final fun getFirstAirDate ()Ljava/lang/String; + public final fun getGenres ()Ljava/util/List; + public final fun getId ()Ljava/lang/Integer; + public final fun getName ()Ljava/lang/String; + public final fun getOriginalName ()Ljava/lang/String; + public final fun getOverview ()Ljava/lang/String; + public final fun getPosterPath ()Ljava/lang/String; + public final fun getRecommendations ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult; + public final fun getSeasons ()Ljava/util/List; + public final fun getSimilar ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbPageResult; + public final fun getVideos ()Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos; + public final fun getVoteAverage ()Ljava/lang/Double; + public final fun getYear ()Ljava/lang/Integer; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbTvDetail$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbTvDetail$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbTvDetail; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbTvDetail;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbTvDetail$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideo { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideo$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideo; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideo;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideo; + public fun equals (Ljava/lang/Object;)Z + public final fun getKey ()Ljava/lang/String; + public final fun getSite ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideo$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideo$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideo; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideo;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideo$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos { + public static final field Companion Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos$Companion; + public fun ()V + public fun (Ljava/util/List;)V + public synthetic fun (Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos;Ljava/util/List;ILjava/lang/Object;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos; + public fun equals (Ljava/lang/Object;)Z + public final fun getResults ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/lagradost/cloudstream3/metaproviders/TmdbProvider$TmdbVideos$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public class com/lagradost/cloudstream3/metaproviders/TraktProvider : com/lagradost/cloudstream3/MainAPI { public fun ()V public fun getHasMainPage ()Z diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 18579eb0c..92969baa6 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -64,7 +64,6 @@ kotlin { implementation(libs.ktor.http) implementation(libs.nicehttp) // HTTP Library implementation(libs.rhino) // Run JavaScript - implementation(libs.tmdb.java) // TMDB API v3 Wrapper Made with RetroFit implementation(libs.bundles.cryptography) // Cryptography } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt index 89f935da3..88dda631a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt @@ -1,7 +1,9 @@ package com.lagradost.cloudstream3.metaproviders +import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.Actor +import com.lagradost.cloudstream3.ActorData import com.lagradost.cloudstream3.Episode import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.HomePageList @@ -20,6 +22,8 @@ import com.lagradost.cloudstream3.SearchResponseList import com.lagradost.cloudstream3.TvSeriesLoadResponse import com.lagradost.cloudstream3.TvSeriesSearchResponse import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.addDate +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.newEpisode import com.lagradost.cloudstream3.newHomePageResponse import com.lagradost.cloudstream3.newMovieLoadResponse @@ -28,35 +32,22 @@ import com.lagradost.cloudstream3.newTvSeriesLoadResponse import com.lagradost.cloudstream3.newTvSeriesSearchResponse import com.lagradost.cloudstream3.runAllAsync import com.lagradost.cloudstream3.toNewSearchResponseList +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.uwetrottmann.tmdb2.Tmdb -import com.uwetrottmann.tmdb2.entities.AppendToResponse -import com.uwetrottmann.tmdb2.entities.BaseMovie -import com.uwetrottmann.tmdb2.entities.BaseTvShow -import com.uwetrottmann.tmdb2.entities.CastMember -import com.uwetrottmann.tmdb2.entities.ContentRating -import com.uwetrottmann.tmdb2.entities.Movie -import com.uwetrottmann.tmdb2.entities.ReleaseDate -import com.uwetrottmann.tmdb2.entities.ReleaseDatesResult -import com.uwetrottmann.tmdb2.entities.TvSeason -import com.uwetrottmann.tmdb2.entities.TvShow -import com.uwetrottmann.tmdb2.entities.Videos -import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem -import com.uwetrottmann.tmdb2.enumerations.VideoType -import retrofit2.awaitResponse -import retrofit2.Response -import java.util.Calendar +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable /** * episode and season starting from 1 * they are null if movie - * */ + */ +@Serializable data class TmdbLink( - @JsonProperty("imdbID") val imdbID: String?, - @JsonProperty("tmdbID") val tmdbID: Int?, - @JsonProperty("episode") val episode: Int?, - @JsonProperty("season") val season: Int?, - @JsonProperty("movieName") val movieName: String? = null, + @JsonProperty("imdbID") @SerialName("imdbID") val imdbID: String?, + @JsonProperty("tmdbID") @SerialName("tmdbID") val tmdbID: Int?, + @JsonProperty("episode") @SerialName("episode") val episode: Int?, + @JsonProperty("season") @SerialName("season") val season: Int?, + @JsonProperty("movieName") @SerialName("movieName") val movieName: String? = null, ) open class TmdbProvider : MainAPI() { @@ -67,19 +58,184 @@ open class TmdbProvider : MainAPI() { open val useMetaLoadResponse = false open val apiName = "TMDB" - // As some sites doesn't support s0 + // As some sites don't support s0 open val disableSeasonZero = true override val hasMainPage = true override val providerType = ProviderType.MetaProvider - // Fuck it, public private api key because github actions won't co-operate. - // Please no stealy. - private val tmdb = Tmdb("e6333b32409e02a4a6eba6fb7ff866bb") + private val tmdbApiKey = "e6333b32409e02a4a6eba6fb7ff866bb" + private val tmdbApiUrl = "https://api.themoviedb.org/3" + + @Serializable + data class TmdbIds( + @JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String? = null, + @JsonProperty("tvdb_id") @SerialName("tvdb_id") val tvdbId: Int? = null, + ) + + @Serializable + data class TmdbGenre( + @JsonProperty("id") @SerialName("id") val id: Int? = null, + @JsonProperty("name") @SerialName("name") val name: String? = null, + ) + + @Serializable + data class TmdbCastMember( + @JsonProperty("name") @SerialName("name") val name: String? = null, + @JsonProperty("character") @SerialName("character") val character: String? = null, + @JsonProperty("profile_path") @SerialName("profile_path") val profilePath: String? = null, + ) + + @Serializable + data class TmdbCredits( + @JsonProperty("cast") @SerialName("cast") val cast: List? = null, + ) + + @Serializable + data class TmdbVideo( + @JsonProperty("key") @SerialName("key") val key: String? = null, + @JsonProperty("site") @SerialName("site") val site: String? = null, + @JsonProperty("type") @SerialName("type") val type: String? = null, + ) + + @Serializable + data class TmdbVideos( + @JsonProperty("results") @SerialName("results") val results: List? = null, + ) + + // Shared between movie and tv search results + @Serializable + data class TmdbSearchResult( + @JsonProperty("id") @SerialName("id") val id: Int? = null, + @JsonProperty("title") @SerialName("title") val title: String? = null, // movies + @JsonProperty("original_title") @SerialName("original_title") val originalTitle: String? = null, + @JsonProperty("name") @SerialName("name") val name: String? = null, // tv + @JsonProperty("original_name") @SerialName("original_name") val originalName: String? = null, + @JsonProperty("poster_path") @SerialName("poster_path") val posterPath: String? = null, + @JsonProperty("vote_average") @SerialName("vote_average") val voteAverage: Double? = null, + @JsonProperty("release_date") @SerialName("release_date") val releaseDate: String? = null, + @JsonProperty("first_air_date") @SerialName("first_air_date") val firstAirDate: String? = null, + @JsonProperty("media_type") @SerialName("media_type") val mediaType: String? = null, // for multi-search + ) { + @get:JsonIgnore val isTv get() = name != null || mediaType == "tv" + @get:JsonIgnore val displayTitle get() = title ?: originalTitle ?: name ?: originalName ?: "" + @get:JsonIgnore val year get() = (releaseDate ?: firstAirDate)?.take(4)?.toIntOrNull() + } + + @Serializable + data class TmdbPageResult( + @JsonProperty("results") @SerialName("results") val results: List? = null, + @JsonProperty("total_pages") @SerialName("total_pages") val totalPages: Int? = null, + @JsonProperty("total_results") @SerialName("total_results") val totalResults: Int? = null, + ) + + @Serializable + data class TmdbMultiResult( + @JsonProperty("results") @SerialName("results") val results: List? = null, + ) + + @Serializable + data class TmdbEpisode( + @JsonProperty("id") @SerialName("id") val id: Int? = null, + @JsonProperty("name") @SerialName("name") val name: String? = null, + @JsonProperty("overview") @SerialName("overview") val overview: String? = null, + @JsonProperty("episode_number") @SerialName("episode_number") val episodeNumber: Int? = null, + @JsonProperty("season_number") @SerialName("season_number") val seasonNumber: Int? = null, + @JsonProperty("still_path") @SerialName("still_path") val stillPath: String? = null, + @JsonProperty("air_date") @SerialName("air_date") val airDate: String? = null, + @JsonProperty("vote_average") @SerialName("vote_average") val voteAverage: Double? = null, + @JsonProperty("external_ids") @SerialName("external_ids") val externalIds: TmdbIds? = null, + ) + + @Serializable + data class TmdbSeasonDetail( + @JsonProperty("season_number") @SerialName("season_number") val seasonNumber: Int? = null, + @JsonProperty("episodes") @SerialName("episodes") val episodes: List? = null, + ) + + @Serializable + data class TmdbSeasonSummary( + @JsonProperty("season_number") @SerialName("season_number") val seasonNumber: Int? = null, + @JsonProperty("episode_count") @SerialName("episode_count") val episodeCount: Int? = null, + ) + + @Serializable + data class TmdbContentRating( + @JsonProperty("iso_3166_1") @SerialName("iso_3166_1") val country: String? = null, + @JsonProperty("rating") @SerialName("rating") val rating: String? = null, + ) + + @Serializable + data class TmdbContentRatings( + @JsonProperty("results") @SerialName("results") val results: List? = null, + ) + + @Serializable + data class TmdbReleaseDateEntry( + @JsonProperty("certification") @SerialName("certification") val certification: String? = null, + @JsonProperty("type") @SerialName("type") val type: Int? = null, + ) + + @Serializable + data class TmdbReleaseDateResult( + @JsonProperty("iso_3166_1") @SerialName("iso_3166_1") val country: String? = null, + @JsonProperty("release_dates") @SerialName("release_dates") val releaseDates: List? = null, + ) + + @Serializable + data class TmdbReleaseDates( + @JsonProperty("results") @SerialName("results") val results: List? = null, + ) + + @Serializable + data class TmdbTvDetail( + @JsonProperty("id") @SerialName("id") val id: Int? = null, + @JsonProperty("name") @SerialName("name") val name: String? = null, + @JsonProperty("original_name") @SerialName("original_name") val originalName: String? = null, + @JsonProperty("overview") @SerialName("overview") val overview: String? = null, + @JsonProperty("poster_path") @SerialName("poster_path") val posterPath: String? = null, + @JsonProperty("first_air_date") @SerialName("first_air_date") val firstAirDate: String? = null, + @JsonProperty("vote_average") @SerialName("vote_average") val voteAverage: Double? = null, + @JsonProperty("genres") @SerialName("genres") val genres: List? = null, + @JsonProperty("episode_run_time") @SerialName("episode_run_time") val episodeRunTime: List? = null, + @JsonProperty("seasons") @SerialName("seasons") val seasons: List? = null, + @JsonProperty("external_ids") @SerialName("external_ids") val externalIds: TmdbIds? = null, + @JsonProperty("videos") @SerialName("videos") val videos: TmdbVideos? = null, + @JsonProperty("credits") @SerialName("credits") val credits: TmdbCredits? = null, + @JsonProperty("recommendations") @SerialName("recommendations") val recommendations: TmdbPageResult? = null, + @JsonProperty("similar") @SerialName("similar") val similar: TmdbPageResult? = null, + @JsonProperty("content_ratings") @SerialName("content_ratings") val contentRatings: TmdbContentRatings? = null, + ) { + @get:JsonIgnore val displayTitle get() = name ?: originalName ?: "" + @get:JsonIgnore val year get() = firstAirDate?.take(4)?.toIntOrNull() + } + + @Serializable + data class TmdbMovieDetail( + @JsonProperty("id") @SerialName("id") val id: Int? = null, + @JsonProperty("title") @SerialName("title") val title: String? = null, + @JsonProperty("original_title") @SerialName("original_title") val originalTitle: String? = null, + @JsonProperty("overview") @SerialName("overview") val overview: String? = null, + @JsonProperty("poster_path") @SerialName("poster_path") val posterPath: String? = null, + @JsonProperty("release_date") @SerialName("release_date") val releaseDate: String? = null, + @JsonProperty("vote_average") @SerialName("vote_average") val voteAverage: Double? = null, + @JsonProperty("genres") @SerialName("genres") val genres: List? = null, + @JsonProperty("runtime") @SerialName("runtime") val runtime: Int? = null, + @JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String? = null, + @JsonProperty("external_ids") @SerialName("external_ids") val externalIds: TmdbIds? = null, + @JsonProperty("videos") @SerialName("videos") val videos: TmdbVideos? = null, + @JsonProperty("credits") @SerialName("credits") val credits: TmdbCredits? = null, + @JsonProperty("recommendations") @SerialName("recommendations") val recommendations: TmdbPageResult? = null, + @JsonProperty("similar") @SerialName("similar") val similar: TmdbPageResult? = null, + @JsonProperty("release_dates") @SerialName("release_dates") val releaseDates: TmdbReleaseDates? = null, + ) { + @get:JsonIgnore val displayTitle get() = title ?: originalTitle ?: "" + @get:JsonIgnore val year get() = releaseDate?.take(4)?.toIntOrNull() + } private fun getImageUrl(link: String?): String? { - if (link == null) return null - return if (link.startsWith("/")) "https://image.tmdb.org/t/p/w500/$link" else link + link ?: return null + return if (link.startsWith("/")) "https://image.tmdb.org/t/p/w500$link" else link } private fun getUrl(id: Int?, tvShow: Boolean): String { @@ -87,199 +243,197 @@ open class TmdbProvider : MainAPI() { else "https://www.themoviedb.org/movie/${id ?: -1}" } - private fun BaseTvShow.toSearchResponse(): TvSeriesSearchResponse { - return newTvSeriesSearchResponse( - name = this.name ?: this.original_name, + private suspend fun getApi(path: String, extraParams: Map = emptyMap()): String { + val params = buildMap { + put("api_key", tmdbApiKey) + putAll(extraParams) + } + return app.get( + url = "$tmdbApiUrl$path", + params = params, + ).text + } + + private fun TmdbSearchResult.toSearchResponse() = if (isTv) { + newTvSeriesSearchResponse( + name = displayTitle, url = getUrl(id, true), type = TvType.TvSeries, - fix = false + fix = false, ) { this.id = this@toSearchResponse.id - this.posterUrl = getImageUrl(poster_path) - this.score = Score.from10(vote_average) - this.year = first_air_date?.let { - Calendar.getInstance().apply { - time = it - }.get(Calendar.YEAR) - } + this.posterUrl = getImageUrl(posterPath) + this.score = Score.from10(voteAverage) + this.year = this@toSearchResponse.year } - } - - private fun BaseMovie.toSearchResponse(): MovieSearchResponse { - return newMovieSearchResponse( - name = this.title ?: this.original_title, + } else { + newMovieSearchResponse( + name = displayTitle, url = getUrl(id, false), type = TvType.Movie, - fix = false + fix = false, ) { this.id = this@toSearchResponse.id - this.posterUrl = getImageUrl(poster_path) - this.score = Score.from10(vote_average) - this.year = release_date?.let { - Calendar.getInstance().apply { - time = it - }.get(Calendar.YEAR) - } + this.posterUrl = getImageUrl(posterPath) + this.score = Score.from10(voteAverage) + this.year = this@toSearchResponse.year } } - private fun List?.toActors(): List>? { + private fun List?.toActors(): List>? { return this?.mapNotNull { + it ?: return@mapNotNull null Pair( - Actor(it?.name ?: return@mapNotNull null, getImageUrl(it.profile_path)), - it.character + Actor(it.name ?: return@mapNotNull null, getImageUrl(it.profilePath)), + it.character, ) } } - private suspend fun TvShow.toLoadResponse(): TvSeriesLoadResponse { - val tvSeasonsService = tmdb.tvSeasonsService() - val episodes = mutableListOf() - - val validSeasons = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 } ?: emptyList() - for (season in validSeasons) { - val seasonNumber = season.season_number ?: continue - - val response: Response = tmdb.tvSeasonsService() - .season(this.id, seasonNumber, "external_ids,images,episodes") - .awaitResponse() - - val fullSeason = response.body() ?: continue - - fullSeason.episodes?.forEach { episode -> - episodes += newEpisode( - TmdbLink( - episode.external_ids?.imdb_id ?: this.external_ids?.imdb_id, - this.id, - episode.episode_number, - episode.season_number, - this.name ?: this.original_name - ).toJson() - ) { - this.name = episode.name - this.season = episode.season_number - this.episode = episode.episode_number - this.score = Score.from10(episode.vote_average) - this.description = episode.overview - this.date = episode.air_date?.time - this.posterUrl = getImageUrl(episode.still_path) - } - } - } - - return newTvSeriesLoadResponse( - this.name ?: this.original_name, - getUrl(id, true), - TvType.TvSeries, - episodes - ) { - posterUrl = getImageUrl(poster_path) - year = first_air_date?.let { - Calendar.getInstance().apply { - time = it - }.get(Calendar.YEAR) - } - plot = overview - addImdbId(external_ids?.imdb_id) - tags = genres?.mapNotNull { it.name } - duration = episode_run_time?.average()?.toInt() - score = Score.from10(vote_average) - addTrailer(videos.toTrailers()) - recommendations = (this@toLoadResponse.recommendations - ?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() } - addActors(credits?.cast?.toList().toActors()) - contentRating = fetchContentRating(id, "US") - } - } - - private fun Videos?.toTrailers(): List? { - return this?.results?.filter { it.type != VideoType.OPENING_CREDITS && it.type != VideoType.FEATURETTE } - ?.sortedBy { it.type?.ordinal ?: 10000 } + private fun TmdbVideos?.toTrailers(): List? { + val skipTypes = setOf("Opening Credits", "Featurette") + return this?.results + ?.filter { it.type !in skipTypes } + ?.sortedBy { it.type } ?.mapNotNull { when (it.site?.trim()?.lowercase()) { - "youtube" -> { // TODO FILL SITES - "https://www.youtube.com/watch?v=${it.key}" - } + "youtube" -> "https://www.youtube.com/watch?v=${it.key}" else -> null } } } - private suspend fun Movie.toLoadResponse(): MovieLoadResponse { - return newMovieLoadResponse( - this.title ?: this.original_title, getUrl(id, false), TvType.Movie, TmdbLink( - this.imdb_id, - this.id, - null, - null, - this.title ?: this.original_title, - ).toJson() - ) { - posterUrl = getImageUrl(poster_path) - year = release_date?.let { - Calendar.getInstance().apply { - time = it - }.get(Calendar.YEAR) - } - plot = overview - addImdbId(external_ids?.imdb_id) - tags = genres?.mapNotNull { it.name } - duration = runtime - score = Score.from10(vote_average) - addTrailer(videos.toTrailers()) + open suspend fun fetchContentRating(id: Int?, country: String): String? { + id ?: return null + // Try TV content ratings first + val tvRating = parseJson( + getApi("/tv/$id/content_ratings") + ).results?.firstOrNull { it.country == country }?.rating + if (tvRating != null) return tvRating + // Fall back to movie release dates + return parseJson( + getApi("/movie/$id/release_dates") + ).results?.firstOrNull { it.country == country } + ?.releaseDates?.firstOrNull { !it.certification.isNullOrBlank() } + ?.certification + } + + private suspend fun TmdbTvDetail.toLoadResponse(): TvSeriesLoadResponse { + val episodes = mutableListOf() + val validSeasons = seasons?.filter { !disableSeasonZero || (it.seasonNumber ?: 0) != 0 } + ?: emptyList() + + for (season in validSeasons) { + val seasonNum = season.seasonNumber ?: continue + val fullSeason = parseJson( + getApi("/tv/$id/season/$seasonNum", mapOf("append_to_response" to "external_ids")) + ) + + fullSeason.episodes?.forEach { episode -> + episodes += newEpisode( + TmdbLink( + episode.externalIds?.imdbId ?: externalIds?.imdbId, + id, + episode.episodeNumber, + episode.seasonNumber, + displayTitle, + ).toJson() + ) { + this.name = episode.name + this.season = episode.seasonNumber + this.episode = episode.episodeNumber + this.score = Score.from10(episode.voteAverage) + this.description = episode.overview + this.posterUrl = getImageUrl(episode.stillPath) + this.addDate(episode.airDate) + } + } + } + + return newTvSeriesLoadResponse( + displayTitle, + getUrl(id, true), + TvType.TvSeries, + episodes, + ) { + posterUrl = getImageUrl(posterPath) + this.year = this@toLoadResponse.year + plot = overview + addImdbId(externalIds?.imdbId) + tags = genres?.mapNotNull { it.name } + duration = episodeRunTime?.average()?.toInt() + score = Score.from10(voteAverage) + addTrailer(videos.toTrailers()) recommendations = (this@toLoadResponse.recommendations ?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() } addActors(credits?.cast?.toList().toActors()) + contentRating = contentRatings?.results?.firstOrNull { it.country == "US" }?.rating + ?: fetchContentRating(id, "US") + } + } - contentRating = fetchContentRating(id, "US") + private suspend fun TmdbMovieDetail.toLoadResponse(): MovieLoadResponse { + return newMovieLoadResponse( + displayTitle, + getUrl(id, false), + TvType.Movie, + TmdbLink( + imdbId ?: externalIds?.imdbId, + id, + null, + null, + displayTitle, + ).toJson() + ) { + posterUrl = getImageUrl(posterPath) + this.year = this@toLoadResponse.year + plot = overview + addImdbId(imdbId ?: externalIds?.imdbId) + tags = genres?.mapNotNull { it.name } + duration = runtime + score = Score.from10(voteAverage) + addTrailer(videos.toTrailers()) + recommendations = (this@toLoadResponse.recommendations + ?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() } + addActors(credits?.cast?.toList().toActors()) + contentRating = releaseDates?.results + ?.firstOrNull { it.country == "US" } + ?.releaseDates?.firstOrNull { !it.certification.isNullOrBlank() } + ?.certification } } override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { - - // SAME AS DISCOVER IT SEEMS -// val popularSeries = tmdb.tvService().popular(page, "en-US").execute().body()?.results?.map { -// it.toSearchResponse() -// } ?: listOf() -// -// val popularMovies = -// tmdb.moviesService().popular(page, "en-US", "840").execute().body()?.results?.map { -// it.toSearchResponse() -// } ?: listOf() - var discoverMovies: List = listOf() var discoverSeries: List = listOf() var topMovies: List = listOf() var topSeries: List = listOf() runAllAsync( { - discoverMovies = tmdb.discoverMovie().page(page).build().awaitResponse().body()?.results?.map { - it.toSearchResponse() - } ?: listOf() - }, { - discoverSeries = tmdb.discoverTv().page(page).build().awaitResponse().body()?.results?.map { - it.toSearchResponse() - } ?: listOf() - }, { - // https://en.wikipedia.org/wiki/ISO_3166-1 - topMovies = - tmdb.moviesService().topRated(page, "en-US", "US").awaitResponse() - .body()?.results?.map { - it.toSearchResponse() - } ?: listOf() - }, { - topSeries = - tmdb.tvService().topRated(page, "en-US").awaitResponse().body()?.results?.map { - it.toSearchResponse() - } ?: listOf() - } + discoverMovies = parseJson( + getApi("/discover/movie", mapOf("page" to "$page")) + ).results?.map { it.toSearchResponse() as MovieSearchResponse } ?: listOf() + }, + { + discoverSeries = parseJson( + getApi("/discover/tv", mapOf("page" to "$page")) + ).results?.map { it.toSearchResponse() as TvSeriesSearchResponse } ?: listOf() + }, + { + topMovies = parseJson( + getApi("/movie/top_rated", mapOf("page" to "$page", "language" to "en-US", "region" to "US")) + ).results?.map { it.toSearchResponse() as MovieSearchResponse } ?: listOf() + }, + { + topSeries = parseJson( + getApi("/tv/top_rated", mapOf("page" to "$page", "language" to "en-US")) + ).results?.map { it.toSearchResponse() as TvSeriesSearchResponse } ?: listOf() + }, ) return newHomePageResponse( listOf( -// HomePageList("Popular Series", popularSeries), -// HomePageList("Popular Movies", popularMovies), HomePageList("Popular Movies", discoverMovies), HomePageList("Popular Series", discoverSeries), HomePageList("Top Movies", topMovies), @@ -288,47 +442,14 @@ open class TmdbProvider : MainAPI() { ) } - open fun loadFromImdb(imdb: String, seasons: List): LoadResponse? { - return null - } + open fun loadFromImdb(imdb: String, seasons: List): LoadResponse? = null + open fun loadFromTmdb(tmdbId: Int, seasons: List): LoadResponse? = null + open fun loadFromImdb(imdb: String): LoadResponse? = null + open fun loadFromTmdb(tmdbId: Int): LoadResponse? = null - open fun loadFromTmdb(tmdb: Int, seasons: List): LoadResponse? { - return null - } - - open fun loadFromImdb(imdb: String): LoadResponse? { - return null - } - - open fun loadFromTmdb(tmdb: Int): LoadResponse? { - return null - } - - open suspend fun fetchContentRating(id: Int?, country: String): String? { - id ?: return null - - val contentRatings = tmdb.tvService().content_ratings(id).awaitResponse().body()?.results - return if (!contentRatings.isNullOrEmpty()) { - contentRatings.firstOrNull { it: ContentRating -> - it.iso_3166_1 == country - }?.rating - } else { - val releaseDates = tmdb.moviesService().releaseDates(id).awaitResponse().body()?.results - val certification = releaseDates?.firstOrNull { it: ReleaseDatesResult -> - it.iso_3166_1 == country - }?.release_dates?.firstOrNull { it: ReleaseDate -> - !it.certification.isNullOrBlank() - }?.certification - - certification - } - } - - // Possible to add recommendations and such here. override suspend fun load(url: String): LoadResponse? { // https://www.themoviedb.org/movie/7445-brothers // https://www.themoviedb.org/tv/71914-the-wheel-of-time - val idRegex = Regex("""themoviedb\.org/(.*)/(\d+)""") val found = idRegex.find(url) @@ -337,86 +458,64 @@ open class TmdbProvider : MainAPI() { ?: throw ErrorLoadingException("No id found") return if (useMetaLoadResponse) { - return if (isTvSeries) { - val body = tmdb.tvService() - .tv( - id, - "en-US", - AppendToResponse( - AppendToResponseItem.EXTERNAL_IDS, - AppendToResponseItem.VIDEOS + if (isTvSeries) { + val detail = parseJson( + getApi( + "/tv/$id", + mapOf( + "language" to "en-US", + "append_to_response" to "external_ids,videos,credits,recommendations,similar,content_ratings", ) ) - .awaitResponse().body() - val response = body?.toLoadResponse() - if (response != null) { - if (response.recommendations.isNullOrEmpty()) - tmdb.tvService().recommendations(id, 1, "en-US").awaitResponse().body() - ?.let { - it.results?.map { res -> res.toSearchResponse() } - }?.let { list -> - response.recommendations = list - } - - if (response.actors.isNullOrEmpty()) - tmdb.tvService().credits(id, "en-US").awaitResponse().body()?.let { - response.addActors(it.cast?.toActors()) - } - } - - response + ) + detail.toLoadResponse() } else { - val body = tmdb.moviesService() - .summary( - id, - "en-US", - AppendToResponse( - AppendToResponseItem.EXTERNAL_IDS, - AppendToResponseItem.VIDEOS + val detail = parseJson( + getApi( + "/movie/$id", + mapOf( + "language" to "en-US", + "append_to_response" to "external_ids,videos,credits,recommendations,similar,release_dates", ) ) - .awaitResponse().body() - val response = body?.toLoadResponse() - if (response != null) { - if (response.recommendations.isNullOrEmpty()) - tmdb.moviesService().recommendations(id, 1, "en-US").awaitResponse().body() - ?.let { - it.results?.map { res -> res.toSearchResponse() } - }?.let { list -> - response.recommendations = list - } - - if (response.actors.isNullOrEmpty()) - tmdb.moviesService().credits(id).awaitResponse().body()?.let { - response.addActors(it.cast?.toActors()) - } - } - response + ) + detail.toLoadResponse() } } else { loadFromTmdb(id)?.let { return it } if (isTvSeries) { - tmdb.tvService().externalIds(id).awaitResponse().body()?.imdb_id?.let { - val fromImdb = loadFromImdb(it) - val result = if (fromImdb == null) { - val details = tmdb.tvService().tv(id, "en-US").awaitResponse().body() - loadFromImdb(it, details?.seasons ?: listOf()) - ?: loadFromTmdb(id, details?.seasons ?: listOf()) - } else fromImdb - - result + val externalIds = parseJson(getApi("/tv/$id/external_ids")) + val imdbId = externalIds.imdbId + if (imdbId != null) { + val fromImdb = loadFromImdb(imdbId) + if (fromImdb != null) return fromImdb + } + val seasons = parseJson(getApi("/tv/$id")).seasons ?: listOf() + if (imdbId != null) { + loadFromImdb(imdbId, seasons) ?: loadFromTmdb(id, seasons) + } else { + loadFromTmdb(id, seasons) } } else { - tmdb.moviesService().externalIds(id).awaitResponse() - .body()?.imdb_id?.let { loadFromImdb(it) } + val imdbId = parseJson(getApi("/movie/$id")).imdbId + if (imdbId != null) loadFromImdb(imdbId) else null } } } override suspend fun search(query: String, page: Int): SearchResponseList? { - return tmdb.searchService().multi(query, page, "en-US", "US", includeAdult).awaitResponse() - .body()?.results?.mapNotNull { - it.movie?.toSearchResponse() ?: it.tvShow?.toSearchResponse() - }?.toNewSearchResponseList() + return parseJson( + getApi( + "/search/multi", + mapOf( + "query" to query, + "page" to "$page", + "language" to "en-US", + "include_adult" to "$includeAdult", + ) + ) + ).results?.mapNotNull { + if (it.mediaType == "person") null else it.toSearchResponse() + }?.toNewSearchResponseList() } -} \ No newline at end of file +} From 6ca7507146748bb378a9155cbc5a7e70f3919650 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:34:43 -0700 Subject: [PATCH 07/19] HlsPlaylistParser: replace usage of Java APIs with kotlin equivalents (#2828) This does add kotlinx-io as a dependency, it is more light weight then okio, and when we migrate to ktor for example it will become an implicit dependency anyway so it should be fine to just add. Allows for more reliable and readable usage then doing some of this manually. --- gradle/libs.versions.toml | 2 + library/api/jvm/library.api | 52 +++------------ library/build.gradle.kts | 1 + .../cloudstream3/utils/HlsPlaylistParser.kt | 63 ++++++++++--------- 4 files changed, 44 insertions(+), 74 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 898529d9a..e38895451 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,7 @@ kotlinxAtomicfu = "0.33.0" kotlinxCollectionsImmutable = "0.4.0" kotlinxCoroutines = "1.11.0" kotlinxDatetime = "0.8.0" +kotlinxIOCore = "0.9.1" kotlinxSerializationJson = "1.11.0" ksoup = "0.2.6" ktor = "3.5.0" @@ -98,6 +99,7 @@ kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collec kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } +kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIOCore" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } ksoup = { module = "com.fleeksoft.ksoup:ksoup", version.ref = "ksoup" } ktor-http = { module = "io.ktor:ktor-http", version.ref = "ktor" } diff --git a/library/api/jvm/library.api b/library/api/jvm/library.api index 0d9b8f76e..195cd49fe 100644 --- a/library/api/jvm/library.api +++ b/library/api/jvm/library.api @@ -8327,44 +8327,6 @@ public final class com/lagradost/cloudstream3/utils/HlsPlaylistParser { public final fun parse (Ljava/lang/String;Ljava/lang/String;)Lcom/lagradost/cloudstream3/utils/HlsPlaylistParser$HlsMultivariantPlaylist; } -public final class com/lagradost/cloudstream3/utils/HlsPlaylistParser$C { - public static final field CENC_TYPE_cbc1 Ljava/lang/String; - public static final field CENC_TYPE_cbcs Ljava/lang/String; - public static final field CENC_TYPE_cenc Ljava/lang/String; - public static final field CENC_TYPE_cens Ljava/lang/String; - public static final field INSTANCE Lcom/lagradost/cloudstream3/utils/HlsPlaylistParser$C; - public static final field ROLE_FLAG_ALTERNATE I - public static final field ROLE_FLAG_CAPTION I - public static final field ROLE_FLAG_COMMENTARY I - public static final field ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND I - public static final field ROLE_FLAG_DESCRIBES_VIDEO I - public static final field ROLE_FLAG_DUB I - public static final field ROLE_FLAG_EASY_TO_READ I - public static final field ROLE_FLAG_EMERGENCY I - public static final field ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY I - public static final field ROLE_FLAG_MAIN I - public static final field ROLE_FLAG_SIGN I - public static final field ROLE_FLAG_SUBTITLE I - public static final field ROLE_FLAG_SUPPLEMENTARY I - public static final field ROLE_FLAG_TRANSCRIBES_DIALOG I - public static final field ROLE_FLAG_TRICK_PLAY I - public static final field SELECTION_FLAG_AUTOSELECT I - public static final field SELECTION_FLAG_DEFAULT I - public static final field SELECTION_FLAG_FORCED I - public static final field TRACK_TYPE_AUDIO I - public static final field TRACK_TYPE_CAMERA_MOTION I - public static final field TRACK_TYPE_DEFAULT I - public static final field TRACK_TYPE_IMAGE I - public static final field TRACK_TYPE_METADATA I - public static final field TRACK_TYPE_NONE I - public static final field TRACK_TYPE_TEXT I - public static final field TRACK_TYPE_UNKNOWN I - public static final field TRACK_TYPE_VIDEO I - public final fun getCLEARKEY_UUID ()Ljava/util/UUID; - public final fun getPLAYREADY_UUID ()Ljava/util/UUID; - public final fun getWIDEVINE_UUID ()Ljava/util/UUID; -} - public final class com/lagradost/cloudstream3/utils/HlsPlaylistParser$DrmInitData { public fun (Ljava/lang/String;[Lcom/lagradost/cloudstream3/utils/HlsPlaylistParser$SchemeData;)V public final fun component1 ()Ljava/lang/String; @@ -8630,7 +8592,7 @@ public final class com/lagradost/cloudstream3/utils/HlsPlaylistParser$ParserExce public final class com/lagradost/cloudstream3/utils/HlsPlaylistParser$PsshAtomUtil { public static final field INSTANCE Lcom/lagradost/cloudstream3/utils/HlsPlaylistParser$PsshAtomUtil; - public final fun buildPsshAtom (Ljava/util/UUID;[Ljava/util/UUID;[B)[B + public final fun buildPsshAtom (Lkotlin/uuid/Uuid;[Lkotlin/uuid/Uuid;[B)[B } public final class com/lagradost/cloudstream3/utils/HlsPlaylistParser$Rendition { @@ -8651,19 +8613,19 @@ public final class com/lagradost/cloudstream3/utils/HlsPlaylistParser$Rendition } public final class com/lagradost/cloudstream3/utils/HlsPlaylistParser$SchemeData { - public fun (Ljava/util/UUID;Ljava/lang/String;Ljava/lang/String;[B)V - public synthetic fun (Ljava/util/UUID;Ljava/lang/String;Ljava/lang/String;[BILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/util/UUID; + public fun (Lkotlin/uuid/Uuid;Ljava/lang/String;Ljava/lang/String;[B)V + public synthetic fun (Lkotlin/uuid/Uuid;Ljava/lang/String;Ljava/lang/String;[BILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlin/uuid/Uuid; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; public final fun component4 ()[B - public final fun copy (Ljava/util/UUID;Ljava/lang/String;Ljava/lang/String;[B)Lcom/lagradost/cloudstream3/utils/HlsPlaylistParser$SchemeData; - public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/utils/HlsPlaylistParser$SchemeData;Ljava/util/UUID;Ljava/lang/String;Ljava/lang/String;[BILjava/lang/Object;)Lcom/lagradost/cloudstream3/utils/HlsPlaylistParser$SchemeData; + public final fun copy (Lkotlin/uuid/Uuid;Ljava/lang/String;Ljava/lang/String;[B)Lcom/lagradost/cloudstream3/utils/HlsPlaylistParser$SchemeData; + public static synthetic fun copy$default (Lcom/lagradost/cloudstream3/utils/HlsPlaylistParser$SchemeData;Lkotlin/uuid/Uuid;Ljava/lang/String;Ljava/lang/String;[BILjava/lang/Object;)Lcom/lagradost/cloudstream3/utils/HlsPlaylistParser$SchemeData; public fun equals (Ljava/lang/Object;)Z public final fun getData ()[B public final fun getLicenseServerUrl ()Ljava/lang/String; public final fun getMimeType ()Ljava/lang/String; - public final fun getUuid ()Ljava/util/UUID; + public final fun getUuid ()Lkotlin/uuid/Uuid; public fun hashCode ()I public fun toString ()Ljava/lang/String; } diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 92969baa6..59a36ce7f 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -59,6 +59,7 @@ kotlin { implementation(libs.kotlinx.atomicfu) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.io.core) implementation(libs.kotlinx.serialization.json) // JSON Parser implementation(libs.ksoup) // HTML Parser implementation(libs.ktor.http) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/HlsPlaylistParser.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/HlsPlaylistParser.kt index cd5d752b3..3edd0f410 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/HlsPlaylistParser.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/HlsPlaylistParser.kt @@ -21,9 +21,10 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.base64DecodeArray import io.ktor.http.Url -import java.io.IOException -import java.nio.ByteBuffer -import java.util.UUID +import kotlinx.io.Buffer +import kotlinx.io.IOException +import kotlinx.io.readByteArray +import kotlin.uuid.Uuid @Suppress("unused") object HlsPlaylistParser { @@ -186,13 +187,13 @@ object HlsPlaylistParser { data class SchemeData( /** - * The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is universal (i.e. + * The [Uuid] of the DRM scheme, or [C.UUID_NIL] if the data is universal (i.e. * applies to all schemes). */ - val uuid: UUID, + val uuid: Uuid, /** The URL of the server to which license requests should be made. May be null if unknown. */ val licenseServerUrl: String? = null, - /** The mimeType of {@link #data}. */ + /** The mimeType of [data]. */ val mimeType: String, /** The initialization data. May be null for scheme support checks only. */ val data: ByteArray @@ -454,14 +455,14 @@ object HlsPlaylistParser { // "." or "..", remove the appropriate segments of the path. if (i == segmentStart + 1 && url[segmentStart] == '.') { // Given "abc/def/./ghi", remove "./" to get "abc/def/ghi". - url.delete(segmentStart, nextSegmentStart) + url.deleteRange(segmentStart, nextSegmentStart) limit -= nextSegmentStart - segmentStart i = segmentStart } else if (i == segmentStart + 2 && url[segmentStart] == '.' && url[segmentStart + 1] == '.') { // Given "abc/def/../ghi", remove "def/../" to get "abc/ghi". val prevSegmentStart = url.lastIndexOf("/", segmentStart - 2) + 1 val removeFrom = if (prevSegmentStart > offset) prevSegmentStart else offset - url.delete(removeFrom, nextSegmentStart) + url.deleteRange(removeFrom, nextSegmentStart) limit -= nextSegmentStart - removeFrom segmentStart = prevSegmentStart i = prevSegmentStart @@ -532,31 +533,31 @@ object HlsPlaylistParser { } } - object C { + private object C { /** - * UUID for the ClearKey DRM scheme. + * [Uuid] for the ClearKey DRM scheme. * * * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up. */ - val CLEARKEY_UUID = UUID(-0x1d8e62a7567a4c37L, 0x781AB030AF78D30EL) + val CLEARKEY_UUID = Uuid.fromLongs(-0x1d8e62a7567a4c37L, 0x781AB030AF78D30EL) /** - * UUID for the Widevine DRM scheme. + * [Uuid] for the Widevine DRM scheme. * * * Widevine is supported on Android devices running Android 4.3 (API Level 18) and up. */ - val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L) + val WIDEVINE_UUID = Uuid.fromLongs(-0x121074568629b532L, -0x5c37d8232ae2de13L) /** - * UUID for the PlayReady DRM scheme. + * [Uuid] for the PlayReady DRM scheme. * * * PlayReady is supported on all AndroidTV devices. Note that most other Android devices do not * provide PlayReady support. */ - val PLAYREADY_UUID = UUID(-0x65fb0f8667bfbd7aL, -0x546d19a41f77a06bL) + val PLAYREADY_UUID = Uuid.fromLongs(-0x65fb0f8667bfbd7aL, -0x546d19a41f77a06bL) /** "cenc" scheme type name as defined in ISO/IEC 23001-7:2016. */ @@ -1066,7 +1067,7 @@ object HlsPlaylistParser { object PsshAtomUtil { fun buildPsshAtom( - systemId: UUID, keyIds: Array?, data: ByteArray? + systemId: Uuid, keyIds: Array?, data: ByteArray? ): ByteArray { val dataLength = data?.size ?: 0 var psshBoxLength: Int = @@ -1074,26 +1075,30 @@ object HlsPlaylistParser { if (keyIds != null) { psshBoxLength += 4 /* KID_count */ + (keyIds.size * 16) /* KIDs */ } - val psshBox: ByteBuffer = ByteBuffer.allocate(psshBoxLength) - psshBox.putInt(psshBoxLength) - psshBox.putInt(Mp4Box.TYPE_pssh) - psshBox.putInt(if (keyIds != null) 0x01000000 else 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */) - psshBox.putLong(systemId.mostSignificantBits) - psshBox.putLong(systemId.leastSignificantBits) + val buffer = Buffer() + buffer.writeInt(psshBoxLength) + buffer.writeInt(Mp4Box.TYPE_pssh) + buffer.writeInt(if (keyIds != null) 0x01000000 else 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */) + systemId.toLongs { mostSignificantBits, leastSignificantBits -> + buffer.writeLong(mostSignificantBits) + buffer.writeLong(leastSignificantBits) + } if (keyIds != null) { - psshBox.putInt(keyIds.size) + buffer.writeInt(keyIds.size) for (keyId in keyIds) { - psshBox.putLong(keyId.mostSignificantBits) - psshBox.putLong(keyId.leastSignificantBits) + keyId.toLongs { mostSignificantBits, leastSignificantBits -> + buffer.writeLong(mostSignificantBits) + buffer.writeLong(leastSignificantBits) + } } } if (data != null && data.size != 0) { - psshBox.putInt(data.size) - psshBox.put(data) + buffer.writeInt(data.size) + buffer.write(data) } else { - psshBox.putInt(0) + buffer.writeInt(0) } - return psshBox.array() + return buffer.readByteArray() } } From e48c97363e626ee20e74673910e7f8a771ab351a Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:27:22 -0700 Subject: [PATCH 08/19] [skip ci] Add actual declarations for webMain/webTest (#3072) --- .../com/lagradost/api/ContextHelper.web.kt | 4 +++ .../webMain/kotlin/com/lagradost/api/Log.kt | 21 ++++++++++++ .../extractors/YoutubeExtractor.web.kt | 32 +++++++++++++++++++ .../cloudstream3/mvvm/ArchComponentExt.web.kt | 32 +++++++++++++++++++ .../cloudstream3/utils/Coroutines.web.kt | 14 ++++++++ .../cloudstream3/utils/Ignore.web.kt | 5 +++ 6 files changed, 108 insertions(+) create mode 100644 library/src/webMain/kotlin/com/lagradost/api/ContextHelper.web.kt create mode 100644 library/src/webMain/kotlin/com/lagradost/api/Log.kt create mode 100644 library/src/webMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.web.kt create mode 100644 library/src/webMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.web.kt create mode 100644 library/src/webMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.web.kt create mode 100644 library/src/webTest/kotlin/com/lagradost/cloudstream3/utils/Ignore.web.kt diff --git a/library/src/webMain/kotlin/com/lagradost/api/ContextHelper.web.kt b/library/src/webMain/kotlin/com/lagradost/api/ContextHelper.web.kt new file mode 100644 index 000000000..83cae11a5 --- /dev/null +++ b/library/src/webMain/kotlin/com/lagradost/api/ContextHelper.web.kt @@ -0,0 +1,4 @@ +package com.lagradost.api + +actual fun getContext(): Any? = null +actual fun setContext(context: Any?) = Unit diff --git a/library/src/webMain/kotlin/com/lagradost/api/Log.kt b/library/src/webMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 000000000..be185f0ca --- /dev/null +++ b/library/src/webMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,21 @@ +package com.lagradost.api + +actual object Log { + actual fun d(tag: String, message: String) { + // There is no console.debug in Kotlin/JS (Yet) + // We will switch this when it is added. + console.log("[$tag] $message") + } + + actual fun i(tag: String, message: String) { + console.info("[$tag] $message") + } + + actual fun w(tag: String, message: String) { + console.warn("[$tag] $message") + } + + actual fun e(tag: String, message: String) { + console.error("[$tag] $message") + } +} diff --git a/library/src/webMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.web.kt b/library/src/webMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.web.kt new file mode 100644 index 000000000..07463bd56 --- /dev/null +++ b/library/src/webMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.web.kt @@ -0,0 +1,32 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink + +/** + * Web/JS target shim. + * + * NewPipeExtractor is JVM-only, so the full extraction logic used on + * Android/desktop can't run here. This stub keeps the multiplatform + * source set compiling; it performs no extraction and simply reports + * that no links were found on this target. + * + * If/when a JS-compatible YouTube extraction path exists, replace the + * body of getUrl() with that implementation. + */ +actual open class YoutubeExtractor actual constructor() : ExtractorApi() { + + actual override val mainUrl = "https://www.youtube.com" + actual override val name = "YouTube" + actual override val requiresReferer = false + + actual override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + ) { + // No-op on web target: NewPipeExtractor is unavailable here. + } +} diff --git a/library/src/webMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.web.kt b/library/src/webMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.web.kt new file mode 100644 index 000000000..f108bf7fb --- /dev/null +++ b/library/src/webMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.web.kt @@ -0,0 +1,32 @@ +package com.lagradost.cloudstream3.mvvm + +actual fun platformThrowAbleToResource(throwable: Throwable): Resource { + val message = throwable.message.orEmpty() + return when { + message.contains("timeout", ignoreCase = true) -> { + Resource.Failure( + true, + "Connection Timeout\nPlease try again later." + ) + } + + message.contains("NetworkError", ignoreCase = true) || + message.contains("Failed to fetch", ignoreCase = true) || + message.contains("ERR_NAME_NOT_RESOLVED", ignoreCase = true) -> { + Resource.Failure( + true, + "Cannot connect to server, try again later.\n${throwable.message}" + ) + } + + message.contains("SSL", ignoreCase = true) || + message.contains("certificate", ignoreCase = true) -> { + Resource.Failure( + true, + (throwable.message ?: "SSL error") + "\nTry a VPN or DNS." + ) + } + + else -> safeFail(throwable) + } +} diff --git a/library/src/webMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.web.kt b/library/src/webMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.web.kt new file mode 100644 index 000000000..3ea59f6cc --- /dev/null +++ b/library/src/webMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.web.kt @@ -0,0 +1,14 @@ +package com.lagradost.cloudstream3.utils + +import androidx.annotation.AnyThread +import androidx.annotation.MainThread +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +actual val workerDispatcher: CoroutineDispatcher = Dispatchers.Default +internal actual annotation class WorkerThread() + +@AnyThread +actual fun runOnMainThreadNative(@MainThread work: () -> Unit) { + work.invoke() +} diff --git a/library/src/webTest/kotlin/com/lagradost/cloudstream3/utils/Ignore.web.kt b/library/src/webTest/kotlin/com/lagradost/cloudstream3/utils/Ignore.web.kt new file mode 100644 index 000000000..0381a1944 --- /dev/null +++ b/library/src/webTest/kotlin/com/lagradost/cloudstream3/utils/Ignore.web.kt @@ -0,0 +1,5 @@ +package com.lagradost.cloudstream3.utils + +import kotlin.test.Ignore + +actual typealias IgnoreOnWeb = Ignore From 115be075d4b156b6795ff60bceb96986a629bd99 Mon Sep 17 00:00:00 2001 From: danzybae Date: Mon, 20 Jul 2026 01:30:04 +0700 Subject: [PATCH 09/19] Fix posterHeaders not passed in home preview carousel (#3085) --- .../com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt index e42e774b5..1020205ac 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt @@ -58,7 +58,7 @@ class HomeScrollAdapter( when (binding) { is HomeScrollViewBinding -> { - binding.homeScrollPreview.loadImage(posterUrl) + binding.homeScrollPreview.loadImage(posterUrl, item.posterHeaders) binding.homeScrollPreviewTags.apply { text = item.tags?.joinToString(" • ") ?: "" isGone = item.tags.isNullOrEmpty() @@ -79,8 +79,8 @@ class HomeScrollAdapter( binding.homeScrollPreview.setOnClickListener { view -> callback.invoke(view ?: return@setOnClickListener, position, item) } - binding.homeScrollPreview.loadImage(posterUrl) + binding.homeScrollPreview.loadImage(posterUrl, item.posterHeaders) } } } -} \ No newline at end of file +} From 2cd65aad4f7c2676beeed676d925bef314fc8880 Mon Sep 17 00:00:00 2001 From: danzybae Date: Sun, 26 Jul 2026 02:53:45 +0700 Subject: [PATCH 10/19] Fix posterHeaders not passed in TV detail menu (#3088) --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index cfbacc5d1..ce67763d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -877,7 +877,7 @@ class ResultFragmentTv : BaseFragment( resultCastText.setText(d.actorsText) resultNextAiring.setText(d.nextAiringEpisode) resultNextAiringTime.setText(d.nextAiringDate) - resultPoster.loadImage(d.posterImage) + resultPoster.loadImage(d.posterImage, headers = d.posterHeaders) var isExpanded = false resultDescription.apply { @@ -910,7 +910,7 @@ class ResultFragmentTv : BaseFragment( R.drawable.profile_bg_teal ).random() - backgroundPoster.loadImage(d.posterBackgroundImage) { + backgroundPoster.loadImage(d.posterBackgroundImage, headers = d.posterHeaders) { error { getImageFromDrawable(context ?: return@error null, error) } } From 93d3b36575568bceeaf9e77fef1afb888d88a0dd Mon Sep 17 00:00:00 2001 From: Kraptor123 <89366989+Kraptor123@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:43:11 +0300 Subject: [PATCH 11/19] feat: add settings button to provider selection in home (#3060) --- .../cloudstream3/ui/home/HomeFragment.kt | 24 ++++++++++++- .../sort_bottom_single_provider_choice.xml | 34 ++++++++++++++----- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 96c5f9f56..604695859 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -24,6 +24,8 @@ import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.plugins.PluginManager +import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.chip.Chip @@ -43,6 +45,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable +import com.lagradost.cloudstream3.plugins.Plugin import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.BaseFragment @@ -424,11 +427,30 @@ class HomeFragment : BaseFragment( .inflate(R.layout.sort_bottom_single_provider_choice, parent, false) val titleText = view.findViewById(R.id.text1) val pinIcon = view.findViewById(R.id.pinicon) + val settingsIcon = view.findViewById(R.id.action_settings) + val name = getItem(position) titleText?.text = name + val providerApi = currentValidApis[position] val isPinned = - pinnedphashset.contains(currentValidApis[position].name) + pinnedphashset.contains(providerApi.name) pinIcon.visibility = if (isPinned) View.VISIBLE else View.GONE + + val pluginInstance = providerApi.sourcePlugin?.let { PluginManager.plugins[it] } as? Plugin + val isDownloadedPluginWithSettings = pluginInstance?.openSettings != null && !isLayout(TV) + + settingsIcon.visibility = if (isDownloadedPluginWithSettings) View.VISIBLE else View.GONE + if (isDownloadedPluginWithSettings) { + settingsIcon.setOnClickListener { + try { + val activityContext = it.context.getActivity() ?: it.context + pluginInstance.openSettings?.invoke(activityContext) + } catch (e: Throwable) { + logError(e) + } + } + } + return view } } diff --git a/app/src/main/res/layout/sort_bottom_single_provider_choice.xml b/app/src/main/res/layout/sort_bottom_single_provider_choice.xml index 657e2f2b8..ea778c488 100644 --- a/app/src/main/res/layout/sort_bottom_single_provider_choice.xml +++ b/app/src/main/res/layout/sort_bottom_single_provider_choice.xml @@ -10,13 +10,31 @@ style="@style/CheckLabel" android:layout_gravity="center" tools:text="hello" /> - + android:orientation="horizontal"> + + + + + \ No newline at end of file From 1e33e4227700c90c972b1a3d9071002ad6e8e8ac Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:59:31 +0000 Subject: [PATCH 12/19] fix subtitle clipping (#3099) --- .../lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt | 3 +++ app/src/main/res/layout/player_custom_layout.xml | 2 ++ app/src/main/res/layout/player_custom_layout_tv.xml | 2 ++ app/src/main/res/layout/trailer_custom_layout.xml | 2 ++ 4 files changed, 9 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index cf892424f..bf77ef247 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -115,6 +115,9 @@ class SubtitlesFragment : BaseDialogFragment( ) } + view.clipToPadding = false + view.clipChildren = false + // we default to 25sp, this is needed as RoundedBackgroundColorSpan breaks on override sizes val size = data.fixedTextSize ?: 25.0f view.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, size) diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 04a2a1f1f..c4aa4c473 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -1076,6 +1076,8 @@ diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index 077929d87..88e4cd984 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -1116,6 +1116,8 @@ diff --git a/app/src/main/res/layout/trailer_custom_layout.xml b/app/src/main/res/layout/trailer_custom_layout.xml index 88a318874..ddaa53464 100644 --- a/app/src/main/res/layout/trailer_custom_layout.xml +++ b/app/src/main/res/layout/trailer_custom_layout.xml @@ -102,6 +102,8 @@ From 2eb05e8f354ffdd4f0833dbc365250ac7a99eeb2 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Fri, 31 Jul 2026 09:01:45 -0600 Subject: [PATCH 13/19] DataStore: deprecate toKotlinObject (#2964) 1. It can sometimes cause type erasure 2. It is less clear what it is doing 3. It is easy to replace 4. It is very rarely used, almost all app usage has already been replaced, and I haven't found any extensions actually using it but still following normal deprecation cycle as some still might be using it. --- .../lagradost/cloudstream3/utils/DataStore.kt | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 02ee69791..68f262d8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -181,11 +181,11 @@ object DataStore { } fun Context.getKey(path: String, valueType: Class): T? { - try { + return try { val json: String = getSharedPrefs().getString(path, null) ?: return null - return parseJson(json, valueType.kotlin) - } catch (e: Exception) { - return null + parseJson(json, valueType.kotlin) + } catch (_: Exception) { + null } } @@ -193,21 +193,37 @@ object DataStore { setKey(getFolderName(folder, path), value) } + @Deprecated( + message = "Use parseJson(this) directly instead.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith( + expression = "parseJson(this)", + imports = ["com.lagradost.cloudstream3.utils.AppUtils.parseJson"], + ), + ) inline fun String.toKotlinObject(): T { return parseJson(this) } + @Deprecated( + message = "Use parseJson(this) directly instead.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith( + expression = "parseJson(this)", + imports = ["com.lagradost.cloudstream3.utils.AppUtils.parseJson"], + ), + ) fun String.toKotlinObject(valueType: Class): T { return parseJson(this, valueType.kotlin) } // GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR inline fun Context.getKey(path: String, defVal: T?): T? { - try { + return try { val json: String = getSharedPrefs().getString(path, null) ?: return defVal - return json.toKotlinObject() - } catch (e: Exception) { - return null + parseJson(json) + } catch (_: Exception) { + null } } From f09435dc183ba3e6c37579f10d8d8ba6aa5ba307 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:25:00 +0000 Subject: [PATCH 14/19] Add source hiding (#2570) --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 110 ++++++++++++---- .../ui/player/PlayerGeneratorViewModel.kt | 62 ++++++++- .../source_priority/QualityDataHelper.kt | 48 ++++++- .../source_priority/SourcePriorityDialog.kt | 121 +++++++++--------- .../SourceProfileSettingsDialog.kt | 48 +++++++ .../player_select_source_priority.xml | 34 +++-- .../layout/player_select_source_priority.xml | 7 + .../layout/source_profile_settings_dialog.xml | 99 ++++++++++++++ app/src/main/res/values/strings.xml | 8 ++ 9 files changed, 426 insertions(+), 111 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt create mode 100644 app/src/main/res/layout/source_profile_settings_dialog.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 91e3ff970..683a322c4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -8,6 +8,7 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.graphics.Bitmap +import android.graphics.Typeface import android.os.Build import android.os.Bundle import android.text.Spanned @@ -79,6 +80,7 @@ import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioT import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.ui.player.source_priority.LinkSource +import com.lagradost.cloudstream3.ui.player.source_priority.ProfileSettings import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getLinkPriority import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog @@ -132,6 +134,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.io.Serializable +import java.lang.ref.WeakReference import java.util.Calendar import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -510,7 +513,7 @@ class GeneratorPlayer : FullScreenPlayer() { showDownloadProgress(DownloadEvent(0, 0, 0, null)) // uiReset() // Removed due to UX - + currentSelectedLink = link // setEpisodes(viewModel.getAllMeta() ?: emptyList()) setPlayerDimen(null) @@ -1112,21 +1115,29 @@ class GeneratorPlayer : FullScreenPlayer() { var sourceIndex = 0 var startSource = 0 - var sortedUrls = emptyList>() + // Filtered and sorted links + var currentHiddenFooter: View? = null + var filteredLinks: List = emptyList() fun refreshLinks(qualityProfile: Int) { - sortedUrls = viewModel.state.sortLinks(qualityProfile) - if (sortedUrls.isEmpty()) { + val currentLinkUsed = currentSelectedLink + // Always display current linkFooter + val sortedLinks = viewModel.state.sortLinks(qualityProfile) + + filteredLinks = sortedLinks.filter { it.shouldUseLink || it.link == currentLinkUsed } + + if (sortedLinks.isEmpty()) { sourceDialog.findViewById(R.id.sort_sources_holder)?.isGone = true } else { - startSource = sortedUrls.indexOf(currentSelectedLink) + startSource = filteredLinks.indexOfFirst { it.link == currentLinkUsed } sourceIndex = startSource val sourcesArrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) - sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) -> + sourcesArrayAdapter.addAll(filteredLinks.map { displayLink -> + val (link, uri) = displayLink.link val name = link?.name ?: uri?.name ?: "NULL" "$name ${Qualities.getStringByInt(link?.quality)}" }) @@ -1142,7 +1153,7 @@ class GeneratorPlayer : FullScreenPlayer() { } providerList.setOnItemLongClickListener { _, _, position, _ -> - sortedUrls.getOrNull(position)?.first?.url?.let { + sortedLinks.getOrNull(position)?.link?.first?.url?.let { clipboardHelper( txt(R.string.video_source), it @@ -1150,6 +1161,25 @@ class GeneratorPlayer : FullScreenPlayer() { } true } + + val hiddenLinks = sortedLinks.size - filteredLinks.size + providerList.removeFooterView(currentHiddenFooter) + + if (hiddenLinks > 0) { + val hiddenLinksFooter: TextView = layoutInflater.inflate( + R.layout.sort_bottom_footer_add_choice, null + ) as TextView + + providerList.addFooterView(hiddenLinksFooter, null, false) + currentHiddenFooter = hiddenLinksFooter + + val hiddenLinksText = + ctx.resources.getQuantityString(R.plurals.links_hidden, hiddenLinks) + .format(hiddenLinks) + hiddenLinksFooter.text = hiddenLinksText + hiddenLinksFooter.setCompoundDrawables(null, null, null, null) + hiddenLinksFooter.setTypeface(null, Typeface.ITALIC) + } } } @@ -1363,8 +1393,8 @@ class GeneratorPlayer : FullScreenPlayer() { } } if (init) { - sortedUrls.getOrNull(sourceIndex)?.let { - loadLink(it, true) + filteredLinks.getOrNull(sourceIndex)?.let { + loadLink(it.link, true) } } sourceDialog.dismissSafe(activity) @@ -1532,6 +1562,10 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun playerError(exception: Throwable) { + currentSelectedLink?.let { link -> + viewModel.modifyState { this.addError(link) } + } + val currentUrl = currentSelectedLink?.let { it.first?.url ?: it.second?.uri?.toString() } ?: "unknown" val headers = currentSelectedLink?.first?.headers?.toString() ?: "none" @@ -1554,8 +1588,22 @@ class GeneratorPlayer : FullScreenPlayer() { private fun noLinksFound() { viewModel.forceClearCache = true + val hiddenLinks = viewModel.state.sortLinks(currentQualityProfile).count { !it.shouldUseLink } + + context?.let { ctx -> + // Display that there are hidden links to the user. + if (hiddenLinks > 0) { + val noLinksString = ctx.getString(R.string.no_links_found_toast) + val hiddenString = + ctx.resources.getQuantityString(R.plurals.links_hidden, hiddenLinks) + .format(hiddenLinks) + val toastText = "$noLinksString\n($hiddenString)" + showToast(toastText, Toast.LENGTH_SHORT) + } else { + showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) + } + } - showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) activity?.popCurrentPage() } @@ -1566,7 +1614,9 @@ class GeneratorPlayer : FullScreenPlayer() { } val links = viewModel.state.sortLinks(currentQualityProfile) - if (links.isEmpty()) { + + val firstAvailableLink = links.firstOrNull { it.shouldUseLink }?.link + if (firstAvailableLink == null) { noLinksFound() return } @@ -1574,7 +1624,7 @@ class GeneratorPlayer : FullScreenPlayer() { if (!isPlayerActive.compareAndSet(false, true)) { return } - loadLink(links.first(), false) + loadLink(firstAvailableLink, false) showPlayerMetadata() } @@ -1641,25 +1691,26 @@ class GeneratorPlayer : FullScreenPlayer() { } } - override fun hasNextMirror(): Boolean { + private fun getNextLink(): DisplayLink? { val links = viewModel.state.sortLinks(currentQualityProfile) - return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size + val currentIndex = links.indexOfFirst { it.link == currentSelectedLink } + val nextPotentialLink = + links.withIndex().firstOrNull { it.index > currentIndex && it.value.shouldUseLink } + return nextPotentialLink?.value + } + + override fun hasNextMirror(): Boolean { + return getNextLink() != null } override fun nextMirror() { - val links = viewModel.state.sortLinks(currentQualityProfile) - if (links.isEmpty()) { + val nextLink = getNextLink() + if (nextLink == null) { noLinksFound() return } - val newIndex = links.indexOf(currentSelectedLink) + 1 - if (newIndex >= links.size) { - noLinksFound() - return - } - - loadLink(links[newIndex], true) + loadLink(nextLink.link, true) } override fun onDestroy() { @@ -2166,6 +2217,7 @@ class GeneratorPlayer : FullScreenPlayer() { isPlayerActive.set(false) binding?.overlayLoadingSkipButton?.isVisible = false binding?.playerLoadingOverlay?.isVisible = true + viewModel.modifyState { setError(emptyList()) } uiReset() } @@ -2297,19 +2349,23 @@ class GeneratorPlayer : FullScreenPlayer() { } } - observe(viewModel.currentLinks) { (links, instance) -> + observe(viewModel.currentLinks) { (_, instance) -> if (instance != viewModel.state.instance) return@observe // Outdated observe - val turnVisible = links.isNotEmpty() && viewModel.generator?.canSkipLoading == true + val sortedLinks = viewModel.state.sortLinks(currentQualityProfile) + val usableLinks = sortedLinks.count { link -> link.shouldUseLink } + + val turnVisible = usableLinks > 0 && viewModel.generator?.canSkipLoading == true val wasGone = binding.overlayLoadingSkipButton.isGone binding.overlayLoadingSkipButton.apply { isVisible = turnVisible - if (links.isEmpty()) { + + if (usableLinks == 0) { setText(R.string.skip_loading) } else { @SuppressLint("SetTextI18n") - text = "${context.getString(R.string.skip_loading)} (${links.size})" + text = "${context.getString(R.string.skip_loading)} (${usableLinks})" } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index e3c390d50..cb8cf8bff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -10,6 +10,8 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.ui.player.source_priority.ProfileSettings +import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getLinkPriority import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -40,12 +42,20 @@ data class GeneratorState( val id: Int?, ) +data class DisplayLink( + val link: VideoLink, + // If the link should be displayed and used by the player + val shouldUseLink: Boolean, + val priority: Int +) + /** Immutable state of all current links relevant to displaying the video */ // @MustUseReturnValues // @Immutable data class VideoState( val subtitles: PersistentSet = persistentSetOf(), val links: PersistentSet = persistentSetOf(), + val erroredLinks: PersistentSet = persistentSetOf(), val stamps: PersistentList = persistentListOf(), val loading: Resource = Resource.Loading(), val generatorState: GeneratorState? = null, @@ -56,19 +66,52 @@ data class VideoState( * * sortedBy is not exactly expensive, but each hasNextMirror does it again, so this alleviates unnecessary recomputation * */ - private val sortedLinks: ConcurrentHashMap> = ConcurrentHashMap() + private val sortedLinks: ConcurrentHashMap> = ConcurrentHashMap() + /** + * The cache is guaranteed to be up to date link-wise due to the immutable links. + * However, hideNegativeSources and hideErrorSources could be updated, which requires clearing the cache. + */ fun clearSortedLinksCache() = sortedLinks.clear() + private fun hasLinkErrored(link: VideoLink): Boolean { + return erroredLinks.any { it == link } + } + + private fun VideoLink.toDisplayLink( + qualityProfile: Int, + hideNegativeSources: Boolean, + hideErrorSources: Boolean + ): DisplayLink { + val priority = getLinkPriority(qualityProfile, this.first) + val shouldHideLink = + (hideNegativeSources && priority < 0) || (hideErrorSources && hasLinkErrored(this)) + val displayLink = DisplayLink(this, !shouldHideLink, priority) + + return displayLink + } + // Modifying sortedLinks is not considered a "visible" side effect, and rerunning it does not change the result // It is by all standards, idempotent and by extension also pure as it has no "visible" side effect /** Returns .links in the sorted order according to the qualityProfile. * Use .links if order is not needed */ @Contract(pure = true) - fun sortLinks(qualityProfile: Int): List { - return sortedLinks[qualityProfile] ?: links.sortedBy { link -> + fun sortLinks(qualityProfile: Int): List { + sortedLinks[qualityProfile]?.let { + return it + } + + val hideNegativeSources = + QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideNegativeSources) + val hideErrorSources = + QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideErrorSources) + + return links.map { link -> // negative because we want to sort highest quality first - -getLinkPriority(qualityProfile, link.first) + link.toDisplayLink(qualityProfile, hideNegativeSources, hideErrorSources) + }.sortedBy { + // negative because we want to sort highest quality first + -it.priority }.also { value -> sortedLinks[qualityProfile] = value } } @@ -113,6 +156,12 @@ data class VideoState( @JvmName("setVideoSkipStamp") @Contract(pure = true) fun set(items: Collection): VideoState = copy(stamps = items.toPersistentList()) + + @Contract(pure = true) + fun addError(item: VideoLink): VideoState = copy(erroredLinks = erroredLinks.add(item)) + + @Contract(pure = true) + fun setError(items: Collection): VideoState = copy(erroredLinks = items.toPersistentSet()) } data class VideoLive( @@ -141,9 +190,8 @@ class PlayerGeneratorViewModel : ViewModel() { var state = VideoState(instance = 0) private set - private val _currentLinks = - MutableLiveData>>>(null) - val currentLinks: LiveData>>> = _currentLinks + private val _currentLinks = MutableLiveData>>(null) + val currentLinks: LiveData>> = _currentLinks private val _currentSubtitles = MutableLiveData>>(null) val currentSubtitles: LiveData>> = _currentSubtitles diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt index 15d25148c..d8d47bd10 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt @@ -12,12 +12,16 @@ import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities +import java.util.EnumMap +import java.util.concurrent.ConcurrentHashMap +import kotlin.also import kotlin.math.abs object QualityDataHelper { private const val VIDEO_SOURCE_PRIORITY = "video_source_priority" private const val VIDEO_PROFILE_NAME = "video_profile_name" private const val VIDEO_QUALITY_PRIORITY = "video_quality_priority" + const val VIDEO_PROFILE_SETTINGS = "video_profile_settings" // Old key only supporting one type per profile @Deprecated("Changed to support multiple types per profile") @@ -53,13 +57,21 @@ object QualityDataHelper { val types: Set ) + + // Map profile and name to priority + val sourcePriorityCache: ConcurrentHashMap> = ConcurrentHashMap() + fun getSourcePriority(profile: Int, name: String?): Int { if (name == null) return DEFAULT_SOURCE_PRIORITY - return getKey( + + return sourcePriorityCache[profile]?.get(name) ?: (getKey( "$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile", name, DEFAULT_SOURCE_PRIORITY - ) ?: DEFAULT_SOURCE_PRIORITY + ) ?: DEFAULT_SOURCE_PRIORITY).also { + sourcePriorityCache.getOrPut(profile) { hashMapOf() } + sourcePriorityCache[profile]?.set(name, it) + } } fun getAllSourcePriorityNames(profile: Int): List { @@ -77,6 +89,8 @@ object QualityDataHelper { } else { setKey(folder, name, priority) } + + sourcePriorityCache[profile]?.set(name, priority) } fun setProfileName(profile: Int, name: String?) { @@ -93,12 +107,17 @@ object QualityDataHelper { ?: txt(R.string.profile_number, profile) } + // Map profile and quality to priority + val qualityPriorityCache: ConcurrentHashMap> = ConcurrentHashMap() fun getQualityPriority(profile: Int, quality: Qualities): Int { - return getKey( + return qualityPriorityCache[profile]?.get(quality) ?: (getKey( "$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile", quality.value.toString(), quality.defaultPriority - ) ?: quality.defaultPriority + )?.also { + qualityPriorityCache.getOrPut(profile) { EnumMap(Qualities::class.java) } + qualityPriorityCache[profile]?.set(quality, it) + }) ?: quality.defaultPriority } fun setQualityPriority(profile: Int, quality: Qualities, priority: Int) { @@ -107,8 +126,24 @@ object QualityDataHelper { quality.value.toString(), priority ) + qualityPriorityCache[profile]?.set(quality, priority) } + fun setProfileSetting(profile: Int, setting: ProfileSettings, value: T) { + val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile" + // Prevent unnecessary keys + if (value == setting.defaultValue) { + removeKey(folder, setting.key) + } else { + setKey(folder, setting.key, value) + } + } + + inline fun getProfileSetting(profile: Int, setting: ProfileSettings): T { + val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile" + val value = getKey(folder, setting.key) + return value ?: setting.defaultValue + } @Suppress("DEPRECATION") fun getQualityProfileTypes(profile: Int): Set { @@ -224,3 +259,8 @@ object QualityDataHelper { return Qualities.entries.minBy { abs(it.value - target) } } } + +sealed class ProfileSettings(val key: String, val defaultValue: T) { + object HideErrorSources : ProfileSettings("hide_error_sources", false) + object HideNegativeSources : ProfileSettings("hide_negative_sources", false) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt index c8ac96ebb..604ec3149 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding class SourcePriorityDialog( val ctx: Context, - @StyleRes themeRes: Int, + @StyleRes val themeRes: Int, val links: List, private val profile: QualityDataHelper.QualityProfile, /** @@ -28,76 +28,79 @@ class SourcePriorityDialog( PlayerSelectSourcePriorityBinding.inflate(LayoutInflater.from(ctx), null, false) setContentView(binding.root) fixSystemBarsPadding(binding.root) - val sourcesRecyclerView = binding.sortSources - val qualitiesRecyclerView = binding.sortQualities - val profileText = binding.profileTextEditable - val saveBtt = binding.saveBtt - val exitBtt = binding.closeBtt - val helpBtt = binding.helpBtt - profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context)) - profileText.hint = txt(R.string.profile_number, profile.id).asString(context) + binding.apply { + profileTextEditable.setText( + QualityDataHelper.getProfileName(profile.id).asString(context) + ) + profileTextEditable.hint = txt(R.string.profile_number, profile.id).asString(context) - sourcesRecyclerView.adapter = PriorityAdapter( - ).apply { - submitList(links.map { link -> - SourcePriority( - null, - link.source, - QualityDataHelper.getSourcePriority(profile.id, link.source) - ) - }.distinctBy { it.name }.sortedBy { -it.priority }) - } + sortSources.adapter = PriorityAdapter( + ).apply { + val sortedLinks = links.map { link -> + SourcePriority( + null, + link.source, + QualityDataHelper.getSourcePriority(profile.id, link.source) + ) + }.distinctBy { it.name }.sortedBy { -it.priority } - qualitiesRecyclerView.adapter = PriorityAdapter( - ).apply { - submitList(Qualities.entries.mapNotNull { - SourcePriority( - it, - Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, - QualityDataHelper.getQualityPriority(profile.id, it) - ) - }.sortedBy { -it.priority }) - } - - @Suppress("UNCHECKED_CAST") // We know the types - saveBtt.setOnClickListener { - val qualityAdapter = qualitiesRecyclerView.adapter as? PriorityAdapter - val sourcesAdapter = sourcesRecyclerView.adapter as? PriorityAdapter - - val qualities = qualityAdapter?.immutableCurrentList ?: emptyList() - val sources = sourcesAdapter?.immutableCurrentList ?: emptyList() - - qualities.forEach { - QualityDataHelper.setQualityPriority(profile.id, it.data, it.priority) + submitList(sortedLinks) } - sources.forEach { - QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority) + sortQualities.adapter = PriorityAdapter( + ).apply { + submitList(Qualities.entries.mapNotNull { + SourcePriority( + it, + Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, + QualityDataHelper.getQualityPriority(profile.id, it) + ) + }.sortedBy { -it.priority }) } - qualityAdapter?.submitList(qualities.sortedBy { -it.priority }) - sourcesAdapter?.submitList(sources.sortedBy { -it.priority }) + @Suppress("UNCHECKED_CAST") // We know the types + saveBtt.setOnClickListener { + val qualityAdapter = sortQualities.adapter as? PriorityAdapter + val sourcesAdapter = sortSources.adapter as? PriorityAdapter - val savedProfileName = profileText.text.toString() - if (savedProfileName.isBlank()) { - QualityDataHelper.setProfileName(profile.id, null) - } else { - QualityDataHelper.setProfileName(profile.id, savedProfileName) + val qualities = qualityAdapter?.immutableCurrentList ?: emptyList() + val sources = sourcesAdapter?.immutableCurrentList ?: emptyList() + + qualities.forEach { + QualityDataHelper.setQualityPriority(profile.id, it.data, it.priority) + } + + sources.forEach { + QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority) + } + + qualityAdapter?.submitList(qualities.sortedBy { -it.priority }) + sourcesAdapter?.submitList(sources.sortedBy { -it.priority }) + + val savedProfileName = profileTextEditable.text.toString() + if (savedProfileName.isBlank()) { + QualityDataHelper.setProfileName(profile.id, null) + } else { + QualityDataHelper.setProfileName(profile.id, savedProfileName) + } + updatedCallback.invoke() } - updatedCallback.invoke() - } - exitBtt.setOnClickListener { - this.dismissSafe() - } + closeBtt.setOnClickListener { + dismissSafe() + } - helpBtt.setOnClickListener { - AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { - setMessage(R.string.quality_profile_help) - }.show() - } + helpBtt.setOnClickListener { + AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { + setMessage(R.string.quality_profile_help) + }.show() + } + settingsBtt.setOnClickListener { + SourceProfileSettingsDialog(ctx, themeRes, profile.id).show() + } + } super.show() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt new file mode 100644 index 000000000..c26955b5e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt @@ -0,0 +1,48 @@ +package com.lagradost.cloudstream3.ui.player.source_priority + +import android.app.Dialog +import android.content.Context +import android.view.LayoutInflater +import androidx.annotation.StyleRes +import com.lagradost.cloudstream3.databinding.SourceProfileSettingsDialogBinding +import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding + +class SourceProfileSettingsDialog( + val ctx: Context, + @StyleRes themeRes: Int, + val profile: Int +) : Dialog(ctx, themeRes) { + override fun show() { + val binding = + SourceProfileSettingsDialogBinding.inflate(LayoutInflater.from(ctx), null, false) + setContentView(binding.root) + fixSystemBarsPadding(binding.root) + + binding.apply { + var hideErrorSources = QualityDataHelper.getProfileSetting(profile, ProfileSettings.HideErrorSources) + var hideNegativeSources = QualityDataHelper.getProfileSetting(profile, ProfileSettings.HideNegativeSources) + + profileHideErrorSources.isChecked = hideErrorSources + profileHideErrorSources.setOnCheckedChangeListener { _, bool -> + hideErrorSources = bool + } + + profileHideNegativeSources.isChecked = hideNegativeSources + profileHideNegativeSources.setOnCheckedChangeListener { _, bool -> + hideNegativeSources = bool + } + + applyBtt.setOnClickListener { + QualityDataHelper.setProfileSetting(profile, ProfileSettings.HideErrorSources, hideErrorSources) + QualityDataHelper.setProfileSetting(profile, ProfileSettings.HideNegativeSources, hideNegativeSources) + dismissSafe() + } + + cancelBtt.setOnClickListener { + dismissSafe() + } + } + super.show() + } +} \ No newline at end of file diff --git a/app/src/main/res/layout-port/player_select_source_priority.xml b/app/src/main/res/layout-port/player_select_source_priority.xml index 2cba9c869..926227c58 100644 --- a/app/src/main/res/layout-port/player_select_source_priority.xml +++ b/app/src/main/res/layout-port/player_select_source_priority.xml @@ -1,6 +1,5 @@ - - + @@ -33,14 +32,21 @@ android:textSize="20sp" android:textStyle="bold" /> + + + android:src="@drawable/baseline_help_outline_24" /> - + + tools:ignore="LabelFor" + tools:text="@string/profile_number" /> + android:text="@string/sort_save" /> + android:text="@string/sort_close" /> diff --git a/app/src/main/res/layout/player_select_source_priority.xml b/app/src/main/res/layout/player_select_source_priority.xml index 182cd1861..0d825de77 100644 --- a/app/src/main/res/layout/player_select_source_priority.xml +++ b/app/src/main/res/layout/player_select_source_priority.xml @@ -160,6 +160,13 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31cf951cf..14780da6b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -622,6 +622,14 @@ Subscribe Unsubscribe Profile %d + Profile settings + Hide sources with a negative priority + Hide sources with errors + + %d hidden link + %d hidden links + + Wi-Fi Mobile data Set default From a2adb6caaad38465b7b1e75a2d0de35c7e95ccf2 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:35:02 +0000 Subject: [PATCH 15/19] [skip ci] Swallow exception when device does not support channels --- .../com/lagradost/cloudstream3/utils/TvChannelUtils.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TvChannelUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TvChannelUtils.kt index 4c0717b3f..b5b367262 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TvChannelUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TvChannelUtils.kt @@ -149,10 +149,12 @@ object TvChannelUtils { .setInputId(inputId) .build() - val channelUri = context.contentResolver.insert( - TvContractCompat.Channels.CONTENT_URI, - channel.toContentValues() - ) + val channelUri = runCatching { + context.contentResolver.insert( + TvContractCompat.Channels.CONTENT_URI, + channel.toContentValues() + ) + }.getOrNull() channelUri?.let { val channelId = ContentUris.parseId(it) From ff701cb220897af32b02623db333adf263b443aa Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:59:46 +0000 Subject: [PATCH 16/19] Add Belarusian, Catalan and Albanian --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 3 +++ app/src/main/res/{values-be => values-b+be}/strings.xml | 0 app/src/main/res/{values-ca => values-b+ca}/strings.xml | 0 app/src/main/res/{values-sq => values-b+sq}/strings.xml | 0 4 files changed, 3 insertions(+) rename app/src/main/res/{values-be => values-b+be}/strings.xml (100%) rename app/src/main/res/{values-ca => values-b+ca}/strings.xml (100%) rename app/src/main/res/{values-sq => values-b+sq}/strings.xml (100%) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 354424978..631b12990 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -132,6 +132,9 @@ val appLanguages = arrayListOf( Pair("日本語 (にほんご)", "ja"), Pair("正體中文(臺灣)", "zh-TW"), Pair("한국어", "ko"), + Pair("беларуская мова", "be"), + Pair("català", "ca"), + Pair("Shqip мова", "sq"), /* end language list */ ).sortedBy { it.first.lowercase(Locale.ROOT) } // ye, we go alphabetical, so ppl don't put their lang on top diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-b+be/strings.xml similarity index 100% rename from app/src/main/res/values-be/strings.xml rename to app/src/main/res/values-b+be/strings.xml diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-b+ca/strings.xml similarity index 100% rename from app/src/main/res/values-ca/strings.xml rename to app/src/main/res/values-b+ca/strings.xml diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-b+sq/strings.xml similarity index 100% rename from app/src/main/res/values-sq/strings.xml rename to app/src/main/res/values-b+sq/strings.xml From 7f750964f3929679a6674f1d13d62d082110af56 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:00:05 +0000 Subject: [PATCH 17/19] chore(locales): fix locale issues --- .../lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 6 +++--- app/src/main/res/values-b+be/strings.xml | 2 +- app/src/main/res/values-b+ca/strings.xml | 2 +- app/src/main/res/values-b+sq/strings.xml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 631b12990..b04f9a4a2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -77,6 +77,7 @@ val appLanguages = arrayListOf( Pair("Azərbaycan dili", "az"), Pair("Bahasa Indonesia", "in"), Pair("Bahasa Melayu", "ms"), + Pair("català", "ca"), Pair("Deutsch", "de"), Pair("English", "en"), Pair("Español", "es"), @@ -97,6 +98,7 @@ val appLanguages = arrayListOf( Pair("Português", "pt"), Pair("Português (Brasil)", "pt-BR"), Pair("Română", "ro"), + Pair("Shqip мова", "sq"), Pair("Slovenčina", "sk"), Pair("Soomaaliga", "so"), Pair("Svenska", "sv"), @@ -106,6 +108,7 @@ val appLanguages = arrayListOf( Pair("Wikang Filipino", "fil"), Pair("Čeština", "cs"), Pair("Ελληνικά", "el"), + Pair("беларуская мова", "be"), Pair("български", "bg"), Pair("македонски", "mk"), Pair("русский", "ru"), @@ -132,9 +135,6 @@ val appLanguages = arrayListOf( Pair("日本語 (にほんご)", "ja"), Pair("正體中文(臺灣)", "zh-TW"), Pair("한국어", "ko"), - Pair("беларуская мова", "be"), - Pair("català", "ca"), - Pair("Shqip мова", "sq"), /* end language list */ ).sortedBy { it.first.lowercase(Locale.ROOT) } // ye, we go alphabetical, so ppl don't put their lang on top diff --git a/app/src/main/res/values-b+be/strings.xml b/app/src/main/res/values-b+be/strings.xml index 3d9200faf..9fbf8a8d5 100644 --- a/app/src/main/res/values-b+be/strings.xml +++ b/app/src/main/res/values-b+be/strings.xml @@ -125,7 +125,7 @@ Працягнуць прагляд Выдаліць Больш інфармацыі - \@string/home_play + @string/home_play Для карэктнай працы гэтага пастаўшчыка можа спатрэбіцца VPN Гэты пастаўшчык — Torrent, рэкамендуецца VPN Вэб-сайт не пастаўляе метададзеных, загрузіць відэа не ўдасца, калі на сайце яго няма. diff --git a/app/src/main/res/values-b+ca/strings.xml b/app/src/main/res/values-b+ca/strings.xml index 495eb5e3c..621243c78 100644 --- a/app/src/main/res/values-b+ca/strings.xml +++ b/app/src/main/res/values-b+ca/strings.xml @@ -123,7 +123,7 @@ Continua mirant Eliminar Més info - \@string/home_play + @string/home_play Potser necessiteu una VPN per que aquest proveïdor funcioni correctament Aquest proveïdor és un Torrent, es recomana fer servir una VPN El lloc no proveeix metadades, la càrrega del video fallarà si no existeix al lloc. diff --git a/app/src/main/res/values-b+sq/strings.xml b/app/src/main/res/values-b+sq/strings.xml index a13f87bfa..f45c8d87a 100644 --- a/app/src/main/res/values-b+sq/strings.xml +++ b/app/src/main/res/values-b+sq/strings.xml @@ -127,7 +127,7 @@ Vazhdo shikimin Hiq Më shumë informacion - \@string/home_play + @string/home_play Një VPN mund të nevojitet që ky ofrues të funksionojë në rregull Ky ofrues është Torrent, rekomandohet një VPN Metadata nuk ofrohet nga kjo faqe, ngarkimi i videos do të dështojë nëse nuk ekziston në këtë faqe. From e80fa1b30dc56b6cfb39ace68c8c284b255df785 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 31 Jul 2026 19:00:10 +0200 Subject: [PATCH 18/19] Translated using Weblate (Albanian) Currently translated at 100.0% (733 of 733 strings) Translated using Weblate (Polish) Currently translated at 100.0% (733 of 733 strings) Translated using Weblate (Bengali) Currently translated at 52.6% (384 of 729 strings) Translated using Weblate (English) Currently translated at 100.0% (729 of 729 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Update translation files Updated by "Remove blank strings" hook in Weblate. Translated using Weblate (Bengali) Currently translated at 51.0% (372 of 729 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Translated using Weblate (Dutch) Currently translated at 92.4% (674 of 729 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (729 of 729 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (729 of 729 strings) Translated using Weblate (Bengali) Currently translated at 75.0% (3 of 4 strings) Translated using Weblate (Bengali) Currently translated at 49.9% (364 of 729 strings) Co-authored-by: 007 Co-authored-by: Aishik Mukherjee Co-authored-by: Hosted Weblate Co-authored-by: Man Co-authored-by: Matthaiks Co-authored-by: caviaz Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sq/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/bn/ Translation: Cloudstream/App Translation: Cloudstream/Fastlane --- app/src/main/res/values-b+bn/strings.xml | 49 ++++++++++++++++--- app/src/main/res/values-b+nl/strings.xml | 13 +++++ app/src/main/res/values-b+pl/strings.xml | 9 ++++ app/src/main/res/values-b+sq/strings.xml | 7 +++ app/src/main/res/values-b+vi/strings.xml | 10 ++-- app/src/main/res/values/strings.xml | 1 - fastlane/metadata/android/bn/changelogs/2.txt | 1 + .../metadata/android/bn/full_description.txt | 7 +++ .../metadata/android/bn/short_description.txt | 1 + fastlane/metadata/android/bn/title.txt | 1 + 10 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 fastlane/metadata/android/bn/changelogs/2.txt create mode 100644 fastlane/metadata/android/bn/full_description.txt create mode 100644 fastlane/metadata/android/bn/short_description.txt create mode 100644 fastlane/metadata/android/bn/title.txt diff --git a/app/src/main/res/values-b+bn/strings.xml b/app/src/main/res/values-b+bn/strings.xml index e90608793..b6edbd1cd 100644 --- a/app/src/main/res/values-b+bn/strings.xml +++ b/app/src/main/res/values-b+bn/strings.xml @@ -3,7 +3,7 @@ পোস্টার ক্লাউডস্ট্রিম দিয়ে চালান হোম - অভিনয়েঃ %s + অভিনয়ে: %s %1$dদিন %2$dঘন্টা %3$dমিনিট %1$dঘন্টা %2$dমিনিট %d মিনিট @@ -22,7 +22,7 @@ খুঁজুন ডাউনলোডসমূহ সেটিংস - %1$s এপি %2$d + %1$s পর্ব %2$d পর্ব %d মুক্তির তারিখ শেয়ার ব্রাউজারে খুলুন @@ -131,8 +131,8 @@ স্টোরেজ এর অনুমতি অনুপস্থিত। দয়া করে আবার চেষ্টা করুন। তথ্য ট্রেইলার প্রদর্শন করুন - কিটসু হতে পোস্টারসমূহ প্রদর্শন করুন - স্বয়ংক্রিয়ভাবে প্লাগিন এর হালনাগাদ + Kitsu থেকে পোস্টারসমূহ প্রদর্শন করুন + স্বয়ংক্রিয় প্লাগইন আপডেট স্বয়ংক্রিয়ভাবে প্লাগিনসমুহের ডাউনলোড হালনাগাদ ও ব্যাকআপ অ্যাপ এর হালনাগাদ দেখান @@ -148,9 +148,9 @@ সামনে বা পিছনের দিকে যেতে ডান বা বাম দিকে দুবার আলতো চাপুন ফাইল ডিলিট মান ডিফল্ট এ রিসেট করুন - স্টার্টআপে নতুন আপডেটের জন্য স্বয়ংক্রিয়ভাবে অনুসন্ধান করুন + স্টার্টআপে নতুন আপডেটের জন্য স্বয়ংক্রিয়ভাবে অনুসন্ধান করুন যোগ। সিনেমা - এনিমে এর ফিলার পর্ব দেখায় + অ্যানিমে ফিলার পর্ব প্রদর্শন টিভি সিরিজ লিংক পাওয়া যায়নি চা খাওয়ানো হয়েছে @@ -174,7 +174,7 @@ ডিলিট শুরু কার্টুন - কিছু ফোন নতুন প্যাকেজ ইনস্টলার সাপোর্ট করে না। যদি আপডেটগুলি ইনস্টল না হয় তবে পুরোনো পদ্ধতি ব্যবহার করে দেখুন। + Some devices do not support the new package installer. Try the legacy option if updates do not install। সাবটাইটেল নেই এই প্রোভাইডার ক্রোমকাস্ট সাপোর্ট করে না উন্নত অনুসন্ধান @@ -204,7 +204,7 @@ আমাদের তৈরি Anime দেখার অ্যাপ্লিকেশন 18+ এনিমে - অনুসন্ধান ফলাফলে নির্বাচিত ভিডিও কুয়ালিটি লুকান + অনুসন্ধানের ফলে নির্বাচিত ভিডিওর মান লুকান অ্যাপ লাইভস্ট্রিম APK ইনস্টলার @@ -354,4 +354,37 @@ স্পিচ রিকগনিশন উপলব্ধ নেই কথা বলা শুরু করুন… ডাউনলোড তালিকা + সম্পূর্ণ সিরিজ প্লে করুন + এই ভিডিওটি একটি টরেন্ট, অর্থাৎ আপনার ভিডিও অ্যাক্টিভিটি ট্র্যাক করা সম্ভব।\nএগোনোর আগে টরেন্টিং সম্পর্কে ভালোভাবে বুঝে নিন। + ডিলিট করার জন্য আইটেম সিলেক্ট করুন + বর্তমানে কোনো ডাউনলোড নেই। + বর্তমানে কোনো ডাউনলোড কিউতে নেই। + অফলাইনে দেখার জন্য উপলব্ধ + সব সিলেক্ট করুন + সব ডিসিলেক্ট করুন + লোকাল ভিডিও খুলুন + অতিরিক্ত ব্রাইটনেস + ডিসপ্লে ব্রাইটনেস ১০০% ছাড়িয়ে গেলে ব্রাইটনেস ফিল্টার চালু করুন + সার্চ সাজেশন + টাইপ করার সময় সার্চ সাজেশন দেখান + সাজেশন মুছে ফেলুন + প্লেয়ার মেটাডেটা ওভারলে প্রদর্শন + কাস্ট প্যানেল প্রদর্শন + প্রাক-মুক্তি সংস্করণ ইনস্টলেশন + প্রাক-মুক্তি সংস্করণ ইতোমধ্যে ইনস্টল রয়েছে। + প্রাক-মুক্তি সংস্করণ ইনস্টল করতে সমস্যা হয়েছে। + (%1$d | %2$s) মুছুন + সাবধান + আপনি কি নিম্নলিখিত আইটেমগুলো স্থায়ীভাবে মুছতে চান?\n\n%s + ফাইল মুছুন + আপনি কি নিশ্চিত যে আপনি %1$s-এ নিচের এপিসোডগুলো স্থায়ীভাবে মুছে ফেলতে চান?\n\n%2$s + আপনি নিচের সিরিজগুলোর সব এপিসোডও স্থায়ীভাবে মুছে ফেলবেন:\n\n%s + আপনি কি নিশ্চিত যে আপনি নিচের সিরিজটির সব এপিসোড স্থায়ীভাবে মুছে ফেলতে চান?\n\n%s + সঙ্গীত + অডিও বই + মিডিয়া + অডিও + পডকাস্ট + ভিডিও + এনকোডিং ত্রুটি diff --git a/app/src/main/res/values-b+nl/strings.xml b/app/src/main/res/values-b+nl/strings.xml index 8a2424326..feec84263 100644 --- a/app/src/main/res/values-b+nl/strings.xml +++ b/app/src/main/res/values-b+nl/strings.xml @@ -682,4 +682,17 @@ Zet torrent aan in Instellingen/Providers/Media voorkeur Herstart de app en accepteer de Stream Torrent pop-up om verder te gaan. Handmatige Update Knop + Open de app met Vingerafdruk, Face ID, PIN, Pattern en Wachtwoord. + Seekbar preview + Zet preview thumbnail aan op seekbar + Software decodering + Software decodering staat de mogelijkheid toe om de speler videobestanden af te laten spelen op jouw apparaat, maar het kan een haperige of onstabiele kijkervaring zorgen op hoge resolutie. + Volume heeft de 100% overschreden + Update Plugins + Geen plugins zijn geupdate. + Speler meldingen + De speler melding om de kijkervaring van de achtergrond de besturen + Online + Hoeveel verschillende items kunnen in parallel gedownload worden + Parallel downloads diff --git a/app/src/main/res/values-b+pl/strings.xml b/app/src/main/res/values-b+pl/strings.xml index 35367a1b2..b69146c92 100644 --- a/app/src/main/res/values-b+pl/strings.xml +++ b/app/src/main/res/values-b+pl/strings.xml @@ -742,4 +742,13 @@ Wideo Zapowiedź Na żywo + Ustawienia profilu + Ukryj źródła z negatywnym priorytetem + Ukryj źródła z błędami + + %d ukryty odnośnik + %d ukryte odnośniki + %d ukrytych odnośników + %d ukrytych odnośników + diff --git a/app/src/main/res/values-b+sq/strings.xml b/app/src/main/res/values-b+sq/strings.xml index a13f87bfa..4716d757f 100644 --- a/app/src/main/res/values-b+sq/strings.xml +++ b/app/src/main/res/values-b+sq/strings.xml @@ -735,4 +735,11 @@ %d shkarkime në radhë Live + Cilësimet e profil-it + Fshih burimet me prioritet negativ + Fshih burimet me error-e + + %d link i fshehur + %d linqe të fshehura + diff --git a/app/src/main/res/values-b+vi/strings.xml b/app/src/main/res/values-b+vi/strings.xml index 3b2b3876f..53679c05c 100644 --- a/app/src/main/res/values-b+vi/strings.xml +++ b/app/src/main/res/values-b+vi/strings.xml @@ -36,7 +36,7 @@ Tập tiếp theo Thể loại Chia sẻ - Mở bằng trình duyệt + Phát bằng Trình duyệt Bỏ tải Đang tải… Đang xem @@ -255,13 +255,13 @@ Chất lượng xem ưu tiên (WiFi) Số ký tự tối đa tiêu đề trình phát Hiển thị thông tin trình phát - Kích thước bộ nhớ đệm video - Thời lượng bộ nhớ đệm + Kích thước bộ đệm video + Thời lượng bộ đệm video Bộ nhớ đệm video trên thiết bị Xoá bộ nhớ đệm hình ảnh và video Sẽ gây lỗi nếu đặt quá cao trên thiết bị có bộ nhớ thấp như Android TV. - Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng lưu trữ thấp như Android TV. - DNS over HTTPS + Sẽ gây lỗi nếu đặt quá cao trên thiết bị có dung lượng lưu trữ thấp như Android TV. + DNS qua HTTPS Rất hữu ích để bỏ chặn ISP Bản sao trang web Xoá trang web diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14780da6b..4a918231f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -791,5 +791,4 @@ %d downloads queued Live - diff --git a/fastlane/metadata/android/bn/changelogs/2.txt b/fastlane/metadata/android/bn/changelogs/2.txt new file mode 100644 index 000000000..91032cb8a --- /dev/null +++ b/fastlane/metadata/android/bn/changelogs/2.txt @@ -0,0 +1 @@ +- চেঞ্জলগ যোগ করা হয়েছে! diff --git a/fastlane/metadata/android/bn/full_description.txt b/fastlane/metadata/android/bn/full_description.txt new file mode 100644 index 000000000..e1866f54f --- /dev/null +++ b/fastlane/metadata/android/bn/full_description.txt @@ -0,0 +1,7 @@ +CloudStream-3 দিয়ে সিনেমা, টিভি-সিরিজ ও অ্যানিমে স্ট্রিম ও ডাউনলোড করা যায়। + +অ্যাপে কোনো বিজ্ঞাপন ও অ্যানালিটিক্স নেই এবং +এটি একাধিক ট্রেলার ও মুভি সাইট সাপোর্ট করে, আরও আছে যেমন— +বুকমার্ক +সাবটাইটেল ডাউনলোড +ক্রোমকাস্ট সাপোর্ট diff --git a/fastlane/metadata/android/bn/short_description.txt b/fastlane/metadata/android/bn/short_description.txt new file mode 100644 index 000000000..3d62da485 --- /dev/null +++ b/fastlane/metadata/android/bn/short_description.txt @@ -0,0 +1 @@ +সিনেমা, টিভি সিরিজ ও অ্যানিমে স্ট্রিম ও ডাউনলোড করুন। diff --git a/fastlane/metadata/android/bn/title.txt b/fastlane/metadata/android/bn/title.txt new file mode 100644 index 000000000..dde89d58f --- /dev/null +++ b/fastlane/metadata/android/bn/title.txt @@ -0,0 +1 @@ +CloudStream From caf0db3dc13bd75a496d4a94152bb8f22f8fdb1e Mon Sep 17 00:00:00 2001 From: Jelle Thijs Date: Sat, 1 Aug 2026 03:14:02 +0200 Subject: [PATCH 19/19] Feat: Add optional clock to TV player --- .../cloudstream3/ui/home/HomeFragment.kt | 28 +++++++++++++++++++ .../ui/player/FullScreenPlayer.kt | 3 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 8 ++++++ .../cloudstream3/ui/settings/SettingsUI.kt | 2 ++ .../res/drawable/ic_baseline_clock_24.xml | 9 ++++++ app/src/main/res/layout/fragment_home.xml | 6 ++++ app/src/main/res/layout/fragment_home_tv.xml | 17 +++++++++++ .../main/res/layout/player_custom_layout.xml | 9 ++++++ .../res/layout/player_custom_layout_tv.xml | 16 +++++++++++ .../main/res/layout/trailer_custom_layout.xml | 6 ++++ .../res/values/donottranslate-strings.xml | 1 + app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/settings_ui.xml | 6 ++++ 13 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/ic_baseline_clock_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 604695859..0e52baff6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -701,6 +701,23 @@ class HomeFragment : BaseFragment( homeViewModel.queryTextSubmit("") } + // Load value for toggling Tv layout real time clock. Hide by default at startup + // set visibility first, to apply a scroll effect later + context?.let { + if (isLayout(TV)) { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(it) + val toggleClock = + settingsManager.getBoolean( + getString(R.string.tv_layout_clock_key), + false + ) + binding.homeClock.isVisible = toggleClock + } else { + binding.homeClock.isVisible = false + } + } + + homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (isLayout(PHONE)) { @@ -740,6 +757,17 @@ class HomeFragment : BaseFragment( view.getLocationInWindow(rect) scrollParent.isVisible = true scrollParent.translationY = rect[1].toFloat() - 60.toPx + + // Move the TV layout real time clock out of the way too + // We check if we have the correct layout and if the clock is enabled + if(isLayout(TV) && binding.homeClock.isVisible) { + val scrollParent = binding.homeClock + + val rect = IntArray(2) + view.getLocationInWindow(rect) + scrollParent.isVisible = true + scrollParent.translationY = rect[1].toFloat() - 60.toPx + } } } super.onScrolled(recyclerView, dx, dy) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 20c37dcf4..d90b6043f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -307,6 +307,7 @@ open class FullScreenPlayer : AbstractPlayerFragment( playerVideoTitleRez, playerVideoInfo, playerGoBackHolder, + playerVideoClock, ).forEach { it.animateY(titleMove) } @@ -772,7 +773,7 @@ open class FullScreenPlayer : AbstractPlayerFragment( val showPlayerEpisodes = !isGone && isThereEpisodes() playerEpisodesButtonRoot.isVisible = showPlayerEpisodes playerEpisodesButton.isVisible = showPlayerEpisodes - playerVideoTitleHolder.isGone = togglePlayerTitleGone + playerVideoTitleHolder.isGone = togglePlayerTitleGone || playerVideoTitle.text.isBlank() playerVideoTitleRez.isGone = isGone || playerVideoTitleRez.text.isBlank() playerEpisodeFiller.isGone = isGone playerCenterMenu.isGone = isGone diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 683a322c4..4495a5602 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -2271,6 +2271,14 @@ class GeneratorPlayer : FullScreenPlayer() { fromTagToEnglishLanguageName(it)?.lowercase() ?: return@mapNotNull null } ?: listOf() } + + // Set up TV clock visibility + if (isLayout(TV)) { + val showTvClock = settingsManager.getBoolean(ctx.getString(R.string.tv_layout_clock_key), false) + playerBinding?.playerVideoClock?.isVisible = showTvClock + } else { + playerBinding?.playerVideoClock?.isVisible = false + } } unwrapBundle(savedInstanceState) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt index f4c522bf9..5f86a3a5a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt @@ -228,6 +228,8 @@ class SettingsUI : BasePreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + getPref(R.string.tv_layout_clock_key)?.hideOn(PHONE or EMULATOR) + getPref(R.string.confirm_exit_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.confirm_exit) val prefValues = resources.getIntArray(R.array.confirm_exit_values) diff --git a/app/src/main/res/drawable/ic_baseline_clock_24.xml b/app/src/main/res/drawable/ic_baseline_clock_24.xml new file mode 100644 index 000000000..3436c6208 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_clock_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 99a764dee..8d78c01b3 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -227,6 +227,12 @@ tools:listitem="@layout/homepage_parent" tools:visibility="gone" /> + + + + + + + diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index 88e4cd984..c6445d80f 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -352,6 +352,22 @@ android:layout_marginEnd="32dp" android:orientation="vertical"> + + + + opensubtitles_key subdl_key animeskip_key + tv_layout_clock_key pref_category_security_key pref_category_gestures_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4a918231f..eed41ce81 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -374,6 +374,8 @@ Looks Features General + Show real time clock + Show a real time clock at the top of the screen. Applies to the homepage and player Random Button Show random button on Homepage and Library Extension languages diff --git a/app/src/main/res/xml/settings_ui.xml b/app/src/main/res/xml/settings_ui.xml index 1b516ffa3..774724f99 100644 --- a/app/src/main/res/xml/settings_ui.xml +++ b/app/src/main/res/xml/settings_ui.xml @@ -89,6 +89,12 @@ android:icon="@drawable/metadata_overlay_icon" android:key="@string/show_player_metadata_key" android:title="@string/show_player_metadata_overlay" /> +