Merge branch 'db'

This commit is contained in:
Aria Moradi 2020-12-25 03:08:27 +03:30
commit 38a2c6fc54
9 changed files with 271 additions and 25 deletions

View File

@ -15,6 +15,7 @@ compileKotlin.kotlinOptions {
repositories {
mavenCentral()
jcenter()
}
dependencies {
@ -69,6 +70,16 @@ dependencies {
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
// to get application content root
implementation("net.harawata:appdirs:1.2.0")
// Exposed ORM
val exposed_version = "0.28.1"
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation ("org.xerial:sqlite-jdbc:3.30.1")
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")

View File

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

View File

@ -0,0 +1,8 @@
package ir.armor.tachidesk
import net.harawata.appdirs.AppDirsFactory
object Config {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk",null, null)
val extensionsRoot = "$dataRoot/extensions"
}

View File

@ -2,26 +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.makeDataBaseTables
import ir.armor.tachidesk.database.model.ExtensionDataClass
import ir.armor.tachidesk.database.model.ExtensionsTable
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
class Main {
companion object {
const val contentRoot = "/tmp/tachidesk"
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();
@ -32,8 +37,8 @@ class Main {
}
@JvmStatic
fun testExtensionExecution(){
File(contentRoot).mkdirs()
fun testExtensionExecution() {
File(Config.extensionsRoot).mkdirs()
var sourcePkg = ""
// get list of extensions
@ -48,20 +53,20 @@ 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)
// dex -> jar
Dex2jarCmd.main(dexFilePath, "-o", jarFilePath, "--force")
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarFilePath")), this.javaClass.classLoader)
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)
@ -69,25 +74,145 @@ 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<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 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<String>) {
// 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", "*")
ctx.header("Access-Control-Allow-Origin", "*") // allow the client which is running on another port
}
app.get("/api/v1/extensions") { ctx ->
runBlocking {
val api = ExtensionGithubApi()
val sources = api.findExtensions()
ctx.json(sources)
}
ctx.json(getExtensionList())
}
app.get("/api/v1/extensions/install/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
println(apkName)
ctx.status(
downloadApk(apkName)
)
}
}
}
}

View File

@ -0,0 +1,24 @@
package ir.armor.tachidesk.database
import ir.armor.tachidesk.Config
import ir.armor.tachidesk.database.model.ExtensionsTable
import ir.armor.tachidesk.database.model.SourcesTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
object DBMangaer {
val db by lazy {
Database.connect("jdbc:sqlite:${Config.dataRoot}/database.db", "org.sqlite.JDBC")
}
}
fun makeDataBaseTables() {
// mention db object to connect
DBMangaer.db
transaction {
SchemaUtils.create(ExtensionsTable)
SchemaUtils.create(SourcesTable)
}
}

View File

@ -0,0 +1,37 @@
package ir.armor.tachidesk.database.model
import eu.kanade.tachiyomi.source.Source
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Table
object ExtensionsTable : IntIdTable() {
val name = varchar("name", 128)
val pkgName = varchar("pkg_name", 128)
val versionName = varchar("version_name", 16)
val versionCode = integer("version_code")
val lang = varchar("lang", 5)
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
}
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,
)

View File

@ -0,0 +1,26 @@
package ir.armor.tachidesk.database.model
import org.jetbrains.exposed.dao.Entity
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Table
object SourcesTable : Table() {
val id: Column<Long> = long("id")
val name: Column<String> = varchar("name", 128)
val extension = reference("extension", ExtensionsTable)
override val primaryKey = PrimaryKey(id)
}
//class Source : Entity() {
// companion object : Entity<Source>(SourcesTable)
//
// 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
//}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
@ -36,16 +36,24 @@ interface IProps {
extension: IExtension
}
export default function ExtensionCard({
extension: {
name, lang, versionName, iconUrl,
},
}: IProps) {
export default function ExtensionCard(props: IProps) {
const {
extension: {
name, lang, versionName, iconUrl, installed, apkName,
},
} = props;
const [installedState, setInstalledState] = useState<string>((installed ? 'installed' : 'install'));
const classes = useStyles();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const bull = <span className={classes.bullet}></span>;
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
function install() {
setInstalledState('installing');
fetch(`http://127.0.0.1:4567/api/v1/extensions/install/${apkName}`).then(() => {
setInstalledState('installed');
});
}
return (
<Card>
<CardContent className={classes.root}>
@ -68,7 +76,7 @@ export default function ExtensionCard({
</div>
</div>
<Button variant="outlined">install</Button>
<Button variant="outlined" onClick={() => install()}>{installedState}</Button>
</CardContent>
</Card>
);

View File

@ -3,4 +3,6 @@ interface IExtension {
lang: string
versionName: string
iconUrl: string
installed: boolean
apkName: string
}