refactor Main

This commit is contained in:
Aria Moradi 2020-12-25 13:17:41 +03:30
parent bb94e638cb
commit 5597ad66b4
6 changed files with 289 additions and 285 deletions

View File

@ -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;

View File

@ -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>(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<ExtensionDataClass> {
// 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<Extension.Available>
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>(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>(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<SourceDataClass> {
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<String>) {
// 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())
}
}
}
}

View File

@ -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>(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()
}

View File

@ -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<ExtensionDataClass> {
// 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<Extension.Available>
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]
)
}
}
}

View File

@ -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>(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<SourceDataClass> {
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
)
}
}
}

View File

@ -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()
}