diff --git a/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.android.kt b/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.android.kt deleted file mode 100644 index b0656300..00000000 --- a/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.android.kt +++ /dev/null @@ -1,16 +0,0 @@ -package dev.datlag.burningseries.ui.custom - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.multiplatform.webview.web.WebView -import com.multiplatform.webview.web.WebViewNavigator -import com.multiplatform.webview.web.WebViewState - -@Composable -actual fun PlatformWebView(state: WebViewState, navigator: WebViewNavigator, modifier: Modifier) { - WebView( - state = state, - navigator = navigator, - modifier = modifier - ) -} \ No newline at end of file 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 new file mode 100644 index 00000000..ce5e1db2 --- /dev/null +++ b/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.android.kt @@ -0,0 +1,36 @@ +package dev.datlag.burningseries.ui.screen.initial.series.activate.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import com.multiplatform.webview.web.rememberWebViewNavigator +import com.multiplatform.webview.web.rememberWebViewState +import dev.datlag.burningseries.common.withIOContext +import dev.datlag.burningseries.common.withMainContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive + +@Composable +actual fun WebView(url: String, scrapingJs: String, modifier: Modifier, onScraped: (String) -> Unit) { + val state = rememberWebViewState(url) + val navigator = rememberWebViewNavigator() + + com.multiplatform.webview.web.WebView( + state = state, + modifier = modifier, + navigator = navigator + ) + + LaunchedEffect(navigator) { + withIOContext { + do { + delay(3000) + withMainContext { + navigator.evaluateJavaScript(scrapingJs) { + onScraped(it) + } + } + } while (isActive) + } + } +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/module/NetworkModule.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/module/NetworkModule.kt index 71b2c889..143afa7a 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/module/NetworkModule.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/module/NetworkModule.kt @@ -8,6 +8,7 @@ import dev.datlag.burningseries.network.Firestore import dev.datlag.burningseries.network.JsonBase import dev.datlag.burningseries.network.state.EpisodeStateMachine import dev.datlag.burningseries.network.state.HomeStateMachine +import dev.datlag.burningseries.network.state.SaveStateMachine import dev.datlag.burningseries.network.state.SearchStateMachine import dev.datlag.burningseries.other.StateSaver import io.ktor.client.* @@ -62,5 +63,8 @@ object NetworkModule { bindEagerSingleton { EpisodeStateMachine(instance(), instance(), instanceOrNull(), instanceOrNull(), instanceOrNull()) } + bindEagerSingleton { + SaveStateMachine(instance(), instance(), instanceOrNull(), instanceOrNull(), instanceOrNull()) + } } } \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.kt deleted file mode 100644 index d424cb3a..00000000 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.datlag.burningseries.ui.custom - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.multiplatform.webview.web.WebViewNavigator -import com.multiplatform.webview.web.WebViewState - -@Composable -expect fun PlatformWebView(state: WebViewState, navigator: WebViewNavigator, modifier: Modifier = Modifier) \ No newline at end of file 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 52d83728..4eb50fbf 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 @@ -9,4 +9,5 @@ interface ActivateComponent : Component { val scrapingJs: String fun back() + fun onScraped(data: String) } \ No newline at end of file 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 b446e3f9..6643c080 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 @@ -7,19 +7,11 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect import androidx.compose.ui.Modifier -import com.multiplatform.webview.web.rememberWebViewNavigator -import com.multiplatform.webview.web.rememberWebViewState import dev.icerock.moko.resources.compose.stringResource import dev.datlag.burningseries.SharedRes -import dev.datlag.burningseries.common.withIOContext -import dev.datlag.burningseries.common.withMainContext import dev.datlag.burningseries.model.BSUtil -import dev.datlag.burningseries.ui.custom.PlatformWebView -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive +import dev.datlag.burningseries.ui.screen.initial.series.activate.component.WebView @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -46,10 +38,13 @@ fun ActivateScreen(component: ActivateComponent) { } ) { Box(modifier = Modifier.padding(it)) { - val state = rememberWebViewState(url = BSUtil.getBurningSeriesLink(component.episode.href)) - val navigator = rememberWebViewNavigator() - - PlatformWebView(state, navigator, Modifier.fillMaxSize()) + WebView( + url = BSUtil.getBurningSeriesLink(component.episode.href), + scrapingJs = component.scrapingJs, + modifier = Modifier.fillMaxSize() + ) { data -> + component.onScraped(data) + } } } } \ No newline at end of file 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 3fd2b5c7..709a2d42 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 @@ -6,6 +6,20 @@ import com.arkivanov.essenty.backhandler.BackCallback import dev.datlag.burningseries.model.Series import org.kodein.di.DI import dev.datlag.burningseries.SharedRes +import dev.datlag.burningseries.common.ioScope +import dev.datlag.burningseries.common.launchIO +import dev.datlag.burningseries.model.HosterScraping +import dev.datlag.burningseries.model.common.scopeCatching +import dev.datlag.burningseries.model.state.EpisodeAction +import dev.datlag.burningseries.model.state.EpisodeState +import dev.datlag.burningseries.model.state.SaveAction +import dev.datlag.burningseries.model.state.SaveState +import dev.datlag.burningseries.network.state.EpisodeStateMachine +import dev.datlag.burningseries.network.state.SaveStateMachine +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.serialization.json.Json +import org.kodein.di.instance class ActivateScreenComponent( componentContext: ComponentContext, @@ -15,6 +29,10 @@ class ActivateScreenComponent( ) : ActivateComponent, ComponentContext by componentContext { override val scrapingJs: String = SharedRes.assets.scrape_hoster.readText() + private val saveStateMachine by di.instance() + private val saveState = saveStateMachine.state.stateIn(ioScope(), SharingStarted.Lazily, SaveState.Waiting) + private val json by di.instance() + private val savedData: MutableSet = mutableSetOf() private val backCallback = BackCallback { onGoBack() @@ -22,6 +40,17 @@ class ActivateScreenComponent( init { backHandler.register(backCallback) + + ioScope().launchIO { + saveState.collect { state -> + when (val current = state) { + is SaveState.Saving -> { + println("Saving data: ${current.data}") + } + else -> { } + } + } + } } @Composable @@ -32,4 +61,23 @@ class ActivateScreenComponent( override fun back() { onGoBack() } + + override fun onScraped(data: String) { + val trimmed = data.trim() + if (trimmed.isNotBlank() && !trimmed.equals("null", true) && !trimmed.equals("undefined", true)) { + val converted = scopeCatching { + json.decodeFromString(trimmed) + }.getOrNull() + + if (converted != null) { + if (!savedData.contains(converted.href)) { + ioScope().launchIO { + saveStateMachine.dispatch(SaveAction.Save(converted)) + } + } + + savedData.add(converted.href) + } + } + } } \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.kt new file mode 100644 index 00000000..c3a1d8e1 --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.kt @@ -0,0 +1,7 @@ +package dev.datlag.burningseries.ui.screen.initial.series.activate.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +expect fun WebView(url: String, scrapingJs: String, modifier: Modifier = Modifier, onScraped: (String) -> Unit) \ No newline at end of file diff --git a/app/shared/src/commonMain/resources/MR/assets/scrape_hoster.js b/app/shared/src/commonMain/resources/MR/assets/scrape_hoster.js index d6be2b79..694ef6c9 100644 --- a/app/shared/src/commonMain/resources/MR/assets/scrape_hoster.js +++ b/app/shared/src/commonMain/resources/MR/assets/scrape_hoster.js @@ -1,31 +1,29 @@ -(function() { - const hosterTabs = document.getElementsByClassName("hoster-tabs"); - let activeHoster = null; - if ((hosterTabs !== null && hosterTabs !== undefined) && hosterTabs.length > 0) { - const activeTabs = hosterTabs[0].getElementsByClassName("active"); - if ((activeTabs !== null && activeTabs !== undefined) && activeTabs.length > 0) { - const hosters = activeTabs[0].getElementsByTagName("a"); - if ((hosters !== null && hosters !== undefined) && hosters.length > 0) { - activeHoster = hosters[0].getAttribute("href"); - } +const hosterTabs = document.getElementsByClassName("hoster-tabs"); +let activeHoster = null; +if ((hosterTabs !== null && hosterTabs !== undefined) && hosterTabs.length > 0) { + const activeTabs = hosterTabs[0].getElementsByClassName("active"); + if ((activeTabs !== null && activeTabs !== undefined) && activeTabs.length > 0) { + const hosters = activeTabs[0].getElementsByTagName("a"); + if ((hosters !== null && hosters !== undefined) && hosters.length > 0) { + activeHoster = hosters[0].getAttribute("href"); } } +} - const hosterPlayers = document.getElementsByClassName("hoster-player"); - if ((hosterPlayers !== null && hosterPlayers !== undefined) && hosterPlayers.length > 0) { - const player = hosterPlayers[0]; - const links = player.getElementsByTagName("a"); - if ((links !== null && links !== undefined) && links.length > 0) { - const link = links[0].getAttribute("href"); - return {href: activeHoster, url: link, embed: false}; +const hosterPlayers = document.getElementsByClassName("hoster-player"); +if ((hosterPlayers !== null && hosterPlayers !== undefined) && hosterPlayers.length > 0) { + const player = hosterPlayers[0]; + const links = player.getElementsByTagName("a"); + if ((links !== null && links !== undefined) && links.length > 0) { + const link = links[0].getAttribute("href"); + return JSON.stringify({href: activeHoster, url: link, embed: false}); + } else { + const frames = player.getElementsByTagName("iframe"); + if ((frames !== null && frames !== undefined) && frames.length > 0) { + const link = frames[0].getAttribute("src"); + return JSON.stringify({href: activeHoster, url: link, embed: true}); } else { - const frames = player.getElementsByTagName("iframe"); - if ((frames !== null && frames !== undefined) && frames.length > 0) { - const link = frames[0].getAttribute("src"); - return {href: activeHoster, url: link, embed: true}; - } else { - return null; - } + return null; } } -})(); \ No newline at end of file +} \ No newline at end of file diff --git a/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.desktop.kt b/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.desktop.kt deleted file mode 100644 index 34b8a5f1..00000000 --- a/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/custom/PlatformWebView.desktop.kt +++ /dev/null @@ -1,27 +0,0 @@ -package dev.datlag.burningseries.ui.custom - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import com.multiplatform.webview.web.WebView -import com.multiplatform.webview.web.WebViewNavigator -import com.multiplatform.webview.web.WebViewState -import dev.datlag.burningseries.SharedRes -import dev.datlag.burningseries.other.CEFState -import dev.datlag.burningseries.other.LocalCEFInitialization -import dev.datlag.burningseries.ui.custom.state.BrowserState - -@Composable -actual fun PlatformWebView(state: WebViewState, navigator: WebViewNavigator, modifier: Modifier) { - val cefInitState by LocalCEFInitialization.current - - if (cefInitState is CEFState.INITIALIZED) { - WebView( - state = state, - navigator = navigator, - modifier = modifier - ) - } else { - BrowserState(SharedRes.strings.browser_initializing) - } -} \ 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 new file mode 100644 index 00000000..5e298909 --- /dev/null +++ b/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/activate/component/WebView.desktop.kt @@ -0,0 +1,53 @@ +package dev.datlag.burningseries.ui.screen.initial.series.activate.component + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +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.SwingPanel +import com.multiplatform.webview.web.WebContent +import dev.datlag.burningseries.SharedRes +import dev.datlag.burningseries.common.withIOContext +import dev.datlag.burningseries.common.withMainContext +import dev.datlag.burningseries.other.CEFState +import dev.datlag.burningseries.other.LocalCEFInitialization +import dev.datlag.burningseries.ui.custom.state.BrowserState +import dev.datlag.kcef.KCEF +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive + +@Composable +actual fun WebView(url: String, scrapingJs: String, modifier: Modifier, onScraped: (String) -> Unit) { + val cefInitState by LocalCEFInitialization.current + + if (cefInitState is CEFState.INITIALIZED) { + val client = remember { KCEF.newClientBlocking() } + val browser = remember { client.createBrowser(url) } + SwingPanel( + background = MaterialTheme.colorScheme.background, + factory = { + browser.uiComponent + }, + modifier = modifier + ) + + LaunchedEffect(browser) { + withIOContext { + do { + delay(3000) + withMainContext { + browser.evaluateJavaScript(scrapingJs) { + if (it != null) { + onScraped(it) + } + } + } + } while (isActive) + } + } + } else { + BrowserState(SharedRes.strings.browser_initializing) + } +} \ No newline at end of file diff --git a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/FirestoreDocument.kt b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/FirestoreDocument.kt new file mode 100644 index 00000000..ad96a7f9 --- /dev/null +++ b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/FirestoreDocument.kt @@ -0,0 +1,13 @@ +package dev.datlag.burningseries.model + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@OptIn(ExperimentalSerializationApi::class) +data class FirestoreDocument( + @EncodeDefault(EncodeDefault.Mode.NEVER) @SerialName("name") val name: String? = null, + @SerialName("fields") val fields: Map +) \ No newline at end of file diff --git a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/FirestoreQuery.kt b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/FirestoreQuery.kt index 82f34cc8..4240e938 100644 --- a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/FirestoreQuery.kt +++ b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/FirestoreQuery.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable +@OptIn(ExperimentalSerializationApi::class) data class FirestoreQuery( @SerialName("structuredQuery") val structuredQuery: StructuredQuery ) { diff --git a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/FirestoreQueryResponse.kt b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/FirestoreQueryResponse.kt index 13c6a0e3..a3b1b140 100644 --- a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/FirestoreQueryResponse.kt +++ b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/FirestoreQueryResponse.kt @@ -5,11 +5,5 @@ import kotlinx.serialization.Serializable @Serializable data class FirestoreQueryResponse( - @SerialName("document") val document: Document -) { - - @Serializable - data class Document( - @SerialName("fields") val fields: Map - ) -} \ No newline at end of file + @SerialName("document") val document: FirestoreDocument +) \ No newline at end of file diff --git a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/HosterScraping.kt b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/HosterScraping.kt new file mode 100644 index 00000000..d18f3b5c --- /dev/null +++ b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/HosterScraping.kt @@ -0,0 +1,28 @@ +package dev.datlag.burningseries.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class HosterScraping( + @SerialName("href") val href: String, + @SerialName("url") val url: String, + @SerialName("embed") val embed: Boolean = false +) { + val firestore = Firestore( + id = href, + url = url + ) + + val jsonBase = JsonBaseCaptchaEntry( + url = url, + embed = embed, + broken = false + ) + + @Serializable + data class Firestore( + @SerialName("id") val id: String, + @SerialName("url") val url: String + ) +} diff --git a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/EpisodeState.kt b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/EpisodeState.kt index b4f5f4bb..83b0be6d 100644 --- a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/EpisodeState.kt +++ b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/EpisodeState.kt @@ -1,5 +1,6 @@ package dev.datlag.burningseries.model.state +import dev.datlag.burningseries.model.HosterScraping import dev.datlag.burningseries.model.Series import dev.datlag.burningseries.model.Stream diff --git a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/SaveState.kt b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/SaveState.kt new file mode 100644 index 00000000..67794cf2 --- /dev/null +++ b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/SaveState.kt @@ -0,0 +1,12 @@ +package dev.datlag.burningseries.model.state + +import dev.datlag.burningseries.model.HosterScraping + +sealed interface SaveState { + data object Waiting : SaveState + data class Saving(val data: HosterScraping) : SaveState +} + +sealed interface SaveAction { + data class Save(val data: HosterScraping) : SaveAction +} \ No newline at end of file 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 71399b4b..b626ccda 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,7 +1,9 @@ package dev.datlag.burningseries.network.firebase +import dev.datlag.burningseries.model.HosterScraping import dev.datlag.burningseries.network.Firestore import dev.gitlive.firebase.auth.FirebaseUser +import dev.gitlive.firebase.firestore.DocumentReference import dev.gitlive.firebase.firestore.FirebaseFirestore import dev.gitlive.firebase.firestore.where @@ -15,4 +17,28 @@ actual object FireStore { it.get("url") } } + + actual suspend fun addStream( + firebaseUser: FirebaseUser?, + firestore: FirebaseFirestore?, + firestoreApi: Firestore?, + data: HosterScraping.Firestore + ): Boolean { + if (firestore == null) { + return false + } + + 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 + } } \ No newline at end of file diff --git a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/Firestore.kt b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/Firestore.kt index cc434281..588240fd 100644 --- a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/Firestore.kt +++ b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/Firestore.kt @@ -1,9 +1,7 @@ package dev.datlag.burningseries.network -import de.jensklingenberg.ktorfit.http.Body -import de.jensklingenberg.ktorfit.http.Headers -import de.jensklingenberg.ktorfit.http.POST -import de.jensklingenberg.ktorfit.http.Path +import de.jensklingenberg.ktorfit.http.* +import dev.datlag.burningseries.model.FirestoreDocument import dev.datlag.burningseries.model.FirestoreQuery import dev.datlag.burningseries.model.FirestoreQueryResponse import io.ktor.client.statement.* @@ -17,4 +15,18 @@ interface Firestore { ) @POST("databases/(default)/documents:runQuery") suspend fun query(@Body request: FirestoreQuery): List + + @Headers( + "Content-Type: application/json", + "Accept: application/json" + ) + @PATCH("databases/(default)/documents/{collection}") + suspend fun patch(@Header("Authorization") token: String, @Path("collection") collection: String, @Body request: FirestoreDocument): HttpResponse + + @Headers( + "Content-Type: application/json", + "Accept: application/json" + ) + @POST("databases/(default)/documents/{collection}") + suspend fun create(@Header("Authorization") token: String, @Path("collection") collection: String, @Body request: FirestoreDocument): HttpResponse } \ No newline at end of file diff --git a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.kt b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.kt index 17877773..77059cd5 100644 --- a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.kt +++ b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/firebase/FireStore.kt @@ -1,5 +1,6 @@ package dev.datlag.burningseries.network.firebase +import dev.datlag.burningseries.model.HosterScraping import dev.datlag.burningseries.network.Firestore import dev.gitlive.firebase.auth.FirebaseUser import dev.gitlive.firebase.firestore.FirebaseFirestore @@ -10,4 +11,11 @@ expect object FireStore { firestoreApi: Firestore, idList: List ): List + + suspend fun addStream( + firebaseUser: FirebaseUser?, + firestore: FirebaseFirestore?, + firestoreApi: Firestore?, + data: HosterScraping.Firestore + ): Boolean } \ 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 c390c714..c467da86 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 @@ -37,22 +37,19 @@ class EpisodeStateMachine( private val firestoreApi: Firestore? ) : FlowReduxStateMachine(initialState = EpisodeState.Waiting) { - private var mongoUser: User? = null private val mongoHosterMap: MutableMap> = mutableMapOf() - private var firebaseUser: FirebaseUser? = null - init { spec { inState { onEnterEffect { - if (mongoUser == null) { - mongoUser = suspendCatching { + if (StateSaver.mongoUser == null) { + StateSaver.mongoUser = suspendCatching { app?.login(Credentials.anonymous()) }.getOrNull() } - if (firebaseUser == null) { - firebaseUser = suspendCatching { + if (StateSaver.firebaseUser == null) { + StateSaver.firebaseUser = suspendCatching { Firebase.auth.signInAnonymously().user }.getOrNull() } @@ -67,47 +64,56 @@ class EpisodeStateMachine( val episodeHref = state.snapshot.episode.href val hosterHref = state.snapshot.episode.hosters.map { it.href } - val jsonBaseResults = coroutineScope { - hosterHref.map { async { - try { - val entry = jsonBase.burningSeriesCaptcha(MD5.hexString(it)) - if (!entry.broken) { - entry.url - } else { + val allResults = coroutineScope { + val jsonBaseResults = async { + hosterHref.map { async { + try { + val entry = jsonBase.burningSeriesCaptcha(MD5.hexString(it)) + if (!entry.broken) { + entry.url + } else { + null + } + } catch (ignored: Throwable) { null } - } catch (ignored: Throwable) { - null + } }.awaitAll().filterNotNull() + } + + val mongoHoster = mongoHosterMap[episodeHref] ?: emptyList() + val mongoDBResults = async { + mongoHoster.ifEmpty { + val newList = suspendCatching { + val doc = StateSaver.mongoUser!!.functions.call("query", hosterHref.toTypedArray()) + doc.getArray("result").values.map { it.asDocument().getString("url").value } + }.getOrNull() ?: emptyList() + + mongoHosterMap[episodeHref] = newList + newList } - } }.awaitAll().filterNotNull() - } - val mongoHoster = mongoHosterMap[episodeHref] ?: emptyList() - val mongoDBResults = mongoHoster.ifEmpty { - val newList = suspendCatching { - val doc = mongoUser!!.functions.call("query", hosterHref.toTypedArray()) - doc.getArray("result").values.map { it.asDocument().getString("url").value } - }.getOrNull() ?: emptyList() - - mongoHosterMap[episodeHref] = newList - newList - } + } - val firebaseResults = suspendCatching { - if (firestore != null && firestoreApi != null) { - FireStore.getStreams(firestore, firestoreApi, hosterHref) - } else { - null + val firebaseResults = async { + suspendCatching { + if (firestore != null && firestoreApi != null) { + FireStore.getStreams(firestore, firestoreApi, hosterHref) + } else { + null + } + }.getOrNull() ?: emptyList() } - }.getOrNull() ?: emptyList() - if (jsonBaseResults.isNotEmpty() || mongoDBResults.isNotEmpty()) { + return@coroutineScope setFrom( + jsonBaseResults.await(), + mongoDBResults.await(), + firebaseResults.await() + ) + } + + if (allResults.isNotEmpty()) { state.override { EpisodeState.SuccessHoster( episode = state.snapshot.episode, - results = setFrom( - jsonBaseResults, - mongoDBResults, - firebaseResults - ) + results = allResults ) } } else { state.override { EpisodeState.ErrorHoster(state.snapshot.episode) } 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 new file mode 100644 index 00000000..b5e5c6a0 --- /dev/null +++ b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/SaveStateMachine.kt @@ -0,0 +1,65 @@ +package dev.datlag.burningseries.network.state + +import com.freeletics.flowredux.dsl.FlowReduxStateMachine +import dev.datlag.burningseries.model.common.suspendCatching +import dev.datlag.burningseries.model.state.EpisodeState +import dev.datlag.burningseries.model.state.SaveAction +import dev.datlag.burningseries.model.state.SaveState +import dev.datlag.burningseries.network.Firestore +import dev.datlag.burningseries.network.JsonBase +import dev.datlag.burningseries.network.firebase.FireStore +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.auth.FirebaseUser +import dev.gitlive.firebase.auth.auth +import dev.gitlive.firebase.firestore.FirebaseFirestore +import io.ktor.client.* +import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.Credentials +import io.realm.kotlin.mongodb.User +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@OptIn(ExperimentalCoroutinesApi::class) +class SaveStateMachine( + private val client: HttpClient, + private val jsonBase: JsonBase, + private val app: App?, + private val firestore: FirebaseFirestore?, + private val firestoreApi: Firestore? +) : FlowReduxStateMachine(initialState = SaveState.Waiting) { + + init { + spec { + inState { + onEnterEffect { + if (StateSaver.mongoUser == null) { + StateSaver.mongoUser = suspendCatching { + app?.login(Credentials.anonymous()) + }.getOrNull() + } + if (StateSaver.firebaseUser == null) { + StateSaver.firebaseUser = suspendCatching { + Firebase.auth.signInAnonymously().user + }.getOrNull() + } + } + + on { action, state -> + state.override { SaveState.Saving(action.data) } + } + } + + inState { + onEnter { state -> + val worked = FireStore.addStream( + firebaseUser = StateSaver.firebaseUser, + firestore = firestore, + firestoreApi = firestoreApi, + data = state.snapshot.data.firestore + ) + println("Worked: $worked") + state.noChange() + } + } + } + } +} \ No newline at end of file diff --git a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/StateSaver.kt b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/StateSaver.kt new file mode 100644 index 00000000..fd2e9956 --- /dev/null +++ b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/StateSaver.kt @@ -0,0 +1,10 @@ +package dev.datlag.burningseries.network.state + +import dev.gitlive.firebase.auth.FirebaseUser +import io.realm.kotlin.mongodb.User + +internal data object StateSaver { + + var mongoUser: User? = null + var firebaseUser: FirebaseUser? = null +} \ No newline at end of file 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 71399b4b..e731c7c8 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,5 +1,6 @@ package dev.datlag.burningseries.network.firebase +import dev.datlag.burningseries.model.HosterScraping import dev.datlag.burningseries.network.Firestore import dev.gitlive.firebase.auth.FirebaseUser import dev.gitlive.firebase.firestore.FirebaseFirestore @@ -15,4 +16,28 @@ actual object FireStore { it.get("url") } } + + actual suspend fun addStream( + firebaseUser: FirebaseUser?, + firestore: FirebaseFirestore?, + firestoreApi: Firestore?, + data: HosterScraping.Firestore + ): Boolean { + if (firestore == null) { + return false + } + + 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 + } } \ 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 0cea57eb..e15a329f 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 @@ -1,9 +1,14 @@ 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.client.statement.* +import io.ktor.http.* actual object FireStore { actual suspend fun getStreams( @@ -46,4 +51,74 @@ actual object FireStore { it.document.fields["url"]?.stringValue } } + + 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() + } } \ No newline at end of file