mirror of
https://github.com/tachiyomiorg/tachiyomi-extensions-inspector.git
synced 2024-10-31 22:45:06 +01:00
Add initial testing suit
This commit is contained in:
parent
c8a8ce07e2
commit
c0df7d314b
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,3 +7,5 @@ gradle.properties
|
|||||||
build
|
build
|
||||||
|
|
||||||
server/src/main/resources/react
|
server/src/main/resources/react
|
||||||
|
server/tmp/
|
||||||
|
server/tachiserver-data/
|
@ -53,6 +53,7 @@ configure(projects) {
|
|||||||
val coroutinesVersion = "1.4.2"
|
val coroutinesVersion = "1.4.2"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
||||||
|
|
||||||
|
|
||||||
// Dependency Injection
|
// Dependency Injection
|
||||||
|
@ -80,6 +80,9 @@ dependencies {
|
|||||||
|
|
||||||
// uncomment to test extensions directly
|
// uncomment to test extensions directly
|
||||||
// implementation(fileTree("lib/"))
|
// implementation(fileTree("lib/"))
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
testImplementation(kotlin("test-junit5"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val name = "ir.armor.tachidesk.Main"
|
val name = "ir.armor.tachidesk.Main"
|
||||||
@ -137,6 +140,9 @@ tasks {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
test {
|
||||||
|
useJUnit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
launch4j { //used for windows
|
launch4j { //used for windows
|
||||||
|
@ -39,12 +39,17 @@ import org.jetbrains.exposed.sql.insert
|
|||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
object Extension {
|
object Extension {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
private val dirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
|
||||||
|
|
||||||
data class InstallableAPK(
|
data class InstallableAPK(
|
||||||
val apkFilePath: String,
|
val apkFilePath: String,
|
||||||
@ -58,7 +63,7 @@ object Extension {
|
|||||||
return installAPK {
|
return installAPK {
|
||||||
val apkURL = ExtensionGithubApi.getApkUrl(extensionRecord)
|
val apkURL = ExtensionGithubApi.getApkUrl(extensionRecord)
|
||||||
val apkName = Uri.parse(apkURL).lastPathSegment!!
|
val apkName = Uri.parse(apkURL).lastPathSegment!!
|
||||||
val apkSavePath = "${ApplicationDirs.extensionsRoot}/$apkName"
|
val apkSavePath = "${dirs.extensionsRoot}/$apkName"
|
||||||
// download apk file
|
// download apk file
|
||||||
downloadAPKFile(apkURL, apkSavePath)
|
downloadAPKFile(apkURL, apkSavePath)
|
||||||
|
|
||||||
@ -79,7 +84,7 @@ object Extension {
|
|||||||
if (!isInstalled) {
|
if (!isInstalled) {
|
||||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||||
|
|
||||||
val dirPathWithoutType = "${ApplicationDirs.extensionsRoot}/$fileNameWithoutType"
|
val dirPathWithoutType = "${dirs.extensionsRoot}/$fileNameWithoutType"
|
||||||
val jarFilePath = "$dirPathWithoutType.jar"
|
val jarFilePath = "$dirPathWithoutType.jar"
|
||||||
val dexFilePath = "$dirPathWithoutType.dex"
|
val dexFilePath = "$dirPathWithoutType.dex"
|
||||||
|
|
||||||
@ -193,7 +198,7 @@ object Extension {
|
|||||||
|
|
||||||
val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!! }
|
val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!! }
|
||||||
val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk")
|
val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk")
|
||||||
val jarPath = "${ApplicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
val jarPath = "${dirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
transaction {
|
transaction {
|
||||||
val extensionId = extensionRecord[ExtensionTable.id].value
|
val extensionId = extensionRecord[ExtensionTable.id].value
|
||||||
|
|
||||||
@ -232,7 +237,7 @@ object Extension {
|
|||||||
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||||
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
|
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
|
||||||
|
|
||||||
val saveDir = "${ApplicationDirs.extensionsRoot}/icon"
|
val saveDir = "${dirs.extensionsRoot}/icon"
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, apkName) {
|
return getCachedImageResponse(saveDir, apkName) {
|
||||||
network.client.newCall(
|
network.client.newCall(
|
||||||
|
@ -22,6 +22,9 @@ import ir.armor.tachidesk.server.ApplicationDirs
|
|||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
object Manga {
|
object Manga {
|
||||||
@ -95,9 +98,10 @@ object Manga {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val dirs by DI.global.instance<ApplicationDirs>()
|
||||||
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
val saveDir = ApplicationDirs.thumbnailsRoot
|
val saveDir = dirs.thumbnailsRoot
|
||||||
val fileName = mangaId.toString()
|
val fileName = mangaId.toString()
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, fileName) {
|
return getCachedImageResponse(saveDir, fileName) {
|
||||||
|
@ -21,6 +21,9 @@ import org.jetbrains.exposed.sql.and
|
|||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@ -73,6 +76,7 @@ object Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: rewrite this to match tachiyomi
|
// TODO: rewrite this to match tachiyomi
|
||||||
|
private val dirs by DI.global.instance<ApplicationDirs>()
|
||||||
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||||
@ -88,7 +92,7 @@ object Page {
|
|||||||
val mangaTitle = mangaEntry[MangaTable.title]
|
val mangaTitle = mangaEntry[MangaTable.title]
|
||||||
val sourceName = source.toString()
|
val sourceName = source.toString()
|
||||||
|
|
||||||
val mangaDir = "${ApplicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
val mangaDir = "${dirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
||||||
// make sure dirs exist
|
// make sure dirs exist
|
||||||
File(mangaDir).mkdirs()
|
File(mangaDir).mkdirs()
|
||||||
return mangaDir
|
return mangaDir
|
||||||
|
@ -24,7 +24,7 @@ object Source {
|
|||||||
return transaction {
|
return transaction {
|
||||||
SourceTable.selectAll().map {
|
SourceTable.selectAll().map {
|
||||||
SourceDataClass(
|
SourceDataClass(
|
||||||
it[SourceTable.id].value.toString(),
|
it[SourceTable.id].value,
|
||||||
it[SourceTable.name],
|
it[SourceTable.name],
|
||||||
it[SourceTable.lang],
|
it[SourceTable.lang],
|
||||||
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
|
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
|
||||||
@ -39,7 +39,7 @@ object Source {
|
|||||||
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
|
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
|
||||||
|
|
||||||
SourceDataClass(
|
SourceDataClass(
|
||||||
sourceId.toString(),
|
sourceId,
|
||||||
source?.get(SourceTable.name),
|
source?.get(SourceTable.name),
|
||||||
source?.get(SourceTable.lang),
|
source?.get(SourceTable.lang),
|
||||||
source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
|
source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
|
||||||
|
@ -16,10 +16,14 @@ import ir.armor.tachidesk.model.database.SourceTable
|
|||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
object GetHttpSource {
|
object GetHttpSource {
|
||||||
private val sourceCache = ConcurrentHashMap<Long, HttpSource>()
|
private val sourceCache = ConcurrentHashMap<Long, HttpSource>()
|
||||||
|
private val dirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
|
||||||
fun getHttpSource(sourceId: Long): HttpSource {
|
fun getHttpSource(sourceId: Long): HttpSource {
|
||||||
val cachedResult: HttpSource? = sourceCache[sourceId]
|
val cachedResult: HttpSource? = sourceCache[sourceId]
|
||||||
@ -39,7 +43,7 @@ object GetHttpSource {
|
|||||||
val apkName = extensionRecord[ExtensionTable.apkName]
|
val apkName = extensionRecord[ExtensionTable.apkName]
|
||||||
val className = extensionRecord[ExtensionTable.classFQName]
|
val className = extensionRecord[ExtensionTable.classFQName]
|
||||||
val jarName = apkName.substringBefore(".apk") + ".jar"
|
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||||
val jarPath = "${ApplicationDirs.extensionsRoot}/$jarName"
|
val jarPath = "${dirs.extensionsRoot}/$jarName"
|
||||||
|
|
||||||
when (val instance = loadExtensionSources(jarPath, className)) {
|
when (val instance = loadExtensionSources(jarPath, className)) {
|
||||||
is Source -> listOf(instance)
|
is Source -> listOf(instance)
|
||||||
|
@ -11,6 +11,9 @@ import ir.armor.tachidesk.server.ApplicationDirs
|
|||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import net.dongliu.apk.parser.ApkFile
|
import net.dongliu.apk.parser.ApkFile
|
||||||
import net.dongliu.apk.parser.ApkParsers
|
import net.dongliu.apk.parser.ApkParsers
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
import org.w3c.dom.Element
|
import org.w3c.dom.Element
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
import xyz.nulldev.androidcompat.pm.InstalledPackage.Companion.toList
|
import xyz.nulldev.androidcompat.pm.InstalledPackage.Companion.toList
|
||||||
@ -31,6 +34,7 @@ import javax.xml.parsers.DocumentBuilderFactory
|
|||||||
|
|
||||||
object PackageTools {
|
object PackageTools {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
private val dirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
|
||||||
const val EXTENSION_FEATURE = "tachiyomi.extension"
|
const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||||
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||||
@ -65,7 +69,7 @@ object PackageTools {
|
|||||||
.skipExceptions(false)
|
.skipExceptions(false)
|
||||||
.to(jarFilePath)
|
.to(jarFilePath)
|
||||||
if (handler.hasException()) {
|
if (handler.hasException()) {
|
||||||
val errorFile: Path = File(ApplicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
|
val errorFile: Path = File(dirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
|
||||||
logger.error(
|
logger.error(
|
||||||
"Detail Error Information in File $errorFile\n" +
|
"Detail Error Information in File $errorFile\n" +
|
||||||
"Please report this file to one of following link if possible (any one).\n" +
|
"Please report this file to one of following link if possible (any one).\n" +
|
||||||
|
@ -18,10 +18,14 @@ import ir.armor.tachidesk.server.ApplicationDirs
|
|||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
|
|
||||||
object DBMangaer {
|
object DBMangaer {
|
||||||
val db by lazy {
|
val db by lazy {
|
||||||
Database.connect("jdbc:h2:${ApplicationDirs.dataRoot}/database", "org.h2.Driver")
|
val dirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
Database.connect("jdbc:h2:${dirs.dataRoot}/database", "org.h2.Driver")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ package ir.armor.tachidesk.model.dataclass
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
data class SourceDataClass(
|
data class SourceDataClass(
|
||||||
val id: String,
|
val id: Long,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val lang: String?,
|
val lang: String?,
|
||||||
val iconUrl: String?,
|
val iconUrl: String?,
|
||||||
|
@ -8,6 +8,7 @@ package ir.armor.tachidesk.server
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ch.qos.logback.classic.Level
|
import ch.qos.logback.classic.Level
|
||||||
|
import com.typesafe.config.Config
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import ir.armor.tachidesk.Main
|
import ir.armor.tachidesk.Main
|
||||||
import ir.armor.tachidesk.model.dataclass.makeDataBaseTables
|
import ir.armor.tachidesk.model.dataclass.makeDataBaseTables
|
||||||
@ -15,7 +16,11 @@ import ir.armor.tachidesk.server.util.systemTray
|
|||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import net.harawata.appdirs.AppDirsFactory
|
import net.harawata.appdirs.AppDirsFactory
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.bind
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import org.kodein.di.singleton
|
||||||
|
import org.slf4j.Logger
|
||||||
import xyz.nulldev.androidcompat.AndroidCompat
|
import xyz.nulldev.androidcompat.AndroidCompat
|
||||||
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
||||||
import xyz.nulldev.ts.config.ConfigKodeinModule
|
import xyz.nulldev.ts.config.ConfigKodeinModule
|
||||||
@ -24,43 +29,57 @@ import java.io.File
|
|||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
object ApplicationDirs {
|
class ApplicationDirs(
|
||||||
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!!
|
val dataRoot: String = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
||||||
|
) {
|
||||||
val extensionsRoot = "$dataRoot/extensions"
|
val extensionsRoot = "$dataRoot/extensions"
|
||||||
val thumbnailsRoot = "$dataRoot/thumbnails"
|
val thumbnailsRoot = "$dataRoot/thumbnails"
|
||||||
val mangaRoot = "$dataRoot/manga"
|
val mangaRoot = "$dataRoot/manga"
|
||||||
}
|
}
|
||||||
|
|
||||||
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
|
val serverConfig: ServerConfig by DI.global.instance()
|
||||||
|
|
||||||
val systemTray by lazy { systemTray() }
|
val systemTray by lazy { systemTray() }
|
||||||
|
|
||||||
val androidCompat by lazy { AndroidCompat() }
|
val androidCompat by lazy { AndroidCompat() }
|
||||||
|
|
||||||
fun applicationSetup() {
|
fun applicationSetup(rootDir: String? = null, config: Config = GlobalConfigManager.config) {
|
||||||
// register server config
|
val dirs = if (rootDir != null) {
|
||||||
GlobalConfigManager.registerModule(
|
ApplicationDirs(rootDir)
|
||||||
ServerConfig.register(GlobalConfigManager.config)
|
} else {
|
||||||
)
|
ApplicationDirs()
|
||||||
|
|
||||||
// set application wide logging level
|
|
||||||
if (serverConfig.debugLogsEnabled) {
|
|
||||||
(mu.KotlinLogging.logger(org.slf4j.Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = Level.DEBUG
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// make dirs we need
|
// make dirs we need
|
||||||
listOf(
|
listOf(
|
||||||
ApplicationDirs.dataRoot,
|
dirs.dataRoot,
|
||||||
ApplicationDirs.extensionsRoot,
|
dirs.extensionsRoot,
|
||||||
"${ApplicationDirs.extensionsRoot}/icon",
|
dirs.extensionsRoot + "/icon",
|
||||||
ApplicationDirs.thumbnailsRoot
|
dirs.thumbnailsRoot
|
||||||
).forEach {
|
).forEach {
|
||||||
File(it).mkdirs()
|
File(it).mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Application dirs
|
||||||
|
DI.global.addImport(DI.Module("Server") {
|
||||||
|
bind<ApplicationDirs>() with singleton { dirs }
|
||||||
|
bind<ServerConfig>() with singleton { ServerConfig.register(config) }
|
||||||
|
})
|
||||||
|
// Load config API
|
||||||
|
DI.global.addImport(ConfigKodeinModule().create())
|
||||||
|
// Load Android compatibility dependencies
|
||||||
|
AndroidCompatInitializer().init()
|
||||||
|
// start app
|
||||||
|
androidCompat.startApp(App())
|
||||||
|
|
||||||
|
// set application wide logging level
|
||||||
|
if (serverConfig.debugLogsEnabled) {
|
||||||
|
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = Level.DEBUG
|
||||||
|
}
|
||||||
|
|
||||||
// create conf file if doesn't exist
|
// create conf file if doesn't exist
|
||||||
try {
|
try {
|
||||||
val dataConfFile = File("${ApplicationDirs.dataRoot}/server.conf")
|
val dataConfFile = File("${dirs.dataRoot}/server.conf")
|
||||||
if (!dataConfFile.exists()) {
|
if (!dataConfFile.exists()) {
|
||||||
Main::class.java.getResourceAsStream("/server-reference.conf").use { input ->
|
Main::class.java.getResourceAsStream("/server-reference.conf").use { input ->
|
||||||
dataConfFile.outputStream().use { output ->
|
dataConfFile.outputStream().use { output ->
|
||||||
@ -75,19 +94,13 @@ fun applicationSetup() {
|
|||||||
makeDataBaseTables()
|
makeDataBaseTables()
|
||||||
|
|
||||||
// create system tray
|
// create system tray
|
||||||
if (serverConfig.systemTrayEnabled)
|
if (serverConfig.systemTrayEnabled) {
|
||||||
try {
|
try {
|
||||||
systemTray
|
systemTray
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Load config API
|
|
||||||
DI.global.addImport(ConfigKodeinModule().create())
|
|
||||||
// Load Android compatibility dependencies
|
|
||||||
AndroidCompatInitializer().init()
|
|
||||||
// start app
|
|
||||||
androidCompat.startApp(App())
|
|
||||||
|
|
||||||
// Disable jetty's logging
|
// Disable jetty's logging
|
||||||
System.setProperty("org.eclipse.jetty.util.log.announce", "false")
|
System.setProperty("org.eclipse.jetty.util.log.announce", "false")
|
||||||
|
186
server/src/test/kotlin/ir/armor/tachidesk/TestExtensions.kt
Normal file
186
server/src/test/kotlin/ir/armor/tachidesk/TestExtensions.kt
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
package ir.armor.tachidesk
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import ir.armor.tachidesk.impl.Extension.installExtension
|
||||||
|
import ir.armor.tachidesk.impl.Extension.uninstallExtension
|
||||||
|
import ir.armor.tachidesk.impl.Extension.updateExtension
|
||||||
|
import ir.armor.tachidesk.impl.ExtensionsList.getExtensionList
|
||||||
|
import ir.armor.tachidesk.impl.Source.getSourceList
|
||||||
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import ir.armor.tachidesk.impl.util.awaitSingle
|
||||||
|
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
||||||
|
import ir.armor.tachidesk.server.applicationSetup
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.junit.jupiter.api.BeforeAll
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
import rx.Observable
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
class TestExtensions {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
private lateinit var extensions: List<ExtensionDataClass>
|
||||||
|
private lateinit var sources: List<HttpSource>
|
||||||
|
|
||||||
|
private val mangaToFetch = mutableListOf<Pair<HttpSource, SManga>>()
|
||||||
|
private val failedToFetch = mutableListOf<Pair<HttpSource, Exception>>()
|
||||||
|
private val mangaFailedToFetch = mutableListOf<Triple<HttpSource, SManga, Exception>>()
|
||||||
|
private val chaptersToFetch = mutableListOf<Triple<HttpSource, SManga, SChapter>>()
|
||||||
|
private val chaptersFailedToFetch = mutableListOf<Triple<HttpSource, SManga, Throwable>>()
|
||||||
|
private val chaptersPageListFailedToFetch = mutableListOf<Triple<HttpSource, Pair<SManga, SChapter>, Exception>>()
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
fun setup() {
|
||||||
|
val dataRoot = File("tmp/TestDesk").absolutePath
|
||||||
|
applicationSetup(dataRoot, loadConfigs(dataRoot))
|
||||||
|
setLoggingEnabled(false)
|
||||||
|
runBlocking {
|
||||||
|
extensions = getExtensionList()
|
||||||
|
extensions.forEach {
|
||||||
|
when {
|
||||||
|
it.obsolete -> {
|
||||||
|
uninstallExtension(it.pkgName)
|
||||||
|
}
|
||||||
|
it.hasUpdate -> {
|
||||||
|
updateExtension(it.pkgName)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
installExtension(it.pkgName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sources = getSourceList().map { getHttpSource(it.id) }
|
||||||
|
}
|
||||||
|
setLoggingEnabled(true)
|
||||||
|
File("tmp/TestDesk/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.toUpperCase()} - ${it.id}" })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun runTest() {
|
||||||
|
runBlocking(Dispatchers.Default) {
|
||||||
|
val semaphore = Semaphore(10)
|
||||||
|
sources.mapIndexed { index, source ->
|
||||||
|
async {
|
||||||
|
semaphore.withPermit {
|
||||||
|
logger.info { "$index - Now fetching popular manga from $source" }
|
||||||
|
try {
|
||||||
|
mangaToFetch += source to (source.fetchPopularManga(1)
|
||||||
|
.awaitSingleRepeat().mangas.firstOrNull()
|
||||||
|
?: throw Exception("Source returned no manga"))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn { "Failed to fetch popular manga from $source: ${e.message}" }
|
||||||
|
failedToFetch += source to e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
File("tmp/TestDesk/failedToFetch.txt").writeText(
|
||||||
|
failedToFetch.joinToString("\n") { (source, exception) ->
|
||||||
|
"${source.name} (${source.lang.toUpperCase()}, ${source.id}):" +
|
||||||
|
" ${exception.message}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info { "Now fetching manga info from ${mangaToFetch.size} sources" }
|
||||||
|
|
||||||
|
mangaToFetch.mapIndexed { index, (source, manga) ->
|
||||||
|
async {
|
||||||
|
semaphore.withPermit {
|
||||||
|
logger.info { "$index - Now fetching manga from $source" }
|
||||||
|
try {
|
||||||
|
manga.copyFrom(source.fetchMangaDetails(manga).awaitSingleRepeat())
|
||||||
|
manga.initialized = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn {
|
||||||
|
"Failed to fetch manga info from $source for ${manga.title} (${source.mangaDetailsRequest(manga).url}): ${e.message}"
|
||||||
|
}
|
||||||
|
mangaFailedToFetch += Triple(source, manga, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
File("tmp/TestDesk/MangaFailedToFetch.txt").writeText(
|
||||||
|
mangaFailedToFetch.joinToString("\n") { (source, manga, exception) ->
|
||||||
|
"${source.name} (${source.lang}, ${source.id}):" +
|
||||||
|
" ${manga.title} (${source.mangaDetailsRequest(manga).url}):" +
|
||||||
|
" ${exception.message}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info { "Now fetching manga chapters from ${mangaToFetch.size} sources" }
|
||||||
|
|
||||||
|
mangaToFetch.filter { it.second.initialized }.mapIndexed { index, (source, manga) ->
|
||||||
|
async {
|
||||||
|
semaphore.withPermit {
|
||||||
|
logger.info { "$index - Now fetching manga chapters from $source" }
|
||||||
|
try {
|
||||||
|
chaptersToFetch += Triple(
|
||||||
|
source,
|
||||||
|
manga,
|
||||||
|
source.fetchChapterList(manga).awaitSingleRepeat().firstOrNull() ?: throw Exception("Source returned no chapters")
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn {
|
||||||
|
"Failed to fetch manga chapters from $source for ${manga.title} (${source.mangaDetailsRequest(manga).url}): ${e.message}"
|
||||||
|
}
|
||||||
|
chaptersFailedToFetch += Triple(source, manga, e)
|
||||||
|
} catch (e: NoClassDefFoundError) {
|
||||||
|
logger.warn {
|
||||||
|
"Failed to fetch manga chapters from $source for ${manga.title} (${source.mangaDetailsRequest(manga).url}): ${e.message}"
|
||||||
|
}
|
||||||
|
chaptersFailedToFetch += Triple(source, manga, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
|
||||||
|
File("tmp/TestDesk/ChaptersFailedToFetch.txt").writeText(
|
||||||
|
chaptersFailedToFetch.joinToString("\n") { (source, manga, exception) ->
|
||||||
|
"${source.name} (${source.lang}, ${source.id}):" +
|
||||||
|
" ${manga.title} (${source.mangaDetailsRequest(manga).url}):" +
|
||||||
|
" ${exception.message}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
chaptersToFetch.mapIndexed { index, (source, manga, chapter) ->
|
||||||
|
async {
|
||||||
|
semaphore.withPermit {
|
||||||
|
logger.info { "$index - Now fetching page list from $source" }
|
||||||
|
try {
|
||||||
|
source.fetchPageList(chapter).awaitSingleRepeat()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn {
|
||||||
|
"Failed to fetch manga info from $source for ${manga.title} (${source.mangaDetailsRequest(manga).url}): ${e.message}"
|
||||||
|
}
|
||||||
|
chaptersPageListFailedToFetch += Triple(source, manga to chapter, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
|
||||||
|
File("tmp/TestDesk/ChapterPageListFailedToFetch.txt").writeText(
|
||||||
|
chaptersPageListFailedToFetch.joinToString("\n") { (source, manga, exception) ->
|
||||||
|
"${source.name} (${source.lang}, ${source.id}):" +
|
||||||
|
" ${manga.first.title} (${source.mangaDetailsRequest(manga.first).url}):" +
|
||||||
|
" ${manga.second.name} (${manga.second.url}): ${exception.message}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <T> Observable<T>.awaitSingleRepeat(): T {
|
||||||
|
for (i in 1..2) {
|
||||||
|
try {
|
||||||
|
return awaitSingle()
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
}
|
||||||
|
return awaitSingle()
|
||||||
|
}
|
||||||
|
}
|
44
server/src/test/kotlin/ir/armor/tachidesk/TestUtils.kt
Normal file
44
server/src/test/kotlin/ir/armor/tachidesk/TestUtils.kt
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package ir.armor.tachidesk
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.Level
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import com.typesafe.config.ConfigRenderOptions
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configs
|
||||||
|
*/
|
||||||
|
fun loadConfigs(dataRoot: String): Config {
|
||||||
|
val logger = KotlinLogging.logger {}
|
||||||
|
//Load reference configs
|
||||||
|
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
|
||||||
|
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
|
||||||
|
|
||||||
|
//Load user config
|
||||||
|
val userConfig =
|
||||||
|
File(dataRoot, "server.conf").let {
|
||||||
|
ConfigFactory.parseFile(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val config = ConfigFactory.empty()
|
||||||
|
.withFallback(userConfig)
|
||||||
|
.withFallback(compatConfig)
|
||||||
|
.withFallback(serverConfig)
|
||||||
|
.resolve()
|
||||||
|
|
||||||
|
logger.debug {
|
||||||
|
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLoggingEnabled(enabled: Boolean = true) {
|
||||||
|
val logger = (KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger)
|
||||||
|
logger.level = if (enabled) {
|
||||||
|
Level.DEBUG
|
||||||
|
} else Level.ERROR
|
||||||
|
}
|
13
server/src/test/resources/server-reference.conf
Normal file
13
server/src/test/resources/server-reference.conf
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Server ip and port bindings
|
||||||
|
server.ip = "0.0.0.0"
|
||||||
|
server.port = 4567
|
||||||
|
|
||||||
|
# Socks5 proxy
|
||||||
|
server.socksProxy = false
|
||||||
|
server.socksProxyHost = ""
|
||||||
|
server.socksProxyPort = ""
|
||||||
|
|
||||||
|
# misc
|
||||||
|
server.debugLogsEnabled = true
|
||||||
|
server.systemTrayEnabled = false
|
||||||
|
server.initialOpenInBrowserEnabled = true
|
Loading…
Reference in New Issue
Block a user