diff --git a/app/android/src/androidMain/kotlin/dev/datlag/burningseries/App.kt b/app/android/src/androidMain/kotlin/dev/datlag/burningseries/App.kt index d3963a91..48ad32c1 100644 --- a/app/android/src/androidMain/kotlin/dev/datlag/burningseries/App.kt +++ b/app/android/src/androidMain/kotlin/dev/datlag/burningseries/App.kt @@ -5,10 +5,12 @@ import androidx.lifecycle.LifecycleOwner import androidx.multidex.MultiDexApplication import dev.datlag.burningseries.model.common.systemProperty import dev.datlag.burningseries.module.NetworkModule +import dev.datlag.burningseries.network.state.NetworkStateSaver import dev.datlag.burningseries.other.StateSaver import dev.datlag.sekret.NativeLoader import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier +import kotlinx.coroutines.runBlocking import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.bindSingleton @@ -42,5 +44,8 @@ class App : MultiDexApplication(), DIAware, DefaultLifecycleObserver { defaultAllowRestrictedHeaders?.ifBlank { null }?.let { systemProperty("sun.net.http.allowRestrictedHeaders", it) } + runBlocking { + NetworkStateSaver.firebaseUser?.delete() + } } } \ No newline at end of file diff --git a/app/android/src/androidMain/kotlin/dev/datlag/burningseries/MainActivity.kt b/app/android/src/androidMain/kotlin/dev/datlag/burningseries/MainActivity.kt index 70ca64c4..93411c28 100644 --- a/app/android/src/androidMain/kotlin/dev/datlag/burningseries/MainActivity.kt +++ b/app/android/src/androidMain/kotlin/dev/datlag/burningseries/MainActivity.kt @@ -22,6 +22,7 @@ import com.arkivanov.essenty.lifecycle.essentyLifecycle import com.google.android.gms.cast.framework.CastContext import dev.datlag.burningseries.common.lifecycle.LocalLifecycleOwner import dev.datlag.burningseries.common.lifecycle.collectAsStateWithLifecycle +import dev.datlag.burningseries.network.state.NetworkStateSaver import dev.datlag.burningseries.ui.KeyEventDispatcher import dev.datlag.burningseries.ui.PIPActions import dev.datlag.burningseries.ui.PIPEventDispatcher @@ -35,6 +36,8 @@ import io.kamel.image.config.LocalKamelConfig import io.kamel.image.config.resourcesFetcher import io.kamel.image.config.resourcesIdMapper import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.runBlocking +import org.kodein.di.instance import java.util.concurrent.Executors class MainActivity : AppCompatActivity() { diff --git a/app/desktop/src/jvmMain/kotlin/dev/datlag/burningseries/Main.kt b/app/desktop/src/jvmMain/kotlin/dev/datlag/burningseries/Main.kt index 87f5b180..a7cae128 100644 --- a/app/desktop/src/jvmMain/kotlin/dev/datlag/burningseries/Main.kt +++ b/app/desktop/src/jvmMain/kotlin/dev/datlag/burningseries/Main.kt @@ -22,6 +22,7 @@ import dev.datlag.burningseries.common.lifecycle.LocalLifecycleOwner import dev.datlag.burningseries.model.common.systemProperty import dev.datlag.burningseries.module.NetworkModule import dev.datlag.burningseries.SharedRes +import dev.datlag.burningseries.network.state.NetworkStateSaver import dev.datlag.burningseries.other.StateSaver import dev.datlag.burningseries.ui.navigation.NavHostComponent import dev.datlag.burningseries.window.disposableSingleWindowApplication @@ -36,19 +37,11 @@ import io.kamel.image.config.Default import io.kamel.image.config.LocalKamelConfig import io.kamel.image.config.resourcesFetcher import io.kamel.image.config.svgDecoder +import kotlinx.coroutines.runBlocking import org.kodein.di.DI import java.io.File fun main(vararg args: String) { - runWindow() -} - -@OptIn(ExperimentalDecomposeApi::class) -private fun runWindow() { - val appTitle = StringDesc.Resource(SharedRes.strings.app_name).localized() - AppIO.applyTitle(appTitle) - Napier.base(DebugAntilog()) - StateSaver.sekretLibraryLoaded = NativeLoader.loadLibrary("sekret", systemProperty("compose.application.resources.dir")?.let { File(it) }) FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() { @@ -59,15 +52,35 @@ private fun runWindow() { override fun log(msg: String) = println(msg) }) + val di = DI { + import(NetworkModule.di) + } + + Runtime.getRuntime().addShutdownHook(Thread { + runBlocking { + NetworkStateSaver.firebaseUser?.delete() + } + }) + + runWindow(di) + + runBlocking { + NetworkStateSaver.firebaseUser?.delete() + } +} + +@OptIn(ExperimentalDecomposeApi::class) +private fun runWindow(di: DI) { + val appTitle = StringDesc.Resource(SharedRes.strings.app_name).localized() + AppIO.applyTitle(appTitle) + Napier.base(DebugAntilog()) + val windowState = WindowState() val lifecycle = LifecycleRegistry() val lifecycleOwner = object : LifecycleOwner { override val lifecycle: Lifecycle = lifecycle } val backDispatcher = BackDispatcher() - val di = DI { - import(NetworkModule.di) - } val root = NavHostComponent( componentContext = DefaultComponentContext( lifecycle, diff --git a/app/shared/build.gradle.kts b/app/shared/build.gradle.kts index 7fee873f..17bf3876 100644 --- a/app/shared/build.gradle.kts +++ b/app/shared/build.gradle.kts @@ -60,7 +60,6 @@ kotlin { api(libs.windowsize.multiplatform) api(libs.insetsx) - api(libs.webview) api(libs.ktor) api(libs.ktor.content.negotiation) @@ -98,6 +97,7 @@ kotlin { api(libs.cast) api(libs.cast.framework) api(libs.accompanist.uicontroller) + api(libs.webview.android) } } val desktopMain by getting { @@ -111,6 +111,7 @@ kotlin { api(libs.ktor.jvm) api(libs.appdirs) api(libs.vlcj) + api(libs.webview.desktop) } } } diff --git a/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.android.kt b/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.android.kt index 70bb4b76..95b09390 100644 --- a/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.android.kt +++ b/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.android.kt @@ -1,40 +1,55 @@ package dev.datlag.burningseries.ui.screen.initial.series.activate.component -import androidx.compose.foundation.layout.Box +import android.annotation.SuppressLint +import android.webkit.WebView import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import com.multiplatform.webview.web.rememberWebViewNavigator -import com.multiplatform.webview.web.rememberWebViewState +import com.google.accompanist.web.rememberWebViewNavigator +import com.google.accompanist.web.rememberWebViewState import dev.datlag.burningseries.SharedRes import dev.datlag.burningseries.common.withIOContext import dev.datlag.burningseries.common.withMainContext +import dev.datlag.burningseries.model.BSUtil import kotlinx.coroutines.delay import kotlinx.coroutines.isActive +@SuppressLint("SetJavaScriptEnabled") @Composable actual fun WebView(url: String, modifier: Modifier, onScraped: (String) -> Unit) { val state = rememberWebViewState(url) - val navigator = rememberWebViewNavigator() + var webView = remember { null } val scrapingJs = SharedRes.assets.scrape_hoster_android.readText(LocalContext.current) - com.multiplatform.webview.web.WebView( + + com.google.accompanist.web.WebView( state = state, modifier = modifier, - navigator = navigator + captureBackPresses = true, + client = WebViewClient( + allowedHosts = setOf(BSUtil.HOST_BS_TO) + ), + onCreated = { + it.settings.allowFileAccess = false + it.settings.javaScriptEnabled = true + it.settings.javaScriptCanOpenWindowsAutomatically = false + it.settings.mediaPlaybackRequiresUserGesture = true + webView = it + } ) - LaunchedEffect(navigator) { + LaunchedEffect(webView) { withIOContext { do { delay(3000) withMainContext { - navigator.evaluateJavaScript(scrapingJs) { - onScraped(it) + webView?.evaluateJavascript(scrapingJs) { result -> + result?.let(onScraped) } } - } while (isActive) + } while (isActive && webView != null) } } } \ No newline at end of file diff --git a/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebViewClient.kt b/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebViewClient.kt new file mode 100644 index 00000000..fdba112d --- /dev/null +++ b/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebViewClient.kt @@ -0,0 +1,18 @@ +package dev.datlag.burningseries.ui.screen.initial.series.activate.component + +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import com.google.accompanist.web.AccompanistWebViewClient + +data class WebViewClient( + private val allowedHosts: Set = setOf() +) : AccompanistWebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + return if (request?.url?.host != null) { + !allowedHosts.contains(request.url.host) + } else { + super.shouldOverrideUrlLoading(view, request) + } + } +} diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateComponent.kt index a8baa897..5fa919c9 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateComponent.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateComponent.kt @@ -7,11 +7,13 @@ import dev.datlag.burningseries.model.Stream import dev.datlag.burningseries.ui.navigation.Component import dev.datlag.burningseries.ui.navigation.DialogComponent import dev.datlag.burningseries.ui.screen.initial.series.activate.component.DialogConfig +import kotlinx.coroutines.flow.StateFlow interface ActivateComponent : Component { val onDeviceReachable: Boolean val episode: Series.Episode + val isSaving: StateFlow val dialog: Value> diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreen.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreen.kt index 7bec038b..9e24e104 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreen.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreen.kt @@ -3,15 +3,18 @@ package dev.datlag.burningseries.ui.screen.initial.series.activate import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.filled.Save import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState import dev.icerock.moko.resources.compose.stringResource import dev.datlag.burningseries.SharedRes +import dev.datlag.burningseries.common.lifecycle.collectAsStateWithLifecycle import dev.datlag.burningseries.model.BSUtil import dev.datlag.burningseries.ui.custom.state.UnreachableState import dev.datlag.burningseries.ui.screen.initial.series.activate.component.WebView @@ -36,6 +39,21 @@ fun ActivateScreen(component: ActivateComponent) { contentDescription = stringResource(SharedRes.strings.back) ) } + }, + actions = { + Box( + contentAlignment = Alignment.Center + ) { + val isSaving by component.isSaving.collectAsStateWithLifecycle() + + if (isSaving) { + Icon( + imageVector = Icons.Default.Save, + contentDescription = stringResource(SharedRes.strings.saving) + ) + CircularProgressIndicator() + } + } } ) } diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreenComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreenComponent.kt index 70ebc790..22268909 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreenComponent.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/ActivateScreenComponent.kt @@ -27,6 +27,7 @@ import dev.datlag.burningseries.ui.screen.initial.series.activate.component.Dial import dev.datlag.burningseries.ui.screen.initial.series.activate.dialog.error.ErrorDialogComponent import dev.datlag.burningseries.ui.screen.initial.series.activate.dialog.success.SuccessDialogComponent import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.serialization.json.Json import org.kodein.di.instance @@ -45,6 +46,8 @@ class ActivateScreenComponent( private val json by di.instance() private val savedData: MutableSet = mutableSetOf() + override val isSaving = saveState.map { it is SaveState.Saving }.stateIn(ioScope(), SharingStarted.Lazily, saveState.value is SaveState.Saving) + private val dialogNavigation = SlotNavigation() override val dialog = childSlot( source = dialogNavigation diff --git a/app/shared/src/commonMain/resources/MR/base/strings.xml b/app/shared/src/commonMain/resources/MR/base/strings.xml index 48831b4c..c51470d3 100644 --- a/app/shared/src/commonMain/resources/MR/base/strings.xml +++ b/app/shared/src/commonMain/resources/MR/base/strings.xml @@ -47,4 +47,5 @@ Exit Fullscreen %s - %s Unable to activate with your current network as the website is unreachable. + Saving \ No newline at end of file diff --git a/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.desktop.kt b/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.desktop.kt index d510ff15..f6a63bf9 100644 --- a/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.desktop.kt +++ b/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.desktop.kt @@ -8,11 +8,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.awt.ComposePanel import androidx.compose.ui.awt.SwingPanel -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.multiplatform.webview.web.WebContent import dev.datlag.burningseries.SharedRes import dev.datlag.burningseries.common.withIOContext import dev.datlag.burningseries.common.withMainContext diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f6bef1ff..6cf0f8d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,7 @@ grpc = "1.59.0" instesx = "0.1.0-alpha10" jsunpacker = "1.0.2" kamel = "0.8.3" +kcef = "2023.10.13" kodein = "7.21.0" kotlin = "1.9.20" ksp = "1.9.20-1.0.14" @@ -46,7 +47,6 @@ sqldelight = "2.0.0" turbine = "1.0.0" versions = "0.50.0" vlcj = "4.8.2" -webview = "1.7.2" windowsize-multiplatform = "0.3.1" window-styler = "0.3.2" @@ -124,7 +124,8 @@ sqldelight-native = { group = "app.cash.sqldelight", name = "native-driver", ver turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" } test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } vlcj = { group = "uk.co.caprica", name = "vlcj", version.ref = "vlcj" } -webview = { group = "io.github.kevinnzou", name = "compose-webview-multiplatform", version.ref = "webview" } +webview-android = { group = "com.google.accompanist", name = "accompanist-webview", version.ref = "accompanist" } +webview-desktop = { group = "dev.datlag", name = "kcef", version.ref = "kcef" } windowsize-multiplatform = { group = "dev.chrisbanes.material3", name = "material3-window-size-class-multiplatform", version.ref = "windowsize-multiplatform" } window-styler = { group = "com.mayakapps.compose", name = "window-styler", version.ref = "window-styler" } diff --git a/network/src/androidMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.android.kt b/network/src/androidMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.android.kt index b626ccda..e844cca5 100644 --- a/network/src/androidMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.android.kt +++ b/network/src/androidMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.android.kt @@ -1,6 +1,7 @@ package dev.datlag.burningseries.network.firebase import dev.datlag.burningseries.model.HosterScraping +import dev.datlag.burningseries.model.common.suspendCatching import dev.datlag.burningseries.network.Firestore import dev.gitlive.firebase.auth.FirebaseUser import dev.gitlive.firebase.firestore.DocumentReference @@ -13,8 +14,12 @@ actual object FireStore { firestoreApi: Firestore, idList: List ): List { - return firestore.collection("stream").where("id", inArray = idList).get().documents.map { - it.get("url") + return (suspendCatching { + firestore.collection("stream").where("id", inArray = idList).get().documents.map { + it.get("url") + } + }.getOrNull() ?: emptyList()).ifEmpty { + RESTFireStore.getStreams(firestore, firestoreApi, idList) } } @@ -25,20 +30,22 @@ actual object FireStore { data: HosterScraping.Firestore ): Boolean { if (firestore == null) { - return false + return RESTFireStore.addStream(firebaseUser, firestore, firestoreApi, data) } - val existing = firestore - .collection("stream") - .where("id", equalTo = data.id) - .get() - .documents - .firstOrNull() - ?.reference - ?: firestore.collection("stream").document + return suspendCatching { + val existing = firestore + .collection("stream") + .where("id", equalTo = data.id) + .get() + .documents + .firstOrNull() + ?.reference + ?: firestore.collection("stream").document - return firestore.runTransaction { - set(documentRef = existing, data = data, merge = true) - }.get(existing).exists + firestore.runTransaction { + set(documentRef = existing, data = data, merge = true) + } + }.isSuccess || RESTFireStore.addStream(firebaseUser, firestore, firestoreApi, data) } } \ No newline at end of file diff --git a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/firebase/RESTFireStore.kt b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/firebase/RESTFireStore.kt new file mode 100644 index 00000000..3d4a3d9a --- /dev/null +++ b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/firebase/RESTFireStore.kt @@ -0,0 +1,124 @@ +package dev.datlag.burningseries.network.firebase + +import dev.datlag.burningseries.model.FirestoreDocument +import dev.datlag.burningseries.model.FirestoreQuery +import dev.datlag.burningseries.model.HosterScraping +import dev.datlag.burningseries.model.common.suspendCatching +import dev.datlag.burningseries.network.Firestore +import dev.gitlive.firebase.auth.FirebaseUser +import dev.gitlive.firebase.firestore.FirebaseFirestore +import io.ktor.http.* + +data object RESTFireStore { + + suspend fun getStreams( + firestore: FirebaseFirestore, + firestoreApi: Firestore, + idList: List + ): List { + val result = firestoreApi.query(FirestoreQuery( + structuredQuery = FirestoreQuery.StructuredQuery( + from = listOf( + FirestoreQuery.StructuredQuery.From( + collectionId = "stream" + ) + ), + where = FirestoreQuery.StructuredQuery.Where( + fieldFilter = FirestoreQuery.StructuredQuery.Where.FieldFilter( + field = FirestoreQuery.StructuredQuery.Field( + fieldPath = "id" + ), + op = FirestoreQuery.StructuredQuery.Where.FieldFilter.OP.IN, + value = FirestoreQuery.Value( + arrayValue = FirestoreQuery.Value.ArrayValue( + values = idList.map { FirestoreQuery.Value( + stringValue = it + ) } + ) + ) + ) + ), + select = FirestoreQuery.StructuredQuery.Select( + fields = listOf( + FirestoreQuery.StructuredQuery.Field( + fieldPath = "url" + ) + ) + ) + ) + )) + return result.mapNotNull { + it.document.fields["url"]?.stringValue + } + } + + suspend fun addStream( + firebaseUser: FirebaseUser?, + firestore: FirebaseFirestore?, + firestoreApi: Firestore?, + data: HosterScraping.Firestore + ): Boolean { + if (firestoreApi == null) { + return false + } + val token = firebaseUser?.getIdToken(false)?.let { "Bearer $it" } ?: return false + + val existing = suspendCatching { + firestoreApi.query( + FirestoreQuery( + structuredQuery = FirestoreQuery.StructuredQuery( + from = listOf( + FirestoreQuery.StructuredQuery.From( + collectionId = "stream" + ) + ), + where = FirestoreQuery.StructuredQuery.Where( + fieldFilter = FirestoreQuery.StructuredQuery.Where.FieldFilter( + field = FirestoreQuery.StructuredQuery.Field( + fieldPath = "id" + ), + op = FirestoreQuery.StructuredQuery.Where.FieldFilter.OP.EQUAL, + value = FirestoreQuery.Value( + stringValue = data.id + ) + ) + ) + ) + ) + ) + }.getOrNull()?.firstOrNull() + + val response = if (existing != null) { + val docName = existing.document.name!!.removeSuffix("/").substringAfterLast("/") + + firestoreApi.patch( + token = token, + collection = "stream/$docName", + request = FirestoreDocument( + fields = existing.document.fields.toMutableMap().apply { + put("url", FirestoreQuery.Value( + stringValue = data.url + )) + } + ) + ) + } else { + firestoreApi.create( + token = token, + collection = "stream", + request = FirestoreDocument( + fields = mapOf( + "id" to FirestoreQuery.Value( + stringValue = data.id + ), + "url" to FirestoreQuery.Value( + stringValue = data.url + ) + ) + ) + ) + } + + return response.status.isSuccess() + } +} \ No newline at end of file diff --git a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/EpisodeStateMachine.kt b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/EpisodeStateMachine.kt index 7b2f5500..91d62e90 100644 --- a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/EpisodeStateMachine.kt +++ b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/EpisodeStateMachine.kt @@ -11,21 +11,16 @@ import dev.datlag.burningseries.network.JsonBase import dev.datlag.burningseries.network.firebase.FireStore import dev.datlag.burningseries.network.scraper.Video import dev.gitlive.firebase.Firebase -import dev.gitlive.firebase.auth.FirebaseUser import dev.gitlive.firebase.auth.auth import dev.gitlive.firebase.firestore.FirebaseFirestore -import dev.gitlive.firebase.firestore.firestore -import dev.gitlive.firebase.firestore.where import io.ktor.client.* import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.Credentials -import io.realm.kotlin.mongodb.User import io.realm.kotlin.mongodb.ext.call import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import kotlinx.serialization.json.JsonPrimitive import org.mongodb.kbson.BsonDocument @OptIn(ExperimentalCoroutinesApi::class) @@ -41,13 +36,13 @@ class EpisodeStateMachine( spec { inState { onEnterEffect { - if (StateSaver.mongoUser == null) { - StateSaver.mongoUser = suspendCatching { + if (NetworkStateSaver.mongoUser == null) { + NetworkStateSaver.mongoUser = suspendCatching { app?.login(Credentials.anonymous()) }.getOrNull() } - if (StateSaver.firebaseUser == null) { - StateSaver.firebaseUser = suspendCatching { + if (NetworkStateSaver.firebaseUser == null) { + NetworkStateSaver.firebaseUser = suspendCatching { Firebase.auth.signInAnonymously().user }.getOrNull() } @@ -78,15 +73,15 @@ class EpisodeStateMachine( } }.awaitAll().filterNotNull() } - val mongoHoster = StateSaver.mongoHosterMap[episodeHref] ?: emptyList() + val mongoHoster = NetworkStateSaver.mongoHosterMap[episodeHref] ?: emptyList() val mongoDBResults = async { mongoHoster.ifEmpty { val newList = suspendCatching { - val doc = StateSaver.mongoUser!!.functions.call("query", hosterHref.toTypedArray()) + val doc = NetworkStateSaver.mongoUser!!.functions.call("query", hosterHref.toTypedArray()) doc.getArray("result").values.map { it.asDocument().getString("url").value } }.getOrNull() ?: emptyList() - StateSaver.mongoHosterMap[episodeHref] = newList + NetworkStateSaver.mongoHosterMap[episodeHref] = newList newList } } diff --git a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/StateSaver.kt b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/NetworkStateSaver.kt similarity index 89% rename from network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/StateSaver.kt rename to network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/NetworkStateSaver.kt index 08cdfcfd..65c85b7f 100644 --- a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/StateSaver.kt +++ b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/NetworkStateSaver.kt @@ -3,7 +3,7 @@ package dev.datlag.burningseries.network.state import dev.gitlive.firebase.auth.FirebaseUser import io.realm.kotlin.mongodb.User -internal data object StateSaver { +data object NetworkStateSaver { var mongoUser: User? = null var firebaseUser: FirebaseUser? = null diff --git a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/SaveStateMachine.kt b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/SaveStateMachine.kt index a22241f0..715dbe5d 100644 --- a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/SaveStateMachine.kt +++ b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/SaveStateMachine.kt @@ -34,13 +34,13 @@ class SaveStateMachine( spec { inState { onEnterEffect { - if (StateSaver.mongoUser == null) { - StateSaver.mongoUser = suspendCatching { + if (NetworkStateSaver.mongoUser == null) { + NetworkStateSaver.mongoUser = suspendCatching { app?.login(Credentials.anonymous()) }.getOrNull() } - if (StateSaver.firebaseUser == null) { - StateSaver.firebaseUser = suspendCatching { + if (NetworkStateSaver.firebaseUser == null) { + NetworkStateSaver.firebaseUser = suspendCatching { Firebase.auth.signInAnonymously().user }.getOrNull() } @@ -62,14 +62,14 @@ class SaveStateMachine( val mongoSaved = async { suspendCatching { - StateSaver.mongoUser!!.functions.call("add", state.snapshot.data.href, state.snapshot.data.url) + NetworkStateSaver.mongoUser!!.functions.call("add", state.snapshot.data.href, state.snapshot.data.url) }.getOrNull() != null } val firebaseSaved = async { suspendCatching { FireStore.addStream( - firebaseUser = StateSaver.firebaseUser, + firebaseUser = NetworkStateSaver.firebaseUser, firestore = firestore, firestoreApi = firestoreApi, data = state.snapshot.data.firestore diff --git a/network/src/iosMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.ios.kt b/network/src/iosMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.ios.kt index e731c7c8..384be6ae 100644 --- a/network/src/iosMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.ios.kt +++ b/network/src/iosMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.ios.kt @@ -1,6 +1,7 @@ package dev.datlag.burningseries.network.firebase import dev.datlag.burningseries.model.HosterScraping +import dev.datlag.burningseries.model.common.suspendCatching import dev.datlag.burningseries.network.Firestore import dev.gitlive.firebase.auth.FirebaseUser import dev.gitlive.firebase.firestore.FirebaseFirestore @@ -12,8 +13,12 @@ actual object FireStore { firestoreApi: Firestore, idList: List ): List { - return firestore.collection("stream").where("id", inArray = idList).get().documents.map { - it.get("url") + return (suspendCatching { + firestore.collection("stream").where("id", inArray = idList).get().documents.map { + it.get("url") + } + }.getOrNull() ?: emptyList()).ifEmpty { + RESTFireStore.getStreams(firestore, firestoreApi, idList) } } @@ -24,20 +29,22 @@ actual object FireStore { data: HosterScraping.Firestore ): Boolean { if (firestore == null) { - return false + return RESTFireStore.addStream(firebaseUser, firestore, firestoreApi, data) } - val existing = firestore - .collection("stream") - .where("id", equalTo = data.id) - .get() - .documents - .firstOrNull() - ?.reference - ?: firestore.collection("stream").document + return suspendCatching { + val existing = firestore + .collection("stream") + .where("id", equalTo = data.id) + .get() + .documents + .firstOrNull() + ?.reference + ?: firestore.collection("stream").document - return firestore.runTransaction { - set(documentRef = existing, data = data, merge = true) - }.get(existing).exists + firestore.runTransaction { + set(documentRef = existing, data = data, merge = true) + } + }.isSuccess || RESTFireStore.addStream(firebaseUser, firestore, firestoreApi, data) } } \ No newline at end of file diff --git a/network/src/jvmMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.jvm.kt b/network/src/jvmMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.jvm.kt index e15a329f..3bbeaae4 100644 --- a/network/src/jvmMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.jvm.kt +++ b/network/src/jvmMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.jvm.kt @@ -15,110 +15,12 @@ actual object FireStore { firestore: FirebaseFirestore, firestoreApi: Firestore, idList: List - ): List { - val result = firestoreApi.query(FirestoreQuery( - structuredQuery = FirestoreQuery.StructuredQuery( - from = listOf( - FirestoreQuery.StructuredQuery.From( - collectionId = "stream" - ) - ), - where = FirestoreQuery.StructuredQuery.Where( - fieldFilter = FirestoreQuery.StructuredQuery.Where.FieldFilter( - field = FirestoreQuery.StructuredQuery.Field( - fieldPath = "id" - ), - op = FirestoreQuery.StructuredQuery.Where.FieldFilter.OP.IN, - value = FirestoreQuery.Value( - arrayValue = FirestoreQuery.Value.ArrayValue( - values = idList.map { FirestoreQuery.Value( - stringValue = it - ) } - ) - ) - ) - ), - select = FirestoreQuery.StructuredQuery.Select( - fields = listOf( - FirestoreQuery.StructuredQuery.Field( - fieldPath = "url" - ) - ) - ) - ) - )) - return result.mapNotNull { - it.document.fields["url"]?.stringValue - } - } + ): List = RESTFireStore.getStreams(firestore, firestoreApi, idList) actual suspend fun addStream( firebaseUser: FirebaseUser?, firestore: FirebaseFirestore?, firestoreApi: Firestore?, data: HosterScraping.Firestore - ): Boolean { - if (firestoreApi == null) { - return false - } - val token = firebaseUser?.getIdToken(false)?.let { "Bearer $it" } ?: return false - - val existing = suspendCatching { - firestoreApi.query( - FirestoreQuery( - structuredQuery = FirestoreQuery.StructuredQuery( - from = listOf( - FirestoreQuery.StructuredQuery.From( - collectionId = "stream" - ) - ), - where = FirestoreQuery.StructuredQuery.Where( - fieldFilter = FirestoreQuery.StructuredQuery.Where.FieldFilter( - field = FirestoreQuery.StructuredQuery.Field( - fieldPath = "id" - ), - op = FirestoreQuery.StructuredQuery.Where.FieldFilter.OP.EQUAL, - value = FirestoreQuery.Value( - stringValue = data.id - ) - ) - ) - ) - ) - ) - }.getOrNull()?.firstOrNull() - - val response = if (existing != null) { - val docName = existing.document.name!!.removeSuffix("/").substringAfterLast("/") - - firestoreApi.patch( - token = token, - collection = "stream/$docName", - request = FirestoreDocument( - fields = existing.document.fields.toMutableMap().apply { - put("url", FirestoreQuery.Value( - stringValue = data.url - )) - } - ) - ) - } else { - firestoreApi.create( - token = token, - collection = "stream", - request = FirestoreDocument( - fields = mapOf( - "id" to FirestoreQuery.Value( - stringValue = data.id - ), - "url" to FirestoreQuery.Value( - stringValue = data.url - ) - ) - ) - ) - } - - return response.status.isSuccess() - } + ): Boolean = RESTFireStore.addStream(firebaseUser, firestore, firestoreApi, data) } \ No newline at end of file