mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	Added Simkl (#548)
This commit is contained in:
		
							parent
							
								
									dd4f4a2b78
								
							
						
					
					
						commit
						d2d2e41fb3
					
				
					 16 changed files with 988 additions and 63 deletions
				
			
		
							
								
								
									
										2
									
								
								.github/workflows/build_to_archive.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/build_to_archive.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -56,6 +56,8 @@ jobs: | ||||||
|         SIGNING_KEY_ALIAS: "key0" |         SIGNING_KEY_ALIAS: "key0" | ||||||
|         SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} |         SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} | ||||||
|         SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} |         SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} | ||||||
|  |         SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} | ||||||
|  |         SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} | ||||||
|     - uses: actions/checkout@v3 |     - uses: actions/checkout@v3 | ||||||
|       with: |       with: | ||||||
|         repository: "recloudstream/cloudstream-archive" |         repository: "recloudstream/cloudstream-archive" | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								.github/workflows/prerelease.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/prerelease.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -48,6 +48,8 @@ jobs: | ||||||
|         SIGNING_KEY_ALIAS: "key0" |         SIGNING_KEY_ALIAS: "key0" | ||||||
|         SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} |         SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} | ||||||
|         SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} |         SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} | ||||||
|  |         SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} | ||||||
|  |         SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} | ||||||
|     - name: Create pre-release |     - name: Create pre-release | ||||||
|       uses: "marvinpinto/action-automatic-releases@latest" |       uses: "marvinpinto/action-automatic-releases@latest" | ||||||
|       with: |       with: | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties | ||||||
| import org.jetbrains.dokka.gradle.DokkaTask | import org.jetbrains.dokka.gradle.DokkaTask | ||||||
| import java.io.ByteArrayOutputStream | import java.io.ByteArrayOutputStream | ||||||
| import java.net.URL | import java.net.URL | ||||||
|  | @ -54,17 +55,27 @@ android { | ||||||
|         versionName = "4.1.3" |         versionName = "4.1.3" | ||||||
| 
 | 
 | ||||||
|         resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") |         resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") | ||||||
| 
 |  | ||||||
|         resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") |         resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") | ||||||
| 
 |  | ||||||
|         resValue("bool", "is_prerelease", "false") |         resValue("bool", "is_prerelease", "false") | ||||||
| 
 | 
 | ||||||
|  |         // Reads local.properties | ||||||
|  |         val localProperties = gradleLocalProperties(rootDir) | ||||||
|  | 
 | ||||||
|         buildConfigField( |         buildConfigField( | ||||||
|             "String", |             "String", | ||||||
|             "BUILDDATE", |             "BUILDDATE", | ||||||
|             "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));" |             "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));" | ||||||
|         ) |         ) | ||||||
| 
 |         buildConfigField( | ||||||
|  |             "String", | ||||||
|  |             "SIMKL_CLIENT_ID", | ||||||
|  |             "\"" + (System.getenv("SIMKL_CLIENT_ID") ?: localProperties["simkl.id"]) + "\"" | ||||||
|  |         ) | ||||||
|  |         buildConfigField( | ||||||
|  |             "String", | ||||||
|  |             "SIMKL_CLIENT_SECRET", | ||||||
|  |             "\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\"" | ||||||
|  |         ) | ||||||
|         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" |         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||||||
| 
 | 
 | ||||||
|         kapt { |         kapt { | ||||||
|  | @ -108,9 +119,9 @@ android { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     //toolchain { |     //toolchain { | ||||||
|    //     languageVersion.set(JavaLanguageVersion.of(17)) |     //     languageVersion.set(JavaLanguageVersion.of(17)) | ||||||
|    // } |     // } | ||||||
|    // jvmToolchain(17) |     // jvmToolchain(17) | ||||||
| 
 | 
 | ||||||
|     compileOptions { |     compileOptions { | ||||||
|         isCoreLibraryDesugaringEnabled = true |         isCoreLibraryDesugaringEnabled = true | ||||||
|  | @ -211,7 +222,7 @@ dependencies { | ||||||
|     // Networking |     // Networking | ||||||
| //    implementation("com.squareup.okhttp3:okhttp:4.9.2") | //    implementation("com.squareup.okhttp3:okhttp:4.9.2") | ||||||
| //    implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") | //    implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") | ||||||
|     implementation("com.github.Blatzar:NiceHttp:0.4.2") |     implementation("com.github.Blatzar:NiceHttp:0.4.3") | ||||||
|     // To fix SSL fuckery on android 9 |     // To fix SSL fuckery on android 9 | ||||||
|     implementation("org.conscrypt:conscrypt-android:2.2.1") |     implementation("org.conscrypt:conscrypt-android:2.2.1") | ||||||
|     // Util to skip the URI file fuckery 🙏 |     // Util to skip the URI file fuckery 🙏 | ||||||
|  |  | ||||||
|  | @ -11,9 +11,12 @@ import com.fasterxml.jackson.databind.DeserializationFeature | ||||||
| import com.fasterxml.jackson.databind.json.JsonMapper | import com.fasterxml.jackson.databind.json.JsonMapper | ||||||
| import com.fasterxml.jackson.module.kotlin.KotlinModule | import com.fasterxml.jackson.module.kotlin.KotlinModule | ||||||
| import com.lagradost.cloudstream3.mvvm.logError | import com.lagradost.cloudstream3.mvvm.logError | ||||||
|  | import com.lagradost.cloudstream3.mvvm.normalSafeApiCall | ||||||
| import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi | ||||||
| import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi | ||||||
| import com.lagradost.cloudstream3.syncproviders.SyncIdName | import com.lagradost.cloudstream3.syncproviders.SyncIdName | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.providers.SimklApi | ||||||
| import com.lagradost.cloudstream3.ui.player.SubtitleData | import com.lagradost.cloudstream3.ui.player.SubtitleData | ||||||
| import com.lagradost.cloudstream3.utils.* | import com.lagradost.cloudstream3.utils.* | ||||||
| import com.lagradost.cloudstream3.utils.AppUtils.toJson | import com.lagradost.cloudstream3.utils.AppUtils.toJson | ||||||
|  | @ -821,7 +824,8 @@ public enum class AutoDownloadMode(val value: Int) { | ||||||
|     ; |     ; | ||||||
| 
 | 
 | ||||||
|     companion object { |     companion object { | ||||||
|         infix fun getEnum(value: Int): AutoDownloadMode? = AutoDownloadMode.values().firstOrNull { it.value == value } |         infix fun getEnum(value: Int): AutoDownloadMode? = | ||||||
|  |             AutoDownloadMode.values().firstOrNull { it.value == value } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -1143,6 +1147,7 @@ interface LoadResponse { | ||||||
|     companion object { |     companion object { | ||||||
|         private val malIdPrefix = malApi.idPrefix |         private val malIdPrefix = malApi.idPrefix | ||||||
|         private val aniListIdPrefix = aniListApi.idPrefix |         private val aniListIdPrefix = aniListApi.idPrefix | ||||||
|  |         private val simklIdPrefix = simklApi.idPrefix | ||||||
|         var isTrailersEnabled = true |         var isTrailersEnabled = true | ||||||
| 
 | 
 | ||||||
|         fun LoadResponse.isMovie(): Boolean { |         fun LoadResponse.isMovie(): Boolean { | ||||||
|  | @ -1164,6 +1169,20 @@ interface LoadResponse { | ||||||
|             this.actors = actors?.map { (actor, role) -> ActorData(actor, role = role) } |             this.actors = actors?.map { (actor, role) -> ActorData(actor, role = role) } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         /** | ||||||
|  |          * Internal helper function to add simkl ids from other databases. | ||||||
|  |          */ | ||||||
|  |         private fun LoadResponse.addSimklId( | ||||||
|  |             database: SimklApi.Companion.SyncServices, | ||||||
|  |             id: String? | ||||||
|  |         ) { | ||||||
|  |             normalSafeApiCall { | ||||||
|  |                 this.syncData[simklIdPrefix] = | ||||||
|  |                     SimklApi.addIdToString(this.syncData[simklIdPrefix], database, id.toString()) | ||||||
|  |                         ?: return@normalSafeApiCall | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         @JvmName("addActorsOnly") |         @JvmName("addActorsOnly") | ||||||
|         fun LoadResponse.addActors(actors: List<Actor>?) { |         fun LoadResponse.addActors(actors: List<Actor>?) { | ||||||
|             this.actors = actors?.map { actor -> ActorData(actor) } |             this.actors = actors?.map { actor -> ActorData(actor) } | ||||||
|  | @ -1179,10 +1198,16 @@ interface LoadResponse { | ||||||
| 
 | 
 | ||||||
|         fun LoadResponse.addMalId(id: Int?) { |         fun LoadResponse.addMalId(id: Int?) { | ||||||
|             this.syncData[malIdPrefix] = (id ?: return).toString() |             this.syncData[malIdPrefix] = (id ?: return).toString() | ||||||
|  |             this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString()) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         fun LoadResponse.addAniListId(id: Int?) { |         fun LoadResponse.addAniListId(id: Int?) { | ||||||
|             this.syncData[aniListIdPrefix] = (id ?: return).toString() |             this.syncData[aniListIdPrefix] = (id ?: return).toString() | ||||||
|  |             this.addSimklId(SimklApi.Companion.SyncServices.AniList, id.toString()) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         fun LoadResponse.addSimklId(id: Int?) { | ||||||
|  |             this.addSimklId(SimklApi.Companion.SyncServices.Simkl, id.toString()) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         fun LoadResponse.addImdbUrl(url: String?) { |         fun LoadResponse.addImdbUrl(url: String?) { | ||||||
|  | @ -1264,6 +1289,7 @@ interface LoadResponse { | ||||||
| 
 | 
 | ||||||
|         fun LoadResponse.addImdbId(id: String?) { |         fun LoadResponse.addImdbId(id: String?) { | ||||||
|             // TODO add imdb sync |             // TODO add imdb sync | ||||||
|  |             this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         fun LoadResponse.addTrackId(id: String?) { |         fun LoadResponse.addTrackId(id: String?) { | ||||||
|  | @ -1276,6 +1302,7 @@ interface LoadResponse { | ||||||
| 
 | 
 | ||||||
|         fun LoadResponse.addTMDbId(id: String?) { |         fun LoadResponse.addTMDbId(id: String?) { | ||||||
|             // TODO add TMDb sync |             // TODO add TMDb sync | ||||||
|  |             this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         fun LoadResponse.addRating(text: String?) { |         fun LoadResponse.addRating(text: String?) { | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { | ||||||
|         val malApi = MALApi(0) |         val malApi = MALApi(0) | ||||||
|         val aniListApi = AniListApi(0) |         val aniListApi = AniListApi(0) | ||||||
|         val openSubtitlesApi = OpenSubtitlesApi(0) |         val openSubtitlesApi = OpenSubtitlesApi(0) | ||||||
|  |         val simklApi = SimklApi(0) | ||||||
|         val indexSubtitlesApi = IndexSubtitleApi() |         val indexSubtitlesApi = IndexSubtitleApi() | ||||||
|         val addic7ed = Addic7ed() |         val addic7ed = Addic7ed() | ||||||
|         val localListApi = LocalList() |         val localListApi = LocalList() | ||||||
|  | @ -18,19 +19,19 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { | ||||||
|         // used to login via app intent |         // used to login via app intent | ||||||
|         val OAuth2Apis |         val OAuth2Apis | ||||||
|             get() = listOf<OAuth2API>( |             get() = listOf<OAuth2API>( | ||||||
|                 malApi, aniListApi |                 malApi, aniListApi, simklApi | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         // this needs init with context and can be accessed in settings |         // this needs init with context and can be accessed in settings | ||||||
|         val accountManagers |         val accountManagers | ||||||
|             get() = listOf( |             get() = listOf( | ||||||
|                 malApi, aniListApi, openSubtitlesApi, //nginxApi |                 malApi, aniListApi, openSubtitlesApi, simklApi //nginxApi | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         // used for active syncing |         // used for active syncing | ||||||
|         val SyncApis |         val SyncApis | ||||||
|             get() = listOf( |             get() = listOf( | ||||||
|                 SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi) |                 SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi), SyncRepo(simklApi) | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         val inAppAuths |         val inAppAuths | ||||||
|  |  | ||||||
|  | @ -10,7 +10,8 @@ enum class SyncIdName { | ||||||
|     MyAnimeList, |     MyAnimeList, | ||||||
|     Trakt, |     Trakt, | ||||||
|     Imdb, |     Imdb, | ||||||
|     LocalList |     Simkl, | ||||||
|  |     LocalList, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface SyncAPI : OAuth2API { | interface SyncAPI : OAuth2API { | ||||||
|  | @ -35,9 +36,9 @@ interface SyncAPI : OAuth2API { | ||||||
|     4 -> PlanToWatch |     4 -> PlanToWatch | ||||||
|     5 -> ReWatching |     5 -> ReWatching | ||||||
|      */ |      */ | ||||||
|     suspend fun score(id: String, status: SyncStatus): Boolean |     suspend fun score(id: String, status: AbstractSyncStatus): Boolean | ||||||
| 
 | 
 | ||||||
|     suspend fun getStatus(id: String): SyncStatus? |     suspend fun getStatus(id: String): AbstractSyncStatus? | ||||||
| 
 | 
 | ||||||
|     suspend fun getResult(id: String): SyncResult? |     suspend fun getResult(id: String): SyncResult? | ||||||
| 
 | 
 | ||||||
|  | @ -59,14 +60,24 @@ interface SyncAPI : OAuth2API { | ||||||
|         override var id: Int? = null, |         override var id: Int? = null, | ||||||
|     ) : SearchResponse |     ) : SearchResponse | ||||||
| 
 | 
 | ||||||
|     data class SyncStatus( |     abstract class AbstractSyncStatus { | ||||||
|         val status: Int, |         abstract var status: Int | ||||||
|  | 
 | ||||||
|         /** 1-10 */ |         /** 1-10 */ | ||||||
|         val score: Int?, |         abstract var score: Int? | ||||||
|         val watchedEpisodes: Int?, |         abstract var watchedEpisodes: Int? | ||||||
|         var isFavorite: Boolean? = null, |         abstract var isFavorite: Boolean? | ||||||
|         var maxEpisodes: Int? = null, |         abstract var maxEpisodes: Int? | ||||||
|     ) |     } | ||||||
|  | 
 | ||||||
|  |     data class SyncStatus( | ||||||
|  |         override var status: Int, | ||||||
|  |         /** 1-10 */ | ||||||
|  |         override var score: Int?, | ||||||
|  |         override var watchedEpisodes: Int?, | ||||||
|  |         override var isFavorite: Boolean? = null, | ||||||
|  |         override var maxEpisodes: Int? = null, | ||||||
|  |     ) : AbstractSyncStatus() | ||||||
| 
 | 
 | ||||||
|     data class SyncResult( |     data class SyncResult( | ||||||
|         /**Used to verify*/ |         /**Used to verify*/ | ||||||
|  |  | ||||||
|  | @ -18,11 +18,11 @@ class SyncRepo(private val repo: SyncAPI) { | ||||||
|             repo.requireLibraryRefresh = value |             repo.requireLibraryRefresh = value | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> { |     suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Resource<Boolean> { | ||||||
|         return safeApiCall { repo.score(id, status) } |         return safeApiCall { repo.score(id, status) } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suspend fun getStatus(id: String): Resource<SyncAPI.SyncStatus> { |     suspend fun getStatus(id: String): Resource<SyncAPI.AbstractSyncStatus> { | ||||||
|         return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") } |         return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -158,7 +158,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { |     override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { | ||||||
|         val internalId = id.toIntOrNull() ?: return null |         val internalId = id.toIntOrNull() ?: return null | ||||||
|         val data = getDataAboutId(internalId) ?: return null |         val data = getDataAboutId(internalId) ?: return null | ||||||
| 
 | 
 | ||||||
|  | @ -171,7 +171,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { |     override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { | ||||||
|         return postDataAboutId( |         return postDataAboutId( | ||||||
|             id.toIntOrNull() ?: return false, |             id.toIntOrNull() ?: return false, | ||||||
|             fromIntToAnimeStatus(status.status), |             fromIntToAnimeStatus(status.status), | ||||||
|  |  | ||||||
|  | @ -45,11 +45,11 @@ class LocalList : SyncAPI { | ||||||
| 
 | 
 | ||||||
|     override val mainUrl = "" |     override val mainUrl = "" | ||||||
|     override val syncIdName = SyncIdName.LocalList |     override val syncIdName = SyncIdName.LocalList | ||||||
|     override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { |     override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { |     override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { | ||||||
|         return null |         return null | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -91,7 +91,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|         return Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first() |         return Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { |     override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { | ||||||
|         return setScoreRequest( |         return setScoreRequest( | ||||||
|             id.toIntOrNull() ?: return false, |             id.toIntOrNull() ?: return false, | ||||||
|             fromIntToAnimeStatus(status.status), |             fromIntToAnimeStatus(status.status), | ||||||
|  |  | ||||||
|  | @ -0,0 +1,848 @@ | ||||||
|  | package com.lagradost.cloudstream3.syncproviders.providers | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.StringRes | ||||||
|  | import androidx.core.net.toUri | ||||||
|  | import androidx.fragment.app.FragmentActivity | ||||||
|  | import com.fasterxml.jackson.annotation.JsonInclude | ||||||
|  | import com.fasterxml.jackson.annotation.JsonProperty | ||||||
|  | import com.lagradost.cloudstream3.AcraApplication.Companion.getKey | ||||||
|  | import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser | ||||||
|  | import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey | ||||||
|  | import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||||
|  | import com.lagradost.cloudstream3.BuildConfig | ||||||
|  | import com.lagradost.cloudstream3.R | ||||||
|  | import com.lagradost.cloudstream3.TvType | ||||||
|  | import com.lagradost.cloudstream3.app | ||||||
|  | import com.lagradost.cloudstream3.mvvm.debugAssert | ||||||
|  | import com.lagradost.cloudstream3.mvvm.debugPrint | ||||||
|  | import com.lagradost.cloudstream3.mvvm.logError | ||||||
|  | import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AccountManager | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AuthAPI | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.SyncIdName | ||||||
|  | import com.lagradost.cloudstream3.ui.library.ListSorting | ||||||
|  | import com.lagradost.cloudstream3.ui.result.txt | ||||||
|  | import com.lagradost.cloudstream3.utils.AppUtils.toJson | ||||||
|  | import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson | ||||||
|  | import okhttp3.Interceptor | ||||||
|  | import okhttp3.Response | ||||||
|  | import java.math.BigInteger | ||||||
|  | import java.security.SecureRandom | ||||||
|  | import java.text.SimpleDateFormat | ||||||
|  | import java.time.Instant | ||||||
|  | import java.util.Date | ||||||
|  | import java.util.TimeZone | ||||||
|  | 
 | ||||||
|  | class SimklApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|  |     override var name = "Simkl" | ||||||
|  |     override val key = "simkl-key" | ||||||
|  |     override val redirectUrl = "simkl" | ||||||
|  |     override val idPrefix = "simkl" | ||||||
|  |     override var requireLibraryRefresh = true | ||||||
|  |     override var mainUrl = "https://api.simkl.com" | ||||||
|  |     override val icon = R.drawable.simkl_logo | ||||||
|  |     override val requiresLogin = false | ||||||
|  |     override val createAccountUrl = "$mainUrl/signup" | ||||||
|  |     override val syncIdName = SyncIdName.Simkl | ||||||
|  |     private val token: String? | ||||||
|  |         get() = getKey<String>(accountId, SIMKL_TOKEN_KEY).also { | ||||||
|  |             debugAssert({ it == null }) { "No ${this.name} token!" } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     /** 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. | ||||||
|  |      */ | ||||||
|  |     private var lastScoreTime = -1L | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         private const val clientId = BuildConfig.SIMKL_CLIENT_ID | ||||||
|  |         private const val clientSecret = BuildConfig.SIMKL_CLIENT_SECRET | ||||||
|  |         private var lastLoginState = "" | ||||||
|  | 
 | ||||||
|  |         const val SIMKL_TOKEN_KEY: String = "simkl_token" | ||||||
|  |         const val SIMKL_USER_KEY: String = "simkl_user" | ||||||
|  |         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 */ | ||||||
|  |         private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" | ||||||
|  |         fun getUnixTime(string: String?): Long? { | ||||||
|  |             return try { | ||||||
|  |                 SimpleDateFormat(simklDateFormat).apply { | ||||||
|  |                     this.timeZone = TimeZone.getTimeZone("UTC") | ||||||
|  |                 }.parse( | ||||||
|  |                     string ?: return null | ||||||
|  |                 )?.toInstant()?.epochSecond | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 logError(e) | ||||||
|  |                 return null | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** 1409562611 -> 2014-09-01T09:10:11Z */ | ||||||
|  |         fun getDateTime(unixTime: Long?): String? { | ||||||
|  |             return try { | ||||||
|  |                 SimpleDateFormat(simklDateFormat).apply { | ||||||
|  |                     this.timeZone = TimeZone.getTimeZone("UTC") | ||||||
|  |                 }.format( | ||||||
|  |                     Date.from( | ||||||
|  |                         Instant.ofEpochSecond( | ||||||
|  |                             unixTime ?: return null | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 null | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Set of sync services simkl is compatible with. | ||||||
|  |          * Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id | ||||||
|  |          */ | ||||||
|  |         enum class SyncServices(val originalName: String) { | ||||||
|  |             Simkl("simkl"), | ||||||
|  |             Imdb("imdb"), | ||||||
|  |             Tmdb("tmdb"), | ||||||
|  |             AniList("anilist"), | ||||||
|  |             Mal("mal"), | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * The ID string is a way to keep a collection of services in one single ID using a map | ||||||
|  |          * This adds a database service (like imdb) to the string and returns the new string. | ||||||
|  |          */ | ||||||
|  |         fun addIdToString(idString: String?, database: SyncServices, id: String?): String? { | ||||||
|  |             if (id == null) return idString | ||||||
|  |             return (readIdFromString(idString) + mapOf(database to id)).toJson() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** Read the id string to get all other ids */ | ||||||
|  |         private fun readIdFromString(idString: String?): Map<SyncServices, String> { | ||||||
|  |             return tryParseJson(idString) ?: return emptyMap() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         fun getPosterUrl(poster: String): String { | ||||||
|  |             return "https://wsrv.nl/?url=https://simkl.in/posters/${poster}_m.webp" | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private fun getUrlFromId(id: Int): String { | ||||||
|  |             return "https://simkl.com/shows/$id" | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         enum class SimklListStatusType( | ||||||
|  |             var value: Int, | ||||||
|  |             @StringRes val stringRes: Int, | ||||||
|  |             val originalName: String? | ||||||
|  |         ) { | ||||||
|  |             Watching(0, R.string.type_watching, "watching"), | ||||||
|  |             Completed(1, R.string.type_completed, "completed"), | ||||||
|  |             Paused(2, R.string.type_on_hold, "hold"), | ||||||
|  |             Dropped(3, R.string.type_dropped, "dropped"), | ||||||
|  |             Planning(4, R.string.type_plan_to_watch, "plantowatch"), | ||||||
|  |             ReWatching(5, R.string.type_re_watching, "watching"), | ||||||
|  |             None(-1, R.string.none, null); | ||||||
|  | 
 | ||||||
|  |             companion object { | ||||||
|  |                 fun fromString(string: String): SimklListStatusType? { | ||||||
|  |                     return SimklListStatusType.values().firstOrNull { | ||||||
|  |                         it.originalName == string | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // ------------------- | ||||||
|  |         @JsonInclude(JsonInclude.Include.NON_EMPTY) | ||||||
|  |         data class TokenRequest( | ||||||
|  |             @JsonProperty("code") val code: String, | ||||||
|  |             @JsonProperty("client_id") val client_id: String = clientId, | ||||||
|  |             @JsonProperty("client_secret") val client_secret: String = clientSecret, | ||||||
|  |             @JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl", | ||||||
|  |             @JsonProperty("grant_type") val grant_type: String = "authorization_code" | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         data class TokenResponse( | ||||||
|  |             /** No expiration date */ | ||||||
|  |             val access_token: String, | ||||||
|  |             val token_type: String, | ||||||
|  |             val scope: String | ||||||
|  |         ) | ||||||
|  |         // ------------------- | ||||||
|  | 
 | ||||||
|  |         /** https://simkl.docs.apiary.io/#reference/users/settings/receive-settings */ | ||||||
|  |         data class SettingsResponse( | ||||||
|  |             val user: User | ||||||
|  |         ) { | ||||||
|  |             data class User( | ||||||
|  |                 val name: String, | ||||||
|  |                 /** Url */ | ||||||
|  |                 val avatar: String | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // ------------------- | ||||||
|  |         data class ActivitiesResponse( | ||||||
|  |             val all: String?, | ||||||
|  |             val tv_shows: UpdatedAt, | ||||||
|  |             val anime: UpdatedAt, | ||||||
|  |             val movies: UpdatedAt, | ||||||
|  |         ) { | ||||||
|  |             data class UpdatedAt( | ||||||
|  |                 val all: String?, | ||||||
|  |                 val removed_from_list: String?, | ||||||
|  |                 val rated_at: String?, | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** https://simkl.docs.apiary.io/#reference/tv/episodes/get-tv-show-episodes */ | ||||||
|  |         @JsonInclude(JsonInclude.Include.NON_EMPTY) | ||||||
|  |         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? | ||||||
|  |         ) { | ||||||
|  |             companion object { | ||||||
|  |                 fun convertToEpisodes(list: List<EpisodeMetadata>?): List<MediaObject.Season.Episode> { | ||||||
|  |                     return list?.map { | ||||||
|  |                         MediaObject.Season.Episode(it.episode) | ||||||
|  |                     } ?: emptyList() | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 fun convertToSeasons(list: List<EpisodeMetadata>?): List<MediaObject.Season> { | ||||||
|  |                     return list?.filter { it.season != null }?.groupBy { | ||||||
|  |                         it.season | ||||||
|  |                     }?.map { (season, episodes) -> | ||||||
|  |                         MediaObject.Season(season!!, convertToEpisodes(episodes)) | ||||||
|  |                     } ?: emptyList() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * https://simkl.docs.apiary.io/#introduction/about-simkl-api/standard-media-objects | ||||||
|  |          * 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("poster") val poster: String? = null, | ||||||
|  |             @JsonProperty("type") val type: String? = null, | ||||||
|  |             @JsonProperty("seasons") val seasons: List<Season>? = null, | ||||||
|  |             @JsonProperty("episodes") val episodes: List<Season.Episode>? = null | ||||||
|  |         ) { | ||||||
|  |             @JsonInclude(JsonInclude.Include.NON_EMPTY) | ||||||
|  |             data class Season( | ||||||
|  |                 @JsonProperty("number") val number: Int, | ||||||
|  |                 @JsonProperty("episodes") val episodes: List<Episode> | ||||||
|  |             ) { | ||||||
|  |                 data class Episode(@JsonProperty("number") val number: Int) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @JsonInclude(JsonInclude.Include.NON_EMPTY) | ||||||
|  |             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, | ||||||
|  |             ) { | ||||||
|  |                 companion object { | ||||||
|  |                     fun fromMap(map: Map<SyncServices, String>): Ids { | ||||||
|  |                         return Ids( | ||||||
|  |                             simkl = map[SyncServices.Simkl]?.toIntOrNull(), | ||||||
|  |                             imdb = map[SyncServices.Imdb], | ||||||
|  |                             tmdb = map[SyncServices.Tmdb], | ||||||
|  |                             mal = map[SyncServices.Mal], | ||||||
|  |                             anilist = map[SyncServices.AniList] | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             fun toSyncSearchResult(): SyncAPI.SyncSearchResult? { | ||||||
|  |                 return SyncAPI.SyncSearchResult( | ||||||
|  |                     this.title ?: return null, | ||||||
|  |                     "Simkl", | ||||||
|  |                     this.ids?.simkl?.toString() ?: return null, | ||||||
|  |                     getUrlFromId(this.ids.simkl), | ||||||
|  |                     this.poster?.let { getPosterUrl(it) }, | ||||||
|  |                     if (this.type == "movie") TvType.Movie else TvType.TvSeries | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @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 rated_at: String? = getDateTime(unixTime) | ||||||
|  |         ) : MediaObject(title, year, ids) | ||||||
|  | 
 | ||||||
|  |         @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 watched_at: String? = getDateTime(unixTime) | ||||||
|  |         ) : MediaObject(title, year, ids) | ||||||
|  | 
 | ||||||
|  |         @JsonInclude(JsonInclude.Include.NON_EMPTY) | ||||||
|  |         class HistoryMediaObject( | ||||||
|  |             @JsonProperty("title") title: String?, | ||||||
|  |             @JsonProperty("year") year: Int?, | ||||||
|  |             @JsonProperty("ids") ids: Ids?, | ||||||
|  |             @JsonProperty("seasons") seasons: List<Season>?, | ||||||
|  |             @JsonProperty("episodes") episodes: List<Season.Episode>?, | ||||||
|  |         ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) | ||||||
|  | 
 | ||||||
|  |         @JsonInclude(JsonInclude.Include.NON_EMPTY) | ||||||
|  |         data class StatusRequest( | ||||||
|  |             @JsonProperty("movies") val movies: List<MediaObject>, | ||||||
|  |             @JsonProperty("shows") val shows: List<MediaObject> | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         /** https://simkl.docs.apiary.io/#reference/sync/get-all-items/get-all-items-in-the-user's-watchlist */ | ||||||
|  |         data class AllItemsResponse( | ||||||
|  |             val shows: List<ShowMetadata>, | ||||||
|  |             val anime: List<ShowMetadata>, | ||||||
|  |             val movies: List<MovieMetadata>, | ||||||
|  |         ) { | ||||||
|  |             companion object { | ||||||
|  |                 fun merge(first: AllItemsResponse?, second: AllItemsResponse?): AllItemsResponse { | ||||||
|  | 
 | ||||||
|  |                     // Replace the first item with the same id, or add the new item | ||||||
|  |                     fun <T> MutableList<T>.replaceOrAddItem(newItem: T, predicate: (T) -> Boolean) { | ||||||
|  |                         for (i in this.indices) { | ||||||
|  |                             if (predicate(this[i])) { | ||||||
|  |                                 this[i] = newItem | ||||||
|  |                                 return | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         this.add(newItem) | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     // | ||||||
|  |                     fun <T : Metadata> merge( | ||||||
|  |                         first: List<T>?, | ||||||
|  |                         second: List<T>? | ||||||
|  |                     ): List<T> { | ||||||
|  |                         return (first?.toMutableList() ?: mutableListOf()).apply { | ||||||
|  |                             second?.forEach { secondShow -> | ||||||
|  |                                 this.replaceOrAddItem(secondShow) { | ||||||
|  |                                     it.getIds().simkl == secondShow.getIds().simkl | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     return AllItemsResponse( | ||||||
|  |                         merge(first?.shows, second?.shows), | ||||||
|  |                         merge(first?.anime, second?.anime), | ||||||
|  |                         merge(first?.movies, second?.movies), | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             interface Metadata { | ||||||
|  |                 val last_watched_at: String? | ||||||
|  |                 val status: String? | ||||||
|  |                 val user_rating: Int? | ||||||
|  |                 val last_watched: String? | ||||||
|  |                 val watched_episodes_count: Int? | ||||||
|  |                 val total_episodes_count: Int? | ||||||
|  | 
 | ||||||
|  |                 fun getIds(): ShowMetadata.Show.Ids | ||||||
|  |                 fun toLibraryItem(): SyncAPI.LibraryItem | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             data class MovieMetadata( | ||||||
|  |                 override val last_watched_at: String?, | ||||||
|  |                 override val status: String, | ||||||
|  |                 override val user_rating: Int?, | ||||||
|  |                 override val last_watched: String?, | ||||||
|  |                 override val watched_episodes_count: Int?, | ||||||
|  |                 override val total_episodes_count: Int?, | ||||||
|  |                 val movie: ShowMetadata.Show | ||||||
|  |             ) : Metadata { | ||||||
|  |                 override fun getIds(): ShowMetadata.Show.Ids { | ||||||
|  |                     return this.movie.ids | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 override fun toLibraryItem(): SyncAPI.LibraryItem { | ||||||
|  |                     return SyncAPI.LibraryItem( | ||||||
|  |                         this.movie.title, | ||||||
|  |                         "https://simkl.com/tv/${movie.ids.simkl}", | ||||||
|  |                         movie.ids.simkl.toString(), | ||||||
|  |                         this.watched_episodes_count, | ||||||
|  |                         this.total_episodes_count, | ||||||
|  |                         this.user_rating?.times(10), | ||||||
|  |                         getUnixTime(last_watched_at) ?: 0, | ||||||
|  |                         "Simkl", | ||||||
|  |                         TvType.Movie, | ||||||
|  |                         this.movie.poster?.let { getPosterUrl(it) }, | ||||||
|  |                         null, | ||||||
|  |                         null, | ||||||
|  |                         movie.ids.simkl | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             data class ShowMetadata( | ||||||
|  |                 override val last_watched_at: String?, | ||||||
|  |                 override val status: String, | ||||||
|  |                 override val user_rating: Int?, | ||||||
|  |                 override val last_watched: String?, | ||||||
|  |                 override val watched_episodes_count: Int?, | ||||||
|  |                 override val total_episodes_count: Int?, | ||||||
|  |                 val show: Show | ||||||
|  |             ) : Metadata { | ||||||
|  |                 override fun getIds(): Show.Ids { | ||||||
|  |                     return this.show.ids | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 override fun toLibraryItem(): SyncAPI.LibraryItem { | ||||||
|  |                     return SyncAPI.LibraryItem( | ||||||
|  |                         this.show.title, | ||||||
|  |                         "https://simkl.com/tv/${show.ids.simkl}", | ||||||
|  |                         show.ids.simkl.toString(), | ||||||
|  |                         this.watched_episodes_count, | ||||||
|  |                         this.total_episodes_count, | ||||||
|  |                         this.user_rating?.times(10), | ||||||
|  |                         getUnixTime(last_watched_at) ?: 0, | ||||||
|  |                         "Simkl", | ||||||
|  |                         TvType.Anime, | ||||||
|  |                         this.show.poster?.let { getPosterUrl(it) }, | ||||||
|  |                         null, | ||||||
|  |                         null, | ||||||
|  |                         show.ids.simkl | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 data class Show( | ||||||
|  |                     val title: String, | ||||||
|  |                     val poster: String?, | ||||||
|  |                     val year: Int?, | ||||||
|  |                     val ids: Ids, | ||||||
|  |                 ) { | ||||||
|  |                     data class Ids( | ||||||
|  |                         val simkl: Int, | ||||||
|  |                         val slug: String?, | ||||||
|  |                         val imdb: String?, | ||||||
|  |                         val zap2it: String?, | ||||||
|  |                         val tmdb: String?, | ||||||
|  |                         val offen: String?, | ||||||
|  |                         val tvdb: String?, | ||||||
|  |                         val mal: String?, | ||||||
|  |                         val anidb: String?, | ||||||
|  |                         val anilist: String?, | ||||||
|  |                         val traktslug: String? | ||||||
|  |                     ) { | ||||||
|  |                         fun matchesId(database: SyncServices, id: String): Boolean { | ||||||
|  |                             return when (database) { | ||||||
|  |                                 SyncServices.Simkl -> this.simkl == id.toIntOrNull() | ||||||
|  |                                 SyncServices.AniList -> this.anilist == id | ||||||
|  |                                 SyncServices.Mal -> this.mal == id | ||||||
|  |                                 SyncServices.Tmdb -> this.tmdb == id | ||||||
|  |                                 SyncServices.Imdb -> this.imdb == id | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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", clientId) | ||||||
|  |                     .build() | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private suspend fun getUser(): SettingsResponse.User? { | ||||||
|  |         return suspendSafeApiCall { | ||||||
|  |             app.post("$mainUrl/users/settings", interceptor = interceptor) | ||||||
|  |                 .parsedSafe<SettingsResponse>()?.user | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     class SimklSyncStatus( | ||||||
|  |         override var status: Int, | ||||||
|  |         override var score: Int?, | ||||||
|  |         override var watchedEpisodes: Int?, | ||||||
|  |         val episodes: Array<EpisodeMetadata>?, | ||||||
|  |         override var isFavorite: Boolean? = null, | ||||||
|  |         override var maxEpisodes: Int? = null, | ||||||
|  |     ) : SyncAPI.AbstractSyncStatus() | ||||||
|  | 
 | ||||||
|  |     override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { | ||||||
|  |         val realIds = readIdFromString(id) | ||||||
|  |         val foundItem = getSyncListSmart()?.let { list -> | ||||||
|  |             listOf(list.shows, list.anime, list.movies).flatten().firstOrNull { show -> | ||||||
|  |                 realIds.any { (database, id) -> | ||||||
|  |                     show.getIds().matchesId(database, id) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Search to get episodes | ||||||
|  |         val searchResult = searchByIds(realIds)?.firstOrNull() | ||||||
|  |         val episodes = getEpisodes(searchResult?.ids?.simkl, searchResult?.type) | ||||||
|  | 
 | ||||||
|  |         if (foundItem != null) { | ||||||
|  |             return SimklSyncStatus( | ||||||
|  |                 status = foundItem.status?.let { SimklListStatusType.fromString(it)?.value } | ||||||
|  |                     ?: return null, | ||||||
|  |                 score = foundItem.user_rating, | ||||||
|  |                 watchedEpisodes = foundItem.watched_episodes_count, | ||||||
|  |                 maxEpisodes = foundItem.total_episodes_count, | ||||||
|  |                 episodes = episodes | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             return if (searchResult != null) { | ||||||
|  |                 SimklSyncStatus( | ||||||
|  |                     status = SimklListStatusType.None.value, | ||||||
|  |                     score = 0, | ||||||
|  |                     watchedEpisodes = 0, | ||||||
|  |                     maxEpisodes = if (searchResult.type == "movie") 0 else null, | ||||||
|  |                     episodes = episodes | ||||||
|  |                 ) | ||||||
|  |             } else { | ||||||
|  |                 null | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { | ||||||
|  |         val parsedId = readIdFromString(id) | ||||||
|  |         lastScoreTime = unixTime | ||||||
|  | 
 | ||||||
|  |         if (status.status == SimklListStatusType.None.value) { | ||||||
|  |             return app.post( | ||||||
|  |                 "$mainUrl/sync/history/remove", | ||||||
|  |                 json = StatusRequest( | ||||||
|  |                     shows = listOf( | ||||||
|  |                         HistoryMediaObject( | ||||||
|  |                             null, | ||||||
|  |                             null, | ||||||
|  |                             MediaObject.Ids.fromMap(parsedId), | ||||||
|  |                             emptyList(), | ||||||
|  |                             emptyList() | ||||||
|  |                         ) | ||||||
|  |                     ), | ||||||
|  |                     movies = emptyList() | ||||||
|  |                 ), | ||||||
|  |                 interceptor = interceptor | ||||||
|  |             ).isSuccessful | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val realScore = status.score | ||||||
|  |         val ratingResponseSuccess = if (realScore != null) { | ||||||
|  |             // Remove rating if score is 0 | ||||||
|  |             val ratingsSuffix = if (realScore == 0) "/remove" else "" | ||||||
|  |             debugPrint { "Rate ${this.name} item: rating=$realScore" } | ||||||
|  |             app.post( | ||||||
|  |                 "$mainUrl/sync/ratings$ratingsSuffix", | ||||||
|  |                 json = StatusRequest( | ||||||
|  |                     // Not possible to know if TV or Movie | ||||||
|  |                     shows = listOf( | ||||||
|  |                         RatingMediaObject( | ||||||
|  |                             null, | ||||||
|  |                             null, | ||||||
|  |                             MediaObject.Ids.fromMap(parsedId), | ||||||
|  |                             realScore | ||||||
|  |                         ) | ||||||
|  |                     ), | ||||||
|  |                     movies = emptyList() | ||||||
|  |                 ), | ||||||
|  |                 interceptor = interceptor | ||||||
|  |             ).isSuccessful | ||||||
|  |         } else { | ||||||
|  |             true | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val simklStatus = status as? SimklSyncStatus | ||||||
|  |         // All episodes if marked as completed | ||||||
|  |         val watchedEpisodes = if (status.status == SimklListStatusType.Completed.value) { | ||||||
|  |             simklStatus?.episodes?.size | ||||||
|  |         } else { | ||||||
|  |             status.watchedEpisodes | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Only post episodes if available episodes and the status is correct | ||||||
|  |         val episodeResponseSuccess = | ||||||
|  |             if (simklStatus != null && watchedEpisodes != null && !simklStatus.episodes.isNullOrEmpty() && listOf( | ||||||
|  |                     SimklListStatusType.Paused.value, | ||||||
|  |                     SimklListStatusType.Dropped.value, | ||||||
|  |                     SimklListStatusType.Watching.value, | ||||||
|  |                     SimklListStatusType.Completed.value, | ||||||
|  |                     SimklListStatusType.ReWatching.value | ||||||
|  |                 ).contains(status.status) | ||||||
|  |             ) { | ||||||
|  |                 val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) | ||||||
|  | 
 | ||||||
|  |                 val (seasons, episodes) = if (cutEpisodes.any { it.season != null }) { | ||||||
|  |                     EpisodeMetadata.convertToSeasons(cutEpisodes) to null | ||||||
|  |                 } else { | ||||||
|  |                     null to EpisodeMetadata.convertToEpisodes(cutEpisodes) | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 debugPrint { "Synced history for ${status.watchedEpisodes} given size of ${simklStatus.episodes.size}: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } | ||||||
|  |                 val episodeResponse = app.post( | ||||||
|  |                     "$mainUrl/sync/history", | ||||||
|  |                     json = StatusRequest( | ||||||
|  |                         shows = listOf( | ||||||
|  |                             HistoryMediaObject( | ||||||
|  |                                 null, | ||||||
|  |                                 null, | ||||||
|  |                                 MediaObject.Ids.fromMap(parsedId), | ||||||
|  |                                 seasons, | ||||||
|  |                                 episodes | ||||||
|  |                             ) | ||||||
|  |                         ), | ||||||
|  |                         movies = emptyList() | ||||||
|  |                     ), | ||||||
|  |                     interceptor = interceptor | ||||||
|  |                 ) | ||||||
|  |                 episodeResponse.isSuccessful | ||||||
|  |             } else true | ||||||
|  | 
 | ||||||
|  |         val newStatus = | ||||||
|  |             SimklListStatusType.values().firstOrNull { it.value == status.status }?.originalName | ||||||
|  |                 ?: SimklListStatusType.Watching.originalName | ||||||
|  | 
 | ||||||
|  |         val statusResponseSuccess = if (newStatus != null) { | ||||||
|  |             debugPrint { "Add to ${this.name} list: status=$newStatus" } | ||||||
|  |             app.post( | ||||||
|  |                 "$mainUrl/sync/add-to-list", | ||||||
|  |                 json = StatusRequest( | ||||||
|  |                     shows = listOf( | ||||||
|  |                         StatusMediaObject( | ||||||
|  |                             null, | ||||||
|  |                             null, | ||||||
|  |                             MediaObject.Ids.fromMap(parsedId), | ||||||
|  |                             newStatus | ||||||
|  |                         ) | ||||||
|  |                     ), | ||||||
|  |                     movies = emptyList() | ||||||
|  |                 ), | ||||||
|  |                 interceptor = interceptor | ||||||
|  |             ).isSuccessful | ||||||
|  |         } else { | ||||||
|  |             true | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         debugPrint { "All scoring complete: rating=$ratingResponseSuccess, status=$statusResponseSuccess, episode=$episodeResponseSuccess" } | ||||||
|  |         requireLibraryRefresh = true | ||||||
|  |         return ratingResponseSuccess && statusResponseSuccess && episodeResponseSuccess | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ | ||||||
|  |     suspend fun searchByIds(serviceMap: Map<SyncServices, String>): Array<MediaObject>? { | ||||||
|  |         if (serviceMap.isEmpty()) return emptyArray() | ||||||
|  | 
 | ||||||
|  |         return app.get( | ||||||
|  |             "$mainUrl/search/id", | ||||||
|  |             params = mapOf("client_id" to clientId) + serviceMap.map { (service, id) -> | ||||||
|  |                 service.originalName to id | ||||||
|  |             } | ||||||
|  |         ).parsedSafe() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     suspend fun getEpisodes(simklId: Int?, type: String?): Array<EpisodeMetadata>? { | ||||||
|  |         if (simklId == null) return null | ||||||
|  |         val url = when (type) { | ||||||
|  |             "anime" -> "https://api.simkl.com/anime/episodes/$simklId" | ||||||
|  |             "tv" -> "https://api.simkl.com/tv/episodes/$simklId" | ||||||
|  |             "movie" -> return null | ||||||
|  |             else -> return null | ||||||
|  |         } | ||||||
|  |         return app.get(url, params = mapOf("client_id" to clientId)).parsedSafe() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? { | ||||||
|  |         return app.get( | ||||||
|  |             "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) | ||||||
|  |         ).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun authenticate(activity: FragmentActivity?) { | ||||||
|  |         lastLoginState = BigInteger(130, SecureRandom()).toString(32) | ||||||
|  |         val url = | ||||||
|  |             "https://simkl.com/oauth/authorize?response_type=code&client_id=$clientId&redirect_uri=$appString://${redirectUrl}&state=$lastLoginState" | ||||||
|  |         openBrowser(url, activity) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun loginInfo(): AuthAPI.LoginInfo? { | ||||||
|  |         return getKey<SettingsResponse.User>(accountId, SIMKL_USER_KEY)?.let { user -> | ||||||
|  |             AuthAPI.LoginInfo( | ||||||
|  |                 name = user.name, | ||||||
|  |                 profilePicture = user.avatar, | ||||||
|  |                 accountIndex = accountIndex | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun logOut() { | ||||||
|  |         requireLibraryRefresh = true | ||||||
|  |         removeAccountKeys() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override suspend fun getResult(id: String): SyncAPI.SyncResult? { | ||||||
|  |         return null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private suspend fun getSyncListSince(since: Long?): AllItemsResponse { | ||||||
|  |         val params = getDateTime(since)?.let { | ||||||
|  |             mapOf("date_from" to it) | ||||||
|  |         } ?: emptyMap() | ||||||
|  | 
 | ||||||
|  |         return app.get( | ||||||
|  |             "$mainUrl/sync/all-items/", | ||||||
|  |             params = params, | ||||||
|  |             interceptor = interceptor | ||||||
|  |         ).parsed() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private suspend fun getActivities(): ActivitiesResponse? { | ||||||
|  |         return app.post("$mainUrl/sync/activities", interceptor = interceptor).parsedSafe() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun getSyncListCached(): AllItemsResponse? { | ||||||
|  |         return getKey(accountId, SIMKL_CACHED_LIST) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private suspend fun getSyncListSmart(): AllItemsResponse? { | ||||||
|  |         if (token == null) return null | ||||||
|  | 
 | ||||||
|  |         val activities = getActivities() | ||||||
|  |         val lastCacheUpdate = getKey<Long>(accountId, SIMKL_CACHED_LIST_TIME) | ||||||
|  |         val lastRemoval = listOf( | ||||||
|  |             activities?.tv_shows?.removed_from_list, | ||||||
|  |             activities?.anime?.removed_from_list, | ||||||
|  |             activities?.movies?.removed_from_list | ||||||
|  |         ).maxOf { | ||||||
|  |             getUnixTime(it) ?: -1 | ||||||
|  |         } | ||||||
|  |         val lastRealUpdate = | ||||||
|  |             listOf( | ||||||
|  |                 activities?.tv_shows?.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) { | ||||||
|  |             debugPrint { "Full list update in ${this.name}." } | ||||||
|  |             setKey(accountId, SIMKL_CACHED_LIST_TIME, lastRemoval) | ||||||
|  |             getSyncListSince(null) | ||||||
|  |         } else if (lastCacheUpdate < lastRealUpdate || lastCacheUpdate < lastScoreTime) { | ||||||
|  |             debugPrint { "Partial list update in ${this.name}." } | ||||||
|  |             setKey(accountId, SIMKL_CACHED_LIST_TIME, lastCacheUpdate) | ||||||
|  |             AllItemsResponse.merge(getSyncListCached(), getSyncListSince(lastCacheUpdate)) | ||||||
|  |         } else { | ||||||
|  |             debugPrint { "Cached list update in ${this.name}." } | ||||||
|  |             getSyncListCached() | ||||||
|  |         } | ||||||
|  |         debugPrint { "List sizes: movies=${list?.movies?.size}, shows=${list?.shows?.size}, anime=${list?.anime?.size}" } | ||||||
|  | 
 | ||||||
|  |         setKey(accountId, SIMKL_CACHED_LIST, list) | ||||||
|  | 
 | ||||||
|  |         return list | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? { | ||||||
|  |         val list = getSyncListSmart() ?: return null | ||||||
|  | 
 | ||||||
|  |         val baseMap = | ||||||
|  |             SimklListStatusType.values() | ||||||
|  |                 .filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value } | ||||||
|  |                 .associate { | ||||||
|  |                     it.stringRes to emptyList<SyncAPI.LibraryItem>() | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |         val syncMap = listOf(list.anime, list.movies, list.shows) | ||||||
|  |             .flatten() | ||||||
|  |             .groupBy { | ||||||
|  |                 it.status | ||||||
|  |             } | ||||||
|  |             .mapNotNull { (status, list) -> | ||||||
|  |                 val stringRes = | ||||||
|  |                     status?.let { SimklListStatusType.fromString(it)?.stringRes } | ||||||
|  |                         ?: return@mapNotNull null | ||||||
|  |                 val libraryList = list.map { it.toLibraryItem() } | ||||||
|  |                 stringRes to libraryList | ||||||
|  |             }.toMap() | ||||||
|  | 
 | ||||||
|  |         return SyncAPI.LibraryMetadata( | ||||||
|  |             (baseMap + syncMap).map { SyncAPI.LibraryList(txt(it.key), it.value) }, setOf( | ||||||
|  |                 ListSorting.AlphabeticalA, | ||||||
|  |                 ListSorting.AlphabeticalZ, | ||||||
|  |                 ListSorting.UpdatedNew, | ||||||
|  |                 ListSorting.UpdatedOld, | ||||||
|  |                 ListSorting.RatingHigh, | ||||||
|  |                 ListSorting.RatingLow, | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getIdFromUrl(url: String): String { | ||||||
|  |         val simklUrlRegex = Regex("""https://simkl\.com/[^/]*/(\d+).*""") | ||||||
|  |         return simklUrlRegex.find(url)?.groupValues?.get(1) ?: "" | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override suspend fun handleRedirect(url: String): Boolean { | ||||||
|  |         val uri = url.toUri() | ||||||
|  |         val state = uri.getQueryParameter("state") | ||||||
|  |         // Ensure consistent state | ||||||
|  |         if (state != lastLoginState) return false | ||||||
|  |         lastLoginState = "" | ||||||
|  | 
 | ||||||
|  |         val code = uri.getQueryParameter("code") ?: return false | ||||||
|  |         val token = app.post( | ||||||
|  |             "$mainUrl/oauth/token", json = TokenRequest(code) | ||||||
|  |         ).parsedSafe<TokenResponse>() ?: return false | ||||||
|  | 
 | ||||||
|  |         switchToNewAccount() | ||||||
|  |         setKey(accountId, SIMKL_TOKEN_KEY, token.access_token) | ||||||
|  | 
 | ||||||
|  |         val user = getUser() | ||||||
|  |         if (user == null) { | ||||||
|  |             removeKey(accountId, SIMKL_TOKEN_KEY) | ||||||
|  |             switchToOldAccount() | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         setKey(accountId, SIMKL_USER_KEY, user) | ||||||
|  |         registerAccount() | ||||||
|  |         requireLibraryRefresh = true | ||||||
|  | 
 | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -36,18 +36,18 @@ class SyncViewModel : ViewModel() { | ||||||
| 
 | 
 | ||||||
|     val metadata: LiveData<Resource<SyncAPI.SyncResult>> get() = _metaResponse |     val metadata: LiveData<Resource<SyncAPI.SyncResult>> get() = _metaResponse | ||||||
| 
 | 
 | ||||||
|     private val _userDataResponse: MutableLiveData<Resource<SyncAPI.SyncStatus>?> = |     private val _userDataResponse: MutableLiveData<Resource<SyncAPI.AbstractSyncStatus>?> = | ||||||
|         MutableLiveData(null) |         MutableLiveData(null) | ||||||
| 
 | 
 | ||||||
|     val userData: LiveData<Resource<SyncAPI.SyncStatus>?> get() = _userDataResponse |     val userData: LiveData<Resource<SyncAPI.AbstractSyncStatus>?> get() = _userDataResponse | ||||||
| 
 | 
 | ||||||
|     // prefix, id |     // prefix, id | ||||||
|     private var syncs = mutableMapOf<String, String>() |     private val syncs = mutableMapOf<String, String>() | ||||||
|     //private val _syncIds: MutableLiveData<MutableMap<String, String>> = |     //private val _syncIds: MutableLiveData<MutableMap<String, String>> = | ||||||
|     //    MutableLiveData(mutableMapOf()) |     //    MutableLiveData(mutableMapOf()) | ||||||
|     //val syncIds: LiveData<MutableMap<String, String>> get() = _syncIds |     //val syncIds: LiveData<MutableMap<String, String>> get() = _syncIds | ||||||
| 
 | 
 | ||||||
|     fun getSyncs() : Map<String,String> { |     fun getSyncs(): Map<String, String> { | ||||||
|         return syncs |         return syncs | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -106,7 +106,7 @@ class SyncViewModel : ViewModel() { | ||||||
|         Log.i(TAG, "addFromUrl = $url") |         Log.i(TAG, "addFromUrl = $url") | ||||||
| 
 | 
 | ||||||
|         if (url == null || hasAddedFromUrl.contains(url)) return@ioSafe |         if (url == null || hasAddedFromUrl.contains(url)) return@ioSafe | ||||||
|         if(!url.startsWith("http")) return@ioSafe |         if (!url.startsWith("http")) return@ioSafe | ||||||
| 
 | 
 | ||||||
|         SyncUtil.getIdsFromUrl(url)?.let { (malId, aniListId) -> |         SyncUtil.getIdsFromUrl(url)?.let { (malId, aniListId) -> | ||||||
|             hasAddedFromUrl.add(url) |             hasAddedFromUrl.add(url) | ||||||
|  | @ -150,7 +150,8 @@ class SyncViewModel : ViewModel() { | ||||||
| 
 | 
 | ||||||
|         val user = userData.value |         val user = userData.value | ||||||
|         if (user is Resource.Success) { |         if (user is Resource.Success) { | ||||||
|             _userDataResponse.postValue(Resource.Success(user.value.copy(watchedEpisodes = episodes))) |             user.value.watchedEpisodes = episodes | ||||||
|  |             _userDataResponse.postValue(Resource.Success(user.value)) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -158,7 +159,8 @@ class SyncViewModel : ViewModel() { | ||||||
|         Log.i(TAG, "setScore = $score") |         Log.i(TAG, "setScore = $score") | ||||||
|         val user = userData.value |         val user = userData.value | ||||||
|         if (user is Resource.Success) { |         if (user is Resource.Success) { | ||||||
|             _userDataResponse.postValue(Resource.Success(user.value.copy(score = score))) |             user.value.score = score | ||||||
|  |             _userDataResponse.postValue(Resource.Success(user.value)) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -167,7 +169,8 @@ class SyncViewModel : ViewModel() { | ||||||
|         if (which < -1 || which > 5) return // validate input |         if (which < -1 || which > 5) return // validate input | ||||||
|         val user = userData.value |         val user = userData.value | ||||||
|         if (user is Resource.Success) { |         if (user is Resource.Success) { | ||||||
|             _userDataResponse.postValue(Resource.Success(user.value.copy(status = which))) |             user.value.status = which | ||||||
|  |             _userDataResponse.postValue(Resource.Success(user.value)) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -185,17 +188,16 @@ class SyncViewModel : ViewModel() { | ||||||
|     fun modifyMaxEpisode(episodeNum: Int) { |     fun modifyMaxEpisode(episodeNum: Int) { | ||||||
|         Log.i(TAG, "modifyMaxEpisode = $episodeNum") |         Log.i(TAG, "modifyMaxEpisode = $episodeNum") | ||||||
|         modifyData { status -> |         modifyData { status -> | ||||||
|             status.copy( |             status.watchedEpisodes = maxOf( | ||||||
|                 watchedEpisodes = maxOf( |                 episodeNum, | ||||||
|                     episodeNum, |                 status.watchedEpisodes ?: return@modifyData null | ||||||
|                     status.watchedEpisodes ?: return@modifyData null |  | ||||||
|                 ) |  | ||||||
|             ) |             ) | ||||||
|  |             status | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// modifies the current sync data, return null if you don't want to change it |     /// modifies the current sync data, return null if you don't want to change it | ||||||
|     private fun modifyData(update: ((SyncAPI.SyncStatus) -> (SyncAPI.SyncStatus?))) = |     private fun modifyData(update: ((SyncAPI.AbstractSyncStatus) -> (SyncAPI.AbstractSyncStatus?))) = | ||||||
|         ioSafe { |         ioSafe { | ||||||
|             syncs.amap { (prefix, id) -> |             syncs.amap { (prefix, id) -> | ||||||
|                 repos.firstOrNull { it.idPrefix == prefix }?.let { repo -> |                 repos.firstOrNull { it.idPrefix == prefix }?.let { repo -> | ||||||
|  | @ -245,8 +247,12 @@ class SyncViewModel : ViewModel() { | ||||||
|         // shitty way to sort anilist first, as it has trailers while mal does not |         // shitty way to sort anilist first, as it has trailers while mal does not | ||||||
|         if (syncs.containsKey(aniListApi.idPrefix)) { |         if (syncs.containsKey(aniListApi.idPrefix)) { | ||||||
|             try { // swap can throw error |             try { // swap can throw error | ||||||
|                 Collections.swap(current, current.indexOfFirst { it.first == aniListApi.idPrefix }, 0) |                 Collections.swap( | ||||||
|             } catch (t : Throwable) { |                     current, | ||||||
|  |                     current.indexOfFirst { it.first == aniListApi.idPrefix }, | ||||||
|  |                     0 | ||||||
|  |                 ) | ||||||
|  |             } catch (t: Throwable) { | ||||||
|                 logError(t) |                 logError(t) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager | ||||||
| import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi | ||||||
| import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi | ||||||
| import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi | ||||||
| import com.lagradost.cloudstream3.syncproviders.AuthAPI | import com.lagradost.cloudstream3.syncproviders.AuthAPI | ||||||
| import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI | import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | import com.lagradost.cloudstream3.syncproviders.OAuth2API | ||||||
|  | @ -257,6 +258,7 @@ class SettingsAccount : PreferenceFragmentCompat() { | ||||||
|             listOf( |             listOf( | ||||||
|                 R.string.mal_key to malApi, |                 R.string.mal_key to malApi, | ||||||
|                 R.string.anilist_key to aniListApi, |                 R.string.anilist_key to aniListApi, | ||||||
|  |                 R.string.simkl_key to simklApi, | ||||||
|                 R.string.opensubtitles_key to openSubtitlesApi, |                 R.string.opensubtitles_key to openSubtitlesApi, | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/simkl_logo.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/simkl_logo.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="20dp" | ||||||
|  |     android:height="20dp" | ||||||
|  |     android:viewportWidth="1536" | ||||||
|  |     android:viewportHeight="1536"> | ||||||
|  |     <path | ||||||
|  |         android:fillColor="?attr/white" | ||||||
|  |         android:pathData="M205.8,970.3c0,35.3 1.8,65.9 5.4,91.8 6.7,53.2 28.2,94.4 64.5,123.5 33.6,27.3 88.9,45.7 166,55.1 63.2,8 166,12 308.5,12 234.3,0 377.5,-8 429.4,-23.9 52.9,-16 90.9,-45.8 114.2,-89.5 23.3,-44.2 34.9,-113.4 34.9,-207.8 0,-84.5 -11.9,-145.8 -35.6,-183.9 -15.7,-25.8 -36.6,-45.8 -62.9,-59.9 -26.2,-14.1 -61.5,-24.6 -105.9,-31.7 -43.9,-7 -145.5,-12.9 -304.6,-17.6 -175.8,-5.6 -274.6,-11.5 -296.5,-17.6 -26,-7.5 -39,-28.2 -39,-62 0,-35.2 12.3,-57 37,-65.5 22,-7 92.1,-10.6 210.5,-10.6 119.7,0 195,1.4 225.9,4.2 34.9,3.3 55.6,18 61.8,44.3 2.3,8.9 3.8,24.8 4.7,47.8h271c0.4,-22.4 0.6,-38.8 0.6,-49.1 0,-95.5 -21.5,-161.7 -64.6,-198.7 -31.8,-26.7 -83.9,-45.4 -156,-56.2 -54.7,-7.9 -148.4,-11.9 -281.1,-11.9 -204,0 -333.1,4.7 -387.4,14.1 -60.1,10.8 -104.9,33 -134.5,66.7 -39.5,44.5 -59.2,121.2 -59.2,229.8 0,51.6 4,93.3 12.1,125.1 14.3,57.6 44.8,98.4 91.5,122.3 36.3,18.7 98.9,29.5 187.6,32.4 34.1,0.9 108.4,4.2 223.2,9.8 111.2,5.6 184.2,8.4 219.2,8.4 50.6,0.9 82.7,8.2 96.2,21.8 9.8,9.8 14.8,26.9 14.8,51.3 0,35.6 -7.9,58.6 -23.5,68.9 -11.2,7 -34.5,12 -69.9,14.7 -18.8,1 -87.8,1.7 -207,2.1 -87,-0.5 -138.3,-0.9 -153.9,-1.4 -31.4,-0.9 -53.3,-2.6 -65.9,-4.9 -12.5,-2.3 -23.7,-6.4 -33.6,-13 -18.8,-13.6 -28,-44 -27.6,-91.4H205.8v50.8,-0.3z" /> | ||||||
|  | </vector> | ||||||
|  | @ -449,6 +449,7 @@ | ||||||
|     <string name="bottom_title_settings_des">Put the title under the poster</string> |     <string name="bottom_title_settings_des">Put the title under the poster</string> | ||||||
|     <!-- account stuff --> |     <!-- account stuff --> | ||||||
|     <string name="anilist_key" translatable="false">anilist_key</string> |     <string name="anilist_key" translatable="false">anilist_key</string> | ||||||
|  |     <string name="simkl_key" translatable="false">simkl_key</string> | ||||||
|     <string name="mal_key" translatable="false">mal_key</string> |     <string name="mal_key" translatable="false">mal_key</string> | ||||||
|     <string name="opensubtitles_key" translatable="false">opensubtitles_key</string> |     <string name="opensubtitles_key" translatable="false">opensubtitles_key</string> | ||||||
|     <string name="nginx_key" translatable="false">nginx_key</string> |     <string name="nginx_key" translatable="false">nginx_key</string> | ||||||
|  |  | ||||||
|  | @ -1,27 +1,32 @@ | ||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" | <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|         xmlns:app="http://schemas.android.com/apk/res-auto"> |     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||||
|     <Preference |     <Preference | ||||||
|             android:key="@string/mal_key" |         android:icon="@drawable/mal_logo" | ||||||
|             android:icon="@drawable/mal_logo" /> |         android:key="@string/mal_key" /> | ||||||
| 
 | 
 | ||||||
|     <Preference |     <Preference | ||||||
|             android:key="@string/anilist_key" |         android:icon="@drawable/ic_anilist_icon" | ||||||
|             android:icon="@drawable/ic_anilist_icon" /> |         android:key="@string/anilist_key" /> | ||||||
|     <Preference |  | ||||||
|             android:key="@string/opensubtitles_key" |  | ||||||
|             android:icon="@drawable/open_subtitles_icon" /> |  | ||||||
| <!--    <Preference--> |  | ||||||
| <!--            android:key="@string/nginx_key"--> |  | ||||||
| <!--            android:icon="@drawable/nginx" />--> |  | ||||||
| 
 | 
 | ||||||
| <!--    <Preference--> |     <Preference | ||||||
| <!--            android:title="@string/nginx_info_title"--> |         android:icon="@drawable/simkl_logo" | ||||||
| <!--            android:icon="@drawable/nginx_question"--> |         android:key="@string/simkl_key" /> | ||||||
| <!--            android:summary="@string/nginx_info_summary">--> | 
 | ||||||
| <!--        <intent--> |     <Preference | ||||||
| <!--                android:action="android.intent.action.VIEW"--> |         android:icon="@drawable/open_subtitles_icon" | ||||||
| <!--                android:data="https://www.sarlays.com/use-nginx-with-cloudstream/" />--> |         android:key="@string/opensubtitles_key" /> | ||||||
| <!--    </Preference>--> |     <!--    <Preference--> | ||||||
|  |     <!--            android:key="@string/nginx_key"--> | ||||||
|  |     <!--            android:icon="@drawable/nginx" />--> | ||||||
|  | 
 | ||||||
|  |     <!--    <Preference--> | ||||||
|  |     <!--            android:title="@string/nginx_info_title"--> | ||||||
|  |     <!--            android:icon="@drawable/nginx_question"--> | ||||||
|  |     <!--            android:summary="@string/nginx_info_summary">--> | ||||||
|  |     <!--        <intent--> | ||||||
|  |     <!--                android:action="android.intent.action.VIEW"--> | ||||||
|  |     <!--                android:data="https://www.sarlays.com/use-nginx-with-cloudstream/" />--> | ||||||
|  |     <!--    </Preference>--> | ||||||
| 
 | 
 | ||||||
| </PreferenceScreen> | </PreferenceScreen> | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue