mirror of
https://github.com/tachiyomiorg/tachiyomi-extensions-inspector.git
synced 2024-12-26 00:31:49 +01:00
Merge branch 'db'
This commit is contained in:
commit
38a2c6fc54
@ -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")
|
||||
|
@ -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"
|
||||
|
8
server/src/main/kotlin/ir/armor/tachidesk/Config.kt
Normal file
8
server/src/main/kotlin/ir/armor/tachidesk/Config.kt
Normal 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"
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
@ -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
|
||||
//}
|
@ -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>
|
||||
);
|
||||
|
2
webUI/react/src/typings.d.ts
vendored
2
webUI/react/src/typings.d.ts
vendored
@ -3,4 +3,6 @@ interface IExtension {
|
||||
lang: string
|
||||
versionName: string
|
||||
iconUrl: string
|
||||
installed: boolean
|
||||
apkName: string
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user