diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index bf499d2a77e..8f5c6286636 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -27,7 +27,7 @@ jobs: cache-read-only: false - name: Run Gradle - run: ./gradlew assemblePrereleaseDebug + run: ./gradlew assemblePrereleaseDebug lint check - name: Upload Artifact uses: actions/upload-artifact@v7 diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt index de680263572..84ef1fee070 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt @@ -136,9 +136,9 @@ class SerializationClassTester { runCatching { Class.forName(it).kotlin }.getOrNull() }.filter { kClass -> // Not possible to use .hasAnnotation() on newer Android versions. - kClass.java.annotations.any { - it is Serializable - } && kClass.java.annotations.none { it is SkipSerializationTest } + kClass.java.annotations.any { it is Serializable } + && kClass.java.annotations.none { it is SkipSerializationTest } + && !kClass.isAbstract } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt index a9cd9c01edd..4b8e2457931 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt @@ -121,7 +121,7 @@ class CloudStreamApp : Application(), SingletonImageLoader.Factory { } fun setKeyClass(path: String, value: T) { - context?.setKey(path, value) + context?.setKey(path, value) } fun removeKeys(folder: String): Int? { @@ -132,6 +132,11 @@ class CloudStreamApp : Application(), SingletonImageLoader.Factory { context?.setKey(path, value) } + @JvmName("setKeyReified") + inline fun setKey(path: String, value: T) { + context?.setKey(path, value) + } + fun setKey(folder: String, path: String, value: T) { context?.setKey(folder, path, value) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 075c08bb81d..f1c12a183e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -2,11 +2,11 @@ package com.lagradost.cloudstream3.syncproviders.providers import androidx.annotation.StringRes import androidx.core.net.toUri +import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.BuildConfig -import com.lagradost.cloudstream3.CloudStreamApp import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKeys import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey @@ -32,7 +32,12 @@ import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear +import com.lagradost.cloudstream3.utils.serializers.NonEmptySerializer import com.lagradost.cloudstream3.utils.txt +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KeepGeneratedSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import java.math.BigInteger import java.security.SecureRandom import java.text.SimpleDateFormat @@ -45,22 +50,18 @@ import kotlin.time.DurationUnit import kotlin.time.toDuration class SimklApi : SyncAPI() { - override var name = "Simkl" + override val name = "Simkl" override val idPrefix = "simkl" - val key = "simkl-key" override val redirectUrlIdentifier = "simkl" override val hasOAuth2 = true override val hasPin = true override var requireLibraryRefresh = true - override var mainUrl = "https://api.simkl.com" + override val mainUrl = "https://api.simkl.com" override val icon = R.drawable.simkl_logo override val createAccountUrl = "$mainUrl/signup" override val syncIdName = SyncIdName.Simkl - /** Automatically adds simkl auth headers */ - // private val interceptor = HeaderInterceptor() - /** * This is required to override the reported last activity as simkl activites * may not always update based on testing. @@ -69,63 +70,97 @@ class SimklApi : SyncAPI() { private object SimklCache { private const val SIMKL_CACHE_KEY = "SIMKL_API_CACHE" - enum class CacheTimes(val value: String) { OneMonth("30d"), ThirtyMinutes("30m") } - private class SimklCacheWrapper( - @JsonProperty("obj") val obj: T?, - @JsonProperty("validUntil") val validUntil: Long, - @JsonProperty("cacheTime") val cacheTime: Long = APIHolder.unixTime, - ) { - /** Returns true if cache is newer than cacheDays */ - fun isFresh(): Boolean { - return validUntil > APIHolder.unixTime - } + @Serializable + private data class MediaObjectCacheEntry( + @JsonProperty("obj") @SerialName("obj") val obj: MediaObject?, + @JsonProperty("validUntil") @SerialName("validUntil") val validUntil: Long, + @JsonProperty("cacheTime") @SerialName("cacheTime") val cacheTime: Long = APIHolder.unixTime, + ) - fun remainingTime(): Duration { - val unixTime = APIHolder.unixTime - return if (validUntil > unixTime) { - (validUntil - unixTime).toDuration(DurationUnit.SECONDS) - } else { - Duration.ZERO - } - } + @Serializable + private data class EpisodesCacheEntry( + @JsonProperty("obj") @SerialName("obj") val obj: Array?, + @JsonProperty("validUntil") @SerialName("validUntil") val validUntil: Long, + @JsonProperty("cacheTime") @SerialName("cacheTime") val cacheTime: Long = APIHolder.unixTime, + ) + + /** + * Minimal class used only to peek at an entry's expiry, without caring which of the + * concrete entry types above actually produced it. + */ + @Serializable + private data class CacheFreshness( + @JsonProperty("validUntil") @SerialName("validUntil") val validUntil: Long, + ) + + private fun Long.isFresh(): Boolean = this > APIHolder.unixTime + + private fun Long.remaining(): Duration { + val unixTime = APIHolder.unixTime + return if (this > unixTime) { + (this - unixTime).toDuration(DurationUnit.SECONDS) + } else Duration.ZERO } fun cleanOldCache() { getKeys(SIMKL_CACHE_KEY)?.forEach { - val isOld = CloudStreamApp.getKey>(it)?.isFresh() == false - if (isOld) { - removeKey(it) + val isOld = getKey(it)?.validUntil?.isFresh() == false + if (isOld) removeKey(it) + } + } + + fun setMediaObject(path: String, value: MediaObject, cacheTime: Duration) { + debugPrint { "Set cache: $SIMKL_CACHE_KEY/$path for ${cacheTime.inWholeDays} days or ${cacheTime.inWholeSeconds} seconds." } + setKey( + SIMKL_CACHE_KEY, + path, + MediaObjectCacheEntry(value, APIHolder.unixTime + cacheTime.inWholeSeconds).toJson(), + ) + } + + /** Gets the cached [MediaObject], if it's not fresh returns null and removes it from cache */ + fun getMediaObject(path: String): MediaObject? { + val cache = getKey(SIMKL_CACHE_KEY, path)?.let { + tryParseJson(it) + } + + return if (cache?.validUntil?.isFresh() == true) { + debugPrint { + "Cache hit at: $SIMKL_CACHE_KEY/$path. " + + "Remains fresh for ${cache.validUntil.remaining().inWholeDays} days or ${cache.validUntil.remaining().inWholeSeconds} seconds." } + cache.obj + } else { + debugPrint { "Cache miss at: $SIMKL_CACHE_KEY/$path" } + removeKey(SIMKL_CACHE_KEY, path) + null } } - fun setKey(path: String, value: T, cacheTime: Duration) { + fun setEpisodes(path: String, value: Array, cacheTime: Duration) { debugPrint { "Set cache: $SIMKL_CACHE_KEY/$path for ${cacheTime.inWholeDays} days or ${cacheTime.inWholeSeconds} seconds." } setKey( SIMKL_CACHE_KEY, path, - // Storing as plain sting is required to make generics work. - SimklCacheWrapper(value, APIHolder.unixTime + cacheTime.inWholeSeconds).toJson() + EpisodesCacheEntry(value, APIHolder.unixTime + cacheTime.inWholeSeconds).toJson(), ) } - /** - * Gets cached object, if object is not fresh returns null and removes it from cache - */ - inline fun getKey(path: String): T? { + /** Gets the cached episode list, if it's not fresh returns null and removes it from cache */ + fun getEpisodes(path: String): Array? { val cache = getKey(SIMKL_CACHE_KEY, path)?.let { - tryParseJson>(it) + tryParseJson(it) } - return if (cache?.isFresh() == true) { + return if (cache?.validUntil?.isFresh() == true) { debugPrint { "Cache hit at: $SIMKL_CACHE_KEY/$path. " + - "Remains fresh for ${cache.remainingTime().inWholeDays} days or ${cache.remainingTime().inWholeSeconds} seconds." + "Remains fresh for ${cache.validUntil.remaining().inWholeDays} days or ${cache.validUntil.remaining().inWholeSeconds} seconds." } cache.obj } else { @@ -169,7 +204,7 @@ class SimklApi : SyncAPI() { ) ) ) - } catch (e: Exception) { + } catch (_: Exception) { null } } @@ -185,7 +220,7 @@ class SimklApi : SyncAPI() { enum class SimklListStatusType( var value: Int, @StringRes val stringRes: Int, - val originalName: String? + val originalName: String?, ) { Watching(0, R.string.type_watching, "watching"), Completed(1, R.string.type_completed, "completed"), @@ -204,83 +239,92 @@ class SimklApi : SyncAPI() { } } - // ------------------- @JsonInclude(JsonInclude.Include.NON_EMPTY) + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = TokenRequest.Serializer::class) data class TokenRequest( - @JsonProperty("code") val code: String, - @JsonProperty("client_id") val clientId: String = CLIENT_ID, - @JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET, - @JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl", - @JsonProperty("grant_type") val grantType: String = "authorization_code" - ) + @JsonProperty("code") @SerialName("code") val code: String, + @JsonProperty("client_id") @SerialName("client_id") val clientId: String = CLIENT_ID, + @JsonProperty("client_secret") @SerialName("client_secret") val clientSecret: String = CLIENT_SECRET, + @JsonProperty("redirect_uri") @SerialName("redirect_uri") val redirectUri: String = "$APP_STRING://simkl", + @JsonProperty("grant_type") @SerialName("grant_type") val grantType: String = "authorization_code", + ) { + object Serializer : NonEmptySerializer(TokenRequest.generatedSerializer()) + } + @Serializable data class TokenResponse( /** No expiration date */ - @JsonProperty("access_token") val accessToken: String, - @JsonProperty("token_type") val tokenType: String, - @JsonProperty("scope") val scope: String + @JsonProperty("access_token") @SerialName("access_token") val accessToken: String, + @JsonProperty("token_type") @SerialName("token_type") val tokenType: String, + @JsonProperty("scope") @SerialName("scope") val scope: String, ) - // ------------------- /** https://simkl.docs.apiary.io/#reference/users/settings/receive-settings */ + @Serializable data class SettingsResponse( - @JsonProperty("user") - val user: User, - @JsonProperty("account") - val account: Account, + @JsonProperty("user") @SerialName("user") val user: User, + @JsonProperty("account") @SerialName("account") val account: Account, ) { + @Serializable data class User( - @JsonProperty("name") - val name: String, - /** Url */ - @JsonProperty("avatar") - val avatar: String + @JsonProperty("name") @SerialName("name") val name: String, + @JsonProperty("avatar") @SerialName("avatar") val avatar: String, // Url ) + @Serializable data class Account( - @JsonProperty("id") - val id: Int, + @JsonProperty("id") @SerialName("id") val id: Int, ) } + @Serializable data class PinAuthResponse( - @JsonProperty("result") val result: String, - @JsonProperty("device_code") val deviceCode: String, - @JsonProperty("user_code") val userCode: String, - @JsonProperty("verification_url") val verificationUrl: String, - @JsonProperty("expires_in") val expiresIn: Int, - @JsonProperty("interval") val interval: Int, + @JsonProperty("result") @SerialName("result") val result: String, + @JsonProperty("device_code") @SerialName("device_code") val deviceCode: String, + @JsonProperty("user_code") @SerialName("user_code") val userCode: String, + @JsonProperty("verification_url") @SerialName("verification_url") val verificationUrl: String, + @JsonProperty("expires_in") @SerialName("expires_in") val expiresIn: Int, + @JsonProperty("interval") @SerialName("interval") val interval: Int, ) + @Serializable data class PinExchangeResponse( - @JsonProperty("result") val result: String, - @JsonProperty("message") val message: String? = null, - @JsonProperty("access_token") val accessToken: String? = null, + @JsonProperty("result") @SerialName("result") val result: String, + @JsonProperty("message") @SerialName("message") val message: String? = null, + @JsonProperty("access_token") @SerialName("access_token") val accessToken: String? = null, ) - // ------------------- + @Serializable data class ActivitiesResponse( - @JsonProperty("all") val all: String?, - @JsonProperty("tv_shows") val tvShows: UpdatedAt, - @JsonProperty("anime") val anime: UpdatedAt, - @JsonProperty("movies") val movies: UpdatedAt, + @JsonProperty("all") @SerialName("all") val all: String?, + @JsonProperty("tv_shows") @SerialName("tv_shows") val tvShows: UpdatedAt, + @JsonProperty("anime") @SerialName("anime") val anime: UpdatedAt, + @JsonProperty("movies") @SerialName("movies") val movies: UpdatedAt, ) { + @Serializable data class UpdatedAt( - @JsonProperty("all") val all: String?, - @JsonProperty("removed_from_list") val removedFromList: String?, - @JsonProperty("rated_at") val ratedAt: String?, + @JsonProperty("all") @SerialName("all") val all: String?, + @JsonProperty("removed_from_list") @SerialName("removed_from_list") val removedFromList: String?, + @JsonProperty("rated_at") @SerialName("rated_at") val ratedAt: String?, ) } /** https://simkl.docs.apiary.io/#reference/tv/episodes/get-tv-show-episodes */ @JsonInclude(JsonInclude.Include.NON_EMPTY) + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = EpisodeMetadata.Serializer::class) data class EpisodeMetadata( - @JsonProperty("title") val title: String?, - @JsonProperty("description") val description: String?, - @JsonProperty("season") val season: Int?, - @JsonProperty("episode") val episode: Int, - @JsonProperty("img") val img: String? + @JsonProperty("title") @SerialName("title") val title: String?, + @JsonProperty("description") @SerialName("description") val description: String?, + @JsonProperty("season") @SerialName("season") val season: Int?, + @JsonProperty("episode") @SerialName("episode") val episode: Int, + @JsonProperty("img") @SerialName("img") val img: String?, ) { + object Serializer : NonEmptySerializer(EpisodeMetadata.generatedSerializer()) + companion object { fun convertToEpisodes(list: List?): List? { return list?.map { @@ -300,40 +344,58 @@ class SimklApi : SyncAPI() { /** * https://simkl.docs.apiary.io/#introduction/about-simkl-api/standard-media-objects - * Useful for finding shows from metadata + * Useful for finding shows from metadata. */ @JsonInclude(JsonInclude.Include.NON_EMPTY) - open class MediaObject( - @JsonProperty("title") val title: String?, - @JsonProperty("year") val year: Int?, - @JsonProperty("ids") val ids: Ids?, - @JsonProperty("total_episodes") val totalEpisodes: Int? = null, - @JsonProperty("status") val status: String? = null, - @JsonProperty("poster") val poster: String? = null, - @JsonProperty("type") val type: String? = null, - @JsonProperty("seasons") val seasons: List? = null, - @JsonProperty("episodes") val episodes: List? = null + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = MediaObject.Serializer::class) + data class MediaObject( + @JsonProperty("title") @SerialName("title") val title: String?, + @JsonProperty("year") @SerialName("year") val year: Int?, + @JsonProperty("ids") @SerialName("ids") val ids: Ids?, + @JsonProperty("total_episodes") @SerialName("total_episodes") val totalEpisodes: Int? = null, + @JsonProperty("status") @SerialName("status") val status: String? = null, + @JsonProperty("poster") @SerialName("poster") val poster: String? = null, + @JsonProperty("type") @SerialName("type") val type: String? = null, + @JsonProperty("seasons") @SerialName("seasons") val seasons: List? = null, + @JsonProperty("episodes") @SerialName("episodes") val episodes: List? = null, ) { + object Serializer : NonEmptySerializer(MediaObject.generatedSerializer()) + fun hasEnded(): Boolean { return status == "released" || status == "ended" } @JsonInclude(JsonInclude.Include.NON_EMPTY) + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = Season.Serializer::class) data class Season( - @JsonProperty("number") val number: Int, - @JsonProperty("episodes") val episodes: List + @JsonProperty("number") @SerialName("number") val number: Int, + @JsonProperty("episodes") @SerialName("episodes") val episodes: List, ) { - data class Episode(@JsonProperty("number") val number: Int) + object Serializer : NonEmptySerializer(Season.generatedSerializer()) + + @Serializable + data class Episode( + @JsonProperty("number") @SerialName("number") val number: Int, + ) } @JsonInclude(JsonInclude.Include.NON_EMPTY) + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = Ids.Serializer::class) data class Ids( - @JsonProperty("simkl") val simkl: Int?, - @JsonProperty("imdb") val imdb: String? = null, - @JsonProperty("tmdb") val tmdb: String? = null, - @JsonProperty("mal") val mal: String? = null, - @JsonProperty("anilist") val anilist: String? = null, + @JsonProperty("simkl") @SerialName("simkl") val simkl: Int?, + @JsonProperty("imdb") @SerialName("imdb") val imdb: String? = null, + @JsonProperty("tmdb") @SerialName("tmdb") val tmdb: String? = null, + @JsonProperty("mal") @SerialName("mal") val mal: String? = null, + @JsonProperty("anilist") @SerialName("anilist") val anilist: String? = null, ) { + object Serializer : NonEmptySerializer(Ids.generatedSerializer()) + companion object { fun fromMap(map: Map): Ids { return Ids( @@ -341,20 +403,21 @@ class SimklApi : SyncAPI() { imdb = map[SimklSyncServices.Imdb], tmdb = map[SimklSyncServices.Tmdb], mal = map[SimklSyncServices.Mal], - anilist = map[SimklSyncServices.AniList] + anilist = map[SimklSyncServices.AniList], ) } } } fun toSyncSearchResult(): SyncAPI.SyncSearchResult? { + val currentIds = this.ids return SyncAPI.SyncSearchResult( this.title ?: return null, "Simkl", - this.ids?.simkl?.toString() ?: return null, - getUrlFromId(this.ids.simkl), + currentIds?.simkl?.toString() ?: return null, + getUrlFromId(currentIds.simkl), this.poster?.let { getPosterUrl(it) }, - if (this.type == "movie") TvType.Movie else TvType.TvSeries + if (this.type == "movie") TvType.Movie else TvType.TvSeries, ) } } @@ -369,7 +432,7 @@ class SimklApi : SyncAPI() { private var addEpisodes: Pair?, List?>? = null, private var removeEpisodes: Pair?, List?>? = null, // Required for knowing if the status should be overwritten - private var onList: Boolean = false + private var onList: Boolean = false, ) { fun token(token: AuthToken) = apply { this.headers = getHeaders(token) } fun apiUrl(url: String) = apply { this.url = url } @@ -383,11 +446,7 @@ class SimklApi : SyncAPI() { fun status(newStatus: Int?, oldStatus: Int?) = apply { onList = oldStatus != null // Only set status if its new - if (newStatus != oldStatus) { - this.status = newStatus - } else { - this.status = null - } + this.status = if (newStatus != oldStatus) newStatus else null } fun episodes( @@ -396,7 +455,6 @@ class SimklApi : SyncAPI() { oldEpisodes: Int?, ) = apply { if (allEpisodes == null || newEpisodes == null) return@apply - fun getEpisodes(rawEpisodes: List) = if (rawEpisodes.any { it.season != null }) { EpisodeMetadata.convertToSeasons(rawEpisodes) to null @@ -407,12 +465,12 @@ class SimklApi : SyncAPI() { // Do not add episodes if there is no change if (newEpisodes > (oldEpisodes ?: 0)) { this.addEpisodes = getEpisodes(allEpisodes.take(newEpisodes)) - // Set to watching if episodes are added and there is no current status if (!onList) { status = SimklListStatusType.Watching.value } } + if ((oldEpisodes ?: 0) > newEpisodes) { this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes)) } @@ -424,19 +482,17 @@ class SimklApi : SyncAPI() { return if (this.status == SimklListStatusType.None.value) { app.post( "$url/sync/history/remove", - json = StatusRequest( + json = HistoryRequest( shows = listOf(HistoryMediaObject(ids = ids)), - movies = emptyList() + movies = emptyList(), ), - headers = headers + headers = headers, ).isSuccessful } else { val statusResponse = this.status?.let { setStatus -> - val newStatus = - SimklListStatusType.entries - .firstOrNull { it.value == setStatus }?.originalName - ?: SimklListStatusType.Watching.originalName!! - + val newStatus = SimklListStatusType.entries.firstOrNull { + it.value == setStatus + }?.originalName ?: SimklListStatusType.Watching.originalName!! app.post( "${this.url}/sync/add-to-list", json = StatusRequest( @@ -446,41 +502,40 @@ class SimklApi : SyncAPI() { null, ids, newStatus, - ) - ), movies = emptyList() + ), + ), + movies = emptyList(), ), - headers = headers + headers = headers, ).isSuccessful } ?: true val episodeRemovalResponse = removeEpisodes?.let { (seasons, episodes) -> app.post( "${this.url}/sync/history/remove", - json = StatusRequest( + json = HistoryRequest( shows = listOf( HistoryMediaObject( ids = ids, seasons = seasons, - episodes = episodes - ) + episodes = episodes, + ), ), - movies = emptyList() + movies = emptyList(), ), - headers = headers + headers = headers, ).isSuccessful } ?: true // You cannot rate if you are planning to watch it. - val shouldRate = - score != null && status != SimklListStatusType.Planning.value + val shouldRate = score != null && status != SimklListStatusType.Planning.value val realScore = if (shouldRate) score else null - val historyResponse = // Only post if there are episodes or score to upload if (addEpisodes != null || shouldRate) { app.post( "${this.url}/sync/history", - json = StatusRequest( + json = HistoryRequest( shows = listOf( HistoryMediaObject( null, @@ -490,34 +545,33 @@ class SimklApi : SyncAPI() { addEpisodes?.second, realScore, realScore?.let { time }, - ) - ), movies = emptyList() + ), + ), + movies = emptyList(), ), - headers = headers + headers = headers, ).isSuccessful - } else { - true - } - + } else true statusResponse && episodeRemovalResponse && historyResponse } } } } - fun getHeaders(token: AuthToken): Map = - mapOf("Authorization" to "Bearer ${token.accessToken}", "simkl-api-key" to CLIENT_ID) + fun getHeaders(token: AuthToken): Map = mapOf( + "Authorization" to "Bearer ${token.accessToken}", + "simkl-api-key" to CLIENT_ID, + ) suspend fun getEpisodes( simklId: Int?, type: String?, episodes: Int?, - hasEnded: Boolean? + hasEnded: Boolean?, ): Array? { if (simklId == null) return null - val cacheKey = "Episodes/$simklId" - val cache = SimklCache.getKey>(cacheKey) + val cache = SimklCache.getEpisodes(cacheKey) // Return cached result if its higher or equal the amount of episodes. if (cache != null && cache.size >= (episodes ?: 0)) { @@ -534,6 +588,7 @@ class SimklApi : SyncAPI() { }.toTypedArray() } } + val url = when (type) { "anime" -> "https://api.simkl.com/anime/episodes/$simklId" "tv" -> "https://api.simkl.com/tv/episodes/$simklId" @@ -548,57 +603,86 @@ class SimklApi : SyncAPI() { if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value // 1 Month cache - SimklCache.setKey(cacheKey, it, Duration.parse(cacheTime)) + SimklCache.setEpisodes(cacheKey, it, Duration.parse(cacheTime)) } } @JsonInclude(JsonInclude.Include.NON_EMPTY) - class HistoryMediaObject( - @JsonProperty("title") title: String? = null, - @JsonProperty("year") year: Int? = null, - @JsonProperty("ids") ids: Ids? = null, - @JsonProperty("seasons") seasons: List? = null, - @JsonProperty("episodes") episodes: List? = null, - @JsonProperty("rating") val rating: Int? = null, - @JsonProperty("rated_at") val ratedAt: String? = null, - ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = HistoryMediaObject.Serializer::class) + data class HistoryMediaObject( + @JsonProperty("title") @SerialName("title") val title: String? = null, + @JsonProperty("year") @SerialName("year") val year: Int? = null, + @JsonProperty("ids") @SerialName("ids") val ids: MediaObject.Ids? = null, + @JsonProperty("seasons") @SerialName("seasons") val seasons: List? = null, + @JsonProperty("episodes") @SerialName("episodes") val episodes: List? = null, + @JsonProperty("rating") @SerialName("rating") val rating: Int? = null, + @JsonProperty("rated_at") @SerialName("rated_at") val ratedAt: String? = null, + ) { + object Serializer : NonEmptySerializer(HistoryMediaObject.generatedSerializer()) + } @JsonInclude(JsonInclude.Include.NON_EMPTY) - class RatingMediaObject( - @JsonProperty("title") title: String?, - @JsonProperty("year") year: Int?, - @JsonProperty("ids") ids: Ids?, - @JsonProperty("rating") val rating: Int, - @JsonProperty("rated_at") val ratedAt: String? = getDateTime(APIHolder.unixTime) - ) : MediaObject(title, year, ids) + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = RatingMediaObject.Serializer::class) + data class RatingMediaObject( + @JsonProperty("title") @SerialName("title") val title: String? = null, + @JsonProperty("year") @SerialName("year") val year: Int? = null, + @JsonProperty("ids") @SerialName("ids") val ids: MediaObject.Ids? = null, + @JsonProperty("rating") @SerialName("rating") val rating: Int, + @JsonProperty("rated_at") @SerialName("rated_at") val ratedAt: String? = getDateTime(APIHolder.unixTime), + ) { + object Serializer : NonEmptySerializer(RatingMediaObject.generatedSerializer()) + } @JsonInclude(JsonInclude.Include.NON_EMPTY) - class StatusMediaObject( - @JsonProperty("title") title: String?, - @JsonProperty("year") year: Int?, - @JsonProperty("ids") ids: Ids?, - @JsonProperty("to") val to: String, - @JsonProperty("watched_at") val watchedAt: String? = getDateTime(APIHolder.unixTime) - ) : MediaObject(title, year, ids) + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = StatusMediaObject.Serializer::class) + data class StatusMediaObject( + @JsonProperty("title") @SerialName("title") val title: String? = null, + @JsonProperty("year") @SerialName("year") val year: Int? = null, + @JsonProperty("ids") @SerialName("ids") val ids: MediaObject.Ids? = null, + @JsonProperty("to") @SerialName("to") val to: String, + @JsonProperty("watched_at") @SerialName("watched_at") val watchedAt: String? = getDateTime(APIHolder.unixTime), + ) { + object Serializer : NonEmptySerializer(StatusMediaObject.generatedSerializer()) + } @JsonInclude(JsonInclude.Include.NON_EMPTY) + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = StatusRequest.Serializer::class) data class StatusRequest( - @JsonProperty("movies") val movies: List, - @JsonProperty("shows") val shows: List - ) + @JsonProperty("movies") @SerialName("movies") val movies: List, + @JsonProperty("shows") @SerialName("shows") val shows: List, + ) { + object Serializer : NonEmptySerializer(StatusRequest.generatedSerializer()) + } + + /** Same shape as [StatusRequest], for the endpoints that post [HistoryMediaObject]s instead. */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = HistoryRequest.Serializer::class) + data class HistoryRequest( + @JsonProperty("movies") @SerialName("movies") val movies: List, + @JsonProperty("shows") @SerialName("shows") val shows: List, + ) { + object Serializer : NonEmptySerializer(HistoryRequest.generatedSerializer()) + } /** https://simkl.docs.apiary.io/#reference/sync/get-all-items/get-all-items-in-the-user's-watchlist */ + @Serializable data class AllItemsResponse( - @JsonProperty("shows") - val shows: List = emptyList(), - @JsonProperty("anime") - val anime: List = emptyList(), - @JsonProperty("movies") - val movies: List = emptyList(), + @JsonProperty("shows") @SerialName("shows") val shows: List = emptyList(), + @JsonProperty("anime") @SerialName("anime") val anime: List = emptyList(), + @JsonProperty("movies") @SerialName("movies") val movies: List = emptyList(), ) { companion object { fun merge(first: AllItemsResponse?, second: AllItemsResponse?): AllItemsResponse { - // Replace the first item with the same id, or add the new item fun MutableList.replaceOrAddItem(newItem: T, predicate: (T) -> Boolean) { for (i in this.indices) { @@ -607,10 +691,10 @@ class SimklApi : SyncAPI() { return } } + this.add(newItem) } - // fun merge( first: List?, second: List? @@ -644,15 +728,17 @@ class SimklApi : SyncAPI() { fun toLibraryItem(): SyncAPI.LibraryItem } + @Serializable data class MovieMetadata( - @JsonProperty("last_watched_at") override val lastWatchedAt: String?, - @JsonProperty("status") override val status: String, - @JsonProperty("user_rating") override val userRating: Int?, - @JsonProperty("last_watched") override val lastWatched: String?, - @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, - @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, - val movie: ShowMetadata.Show + @JsonProperty("last_watched_at") @SerialName("last_watched_at") override val lastWatchedAt: String?, + @JsonProperty("status") @SerialName("status") override val status: String, + @JsonProperty("user_rating") @SerialName("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") @SerialName("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") @SerialName("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") @SerialName("total_episodes_count") override val totalEpisodesCount: Int?, + @JsonProperty("movie") @SerialName("movie") val movie: ShowMetadata.Show, ) : Metadata { + @JsonIgnore override fun getIds(): ShowMetadata.Show.Ids { return this.movie.ids } @@ -672,20 +758,22 @@ class SimklApi : SyncAPI() { null, null, this.movie.year?.toYear(), - movie.ids.simkl + movie.ids.simkl, ) } } + @Serializable data class ShowMetadata( - @JsonProperty("last_watched_at") override val lastWatchedAt: String?, - @JsonProperty("status") override val status: String, - @JsonProperty("user_rating") override val userRating: Int?, - @JsonProperty("last_watched") override val lastWatched: String?, - @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, - @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, - @JsonProperty("show") val show: Show + @JsonProperty("last_watched_at") @SerialName("last_watched_at") override val lastWatchedAt: String?, + @JsonProperty("status") @SerialName("status") override val status: String, + @JsonProperty("user_rating") @SerialName("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") @SerialName("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") @SerialName("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") @SerialName("total_episodes_count") override val totalEpisodesCount: Int?, + @JsonProperty("show") @SerialName("show") val show: Show, ) : Metadata { + @JsonIgnore override fun getIds(): Show.Ids { return this.show.ids } @@ -705,28 +793,30 @@ class SimklApi : SyncAPI() { null, null, this.show.year?.toYear(), - show.ids.simkl + show.ids.simkl, ) } + @Serializable data class Show( - @JsonProperty("title") val title: String, - @JsonProperty("poster") val poster: String?, - @JsonProperty("year") val year: Int?, - @JsonProperty("ids") val ids: Ids, + @JsonProperty("title") @SerialName("title") val title: String, + @JsonProperty("poster") @SerialName("poster") val poster: String?, + @JsonProperty("year") @SerialName("year") val year: Int?, + @JsonProperty("ids") @SerialName("ids") val ids: Ids, ) { + @Serializable data class Ids( - @JsonProperty("simkl") val simkl: Int, - @JsonProperty("slug") val slug: String?, - @JsonProperty("imdb") val imdb: String?, - @JsonProperty("zap2it") val zap2it: String?, - @JsonProperty("tmdb") val tmdb: String?, - @JsonProperty("offen") val offen: String?, - @JsonProperty("tvdb") val tvdb: String?, - @JsonProperty("mal") val mal: String?, - @JsonProperty("anidb") val anidb: String?, - @JsonProperty("anilist") val anilist: String?, - @JsonProperty("traktslug") val traktslug: String? + @JsonProperty("simkl") @SerialName("simkl") val simkl: Int, + @JsonProperty("slug") @SerialName("slug") val slug: String?, + @JsonProperty("imdb") @SerialName("imdb") val imdb: String?, + @JsonProperty("zap2it") @SerialName("zap2it") val zap2it: String?, + @JsonProperty("tmdb") @SerialName("tmdb") val tmdb: String?, + @JsonProperty("offen") @SerialName("offen") val offen: String?, + @JsonProperty("tvdb") @SerialName("tvdb") val tvdb: String?, + @JsonProperty("mal") @SerialName("mal") val mal: String?, + @JsonProperty("anidb") @SerialName("anidb") val anidb: String?, + @JsonProperty("anilist") @SerialName("anilist") val anilist: String?, + @JsonProperty("traktslug") @SerialName("traktslug") val traktslug: String?, ) { fun matchesId(database: SimklSyncServices, id: String): Boolean { return when (database) { @@ -743,27 +833,10 @@ class SimklApi : SyncAPI() { } } - /** - * Appends api keys to the requests - **/ - /*private inner class HeaderInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - debugPrint { "${this@SimklApi.name} made request to ${chain.request().url}" } - return chain.proceed( - chain.request() - .newBuilder() - .addHeader("Authorization", "Bearer $token") - .addHeader("simkl-api-key", CLIENT_ID) - .build() - ) - } - }*/ - private suspend fun getUser(token: AuthToken): SettingsResponse = app.post("$mainUrl/users/settings", headers = getHeaders(token)) .parsed() - /** * Useful to get episodes on demand to prevent unnecessary requests. */ @@ -771,7 +844,7 @@ class SimklApi : SyncAPI() { private val simklId: Int?, private val type: String?, private val totalEpisodeCount: Int?, - private val hasEnded: Boolean? + private val hasEnded: Boolean?, ) { suspend fun getEpisodes(): Array? { return getEpisodes(simklId, type, totalEpisodeCount, hasEnded) @@ -786,10 +859,12 @@ class SimklApi : SyncAPI() { val episodeConstructor: SimklEpisodeConstructor, override var isFavorite: Boolean? = null, override var maxEpisodes: Int? = null, - /** Save seen episodes separately to know the change from old to new. - * Required to remove seen episodes if count decreases */ + /** + * Save seen episodes separately to know the change from old to new. + * Required to remove seen episodes if count decreases. + */ val oldEpisodes: Int, - val oldStatus: String? + val oldStatus: String?, ) : SyncAPI.AbstractSyncStatus() override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? { @@ -799,22 +874,23 @@ class SimklApi : SyncAPI() { // Key which assumes all ids are the same each time :/ // This could be some sort of reference system to make multiple IDs // point to the same key. - val idKey = - realIds.toList().map { "${it.first.originalName}=${it.second}" }.sorted().joinToString() + val idKey = realIds.toList().map { + "${it.first.originalName}=${it.second}" + }.sorted().joinToString() - val cachedObject = SimklCache.getKey(idKey) + val cachedObject = SimklCache.getMediaObject(idKey) val searchResult: MediaObject = cachedObject ?: (searchByIds(realIds)?.firstOrNull()?.also { result -> val cacheTime = if (result.hasEnded()) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value - SimklCache.setKey(idKey, result, Duration.parse(cacheTime)) + SimklCache.setMediaObject(idKey, result, Duration.parse(cacheTime)) }) ?: return null val episodeConstructor = SimklEpisodeConstructor( searchResult.ids?.simkl, searchResult.type, searchResult.totalEpisodes, - searchResult.hasEnded() + searchResult.hasEnded(), ) val foundItem = getSyncListSmart(auth)?.let { list -> @@ -829,19 +905,16 @@ class SimklApi : SyncAPI() { return SimklSyncStatus( status = foundItem.status?.let { SyncWatchType.fromInternalId( - SimklListStatusType.fromString( - it - )?.value + SimklListStatusType.fromString(it)?.value ) - } - ?: return null, + } ?: return null, score = Score.from10(foundItem.userRating), watchedEpisodes = foundItem.watchedEpisodesCount, maxEpisodes = searchResult.totalEpisodes, episodeConstructor = episodeConstructor, oldEpisodes = foundItem.watchedEpisodesCount ?: 0, oldScore = foundItem.userRating, - oldStatus = foundItem.status + oldStatus = foundItem.status, ) } else { return SimklSyncStatus( @@ -852,7 +925,7 @@ class SimklApi : SyncAPI() { episodeConstructor = episodeConstructor, oldEpisodes = 0, oldStatus = null, - oldScore = null + oldScore = null, ) } } @@ -860,12 +933,11 @@ class SimklApi : SyncAPI() { override suspend fun updateStatus( auth: AuthData?, id: String, - newStatus: AbstractSyncStatus + newStatus: AbstractSyncStatus, ): Boolean { - val parsedId = readIdFromString(id) lastScoreTime = APIHolder.unixTime + val parsedId = readIdFromString(id) val simklStatus = newStatus as? SimklSyncStatus - val builder = SimklScoreBuilder.Builder() .apiUrl(this.mainUrl) .score(newStatus.score?.toInt(10), simklStatus?.oldScore) @@ -879,7 +951,6 @@ class SimklApi : SyncAPI() { .token(auth?.token ?: return false) .ids(MediaObject.Ids.fromMap(parsedId)) - // Get episodes only when required val episodes = simklStatus?.episodeConstructor?.getEpisodes() @@ -887,27 +958,22 @@ class SimklApi : SyncAPI() { val watchedEpisodes = if (newStatus.status.internalId == SimklListStatusType.Completed.value) { episodes?.size - } else { - newStatus.watchedEpisodes - } + } else newStatus.watchedEpisodes builder.episodes(episodes?.toList(), watchedEpisodes, simklStatus?.oldEpisodes) - requireLibraryRefresh = true return builder.execute() } - /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ private suspend fun searchByIds(serviceMap: Map): Array? { if (serviceMap.isEmpty()) return emptyArray() - return app.get( "$mainUrl/search/id", params = mapOf("client_id" to CLIENT_ID) + serviceMap.map { (service, id) -> service.originalName to id } - ).parsedSafe() + ).parsedSafe>() } override suspend fun search(auth: AuthData?, query: String): List? { @@ -918,12 +984,10 @@ class SimklApi : SyncAPI() { override fun loginRequest(): AuthLoginPage? { val lastLoginState = BigInteger(130, SecureRandom()).toString(32) - val url = - "https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrlIdentifier}&state=$lastLoginState" - + val url = "https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://$redirectUrlIdentifier&state=$lastLoginState" return AuthLoginPage( url = url, - payload = lastLoginState + payload = lastLoginState, ) } @@ -938,12 +1002,12 @@ class SimklApi : SyncAPI() { return app.get( "$mainUrl/sync/all-items/", params = params, - headers = getHeaders(auth.token) - ).parsedSafe() + headers = getHeaders(auth.token), + ).parsedSafe() } private suspend fun getActivities(token: AuthToken): ActivitiesResponse? { - return app.post("$mainUrl/sync/activities", headers = getHeaders(token)).parsedSafe() + return app.post("$mainUrl/sync/activities", headers = getHeaders(token)).parsedSafe() } private fun getSyncListCached(auth: AuthData): AllItemsResponse? { @@ -957,18 +1021,13 @@ class SimklApi : SyncAPI() { val lastRemoval = listOf( activities?.tvShows?.removedFromList, activities?.anime?.removedFromList, - activities?.movies?.removedFromList - ).maxOf { - getUnixTime(it) ?: -1 - } - val lastRealUpdate = - listOf( - activities?.tvShows?.all, - activities?.anime?.all, - activities?.movies?.all, - ).maxOf { - getUnixTime(it) ?: -1 - } + activities?.movies?.removedFromList, + ).maxOf { getUnixTime(it) ?: -1 } + val lastRealUpdate = listOf( + activities?.tvShows?.all, + activities?.anime?.all, + activities?.movies?.all, + ).maxOf { getUnixTime(it) ?: -1 } debugPrint { "Cache times: lastCacheUpdate=$lastCacheUpdate, lastRemoval=$lastRemoval, lastRealUpdate=$lastRealUpdate" } val list = if (lastCacheUpdate == null || lastCacheUpdate < lastRemoval) { @@ -980,38 +1039,33 @@ class SimklApi : SyncAPI() { setKey(SIMKL_CACHED_LIST_TIME, userId, lastCacheUpdate) AllItemsResponse.merge( getSyncListCached(auth), - getSyncListSince(auth, lastCacheUpdate) + getSyncListSince(auth, lastCacheUpdate), ) } else { debugPrint { "Cached list update in ${this.name}." } getSyncListCached(auth) } - debugPrint { "List sizes: movies=${list?.movies?.size}, shows=${list?.shows?.size}, anime=${list?.anime?.size}" } + debugPrint { "List sizes: movies=${list?.movies?.size}, shows=${list?.shows?.size}, anime=${list?.anime?.size}" } setKey(SIMKL_CACHED_LIST, userId, list) - return list } override suspend fun library(auth: AuthData?): SyncAPI.LibraryMetadata? { val list = getSyncListSmart(auth ?: return null) ?: return null - - val baseMap = - SimklListStatusType.entries - .filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value } - .associate { - it.stringRes to emptyList() - } + val baseMap = SimklListStatusType.entries + .filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value } + .associate { + it.stringRes to emptyList() + } val syncMap = listOf(list.anime, list.movies, list.shows) .flatten() - .groupBy { - it.status - } + .groupBy { it.status } .mapNotNull { (status, list) -> - val stringRes = - status?.let { SimklListStatusType.fromString(it)?.stringRes } - ?: return@mapNotNull null + val stringRes = status?.let { + SimklListStatusType.fromString(it)?.stringRes + } ?: return@mapNotNull null val libraryList = list.map { it.toLibraryItem() } stringRes to libraryList }.toMap() @@ -1037,15 +1091,14 @@ class SimklApi : SyncAPI() { override suspend fun pinRequest(): AuthPinData? { val pinAuthResp = app.get( - "$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrlIdentifier}" + "$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://$redirectUrlIdentifier" ).parsedSafe() ?: return null - return AuthPinData( deviceCode = pinAuthResp.deviceCode, userCode = pinAuthResp.userCode, verificationUrl = pinAuthResp.verificationUrl, expiresIn = pinAuthResp.expiresIn, - interval = pinAuthResp.interval + interval = pinAuthResp.interval, ) } @@ -1053,7 +1106,6 @@ class SimklApi : SyncAPI() { val pinAuthResp = app.get( "$mainUrl/oauth/pin/${payload.userCode}?client_id=$CLIENT_ID" ).parsedSafe() ?: return null - return AuthToken( accessToken = pinAuthResp.accessToken ?: return null, ) @@ -1069,7 +1121,6 @@ class SimklApi : SyncAPI() { val tokenResponse = app.post( "$mainUrl/oauth/token", json = TokenRequest(code) ).parsedSafe() ?: return null - return AuthToken( accessToken = tokenResponse.accessToken, ) @@ -1080,7 +1131,7 @@ class SimklApi : SyncAPI() { return AuthUser( id = user.account.id, name = user.user.name, - profilePicture = user.user.avatar + profilePicture = user.user.avatar, ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 2aadfb13c5a..f00fd803150 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -12,6 +12,7 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.ListView import androidx.appcompat.app.AlertDialog +import com.fasterxml.jackson.annotation.JsonProperty import com.google.android.gms.cast.MediaLoadOptions import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.MediaSeekOptions @@ -34,35 +35,24 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import org.json.JSONObject -/*class SkipOpController(val view: ImageView) : UIController() { - init { - view.setImageResource(R.drawable.exo_controls_fastforward) - view.setOnClickListener { - remoteMediaClient?.let { - val options = MediaSeekOptions.Builder() - .setPosition(it.approximateStreamPosition + 85000) - it.seek(options.build()) - } - } - } -}*/ - private fun RemoteMediaClient.getItemIndex(): Int? { return try { val index = this.mediaQueue.itemIds.indexOf(this.currentItem?.itemId ?: 0) if (index < 0) null else index - } catch (e: Exception) { + } catch (_: Exception) { null } } @@ -89,48 +79,41 @@ class SkipNextEpisodeController(val view: ImageView) : UIController() { } } +@Serializable data class MetadataHolder( - val apiName: String, - val isMovie: Boolean, - val title: String?, - val poster: String?, - val currentEpisodeIndex: Int, - val episodes: List, - val currentLinks: List, - val currentSubtitles: List + @JsonProperty("apiName") @SerialName("apiName") val apiName: String, + @JsonProperty("isMovie") @SerialName("isMovie") val isMovie: Boolean, + @JsonProperty("title") @SerialName("title") val title: String?, + @JsonProperty("poster") @SerialName("poster") val poster: String?, + @JsonProperty("currentEpisodeIndex") @SerialName("currentEpisodeIndex") val currentEpisodeIndex: Int, + @JsonProperty("episodes") @SerialName("episodes") val episodes: List, + @JsonProperty("currentLinks") @SerialName("currentLinks") val currentLinks: List, + @JsonProperty("currentSubtitles") @SerialName("currentSubtitles") val currentSubtitles: List, ) -class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : - UIController() { +class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() { init { view.setImageResource(R.drawable.ic_baseline_playlist_play_24) view.setOnClickListener { - // lateinit var dialog: AlertDialog val holder = getCurrentMetaData() - if (holder != null) { val items = holder.currentLinks if (items.isNotEmpty() && remoteMediaClient?.currentItem != null) { - val subTracks = - remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT } - ?: ArrayList() + val subTracks = remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT } + ?: ArrayList() - val bottomSheetDialogBuilder = - AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack) + val bottomSheetDialogBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack) bottomSheetDialogBuilder.setView(R.layout.sort_bottom_sheet) + val bottomSheetDialog = bottomSheetDialogBuilder.create() bottomSheetDialog.show() - // bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet) - val providerList = - bottomSheetDialog.findViewById(R.id.sort_providers)!! - val subtitleList = - bottomSheetDialog.findViewById(R.id.sort_subtitles)!! + + val providerList = bottomSheetDialog.findViewById(R.id.sort_providers)!! + val subtitleList = bottomSheetDialog.findViewById(R.id.sort_subtitles)!! if (subTracks.isEmpty()) { - bottomSheetDialog.findViewById(R.id.sort_subtitles_holder)?.visibility = - GONE + bottomSheetDialog.findViewById(R.id.sort_subtitles_holder)?.visibility = GONE } else { - val arrayAdapter = - ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) + val arrayAdapter = ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) arrayAdapter.add(view.context.getString(R.string.no_subtitles)) arrayAdapter.addAll(subTracks.mapNotNull { it.name }) @@ -138,10 +121,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi subtitleList.adapter = arrayAdapter val currentTracks = remoteMediaClient?.mediaStatus?.activeTrackIds - - val subtitleIndex = - if (currentTracks == null) 0 else subTracks.map { it.id } - .indexOfFirst { currentTracks.contains(it) } + 1 + val subtitleIndex = if (currentTracks == null) 0 else subTracks.map { it.id } + .indexOfFirst { currentTracks.contains(it) } + 1 subtitleList.setSelection(subtitleIndex) subtitleList.setItemChecked(subtitleIndex, true) @@ -153,9 +134,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi ChromecastSubtitlesFragment.getCurrentSavedStyle().apply { val font = TextTrackStyle() font.setFontFamily(fontFamily ?: "Google Sans") - fontGenericFamily?.let { - font.fontGenericFamily = it - } + fontGenericFamily?.let { font.fontGenericFamily = it } font.windowColor = windowColor font.backgroundColor = backgroundColor @@ -172,7 +151,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi if (!it.status.isSuccess) { Log.e( "CHROMECAST", "Failed with status code:" + - it.status.statusCode + " > " + it.status.statusMessage + it.status.statusCode + " > " + it.status.statusMessage ) } } @@ -181,17 +160,15 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi } } - //https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages.MediaInformation + // https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages.MediaInformation val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl ?: remoteMediaClient?.currentItem?.media?.contentId) - val sortingMethods = - items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" } - .toTypedArray() + val sortingMethods = items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" } + .toTypedArray() val sotringIndex = items.indexOfFirst { it.url == contentUrl } - val arrayAdapter = - ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) + val arrayAdapter = ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) arrayAdapter.addAll(sortingMethods.toMutableList()) providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE @@ -201,10 +178,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi providerList.setOnItemClickListener { _, _, which, _ -> val epData = holder.episodes[holder.currentEpisodeIndex] - fun loadMirror(index: Int) { if (holder.currentLinks.size <= index) return - val mediaItem = getMediaInfo( epData, holder, @@ -214,25 +189,21 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi ) val startAt = remoteMediaClient?.approximateStreamPosition ?: 0 - - //remoteMediaClient.load(mediaItem, true, startAt) try { // THIS IS VERY IMPORTANT BECAUSE WE NEVER WANT TO AUTOLOAD THE NEXT EPISODE val currentIdIndex = remoteMediaClient?.getItemIndex() - val nextId = remoteMediaClient?.mediaQueue?.itemIds?.get( currentIdIndex?.plus(1) ?: 0 ) + if (currentIdIndex == null && nextId != null) { awaitLinks( remoteMediaClient?.queueInsertAndPlayItem( MediaQueueItem.Builder(mediaItem).build(), nextId, startAt, - JSONObject() + JSONObject(), ) - ) { - loadMirror(index + 1) - } + ) { loadMirror(index + 1) } } else { val mediaLoadOptions = MediaLoadOptions.Builder() @@ -244,11 +215,9 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi mediaItem, mediaLoadOptions ) - ) { - loadMirror(index + 1) - } + ) { loadMirror(index + 1) } } - } catch (e: Exception) { + } catch (_: Exception) { val mediaLoadOptions = MediaLoadOptions.Builder() .setPlayPosition(startAt) @@ -259,8 +228,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi } } } - loadMirror(which) + loadMirror(which) bottomSheetDialog.dismissSafe(activity) } } @@ -270,23 +239,19 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi private fun getCurrentMetaData(): MetadataHolder? { return try { - val data = remoteMediaClient?.mediaInfo?.customData?.toString() - data?.toKotlinObject() - } catch (e: Exception) { + val data = remoteMediaClient?.mediaInfo?.customData?.toString() ?: return null + parseJson(data) + } catch (_: Exception) { null } } var isLoadingMore = false - - override fun onMediaStatusUpdated() { super.onMediaStatusUpdated() val meta = getCurrentMetaData() + view.visibility = if ((meta?.currentLinks?.size ?: 0) > 1) VISIBLE else INVISIBLE - view.visibility = if ((meta?.currentLinks?.size - ?: 0) > 1 - ) VISIBLE else INVISIBLE try { if (meta != null && meta.episodes.size > meta.currentEpisodeIndex + 1) { val currentIdIndex = remoteMediaClient?.getItemIndex() ?: return @@ -303,7 +268,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi currentPosition, currentDuration, epData, - meta.episodes.getOrNull(index + 1) + meta.episodes.getOrNull(index + 1), ) } catch (t: Throwable) { logError(t) @@ -314,9 +279,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi ioSafe { val currentLinks = mutableSetOf() val currentSubs = mutableSetOf() - val generator = RepoLinkGenerator(listOf(epData)) - val isSuccessful = safeApiCall { generator.generateLinks( clearCache = false, @@ -329,7 +292,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi currentSubs.add(it) }, offset = 0, - isCasting = true + isCasting = true, ) } @@ -340,32 +303,18 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi val jsonCopy = meta.copy( currentLinks = sortedLinks, currentSubtitles = sortedSubs, - currentEpisodeIndex = index + currentEpisodeIndex = index, ) - val done = - JSONObject(jsonCopy.toJson()) - + val done = JSONObject(jsonCopy.toJson()) val mediaInfo = getMediaInfo( epData, jsonCopy, 0, done, - sortedSubs + sortedSubs, ) - /*fun loadIndex(index: Int) { - println("LOAD INDEX::::: $index") - if (meta.currentLinks.size <= index) return - val info = getMediaInfo( - epData, - meta, - index, - done) - awaitLinks(remoteMediaClient?.load(info, true, 0)) { - loadIndex(index + 1) - } - }*/ activity.runOnUiThread { awaitLinks( remoteMediaClient?.queueAppendItem( @@ -374,7 +323,6 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi ) ) { println("FAILED TO LOAD NEXT ITEM") - // loadIndex(1) } isLoadingMore = false } @@ -397,10 +345,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi class SkipTimeController(val view: ImageView, forwards: Boolean) : UIController() { init { - //val settingsManager = PreferenceManager.getDefaultSharedPreferences() - //val time = settingsManager?.getInt("chromecast_tap_time", 30) ?: 30 val time = 30 - //view.setImageResource(if (forwards) R.drawable.netflix_skip_forward else R.drawable.netflix_skip_back) view.setImageResource(if (forwards) R.drawable.go_forward_30 else R.drawable.go_back_30) view.setOnClickListener { remoteMediaClient?.let { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index ee6170aa53f..f62bad58d91 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -8,11 +8,14 @@ import androidx.annotation.OptIn import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi import androidx.media3.ui.SubtitleView +import com.fasterxml.jackson.annotation.JsonIgnore import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.setSubtitleViewStyle import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF import com.lagradost.cloudstream3.utils.UIHelper.toPx +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable enum class SubtitleStatus { IS_ACTIVE, @@ -32,17 +35,19 @@ enum class SubtitleOrigin { * @param url Url for the subtitle, when EMBEDDED_IN_VIDEO this variable is used as the real backend id * @param headers if empty it will use the base onlineDataSource headers else only the specified headers * @param languageCode usually, tags such as "en", "es-mx", or "zh-hant-TW". But it could be something like "English 4" - * */ + */ +@Serializable data class SubtitleData( - val originalName: String, - val nameSuffix: String, - val url: String, - val origin: SubtitleOrigin, - val mimeType: String, - val headers: Map, - val languageCode: String?, + @SerialName("originalName") val originalName: String, + @SerialName("nameSuffix") val nameSuffix: String, + @SerialName("url") val url: String, + @SerialName("origin") val origin: SubtitleOrigin, + @SerialName("mimeType") val mimeType: String, + @SerialName("headers") val headers: Map, + @SerialName("languageCode") val languageCode: String?, ) { - /** Internal ID for exoplayer, unique for each link*/ + /** Internal ID for media3, unique for each link. */ + @JsonIgnore fun getId(): String { return if (origin == SubtitleOrigin.EMBEDDED_IN_VIDEO) url else "$url|$name" @@ -54,22 +59,22 @@ data class SubtitleData( } /** Tries hard to figure out a valid IETF tag based on language code and name. Will return null if not found. */ + @JsonIgnore fun getIETF_tag(): String? { return fromLanguageToTagIETF(this.languageCode) ?: fromLanguageToTagIETF(this.originalName, halfMatch = true) } - val name = "$originalName $nameSuffix" + @SerialName("name") val name = "$originalName $nameSuffix" /** * Gets the URL, but tries to fix it if it is malformed. */ + @JsonIgnore fun getFixedUrl(): String { // Some extensions fail to include the protocol, this helps with that. val fixedSubUrl = if (this.url.startsWith("//")) { "https:${this.url}" - } else { - this.url - } + } else this.url return fixedSubUrl } } @@ -142,4 +147,4 @@ class PlayerSubtitleHelper { setSubStyle(it) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/Torrent.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/Torrent.kt index 1c24ca7e4f6..7e5d04eb0c7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/Torrent.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/Torrent.kt @@ -9,6 +9,8 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.newExtractorLink +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import torrServer.TorrServer import java.io.File import java.net.ConnectException @@ -32,14 +34,14 @@ object Torrent { /** Returns true if the server is up */ private suspend fun echo(): Boolean { - if(TORRENT_SERVER_URL.isEmpty()) { + if (TORRENT_SERVER_URL.isEmpty()) { return false } return try { app.get( "$TORRENT_SERVER_URL/echo", - ).text().isNotEmpty() - } catch (e: ConnectException) { + ).text.isNotEmpty() + } catch (_: ConnectException) { // `Failed to connect to /127.0.0.1:8090` if the server is down false } catch (t: Throwable) { @@ -52,7 +54,7 @@ object Torrent { /** Gracefully shutdown the server. * should not be used because I am unable to start it again, and the stopTorrentServer() crashes the app */ suspend fun shutdown(): Boolean { - if(TORRENT_SERVER_URL.isEmpty()) { + if (TORRENT_SERVER_URL.isEmpty()) { return false } return try { @@ -68,7 +70,7 @@ object Torrent { /** Lists all torrents by the server */ @Throws private suspend fun list(): Array { - if(TORRENT_SERVER_URL.isEmpty()) { + if (TORRENT_SERVER_URL.isEmpty()) { throw ErrorLoadingException("Not initialized") } return app.post( @@ -83,7 +85,7 @@ object Torrent { /** Drops a single torrent, (I think) this means closing the stream. Returns returns if it is successful */ private suspend fun drop(hash: String): Boolean { - if(TORRENT_SERVER_URL.isEmpty()) { + if (TORRENT_SERVER_URL.isEmpty()) { return false } return try { @@ -104,7 +106,7 @@ object Torrent { /** Removes a single torrent from the server registry */ private suspend fun rem(hash: String): Boolean { - if(TORRENT_SERVER_URL.isEmpty()) { + if (TORRENT_SERVER_URL.isEmpty()) { return false } return try { @@ -126,7 +128,7 @@ object Torrent { /** Removes all torrents from the server, and returns if it is successful */ suspend fun clearAll(): Boolean { - if(TORRENT_SERVER_URL.isEmpty()) { + if (TORRENT_SERVER_URL.isEmpty()) { return true } return try { @@ -164,10 +166,8 @@ object Torrent { /** Gets all the metadata of a torrent, will throw if that hash does not exists * https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/c18f58e51b6738f053261bc863177078aa9c1c98/web/api/torrents.go#L126 */ @Throws - suspend fun get( - hash: String, - ): TorrentStatus { - if(TORRENT_SERVER_URL.isEmpty()) { + suspend fun get(hash: String): TorrentStatus { + if (TORRENT_SERVER_URL.isEmpty()) { throw ErrorLoadingException("Not initialized") } return app.post( @@ -184,7 +184,7 @@ object Torrent { /** Adds a torrent to the server, this is needed for us to get the hash for further modification, as well as start streaming it*/ @Throws private suspend fun add(url: String): TorrentStatus { - if(TORRENT_SERVER_URL.isEmpty()) { + if (TORRENT_SERVER_URL.isEmpty()) { throw ErrorLoadingException("Not initialized") } return app.post( @@ -204,7 +204,7 @@ object Torrent { return true } val port = TorrServer.startTorrentServer(dir, 0) - if(port < 0) { + if (port < 0) { return false } TORRENT_SERVER_URL = "http://127.0.0.1:$port" @@ -278,94 +278,55 @@ object Torrent { // https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/c18f58e51b6738f053261bc863177078aa9c1c98/web/api/torrents.go#L18 // https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/main/web/api/route.go#L7 + @Serializable data class TorrentRequest( - @JsonProperty("action") - val action: String, - @JsonProperty("hash") - val hash: String = "", - @JsonProperty("link") - val link: String = "", - @JsonProperty("title") - val title: String = "", - @JsonProperty("poster") - val poster: String = "", - @JsonProperty("data") - val data: String = "", - @JsonProperty("save_to_db") - val saveToDB: Boolean = false, + @JsonProperty("action") @SerialName("action") val action: String, + @JsonProperty("hash") @SerialName("hash") val hash: String = "", + @JsonProperty("link") @SerialName("link") val link: String = "", + @JsonProperty("title") @SerialName("title") val title: String = "", + @JsonProperty("poster") @SerialName("poster") val poster: String = "", + @JsonProperty("data") @SerialName("data") val data: String = "", + @JsonProperty("save_to_db") @SerialName("save_to_db") val saveToDB: Boolean = false, ) // https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/c18f58e51b6738f053261bc863177078aa9c1c98/torr/state/state.go#L33 // omitempty = nullable + @Serializable data class TorrentStatus( - @JsonProperty("title") - var title: String, - @JsonProperty("poster") - var poster: String, - @JsonProperty("data") - var data: String?, - @JsonProperty("timestamp") - var timestamp: Long, - @JsonProperty("name") - var name: String?, - @JsonProperty("hash") - var hash: String?, - @JsonProperty("stat") - var stat: Int, - @JsonProperty("stat_string") - var statString: String, - @JsonProperty("loaded_size") - var loadedSize: Long?, - @JsonProperty("torrent_size") - var torrentSize: Long?, - @JsonProperty("preloaded_bytes") - var preloadedBytes: Long?, - @JsonProperty("preload_size") - var preloadSize: Long?, - @JsonProperty("download_speed") - var downloadSpeed: Double?, - @JsonProperty("upload_speed") - var uploadSpeed: Double?, - @JsonProperty("total_peers") - var totalPeers: Int?, - @JsonProperty("pending_peers") - var pendingPeers: Int?, - @JsonProperty("active_peers") - var activePeers: Int?, - @JsonProperty("connected_seeders") - var connectedSeeders: Int?, - @JsonProperty("half_open_peers") - var halfOpenPeers: Int?, - @JsonProperty("bytes_written") - var bytesWritten: Long?, - @JsonProperty("bytes_written_data") - var bytesWrittenData: Long?, - @JsonProperty("bytes_read") - var bytesRead: Long?, - @JsonProperty("bytes_read_data") - var bytesReadData: Long?, - @JsonProperty("bytes_read_useful_data") - var bytesReadUsefulData: Long?, - @JsonProperty("chunks_written") - var chunksWritten: Long?, - @JsonProperty("chunks_read") - var chunksRead: Long?, - @JsonProperty("chunks_read_useful") - var chunksReadUseful: Long?, - @JsonProperty("chunks_read_wasted") - var chunksReadWasted: Long?, - @JsonProperty("pieces_dirtied_good") - var piecesDirtiedGood: Long?, - @JsonProperty("pieces_dirtied_bad") - var piecesDirtiedBad: Long?, - @JsonProperty("duration_seconds") - var durationSeconds: Double?, - @JsonProperty("bit_rate") - var bitRate: String?, - @JsonProperty("file_stats") - var fileStats: List?, - @JsonProperty("trackers") - var trackers: List?, + @JsonProperty("title") @SerialName("title") var title: String, + @JsonProperty("poster") @SerialName("poster") var poster: String, + @JsonProperty("data") @SerialName("data") var data: String?, + @JsonProperty("timestamp") @SerialName("timestamp") var timestamp: Long, + @JsonProperty("name") @SerialName("name") var name: String?, + @JsonProperty("hash") @SerialName("hash") var hash: String?, + @JsonProperty("stat") @SerialName("stat") var stat: Int, + @JsonProperty("stat_string") @SerialName("stat_string") var statString: String, + @JsonProperty("loaded_size") @SerialName("loaded_size") var loadedSize: Long?, + @JsonProperty("torrent_size") @SerialName("torrent_size") var torrentSize: Long?, + @JsonProperty("preloaded_bytes") @SerialName("preloaded_bytes") var preloadedBytes: Long?, + @JsonProperty("preload_size") @SerialName("preload_size") var preloadSize: Long?, + @JsonProperty("download_speed") @SerialName("download_speed") var downloadSpeed: Double?, + @JsonProperty("upload_speed") @SerialName("upload_speed") var uploadSpeed: Double?, + @JsonProperty("total_peers") @SerialName("total_peers") var totalPeers: Int?, + @JsonProperty("pending_peers") @SerialName("pending_peers") var pendingPeers: Int?, + @JsonProperty("active_peers") @SerialName("active_peers") var activePeers: Int?, + @JsonProperty("connected_seeders") @SerialName("connected_seeders") var connectedSeeders: Int?, + @JsonProperty("half_open_peers") @SerialName("half_open_peers") var halfOpenPeers: Int?, + @JsonProperty("bytes_written") @SerialName("bytes_written") var bytesWritten: Long?, + @JsonProperty("bytes_written_data") @SerialName("bytes_written_data") var bytesWrittenData: Long?, + @JsonProperty("bytes_read") @SerialName("bytes_read") var bytesRead: Long?, + @JsonProperty("bytes_read_data") @SerialName("bytes_read_data") var bytesReadData: Long?, + @JsonProperty("bytes_read_useful_data") @SerialName("bytes_read_useful_data") var bytesReadUsefulData: Long?, + @JsonProperty("chunks_written") @SerialName("chunks_written") var chunksWritten: Long?, + @JsonProperty("chunks_read") @SerialName("chunks_read") var chunksRead: Long?, + @JsonProperty("chunks_read_useful") @SerialName("chunks_read_useful") var chunksReadUseful: Long?, + @JsonProperty("chunks_read_wasted") @SerialName("chunks_read_wasted") var chunksReadWasted: Long?, + @JsonProperty("pieces_dirtied_good") @SerialName("pieces_dirtied_good") var piecesDirtiedGood: Long?, + @JsonProperty("pieces_dirtied_bad") @SerialName("pieces_dirtied_bad") var piecesDirtiedBad: Long?, + @JsonProperty("duration_seconds") @SerialName("duration_seconds") var durationSeconds: Double?, + @JsonProperty("bit_rate") @SerialName("bit_rate") var bitRate: String?, + @JsonProperty("file_stats") @SerialName("file_stats") var fileStats: List?, + @JsonProperty("trackers") @SerialName("trackers") var trackers: List?, ) { fun streamUrl(url: String): String { val fileName = @@ -381,12 +342,10 @@ object Torrent { } } + @Serializable data class TorrentFileStat( - @JsonProperty("id") - val id: Int?, - @JsonProperty("path") - val path: String?, - @JsonProperty("length") - val length: Long?, + @JsonProperty("id") @SerialName("id") val id: Int?, + @JsonProperty("path") @SerialName("path") val path: String?, + @JsonProperty("length") @SerialName("length") val length: Long?, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index cbf94fd9796..0540e437023 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -21,6 +21,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.ImageLoader.loadImage import com.lagradost.cloudstream3.utils.UiImage +import kotlinx.serialization.Serializable const val START_ACTION_RESUME_LATEST = 1 const val START_ACTION_LOAD_EP = 2 @@ -34,6 +35,7 @@ enum class VideoWatchState { Watched } +@Serializable data class ResultEpisode( val headerName: String, val name: String?, 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 02ee697911f..9510c31b7c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -7,6 +7,7 @@ import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKeyClass import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKeyClass +import com.lagradost.cloudstream3.InternalAPI import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJsonLiteral @@ -170,22 +171,34 @@ object DataStore { } } - fun Context.setKey(path: String, value: T) { + @InternalAPI + fun Context.putStringKey(path: String, value: String?) { try { getSharedPrefs().edit { - putString(path, value?.toJsonLiteral()) + putString(path, value) } } catch (e: Exception) { logError(e) } } + /** Non-reified fallback for binary compat. Prefer the reified overload where possible. */ + fun Context.setKey(path: String, value: T) { + putStringKey(path, value?.toJsonLiteral()) + } + + /** Reified overload, prevents type erasure for generic types. */ + @JvmName("setKeyReified") + inline fun Context.setKey(path: String, value: T) { + putStringKey(path, value.toJsonLiteral()) + } + 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 +206,37 @@ object DataStore { setKey(getFolderName(folder, path), value) } + @Deprecated( + message = "Use parseJson(this) directly instead.", + level = DeprecationLevel.ERROR, + 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.ERROR, + 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 } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 19caead21ee..7f3c5c8869c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.utils import android.content.Context +import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.CloudStreamApp.Companion.context @@ -31,6 +32,12 @@ import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.VideoWatchState import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia import com.lagradost.cloudstream3.utils.downloader.DownloadObjects +import com.lagradost.cloudstream3.utils.serializers.WriteOnlySerializer +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KeepGeneratedSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import java.util.Calendar import java.util.Date import java.util.GregorianCalendar @@ -43,17 +50,18 @@ const val RESULT_WATCH_STATE = "result_watch_state" const val RESULT_WATCH_STATE_DATA = "result_watch_state_data" const val RESULT_SUBSCRIBED_STATE_DATA = "result_subscribed_state_data" const val RESULT_FAVORITES_STATE_DATA = "result_favorites_state_data" -const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes +const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // Changed due to id changes const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching" const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated" const val RESULT_EPISODE = "result_episode" const val RESULT_SEASON = "result_season" const val RESULT_DUB = "result_dub" const val KEY_RESULT_SORT = "result_sort" -const val USER_PINNED_PROVIDERS = "user_pinned_providers" //key for pinned user set +const val USER_PINNED_PROVIDERS = "user_pinned_providers" // Key for pinned user set class UserPreferenceDelegate( - private val key: String, private val default: T //, private val klass: KClass + private val key: String, + private val default: T, ) { private val klass: KClass = default::class private val realKey get() = "${DataStoreHelper.currentAccount}/$key" @@ -63,13 +71,11 @@ class UserPreferenceDelegate( operator fun setValue( self: Any?, property: KProperty<*>, - t: T? + t: T?, ) { if (t == null) { removeKey(realKey) - } else { - setKeyClass(realKey, t) - } + } else setKeyClass(realKey, t) } } @@ -82,7 +88,7 @@ object DataStoreHelper { R.drawable.profile_bg_pink, R.drawable.profile_bg_purple, R.drawable.profile_bg_red, - R.drawable.profile_bg_teal + R.drawable.profile_bg_teal, ) private var searchPreferenceProvidersStrings: List by UserPreferenceDelegate( @@ -112,16 +118,17 @@ object DataStoreHelper { private var searchPreferenceTagsStrings: List by UserPreferenceDelegate( "search_pref_tags", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) + var searchPreferenceTags: List get() = deserializeTv(searchPreferenceTagsStrings) set(value) { searchPreferenceTagsStrings = serializeTv(value) } - private var homePreferenceStrings: List by UserPreferenceDelegate( "home_pref_homepage", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) + var homePreference: List get() = deserializeTv(homePreferenceStrings) set(value) { @@ -132,38 +139,38 @@ object DataStoreHelper { "home_bookmarked_last_list", IntArray(0) ) + var playBackSpeed: Float by UserPreferenceDelegate("playback_speed", 1.0f) var resizeMode: Int by UserPreferenceDelegate("resize_mode", 0) var librarySortingMode: Int by UserPreferenceDelegate( "library_sorting_mode", ListSorting.AlphabeticalA.ordinal ) + private var _resultsSortingMode: Int by UserPreferenceDelegate( "results_sorting_mode", EpisodeSortType.NUMBER_ASC.ordinal ) + var resultsSortingMode: EpisodeSortType get() = EpisodeSortType.entries.getOrNull(_resultsSortingMode) ?: EpisodeSortType.NUMBER_ASC set(value) { _resultsSortingMode = value.ordinal } + @Serializable data class Account( - @JsonProperty("keyIndex") - val keyIndex: Int, - @JsonProperty("name") - val name: String, - @JsonProperty("customImage") - val customImage: String? = null, - @JsonProperty("defaultImageIndex") - val defaultImageIndex: Int, - @JsonProperty("lockPin") - val lockPin: String? = null, + @JsonProperty("keyIndex") @SerialName("keyIndex") val keyIndex: Int, + @JsonProperty("name") @SerialName("name") val name: String, + @JsonProperty("customImage") @SerialName("customImage") val customImage: String? = null, + @JsonProperty("defaultImageIndex") @SerialName("defaultImageIndex") val defaultImageIndex: Int, + @JsonProperty("lockPin") @SerialName("lockPin") val lockPin: String? = null, ) { - val image - get() = customImage?.let { UiImage.Image(it) } ?: profileImages.getOrNull( - defaultImageIndex - )?.let { UiImage.Drawable(it) } ?: UiImage.Drawable(profileImages.first()) + @get:JsonIgnore + val image get() = customImage?.let { UiImage.Image(it) } ?: + profileImages.getOrNull(defaultImageIndex)?.let { + UiImage.Drawable(it) + } ?: UiImage.Drawable(profileImages.first()) } const val TAG = "data_store_helper" @@ -181,14 +188,11 @@ object DataStoreHelper { val key = "$currentAccount/$USER_SELECTED_HOMEPAGE_API" if (value == null) { removeKey(key) - } else { - setKey(key, value) - } + } else setKey(key, value) } fun setAccount(account: Account) { val homepage = currentHomePage - selectedKeyIndex = account.keyIndex AccountManager.updateAccountIds() showToast(context?.getString(R.string.logged_account, account.name) ?: account.name) @@ -206,7 +210,7 @@ object DataStoreHelper { currentAccounts.getOrNull(currentAccounts.indexOfFirst { it.keyIndex == 0 }) ?: Account( keyIndex = 0, name = context.getString(R.string.default_account), - defaultImageIndex = 0 + defaultImageIndex = 0, ) } } @@ -226,24 +230,25 @@ object DataStoreHelper { } ?: accounts.toList()).firstNotNullOfOrNull { account -> if (account.keyIndex == selectedKeyIndex) { account - } else { - null - } + } else null } } + @Serializable data class PosDur( - @JsonProperty("position") val position: Long, - @JsonProperty("duration") val duration: Long + @JsonProperty("position") @SerialName("position") val position: Long, + @JsonProperty("duration") @SerialName("duration") val duration: Long, ) fun PosDur.fixVisual(): PosDur { if (duration <= 0) return PosDur(0, duration) val percentage = position * 100 / duration - if (percentage <= 1) return PosDur(0, duration) - if (percentage <= 5) return PosDur(5 * duration / 100, duration) - if (percentage >= 95) return PosDur(duration, duration) - return this + return when { + percentage <= 1 -> PosDur(0, duration) + percentage <= 5 -> PosDur(5 * duration / 100, duration) + percentage >= 95 -> PosDur(duration, duration) + else -> this + } } fun Int.toYear(): Date = @@ -251,30 +256,24 @@ object DataStoreHelper { /** * Used to display notifications on new episodes and posters in library. - **/ - abstract class LibrarySearchResponse( - @JsonProperty("id") override var id: Int?, - @JsonProperty("latestUpdatedTime") open val latestUpdatedTime: Long, - @JsonProperty("name") override val name: String, - @JsonProperty("url") override val url: String, - @JsonProperty("apiName") override val apiName: String, - @JsonProperty("type") override var type: TvType?, - @JsonProperty("posterUrl") override var posterUrl: String?, - @JsonProperty("year") open val year: Int?, - @JsonProperty("syncData") open val syncData: Map?, - @JsonProperty("quality") override var quality: SearchQuality?, - @JsonProperty("posterHeaders") override var posterHeaders: Map?, - @JsonProperty("plot") open val plot: String? = null, - @JsonProperty("score") override var score: Score? = null, - @JsonProperty("tags") open val tags: List? = null, - ) : SearchResponse { - @JsonProperty("rating", access = JsonProperty.Access.WRITE_ONLY) + */ + interface LibrarySearchResponse : SearchResponse { + val latestUpdatedTime: Long + val year: Int? + val syncData: Map? + val plot: String? + val tags: List? + + @get:JsonProperty("rating", access = JsonProperty.Access.WRITE_ONLY) + @set:JsonProperty("rating", access = JsonProperty.Access.WRITE_ONLY) + @SerialName("rating") @Deprecated( "`rating` is the old scoring system, use score instead", replaceWith = ReplaceWith("score"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) - var rating: Int? = null + var rating: Int? + get() = null set(value) { if (value != null) { @Suppress("DEPRECATION_ERROR") @@ -283,39 +282,32 @@ object DataStoreHelper { } } + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = SubscribedData.Serializer::class) data class SubscribedData( - @JsonProperty("subscribedTime") val subscribedTime: Long, - @JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map, - override var id: Int?, - override val latestUpdatedTime: Long, - override val name: String, - override val url: String, - override val apiName: String, - override var type: TvType?, - override var posterUrl: String?, - override val year: Int?, - override val syncData: Map? = null, - override var quality: SearchQuality? = null, - override var posterHeaders: Map? = null, - override val plot: String? = null, - override var score: Score? = null, - override val tags: List? = null, - ) : LibrarySearchResponse( - id, - latestUpdatedTime, - name, - url, - apiName, - type, - posterUrl, - year, - syncData, - quality, - posterHeaders, - plot, - score, - tags - ) { + @JsonProperty("subscribedTime") @SerialName("subscribedTime") val subscribedTime: Long, + @JsonProperty("lastSeenEpisodeCount") @SerialName("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map, + @JsonProperty("id") @SerialName("id") override var id: Int?, + @JsonProperty("latestUpdatedTime") @SerialName("latestUpdatedTime") override val latestUpdatedTime: Long, + @JsonProperty("name") @SerialName("name") override val name: String, + @JsonProperty("url") @SerialName("url") override val url: String, + @JsonProperty("apiName") @SerialName("apiName") override val apiName: String, + @JsonProperty("type") @SerialName("type") override var type: TvType?, + @JsonProperty("posterUrl") @SerialName("posterUrl") override var posterUrl: String?, + @JsonProperty("year") @SerialName("year") override val year: Int?, + @JsonProperty("syncData") @SerialName("syncData") override val syncData: Map? = null, + @JsonProperty("quality") @SerialName("quality") override var quality: SearchQuality? = null, + @JsonProperty("posterHeaders") @SerialName("posterHeaders") override var posterHeaders: Map? = null, + @JsonProperty("plot") @SerialName("plot") override val plot: String? = null, + @JsonProperty("score") @SerialName("score") override var score: Score? = null, + @JsonProperty("tags") @SerialName("tags") override val tags: List? = null, + ) : LibrarySearchResponse { + object Serializer : WriteOnlySerializer( + SubscribedData.generatedSerializer(), + setOf("rating2"), + ) + fun toLibraryItem(): SyncAPI.LibraryItem? { return SyncAPI.LibraryItem( name, @@ -334,41 +326,36 @@ object DataStoreHelper { this.id, plot = this.plot, score = this.score, - tags = this.tags + tags = this.tags, ) } } + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = BookmarkedData.Serializer::class) data class BookmarkedData( - @JsonProperty("bookmarkedTime") val bookmarkedTime: Long, - override var id: Int?, - override val latestUpdatedTime: Long, - override val name: String, - override val url: String, - override val apiName: String, - override var type: TvType?, - override var posterUrl: String?, - override val year: Int?, - override val syncData: Map? = null, - override var quality: SearchQuality? = null, - override var posterHeaders: Map? = null, - override val plot: String? = null, - override var score: Score? = null, - override val tags: List? = null, - ) : LibrarySearchResponse( - id, - latestUpdatedTime, - name, - url, - apiName, - type, - posterUrl, - year, - syncData, - quality, - posterHeaders, - plot - ) { + @JsonProperty("bookmarkedTime") @SerialName("bookmarkedTime") val bookmarkedTime: Long, + @JsonProperty("id") @SerialName("id") override var id: Int?, + @JsonProperty("latestUpdatedTime") @SerialName("latestUpdatedTime") override val latestUpdatedTime: Long, + @JsonProperty("name") @SerialName("name") override val name: String, + @JsonProperty("url") @SerialName("url") override val url: String, + @JsonProperty("apiName") @SerialName("apiName") override val apiName: String, + @JsonProperty("type") @SerialName("type") override var type: TvType?, + @JsonProperty("posterUrl") @SerialName("posterUrl") override var posterUrl: String?, + @JsonProperty("year") @SerialName("year") override val year: Int?, + @JsonProperty("syncData") @SerialName("syncData") override val syncData: Map? = null, + @JsonProperty("quality") @SerialName("quality") override var quality: SearchQuality? = null, + @JsonProperty("posterHeaders") @SerialName("posterHeaders") override var posterHeaders: Map? = null, + @JsonProperty("plot") @SerialName("plot") override val plot: String? = null, + @JsonProperty("score") @SerialName("score") override var score: Score? = null, + @JsonProperty("tags") @SerialName("tags") override val tags: List? = null, + ) : LibrarySearchResponse { + object Serializer : WriteOnlySerializer( + BookmarkedData.generatedSerializer(), + setOf("rating"), + ) + fun toLibraryItem(id: String): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( name, @@ -387,41 +374,36 @@ object DataStoreHelper { this.id, plot = this.plot, score = this.score, - tags = this.tags + tags = this.tags, ) } } + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = FavoritesData.Serializer::class) data class FavoritesData( - @JsonProperty("favoritesTime") val favoritesTime: Long, - override var id: Int?, - override val latestUpdatedTime: Long, - override val name: String, - override val url: String, - override val apiName: String, - override var type: TvType?, - override var posterUrl: String?, - override val year: Int?, - override val syncData: Map? = null, - override var quality: SearchQuality? = null, - override var posterHeaders: Map? = null, - override val plot: String? = null, - override var score: Score? = null, - override val tags: List? = null, - ) : LibrarySearchResponse( - id, - latestUpdatedTime, - name, - url, - apiName, - type, - posterUrl, - year, - syncData, - quality, - posterHeaders, - plot - ) { + @JsonProperty("favoritesTime") @SerialName("favoritesTime") val favoritesTime: Long, + @JsonProperty("id") @SerialName("id") override var id: Int?, + @JsonProperty("latestUpdatedTime") @SerialName("latestUpdatedTime") override val latestUpdatedTime: Long, + @JsonProperty("name") @SerialName("name") override val name: String, + @JsonProperty("url") @SerialName("url") override val url: String, + @JsonProperty("apiName") @SerialName("apiName") override val apiName: String, + @JsonProperty("type") @SerialName("type") override var type: TvType?, + @JsonProperty("posterUrl") @SerialName("posterUrl") override var posterUrl: String?, + @JsonProperty("year") @SerialName("year") override val year: Int?, + @JsonProperty("syncData") @SerialName("syncData") override val syncData: Map? = null, + @JsonProperty("quality") @SerialName("quality") override var quality: SearchQuality? = null, + @JsonProperty("posterHeaders") @SerialName("posterHeaders") override var posterHeaders: Map? = null, + @JsonProperty("plot") @SerialName("plot") override val plot: String? = null, + @JsonProperty("score") @SerialName("score") override var score: Score? = null, + @JsonProperty("tags") @SerialName("tags") override val tags: List? = null, + ) : LibrarySearchResponse { + object Serializer : WriteOnlySerializer( + FavoritesData.generatedSerializer(), + setOf("rating"), + ) + fun toLibraryItem(): SyncAPI.LibraryItem? { return SyncAPI.LibraryItem( name, @@ -440,31 +422,32 @@ object DataStoreHelper { this.id, plot = this.plot, score = this.score, - tags = this.tags + tags = this.tags, ) } } + @Serializable data class ResumeWatchingResult( - @JsonProperty("name") override val name: String, - @JsonProperty("url") override val url: String, - @JsonProperty("apiName") override val apiName: String, - @JsonProperty("type") override var type: TvType? = null, - @JsonProperty("posterUrl") override var posterUrl: String?, - @JsonProperty("watchPos") val watchPos: PosDur?, - @JsonProperty("id") override var id: Int?, - @JsonProperty("parentId") val parentId: Int?, - @JsonProperty("episode") val episode: Int?, - @JsonProperty("season") val season: Int?, - @JsonProperty("isFromDownload") val isFromDownload: Boolean, - @JsonProperty("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, - @JsonProperty("score") override var score: Score? = null, + @JsonProperty("name") @SerialName("name") override val name: String, + @JsonProperty("url") @SerialName("url") override val url: String, + @JsonProperty("apiName") @SerialName("apiName") override val apiName: String, + @JsonProperty("type") @SerialName("type") override var type: TvType? = null, + @JsonProperty("posterUrl") @SerialName("posterUrl") override var posterUrl: String?, + @JsonProperty("watchPos") @SerialName("watchPos") val watchPos: PosDur?, + @JsonProperty("id") @SerialName("id") override var id: Int?, + @JsonProperty("parentId") @SerialName("parentId") val parentId: Int?, + @JsonProperty("episode") @SerialName("episode") val episode: Int?, + @JsonProperty("season") @SerialName("season") val season: Int?, + @JsonProperty("isFromDownload") @SerialName("isFromDownload") val isFromDownload: Boolean, + @JsonProperty("quality") @SerialName("quality") override var quality: SearchQuality? = null, + @JsonProperty("posterHeaders") @SerialName("posterHeaders") override var posterHeaders: Map? = null, + @JsonProperty("score") @SerialName("score") override var score: Score? = null, ) : SearchResponse /** * A datastore wide account for future implementations of a multiple account system - **/ + */ fun getAllWatchStateIds(): List? { val folder = "$currentAccount/$RESULT_WATCH_STATE" @@ -500,7 +483,7 @@ object DataStoreHelper { } fun migrateResumeWatching() { - // if (getKey(RESULT_RESUME_WATCHING_HAS_MIGRATED, false) != true) { + // if (getKey(RESULT_RESUME_WATCHING_HAS_MIGRATED, false) != true) { setKey(RESULT_RESUME_WATCHING_HAS_MIGRATED, true) getAllResumeStateIdsOld()?.forEach { id -> getLastWatchedOld(id)?.let { @@ -510,12 +493,12 @@ object DataStoreHelper { it.episode, it.season, it.isFromDownload, - it.updateTime + it.updateTime, ) removeLastWatchedOld(it.parentId) } } - //} + // } } fun setLastWatched( @@ -536,7 +519,7 @@ object DataStoreHelper { episode, season, updateTime ?: System.currentTimeMillis(), - isFromDownload + isFromDownload, ) ) } @@ -553,7 +536,7 @@ object DataStoreHelper { fun getLastWatched(id: Int?): DownloadObjects.ResumeWatching? { if (id == null) return null - return getKey( + return getKey( "$currentAccount/$RESULT_RESUME_WATCHING", id.toString(), ) @@ -561,7 +544,7 @@ object DataStoreHelper { private fun getLastWatchedOld(id: Int?): DownloadObjects.ResumeWatching? { if (id == null) return null - return getKey( + return getKey( "$currentAccount/$RESULT_RESUME_WATCHING_OLD", id.toString(), ) @@ -575,18 +558,18 @@ object DataStoreHelper { fun getBookmarkedData(id: Int?): BookmarkedData? { if (id == null) return null - return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) + return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) } fun getAllBookmarkedData(): List { return getKeys("$currentAccount/$RESULT_WATCH_STATE_DATA")?.mapNotNull { - getKey(it) + getKey(it) } ?: emptyList() } fun getAllSubscriptions(): List { return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull { - getKey(it) + getKey(it) } ?: emptyList() } @@ -603,7 +586,7 @@ object DataStoreHelper { if (id == null || data == null || episodeResponse == null) return val newData = data.copy( latestUpdatedTime = unixTimeMS, - lastSeenEpisodeCount = episodeResponse.getLatestEpisodes() + lastSeenEpisodeCount = episodeResponse.getLatestEpisodes(), ) setKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString(), newData) } @@ -616,12 +599,12 @@ object DataStoreHelper { fun getSubscribedData(id: Int?): SubscribedData? { if (id == null) return null - return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString()) + return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString()) } fun getAllFavorites(): List { return getKeys("$currentAccount/$RESULT_FAVORITES_STATE_DATA")?.mapNotNull { - getKey(it) + getKey(it) } ?: emptyList() } @@ -639,7 +622,7 @@ object DataStoreHelper { fun getFavoritesData(id: Int?): FavoritesData? { if (id == null) return null - return getKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString()) + return getKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString()) } fun setViewPos(id: Int?, pos: Long, dur: Long) { @@ -687,7 +670,7 @@ object DataStoreHelper { resumeMeta.id, resumeMeta.episode, resumeMeta.season, - isFromDownload = false + isFromDownload = false, ) } @@ -697,7 +680,7 @@ object DataStoreHelper { resumeMeta.id, resumeMeta.episode, resumeMeta.season, - isFromDownload = true + isFromDownload = true, ) } } @@ -706,17 +689,16 @@ object DataStoreHelper { fun getViewPos(id: Int?): PosDur? { if (id == null) return null - return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null) + return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null) } fun getVideoWatchState(id: Int?): VideoWatchState? { if (id == null) return null - return getKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString(), null) + return getKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString(), null) } fun setVideoWatchState(id: Int?, watchState: VideoWatchState) { if (id == null) return - // None == No key if (watchState == VideoWatchState.None) { removeKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString()) @@ -727,7 +709,7 @@ object DataStoreHelper { fun getDub(id: Int): DubStatus? { return DubStatus.entries - .getOrNull(getKey("$currentAccount/$RESULT_DUB", id.toString(), -1) ?: -1) + .getOrNull(getKey("$currentAccount/$RESULT_DUB", id.toString(), -1) ?: -1) } fun setDub(id: Int, status: DubStatus) { @@ -748,13 +730,13 @@ object DataStoreHelper { getKey( "$currentAccount/$RESULT_WATCH_STATE", id.toString(), - null + null, ) ) } fun getResultSeason(id: Int): Int? { - return getKey("$currentAccount/$RESULT_SEASON", id.toString(), null) + return getKey("$currentAccount/$RESULT_SEASON", id.toString(), null) } fun setResultSeason(id: Int, value: Int?) { @@ -762,7 +744,7 @@ object DataStoreHelper { } fun getResultEpisode(id: Int): Int? { - return getKey("$currentAccount/$RESULT_EPISODE", id.toString(), null) + return getKey("$currentAccount/$RESULT_EPISODE", id.toString(), null) } fun setResultEpisode(id: Int, value: Int?) { @@ -775,12 +757,11 @@ object DataStoreHelper { fun getSync(id: Int, idPrefixes: List): List { return idPrefixes.map { idPrefix -> - getKey("${idPrefix}_sync", id.toString()) + getKey("${idPrefix}_sync", id.toString()) } } var pinnedProviders: Array - get() = getKey(USER_PINNED_PROVIDERS) ?: emptyArray() + get() = getKey>(USER_PINNED_PROVIDERS) ?: emptyArray() set(value) = setKey(USER_PINNED_PROVIDERS, value) - } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index cb5c1b25c25..517528e3d10 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -72,7 +72,7 @@ object SyncUtil { // Gogoanime, Twistmoe and 9anime val url = "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/pages/$site/$slug.json" val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text() - val mapped = tryParseJson(response) + val mapped = tryParseJson(response) val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt index 25a9fdf2a4f..9dd478ab4ed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt @@ -1,23 +1,33 @@ package com.lagradost.cloudstream3.utils.downloader import android.net.Uri +import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.Score +import com.lagradost.cloudstream3.SkipSerializationTest import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.services.DownloadQueueService import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.serializers.UriSerializer +import com.lagradost.cloudstream3.utils.serializers.WriteOnlySerializer import com.lagradost.safefile.SafeFile +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KeepGeneratedSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import java.io.IOException import java.io.OutputStream import java.util.Objects object DownloadObjects { /** An item can either be something to resume or something new to start */ + @Serializable data class DownloadQueueWrapper( - @JsonProperty("resumePackage") val resumePackage: DownloadResumePackage?, - @JsonProperty("downloadItem") val downloadItem: DownloadQueueItem?, + @JsonProperty("resumePackage") @SerialName("resumePackage") val resumePackage: DownloadResumePackage?, + @JsonProperty("downloadItem") @SerialName("downloadItem") val downloadItem: DownloadQueueItem?, ) { init { assert(resumePackage != null || downloadItem != null) { @@ -26,56 +36,66 @@ object DownloadObjects { } /** Loop through the current download instances to see if it is currently downloading. Also includes link loading. */ + @JsonIgnore fun isCurrentlyDownloading(): Boolean { return DownloadQueueService.downloadInstances.value.any { it.downloadQueueWrapper.id == this.id } } - @JsonProperty("id") + @JsonProperty("id") @SerialName("id") val id = resumePackage?.item?.ep?.id ?: downloadItem!!.episode.id - @JsonProperty("parentId") + @JsonProperty("parentId") @SerialName("parentId") val parentId = resumePackage?.item?.ep?.parentId ?: downloadItem!!.episode.parentId } /** General data about the episode and show to start a download from. */ + @Serializable data class DownloadQueueItem( - @JsonProperty("episode") val episode: ResultEpisode, - @JsonProperty("isMovie") val isMovie: Boolean, - @JsonProperty("resultName") val resultName: String, - @JsonProperty("resultType") val resultType: TvType, - @JsonProperty("resultPoster") val resultPoster: String?, - @JsonProperty("apiName") val apiName: String, - @JsonProperty("resultId") val resultId: Int, - @JsonProperty("resultUrl") val resultUrl: String, - @JsonProperty("links") val links: List? = null, - @JsonProperty("subs") val subs: List? = null, + @JsonProperty("episode") @SerialName("episode") val episode: ResultEpisode, + @JsonProperty("isMovie") @SerialName("isMovie") val isMovie: Boolean, + @JsonProperty("resultName") @SerialName("resultName") val resultName: String, + @JsonProperty("resultType") @SerialName("resultType") val resultType: TvType, + @JsonProperty("resultPoster") @SerialName("resultPoster") val resultPoster: String?, + @JsonProperty("apiName") @SerialName("apiName") val apiName: String, + @JsonProperty("resultId") @SerialName("resultId") val resultId: Int, + @JsonProperty("resultUrl") @SerialName("resultUrl") val resultUrl: String, + @JsonProperty("links") @SerialName("links") val links: List? = null, + @JsonProperty("subs") @SerialName("subs") val subs: List? = null, ) { fun toWrapper(): DownloadQueueWrapper { return DownloadQueueWrapper(null, this) } } + interface DownloadCached { + val id: Int + } - abstract class DownloadCached( - @JsonProperty("id") open val id: Int, - ) - + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @KeepGeneratedSerializer + @Serializable(with = DownloadEpisodeCached.Serializer::class) data class DownloadEpisodeCached( - @JsonProperty("name") val name: String?, - @JsonProperty("poster") val poster: String?, - @JsonProperty("episode") val episode: Int, - @JsonProperty("season") val season: Int?, - @JsonProperty("parentId") val parentId: Int, - @JsonProperty("score") var score: Score? = null, - @JsonProperty("description") val description: String?, - @JsonProperty("cacheTime") val cacheTime: Long, - override val id: Int, - ) : DownloadCached(id) { + @JsonProperty("name") @SerialName("name") val name: String?, + @JsonProperty("poster") @SerialName("poster") val poster: String?, + @JsonProperty("episode") @SerialName("episode") val episode: Int, + @JsonProperty("season") @SerialName("season") val season: Int?, + @JsonProperty("parentId") @SerialName("parentId") val parentId: Int, + @JsonProperty("score") @SerialName("score") var score: Score? = null, + @JsonProperty("description") @SerialName("description") val description: String?, + @JsonProperty("cacheTime") @SerialName("cacheTime") val cacheTime: Long, + @JsonProperty("id") @SerialName("id") override val id: Int, + ) : DownloadCached { + object Serializer : WriteOnlySerializer( + DownloadEpisodeCached.generatedSerializer(), + setOf("rating"), + ) + @JsonProperty("rating", access = JsonProperty.Access.WRITE_ONLY) + @SerialName("rating") @Deprecated( "`rating` is the old scoring system, use score instead", replaceWith = ReplaceWith("score"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) var rating: Int? = null set(value) { @@ -87,74 +107,81 @@ object DownloadObjects { } /** What to display to the user for a downloaded show/movie. Includes info such as name, poster and url */ + @Serializable data class DownloadHeaderCached( - @JsonProperty("apiName") val apiName: String, - @JsonProperty("url") val url: String, - @JsonProperty("type") val type: TvType, - @JsonProperty("name") val name: String, - @JsonProperty("poster") val poster: String?, - @JsonProperty("cacheTime") val cacheTime: Long, - override val id: Int, - ) : DownloadCached(id) - + @JsonProperty("apiName") @SerialName("apiName") val apiName: String, + @JsonProperty("url") @SerialName("url") val url: String, + @JsonProperty("type") @SerialName("type") val type: TvType, + @JsonProperty("name") @SerialName("name") val name: String, + @JsonProperty("poster") @SerialName("poster") val poster: String?, + @JsonProperty("cacheTime") @SerialName("cacheTime") val cacheTime: Long, + @JsonProperty("id") @SerialName("id") override val id: Int, + ) : DownloadCached + + @Serializable data class DownloadResumePackage( - @JsonProperty("item") val item: DownloadItem, + @JsonProperty("item") @SerialName("item") val item: DownloadItem, /** Tills which link should get resumed */ - @JsonProperty("linkIndex") val linkIndex: Int?, + @JsonProperty("linkIndex") @SerialName("linkIndex") val linkIndex: Int?, ) { fun toWrapper(): DownloadQueueWrapper { return DownloadQueueWrapper(this, null) } } + @Serializable data class DownloadItem( - @JsonProperty("source") val source: String?, - @JsonProperty("folder") val folder: String?, - @JsonProperty("ep") val ep: DownloadEpisodeMetadata, - @JsonProperty("links") val links: List, + @JsonProperty("source") @SerialName("source") val source: String?, + @JsonProperty("folder") @SerialName("folder") val folder: String?, + @JsonProperty("ep") @SerialName("ep") val ep: DownloadEpisodeMetadata, + @JsonProperty("links") @SerialName("links") val links: List, ) /** Metadata for a specific episode and how to display it. */ + @Serializable data class DownloadEpisodeMetadata( - @JsonProperty("id") val id: Int, - @JsonProperty("parentId") val parentId: Int, - @JsonProperty("mainName") val mainName: String, - @JsonProperty("sourceApiName") val sourceApiName: String?, - @JsonProperty("poster") val poster: String?, - @JsonProperty("name") val name: String?, - @JsonProperty("season") val season: Int?, - @JsonProperty("episode") val episode: Int?, - @JsonProperty("type") val type: TvType?, + @JsonProperty("id") @SerialName("id") val id: Int, + @JsonProperty("parentId") @SerialName("parentId") val parentId: Int, + @JsonProperty("mainName") @SerialName("mainName") val mainName: String, + @JsonProperty("sourceApiName") @SerialName("sourceApiName") val sourceApiName: String?, + @JsonProperty("poster") @SerialName("poster") val poster: String?, + @JsonProperty("name") @SerialName("name") val name: String?, + @JsonProperty("season") @SerialName("season") val season: Int?, + @JsonProperty("episode") @SerialName("episode") val episode: Int?, + @JsonProperty("type") @SerialName("type") val type: TvType?, ) - + @Serializable data class DownloadedFileInfo( - @JsonProperty("totalBytes") val totalBytes: Long, - @JsonProperty("relativePath") val relativePath: String, - @JsonProperty("displayName") val displayName: String, - @JsonProperty("extraInfo") val extraInfo: String? = null, - @JsonProperty("basePath") val basePath: String? = null, // null is for legacy downloads. See getBasePath() + @JsonProperty("totalBytes") @SerialName("totalBytes") val totalBytes: Long, + @JsonProperty("relativePath") @SerialName("relativePath") val relativePath: String, + @JsonProperty("displayName") @SerialName("displayName") val displayName: String, + @JsonProperty("extraInfo") @SerialName("extraInfo") val extraInfo: String? = null, + @JsonProperty("basePath") @SerialName("basePath") val basePath: String? = null, // null is for legacy downloads. See getBasePath() // Hash of the link associated with this DownloadFile, used so not override old data in the DownloadedFileInfo - @JsonProperty("linkHash") val linkHash : Int? = null + @JsonProperty("linkHash") @SerialName("linkHash") val linkHash : Int? = null, ) + @Serializable + @SkipSerializationTest // Uri has issues with Jackson data class DownloadedFileInfoResult( - @JsonProperty("fileLength") val fileLength: Long, - @JsonProperty("totalBytes") val totalBytes: Long, - @JsonProperty("path") val path: Uri, + @JsonProperty("fileLength") @SerialName("fileLength") val fileLength: Long, + @JsonProperty("totalBytes") @SerialName("totalBytes") val totalBytes: Long, + @JsonProperty("path") @SerialName("path") + @Serializable(with = UriSerializer::class) + val path: Uri, ) - + @Serializable data class ResumeWatching( - @JsonProperty("parentId") val parentId: Int, - @JsonProperty("episodeId") val episodeId: Int?, - @JsonProperty("episode") val episode: Int?, - @JsonProperty("season") val season: Int?, - @JsonProperty("updateTime") val updateTime: Long, - @JsonProperty("isFromDownload") val isFromDownload: Boolean, + @JsonProperty("parentId") @SerialName("parentId") val parentId: Int, + @JsonProperty("episodeId") @SerialName("episodeId") val episodeId: Int?, + @JsonProperty("episode") @SerialName("episode") val episode: Int?, + @JsonProperty("season") @SerialName("season") val season: Int?, + @JsonProperty("updateTime") @SerialName("updateTime") val updateTime: Long, + @JsonProperty("isFromDownload") @SerialName("isFromDownload") val isFromDownload: Boolean, ) - data class DownloadStatus( /** if you should retry with the same args and hope for a better result */ val retrySame: Boolean, @@ -164,20 +191,19 @@ object DownloadObjects { val success: Boolean, ) - data class CreateNotificationMetadata( val type: VideoDownloadManager.DownloadType, val bytesDownloaded: Long, val bytesTotal: Long, val hlsProgress: Long? = null, val hlsTotal: Long? = null, - val bytesPerSecond: Long + val bytesPerSecond: Long, ) data class StreamData( private val fileLength: Long, val file: SafeFile, - //val fileStream: OutputStream, + // val fileStream: OutputStream, ) { @Throws(IOException::class) fun open(): OutputStream { @@ -198,9 +224,11 @@ object DownloadObjects { val exists: Boolean get() = file.exists() == true } - - /** bytes have the size end-start where the byte range is [start,end) - * note that ByteArray is a pointer and therefore cant be stored without cloning it */ + /** + * Bytes have the size end-start where the byte range is [start,end) + * note that ByteArray is a pointer and therefore can't be stored + * without cloning it. + */ data class LazyStreamDownloadResponse( val bytes: ByteArray, val startByte: Long, @@ -221,4 +249,4 @@ object DownloadObjects { return Objects.hash(startByte, endByte) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadQueueManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadQueueManager.kt index f3866408833..58aae29d348 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadQueueManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadQueueManager.kt @@ -53,7 +53,7 @@ object DownloadQueueManager { fun init(context: Context) { ioSafe { _queue.collect { queue -> - setKey(QUEUE_KEY, queue) + setKey>(QUEUE_KEY, queue) } } @@ -247,4 +247,4 @@ object DownloadQueueManager { localQueue.copyOf() } } -} \ No newline at end of file +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 73e0228c9ca..6888c1ef41d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ junit = "4.13.2" junitKtx = "1.3.0" junitVersion = "1.3.0" juniversalchardet = "2.5.0" -kotlinGradlePlugin = "2.3.20" +kotlinGradlePlugin = "2.4.0" kotlinxAtomicfu = "0.33.0" kotlinxCollectionsImmutable = "0.4.0" kotlinxCoroutinesCore = "1.11.0" diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index b47724458ac..f8a644137e5 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -39,6 +39,8 @@ import kotlinx.datetime.format.byUnicodePattern import kotlinx.datetime.format.char import kotlinx.datetime.format.parse import kotlinx.datetime.toInstant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlin.io.encoding.Base64 import kotlin.jvm.JvmName @@ -196,12 +198,12 @@ object APIHolder { referer = referer, cacheTime = 0 ) - .text() + .text .substringAfter("releases/") .substringBefore("/") val recapToken = app.get("https://www.google.com/recaptcha/api2/anchor?ar=1&hl=en&size=invisible&cb=cs3&k=$key&co=$domain&v=$vToken") - .document() + .document .selectFirst("#recaptcha-token")?.attr("value") if (recapToken != null) { return app.post( @@ -214,7 +216,7 @@ object APIHolder { "sa" to "", "reason" to "q" ), cacheTime = 0 - ).text() + ).text .substringAfter("rresp\",\"") .substringBefore("\"") } @@ -393,18 +395,19 @@ const val PROVIDER_STATUS_SLOW = 2 const val PROVIDER_STATUS_OK = 1 const val PROVIDER_STATUS_DOWN = 0 +@Serializable data class ProvidersInfoJson( - @JsonProperty("name") var name: String, - @JsonProperty("url") var url: String, - @JsonProperty("credentials") var credentials: String? = null, - @JsonProperty("status") var status: Int, + @JsonProperty("name") @SerialName("name") var name: String, + @JsonProperty("url") @SerialName("url") var url: String, + @JsonProperty("credentials") @SerialName("credentials") var credentials: String? = null, + @JsonProperty("status") @SerialName("status") var status: Int, ) +@Serializable data class SettingsJson( - @JsonProperty("enableAdult") var enableAdult: Boolean = false, + @JsonProperty("enableAdult") @SerialName("enableAdult") var enableAdult: Boolean = false, ) - data class MainPageData( val name: String, val data: String, @@ -870,10 +873,10 @@ enum class DubStatus(val id: Int) { * of this as a decimal class specifically for ratings. * */ @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) +@Serializable class Score private constructor( /** Decimal between [0, 10^9] representing the min score and max score respectively */ - @JsonProperty("data") - private val data: Int, + @JsonProperty("data") @SerialName("data") private val data: Int, ) { override fun hashCode(): Int = this.data.hashCode() override fun equals(other: Any?): Boolean = other is Score && this.data == other.data @@ -1199,9 +1202,10 @@ suspend fun newSubtitleFile( * @see newAudioFile * */ @ConsistentCopyVisibility +@Serializable data class AudioFile internal constructor( - var url: String, - var headers: Map? = null + @JsonProperty("url") @SerialName("url") var url: String, + @JsonProperty("headers") @SerialName("headers") var headers: Map? = null, ) /** Creates an AudioFile with optional initializer for setting additional properties. @@ -2178,10 +2182,11 @@ data class NextAiring( * @param name To be shown next to the season like "Season $displaySeason $name" but if displaySeason is null then "$name" * @param displaySeason What to be displayed next to the season name, if null then the name is the only thing shown. * */ +@Serializable data class SeasonData( - val season: Int, - val name: String? = null, - val displaySeason: Int? = null, // will use season if null + @JsonProperty("season") @SerialName("season") val season: Int, + @JsonProperty("name") @SerialName("name") val name: String? = null, + @JsonProperty("displaySeason") @SerialName("displaySeason") val displaySeason: Int? = null, // will use season if null ) /** Abstract interface of EpisodeResponse */ @@ -2739,32 +2744,38 @@ data class Tracker( val cover: String? = null, ) +@Serializable data class AniSearch( - @JsonProperty("data") var data: Data? = Data() + @JsonProperty("data") @SerialName("data") var data: Data? = Data(), ) { + @Serializable data class Data( - @JsonProperty("Page") var page: Page? = Page() + @JsonProperty("Page") @SerialName("Page") var page: Page? = Page(), ) { + @Serializable data class Page( - @JsonProperty("media") var media: ArrayList = arrayListOf() + @JsonProperty("media") @SerialName("media") var media: ArrayList = arrayListOf(), ) { + @Serializable data class Media( - @JsonProperty("title") var title: Title? = null, - @JsonProperty("id") var id: Int? = null, - @JsonProperty("idMal") var idMal: Int? = null, - @JsonProperty("seasonYear") var seasonYear: Int? = null, - @JsonProperty("format") var format: String? = null, - @JsonProperty("coverImage") var coverImage: CoverImage? = null, - @JsonProperty("bannerImage") var bannerImage: String? = null, + @JsonProperty("title") @SerialName("title") var title: Title? = null, + @JsonProperty("id") @SerialName("id") var id: Int? = null, + @JsonProperty("idMal") @SerialName("idMal") var idMal: Int? = null, + @JsonProperty("seasonYear") @SerialName("seasonYear") var seasonYear: Int? = null, + @JsonProperty("format") @SerialName("format") var format: String? = null, + @JsonProperty("coverImage") @SerialName("coverImage") var coverImage: CoverImage? = null, + @JsonProperty("bannerImage") @SerialName("bannerImage") var bannerImage: String? = null, ) { + @Serializable data class CoverImage( - @JsonProperty("extraLarge") var extraLarge: String? = null, - @JsonProperty("large") var large: String? = null, + @JsonProperty("extraLarge") @SerialName("extraLarge") var extraLarge: String? = null, + @JsonProperty("large") @SerialName("large") var large: String? = null, ) + @Serializable data class Title( - @JsonProperty("romaji") var romaji: String? = null, - @JsonProperty("english") var english: String? = null, + @JsonProperty("romaji") @SerialName("romaji") var romaji: String? = null, + @JsonProperty("english") @SerialName("english") var english: String? = null, ) { fun isMatchingTitles(title: String?): Boolean { if (title == null) return false diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt index 4fc2ac6c7de..125d74c0c83 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson @@ -39,8 +40,9 @@ open class Blogger : ExtractorApi() { return sources } + @Serializable private data class ResponseSource( - @JsonProperty("play_url") val play_url: String, - @JsonProperty("format_id") val format_id: Int + @SerialName("play_url") val play_url: String, + @SerialName("format_id") val format_id: Int ) } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt index 248b94a7350..df6d7c9b365 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt @@ -1,6 +1,8 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64DecodeArray @@ -128,59 +130,65 @@ open class ByseSX : ExtractorApi() { } } +@Serializable data class DetailsRoot( val id: Long, val code: String, val title: String, - @JsonProperty("poster_url") + @SerialName("poster_url") val posterUrl: String, val description: String, - @JsonProperty("created_at") + @SerialName("created_at") val createdAt: String, - @JsonProperty("owner_private") + @SerialName("owner_private") val ownerPrivate: Boolean, - @JsonProperty("embed_frame_url") + @SerialName("embed_frame_url") val embedFrameUrl: String, ) +@Serializable data class PlaybackRoot( val playback: Playback, ) +@Serializable data class Playback( val algorithm: String, val iv: String, val payload: String, - @JsonProperty("key_parts") + @SerialName("key_parts") val keyParts: List, - @JsonProperty("expires_at") + @SerialName("expires_at") val expiresAt: String, - @JsonProperty("decrypt_keys") + @SerialName("decrypt_keys") val decryptKeys: DecryptKeys, val iv2: String, val payload2: String, ) +@Serializable data class DecryptKeys( - @JsonProperty("edge_1") + @SerialName("edge_1") val edge1: String, - @JsonProperty("edge_2") + @SerialName("edge_2") val edge2: String, - @JsonProperty("legacy_fallback") + @SerialName("legacy_fallback") val legacyFallback: String, ) +@Serializable data class PlaybackDecrypt( val sources: List, ) +@Serializable data class PlaybackDecryptSource( val quality: String, val label: String, - @JsonProperty("mime_type") + @SerialName("mime_type") val mimeType: String, val url: String, - @JsonProperty("bitrate_kbps") + @SerialName("bitrate_kbps") val bitrateKbps: Long, - val height: Any?, + val height: JsonElement?, ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GUpload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GUpload.kt index d8029fcbfb2..d6c60bdb70e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GUpload.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GUpload.kt @@ -1,6 +1,8 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64Decode @@ -41,14 +43,15 @@ open class GUpload: ExtractorApi() { ) } + @Serializable private data class VideoInfo( - @JsonProperty("videoUrl") val videoUrl: String, - @JsonProperty("posterUrl") val posterUrl: String? = null, - @JsonProperty("videoId") val videoId: String? = null, - @JsonProperty("primaryColor") val primaryColor: String? = null, - @JsonProperty("audioTracks") val audioTracks: List = emptyList(), - @JsonProperty("subtitleTracks") val subtitleTracks: List = emptyList(), - @JsonProperty("vastFallbackList") val vastFallbackList: List = emptyList(), - @JsonProperty("videoOwnerId") val videoOwnerId: Long = 0, + @SerialName("videoUrl") val videoUrl: String, + @SerialName("posterUrl") val posterUrl: String? = null, + @SerialName("videoId") val videoId: String? = null, + @SerialName("primaryColor") val primaryColor: String? = null, + @SerialName("audioTracks") val audioTracks: List = emptyList(), + @SerialName("subtitleTracks") val subtitleTracks: List = emptyList(), + @SerialName("vastFallbackList") val vastFallbackList: List = emptyList(), + @SerialName("videoOwnerId") val videoOwnerId: Long = 0, ) -} \ No newline at end of file +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt index 38b90cdb34c..d3a80be5d76 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.fleeksoft.ksoup.nodes.Element import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler @@ -118,9 +119,10 @@ open class Gdriveplayer : ExtractorApi() { } } + @Serializable data class Tracks( - @JsonProperty("file") val file: String, - @JsonProperty("kind") val kind: String, - @JsonProperty("label") val label: String, + @SerialName("file") val file: String, + @SerialName("kind") val kind: String, + @SerialName("label") val label: String, ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt index c21a9c44cba..9f66ed14ab8 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi @@ -84,26 +85,31 @@ open class Gofile : ExtractorApi() { } } + @Serializable data class AccountResponse( - @JsonProperty("data") val data: AccountData? = null + @SerialName("data") val data: AccountData? = null ) + @Serializable data class AccountData( - @JsonProperty("token") val token: String? = null + @SerialName("token") val token: String? = null ) + @Serializable data class GofileResponse( - @JsonProperty("data") val data: GofileData? = null + @SerialName("data") val data: GofileData? = null ) + @Serializable data class GofileData( - @JsonProperty("children") val children: Map? = null + @SerialName("children") val children: Map? = null ) + @Serializable data class GofileFile( - @JsonProperty("type") val type: String? = null, - @JsonProperty("name") val name: String? = null, - @JsonProperty("link") val link: String? = null, - @JsonProperty("size") val size: Long? = 0L + @SerialName("type") val type: String? = null, + @SerialName("name") val name: String? = null, + @SerialName("link") val link: String? = null, + @SerialName("size") val size: Long? = 0L ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt index 3fc1ed0f562..9587c616800 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt @@ -2,10 +2,11 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.extractors.helper.AesHelper +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.parseJson @@ -60,11 +61,12 @@ open class HDMomPlayer : ExtractorApi() { ) } + @Serializable data class Track( - @JsonProperty("file") val file: String?, - @JsonProperty("label") val label: String?, - @JsonProperty("kind") val kind: String?, - @JsonProperty("language") val language: String?, - @JsonProperty("default") val default: String? + @SerialName("file") val file: String?, + @SerialName("label") val label: String?, + @SerialName("kind") val kind: String?, + @SerialName("language") val language: String?, + @SerialName("default") val default: String? ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt index 9cff2049fac..a88fb9af04f 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt @@ -5,7 +5,8 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable open class HDPlayerSystem : ExtractorApi() { override val name = "HDPlayerSystem" @@ -48,10 +49,11 @@ open class HDPlayerSystem : ExtractorApi() { ) } + @Serializable data class SystemResponse( - @JsonProperty("hls") val hls: String, - @JsonProperty("videoImage") val videoImage: String? = null, - @JsonProperty("videoSource") val videoSource: String, - @JsonProperty("securedLink") val securedLink: String + @SerialName("hls") val hls: String, + @SerialName("videoImage") val videoImage: String? = null, + @SerialName("videoSource") val videoSource: String, + @SerialName("securedLink") val securedLink: String ) } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt index c9a9aadae2f..64404a568bf 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper @@ -45,9 +46,10 @@ open class Jeniusplay : ExtractorApi() { } } + @Serializable data class ResponseSource( - @JsonProperty("hls") val hls: Boolean, - @JsonProperty("videoSource") val videoSource: String, - @JsonProperty("securedLink") val securedLink: String?, + @SerialName("hls") val hls: Boolean, + @SerialName("videoSource") val videoSource: String, + @SerialName("securedLink") val securedLink: String?, ) } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt index bfa94326aae..415f3d92528 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi @@ -36,22 +37,26 @@ open class Linkbox : ExtractorApi() { } } + @Serializable data class Resolutions( - @JsonProperty("url") val url: String? = null, - @JsonProperty("resolution") val resolution: String? = null, + @SerialName("url") val url: String? = null, + @SerialName("resolution") val resolution: String? = null, ) + @Serializable data class ItemInfo( - @JsonProperty("resolutionList") val resolutionList: ArrayList? = arrayListOf(), + @SerialName("resolutionList") val resolutionList: ArrayList? = arrayListOf(), ) + @Serializable data class Data( - @JsonProperty("itemInfo") val itemInfo: ItemInfo? = null, - @JsonProperty("itemId") val itemId: String? = null, + @SerialName("itemInfo") val itemInfo: ItemInfo? = null, + @SerialName("itemId") val itemId: String? = null, ) + @Serializable data class Responses( - @JsonProperty("data") val data: Data? = null, + @SerialName("data") val data: Data? = null, ) } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt index f1fd1288caf..437052a4732 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt @@ -5,7 +5,8 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable open class MailRu : ExtractorApi() { override val name = "MailRu" @@ -40,13 +41,15 @@ open class MailRu : ExtractorApi() { } } + @Serializable data class MailRuData( - @JsonProperty("provider") val provider: String, - @JsonProperty("videos") val videos: List + @SerialName("provider") val provider: String, + @SerialName("videos") val videos: List ) + @Serializable data class MailRuVideoData( - @JsonProperty("url") val url: String, - @JsonProperty("key") val key: String + @SerialName("url") val url: String, + @SerialName("key") val key: String ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt index 0a88639f450..5621117c657 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt @@ -2,7 +2,8 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.USER_AGENT @@ -66,8 +67,9 @@ open class Odnoklassniki : ExtractorApi() { } } + @Serializable data class OkRuVideo( - @JsonProperty("name") val name: String, - @JsonProperty("url") val url: String, + @SerialName("name") val name: String, + @SerialName("url") val url: String, ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt index 4efd20589be..7a10aadbed1 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt @@ -5,7 +5,8 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable open class PeaceMakerst : ExtractorApi() { override val name = "PeaceMakerst" @@ -60,29 +61,34 @@ open class PeaceMakerst : ExtractorApi() { ) } + @Serializable data class PeaceResponse( - @JsonProperty("videoImage") val videoImage: String?, - @JsonProperty("videoSources") val videoSources: List, - @JsonProperty("sIndex") val sIndex: String, - @JsonProperty("sourceList") val sourceList: Map + @SerialName("videoImage") val videoImage: String?, + @SerialName("videoSources") val videoSources: List, + @SerialName("sIndex") val sIndex: String, + @SerialName("sourceList") val sourceList: Map ) + @Serializable data class VideoSource( - @JsonProperty("file") val file: String, - @JsonProperty("label") val label: String, - @JsonProperty("type") val type: String + @SerialName("file") val file: String, + @SerialName("label") val label: String, + @SerialName("type") val type: String ) + @Serializable data class Teve2ApiResponse( - @JsonProperty("Media") val media: Teve2Media + @SerialName("Media") val media: Teve2Media ) + @Serializable data class Teve2Media( - @JsonProperty("Link") val link: Teve2Link + @SerialName("Link") val link: Teve2Link ) + @Serializable data class Teve2Link( - @JsonProperty("ServiceUrl") val serviceUrl: String, - @JsonProperty("SecurePath") val securePath: String + @SerialName("ServiceUrl") val serviceUrl: String, + @SerialName("SecurePath") val securePath: String ) } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt index a5ecfb5d83a..eb9098f95d9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt @@ -1,7 +1,8 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.api.Log -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson @@ -11,8 +12,9 @@ open class PlayLtXyz: ExtractorApi() { override val mainUrl: String = "https://play.playlt.xyz" override val requiresReferer = true + @Serializable private data class ResponseData( - @JsonProperty("data") val data: String? = null + @SerialName("data") val data: String? = null ) override suspend fun getUrl(url: String, referer: String?): List { @@ -57,7 +59,7 @@ open class PlayLtXyz: ExtractorApi() { if (data.isSuccessful) { val itemstr = data.text() Log.i(this.name, "Result => (data) $itemstr") - tryParseJson(itemstr)?.let { item -> + tryParseJson(itemstr)?.let { item -> val linkUrl = item.data ?: "" if (linkUrl.isNotBlank()) { extractedLinksList.add( @@ -77,4 +79,4 @@ open class PlayLtXyz: ExtractorApi() { } return extractedLinksList } -} \ No newline at end of file +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt index e4eee8d2dc5..6026552b7bb 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app @@ -172,26 +173,30 @@ open class Rabbitstream : ExtractorApi() { return decryptedData.decodeToString() } + @Serializable data class Tracks( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("kind") val kind: String? = null, + @SerialName("file") val file: String? = null, + @SerialName("label") val label: String? = null, + @SerialName("kind") val kind: String? = null, ) + @Serializable data class Sources( - @JsonProperty("file") val file: String? = null, - @JsonProperty("type") val type: String? = null, - @JsonProperty("label") val label: String? = null, + @SerialName("file") val file: String? = null, + @SerialName("type") val type: String? = null, + @SerialName("label") val label: String? = null, ) + @Serializable data class SourcesResponses( - @JsonProperty("sources") val sources: List? = emptyList(), - @JsonProperty("tracks") val tracks: List? = emptyList(), + @SerialName("sources") val sources: List? = emptyList(), + @SerialName("tracks") val tracks: List? = emptyList(), ) + @Serializable data class SourcesEncrypted( - @JsonProperty("sources") val sources: String? = null, - @JsonProperty("encrypted") val encrypted: Boolean? = null, - @JsonProperty("tracks") val tracks: List? = emptyList(), + @SerialName("sources") val sources: String? = null, + @SerialName("encrypted") val encrypted: Boolean? = null, + @SerialName("tracks") val tracks: List? = emptyList(), ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt index 1ac5a104fe2..42eb8835d34 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt @@ -4,7 +4,8 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable open class Sobreatsesuyp : ExtractorApi() { override val name = "Sobreatsesuyp" @@ -45,8 +46,9 @@ open class Sobreatsesuyp : ExtractorApi() { } } + @Serializable data class SobreatsesuypVideoData( - @JsonProperty("title") val title: String? = null, - @JsonProperty("file") val file: String? = null + @SerialName("title") val title: String? = null, + @SerialName("file") val file: String? = null ) } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamEmbed.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamEmbed.kt index 7b2846d6bbf..57d25392047 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamEmbed.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamEmbed.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.AppUtils.parseJson @@ -30,14 +31,15 @@ open class StreamEmbed : ExtractorApi() { ).forEach(callback) } + @Serializable private data class Details( - @JsonProperty("id") val id: String, - @JsonProperty("uid") val uid: String, - @JsonProperty("slug") val slug: String, - @JsonProperty("title") val title: String, - @JsonProperty("quality") val quality: String, - @JsonProperty("type") val type: String, - @JsonProperty("status") val status: String, - @JsonProperty("md5") val md5: String, + @SerialName("id") val id: String, + @SerialName("uid") val uid: String, + @SerialName("slug") val slug: String, + @SerialName("title") val title: String, + @SerialName("quality") val quality: String, + @SerialName("type") val type: String, + @SerialName("status") val status: String, + @SerialName("md5") val md5: String, ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt index 67cf1f8da9c..6695e7718b7 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.newSubtitleFile @@ -189,25 +190,28 @@ open class StreamSB : ExtractorApi() { } } + @Serializable data class Subs ( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, + @SerialName("file") val file: String? = null, + @SerialName("label") val label: String? = null, ) + @Serializable data class StreamData ( - @JsonProperty("file") val file: String, - @JsonProperty("cdn_img") val cdnImg: String, - @JsonProperty("hash") val hash: String, - @JsonProperty("subs") val subs: ArrayList? = arrayListOf(), - @JsonProperty("length") val length: String, - @JsonProperty("id") val id: String, - @JsonProperty("title") val title: String, - @JsonProperty("backup") val backup: String, + @SerialName("file") val file: String, + @SerialName("cdn_img") val cdnImg: String, + @SerialName("hash") val hash: String, + @SerialName("subs") val subs: ArrayList? = arrayListOf(), + @SerialName("length") val length: String, + @SerialName("id") val id: String, + @SerialName("title") val title: String, + @SerialName("backup") val backup: String, ) + @Serializable data class Main ( - @JsonProperty("stream_data") val streamData: StreamData, - @JsonProperty("status_code") val statusCode: Int, + @SerialName("stream_data") val streamData: StreamData, + @SerialName("status_code") val statusCode: Int, ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt index da2dd62bef5..d5455a28c76 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink @@ -26,18 +27,20 @@ open class Slmaxed : ExtractorApi() { val embedRegex = Regex("""/e/([^/]*)""") + @Serializable data class JsonResponse( - @JsonProperty val status: String? = null, - @JsonProperty val message: String? = null, - @JsonProperty val type: String? = null, - @JsonProperty val token: String? = null, - @JsonProperty val result: Map? = null + @SerialName("status") val status: String? = null, + @SerialName("message") val message: String? = null, + @SerialName("type") val type: String? = null, + @SerialName("token") val token: String? = null, + @SerialName("result") val result: Map? = null ) + @Serializable data class Result( - @JsonProperty val label: String? = null, - @JsonProperty val file: String? = null, - @JsonProperty val type: String? = null + @SerialName("label") val label: String? = null, + @SerialName("file") val file: String? = null, + @SerialName("type") val type: String? = null ) override suspend fun getUrl(url: String, referer: String?): List? { @@ -66,4 +69,4 @@ open class Slmaxed : ExtractorApi() { } } } -} \ No newline at end of file +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt index a8dac673734..e1526ee7455 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.APIHolder.getCaptchaToken import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.SubtitleFile @@ -72,9 +73,10 @@ open class Streamplay : ExtractorApi() { } + @Serializable data class Source( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, + @SerialName("file") val file: String? = null, + @SerialName("label") val label: String? = null, ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt index 1345da96cdf..332d9ccca06 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt @@ -5,7 +5,8 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable open class TRsTX : ExtractorApi() { override val name = "TRsTX" @@ -62,8 +63,9 @@ open class TRsTX : ExtractorApi() { } } + @Serializable data class TrstxVideoData( - @JsonProperty("title") val title: String? = null, - @JsonProperty("file") val file: String? = null + @SerialName("title") val title: String? = null, + @SerialName("file") val file: String? = null ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt index 2ac30d2c69b..c3162ebcf77 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.AppUtils.parseJson @@ -12,17 +13,19 @@ open class Tantifilm : ExtractorApi() { override var mainUrl = "https://cercafilm.net" override val requiresReferer = false + @Serializable data class TantifilmJsonData ( - @JsonProperty("success") val success : Boolean, - @JsonProperty("data") val data : List, - @JsonProperty("captions")val captions : List, - @JsonProperty("is_vr") val is_vr : Boolean + @SerialName("success") val success : Boolean, + @SerialName("data") val data : List, + @SerialName("captions")val captions : List, + @SerialName("is_vr") val is_vr : Boolean ) + @Serializable data class TantifilmData ( - @JsonProperty("file") val file : String, - @JsonProperty("label") val label : String, - @JsonProperty("type") val type : String + @SerialName("file") val file : String, + @SerialName("label") val label : String, + @SerialName("type") val type : String ) override suspend fun getUrl(url: String, referer: String?): List? { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt index 1c9cce2a834..810e23d86dc 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt @@ -5,7 +5,8 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable open class TauVideo : ExtractorApi() { override val name = "TauVideo" @@ -33,12 +34,14 @@ open class TauVideo : ExtractorApi() { } } + @Serializable data class TauVideoUrls( - @JsonProperty("urls") val urls: List + @SerialName("urls") val urls: List ) + @Serializable data class TauVideoData( - @JsonProperty("url") val url: String, - @JsonProperty("label") val label: String, + @SerialName("url") val url: String, + @SerialName("label") val label: String, ) } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt index f6c3002813d..247bd099d42 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.AppUtils @@ -44,10 +45,11 @@ open class Uservideo : ExtractorApi() { } + @Serializable data class Sources( - @JsonProperty("src") val src: String? = null, - @JsonProperty("type") val type: String? = null, - @JsonProperty("label") val label: String? = null, + @SerialName("src") val src: String? = null, + @SerialName("type") val type: String? = null, + @SerialName("label") val label: String? = null, ) } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt index b2b526458a1..d5bd978784d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app @@ -42,13 +43,15 @@ open class Vicloud : ExtractorApi() { } + @Serializable private data class Sources( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, + @SerialName("file") val file: String? = null, + @SerialName("label") val label: String? = null, ) + @Serializable private data class Responses( - @JsonProperty("sources") val sources: List? = arrayListOf(), + @SerialName("sources") val sources: List? = arrayListOf(), ) } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt index 6bbd13d04fa..d6ec5461b85 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt @@ -2,10 +2,11 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson open class VideoSeyred : ExtractorApi() { @@ -47,24 +48,27 @@ open class VideoSeyred : ExtractorApi() { } } + @Serializable data class VideoSeyredSource( - @JsonProperty("image") val image: String, - @JsonProperty("title") val title: String, - @JsonProperty("sources") val sources: List, - @JsonProperty("tracks") val tracks: List + @SerialName("image") val image: String, + @SerialName("title") val title: String, + @SerialName("sources") val sources: List, + @SerialName("tracks") val tracks: List ) + @Serializable data class VSSource( - @JsonProperty("file") val file: String, - @JsonProperty("type") val type: String, - @JsonProperty("default") val default: String + @SerialName("file") val file: String, + @SerialName("type") val type: String, + @SerialName("default") val default: String ) + @Serializable data class VSTrack( - @JsonProperty("file") val file: String, - @JsonProperty("kind") val kind: String, - @JsonProperty("language") val language: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("default") val default: String? = null + @SerialName("file") val file: String, + @SerialName("kind") val kind: String, + @SerialName("language") val language: String? = null, + @SerialName("label") val label: String? = null, + @SerialName("default") val default: String? = null ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidoza.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidoza.kt index 2a078034000..f945b6caaed 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidoza.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidoza.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.api.Log import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app @@ -53,10 +54,11 @@ open class Vidoza: ExtractorApi() { private class VinovoDataList: ArrayList() + @Serializable private data class VinovoVideoData( - @JsonProperty("src") val source: String, - @JsonProperty("type") val type: String?, - @JsonProperty("label") val label: String?, - @JsonProperty("res") val resolution: String?, + @SerialName("src") val source: String, + @SerialName("type") val type: String?, + @SerialName("label") val label: String?, + @SerialName("res") val resolution: String?, ) } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vinovo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vinovo.kt index f490390990c..51c1f7b08d4 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vinovo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vinovo.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app @@ -59,8 +60,9 @@ open class VinovoTo : ExtractorApi() { ) } + @Serializable private data class VinovoFileResp( - @JsonProperty("status") val status: String, - @JsonProperty("token") val token: String, + @SerialName("status") val status: String, + @SerialName("token") val token: String, ) } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt index 6e3a8126def..2adf01be073 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.newSubtitleFile @@ -76,28 +77,32 @@ open class XStreamCdn : ExtractorApi() { override val requiresReferer = false open var domainUrl: String = "embedsito.com" + @Serializable private data class ResponseData( - @JsonProperty("file") val file: String, - @JsonProperty("label") val label: String, + @SerialName("file") val file: String, + @SerialName("label") val label: String, //val type: String // Mp4 ) + @Serializable private data class Player( - @JsonProperty("poster_file") val poster_file: String? = null, + @SerialName("poster_file") val poster_file: String? = null, ) + @Serializable private data class ResponseJson( - @JsonProperty("success") val success: Boolean, - @JsonProperty("player") val player: Player? = null, - @JsonProperty("data") val data: List?, - @JsonProperty("captions") val captions: List?, + @SerialName("success") val success: Boolean, + @SerialName("player") val player: Player? = null, + @SerialName("data") val data: List?, + @SerialName("captions") val captions: List?, ) + @Serializable private data class Captions( - @JsonProperty("id") val id: String, - @JsonProperty("hash") val hash: String, - @JsonProperty("language") val language: String, - @JsonProperty("extension") val extension: String + @SerialName("id") val id: String, + @SerialName("hash") val hash: String, + @SerialName("language") val language: String, + @SerialName("extension") val extension: String ) override fun getExtractorUrl(id: String): String { @@ -117,7 +122,7 @@ open class XStreamCdn : ExtractorApi() { val id = url.trimEnd('/').split("/").last() val newUrl = "https://${domainUrl}/api/source/${id}" app.post(newUrl, headers = headers).let { res -> - val sources = tryParseJson(res.text) + val sources = tryParseJson(res.text) sources?.let { if (it.success && it.data != null) { it.data.map { source -> @@ -146,4 +151,4 @@ open class XStreamCdn : ExtractorApi() { } } } -} \ No newline at end of file +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt index cdb6deb46ed..80ab128d3c3 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi @@ -42,8 +43,9 @@ open class YourUpload: ExtractorApi() { return sources } + @Serializable private data class ResponseSource( - @JsonProperty("file") val file: String, + @SerialName("file") val file: String, ) } \ No newline at end of file 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 89f935da327..77416fe8aa5 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.metaproviders -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import com.lagradost.cloudstream3.Actor import com.lagradost.cloudstream3.Episode import com.lagradost.cloudstream3.ErrorLoadingException @@ -51,12 +52,13 @@ import java.util.Calendar * 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, + @SerialName("imdbID") val imdbID: String?, + @SerialName("tmdbID") val tmdbID: Int?, + @SerialName("episode") val episode: Int?, + @SerialName("season") val season: Int?, + @SerialName("movieName") val movieName: String? = null, ) open class TmdbProvider : MainAPI() { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index dafec4d9707..6011e14ea1d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -34,6 +34,10 @@ import com.lagradost.cloudstream3.newTvSeriesLoadResponse import com.lagradost.cloudstream3.newTvSeriesSearchResponse import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames open class TraktProvider : MainAPI() { override var name = "Trakt" @@ -46,7 +50,6 @@ open class TraktProvider : MainAPI() { ) private val traktApiUrl = "https://api.trakt.tv" - private val traktClientId: String = BuildConfig.TRAKT_CLIENT_ID override val mainPage = mainPageOf( @@ -58,16 +61,21 @@ open class TraktProvider : MainAPI() { override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { val apiResponse = getApi("${request.data}?extended=full,images&page=$page") - val results = parseJson>(apiResponse).map { element -> element.toSearchResponse() } + return newHomePageResponse(request.name, results) } private fun MediaDetails.toSearchResponse(): SearchResponse { - - val media = this.media ?: this + val media = this.media ?: MediaSummary( + title = this.title, + year = this.year, + ids = this.ids, + rating = this.rating, + images = this.images, + ) val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries val poster = media.images?.poster?.firstOrNull() return if (mediaType == TvType.Movie) { @@ -75,7 +83,7 @@ open class TraktProvider : MainAPI() { name = media.title ?: "", url = Data( type = mediaType, - mediaDetails = media, + mediaDetails = this, ).toJson(), type = TvType.Movie, ) { @@ -87,7 +95,7 @@ open class TraktProvider : MainAPI() { name = media.title ?: "", url = Data( type = mediaType, - mediaDetails = media, + mediaDetails = this, ).toJson(), type = TvType.TvSeries, ) { @@ -98,9 +106,7 @@ open class TraktProvider : MainAPI() { } override suspend fun search(query: String, page: Int): SearchResponseList? { - val apiResponse = - getApi("$traktApiUrl/search/movie,show?extended=full,images&limit=20&page=$page&query=$query") - + val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=full,images&limit=20&page=$page&query=$query") return newSearchResponseList(parseJson>(apiResponse).map { element -> element.toSearchResponse() }) @@ -115,9 +121,7 @@ open class TraktProvider : MainAPI() { val backDropUrl = fixPath(mediaDetails?.images?.fanart?.firstOrNull()) val logoUrl = fixPath(mediaDetails?.images?.logo?.firstOrNull()) - val resActor = - getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=full,images") - + val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=full,images") val actors = parseJson(resActor).cast?.map { ActorData( Actor( @@ -128,9 +132,7 @@ open class TraktProvider : MainAPI() { ) } - val resRelated = - getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=full,images&limit=20") - + val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=full,images&limit=20") val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() } val isCartoon = @@ -142,7 +144,6 @@ open class TraktProvider : MainAPI() { val uniqueUrl = data.mediaDetails?.ids?.trakt?.toJson() ?: data.toJson() if (data.type == TvType.Movie) { - val linkData = LinkData( id = mediaDetails?.ids?.tmdb, traktId = mediaDetails?.ids?.trakt, @@ -156,7 +157,7 @@ open class TraktProvider : MainAPI() { year = mediaDetails?.year, orgTitle = mediaDetails?.title, isAnime = isAnime, - //jpTitle = later if needed as it requires another network request, + // jpTitle = later if needed as it requires another network request, airedDate = mediaDetails?.released ?: mediaDetails?.firstAired, isAsian = isAsian, @@ -190,7 +191,6 @@ open class TraktProvider : MainAPI() { addTMDbId(mediaDetails.ids?.tmdb.toString()) } } else { - val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=full,images,episodes") val episodes = mutableListOf() @@ -198,9 +198,7 @@ open class TraktProvider : MainAPI() { var nextAir: NextAiring? = null seasons.forEach { season -> - season.episodes?.map { episode -> - val linkData = LinkData( id = mediaDetails?.ids?.tmdb, traktId = mediaDetails?.ids?.trakt, @@ -234,8 +232,7 @@ open class TraktProvider : MainAPI() { this.episode = episode.number this.description = episode.overview this.runTime = episode.runtime - this.posterUrl = fixPath( episode.images?.screenshot?.firstOrNull()) - //this.rating = episode.rating?.times(10)?.roundToInt() + this.posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()) this.score = Score.from10(episode.rating) this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") @@ -307,143 +304,164 @@ open class TraktProvider : MainAPI() { return "https://$url" } + @Serializable data class Data( - val type: TvType? = null, - val mediaDetails: MediaDetails? = null, + @JsonProperty("type") @SerialName("type") val type: TvType? = null, + @JsonProperty("mediaDetails") @SerialName("mediaDetails") val mediaDetails: MediaDetails? = null, + ) + + @Serializable + data class MediaSummary( + @JsonProperty("title") @SerialName("title") val title: String? = null, + @JsonProperty("year") @SerialName("year") val year: Int? = null, + @JsonProperty("ids") @SerialName("ids") val ids: Ids? = null, + @JsonProperty("rating") @SerialName("rating") val rating: Double? = null, + @JsonProperty("images") @SerialName("images") val images: Images? = null, ) + @OptIn(ExperimentalSerializationApi::class) // JsonNames is an experimental annotation for now + @Serializable data class MediaDetails( - @JsonProperty("title") val title: String? = null, - @JsonProperty("year") val year: Int? = null, - @JsonProperty("ids") val ids: Ids? = null, - @JsonProperty("tagline") val tagline: String? = null, - @JsonProperty("overview") val overview: String? = null, - @JsonProperty("released") val released: String? = null, - @JsonProperty("runtime") val runtime: Int? = null, - @JsonProperty("country") val country: String? = null, - @JsonProperty("updatedAt") val updatedAt: String? = null, - @JsonProperty("trailer") val trailer: String? = null, - @JsonProperty("homepage") val homepage: String? = null, - @JsonProperty("status") val status: String? = null, - @JsonProperty("rating") val rating: Double? = null, - @JsonProperty("votes") val votes: Long? = null, - @JsonProperty("comment_count") val commentCount: Long? = null, - @JsonProperty("language") val language: String? = null, - @JsonProperty("languages") val languages: List? = null, - @JsonProperty("available_translations") val availableTranslations: List? = null, - @JsonProperty("genres") val genres: List? = null, - @JsonProperty("certification") val certification: String? = null, - @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, - @JsonProperty("first_aired") val firstAired: String? = null, - @JsonProperty("airs") val airs: Airs? = null, - @JsonProperty("network") val network: String? = null, - @JsonProperty("images") val images: Images? = null, - @JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null + @JsonProperty("title") @SerialName("title") val title: String? = null, + @JsonProperty("year") @SerialName("year") val year: Int? = null, + @JsonProperty("ids") @SerialName("ids") val ids: Ids? = null, + @JsonProperty("tagline") @SerialName("tagline") val tagline: String? = null, + @JsonProperty("overview") @SerialName("overview") val overview: String? = null, + @JsonProperty("released") @SerialName("released") val released: String? = null, + @JsonProperty("runtime") @SerialName("runtime") val runtime: Int? = null, + @JsonProperty("country") @SerialName("country") val country: String? = null, + @JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: String? = null, + @JsonProperty("trailer") @SerialName("trailer") val trailer: String? = null, + @JsonProperty("homepage") @SerialName("homepage") val homepage: String? = null, + @JsonProperty("status") @SerialName("status") val status: String? = null, + @JsonProperty("rating") @SerialName("rating") val rating: Double? = null, + @JsonProperty("votes") @SerialName("votes") val votes: Long? = null, + @JsonProperty("comment_count") @SerialName("comment_count") val commentCount: Long? = null, + @JsonProperty("language") @SerialName("language") val language: String? = null, + @JsonProperty("languages") @SerialName("languages") val languages: List? = null, + @JsonProperty("available_translations") @SerialName("available_translations") val availableTranslations: List? = null, + @JsonProperty("genres") @SerialName("genres") val genres: List? = null, + @JsonProperty("certification") @SerialName("certification") val certification: String? = null, + @JsonProperty("aired_episodes") @SerialName("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("first_aired") @SerialName("first_aired") val firstAired: String? = null, + @JsonProperty("airs") @SerialName("airs") val airs: Airs? = null, + @JsonProperty("network") @SerialName("network") val network: String? = null, + @JsonProperty("images") @SerialName("images") val images: Images? = null, + @JsonProperty("media") @JsonAlias("movie", "show") @SerialName("media") @JsonNames("movie", "show") val media: MediaSummary? = null, ) + @Serializable data class Airs( - @JsonProperty("day") val day: String? = null, - @JsonProperty("time") val time: String? = null, - @JsonProperty("timezone") val timezone: String? = null, + @JsonProperty("day") @SerialName("day") val day: String? = null, + @JsonProperty("time") @SerialName("time") val time: String? = null, + @JsonProperty("timezone") @SerialName("timezone") val timezone: String? = null, ) + @Serializable data class Ids( - @JsonProperty("trakt") val trakt: Int? = null, - @JsonProperty("slug") val slug: String? = null, - @JsonProperty("tvdb") val tvdb: Int? = null, - @JsonProperty("imdb") val imdb: String? = null, - @JsonProperty("tmdb") val tmdb: Int? = null, - @JsonProperty("tvrage") val tvrage: String? = null, + @JsonProperty("trakt") @SerialName("trakt") val trakt: Int? = null, + @JsonProperty("slug") @SerialName("slug") val slug: String? = null, + @JsonProperty("tvdb") @SerialName("tvdb") val tvdb: Int? = null, + @JsonProperty("imdb") @SerialName("imdb") val imdb: String? = null, + @JsonProperty("tmdb") @SerialName("tmdb") val tmdb: Int? = null, + @JsonProperty("tvrage") @SerialName("tvrage") val tvrage: String? = null, ) + @Serializable data class Images( - @JsonProperty("poster") val poster: List? = null, - @JsonProperty("fanart") val fanart: List? = null, - @JsonProperty("logo") val logo: List? = null, - @JsonProperty("clearart") val clearArt: List? = null, - @JsonProperty("banner") val banner: List? = null, - @JsonProperty("thumb") val thumb: List? = null, - @JsonProperty("screenshot") val screenshot: List? = null, - @JsonProperty("headshot") val headshot: List? = null, + @JsonProperty("poster") @SerialName("poster") val poster: List? = null, + @JsonProperty("fanart") @SerialName("fanart") val fanart: List? = null, + @JsonProperty("logo") @SerialName("logo") val logo: List? = null, + @JsonProperty("clearart") @SerialName("clearart") val clearArt: List? = null, + @JsonProperty("banner") @SerialName("banner") val banner: List? = null, + @JsonProperty("thumb") @SerialName("thumb") val thumb: List? = null, + @JsonProperty("screenshot") @SerialName("screenshot") val screenshot: List? = null, + @JsonProperty("headshot") @SerialName("headshot") val headshot: List? = null, ) + @Serializable data class People( - @JsonProperty("cast") val cast: List? = null, + @JsonProperty("cast") @SerialName("cast") val cast: List? = null, ) + @Serializable data class Cast( - @JsonProperty("character") val character: String? = null, - @JsonProperty("characters") val characters: List? = null, - @JsonProperty("episode_count") val episodeCount: Long? = null, - @JsonProperty("person") val person: Person? = null, - @JsonProperty("images") val images: Images? = null, + @JsonProperty("character") @SerialName("character") val character: String? = null, + @JsonProperty("characters") @SerialName("characters") val characters: List? = null, + @JsonProperty("episode_count") @SerialName("episode_count") val episodeCount: Long? = null, + @JsonProperty("person") @SerialName("person") val person: Person? = null, + @JsonProperty("images") @SerialName("images") val images: Images? = null, ) + @Serializable data class Person( - @JsonProperty("name") val name: String? = null, - @JsonProperty("ids") val ids: Ids? = null, - @JsonProperty("images") val images: Images? = null, + @JsonProperty("name") @SerialName("name") val name: String? = null, + @JsonProperty("ids") @SerialName("ids") val ids: Ids? = null, + @JsonProperty("images") @SerialName("images") val images: Images? = null, ) + @Serializable data class Seasons( - @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, - @JsonProperty("episode_count") val episodeCount: Int? = null, - @JsonProperty("episodes") val episodes: List? = null, - @JsonProperty("first_aired") val firstAired: String? = null, - @JsonProperty("ids") val ids: Ids? = null, - @JsonProperty("images") val images: Images? = null, - @JsonProperty("network") val network: String? = null, - @JsonProperty("number") val number: Int? = null, - @JsonProperty("overview") val overview: String? = null, - @JsonProperty("rating") val rating: Double? = null, - @JsonProperty("title") val title: String? = null, - @JsonProperty("updated_at") val updatedAt: String? = null, - @JsonProperty("votes") val votes: Int? = null, + @JsonProperty("aired_episodes") @SerialName("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("episode_count") @SerialName("episode_count") val episodeCount: Int? = null, + @JsonProperty("episodes") @SerialName("episodes") val episodes: List? = null, + @JsonProperty("first_aired") @SerialName("first_aired") val firstAired: String? = null, + @JsonProperty("ids") @SerialName("ids") val ids: Ids? = null, + @JsonProperty("images") @SerialName("images") val images: Images? = null, + @JsonProperty("network") @SerialName("network") val network: String? = null, + @JsonProperty("number") @SerialName("number") val number: Int? = null, + @JsonProperty("overview") @SerialName("overview") val overview: String? = null, + @JsonProperty("rating") @SerialName("rating") val rating: Double? = null, + @JsonProperty("title") @SerialName("title") val title: String? = null, + @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") @SerialName("votes") val votes: Int? = null, ) + @Serializable data class TraktEpisode( - @JsonProperty("available_translations") val availableTranslations: List? = null, - @JsonProperty("comment_count") val commentCount: Int? = null, - @JsonProperty("episode_type") val episodeType: String? = null, - @JsonProperty("first_aired") val firstAired: String? = null, - @JsonProperty("ids") val ids: Ids? = null, - @JsonProperty("images") val images: Images? = null, - @JsonProperty("number") val number: Int? = null, - @JsonProperty("number_abs") val numberAbs: Int? = null, - @JsonProperty("overview") val overview: String? = null, - @JsonProperty("rating") val rating: Double? = null, - @JsonProperty("runtime") val runtime: Int? = null, - @JsonProperty("season") val season: Int? = null, - @JsonProperty("title") val title: String? = null, - @JsonProperty("updated_at") val updatedAt: String? = null, - @JsonProperty("votes") val votes: Int? = null, + @JsonProperty("available_translations") @SerialName("available_translations") val availableTranslations: List? = null, + @JsonProperty("comment_count") @SerialName("comment_count") val commentCount: Int? = null, + @JsonProperty("episode_type") @SerialName("episode_type") val episodeType: String? = null, + @JsonProperty("first_aired") @SerialName("first_aired") val firstAired: String? = null, + @JsonProperty("ids") @SerialName("ids") val ids: Ids? = null, + @JsonProperty("images") @SerialName("images") val images: Images? = null, + @JsonProperty("number") @SerialName("number") val number: Int? = null, + @JsonProperty("number_abs") @SerialName("number_abs") val numberAbs: Int? = null, + @JsonProperty("overview") @SerialName("overview") val overview: String? = null, + @JsonProperty("rating") @SerialName("rating") val rating: Double? = null, + @JsonProperty("runtime") @SerialName("runtime") val runtime: Int? = null, + @JsonProperty("season") @SerialName("season") val season: Int? = null, + @JsonProperty("title") @SerialName("title") val title: String? = null, + @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") @SerialName("votes") val votes: Int? = null, ) + @Serializable data class LinkData( - @JsonProperty("id") val id: Int? = null, - @JsonProperty("trakt_id") val traktId: Int? = null, - @JsonProperty("trakt_slug") val traktSlug: String? = null, - @JsonProperty("tmdb_id") val tmdbId: Int? = null, - @JsonProperty("imdb_id") val imdbId: String? = null, - @JsonProperty("tvdb_id") val tvdbId: Int? = null, - @JsonProperty("tvrage_id") val tvrageId: String? = null, - @JsonProperty("type") val type: String? = null, - @JsonProperty("season") val season: Int? = null, - @JsonProperty("episode") val episode: Int? = null, - @JsonProperty("ani_id") val aniId: String? = null, - @JsonProperty("anime_id") val animeId: String? = null, - @JsonProperty("title") val title: String? = null, - @JsonProperty("year") val year: Int? = null, - @JsonProperty("org_title") val orgTitle: String? = null, - @JsonProperty("is_anime") val isAnime: Boolean = false, - @JsonProperty("aired_year") val airedYear: Int? = null, - @JsonProperty("last_season") val lastSeason: Int? = null, - @JsonProperty("eps_title") val epsTitle: String? = null, - @JsonProperty("jp_title") val jpTitle: String? = null, - @JsonProperty("date") val date: String? = null, - @JsonProperty("aired_date") val airedDate: String? = null, - @JsonProperty("is_asian") val isAsian: Boolean = false, - @JsonProperty("is_bollywood") val isBollywood: Boolean = false, - @JsonProperty("is_cartoon") val isCartoon: Boolean = false, + @JsonProperty("id") @SerialName("id") val id: Int? = null, + @JsonProperty("trakt_id") @SerialName("trakt_id") val traktId: Int? = null, + @JsonProperty("trakt_slug") @SerialName("trakt_slug") val traktSlug: String? = null, + @JsonProperty("tmdb_id") @SerialName("tmdb_id") val tmdbId: Int? = null, + @JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String? = null, + @JsonProperty("tvdb_id") @SerialName("tvdb_id") val tvdbId: Int? = null, + @JsonProperty("tvrage_id") @SerialName("tvrage_id") val tvrageId: String? = null, + @JsonProperty("type") @SerialName("type") val type: String? = null, + @JsonProperty("season") @SerialName("season") val season: Int? = null, + @JsonProperty("episode") @SerialName("episode") val episode: Int? = null, + @JsonProperty("ani_id") @SerialName("ani_id") val aniId: String? = null, + @JsonProperty("anime_id") @SerialName("anime_id") val animeId: String? = null, + @JsonProperty("title") @SerialName("title") val title: String? = null, + @JsonProperty("year") @SerialName("year") val year: Int? = null, + @JsonProperty("org_title") @SerialName("org_title") val orgTitle: String? = null, + @JsonProperty("is_anime") @SerialName("is_anime") val isAnime: Boolean = false, + @JsonProperty("aired_year") @SerialName("aired_year") val airedYear: Int? = null, + @JsonProperty("last_season") @SerialName("last_season") val lastSeason: Int? = null, + @JsonProperty("eps_title") @SerialName("eps_title") val epsTitle: String? = null, + @JsonProperty("jp_title") @SerialName("jp_title") val jpTitle: String? = null, + @JsonProperty("date") @SerialName("date") val date: String? = null, + @JsonProperty("aired_date") @SerialName("aired_date") val airedDate: String? = null, + @JsonProperty("is_asian") @SerialName("is_asian") val isAsian: Boolean = false, + @JsonProperty("is_bollywood") @SerialName("is_bollywood") val isBollywood: Boolean = false, + @JsonProperty("is_cartoon") @SerialName("is_cartoon") val isCartoon: Boolean = false, ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt index 1c635013fc2..26d5daa6ffc 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.module.kotlin.readValue import com.lagradost.cloudstream3.InternalAPI import com.lagradost.cloudstream3.json import com.lagradost.cloudstream3.mapper +import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.logError import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi @@ -21,60 +22,89 @@ object AppUtils { return toJsonLiteral() } - /** Sometimes we want to encode as JSON even if it is already a String. */ @InternalAPI - fun Any.toJsonLiteral(): String { - // @Serializable generates a serializer at compile time; contextual serializers are - // registered manually in serializersModule, we need both to support all cases - val serializer = - this::class.serializerOrNull() ?: json.serializersModule.getContextual(this::class) - return if (serializer != null) { + fun Any.toJsonLiteralImpl(serializer: KSerializer?): String { + var fallbackTrace: String? = null + if (serializer != null) { try { - @Suppress("UNCHECKED_CAST") - json.encodeToString(serializer as KSerializer, this) + debugPrint { "AppUtils/toJsonLiteral: using kotlinx serialization for ${this::class.qualifiedName}" } + return json.encodeToString(serializer, this) } catch (e: SerializationException) { logError(e) - mapper.writeValueAsString(this) + fallbackTrace = e.stackTraceToString() + debugPrint { "AppUtils/toJsonLiteral: kotlinx failed, falling back to Jackson for ${this::class.qualifiedName}" } } } else { - mapper.writeValueAsString(this) + fallbackTrace = Exception().stackTraceToString() } + debugPrint { "AppUtils/toJsonLiteral: using Jackson for ${this::class.qualifiedName}\n$fallbackTrace" } + return mapper.writeValueAsString(this) + } + + /** Runtime lookup version, subject to type erasure for generic types. */ + @InternalAPI + fun Any.toJsonLiteral(): String { + val serializer = this::class.serializerOrNull() + ?: json.serializersModule.getContextual(this::class) + @Suppress("UNCHECKED_CAST") + return toJsonLiteralImpl(serializer as KSerializer?) + } + + /** Reified version, preserves full generic type info at call site. */ + @InternalAPI + @JvmName("toJsonLiteralReified") + inline fun T.toJsonLiteral(): String { + val serializer = runCatching { serializer() } + .recoverCatching { json.serializersModule.getContextual(T::class) } + .getOrNull() + @Suppress("UNCHECKED_CAST") + return toJsonLiteralImpl(serializer as KSerializer?) } @InternalAPI fun parseJson(value: String, kClass: KClass): T { val serializer = kClass.serializerOrNull() ?: json.serializersModule.getContextual(kClass) + var fallbackTrace: String? = null if (serializer != null) { try { + debugPrint { "AppUtils/parseJson(kClass): using kotlinx serialization for ${kClass.qualifiedName}" } return json.decodeFromString(serializer, value) } catch (e: SerializationException) { logError(e) + fallbackTrace = e.stackTraceToString() + debugPrint { "AppUtils/parseJson(kClass): kotlinx failed, falling back to Jackson for ${kClass.qualifiedName}" } } + } else { + fallbackTrace = Exception().stackTraceToString() } - + debugPrint { "AppUtils/parseJson(kClass): using Jackson for ${kClass.qualifiedName}\n$fallbackTrace" } return mapper.readValue(value, kClass.java) } // This is inlined code and can easily cause breakage in extensions! // Watch out when editing this to make sure stable also supports all inlined code! inline fun parseJson(value: String): T { - // @Serializable generates a serializer at compile time; contextual serializers are - // registered manually in serializersModule, we need both to support all cases val serializer = runCatching { serializer() } .recoverCatching { json.serializersModule.getContextual(T::class) } .getOrNull() - // Prefer Kotlin Serialization over Jackson + var fallbackTrace: String? = null if (serializer != null) { try { + debugPrint { "AppUtils/parseJson: using kotlinx serialization for ${T::class.qualifiedName}" } return json.decodeFromString(serializer, value) } catch (e: SerializationException) { logError(e) - } catch (_: Throwable) { - // Pass, the above code will trigger a NoSuchMethodError on stable due to our previously undefined json variable + fallbackTrace = e.stackTraceToString() + debugPrint { "AppUtils/parseJson: kotlinx failed, falling back to Jackson for ${T::class.qualifiedName}" } + } catch (e: Throwable) { + fallbackTrace = e.stackTraceToString() + debugPrint { "AppUtils/parseJson: unexpected error for ${T::class.qualifiedName}, falling back to Jackson" } } + } else { + fallbackTrace = Exception().stackTraceToString() } - + debugPrint { "AppUtils/parseJson: using Jackson for ${T::class.qualifiedName}\n$fallbackTrace" } return mapper.readValue(value) } @@ -85,7 +115,6 @@ object AppUtils { replaceWith = ReplaceWith("parseJson(reader.readText())") ) inline fun parseJson(reader: java.io.Reader, valueType: Class): T { - // Reader-based parsing has no kotlinx equivalent, fall back to Jackson return mapper.readValue(reader, valueType) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 93a146c6da1..ded886b4054 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -327,6 +327,9 @@ import io.ktor.http.decodeURLPart import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import kotlin.coroutines.cancellation.CancellationException import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -417,6 +420,7 @@ enum class ExtractorLinkType { MAGNET; // See https://www.iana.org/assignments/media-types/media-types.xhtml + @JsonIgnore fun getMimeType(): String { return when (this) { VIDEO -> "video/mp4" @@ -684,26 +688,27 @@ open class DrmExtractorLink private constructor( * @property audioTracks List of separate audio tracks that can be used with this video * @see newExtractorLink * */ +@Serializable open class ExtractorLink @Deprecated("Use newExtractorLink", level = DeprecationLevel.WARNING) constructor( - open val source: String, - open val name: String, - override val url: String, - override var referer: String, - open var quality: Int, - override var headers: Map = mapOf(), + @SerialName("source") open val source: String, + @SerialName("name") open val name: String, + @SerialName("url") override val url: String, + @SerialName("referer") override var referer: String, + @SerialName("quality") open var quality: Int, + @SerialName("headers") override var headers: Map = mapOf(), /** Used for getExtractorVerifierJob() */ - open var extractorData: String? = null, - open var type: ExtractorLinkType, + @SerialName("extractorData") open var extractorData: String? = null, + @SerialName("type") open var type: ExtractorLinkType, /** List of separate audio tracks that can be merged with this video */ - open var audioTracks: List = emptyList(), + @SerialName("audioTracks") open var audioTracks: List = emptyList(), ) : IDownloadableMinimum { - val isM3u8: Boolean get() = type == ExtractorLinkType.M3U8 - val isDash: Boolean get() = type == ExtractorLinkType.DASH + @get:JsonIgnore val isM3u8: Boolean get() = type == ExtractorLinkType.M3U8 + @get:JsonIgnore val isDash: Boolean get() = type == ExtractorLinkType.DASH // Cached video size - private var videoSize: Long? = null + @Transient private var videoSize: Long? = null /** * Get video size in bytes with one head request. Only available for ExtractorLinkType.Video @@ -1367,7 +1372,7 @@ suspend fun getPostForm(requestUrl: String, html: String): String? { "accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" ), data = mapOf("op" to op, "id" to id, "mode" to mode, "hash" to hash) - ).text() + ).text } fun ExtractorApi.fixUrl(url: String): String {