From 6ccbd574d6500b2d059f87c1ffdce8d17d75ed38 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 22 Jun 2026 23:28:10 +0000 Subject: [PATCH 01/99] Fix subtitleoffset --- .../cloudstream3/ui/player/FullScreenPlayer.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 4ba933e13..9d8ac09bf 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 @@ -531,9 +531,22 @@ open class FullScreenPlayer : AbstractPlayerFragment( var currentOffset = subtitleDelay binding.apply { + var subtitleAdapter: SubtitleOffsetItemAdapter? = null + subtitleOffsetInput.doOnTextChanged { text, _, _, _ -> text?.toString()?.toLongOrNull()?.let { time -> currentOffset = time + + // Scroll to the first active subtitle + val playerPosition = player.getPosition() ?: 0 + val totalPosition = playerPosition - currentOffset + subtitleAdapter?.updateTime(totalPosition) + + subtitleAdapter?.getLatestActiveItem(totalPosition) + ?.let { subtitlePos -> + subtitleOffsetRecyclerview.scrollToPosition(subtitlePos) + } + val str = when { time > 0L -> { txt(R.string.subtitle_offset_extra_hint_later_format, time) @@ -559,7 +572,7 @@ open class FullScreenPlayer : AbstractPlayerFragment( noSubtitlesLoadedNotice.isVisible = subtitles.isEmpty() val initialSubtitlePosition = (player.getPosition() ?: 0) - currentOffset - val subtitleAdapter = + subtitleAdapter = SubtitleOffsetItemAdapter(initialSubtitlePosition) { subtitleCue -> val playerPosition = player.getPosition() ?: 0 subtitleOffsetInput.text = Editable.Factory.getInstance() From be8e9437e97694ba4b4c393f43858f4b64e3e9fe Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Tue, 23 Jun 2026 01:32:29 +0200 Subject: [PATCH 02/99] Fix regression in subtitle offset animation (#2942) --- .../cloudstream3/ui/player/FullScreenPlayer.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 4ba933e13..9d8ac09bf 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 @@ -531,9 +531,22 @@ open class FullScreenPlayer : AbstractPlayerFragment( var currentOffset = subtitleDelay binding.apply { + var subtitleAdapter: SubtitleOffsetItemAdapter? = null + subtitleOffsetInput.doOnTextChanged { text, _, _, _ -> text?.toString()?.toLongOrNull()?.let { time -> currentOffset = time + + // Scroll to the first active subtitle + val playerPosition = player.getPosition() ?: 0 + val totalPosition = playerPosition - currentOffset + subtitleAdapter?.updateTime(totalPosition) + + subtitleAdapter?.getLatestActiveItem(totalPosition) + ?.let { subtitlePos -> + subtitleOffsetRecyclerview.scrollToPosition(subtitlePos) + } + val str = when { time > 0L -> { txt(R.string.subtitle_offset_extra_hint_later_format, time) @@ -559,7 +572,7 @@ open class FullScreenPlayer : AbstractPlayerFragment( noSubtitlesLoadedNotice.isVisible = subtitles.isEmpty() val initialSubtitlePosition = (player.getPosition() ?: 0) - currentOffset - val subtitleAdapter = + subtitleAdapter = SubtitleOffsetItemAdapter(initialSubtitlePosition) { subtitleCue -> val playerPosition = player.getPosition() ?: 0 subtitleOffsetInput.text = Editable.Factory.getInstance() From 41a9f25cbe486b14df192d8a3446eb5bf71088bd Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:49:41 -0600 Subject: [PATCH 03/99] subtitles: migrate to kotlinx serialization (#2885) --- .../subtitles/ChromecastSubtitlesFragment.kt | 19 ++++--- .../ui/subtitles/SubtitlesFragment.kt | 49 +++++++++---------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index f9b1cb1fe..95ef264f8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -38,18 +38,21 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable const val CHROME_SUBTITLE_KEY = "chome_subtitle_settings" +@Serializable data class SaveChromeCaptionStyle( - @JsonProperty("fontFamily") var fontFamily: String? = null, - @JsonProperty("fontGenericFamily") var fontGenericFamily: Int? = null, - @JsonProperty("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent - @JsonProperty("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK - @JsonProperty("edgeType") var edgeType: Int = EDGE_TYPE_OUTLINE, - @JsonProperty("foregroundColor") var foregroundColor: Int = Color.WHITE, - @JsonProperty("fontScale") var fontScale: Float = 1.05f, - @JsonProperty("windowColor") var windowColor: Int = Color.TRANSPARENT, + @JsonProperty("fontFamily") @SerialName("fontFamily") var fontFamily: String? = null, + @JsonProperty("fontGenericFamily") @SerialName("fontGenericFamily") var fontGenericFamily: Int? = null, + @JsonProperty("backgroundColor") @SerialName("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent + @JsonProperty("edgeColor") @SerialName("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK + @JsonProperty("edgeType") @SerialName("edgeType") var edgeType: Int = EDGE_TYPE_OUTLINE, + @JsonProperty("foregroundColor") @SerialName("foregroundColor") var foregroundColor: Int = Color.WHITE, + @JsonProperty("fontScale") @SerialName("fontScale") var fontScale: Float = 1.05f, + @JsonProperty("windowColor") @SerialName("windowColor") var windowColor: Int = Color.TRANSPARENT, ) class ChromecastSubtitlesFragment : BaseFragment( 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 5f716cca3..860cb4fcb 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 @@ -54,40 +54,39 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import java.io.File const val SUBTITLE_KEY = "subtitle_settings" const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select" const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download" +@Serializable data class SaveCaptionStyle( - @JsonProperty("foregroundColor") var foregroundColor: Int, - @JsonProperty("backgroundColor") var backgroundColor: Int, - @JsonProperty("windowColor") var windowColor: Int, + @JsonProperty("foregroundColor") @SerialName("foregroundColor") var foregroundColor: Int, + @JsonProperty("backgroundColor") @SerialName("backgroundColor") var backgroundColor: Int, + @JsonProperty("windowColor") @SerialName("windowColor") var windowColor: Int, @OptIn(UnstableApi::class) - @JsonProperty("edgeType") var edgeType: @CaptionStyleCompat.EdgeType Int, - @JsonProperty("edgeColor") var edgeColor: Int, - @FontRes - @JsonProperty("typeface") var typeface: Int?, - @JsonProperty("typefaceFilePath") var typefaceFilePath: String?, - /**in dp**/ - @JsonProperty("elevation") var elevation: Int, - /**in sp**/ - @JsonProperty("fixedTextSize") var fixedTextSize: Float?, - @Px - @JsonProperty("edgeSize") var edgeSize: Float? = null, - @JsonProperty("removeCaptions") var removeCaptions: Boolean = false, - @JsonProperty("removeBloat") var removeBloat: Boolean = true, - /** Apply caps lock to the text **/ - @JsonProperty("upperCase") var upperCase: Boolean = false, - /** Apply bold to the text **/ - @JsonProperty("bold") var bold: Boolean = false, - /** Apply italic to the text **/ - @JsonProperty("italic") var italic: Boolean = false, - /** in px, background radius, aka how round the background (backgroundColor) on each row is **/ - @JsonProperty("backgroundRadius") var backgroundRadius: Float? = null, + @JsonProperty("edgeType") @SerialName("edgeType") var edgeType: @CaptionStyleCompat.EdgeType Int, + @JsonProperty("edgeColor") @SerialName("edgeColor") var edgeColor: Int, + @FontRes @JsonProperty("typeface") @SerialName("typeface") var typeface: Int?, + @JsonProperty("typefaceFilePath") @SerialName("typefaceFilePath") var typefaceFilePath: String?, + @JsonProperty("elevation") @SerialName("elevation") var elevation: Int, // in dp + @JsonProperty("fixedTextSize") @SerialName("fixedTextSize") var fixedTextSize: Float?, // in sp + @Px @JsonProperty("edgeSize") @SerialName("edgeSize") var edgeSize: Float? = null, + @JsonProperty("removeCaptions") @SerialName("removeCaptions") var removeCaptions: Boolean = false, + @JsonProperty("removeBloat") @SerialName("removeBloat") var removeBloat: Boolean = true, + /** Apply caps lock to the text */ + @JsonProperty("upperCase") @SerialName("upperCase") var upperCase: Boolean = false, + /** Apply bold to the text */ + @JsonProperty("bold") @SerialName("bold") var bold: Boolean = false, + /** Apply italic to the text */ + @JsonProperty("italic") @SerialName("italic") var italic: Boolean = false, + /** in px, background radius, aka how round the background (backgroundColor) on each row is */ + @JsonProperty("backgroundRadius") @SerialName("backgroundRadius") var backgroundRadius: Float? = null, /** The SSA_ALIGNMENT */ - @JsonProperty("alignment") var alignment: Int? = null, + @JsonProperty("alignment") @SerialName("alignment") var alignment: Int? = null, ) const val DEF_SUBS_ELEVATION = 20 From acff04e04f564208185898996cdd60aac9c8e635 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:02:34 -0600 Subject: [PATCH 04/99] videoskip: migrate to kotlinx serialization (#2881) --- .../cloudstream3/SerializationClassTester.kt | 4 +- .../cloudstream3/utils/videoskip/AniSkip.kt | 29 +++-- .../cloudstream3/utils/videoskip/AnimeSkip.kt | 107 ++++++++---------- .../utils/videoskip/IntroDbSkip.kt | 33 +++--- .../utils/videoskip/TheIntroDBSkip.kt | 30 +++-- 5 files changed, 96 insertions(+), 107 deletions(-) diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt index 80c7b49b0..03308d9de 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt @@ -30,7 +30,7 @@ class SerializationClassTester { println("Number of serializable classes: ${serializableClasses.size}") serializableClasses.forEach { kClass -> - val instance = Instancio.create(kClass.java) + val instance = Instancio.of(kClass.java).withMaxDepth(10).create() val jacksonJson = jacksonMapper.writeValueAsString(instance) val kotlinxJson = serializeWithKotlinx(kClass, instance) @@ -61,7 +61,7 @@ class SerializationClassTester { println("Number of serializable classes: ${serializableClasses.size}") serializableClasses.forEach { kClass -> - val instance = Instancio.create(kClass.java) + val instance = Instancio.of(kClass.java).withMaxDepth(10).create() // Convert to JSON to get example JSON object // We prefer jackson here because the app may have many jackson JSON strings in local storage val originalJson = jacksonMapper.writeValueAsString(instance) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt index 0db90afea..4bb78a1b0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt @@ -1,12 +1,14 @@ package com.lagradost.cloudstream3.utils.videoskip -import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.AnimeLoadResponse import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.ui.result.ResultEpisode +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable // taken from https://github.com/saikou-app/saikou/blob/3803f8a7a59b826ca193664d46af3a22bbc989f7/app/src/main/java/ani/saikou/others/AniSkip.kt // the following is GPLv3 code https://github.com/saikou-app/saikou/blob/main/LICENSE.md @@ -47,22 +49,25 @@ class AniSkip : SkipAPI() { } } + @Serializable data class AniSkipResponse( - @JsonSerialize val found: Boolean, - @JsonSerialize val results: List?, - @JsonSerialize val message: String?, - @JsonSerialize val statusCode: Int + @JsonProperty("found") @SerialName("found") val found: Boolean, + @JsonProperty("results") @SerialName("results") val results: List?, + @JsonProperty("message") @SerialName("message") val message: String?, + @JsonProperty("statusCode") @SerialName("statusCode") val statusCode: Int, ) + @Serializable data class Stamp( - @JsonSerialize val interval: AniSkipInterval, - @JsonSerialize val skipType: String, - @JsonSerialize val skipId: String, - @JsonSerialize val episodeLength: Double + @JsonProperty("interval") @SerialName("interval") val interval: AniSkipInterval, + @JsonProperty("skipType") @SerialName("skipType") val skipType: String, + @JsonProperty("skipId") @SerialName("skipId") val skipId: String, + @JsonProperty("episodeLength") @SerialName("episodeLength") val episodeLength: Double, ) + @Serializable data class AniSkipInterval( - @JsonSerialize val startTime: Double, - @JsonSerialize val endTime: Double + @JsonProperty("startTime") @SerialName("startTime") val startTime: Double, + @JsonProperty("endTime") @SerialName("endTime") val endTime: Double, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AnimeSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AnimeSkip.kt index f9254576b..df9d56217 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AnimeSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AnimeSkip.kt @@ -17,6 +17,8 @@ import com.lagradost.cloudstream3.syncproviders.PlainAuthRepo import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import java.math.BigInteger import java.util.concurrent.ConcurrentHashMap import java.security.MessageDigest @@ -34,58 +36,51 @@ class AnimeSkipAuth : AuthAPI() { return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') } + @Serializable data class LoginRoot( - @JsonProperty("data") - val data: LoginData, + @JsonProperty("data") @SerialName("data") val data: LoginData, ) + @Serializable data class LoginData( - @JsonProperty("login") - val login: Login, + @JsonProperty("login") @SerialName("login") val login: Login, ) + @Serializable data class Login( - @JsonProperty("authToken") - val authToken: String, - @JsonProperty("refreshToken") - val refreshToken: String, - @JsonProperty("account") - val account: Account, + @JsonProperty("authToken") @SerialName("authToken") val authToken: String, + @JsonProperty("refreshToken") @SerialName("refreshToken") val refreshToken: String, + @JsonProperty("account") @SerialName("account") val account: Account, ) + @Serializable data class ApiRoot( - @JsonProperty("data") - val data: ApiData, + @JsonProperty("data") @SerialName("data") val data: ApiData, ) + @Serializable data class ApiData( - @JsonProperty("myApiClients") - val myApiClients: List, + @JsonProperty("myApiClients") @SerialName("myApiClients") val myApiClients: List, ) + @Serializable data class MyApiClient( - @JsonProperty("id") - val id: String, + @JsonProperty("id") @SerialName("id") val id: String, ) + @Serializable data class Account( - @JsonProperty("profileUrl") - val profileUrl: String, - @JsonProperty("username") - val username: String, - @JsonProperty("email") - val email: String, + @JsonProperty("profileUrl") @SerialName("profileUrl") val profileUrl: String, + @JsonProperty("username") @SerialName("username") val username: String, + @JsonProperty("email") @SerialName("email") val email: String, ) + @Serializable data class Payload( - @JsonProperty("profileUrl") - val profileUrl: String, - @JsonProperty("username") - val username: String, - @JsonProperty("email") - val email: String, - @JsonProperty("clientId") - val clientId: String, + @JsonProperty("profileUrl") @SerialName("profileUrl") val profileUrl: String, + @JsonProperty("username") @SerialName("username") val username: String, + @JsonProperty("email") @SerialName("email") val email: String, + @JsonProperty("clientId") @SerialName("clientId") val clientId: String, ) override suspend fun user(token: AuthToken?): AuthUser? { @@ -187,52 +182,43 @@ class AnimeSkip : SkipAPI() { name?.replace(asciiRegex, "")?.lowercase() } + @Serializable data class Root( - @JsonProperty("data") - val data: Data, + @JsonProperty("data") @SerialName("data") val data: Data, ) + @Serializable data class Data( - @JsonProperty("searchShows") - val searchShows: List, + @JsonProperty("searchShows") @SerialName("searchShows") val searchShows: List, ) + @Serializable data class SearchShow( - @JsonProperty("name") - val name: String, - @JsonProperty("originalName") - val originalName: String?, - @JsonProperty("seasonCount") - val seasonCount: Long, - @JsonProperty("episodeCount") - val episodeCount: Long, - @JsonProperty("baseDuration") - val baseDuration: Double, - @JsonProperty("episodes") - val episodes: List, + @JsonProperty("name") @SerialName("name") val name: String, + @JsonProperty("originalName") @SerialName("originalName") val originalName: String?, + @JsonProperty("seasonCount") @SerialName("seasonCount") val seasonCount: Long, + @JsonProperty("episodeCount") @SerialName("episodeCount") val episodeCount: Long, + @JsonProperty("baseDuration") @SerialName("baseDuration") val baseDuration: Double, + @JsonProperty("episodes") @SerialName("episodes") val episodes: List, ) + @Serializable data class Episode( - @JsonProperty("number") - val number: String?, - @JsonProperty("absoluteNumber") - val absoluteNumber: String?, - @JsonProperty("season") - val season: String?, - @JsonProperty("timestamps") - val timestamps: List, + @JsonProperty("number") @SerialName("number") val number: String?, + @JsonProperty("absoluteNumber") @SerialName("absoluteNumber") val absoluteNumber: String?, + @JsonProperty("season") @SerialName("season") val season: String?, + @JsonProperty("timestamps") @SerialName("timestamps") val timestamps: List, ) + @Serializable data class Timestamp( - @JsonProperty("at") - val at: Double, - @JsonProperty("type") - val type: Type, + @JsonProperty("at") @SerialName("at") val at: Double, + @JsonProperty("type") @SerialName("type") val type: Type, ) + @Serializable data class Type( - @JsonProperty("name") - val name: String, + @JsonProperty("name") @SerialName("name") val name: String, ) val cache: ConcurrentHashMap = ConcurrentHashMap() @@ -367,4 +353,3 @@ class AnimeSkip : SkipAPI() { return result } } - diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt index 869515f43..75e22a15f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt @@ -6,6 +6,8 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.ui.result.ResultEpisode +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable class IntroDbSkip : SkipAPI() { override val name = "IntroDb" @@ -55,23 +57,24 @@ class IntroDbSkip : SkipAPI() { ) } - + @Serializable data class IntroDbResponse( - @JsonProperty("imdb_id") val imdbId: String?, - val season: Int?, - val episode: Int?, - val intro: Segment?, - val recap: Segment?, - val outro: Segment?, + @JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String?, + @JsonProperty("season") @SerialName("season") val season: Int?, + @JsonProperty("episode") @SerialName("episode") val episode: Int?, + @JsonProperty("intro") @SerialName("intro") val intro: Segment?, + @JsonProperty("recap") @SerialName("recap") val recap: Segment?, + @JsonProperty("outro") @SerialName("outro") val outro: Segment?, ) + @Serializable data class Segment( - @JsonProperty("start_sec") val startSec: Double?, - @JsonProperty("end_sec") val endSec: Double?, - @JsonProperty("start_ms") val startMs: Long?, - @JsonProperty("end_ms") val endMs: Long?, - val confidence: Double?, - @JsonProperty("submission_count") val submissionCount: Int?, - @JsonProperty("updated_at") val updatedAt: String?, + @JsonProperty("start_sec") @SerialName("start_sec") val startSec: Double?, + @JsonProperty("end_sec") @SerialName("end_sec") val endSec: Double?, + @JsonProperty("start_ms") @SerialName("start_ms") val startMs: Long?, + @JsonProperty("end_ms") @SerialName("end_ms") val endMs: Long?, + @JsonProperty("confidence") @SerialName("confidence") val confidence: Double?, + @JsonProperty("submission_count") @SerialName("submission_count") val submissionCount: Int?, + @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String?, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt index cc2661cb0..3fd050f1a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt @@ -8,6 +8,8 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.app +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable /** https://theintrodb.org/docs */ class TheIntroDBSkip : SkipAPI() { @@ -52,25 +54,19 @@ class TheIntroDBSkip : SkipAPI() { }.flatten() } + @Serializable data class Root( - @JsonProperty("tmdb_id") - val tmdbId: Long, - @JsonProperty("type") - val type: String, - @JsonProperty("intro") - val intro: List = emptyList(), - @JsonProperty("recap") - val recap: List = emptyList(), - @JsonProperty("credits") - val credits: List = emptyList(), - @JsonProperty("preview") - val preview: List = emptyList(), + @JsonProperty("tmdb_id") @SerialName("tmdb_id") val tmdbId: Long, + @JsonProperty("type") @SerialName("type") val type: String, + @JsonProperty("intro") @SerialName("intro") val intro: List = emptyList(), + @JsonProperty("recap") @SerialName("recap") val recap: List = emptyList(), + @JsonProperty("credits") @SerialName("credits") val credits: List = emptyList(), + @JsonProperty("preview") @SerialName("preview") val preview: List = emptyList(), ) + @Serializable data class Stamp( - @JsonProperty("start_ms") - val startMs: Long?, - @JsonProperty("end_ms") - val endMs: Long?, + @JsonProperty("start_ms") @SerialName("start_ms") val startMs: Long?, + @JsonProperty("end_ms") @SerialName("end_ms") val endMs: Long?, ) -} \ No newline at end of file +} From c1fc6c1300846d6b9a2f88c12d164842136248d5 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:05:53 -0600 Subject: [PATCH 05/99] SearchSuggestionApi: migrate to kotlinx serialization (#2879) --- .../ui/search/SearchSuggestionApi.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt index 8dbd78178..a22c2fcef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt @@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.nicehttp.NiceResponse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable /** * API for fetching search suggestions from external sources. @@ -13,16 +15,18 @@ object SearchSuggestionApi { private const val TMDB_API_URL = "https://api.themoviedb.org/3/search/multi" private const val TMDB_API_KEY = "e6333b32409e02a4a6eba6fb7ff866bb" + @Serializable data class TmdbSearchResult( - @JsonProperty("results") val results: List? + @JsonProperty("results") @SerialName("results") val results: List?, ) + @Serializable data class TmdbSearchItem( - @JsonProperty("media_type") val mediaType: String?, - @JsonProperty("title") val title: String?, - @JsonProperty("name") val name: String?, - @JsonProperty("original_title") val originalTitle: String?, - @JsonProperty("original_name") val originalName: String? + @JsonProperty("media_type") @SerialName("media_type") val mediaType: String?, + @JsonProperty("title") @SerialName("title") val title: String?, + @JsonProperty("name") @SerialName("name") val name: String?, + @JsonProperty("original_title") @SerialName("original_title") val originalTitle: String?, + @JsonProperty("original_name") @SerialName("original_name") val originalName: String?, ) /** @@ -57,7 +61,7 @@ object SearchSuggestionApi { * Parses the TMDB search response and extracts movie/TV show titles. * Filters to only include movies, TV shows, and anime. */ - private fun parseSuggestions(response: NiceResponse): List { + private suspend fun parseSuggestions(response: NiceResponse): List { return try { val parsed = response.parsed() parsed.results From dc13c28b7f1aaca0b6b7bc5705f9cff27fa7d0ce Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 23 Jun 2026 02:07:24 +0200 Subject: [PATCH 06/99] Translated using Weblate (Vietnamese) (#2910) Currently translated at 100.0% (729 of 729 strings) Translated using Weblate (Malayalam) Currently translated at 37.8% (276 of 729 strings) Translated using Weblate (Japanese) Currently translated at 99.5% (726 of 729 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Translated using Weblate (Korean) Currently translated at 100.0% (729 of 729 strings) Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ml/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/ Translation: Cloudstream/App Co-authored-by: Man Co-authored-by: Nori Masomi (Mystical) Co-authored-by: hou1234 Co-authored-by: trodiz --- app/src/main/res/values-b+ja/strings.xml | 1 + app/src/main/res/values-b+ko/strings.xml | 6 +-- app/src/main/res/values-b+ml/strings.xml | 52 ++++++++++++++++++++++-- app/src/main/res/values-b+vi/strings.xml | 2 +- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/values-b+ja/strings.xml b/app/src/main/res/values-b+ja/strings.xml index 3de9a971a..2b611ef69 100644 --- a/app/src/main/res/values-b+ja/strings.xml +++ b/app/src/main/res/values-b+ja/strings.xml @@ -729,4 +729,5 @@ ソースの優先順位 プレイヤーでのビデオソースの並び順を設定します + プレイヤーメタデータオーバーレイを表示 diff --git a/app/src/main/res/values-b+ko/strings.xml b/app/src/main/res/values-b+ko/strings.xml index 14d327372..4feeabe33 100644 --- a/app/src/main/res/values-b+ko/strings.xml +++ b/app/src/main/res/values-b+ko/strings.xml @@ -56,8 +56,8 @@ 재생 속도 글자 색상 윤곽선 색상 - 배경 색상 - 배경 색상 + 자막 배경 색상 + 자막 창 색상 윤곽선 유형 자막 높이 폰트 @@ -715,7 +715,7 @@ 입력하는 동안 검색어 제안 표시 출연진 정보 표시 - 배경 테두리 곡률 + 자막 배경 테두리 곡률 공급자 새로고침 검색어 제안 제안 삭제 diff --git a/app/src/main/res/values-b+ml/strings.xml b/app/src/main/res/values-b+ml/strings.xml index c2b25c5ee..577e2ac73 100644 --- a/app/src/main/res/values-b+ml/strings.xml +++ b/app/src/main/res/values-b+ml/strings.xml @@ -83,9 +83,9 @@ പ്ലേയർ സബ്‌ടൈറ്റിലുകളുടെ സെറ്റിങ്‌സ് - വീഡിയോപ്ലേയറിൽ സമയം നിയന്ത്രിക്കാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ സ്വൈപ്പുചെയ്യുക + വീഡിയോയിൽ ഇരുവശങ്ങളിലേക്കും സ്വൈപ്പ് ചെയ്താൽ സ്ഥാനത്തെ നിയന്ത്രിക്കാം - തെളിച്ചം അല്ലെങ്കിൽ വോളിയം മാറ്റാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ സ്വൈപ്പുചെയ്യുക + തെളിച്ചം അല്ലെങ്കിൽ വോളിയം മാറ്റാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ നീക്കുക മുന്നോട്ട് അല്ലെങ്കിൽ പിന്നിലേക്ക് നീങ്ങാൻ വലത്തോട്ടോ ഇടത്തോട്ടോ രണ്ടുതവണ ടാപ്പുചെയ്യുക തിരയുക @@ -186,7 +186,7 @@ ലോഡിംഗ്… ബ്രൗസർ വീണ്ടും കാണുക - സ്ട്രീം + നെറ്റ്‌വർക്ക് സ്ട്രീം %s ൽ ഫോൻ്റ്‌സ് വെച്ചു കൊണ്ട് ഇംപോർട്ട് ചെയ്യുക പ്രശ്‌നമുണ്ടാക്കുന്ന ഒന്ന് കണ്ടെത്താൻ നിങ്ങളെ സഹായിക്കുന്നതിന് ഒരു ക്രാഷ് കാരണം എല്ലാ വിപുലീകരണങ്ങളും ഓഫാക്കി. പൊതു പട്ടിക @@ -267,4 +267,50 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം + സീസൺ %1$d എപ്പിസോഡ് %2$d റിലീസ് ചെയ്യുന്ന സമയം + %1$dh %2$dm %3$ds + %1$dm %2$ds + %1$ds + തുടക്കം മുതൽ പ്ലേ ചെയ്യുക + ഡൗൺലോഡ് ക്യൂ + സ്പീച്ച് റെക്കഗ്നിഷൻ ലഭ്യമല്ല + സംസാരിക്കാൻ തുടങ്ങുക… + മുഴുനീള പരമ്പരയും പ്ലേ ചെയ്യുക + ഈ വീഡിയോ ഒരു ടോറന്റ് ആണ്, അതായത് നിങ്ങളുടെ വീഡിയോ ആക്റ്റിവിറ്റി ട്രാക്ക് ചെയ്യപ്പെടാൻ സാധ്യതയുണ്ട്.\nതുടരുന്നതിന് മുൻപ് ടോറന്റിംഗിനെക്കുറിച്ച് നിങ്ങൾക്ക് കൃത്യമായി അറിയാമെന്ന് ഉറപ്പാക്കുക. + ഡിലീറ്റ് ചെയ്യേണ്ടവ തിരഞ്ഞെടുക്കുക + നിലവിൽ ഡൗൺലോഡുകൾ ഒന്നും തന്നെയില്ല. + നിലവിൽ ക്യൂവിലുള്ള ഡൗൺലോഡുകൾ ഒന്നും തന്നെയില്ല. + ഓഫ്‌ലൈനായി കാണാൻ ലഭ്യമാണ് + എല്ലാം തിരഞ്ഞെടുക്കുക + തിരഞ്ഞെടുത്തവയെല്ലാം ഒഴിവാക്കു + ലോക്കൽ വീഡിയോ തുറക്കുക + സേവ് ചെയ്യുക + സബ്‌ടൈറ്റിൽ ഉയരം + ഫോണ്ട് + ഫോണ്ട് വലുപ്പം + സ്വയം തിരഞ്ഞെടുത്ത ഭാഷ + ഭാഷകൾ ഡൗൺലോഡ് ചെയ്യുക + സബ്ടൈറ്റിൽ ഭാഷ + സൈറ്റിൽ വിവരങ്ങൾ ലഭ്യമല്ല, ആയതിനാൽ വീഡിയോ ലോഡ് ആവുകയില്ല. + പ്ലേയറിന്റെ വലുപ്പം മാറ്റുന്ന ബട്ടൻ + സബ്ടൈട്ടിൽസ് + ക്രോംകാസ്റ്റ് സബ്ടൈട്ടിൽസ് + ക്രോംകാസ്റ്റ് സബ്ടൈട്ടിൽസ് ക്രമീകരണങ്ങൾ + പ്ലേ ചെയ്യുന്ന വേഗത + പ്ലേയറിൽ വേഗതയ്ക്കുള്ള ഓപ്ഷൻ ചേർക്കുന്നു + മുന്നോട്ടും പിന്നോട്ടും പോകുവാൻ സ്വൈപ്പ് ചെയ്യുക + ക്രമീകരണങ്ങൾ മാറ്റുവാൻ സ്വൈപ്പ് ചെയ്യുക + നാമം + ഉറവിട നാമം + ദൃശ്യമേന്മയും നാമവും + എല്ലാം ഡൗൺലോഡ് ചെയ്യുക + അടുത്ത ഭാഗം സ്വയം പ്ലേ ചെയ്യുക + ഇപ്പോളത്തെ ഭാഗം കഴിഞ്ഞയുടനെ അടുത്തത് ആരംഭിക്കുക + തൽസമയം + \@string/home_play + രണ്ടുവട്ടം ടാപ്പ് ചെയ്താൽ സ്ഥാനം നിയന്ത്രിക്കാം + രണ്ടുവട്ടം ടാപ്പ് ചെയ്താൽ താൽകാലികമായി നിർത്തിവെക്കാം + പ്ലേയറിലെ സ്ഥാനനിയന്ത്രണ അളവ് (സെക്കണ്ടസ്) + നടുവിൽ രണ്ട് തവണ ടാപ്പ് ചെയ്താൽ താൽകാലികമായി നിർത്തിവെക്കാം + സിസ്റ്റത്തിന്റെ വെളിച്ചക്രമീകരണം ഉപയോഗിക്കുക diff --git a/app/src/main/res/values-b+vi/strings.xml b/app/src/main/res/values-b+vi/strings.xml index 952bafee7..3b2b3876f 100644 --- a/app/src/main/res/values-b+vi/strings.xml +++ b/app/src/main/res/values-b+vi/strings.xml @@ -553,7 +553,7 @@ Thêm vào Đăng ký Xóa khỏi mục yêu thích - Chọn một Hồ sơ + Chọn hồ sơ của bạn Có vẻ như một mục có khả năng trùng lặp đã tồn tại trong thư viện của bạn: \'%s.\' \n \nBạn vẫn muốn thêm mục này, thay thế mục hiện có hay hủy hành động? Nhập mã PIN PIN From 54618fab99f88e3bccefa05e44d976bb60c1dbda Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 00:07:39 +0000 Subject: [PATCH 07/99] chore(locales): fix locale issues --- app/src/main/res/values-b+ml/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-b+ml/strings.xml b/app/src/main/res/values-b+ml/strings.xml index 577e2ac73..6c5dd3d89 100644 --- a/app/src/main/res/values-b+ml/strings.xml +++ b/app/src/main/res/values-b+ml/strings.xml @@ -307,7 +307,7 @@ അടുത്ത ഭാഗം സ്വയം പ്ലേ ചെയ്യുക ഇപ്പോളത്തെ ഭാഗം കഴിഞ്ഞയുടനെ അടുത്തത് ആരംഭിക്കുക തൽസമയം - \@string/home_play + @string/home_play രണ്ടുവട്ടം ടാപ്പ് ചെയ്താൽ സ്ഥാനം നിയന്ത്രിക്കാം രണ്ടുവട്ടം ടാപ്പ് ചെയ്താൽ താൽകാലികമായി നിർത്തിവെക്കാം പ്ലേയറിലെ സ്ഥാനനിയന്ത്രണ അളവ് (സെക്കണ്ടസ്) From 8b8891c59b846ed7ddafa68617484a92c5ae05f2 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:11:04 -0600 Subject: [PATCH 08/99] SubSource: migrate to kotlinx serialization (#2891) --- .../syncproviders/providers/SubSource.kt | 64 +++++++++++-------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt index 19122768e..fb463ebee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt @@ -10,6 +10,8 @@ import com.lagradost.cloudstream3.syncproviders.SubtitleAPI import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.SubtitleHelper +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable class SubSourceApi : SubtitleAPI() { override val name = "SubSource" @@ -115,53 +117,61 @@ class SubSourceApi : SubtitleAPI() { } } + @Serializable data class ApiSearch( - @JsonProperty("success") val success: Boolean, - @JsonProperty("found") val found: List, + @JsonProperty("success") @SerialName("success") val success: Boolean, + @JsonProperty("found") @SerialName("found") val found: List, ) + @Serializable data class Found( - @JsonProperty("id") val id: Long, - @JsonProperty("title") val title: String, - @JsonProperty("seasons") val seasons: Long, - @JsonProperty("type") val type: String, - @JsonProperty("releaseYear") val releaseYear: Long, - @JsonProperty("linkName") val linkName: String, + @JsonProperty("id") @SerialName("id") val id: Long, + @JsonProperty("title") @SerialName("title") val title: String, + @JsonProperty("seasons") @SerialName("seasons") val seasons: Long, + @JsonProperty("type") @SerialName("type") val type: String, + @JsonProperty("releaseYear") @SerialName("releaseYear") val releaseYear: Long, + @JsonProperty("linkName") @SerialName("linkName") val linkName: String, ) + @Serializable data class ApiResponse( - @JsonProperty("success") val success: Boolean, - @JsonProperty("movie") val movie: Movie, - @JsonProperty("subs") val subs: List, + @JsonProperty("success") @SerialName("success") val success: Boolean, + @JsonProperty("movie") @SerialName("movie") val movie: Movie, + @JsonProperty("subs") @SerialName("subs") val subs: List, ) + @Serializable data class Movie( - @JsonProperty("id") val id: Long? = null, - @JsonProperty("type") val type: String? = null, - @JsonProperty("year") val year: Long? = null, - @JsonProperty("fullName") val fullName: String? = null, + @JsonProperty("id") @SerialName("id") val id: Long? = null, + @JsonProperty("type") @SerialName("type") val type: String? = null, + @JsonProperty("year") @SerialName("year") val year: Long? = null, + @JsonProperty("fullName") @SerialName("fullName") val fullName: String? = null, ) + @Serializable data class Sub( - @JsonProperty("hi") val hi: Int? = null, - @JsonProperty("fullLink") val fullLink: String? = null, - @JsonProperty("linkName") val linkName: String? = null, - @JsonProperty("lang") val lang: String? = null, - @JsonProperty("releaseName") val releaseName: String? = null, - @JsonProperty("subId") val subId: Long? = null, + @JsonProperty("hi") @SerialName("hi") val hi: Int? = null, + @JsonProperty("fullLink") @SerialName("fullLink") val fullLink: String? = null, + @JsonProperty("linkName") @SerialName("linkName") val linkName: String? = null, + @JsonProperty("lang") @SerialName("lang") val lang: String? = null, + @JsonProperty("releaseName") @SerialName("releaseName") val releaseName: String? = null, + @JsonProperty("subId") @SerialName("subId") val subId: Long? = null, ) + @Serializable data class SubData( - @JsonProperty("movie") val movie: String, - @JsonProperty("lang") val lang: String, - @JsonProperty("id") val id: String, + @JsonProperty("movie") @SerialName("movie") val movie: String, + @JsonProperty("lang") @SerialName("lang") val lang: String, + @JsonProperty("id") @SerialName("id") val id: String, ) + @Serializable data class SubTitleLink( - @JsonProperty("sub") val sub: SubToken, + @JsonProperty("sub") @SerialName("sub") val sub: SubToken, ) + @Serializable data class SubToken( - @JsonProperty("downloadToken") val downloadToken: String, + @JsonProperty("downloadToken") @SerialName("downloadToken") val downloadToken: String, ) -} \ No newline at end of file +} From deebac0bb2973a41744a19fc29bebc6847099085 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:14:43 -0600 Subject: [PATCH 09/99] SearchHistoryAdaptor: migrate to kotlinx serialization (#2883) --- .../cloudstream3/ui/search/SearchHistoryAdaptor.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt index 4868abb3d..793d96734 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt @@ -13,12 +13,15 @@ import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +@Serializable data class SearchHistoryItem( - @JsonProperty("searchedAt") val searchedAt: Long, - @JsonProperty("searchText") val searchText: String, - @JsonProperty("type") val type: List, - @JsonProperty("key") val key: String, + @JsonProperty("searchedAt") @SerialName("searchedAt") val searchedAt: Long, + @JsonProperty("searchText") @SerialName("searchText") val searchText: String, + @JsonProperty("type") @SerialName("type") val type: List, + @JsonProperty("key") @SerialName("key") val key: String, ) data class SearchHistoryCallback( From 683da9605f3551c7ae935d5821a1726c9f00b040 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:01:47 +0200 Subject: [PATCH 10/99] Remove flickering from bottom dialog sheets Now also dims the background --- app/src/main/res/values/styles.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f386bb62e..a63b47264 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -472,6 +472,9 @@