diff --git a/app/build.gradle b/app/build.gradle index b1cb3a6b..95b51fad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -199,6 +199,9 @@ dependencies { // Library/extensions searching with Levenshtein distance implementation 'me.xdrop:fuzzywuzzy:1.4.0' + + // Git + implementation 'org.openl.jgit:org.eclipse.jgit:6.2.0.202206071550-openl' } task androidSourcesJar(type: Jar) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 2bc39b54..daffa17d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -11,6 +11,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val malApi = MALApi(0) val aniListApi = AniListApi(0) val openSubtitlesApi = OpenSubtitlesApi(0) + val githubApi = GithubApi(0) val indexSubtitlesApi = IndexSubtitleApi() // used to login via app intent @@ -22,7 +23,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { // this needs init with context and can be accessed in settings val accountManagers get() = listOf( - malApi, aniListApi, openSubtitlesApi, //nginxApi + malApi, aniListApi, openSubtitlesApi, githubApi //nginxApi ) // used for active syncing diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/GithubApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/GithubApi.kt new file mode 100644 index 00000000..53669aca --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/GithubApi.kt @@ -0,0 +1,156 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.nicehttp.RequestBodyTypes +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.transport.URIish +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.attribute.FileAttribute + + +class GithubApi(index: Int) : InAppAuthAPIManager(index){ + override val idPrefix = "Github" + override val name = "Github" + override val icon = R.drawable.ic_github_logo + override val requiresPassword = true + override val createAccountUrl = "https://github.com/settings/tokens/new?description=Cloudstream+Backup&scopes=gist" + + data class GithubOAuthEntity( + var repoUrl: String, + var token: String, + var userName: String, + var userAvatar: String, + var gistUrl: String + ) + companion object { + const val GITHUB_USER_KEY: String = "github_user" // user data like profile + var currentSession: GithubOAuthEntity? = null + } + private fun getAuthKey(): GithubOAuthEntity? { + return getKey(accountId, GITHUB_USER_KEY) + } + + + data class gistsElements ( + @JsonProperty("git_pull_url") val gitUrl: String, + @JsonProperty("url") val gistUrl:String, + @JsonProperty("files") val files: Map, + @JsonProperty("owner") val owner: OwnerData + ) + data class OwnerData( + @JsonProperty("login") val userName: String, + @JsonProperty("avatar_url") val userAvatar : String + ) + data class File ( + @JsonProperty("content") val dataRaw: String? + ) + + private suspend fun initLogin(githubToken: String): Boolean{ + val response = app.get("https://api.github.com/gists", + headers= mapOf( + Pair("Accept" , "application/vnd.github+json"), + Pair("Authorization", "token $githubToken"), + ) + ) + + if (!response.isSuccessful) { return false } + + val repo = tryParseJson>(response.text)?.filter { + it.files.keys.first() == "Cloudstream_Backup_data.txt" + } + + if (repo?.isEmpty() == true){ + val gitresponse = app.post("https://api.github.com/gists", + headers= mapOf( + Pair("Accept" , "application/vnd.github+json"), + Pair("Authorization", "token $githubToken"), + ), + requestBody = """{"description":"Cloudstream private backup gist","public":false,"files":{"Cloudstream_Backup_data.txt":{"content":"initialization"}}}""".toRequestBody( + RequestBodyTypes.JSON.toMediaTypeOrNull())) + if (!gitresponse.isSuccessful) {return false} + tryParseJson(gitresponse.text).let { + setKey(accountId, GITHUB_USER_KEY, GithubOAuthEntity( + token = githubToken, + repoUrl = it?.gitUrl?: run { + return false + }, + userName = it.owner.userName, + userAvatar = it.owner.userAvatar, + gistUrl = it.gistUrl + )) + } + return true + } + else{ + repo?.first().let { + setKey(accountId, GITHUB_USER_KEY, GithubOAuthEntity( + token = githubToken, + repoUrl = it?.gitUrl?: run { + return false + }, + userName = it.owner.userName, + userAvatar = it.owner.userAvatar, + gistUrl = it.gistUrl + )) + return true + } + } + + } + override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { + switchToNewAccount() + val githubToken = data.password ?: throw IllegalArgumentException ("Requires Password") + try { + if (initLogin(githubToken)) { + registerAccount() + return true + } + } catch (e: Exception) { + logError(e) + } + switchToOldAccount() + return false + } + + override fun getLatestLoginData(): InAppAuthAPI.LoginData? { + val current = getAuthKey() ?: return null + return InAppAuthAPI.LoginData(email = current.repoUrl, password = current.token, username = current.userName, server = current.gistUrl) + } + override suspend fun initialize() { + currentSession = getAuthKey() + val repoUrl = currentSession?.repoUrl ?: return + val token = currentSession?.token ?: return + setKey(repoUrl, token) + } + override fun logOut() { + removeKey(accountId, GITHUB_USER_KEY) + removeAccountKeys() + currentSession = getAuthKey() + } + + override fun loginInfo(): AuthAPI.LoginInfo? { + return getAuthKey()?.let { user -> + AuthAPI.LoginInfo( + profilePicture = user.userAvatar, + name = user.userName, + accountIndex = accountIndex, + ) + } + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index bfa65f62..e764f240 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -124,8 +124,8 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi } override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { - val username = data.username ?: throw ErrorLoadingException("Requires Username") - val password = data.password ?: throw ErrorLoadingException("Requires Password") + val username = data.username ?: throw IllegalArgumentException ("Requires Username") + val password = data.password ?: throw IllegalArgumentException ("Requires Password") switchToNewAccount() try { if (initLogin(username, password)) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 2554d6ee..1b6e1e1d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -21,6 +21,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.githubApi import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API @@ -221,6 +222,8 @@ class SettingsAccount : PreferenceFragmentCompat() { R.string.mal_key to malApi, R.string.anilist_key to aniListApi, R.string.opensubtitles_key to openSubtitlesApi, + R.string.github_key to githubApi + ) for ((key, api) in syncApis) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 08c03d6c..a7770fef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -13,23 +13,29 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.module.kotlin.readValue import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.apmap +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.plugins.PLUGINS_KEY import com.lagradost.cloudstream3.plugins.PLUGINS_KEY_LOCAL +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.githubApi import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_SHOULD_UPDATE_LIST import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_TOKEN_KEY import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_UNIXTIME_KEY import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_USER_KEY +import com.lagradost.cloudstream3.syncproviders.providers.GithubApi +import com.lagradost.cloudstream3.syncproviders.providers.GithubApi.Companion.GITHUB_USER_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_CACHED_LIST import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_REFRESH_TOKEN_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_SHOULD_UPDATE_LIST import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_TOKEN_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY -import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson + +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.mapper @@ -38,12 +44,14 @@ import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath import com.lagradost.cloudstream3.utils.VideoDownloadManager.isDownloadDir +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.transport.URIish +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider import java.io.IOException import java.io.PrintWriter import java.lang.System.currentTimeMillis import java.text.SimpleDateFormat import java.util.* - object BackupUtils { /** @@ -66,9 +74,10 @@ object BackupUtils { // The plugins themselves are not backed up PLUGINS_KEY, PLUGINS_KEY_LOCAL, - + GITHUB_USER_KEY, OPEN_SUBTITLES_USER_KEY, "nginx_user", // Nginx user key + ) /** false if blacklisted key */ @@ -283,4 +292,61 @@ object BackupUtils { setKeyRaw(it.key, it.value, isEditingAppSettings) } } -} \ No newline at end of file + + + fun FragmentActivity.backupGithub(){ + val backup = this.getBackup() + ioSafe { + val tmpDir = createTempDir() + val gitUrl = githubApi.getLatestLoginData()?.email ?: throw IllegalArgumentException ("Requires Username") + val token = githubApi.getLatestLoginData()?.password ?: throw IllegalArgumentException ("Requires Username") + val git = Git.cloneRepository() + .setURI(gitUrl) + .setDirectory(tmpDir) + .setTimeout(30) + .setCredentialsProvider( + UsernamePasswordCredentialsProvider(token, "") + ) + .call() + tmpDir.listFiles()?.first { it.name != ".git" }?.writeText(backup.toJson()) + git.add() + .addFilepattern(".") + .call() + git.commit() + .setAll(true) + .setMessage("Update backup") + .call() + git.remoteAdd() + .setName("origin") + .setUri(URIish(gitUrl)) + .call() + git.push() + .setRemote(gitUrl) + .setTimeout(30) + .setCredentialsProvider( + UsernamePasswordCredentialsProvider(token, "") + ) + .call(); + tmpDir.deleteRecursively() + } + showToast( + this, + R.string.backup_success, + Toast.LENGTH_LONG + ) + } + + fun FragmentActivity.restorePromptGithub() = + ioSafe { + val gistUrl = githubApi.getLatestLoginData()?.server ?: throw IllegalAccessException() + val jsondata = app.get(gistUrl).text + val dataraw = parseJson(jsondata ?: "").files.values.first().dataRaw?: throw IllegalAccessException() + val data = parseJson(dataraw) + this@restorePromptGithub.restore( + data, + restoreSettings = true, + restoreDataStore = true + ) + } +} + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 11c90d58..45e5d504 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -465,6 +465,7 @@ anilist_key mal_key opensubtitles_key + github_key nginx_key password123 MyCoolUsername diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index d4bae8c4..754f895c 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -1,27 +1,30 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> + android:key="@string/mal_key" + android:icon="@drawable/mal_logo" /> + android:key="@string/anilist_key" + android:icon="@drawable/ic_anilist_icon" /> - - - + android:key="@string/opensubtitles_key" + android:icon="@drawable/open_subtitles_icon" /> + + + + - - - - - - - - + + + + + + + + \ No newline at end of file