mirror of
https://github.com/tachiyomiorg/tachiyomi-extensions-inspector.git
synced 2024-12-26 16:51:50 +01:00
Merge branch 'db'
This commit is contained in:
commit
38a2c6fc54
@ -15,6 +15,7 @@ compileKotlin.kotlinOptions {
|
|||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
jcenter()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@ -69,6 +70,16 @@ dependencies {
|
|||||||
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
|
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
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")
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
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.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
|
import ir.armor.tachidesk.database.model.ExtensionDataClass
|
||||||
//import kotlinx.coroutines.Dispatchers
|
//import kotlinx.coroutines.Dispatchers
|
||||||
//import kotlinx.coroutines.withContext
|
//import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
@ -75,6 +76,10 @@ internal class ExtensionGithubApi {
|
|||||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getApkUrl(extension: ExtensionDataClass): String {
|
||||||
|
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||||
const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo"
|
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 com.googlecode.dex2jar.tools.Dex2jarCmd
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import io.javalin.Javalin
|
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 kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
class Main {
|
class Main {
|
||||||
companion object {
|
companion object {
|
||||||
const val contentRoot = "/tmp/tachidesk"
|
var lastExtensionCheck: Long = 0
|
||||||
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun downloadAPK(url: String, apkPath: String){
|
fun downloadAPKFile(url: String, apkPath: String) {
|
||||||
val request = Request.Builder().url(url).build()
|
val request = Request.Builder().url(url).build()
|
||||||
val response = NetworkHelper().client.newCall(request).execute();
|
val response = NetworkHelper().client.newCall(request).execute();
|
||||||
|
|
||||||
@ -32,8 +37,8 @@ class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun testExtensionExecution(){
|
fun testExtensionExecution() {
|
||||||
File(contentRoot).mkdirs()
|
File(Config.extensionsRoot).mkdirs()
|
||||||
var sourcePkg = ""
|
var sourcePkg = ""
|
||||||
|
|
||||||
// get list of extensions
|
// get list of extensions
|
||||||
@ -48,20 +53,20 @@ class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val apkFileName = apkToDownload.split("/").last()
|
val apkFileName = apkToDownload.split("/").last()
|
||||||
val apkFilePath = "$contentRoot/$apkFileName"
|
val apkFilePath = "${Config.extensionsRoot}/$apkFileName"
|
||||||
val zipDirPath = apkFilePath.substringBefore(".apk")
|
val zipDirPath = apkFilePath.substringBefore(".apk")
|
||||||
val jarFilePath = "$zipDirPath.jar"
|
val jarFilePath = "$zipDirPath.jar"
|
||||||
val dexFilePath = "$zipDirPath.dex"
|
val dexFilePath = "$zipDirPath.dex"
|
||||||
|
|
||||||
// download apk file
|
// download apk file
|
||||||
downloadAPK(apkToDownload, apkFilePath)
|
downloadAPKFile(apkToDownload, apkFilePath)
|
||||||
|
|
||||||
|
|
||||||
val className = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
|
val className = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
|
||||||
// dex -> jar
|
// dex -> jar
|
||||||
Dex2jarCmd.main(dexFilePath, "-o", jarFilePath, "--force")
|
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 classToLoad = Class.forName(className, true, child)
|
||||||
val instance = classToLoad.newInstance() as HttpSource
|
val instance = classToLoad.newInstance() as HttpSource
|
||||||
val result = instance.fetchPopularManga(1)
|
val result = instance.fetchPopularManga(1)
|
||||||
@ -69,25 +74,145 @@ class Main {
|
|||||||
mangasPage.mangas.forEach {
|
mangasPage.mangas.forEach {
|
||||||
println(it.title)
|
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
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
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)
|
val app = Javalin.create().start(4567)
|
||||||
|
|
||||||
app.before() { ctx ->
|
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 ->
|
app.get("/api/v1/extensions") { ctx ->
|
||||||
runBlocking {
|
ctx.json(getExtensionList())
|
||||||
val api = ExtensionGithubApi()
|
|
||||||
val sources = api.findExtensions()
|
|
||||||
ctx.json(sources)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 { makeStyles } from '@material-ui/core/styles';
|
||||||
import Card from '@material-ui/core/Card';
|
import Card from '@material-ui/core/Card';
|
||||||
import CardContent from '@material-ui/core/CardContent';
|
import CardContent from '@material-ui/core/CardContent';
|
||||||
@ -36,16 +36,24 @@ interface IProps {
|
|||||||
extension: IExtension
|
extension: IExtension
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExtensionCard({
|
export default function ExtensionCard(props: IProps) {
|
||||||
|
const {
|
||||||
extension: {
|
extension: {
|
||||||
name, lang, versionName, iconUrl,
|
name, lang, versionName, iconUrl, installed, apkName,
|
||||||
},
|
},
|
||||||
}: IProps) {
|
} = props;
|
||||||
|
const [installedState, setInstalledState] = useState<string>((installed ? 'installed' : 'install'));
|
||||||
|
|
||||||
const classes = useStyles();
|
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();
|
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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className={classes.root}>
|
<CardContent className={classes.root}>
|
||||||
@ -68,7 +76,7 @@ export default function ExtensionCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button variant="outlined">install</Button>
|
<Button variant="outlined" onClick={() => install()}>{installedState}</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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
|
lang: string
|
||||||
versionName: string
|
versionName: string
|
||||||
iconUrl: string
|
iconUrl: string
|
||||||
|
installed: boolean
|
||||||
|
apkName: string
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user