From eb2701a01ce36bb8773b1e3ff38c2f95ea5f04b8 Mon Sep 17 00:00:00 2001 From: DatLag Date: Sun, 21 Apr 2024 15:50:14 +0200 Subject: [PATCH] prepare character request and bs sync --- .../commonMain/graphql/CharacterQuery.graphql | 25 ++++ ...ry.graphql => MediaListEntryQuery.graphql} | 2 +- .../dev/datlag/aniflow/anilist/Cache.kt | 20 ++++ .../aniflow/anilist/CharacterStateMachine.kt | 112 ++++++++++++++++++ .../dev/datlag/aniflow/anilist/StateSaver.kt | 1 + .../datlag/aniflow/anilist/model/Character.kt | 17 +++ .../aniflow/module/PlatformModule.android.kt | 4 + .../other/BurningSeriesResolver.android.kt | 47 +++++++- .../datlag/aniflow/module/NetworkModule.kt | 12 +- .../aniflow/other/BurningSeriesResolver.kt | 1 + .../screen/medium/MediumScreenComponent.kt | 21 ++-- .../other/BurningSeriesResolver.ios.kt | 2 + .../dev/datlag/aniflow/model/ExtendFlow.kt | 2 +- 13 files changed, 252 insertions(+), 14 deletions(-) create mode 100644 anilist/src/commonMain/graphql/CharacterQuery.graphql rename anilist/src/commonMain/graphql/{MediaListEntry.graphql => MediaListEntryQuery.graphql} (64%) create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterStateMachine.kt create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt diff --git a/anilist/src/commonMain/graphql/CharacterQuery.graphql b/anilist/src/commonMain/graphql/CharacterQuery.graphql new file mode 100644 index 0000000..d0706b0 --- /dev/null +++ b/anilist/src/commonMain/graphql/CharacterQuery.graphql @@ -0,0 +1,25 @@ +query CharacterQuery($id: Int, $html: Boolean) { + Character(id: $id) { + id, + name { + first + middle + last + full + native + userPreferred + }, + image { + large + medium + }, + description(asHtml:$html) + gender, + dateOfBirth { + year + month + day + }, + bloodType + } +} \ No newline at end of file diff --git a/anilist/src/commonMain/graphql/MediaListEntry.graphql b/anilist/src/commonMain/graphql/MediaListEntryQuery.graphql similarity index 64% rename from anilist/src/commonMain/graphql/MediaListEntry.graphql rename to anilist/src/commonMain/graphql/MediaListEntryQuery.graphql index 62b1524..d3dd588 100644 --- a/anilist/src/commonMain/graphql/MediaListEntry.graphql +++ b/anilist/src/commonMain/graphql/MediaListEntryQuery.graphql @@ -1,4 +1,4 @@ -query MediaListEntry($id: Int) { +query MediaListEntryQuery($id: Int) { MediaList(mediaId: $id) { score(format: POINT_5) } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt index 229bfb3..71d543d 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt @@ -2,6 +2,7 @@ package dev.datlag.aniflow.anilist import com.mayakapps.kache.InMemoryKache import com.mayakapps.kache.KacheStrategy +import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.tooling.async.suspendCatching import kotlin.time.Duration.Companion.hours @@ -35,6 +36,13 @@ internal object Cache { expireAfterWriteDuration = 2.hours } + private val character = InMemoryKache( + maxSize = 5L * 1024 * 1024 + ) { + strategy = KacheStrategy.LRU + expireAfterWriteDuration = 2.hours + } + suspend fun getTrending(key: TrendingQuery): TrendingQuery.Data? { return suspendCatching { trendingAnime.get(key) @@ -82,4 +90,16 @@ internal object Cache { medium.put(key, data) }.getOrNull() ?: data } + + suspend fun getCharacter(key: CharacterQuery) : Character? { + return suspendCatching { + character.get(key) + }.getOrNull() + } + + suspend fun setCharacter(key: CharacterQuery, data: Character): Character { + return suspendCatching { + character.put(key, data) + }.getOrNull() ?: data + } } \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterStateMachine.kt new file mode 100644 index 0000000..bb15642 --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterStateMachine.kt @@ -0,0 +1,112 @@ +package dev.datlag.aniflow.anilist + +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.api.Optional +import com.freeletics.flowredux.dsl.FlowReduxStateMachine +import dev.datlag.aniflow.anilist.model.Character +import dev.datlag.aniflow.firebase.FirebaseFactory +import dev.datlag.aniflow.model.CatchResult +import dev.datlag.aniflow.model.mapError +import dev.datlag.aniflow.model.saveFirstOrNull +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalCoroutinesApi::class) +class CharacterStateMachine( + private val client: ApolloClient, + private val fallbackClient: ApolloClient, + private val crashlytics: FirebaseFactory.Crashlytics? +) : FlowReduxStateMachine( + initialState = currentState +) { + + init { + spec { + inState { + onEnterEffect { + currentState = it + } + on { action, state -> + state.override { State.Loading(action.id) } + } + } + inState { + onEnterEffect { + currentState = it + } + onEnter { state -> + Cache.getCharacter(state.snapshot.query)?.let { + return@onEnter state.override { State.Success(state.snapshot.query, it) } + } + + val response = CatchResult.repeat(2, timeoutDuration = 30.seconds) { + val query = client.query(state.snapshot.query) + + query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + }.mapError { + val query = fallbackClient.query(state.snapshot.query) + + query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + }.mapSuccess { + it.Character?.let { data -> + State.Success(state.snapshot.query, Character(data)) + } + } + + state.override { + response.asSuccess { + crashlytics?.log(it) + + State.Error + } + } + } + } + inState { + onEnterEffect { + Cache.setCharacter(it.query, it.character) + currentState = it + } + on { action, state -> + state.override { State.Loading(action.id) } + } + } + inState { + onEnterEffect { + currentState = it + } + on { action, state -> + state.override { State.Loading(action.id) } + } + } + } + } + + sealed interface State { + data object Waiting : State + data class Loading(internal val query: CharacterQuery) : State { + constructor(id: Int) : this( + query = CharacterQuery( + id = Optional.present(id) + ) + ) + } + data class Success( + internal val query: CharacterQuery, + val character: Character + ) : State + data object Error : State + } + + sealed interface Action { + data class Load(val id: Int) : Action + } + + companion object { + var currentState: State + get() = StateSaver.character + set(value) { + StateSaver.character = value + } + } +} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt index 70cea9c..a63634c 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt @@ -17,4 +17,5 @@ internal object StateSaver { year = nextYear ) } + var character: CharacterStateMachine.State = CharacterStateMachine.State.Waiting } \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt new file mode 100644 index 0000000..67e6c63 --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt @@ -0,0 +1,17 @@ +package dev.datlag.aniflow.anilist.model + +import dev.datlag.aniflow.anilist.CharacterQuery +import kotlinx.serialization.Serializable + +@Serializable +open class Character( + open val id: Int, + open val gender: String?, + open val bloodType: String?, +) { + constructor(char: CharacterQuery.Character) : this( + id = char.id, + gender = char.gender?.ifBlank { null }, + bloodType = char.bloodType?.ifBlank { null }, + ) +} diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/module/PlatformModule.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/module/PlatformModule.android.kt index 108bec8..cd6b448 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/module/PlatformModule.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/module/PlatformModule.android.kt @@ -10,6 +10,7 @@ import dev.datlag.aniflow.BuildKonfig import dev.datlag.aniflow.Sekret import dev.datlag.aniflow.firebase.FirebaseFactory import dev.datlag.aniflow.firebase.initialize +import dev.datlag.aniflow.other.BurningSeriesResolver import dev.datlag.aniflow.other.Constants import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.settings.DataStoreUserSettings @@ -89,6 +90,9 @@ actual object PlatformModule { bindSingleton { DataStoreUserSettings(instance()) } + bindSingleton { + BurningSeriesResolver(context = instance()) + } } } diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.android.kt index 5b8ed1b..57737bc 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.android.kt @@ -19,7 +19,7 @@ actual class BurningSeriesResolver( constructor(context: Context) : this(context.contentResolver) actual fun resolveWatchedEpisodes(): Set { - if (episodeClient == null || seriesClient == null) { + if (episodeClient == null) { return emptySet() } @@ -60,6 +60,8 @@ actual class BurningSeriesResolver( ) ) ) + + episodeCursor.moveToNext() } } @@ -67,6 +69,49 @@ actual class BurningSeriesResolver( return episodes } + actual fun resolveByName(english: String?, romaji: String?) { + val englishTrimmed = english?.trim()?.ifBlank { null } + val romajiTrimmed = romaji?.trim()?.ifBlank { null } + + if (seriesClient == null || (englishTrimmed == null && romajiTrimmed == null)) { + return + } + + val selection = if (englishTrimmed != null && romajiTrimmed != null) { + "title LIKE '%$englishTrimmed%' OR title LIKE '%$romajiTrimmed%'" + } else if (englishTrimmed != null) { + "title LIKE '%$englishTrimmed%'" + } else { + "title LIKE '%$romajiTrimmed%'" + } + val seriesCursor = seriesClient.query( + seriesContentUri, + null, + selection, + null, + null + ) ?: return + + if (seriesCursor.moveToFirst()) { + while (!seriesCursor.isAfterLast) { + val titleIndex = seriesCursor.getColumnIndex("title") + + if (titleIndex == -1) { + seriesCursor.moveToNext() + continue + } + + val title = seriesCursor.getString(titleIndex) + Napier.e("Series matching name: $title") + + seriesCursor.moveToNext() + } + } + + seriesCursor.close() + return + } + actual fun close() { episodeClient?.close() seriesClient?.close() diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt index fa992ca..94ba901 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -16,10 +16,7 @@ import de.jensklingenberg.ktorfit.Ktorfit import de.jensklingenberg.ktorfit.ktorfitBuilder import dev.datlag.aniflow.BuildKonfig import dev.datlag.aniflow.Sekret -import dev.datlag.aniflow.anilist.AiringTodayStateMachine -import dev.datlag.aniflow.anilist.PopularNextSeasonStateMachine -import dev.datlag.aniflow.anilist.PopularSeasonStateMachine -import dev.datlag.aniflow.anilist.TrendingAnimeStateMachine +import dev.datlag.aniflow.anilist.* import dev.datlag.aniflow.other.Constants import dev.datlag.tooling.compose.ioDispatcher import io.ktor.client.* @@ -123,6 +120,13 @@ data object NetworkModule { crashlytics = nullableFirebaseInstance()?.crashlytics ) } + bindProvider { + CharacterStateMachine( + client = instance(Constants.AniList.APOLLO_CLIENT), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), + crashlytics = nullableFirebaseInstance()?.crashlytics + ) + } bindSingleton(Constants.AniList.Auth.CLIENT) { OpenIdConnectClient { endpoints { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt index 5b03dce..0097c3d 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt @@ -2,6 +2,7 @@ package dev.datlag.aniflow.other expect class BurningSeriesResolver { fun resolveWatchedEpisodes(): Set + fun resolveByName(english: String?, romaji: String?) fun close() } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt index 721cedf..6bcc8cb 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt @@ -10,10 +10,7 @@ import com.arkivanov.essenty.backhandler.BackCallback import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import dev.chrisbanes.haze.HazeState import dev.datlag.aniflow.LocalHaze -import dev.datlag.aniflow.anilist.MediaListEntryQuery -import dev.datlag.aniflow.anilist.MediumQuery -import dev.datlag.aniflow.anilist.MediumStateMachine -import dev.datlag.aniflow.anilist.RatingMutation +import dev.datlag.aniflow.anilist.* import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.MediaFormat import dev.datlag.aniflow.anilist.type.MediaStatus @@ -22,6 +19,7 @@ import dev.datlag.aniflow.common.onRenderApplyCommonScheme import dev.datlag.aniflow.common.popular import dev.datlag.aniflow.common.rated import dev.datlag.aniflow.model.* +import dev.datlag.aniflow.other.BurningSeriesResolver import dev.datlag.aniflow.other.Constants import dev.datlag.aniflow.other.TokenRefreshHandler import dev.datlag.aniflow.settings.Settings @@ -302,7 +300,16 @@ class MediumScreenComponent( ) private val userSettings by di.instance() - private val apolloClient by di.instance(Constants.AniList.APOLLO_CLIENT) + private val characterStateMachine by di.instance() + private val burningSeriesResolver by di.instance() + + init { + launchIO { + title.mapNotNull { it.english to it.romaji }.collect { (english, romaji) -> + burningSeriesResolver.resolveByName(english = english, romaji = romaji) + } + } + } @Composable override fun render() { @@ -346,7 +353,7 @@ class MediumScreenComponent( id = Optional.present(mediaId.saveFirstOrNull() ?: mediaId.value) ) val execution = CatchResult.timeout(5.seconds) { - apolloClient.query(query).execute() + aniListClient.query(query).execute() }.asNullableSuccess() execution?.data?.MediaList?.let { entry -> @@ -383,7 +390,7 @@ class MediumScreenComponent( rating = Optional.present(value * 20) ) launchIO { - apolloClient.mutation(mutation).execute().data?.SaveMediaListEntry?.score?.let { + aniListClient.mutation(mutation).execute().data?.SaveMediaListEntry?.score?.let { changedRating.emit(it.toInt()) } } diff --git a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.ios.kt b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.ios.kt index 06303b4..1ce1092 100644 --- a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.ios.kt +++ b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.ios.kt @@ -6,5 +6,7 @@ actual class BurningSeriesResolver { return emptySet() } + actual fun resolveByName(english: String?, romaji: String?) { } + actual fun close() { } } \ No newline at end of file diff --git a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/ExtendFlow.kt b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/ExtendFlow.kt index 30dc871..3296fc5 100644 --- a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/ExtendFlow.kt +++ b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/ExtendFlow.kt @@ -5,5 +5,5 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.firstOrNull suspend fun Flow.saveFirstOrNull(): T? { - return this.firstOrNull() ?: this.firstOrNull() ?: (this as? StateFlow)?.value + return this.firstOrNull() ?: (this as? StateFlow)?.value ?: this.firstOrNull() } \ No newline at end of file