diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt index 8f41ef1d..71593f23 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt @@ -7,6 +7,7 @@ import com.github.salomonbrys.kotson.int import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.util.ExtensionLoader +import ir.armor.tachidesk.database.model.ExtensionDataClass //import kotlinx.coroutines.Dispatchers //import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonArray @@ -75,6 +76,10 @@ internal class ExtensionGithubApi { return "$REPO_URL_PREFIX/apk/${extension.apkName}" } + fun getApkUrl(extension: ExtensionDataClass): String { + return "$REPO_URL_PREFIX/apk/${extension.apkName}" + } + companion object { const val BASE_URL = "https://raw.githubusercontent.com/" const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo" diff --git a/server/src/main/kotlin/ir/armor/tachidesk/Config.kt b/server/src/main/kotlin/ir/armor/tachidesk/Config.kt index 40bdbbd6..c3822e50 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/Config.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/Config.kt @@ -4,4 +4,5 @@ import net.harawata.appdirs.AppDirsFactory object Config { val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk",null, null) + val extensionsRoot = "$dataRoot/extensions" } \ No newline at end of file diff --git a/server/src/main/kotlin/ir/armor/tachidesk/Main.kt b/server/src/main/kotlin/ir/armor/tachidesk/Main.kt index 089dc713..1e229bcf 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/Main.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/Main.kt @@ -2,32 +2,31 @@ package ir.armor.tachidesk import com.googlecode.dex2jar.tools.Dex2jarCmd import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi +import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.online.HttpSource import io.javalin.Javalin -import io.javalin.http.Context -import ir.armor.tachidesk.database.DBMangaer import ir.armor.tachidesk.database.makeDataBaseTables +import ir.armor.tachidesk.database.model.ExtensionDataClass import ir.armor.tachidesk.database.model.ExtensionsTable -import ir.armor.tachidesk.database.model.SourcesTable import kotlinx.coroutines.runBlocking -import net.harawata.appdirs.AppDirsFactory import okhttp3.Request import okio.buffer import okio.sink -import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.* import java.io.File import java.net.URL import java.net.URLClassLoader -import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.transactions.transaction - class Main { companion object { + var lastExtensionCheck: Long = 0 + + @JvmStatic - fun downloadAPK(url: String, apkPath: String){ + fun downloadAPKFile(url: String, apkPath: String) { val request = Request.Builder().url(url).build() val response = NetworkHelper().client.newCall(request).execute(); @@ -38,9 +37,8 @@ class Main { } @JvmStatic - fun testExtensionExecution(){ - val contentRoot = Config.dataRoot + "/extensions" - File(contentRoot).mkdirs() + fun testExtensionExecution() { + File(Config.extensionsRoot).mkdirs() var sourcePkg = "" // get list of extensions @@ -55,13 +53,13 @@ class Main { } val apkFileName = apkToDownload.split("/").last() - val apkFilePath = "$contentRoot/$apkFileName" + val apkFilePath = "${Config.extensionsRoot}/$apkFileName" val zipDirPath = apkFilePath.substringBefore(".apk") val jarFilePath = "$zipDirPath.jar" val dexFilePath = "$zipDirPath.dex" // download apk file - downloadAPK(apkToDownload, apkFilePath) + downloadAPKFile(apkToDownload, apkFilePath) val className = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath) @@ -76,40 +74,144 @@ class Main { mangasPage.mangas.forEach { println(it.title) } -// exitProcess(0) + } + + fun extensionDatabaseIsEmtpy(): Boolean { + return transaction { + return@transaction ExtensionsTable.selectAll().count() == 0L + } + } + + fun getExtensionList(offline: Boolean = false): List { + // update if 60 seconds has passed or requested offline and database is empty + if (lastExtensionCheck + 60 * 1000 < System.currentTimeMillis() || (offline && extensionDatabaseIsEmtpy())) { + println("Getting extensions list from the internet") + lastExtensionCheck = System.currentTimeMillis() + var foundExtensions: List + runBlocking { + val api = ExtensionGithubApi() + foundExtensions = api.findExtensions() + transaction { + foundExtensions.forEach { foundExtension -> + val extensionRecord = ExtensionsTable.select { ExtensionsTable.name eq foundExtension.name }.firstOrNull() + if (extensionRecord != null) { + // update the record + ExtensionsTable.update({ ExtensionsTable.name eq foundExtension.name }) { + it[name] = foundExtension.name + it[pkgName] = foundExtension.pkgName + it[versionName] = foundExtension.versionName + it[versionCode] = foundExtension.versionCode + it[lang] = foundExtension.lang + it[isNsfw] = foundExtension.isNsfw + it[apkName] = foundExtension.apkName + it[iconUrl] = foundExtension.iconUrl + } + } else { + // insert new record + ExtensionsTable.insert { + it[name] = foundExtension.name + it[pkgName] = foundExtension.pkgName + it[versionName] = foundExtension.versionName + it[versionCode] = foundExtension.versionCode + it[lang] = foundExtension.lang + it[isNsfw] = foundExtension.isNsfw + it[apkName] = foundExtension.apkName + it[iconUrl] = foundExtension.iconUrl + } + } + } + } + } + } + + return transaction { + return@transaction ExtensionsTable.selectAll().map { + ExtensionDataClass( + it[ExtensionsTable.name], + it[ExtensionsTable.pkgName], + it[ExtensionsTable.versionName], + it[ExtensionsTable.versionCode], + it[ExtensionsTable.lang], + it[ExtensionsTable.isNsfw], + it[ExtensionsTable.apkName], + it[ExtensionsTable.iconUrl], + it[ExtensionsTable.installed], + it[ExtensionsTable.classFQName] + ) + } + + } + } + + fun downloadApk(apkName: String): Int { + val extension = getExtensionList(true).first { it.apkName == apkName } + val fileNameWithoutType = apkName.substringBefore(".apk") + val dirPathWithoutType = "${Config.extensionsRoot}/$apkName" + + // check if we don't have the dex file already downloaded + val dexPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar" + if (!File(dexPath).exists()) { + runBlocking { + val api = ExtensionGithubApi() + val apkToDownload = api.getApkUrl(extension) + + val apkFilePath = "$dirPathWithoutType.apk" + val jarFilePath = "$dirPathWithoutType.jar" + val dexFilePath = "$dirPathWithoutType.dex" + + // download apk file + downloadAPKFile(apkToDownload, apkFilePath) + + + val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath) + + // dex -> jar + Dex2jarCmd.main(dexFilePath, "-o", jarFilePath, "--force") + + File(apkFilePath).delete() + + transaction { + ExtensionsTable.update({ ExtensionsTable.name eq extension.name }) { + it[installed] = true + it[classFQName] = className + } + } + + } + return 201 // we downloaded successfully + } + else { + return 302 + } } @JvmStatic fun main(args: Array) { - // make sure data everything we need exists + // make sure everything we need exists File(Config.dataRoot).mkdirs() + File(Config.extensionsRoot).mkdirs() makeDataBaseTables() -// val app = Javalin.create().start(4567) -// -// app.before() { ctx -> -// ctx.header("Access-Control-Allow-Origin", "*") -// } -// -// app.get("/api/v1/extensions") { ctx -> -// runBlocking { -// val api = ExtensionGithubApi() -// val sources = api.findExtensions() -// ctx.json(sources) -// } -// } -// -// app.get("/api/v1/extensions/install/:extensionURL") { ctx -> -// ctx.pathParam("extensionURL") -// }) + val app = Javalin.create().start(4567) + + app.before() { ctx -> + ctx.header("Access-Control-Allow-Origin", "*") // allow the client which is running on another port + } + + app.get("/api/v1/extensions") { ctx -> + ctx.json(getExtensionList()) + } -// ExtensionTable.new { -// name = "khar" -// pkgName = "eu.khar" -// } + app.get("/api/v1/extensions/install/:apkName") { ctx -> + val apkName = ctx.pathParam("apkName") + ctx.status( + downloadApk(apkName) + ) + } + } } } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/database/model/ExtensionTable.kt b/server/src/main/kotlin/ir/armor/tachidesk/database/model/ExtensionTable.kt index 646e0784..9cd3a055 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/database/model/ExtensionTable.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/database/model/ExtensionTable.kt @@ -15,16 +15,23 @@ object ExtensionsTable : IntIdTable() { val versionName = varchar("version_name", 16) val versionCode = integer("version_code") val lang = varchar("lang", 5) - val isNsfw = bool("is_nsfw") + val isNsfw = bool("is_nsfw") + val apkName = varchar("apk_name", 1024) + val iconUrl = varchar("icon_url", 2048) + + val installed = bool("installed").default(false) + val classFQName = varchar("class_name", 256).default("") // fully qualified name } -//class Extension(id: EntityID) : IntEntity(id) { -// companion object : IntEntityClass(ExtensionsTable) -// -// val name by ExtensionsTable.name -// val pkgName by ExtensionsTable.pkgName -// val versionName by ExtensionsTable.versionName -// val versionCode by ExtensionsTable.versionCode -// val lang by ExtensionsTable.lang -// val isNsfw by ExtensionsTable.isNsfw -//} +data class ExtensionDataClass( + val name: String, + val pkgName: String, + val versionName: String, + val versionCode: Int, + val lang: String, + val isNsfw: Boolean, + val apkName: String, + val iconUrl : String, + val installed: Boolean, + val classFQName: String, +) diff --git a/server/src/main/kotlin/ir/armor/tachidesk/extension/Extension.kt b/server/src/main/kotlin/ir/armor/tachidesk/extension/Extension.kt deleted file mode 100644 index 9d30482b..00000000 --- a/server/src/main/kotlin/ir/armor/tachidesk/extension/Extension.kt +++ /dev/null @@ -1,4 +0,0 @@ -package ir.armor.tachidesk.extension - -class Extension { -} \ No newline at end of file