From 5597ad66b45594831389379b6efdd5274246cb9f Mon Sep 17 00:00:00 2001 From: Aria Moradi Date: Fri, 25 Dec 2020 13:17:41 +0330 Subject: [PATCH] refactor Main --- .../java/ir/armor/tachidesk/APKExtractor.java | 2 +- .../main/kotlin/ir/armor/tachidesk/Main.kt | 290 +----------------- .../kotlin/ir/armor/tachidesk/util/APK.kt | 129 ++++++++ .../ir/armor/tachidesk/util/ExtensionsList.kt | 84 +++++ .../ir/armor/tachidesk/util/SourceList.kt | 55 ++++ .../kotlin/ir/armor/tachidesk/util/Util.kt | 14 + 6 files changed, 289 insertions(+), 285 deletions(-) create mode 100644 server/src/main/kotlin/ir/armor/tachidesk/util/APK.kt create mode 100644 server/src/main/kotlin/ir/armor/tachidesk/util/ExtensionsList.kt create mode 100644 server/src/main/kotlin/ir/armor/tachidesk/util/SourceList.kt create mode 100644 server/src/main/kotlin/ir/armor/tachidesk/util/Util.kt diff --git a/server/src/main/java/ir/armor/tachidesk/APKExtractor.java b/server/src/main/java/ir/armor/tachidesk/APKExtractor.java index 9c848f0..623487d 100644 --- a/server/src/main/java/ir/armor/tachidesk/APKExtractor.java +++ b/server/src/main/java/ir/armor/tachidesk/APKExtractor.java @@ -14,7 +14,7 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; -class APKExtractor { +public class APKExtractor { // decompressXML -- Parse the 'compressed' binary form of Android XML docs // such as for AndroidManifest.xml in .apk files public static int endDocTag = 0x00100101; diff --git a/server/src/main/kotlin/ir/armor/tachidesk/Main.kt b/server/src/main/kotlin/ir/armor/tachidesk/Main.kt index 928844f..1a32739 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/Main.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/Main.kt @@ -1,293 +1,17 @@ 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.SourceFactory -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.online.HttpSource import io.javalin.Javalin -import ir.armor.tachidesk.database.dataclass.ExtensionDataClass -import ir.armor.tachidesk.database.dataclass.SourceDataClass -import ir.armor.tachidesk.database.entity.ExtensionEntity -import ir.armor.tachidesk.database.entity.SourceEntity -import ir.armor.tachidesk.database.makeDataBaseTables -import ir.armor.tachidesk.database.table.* -import kotlinx.coroutines.runBlocking -import okhttp3.Request -import okio.buffer -import okio.sink -import org.jetbrains.exposed.sql.* -import java.io.File -import java.net.URL -import java.net.URLClassLoader -import org.jetbrains.exposed.sql.transactions.transaction +import ir.armor.tachidesk.util.applicationSetup +import ir.armor.tachidesk.util.installAPK +import ir.armor.tachidesk.util.getExtensionList +import ir.armor.tachidesk.util.getSourceList class Main { companion object { - var lastExtensionCheck: Long = 0 - - - @JvmStatic - fun downloadAPKFile(url: String, apkPath: String) { - val request = Request.Builder().url(url).build() - val response = NetworkHelper().client.newCall(request).execute(); - - val downloadedFile = File(apkPath) - val sink = downloadedFile.sink().buffer() - sink.writeAll(response.body!!.source()) - sink.close() - } - - @JvmStatic - fun testExtensionExecution() { - File(Config.extensionsRoot).mkdirs() - var sourcePkg = "" - - // get list of extensions - var apkToDownload: String = "" - runBlocking { - val api = ExtensionGithubApi() - val source = api.findExtensions().first { - api.getApkUrl(it).endsWith("killsixbilliondemons-v1.2.3.apk") - } - apkToDownload = api.getApkUrl(source) - sourcePkg = source.pkgName - } - - val apkFileName = apkToDownload.split("/").last() - val apkFilePath = "${Config.extensionsRoot}/$apkFileName" - val zipDirPath = apkFilePath.substringBefore(".apk") - val jarFilePath = "$zipDirPath.jar" - val dexFilePath = "$zipDirPath.dex" - - // download apk file - downloadAPKFile(apkToDownload, apkFilePath) - - - val className = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath) - // dex -> jar - Dex2jarCmd.main(dexFilePath, "-o", jarFilePath, "--force") - - val child = URLClassLoader(arrayOf(URL("file:$jarFilePath")), this::class.java.classLoader) - val classToLoad = Class.forName(className, true, child) - val instance = classToLoad.newInstance() as HttpSource - val result = instance.fetchPopularManga(1) - val mangasPage = result.toBlocking().first() as MangasPage - mangasPage.mangas.forEach { - println(it.title) - } - } - - 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 extensionRecord = getExtensionList(true).first { it.apkName == apkName } - val fileNameWithoutType = apkName.substringBefore(".apk") - val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType" - - // 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(extensionRecord) - - 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) - println(className) - // dex -> jar - Dex2jarCmd.main(dexFilePath, "-o", jarFilePath, "--force") - - // clean up - File(apkFilePath).delete() - File(dexFilePath).delete() - - // update sources of the extension - val child = URLClassLoader(arrayOf(URL("file:$jarFilePath")), this::class.java.classLoader) - val classToLoad = Class.forName(className, true, child) - val instance = classToLoad.newInstance() - - val extensionId = transaction { - return@transaction ExtensionsTable.select { ExtensionsTable.name eq extensionRecord.name }.first()[ExtensionsTable.id] - } - - if (instance is HttpSource) {// single source - val httpSource = instance as HttpSource - transaction { -// SourceEntity.new { -// sourceId = httpSource.id -// name = httpSource.name -// this.extension = ExtensionEntity.find { ExtensionsTable.name eq extension.name }.first().id -// } - if (SourcesTable.select { SourcesTable.sourceId eq httpSource.id }.count() == 0L) { - SourcesTable.insert { - it[this.sourceId] = httpSource.id - it[name] = httpSource.name - it[this.lang] = httpSource.lang - it[extension] = extensionId - } - } -// println(httpSource.id) -// println(httpSource.name) -// println() - } - - } else { // multi source - val sourceFactory = instance as SourceFactory - transaction { - sourceFactory.createSources().forEachIndexed { index, source -> - val httpSource = source as HttpSource - if (SourcesTable.select { SourcesTable.sourceId eq httpSource.id }.count() == 0L) { - SourcesTable.insert { - it[this.sourceId] = httpSource.id - it[name] = httpSource.name - it[this.lang] = httpSource.lang - it[extension] = extensionId - it[partOfFactorySource] = true - it[positionInFactorySource] = index - } - } -// println(httpSource.id) -// println(httpSource.name) -// println() - } - } - } - - // update extension info - transaction { - ExtensionsTable.update({ ExtensionsTable.name eq extensionRecord.name }) { - it[installed] = true - it[classFQName] = className - } - } - - } - return 201 // we downloaded successfully - } else { - return 302 - } - } - - fun getHttpSource(sourceId: Long): HttpSource { - return transaction { - val sourceRecord = SourceEntity.find { SourcesTable.sourceId eq sourceId }.first() - val extensionId = sourceRecord.extension.id.value - val extensionRecord = ExtensionEntity.get(extensionId) - val apkName = extensionRecord.apkName - val className = extensionRecord.classFQName - val jarName = apkName.substringBefore(".apk") + ".jar" - val jarPath = "${Config.extensionsRoot}/$jarName" - - println(jarPath) - - val child = URLClassLoader(arrayOf(URL("file:$jarPath")), this::class.java.classLoader) - val classToLoad = Class.forName(className, true, child) - val instance = classToLoad.newInstance() - - if (sourceRecord.partOfFactorySource) { - return@transaction (instance as SourceFactory).createSources()[sourceRecord.positionInFactorySource!!] as HttpSource - } else { - return@transaction instance as HttpSource - } - } - } - - fun getSourceList(): List { - return transaction { - return@transaction SourcesTable.selectAll().map { - SourceDataClass( - it[SourcesTable.sourceId], - it[SourcesTable.name], - it[SourcesTable.lang], - ExtensionsTable.select { ExtensionsTable.id eq it[SourcesTable.extension] }.first()[ExtensionsTable.iconUrl], - getHttpSource(it[SourcesTable.sourceId]).supportsLatest - ) - } - } - } - - @JvmStatic fun main(args: Array) { // make sure everything we need exists - File(Config.dataRoot).mkdirs() - File(Config.extensionsRoot).mkdirs() - makeDataBaseTables() + applicationSetup() val app = Javalin.create().start(4567) @@ -305,14 +29,12 @@ class Main { val apkName = ctx.pathParam("apkName") println(apkName) ctx.status( - downloadApk(apkName) + installAPK(apkName) ) } app.get("/api/v1/sources/") { ctx -> ctx.json(getSourceList()) } - - } } } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/util/APK.kt b/server/src/main/kotlin/ir/armor/tachidesk/util/APK.kt new file mode 100644 index 0000000..554af34 --- /dev/null +++ b/server/src/main/kotlin/ir/armor/tachidesk/util/APK.kt @@ -0,0 +1,129 @@ +package ir.armor.tachidesk.util + +import com.googlecode.dex2jar.tools.Dex2jarCmd +import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi +import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.source.SourceFactory +import eu.kanade.tachiyomi.source.online.HttpSource +import ir.armor.tachidesk.APKExtractor +import ir.armor.tachidesk.Config +import ir.armor.tachidesk.database.table.ExtensionsTable +import ir.armor.tachidesk.database.table.SourcesTable +import kotlinx.coroutines.runBlocking +import okhttp3.Request +import okio.buffer +import okio.sink +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update +import java.io.File +import java.net.URL +import java.net.URLClassLoader + +fun installAPK(apkName: String): Int { + val extensionRecord = getExtensionList(true).first { it.apkName == apkName } + val fileNameWithoutType = apkName.substringBefore(".apk") + val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType" + + // 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(extensionRecord) + + 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) + println(className) + // dex -> jar + Dex2jarCmd.main(dexFilePath, "-o", jarFilePath, "--force") + + // clean up + File(apkFilePath).delete() + File(dexFilePath).delete() + + // update sources of the extension + val child = URLClassLoader(arrayOf(URL("file:$jarFilePath")), this::class.java.classLoader) + val classToLoad = Class.forName(className, true, child) + val instance = classToLoad.newInstance() + + val extensionId = transaction { + return@transaction ExtensionsTable.select { ExtensionsTable.name eq extensionRecord.name }.first()[ExtensionsTable.id] + } + + if (instance is HttpSource) {// single source + val httpSource = instance as HttpSource + transaction { +// SourceEntity.new { +// sourceId = httpSource.id +// name = httpSource.name +// this.extension = ExtensionEntity.find { ExtensionsTable.name eq extension.name }.first().id +// } + if (SourcesTable.select { SourcesTable.sourceId eq httpSource.id }.count() == 0L) { + SourcesTable.insert { + it[this.sourceId] = httpSource.id + it[name] = httpSource.name + it[this.lang] = httpSource.lang + it[extension] = extensionId + } + } +// println(httpSource.id) +// println(httpSource.name) +// println() + } + + } else { // multi source + val sourceFactory = instance as SourceFactory + transaction { + sourceFactory.createSources().forEachIndexed { index, source -> + val httpSource = source as HttpSource + if (SourcesTable.select { SourcesTable.sourceId eq httpSource.id }.count() == 0L) { + SourcesTable.insert { + it[this.sourceId] = httpSource.id + it[name] = httpSource.name + it[this.lang] = httpSource.lang + it[extension] = extensionId + it[partOfFactorySource] = true + it[positionInFactorySource] = index + } + } +// println(httpSource.id) +// println(httpSource.name) +// println() + } + } + } + + // update extension info + transaction { + ExtensionsTable.update({ ExtensionsTable.name eq extensionRecord.name }) { + it[installed] = true + it[classFQName] = className + } + } + + } + return 201 // we downloaded successfully + } else { + return 302 + } +} + +private fun downloadAPKFile(url: String, apkPath: String) { + val request = Request.Builder().url(url).build() + val response = NetworkHelper().client.newCall(request).execute(); + + val downloadedFile = File(apkPath) + val sink = downloadedFile.sink().buffer() + sink.writeAll(response.body!!.source()) + sink.close() +} diff --git a/server/src/main/kotlin/ir/armor/tachidesk/util/ExtensionsList.kt b/server/src/main/kotlin/ir/armor/tachidesk/util/ExtensionsList.kt new file mode 100644 index 0000000..e91bfd4 --- /dev/null +++ b/server/src/main/kotlin/ir/armor/tachidesk/util/ExtensionsList.kt @@ -0,0 +1,84 @@ +package ir.armor.tachidesk.util + +import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi +import eu.kanade.tachiyomi.extension.model.Extension +import ir.armor.tachidesk.database.dataclass.ExtensionDataClass +import ir.armor.tachidesk.database.table.ExtensionsTable +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update + +private object Data { + var lastExtensionCheck: Long = 0 +} + +private 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 (Data.lastExtensionCheck + 60 * 1000 < System.currentTimeMillis() || (offline && extensionDatabaseIsEmtpy())) { + println("Getting extensions list from the internet") + Data.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] + ) + } + + } +} diff --git a/server/src/main/kotlin/ir/armor/tachidesk/util/SourceList.kt b/server/src/main/kotlin/ir/armor/tachidesk/util/SourceList.kt new file mode 100644 index 0000000..43f8205 --- /dev/null +++ b/server/src/main/kotlin/ir/armor/tachidesk/util/SourceList.kt @@ -0,0 +1,55 @@ +package ir.armor.tachidesk.util + +import eu.kanade.tachiyomi.source.SourceFactory +import eu.kanade.tachiyomi.source.online.HttpSource +import ir.armor.tachidesk.Config +import ir.armor.tachidesk.Main +import ir.armor.tachidesk.database.dataclass.SourceDataClass +import ir.armor.tachidesk.database.entity.ExtensionEntity +import ir.armor.tachidesk.database.entity.SourceEntity +import ir.armor.tachidesk.database.table.ExtensionsTable +import ir.armor.tachidesk.database.table.SourcesTable +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import java.net.URL +import java.net.URLClassLoader + +fun getHttpSource(sourceId: Long): HttpSource { + return transaction { + val sourceRecord = SourceEntity.find { SourcesTable.sourceId eq sourceId }.first() + val extensionId = sourceRecord.extension.id.value + val extensionRecord = ExtensionEntity.get(extensionId) + val apkName = extensionRecord.apkName + val className = extensionRecord.classFQName + val jarName = apkName.substringBefore(".apk") + ".jar" + val jarPath = "${Config.extensionsRoot}/$jarName" + + println(jarPath) + + val child = URLClassLoader(arrayOf(URL("file:$jarPath")), this::class.java.classLoader) + val classToLoad = Class.forName(className, true, child) + val instance = classToLoad.newInstance() + + if (sourceRecord.partOfFactorySource) { + return@transaction (instance as SourceFactory).createSources()[sourceRecord.positionInFactorySource!!] as HttpSource + } else { + return@transaction instance as HttpSource + } + } +} + +fun getSourceList(): List { + return transaction { + return@transaction SourcesTable.selectAll().map { + SourceDataClass( + it[SourcesTable.sourceId], + it[SourcesTable.name], + it[SourcesTable.lang], + ExtensionsTable.select { ExtensionsTable.id eq it[SourcesTable.extension] }.first()[ExtensionsTable.iconUrl], + getHttpSource(it[SourcesTable.sourceId]).supportsLatest + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/ir/armor/tachidesk/util/Util.kt b/server/src/main/kotlin/ir/armor/tachidesk/util/Util.kt new file mode 100644 index 0000000..99f02d1 --- /dev/null +++ b/server/src/main/kotlin/ir/armor/tachidesk/util/Util.kt @@ -0,0 +1,14 @@ +package ir.armor.tachidesk.util + +import ir.armor.tachidesk.Config +import ir.armor.tachidesk.database.makeDataBaseTables +import java.io.File + +fun applicationSetup() { + // make dirs we need + File(Config.dataRoot).mkdirs() + File(Config.extensionsRoot).mkdirs() + + + makeDataBaseTables() +} \ No newline at end of file