From c03fb5f8da34e3bfa5660b4cc09c296c0835bf79 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 10 Jul 2026 11:24:39 +0300 Subject: [PATCH 1/7] feat(Sync): Scrobble Support + SIMKL Scrobble --- .../cloudstream3/syncproviders/SyncAPI.kt | 30 +++++++ .../cloudstream3/syncproviders/SyncRepo.kt | 12 +++ .../syncproviders/providers/SimklApi.kt | 79 ++++++++++++++++++- .../cloudstream3/ui/player/GeneratorPlayer.kt | 70 ++++++++++++++++ .../cloudstream3/ui/result/SyncViewModel.kt | 19 +++++ 5 files changed, 209 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index f30a647483a..e6802c086c0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -70,6 +70,36 @@ abstract class SyncAPI : AuthAPI() { @Throws open fun urlToId(url: String): String? = null + @Throws + @WorkerThread + open suspend fun onPlaybackStatus( + auth: AuthData?, + progress: PlaybackProgress, + status: PlaybackStatus + ) : Boolean = false + + enum class PlaybackStatus { + Started, + Paused, + Stopped + } + + data class PlaybackProgress( + /** Show/Movie Sync level ID */ + val id: String, + /** Season Number, Should be null for Movies */ + val season: Int?, + /** Episode Number, Should be null for Movies */ + val episode: Int?, + val positionMs: Long, + val durationMs: Long, + val isAnime: Boolean = false, + ) { + val progressPercentage: Double + get() = if (durationMs<= 0L) 0.0 else + (positionMs.toDouble() / durationMs.toDouble() * 100.0).coerceIn(0.0, 100.0) + } + data class SyncSearchResult( override val name: String, override val apiName: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt index de82624fc7e..f847a1e6685 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt @@ -27,4 +27,16 @@ class SyncRepo(override val api: SyncAPI) : AuthRepo(api) { suspend fun library(): Result = runCatching { api.library(freshAuth()) } + + suspend fun onPlaybackStatus( + progress: SyncAPI.PlaybackProgress, + status: SyncAPI.PlaybackStatus + ): Result = + runCatching { + api.onPlaybackStatus( + freshAuth(), + progress, + status + ) + } } 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..aa30931f259 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 @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.syncproviders.providers +import android.util.Log import androidx.annotation.StringRes import androidx.core.net.toUri import com.fasterxml.jackson.annotation.JsonInclude @@ -139,6 +140,8 @@ class SimklApi : SyncAPI() { companion object { private const val CLIENT_ID: String = BuildConfig.SIMKL_CLIENT_ID private const val CLIENT_SECRET: String = BuildConfig.SIMKL_CLIENT_SECRET + private const val APP_NAME: String = "Cloudstream" + private const val APP_VERSION: String = BuildConfig.VERSION_NAME const val SIMKL_CACHED_LIST: String = "simkl_cached_list" const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time" @@ -1083,4 +1086,78 @@ class SimklApi : SyncAPI() { profilePicture = user.user.avatar ) } -} + + override suspend fun onPlaybackStatus( + auth: AuthData?, + progress: PlaybackProgress, + status: PlaybackStatus + ): Boolean { + + val token = auth?.token ?: return false + val ids = MediaObject.Ids.fromMap(readIdFromString(progress.id)) + + val body = if (progress.season != null && progress.episode != null) { + val media = MediaObject( + title = null, + year = null, + ids = ids + ) + ScrobbleRequest( + show = if (!progress.isAnime) media else null, + anime = if (progress.isAnime) media else null, + episode = ScrobbleEpisode( + season = progress.season, + episode = progress.episode + ), + progress = progress.progressPercentage + ) + } else { + ScrobbleRequest( + movie = MediaObject( + title = null, + year = null, + ids = ids + ), + progress = progress.progressPercentage + ) + } + + val endPoint = when (status) { + PlaybackStatus.Started -> "start" + PlaybackStatus.Paused -> "pause" + PlaybackStatus.Stopped -> "stop" + } + + return try { + app.post( + url = "$mainUrl/scrobble/$endPoint", + params = mapOf( + "app-name" to APP_NAME, + "app-version" to APP_VERSION + ), + headers = getHeaders(token) + mapOf("User-Agent" to "$APP_NAME/$APP_VERSION"), + json = body + ).isSuccessful + } catch (e: Exception) { + logError(e) + false + } + } + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class ScrobbleEpisode( + @JsonProperty("season") val season: Int, + @JsonProperty("episode") val episode: Int, + ) + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class ScrobbleRequest( + @JsonProperty("show") val show: MediaObject? = null, + @JsonProperty("movie") val movie: MediaObject? = null, + @JsonProperty("anime") val anime: MediaObject? = null, + @JsonProperty("episode") val episode: ScrobbleEpisode? = null, + @JsonProperty("progress") val progress: Double, + + + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 91e3ff9708d..0683155dcfd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -74,6 +74,7 @@ import com.lagradost.cloudstream3.mvvm.safe import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders +import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding @@ -235,6 +236,57 @@ class GeneratorPlayer : FullScreenPlayer() { if (player.getIsPlaying()) { viewModel.forceClearCache = false } + reportPlaybackStatusToSync() + } + + private var lastSyncPlaybackStatus: CSPlayerLoading? = null + private var lastSyncEpisodeKey: Pair? = null + private var lastSyncPosition: Long = 0L + private var lastSyncDuration: Long = 0L + + private fun reportPlaybackStatusToSync() { + val status = currentPlayerStatus + val episode = currentMeta as? ResultEpisode + val episodeKey = episode?.season to episode?.episode + val isAnime = episode?.tvType?.isAnimeOp() == true + + val episodeChanged = lastSyncEpisodeKey != null && episodeKey != lastSyncEpisodeKey + if (episodeChanged && lastSyncPlaybackStatus != CSPlayerLoading.IsEnded) { + val (oldSeason, oldEpisode) = lastSyncEpisodeKey!! + + sync.updatePlaybackStatus( + status = SyncAPI.PlaybackStatus.Stopped, + season = oldSeason, + episode = oldEpisode, + positionMs = lastSyncPosition, + durationMs = lastSyncDuration, + isAnime = isAnime + ) + } + + if (status == lastSyncPlaybackStatus && !episodeChanged) return + + lastSyncPlaybackStatus = status + lastSyncEpisodeKey = episodeKey + + val playbackStatus = when (status) { + CSPlayerLoading.IsPlaying -> SyncAPI.PlaybackStatus.Started + CSPlayerLoading.IsPaused -> SyncAPI.PlaybackStatus.Paused + CSPlayerLoading.IsEnded -> SyncAPI.PlaybackStatus.Stopped + else -> return //Ignore IsBuffering + } + + lastSyncPosition = player.getPosition() ?: return + lastSyncDuration = player.getDuration() ?: return + + sync.updatePlaybackStatus( + status = playbackStatus, + season = episode?.season, + episode = episode?.episode, + positionMs = lastSyncPosition, + durationMs = lastSyncDuration, + isAnime = isAnime + ) } private fun noSubtitles(): Boolean { @@ -1665,6 +1717,24 @@ class GeneratorPlayer : FullScreenPlayer() { override fun onDestroy() { ResultFragment.updateUI() currentVerifyLink?.cancel() + + if (lastSyncPlaybackStatus != CSPlayerLoading.IsEnded) { + val position = player.getPosition() + val duration = player.getDuration() + if (position != null && duration != null) { + val episode = currentMeta as? ResultEpisode + + sync.updatePlaybackStatus( + status = SyncAPI.PlaybackStatus.Stopped, + season = episode?.season, + episode = episode?.episode, + positionMs = position, + durationMs = duration, + isAnime = episode?.tvType?.isAnimeOp() == true + ) + } + } + super.onDestroy() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index 3161a12044e..bee9ee40873 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -306,4 +306,23 @@ class SyncViewModel : ViewModel() { updateMetadata() updateUserData() } + + /** Triggers on every player event (Start/Pause/Stop/Ended) */ + fun updatePlaybackStatus( + status: SyncAPI.PlaybackStatus, + season: Int?, + episode: Int?, + positionMs: Long, + durationMs: Long, + isAnime: Boolean = false + ) = ioSafe { + syncs.amap { (prefix, id) -> + repos.firstOrNull { + it.idPrefix == prefix + }?.onPlaybackStatus( + progress = SyncAPI.PlaybackProgress(id, season, episode, positionMs, durationMs, isAnime), + status = status + ) + } + } } \ No newline at end of file From 69a1104b9ca8e09ba143cfb6eefe18dc66b2fd93 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 10 Jul 2026 11:29:58 +0300 Subject: [PATCH 2/7] Cleaning --- .../lagradost/cloudstream3/syncproviders/providers/SimklApi.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 aa30931f259..fd889639abf 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 @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.syncproviders.providers -import android.util.Log import androidx.annotation.StringRes import androidx.core.net.toUri import com.fasterxml.jackson.annotation.JsonInclude @@ -140,7 +139,7 @@ class SimklApi : SyncAPI() { companion object { private const val CLIENT_ID: String = BuildConfig.SIMKL_CLIENT_ID private const val CLIENT_SECRET: String = BuildConfig.SIMKL_CLIENT_SECRET - private const val APP_NAME: String = "Cloudstream" + private const val APP_NAME: String = "CloudStream" private const val APP_VERSION: String = BuildConfig.VERSION_NAME const val SIMKL_CACHED_LIST: String = "simkl_cached_list" const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time" From 57935004c7f339f03cd98cf9660c6463b508bbca Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 10 Jul 2026 16:26:48 +0300 Subject: [PATCH 3/7] remove isAnime --- .../lagradost/cloudstream3/syncproviders/SyncAPI.kt | 2 +- .../cloudstream3/syncproviders/providers/SimklApi.kt | 6 ++++-- .../cloudstream3/ui/player/GeneratorPlayer.kt | 10 +++++----- .../cloudstream3/ui/result/SyncViewModel.kt | 12 ++++++++++-- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index e6802c086c0..4ca28705976 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -91,9 +91,9 @@ abstract class SyncAPI : AuthAPI() { val season: Int?, /** Episode Number, Should be null for Movies */ val episode: Int?, + val type: TvType?, val positionMs: Long, val durationMs: Long, - val isAnime: Boolean = false, ) { val progressPercentage: Double get() = if (durationMs<= 0L) 0.0 else 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 fd889639abf..2a956efeb37 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 @@ -17,6 +17,7 @@ import com.lagradost.cloudstream3.Score import com.lagradost.cloudstream3.SimklSyncServices import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.isAnimeOp import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING @@ -1094,6 +1095,7 @@ class SimklApi : SyncAPI() { val token = auth?.token ?: return false val ids = MediaObject.Ids.fromMap(readIdFromString(progress.id)) + val isAnime = progress.type?.isAnimeOp() == true val body = if (progress.season != null && progress.episode != null) { val media = MediaObject( @@ -1102,8 +1104,8 @@ class SimklApi : SyncAPI() { ids = ids ) ScrobbleRequest( - show = if (!progress.isAnime) media else null, - anime = if (progress.isAnime) media else null, + show = if (!isAnime) media else null, + anime = if (isAnime) media else null, episode = ScrobbleEpisode( season = progress.season, episode = progress.episode diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 0683155dcfd..bc729f47f11 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -248,7 +248,7 @@ class GeneratorPlayer : FullScreenPlayer() { val status = currentPlayerStatus val episode = currentMeta as? ResultEpisode val episodeKey = episode?.season to episode?.episode - val isAnime = episode?.tvType?.isAnimeOp() == true + val type = episode?.tvType val episodeChanged = lastSyncEpisodeKey != null && episodeKey != lastSyncEpisodeKey if (episodeChanged && lastSyncPlaybackStatus != CSPlayerLoading.IsEnded) { @@ -258,9 +258,9 @@ class GeneratorPlayer : FullScreenPlayer() { status = SyncAPI.PlaybackStatus.Stopped, season = oldSeason, episode = oldEpisode, + type = type, positionMs = lastSyncPosition, durationMs = lastSyncDuration, - isAnime = isAnime ) } @@ -283,9 +283,9 @@ class GeneratorPlayer : FullScreenPlayer() { status = playbackStatus, season = episode?.season, episode = episode?.episode, + type = type, positionMs = lastSyncPosition, durationMs = lastSyncDuration, - isAnime = isAnime ) } @@ -1728,9 +1728,9 @@ class GeneratorPlayer : FullScreenPlayer() { status = SyncAPI.PlaybackStatus.Stopped, season = episode?.season, episode = episode?.episode, + type = episode?.tvType, positionMs = position, - durationMs = duration, - isAnime = episode?.tvType?.isAnimeOp() == true + durationMs = duration ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index bee9ee40873..1d12d33b2e0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.lagradost.cloudstream3.Score +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError @@ -312,15 +313,22 @@ class SyncViewModel : ViewModel() { status: SyncAPI.PlaybackStatus, season: Int?, episode: Int?, + type: TvType?, positionMs: Long, durationMs: Long, - isAnime: Boolean = false ) = ioSafe { syncs.amap { (prefix, id) -> repos.firstOrNull { it.idPrefix == prefix }?.onPlaybackStatus( - progress = SyncAPI.PlaybackProgress(id, season, episode, positionMs, durationMs, isAnime), + progress = SyncAPI.PlaybackProgress( + id = id, + season = season, + episode = episode, + type = type, + positionMs = positionMs, + durationMs = durationMs + ), status = status ) } From d613403642c5dcfccf08b2c1e34f5a5c3c1035a1 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 10 Jul 2026 19:26:16 +0300 Subject: [PATCH 4/7] Add Scrobble settings --- .../lagradost/cloudstream3/syncproviders/AuthAPI.kt | 5 +++++ .../cloudstream3/syncproviders/AuthRepo.kt | 6 ++++++ .../lagradost/cloudstream3/syncproviders/SyncAPI.kt | 1 + .../syncproviders/providers/SimklApi.kt | 1 + .../cloudstream3/ui/result/SyncViewModel.kt | 2 +- .../cloudstream3/ui/settings/SettingsAccount.kt | 13 ++++++++++++- app/src/main/res/layout/account_managment.xml | 8 ++++++++ app/src/main/res/values/strings.xml | 1 + 8 files changed, 35 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt index 60e41e8b3d5..4fea633307a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt @@ -148,6 +148,11 @@ abstract class AuthAPI { /** The requirements to login in app */ open val inAppLoginRequirement: AuthLoginRequirement? = null + /** Determine if SyncApi supports Playback Scrobbling. + * @see SyncAPI.onPlaybackStatus + * */ + open val supportScrobble: Boolean = false + companion object { @Deprecated( message = "Use APIHolder.unixTime instead", diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt index 645a19e3a60..5457abe24c3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt @@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safe import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.NONE_ID +import com.lagradost.cloudstream3.utils.PreferenceDelegate import com.lagradost.cloudstream3.utils.txt /** General-purpose repo */ @@ -25,6 +26,11 @@ abstract class AuthRepo(open val api: AuthAPI) { val hasInApp get() = api.hasInApp val inAppLoginRequirement get() = api.inAppLoginRequirement val isAvailable get() = !api.requiresLogin || authUser() != null + val supportScrobble get() = api.supportScrobble + + val supportScrobbleDelegate: PreferenceDelegate by lazy { + PreferenceDelegate("$idPrefix/supportScrobble", api.supportScrobble) + } companion object { private val oauthPayload: MutableMap = mutableMapOf() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index 4ca28705976..38fde81e2ff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -84,6 +84,7 @@ abstract class SyncAPI : AuthAPI() { Stopped } + /** Triggers on every player event (Start/Pause/Stop/Ended) */ data class PlaybackProgress( /** Show/Movie Sync level ID */ val id: String, 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 2a956efeb37..47d6459f177 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 @@ -58,6 +58,7 @@ class SimklApi : SyncAPI() { override val icon = R.drawable.simkl_logo override val createAccountUrl = "$mainUrl/signup" override val syncIdName = SyncIdName.Simkl + override val supportScrobble = true /** Automatically adds simkl auth headers */ // private val interceptor = HeaderInterceptor() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index 1d12d33b2e0..9862220bcd3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -319,7 +319,7 @@ class SyncViewModel : ViewModel() { ) = ioSafe { syncs.amap { (prefix, id) -> repos.firstOrNull { - it.idPrefix == prefix + it.idPrefix == prefix && it.supportScrobble }?.onPlaybackStatus( progress = SyncAPI.PlaybackProgress( id = id, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 75ba55674a6..5c4ca16f891 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -61,6 +61,7 @@ import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.ImageLoader.loadImage +import com.lagradost.cloudstream3.utils.PreferenceDelegate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -72,6 +73,7 @@ import com.lagradost.cloudstream3.utils.txt import qrcode.QRCode class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { + companion object { /** Used by nginx plugin too */ @SuppressLint("StringFormatInvalid") @@ -93,6 +95,12 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { !info?.profilePicture.isNullOrEmpty() binding.accountMainProfilePicture.loadImage(info?.profilePicture) + binding.accountSupportScrobbleSwitch.isChecked = api.supportScrobbleDelegate.getValue(this, api::supportScrobble) + binding.accountSupportScrobbleSwitch.isVisible = api.supportScrobble + binding.accountSupportScrobbleSwitch.setOnCheckedChangeListener { _, isChecked -> + api.supportScrobbleDelegate.setValue(this, api::supportScrobble, isChecked) + } + binding.accountLogout.isVisible = info != null binding.accountLogout.setOnClickListener { if (info != null) { @@ -117,7 +125,10 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { } if (isLayout(TV or EMULATOR)) { - binding.accountSwitchAccount.requestFocus() + if (binding.accountSupportScrobbleSwitch.isVisible) + binding.accountSupportScrobbleSwitch.requestFocus() + else + binding.accountSwitchAccount.requestFocus() } } diff --git a/app/src/main/res/layout/account_managment.xml b/app/src/main/res/layout/account_managment.xml index e7afb382c87..e9112a19a75 100644 --- a/app/src/main/res/layout/account_managment.xml +++ b/app/src/main/res/layout/account_managment.xml @@ -61,6 +61,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31cf951cf5f..6863888f002 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -783,5 +783,6 @@ %d downloads queued Live + Enable Scrobbling From 548a0f9eea03624ac5b1db709f6388b9498a7702 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 11 Jul 2026 05:34:14 +0300 Subject: [PATCH 5/7] Respect user choice & Serializable --- .../cloudstream3/syncproviders/AuthRepo.kt | 6 +++++- .../syncproviders/providers/SimklApi.kt | 19 ++++++++++++++----- .../cloudstream3/ui/result/SyncViewModel.kt | 2 +- .../ui/settings/SettingsAccount.kt | 4 ++-- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt index 5457abe24c3..93031a8aeea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt @@ -28,10 +28,14 @@ abstract class AuthRepo(open val api: AuthAPI) { val isAvailable get() = !api.requiresLogin || authUser() != null val supportScrobble get() = api.supportScrobble - val supportScrobbleDelegate: PreferenceDelegate by lazy { + private val supportScrobbleDelegate: PreferenceDelegate by lazy { PreferenceDelegate("$idPrefix/supportScrobble", api.supportScrobble) } + var scrobbleEnabled: Boolean + get() = supportScrobbleDelegate.getValue(this, ::scrobbleEnabled) + set(value) = supportScrobbleDelegate.setValue(this, ::scrobbleEnabled, value) + companion object { private val oauthPayload: MutableMap = mutableMapOf() } 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 47d6459f177..8e3739f7ed2 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 @@ -33,7 +33,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 @@ -1147,10 +1152,16 @@ class SimklApi : SyncAPI() { } @JsonInclude(JsonInclude.Include.NON_EMPTY) + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @Serializable(with = ScrobbleEpisode.Serializer::class) + @KeepGeneratedSerializer data class ScrobbleEpisode( - @JsonProperty("season") val season: Int, - @JsonProperty("episode") val episode: Int, - ) + @JsonProperty("season") @SerialName("season") val season: Int, + @JsonProperty("episode") @SerialName("episode") val episode: Int, + ) { + object Serializer : + NonEmptySerializer(generatedSerializer()) + } @JsonInclude(JsonInclude.Include.NON_EMPTY) data class ScrobbleRequest( @@ -1159,7 +1170,5 @@ class SimklApi : SyncAPI() { @JsonProperty("anime") val anime: MediaObject? = null, @JsonProperty("episode") val episode: ScrobbleEpisode? = null, @JsonProperty("progress") val progress: Double, - - ) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index 9862220bcd3..ea6dd25aff7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -319,7 +319,7 @@ class SyncViewModel : ViewModel() { ) = ioSafe { syncs.amap { (prefix, id) -> repos.firstOrNull { - it.idPrefix == prefix && it.supportScrobble + it.idPrefix == prefix && it.supportScrobble && it.scrobbleEnabled }?.onPlaybackStatus( progress = SyncAPI.PlaybackProgress( id = id, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 5c4ca16f891..c707f59b629 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -95,10 +95,10 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { !info?.profilePicture.isNullOrEmpty() binding.accountMainProfilePicture.loadImage(info?.profilePicture) - binding.accountSupportScrobbleSwitch.isChecked = api.supportScrobbleDelegate.getValue(this, api::supportScrobble) + binding.accountSupportScrobbleSwitch.isChecked = api.scrobbleEnabled binding.accountSupportScrobbleSwitch.isVisible = api.supportScrobble binding.accountSupportScrobbleSwitch.setOnCheckedChangeListener { _, isChecked -> - api.supportScrobbleDelegate.setValue(this, api::supportScrobble, isChecked) + api.scrobbleEnabled = isChecked } binding.accountLogout.isVisible = info != null From efeadaf162cb1bc5539e216b0152fda4c34bde78 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 11 Jul 2026 06:41:40 +0300 Subject: [PATCH 6/7] Serializing ScrobbleRequest --- .../syncproviders/providers/SimklApi.kt | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) 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 60bfc7be9a6..47f03415974 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 @@ -28,6 +28,9 @@ import com.lagradost.cloudstream3.syncproviders.AuthToken import com.lagradost.cloudstream3.syncproviders.AuthUser import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.syncproviders.providers.SimklApi.ScrobbleEpisode +import com.lagradost.cloudstream3.syncproviders.providers.SimklApi.ScrobbleEpisode.Companion.generatedSerializer +import com.lagradost.cloudstream3.syncproviders.providers.SimklApi.ScrobbleRequest import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.toJson @@ -1214,11 +1217,17 @@ class SimklApi : SyncAPI() { } @JsonInclude(JsonInclude.Include.NON_EMPTY) + @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now + @Serializable(with = ScrobbleRequest.Serializer::class) + @KeepGeneratedSerializer data class ScrobbleRequest( - @JsonProperty("show") val show: MediaObject? = null, - @JsonProperty("movie") val movie: MediaObject? = null, - @JsonProperty("anime") val anime: MediaObject? = null, - @JsonProperty("episode") val episode: ScrobbleEpisode? = null, - @JsonProperty("progress") val progress: Double, - ) + @JsonProperty("show") @SerialName("show") val show: MediaObject? = null, + @JsonProperty("movie") @SerialName("movie") val movie: MediaObject? = null, + @JsonProperty("anime") @SerialName("anime") val anime: MediaObject? = null, + @JsonProperty("episode") @SerialName("episode") val episode: ScrobbleEpisode? = null, + @JsonProperty("progress") @SerialName("progress") val progress: Double, + ) { + object Serializer : + NonEmptySerializer(generatedSerializer()) + } } \ No newline at end of file From 52b1bf8e757bfeb98d85198a0943f7ed242d8858 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sun, 12 Jul 2026 09:29:31 +0300 Subject: [PATCH 7/7] Code formatting & KDoc standards --- .../cloudstream3/syncproviders/AuthAPI.kt | 102 +++++++++++++----- .../cloudstream3/syncproviders/AuthRepo.kt | 8 +- .../cloudstream3/syncproviders/SyncAPI.kt | 59 +++++++--- .../cloudstream3/syncproviders/SyncRepo.kt | 4 +- .../syncproviders/providers/SimklApi.kt | 73 +++++++++---- .../cloudstream3/ui/result/SyncViewModel.kt | 4 +- .../ui/settings/SettingsAccount.kt | 4 +- app/src/main/res/layout/account_managment.xml | 98 ++++++++--------- 8 files changed, 235 insertions(+), 117 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt index 4fea633307a..ff74455e449 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt @@ -16,7 +16,9 @@ import java.net.URI import java.security.SecureRandom data class AuthLoginPage( - /** The website to open to authenticate */ + /** + * The website to open to authenticate + */ val url: String, /** * State/control code to verify against the redirectUrl to make sure the request is valid. @@ -33,13 +35,19 @@ data class AuthToken( */ @JsonProperty("accessToken") @SerialName("accessToken") val accessToken: String? = null, - /** For OAuth a special refresh token is issues to refresh the access token. */ + /** + * For OAuth a special refresh token is issues to refresh the access token. + */ @JsonProperty("refreshToken") @SerialName("refreshToken") val refreshToken: String? = null, - /** In UnixTime (sec) when it expires */ + /** + * In UnixTime (sec) when it expires + */ @JsonProperty("accessTokenLifetime") @SerialName("accessTokenLifetime") val accessTokenLifetime: Long? = null, - /** In UnixTime (sec) when it expires */ + /** + * In UnixTime (sec) when it expires + * */ @JsonProperty("refreshTokenLifetime") @SerialName("refreshTokenLifetime") val refreshTokenLifetime: Long? = null, /** @@ -59,7 +67,9 @@ data class AuthToken( @OptIn(ExperimentalSerializationApi::class) // JsonNames is an experimental annotation for now @Serializable data class AuthUser( - /** Account display-name, can also be email if name does not exist */ + /** + * Account display-name, can also be email if name does not exist + */ @JsonProperty("name") @SerialName("name") val name: String?, /** @@ -68,10 +78,14 @@ data class AuthUser( */ @JsonProperty("id") @SerialName("id") val id: Int, - /** Profile picture URL */ + /** + * Profile picture URL + */ @JsonProperty("profilePicture") @SerialName("profilePicture") val profilePicture: String? = null, - /** Profile picture Headers of the URL */ + /** + * Profile picture Headers of the URL + */ @JsonProperty("profilePictureHeaders") @JsonAlias("profilePictureHeader") @SerialName("profilePictureHeaders") @JsonNames("profilePictureHeader") val profilePictureHeaders: Map? = null, @@ -102,7 +116,9 @@ data class AuthPinData( val interval: Int, ) -/** The login field requirements to display to the user */ +/** + * The login field requirements to display to the user + */ data class AuthLoginRequirement( val password: Boolean = false, val username: Boolean = false, @@ -119,38 +135,56 @@ data class AuthLoginResponse( @JsonProperty("server") @SerialName("server") val server: String?, ) -/** Stateless Authentication class used for all personalized content */ +/** + * Stateless Authentication class used for all personalized content + */ abstract class AuthAPI { open val name: String = "NONE" open val idPrefix: String = "NONE" - /** Drawable icon of the service */ + /** + * Drawable icon of the service + */ open val icon: Int? = null - /** If this service requires an account to use */ + /** + * If this service requires an account to use + * */ open val requiresLogin: Boolean = true - /** Link to a website for creating a new account */ + /** + * Link to a website for creating a new account + * */ open val createAccountUrl: String? = null - /** The sensitive redirect URL from OAuth should contain "/redirectUrlIdentifier" to trigger the login */ + /** + * The sensitive redirect URL from OAuth should contain "/redirectUrlIdentifier" to trigger the login + */ open val redirectUrlIdentifier: String? = null - /** Has OAuth2 login support, including login, loginRequest and refreshToken */ + /** + * Has OAuth2 login support, including login, loginRequest and refreshToken + */ open val hasOAuth2: Boolean = false - /** Has on device pin support, aka login with a QR code */ + /** + * Has on device pin support, aka login with a QR code + */ open val hasPin: Boolean = false - /** Has in app login support, aka login with a dialog */ + /** + * Has in app login support, aka login with a dialog + */ open val hasInApp: Boolean = false - /** The requirements to login in app */ + /** + * The requirements to login in app + */ open val inAppLoginRequirement: AuthLoginRequirement? = null /** Determine if SyncApi supports Playback Scrobbling. * @see SyncAPI.onPlaybackStatus - * */ + */ open val supportScrobble: Boolean = false companion object { @@ -195,37 +229,53 @@ abstract class AuthAPI { } } - /** Is this url a valid redirect url for this service? */ + /** + * Is this url a valid redirect url for this service? + */ @Throws open fun isValidRedirectUrl(url: String): Boolean = redirectUrlIdentifier != null && url.contains("/$redirectUrlIdentifier") - /** OAuth2 login from a valid redirectUrl, and payload given in loginRequest */ + /** + * OAuth2 login from a valid redirectUrl, and payload given in loginRequest + */ @Throws open suspend fun login(redirectUrl: String, payload: String?): AuthToken? = throw NotImplementedError() - /** OAuth2 login request, asking the service to provide a url to open in the browser */ + /** + * OAuth2 login request, asking the service to provide a url to open in the browser + */ @Throws open fun loginRequest(): AuthLoginPage? = throw NotImplementedError() - /** Pin login request, asking the service to provide an verificationUrl to display with a QR code */ + /** + * Pin login request, asking the service to provide an verificationUrl to display with a QR code + * */ @Throws open suspend fun pinRequest(): AuthPinData? = throw NotImplementedError() - /** OAuth2 token refresh, this ensures that all token passed to other functions will be valid */ + /** + * OAuth2 token refresh, this ensures that all token passed to other functions will be valid + */ @Throws open suspend fun refreshToken(token: AuthToken): AuthToken? = throw NotImplementedError() - /** Pin login, this will be called periodically while logging in to check if the pin has been verified by the user */ + /** + * Pin login, this will be called periodically while logging in to check if the pin has been verified by the user + */ @Throws open suspend fun login(payload: AuthPinData): AuthToken? = throw NotImplementedError() - /** In app login */ + /** + * In-app login + */ @Throws open suspend fun login(form: AuthLoginResponse): AuthToken? = throw NotImplementedError() - /** Get the visible user account */ + /** + * Get the visible user account + */ @Throws open suspend fun user(token: AuthToken?): AuthUser? = throw NotImplementedError() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt index 93031a8aeea..12f295e8b4c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt @@ -10,10 +10,14 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.NONE_ID import com.lagradost.cloudstream3.utils.PreferenceDelegate import com.lagradost.cloudstream3.utils.txt -/** General-purpose repo */ +/** + * General-purpose repo + */ class PlainAuthRepo(api: AuthAPI) : AuthRepo(api) -/** Safe abstraction for AuthAPI that provides both a catching interface, and automatic token management. */ +/** + * Safe abstraction for AuthAPI that provides both a catching interface, and automatic token management. + */ abstract class AuthRepo(open val api: AuthAPI) { fun isValidRedirectUrl(url: String) = safe { api.isValidRedirectUrl(url) } ?: false val idPrefix get() = api.idPrefix diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index 38fde81e2ff..6257976c873 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -26,16 +26,21 @@ abstract class SyncAPI : AuthAPI() { open var requireLibraryRefresh: Boolean = true open val mainUrl: String = "NONE" - /** Currently unused, but will be used to correctly render the UI. - * This should specify what sync watch types can be used with this service. */ + /** + * Currently unused, but will be used to correctly render the UI. + * This should specify what sync watch types can be used with this service. + * */ open val supportedWatchTypes: Set = SyncWatchType.entries.toSet() + /** * Allows certain providers to open pages from * library links. **/ open val syncIdName: SyncIdName? = null - /** Modify the current status of an item */ + /** + * Modify the current status of an item + * */ @Throws @WorkerThread open suspend fun updateStatus( @@ -44,29 +49,39 @@ abstract class SyncAPI : AuthAPI() { newStatus: AbstractSyncStatus ): Boolean = throw NotImplementedError() - /** Get the current status of an item */ + /** + * Get the current status of an item + * */ @Throws @WorkerThread open suspend fun status(auth: AuthData?, id: String): AbstractSyncStatus? = throw NotImplementedError() - /** Get metadata about an item */ + /** + * Get metadata about an item + * */ @Throws @WorkerThread open suspend fun load(auth: AuthData?, id: String): SyncResult? = throw NotImplementedError() - /** Search this service for any results for a given query */ + /** + * Search this service for any results for a given query + * */ @Throws @WorkerThread open suspend fun search(auth: AuthData?, query: String): List? = throw NotImplementedError() - /** Get the current library/bookmarks of this service */ + /** + * Get the current library/bookmarks of this service + * */ @Throws @WorkerThread open suspend fun library(auth: AuthData?): LibraryMetadata? = throw NotImplementedError() - /** Helper function, may be used in the future */ + /** + * Helper function, may be used in the future + * */ @Throws open fun urlToId(url: String): String? = null @@ -76,7 +91,7 @@ abstract class SyncAPI : AuthAPI() { auth: AuthData?, progress: PlaybackProgress, status: PlaybackStatus - ) : Boolean = false + ): Boolean = false enum class PlaybackStatus { Started, @@ -84,20 +99,28 @@ abstract class SyncAPI : AuthAPI() { Stopped } - /** Triggers on every player event (Start/Pause/Stop/Ended) */ + /** + * Triggers on every player event (Start/Pause/Stop/Ended) + * */ data class PlaybackProgress( - /** Show/Movie Sync level ID */ + /** + * Show/Movie Sync level ID + * */ val id: String, - /** Season Number, Should be null for Movies */ + /** + * Season Number, Should be null for Movies + * */ val season: Int?, - /** Episode Number, Should be null for Movies */ + /** + * Episode Number, Should be null for Movies + * */ val episode: Int?, val type: TvType?, val positionMs: Long, val durationMs: Long, ) { val progressPercentage: Double - get() = if (durationMs<= 0L) 0.0 else + get() = if (durationMs <= 0L) 0.0 else (positionMs.toDouble() / durationMs.toDouble() * 100.0).coerceIn(0.0, 100.0) } @@ -151,9 +174,13 @@ abstract class SyncAPI : AuthAPI() { var posterUrl: String? = null, var backgroundPosterUrl: String? = null, - /** In unixtime */ + /** + * In unixtime + * */ var startDate: Long? = null, - /** In unixtime */ + /** + * In unixtime + * */ var endDate: Long? = null, var recommendations: List? = null, var nextSeason: SyncSearchResult? = null, diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt index f847a1e6685..9522a76620e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt @@ -1,6 +1,8 @@ package com.lagradost.cloudstream3.syncproviders -/** Stateless safe abstraction of SyncAPI */ +/** + * Stateless safe abstraction of SyncAPI + * */ class SyncRepo(override val api: SyncAPI) : AuthRepo(api) { val syncIdName = api.syncIdName var requireLibraryRefresh: Boolean 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 47f03415974..9794b995647 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 @@ -75,6 +75,7 @@ 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") @@ -126,11 +127,16 @@ class SimklApi : SyncAPI() { setKey( SIMKL_CACHE_KEY, path, - MediaObjectCacheEntry(value, APIHolder.unixTime + cacheTime.inWholeSeconds).toJson(), + MediaObjectCacheEntry( + value, + APIHolder.unixTime + cacheTime.inWholeSeconds + ).toJson(), ) } - /** Gets the cached [MediaObject], if it's not fresh returns null and removes it from cache */ + /** + * 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) @@ -139,7 +145,7 @@ class SimklApi : SyncAPI() { 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." + "Remains fresh for ${cache.validUntil.remaining().inWholeDays} days or ${cache.validUntil.remaining().inWholeSeconds} seconds." } cache.obj } else { @@ -158,7 +164,9 @@ class SimklApi : SyncAPI() { ) } - /** Gets the cached episode list, if it's not fresh returns null and removes it from cache */ + /** + * 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) @@ -167,7 +175,7 @@ class SimklApi : SyncAPI() { 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." + "Remains fresh for ${cache.validUntil.remaining().inWholeDays} days or ${cache.validUntil.remaining().inWholeSeconds} seconds." } cache.obj } else { @@ -186,7 +194,9 @@ class SimklApi : SyncAPI() { const val SIMKL_CACHED_LIST: String = "simkl_cached_list" const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time" - /** 2014-09-01T09:10:11Z -> 1409562611 */ + /** + * 2014-09-01T09:10:11Z -> 1409562611 + */ private const val SIMKL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'" fun getUnixTime(string: String?): Long? { return try { @@ -201,7 +211,9 @@ class SimklApi : SyncAPI() { } } - /** 1409562611 -> 2014-09-01T09:10:11Z */ + /** + * 1409562611 -> 2014-09-01T09:10:11Z + */ fun getDateTime(unixTime: Long?): String? { return try { SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply { @@ -320,7 +332,9 @@ class SimklApi : SyncAPI() { ) } - /** https://simkl.docs.apiary.io/#reference/tv/episodes/get-tv-show-episodes */ + /** + * 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 @@ -332,7 +346,8 @@ class SimklApi : SyncAPI() { @JsonProperty("episode") @SerialName("episode") val episode: Int, @JsonProperty("img") @SerialName("img") val img: String?, ) { - object Serializer : NonEmptySerializer(EpisodeMetadata.generatedSerializer()) + object Serializer : + NonEmptySerializer(EpisodeMetadata.generatedSerializer()) companion object { fun convertToEpisodes(list: List?): List? { @@ -537,7 +552,8 @@ class SimklApi : SyncAPI() { } ?: 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 @@ -629,7 +645,8 @@ class SimklApi : SyncAPI() { @JsonProperty("rating") @SerialName("rating") val rating: Int? = null, @JsonProperty("rated_at") @SerialName("rated_at") val ratedAt: String? = null, ) { - object Serializer : NonEmptySerializer(HistoryMediaObject.generatedSerializer()) + object Serializer : + NonEmptySerializer(HistoryMediaObject.generatedSerializer()) } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -641,9 +658,12 @@ class SimklApi : SyncAPI() { @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), + @JsonProperty("rated_at") @SerialName("rated_at") val ratedAt: String? = getDateTime( + APIHolder.unixTime + ), ) { - object Serializer : NonEmptySerializer(RatingMediaObject.generatedSerializer()) + object Serializer : + NonEmptySerializer(RatingMediaObject.generatedSerializer()) } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -655,9 +675,12 @@ class SimklApi : SyncAPI() { @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), + @JsonProperty("watched_at") @SerialName("watched_at") val watchedAt: String? = getDateTime( + APIHolder.unixTime + ), ) { - object Serializer : NonEmptySerializer(StatusMediaObject.generatedSerializer()) + object Serializer : + NonEmptySerializer(StatusMediaObject.generatedSerializer()) } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -668,7 +691,8 @@ class SimklApi : SyncAPI() { @JsonProperty("movies") @SerialName("movies") val movies: List, @JsonProperty("shows") @SerialName("shows") val shows: List, ) { - object Serializer : NonEmptySerializer(StatusRequest.generatedSerializer()) + object Serializer : + NonEmptySerializer(StatusRequest.generatedSerializer()) } /** Same shape as [StatusRequest], for the endpoints that post [HistoryMediaObject]s instead. */ @@ -680,10 +704,13 @@ class SimklApi : SyncAPI() { @JsonProperty("movies") @SerialName("movies") val movies: List, @JsonProperty("shows") @SerialName("shows") val shows: List, ) { - object Serializer : NonEmptySerializer(HistoryRequest.generatedSerializer()) + object Serializer : + NonEmptySerializer(HistoryRequest.generatedSerializer()) } - /** https://simkl.docs.apiary.io/#reference/sync/get-all-items/get-all-items-in-the-user's-watchlist */ + /** + * https://simkl.docs.apiary.io/#reference/sync/get-all-items/get-all-items-in-the-user's-watchlist + */ @Serializable data class AllItemsResponse( @JsonProperty("shows") @SerialName("shows") val shows: List = emptyList(), @@ -976,7 +1003,9 @@ class SimklApi : SyncAPI() { return builder.execute() } - /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ + /** + * 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( @@ -995,7 +1024,8 @@ 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, @@ -1018,7 +1048,8 @@ class SimklApi : SyncAPI() { } 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? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index ea6dd25aff7..263af32b82e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -308,7 +308,9 @@ class SyncViewModel : ViewModel() { updateUserData() } - /** Triggers on every player event (Start/Pause/Stop/Ended) */ + /** + * Triggers on every player event (Start/Pause/Stop/Ended) + * */ fun updatePlaybackStatus( status: SyncAPI.PlaybackStatus, season: Int?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index c707f59b629..bae08452a1a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -75,7 +75,9 @@ import qrcode.QRCode class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { companion object { - /** Used by nginx plugin too */ + /** + * Used by nginx plugin too + */ @SuppressLint("StringFormatInvalid") fun showLoginInfo( activity: FragmentActivity?, diff --git a/app/src/main/res/layout/account_managment.xml b/app/src/main/res/layout/account_managment.xml index e9112a19a75..850f03226b9 100644 --- a/app/src/main/res/layout/account_managment.xml +++ b/app/src/main/res/layout/account_managment.xml @@ -1,84 +1,84 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingStart="20dp" + android:paddingTop="20dp" + android:paddingEnd="20dp" + android:paddingBottom="10dp"> + android:id="@+id/account_main_profile_picture_holder" + android:layout_width="35dp" + android:layout_height="35dp" + android:layout_gravity="center_vertical" + app:cardCornerRadius="100dp"> + android:id="@+id/account_main_profile_picture" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="ContentDescription" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:orientation="vertical" + android:paddingStart="20dp" + android:paddingEnd="20dp"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_rowWeight="1" + android:textColor="?attr/textColor" + android:textSize="20sp" + android:textStyle="bold" + tools:text="MAL HelloWorld" /> + android:id="@+id/account_site" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_rowWeight="1" + android:layout_gravity="center_vertical" + android:textColor="?attr/grayTextColor" + android:textSize="15sp" + tools:text="MyAnimeList" /> + android:focusable="true" + android:text="@string/supports_scrobble"> + android:focusable="true" + android:text="@string/switch_account" /> + android:focusable="true" + android:text="@string/logout">