let's not polute the namespace together

This commit is contained in:
Aria Moradi 2021-03-30 21:10:41 +04:30
parent 90ae180b3e
commit 5656016700
16 changed files with 833 additions and 834 deletions

View File

@ -7,8 +7,8 @@ package ir.armor.tachidesk
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* 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 ir.armor.tachidesk.server.JavalinSetup.javalinSetup
import ir.armor.tachidesk.server.applicationSetup import ir.armor.tachidesk.server.applicationSetup
import ir.armor.tachidesk.server.javalinSetup
class Main { class Main {
companion object { companion object {

View File

@ -45,34 +45,34 @@ object Category {
} }
/** /**
* Move the category from position `from` to `to` * Move the category from position `from` to `to`
*/ */
fun reorderCategory(categoryId: Int, from: Int, to: Int) { fun reorderCategory(categoryId: Int, from: Int, to: Int) {
transaction { transaction {
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList() val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
categories.add(to - 1, categories.removeAt(from - 1)) categories.add(to - 1, categories.removeAt(from - 1))
categories.forEachIndexed { index, cat -> categories.forEachIndexed { index, cat ->
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) { CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
it[CategoryTable.order] = index + 1 it[CategoryTable.order] = index + 1
}
}
}
}
fun removeCategory(categoryId: Int) {
transaction {
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
}
}
fun getCategoryList(): List<CategoryDataClass> {
return transaction {
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
} }
} }
} }
} }
fun removeCategory(categoryId: Int) {
transaction {
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
}
}
fun getCategoryList(): List<CategoryDataClass> {
return transaction {
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
}
}
}
}

View File

@ -22,52 +22,51 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
object CategoryManga { object CategoryManga {
fun addMangaToCategory(mangaId: Int, categoryId: Int) { fun addMangaToCategory(mangaId: Int, categoryId: Int) {
transaction { transaction {
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) { if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
CategoryMangaTable.insert { CategoryMangaTable.insert {
it[CategoryMangaTable.category] = categoryId it[CategoryMangaTable.category] = categoryId
it[CategoryMangaTable.manga] = mangaId it[CategoryMangaTable.manga] = mangaId
} }
MangaTable.update({ MangaTable.id eq mangaId }) { MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.defaultCategory] = false it[MangaTable.defaultCategory] = false
}
} }
} }
} }
}
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) { fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
transaction { transaction {
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) } CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) { if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
MangaTable.update({ MangaTable.id eq mangaId }) { MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.defaultCategory] = true it[MangaTable.defaultCategory] = true
}
} }
} }
} }
}
/** /**
* list of mangas that belong to a category * list of mangas that belong to a category
*/ */
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> { fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
return transaction { return transaction {
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map { CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
MangaTable.toDataClass(it) MangaTable.toDataClass(it)
}
} }
} }
}
/** /**
* list of categories that a manga belongs to * list of categories that a manga belongs to
*/ */
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> { fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
return transaction { return transaction {
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map { CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it) CategoryTable.toDataClass(it)
}
} }
} }
} }
}

View File

@ -29,10 +29,10 @@ object Chapter {
val source = getHttpSource(mangaDetails.sourceId.toLong()) val source = getHttpSource(mangaDetails.sourceId.toLong())
val chapterList = source.fetchChapterList( val chapterList = source.fetchChapterList(
SManga.create().apply { SManga.create().apply {
title = mangaDetails.title title = mangaDetails.title
url = mangaDetails.url url = mangaDetails.url
} }
).toBlocking().first() ).toBlocking().first()
val chapterCount = chapterList.count() val chapterCount = chapterList.count()
@ -72,15 +72,15 @@ object Chapter {
chapterList.mapIndexed { index, it -> chapterList.mapIndexed { index, it ->
ChapterDataClass( ChapterDataClass(
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value, ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
it.url, it.url,
it.name, it.name,
it.date_upload, it.date_upload,
it.chapter_number, it.chapter_number,
it.scanlator, it.scanlator,
mangaId, mangaId,
chapterCount - index, chapterCount - index,
chapterCount chapterCount
) )
} }
} }
@ -95,27 +95,27 @@ object Chapter {
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val pageList = source.fetchPageList( val pageList = source.fetchPageList(
SChapter.create().apply { SChapter.create().apply {
url = chapterEntry[ChapterTable.url] url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name] name = chapterEntry[ChapterTable.name]
} }
).toBlocking().first() ).toBlocking().first()
val chapterId = chapterEntry[ChapterTable.id].value val chapterId = chapterEntry[ChapterTable.id].value
val chapterCount = transaction { ChapterTable.selectAll().count() } val chapterCount = transaction { ChapterTable.selectAll().count() }
val chapter = ChapterDataClass( val chapter = ChapterDataClass(
chapterId, chapterId,
chapterEntry[ChapterTable.url], chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name], chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload], chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number], chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator], chapterEntry[ChapterTable.scanlator],
mangaId, mangaId,
chapterEntry[ChapterTable.chapterIndex], chapterEntry[ChapterTable.chapterIndex],
chapterCount.toInt(), chapterCount.toInt(),
pageList.count() pageList.count()
) )
pageList.forEach { page -> pageList.forEach { page ->

View File

@ -16,10 +16,10 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass
import ir.armor.tachidesk.impl.util.APKExtractor
import ir.armor.tachidesk.model.database.ExtensionTable import ir.armor.tachidesk.model.database.ExtensionTable
import ir.armor.tachidesk.model.database.SourceTable import ir.armor.tachidesk.model.database.SourceTable
import ir.armor.tachidesk.impl.util.APKExtractor import ir.armor.tachidesk.server.ApplicationDirs
import ir.armor.tachidesk.server.applicationDirs
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mu.KotlinLogging import mu.KotlinLogging
import okhttp3.Request import okhttp3.Request
@ -39,206 +39,206 @@ import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
object Extension { object Extension {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
/** /**
* Convert dex to jar, a wrapper for the dex2jar library * Convert dex to jar, a wrapper for the dex2jar library
*/ */
private fun dex2jar(dexFile: String, jarFile: String, fileNameWithoutType: String) { private fun dex2jar(dexFile: String, jarFile: String, fileNameWithoutType: String) {
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine // adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java // source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
val jarFilePath = File(jarFile).toPath() val jarFilePath = File(jarFile).toPath()
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath())) val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
val handler = BaksmaliBaseDexExceptionHandler() val handler = BaksmaliBaseDexExceptionHandler()
Dex2jar Dex2jar
.from(reader) .from(reader)
.withExceptionHandler(handler) .withExceptionHandler(handler)
.reUseReg(false) .reUseReg(false)
.topoLogicalSort() .topoLogicalSort()
.skipDebug(true) .skipDebug(true)
.optimizeSynchronized(false) .optimizeSynchronized(false)
.printIR(false) .printIR(false)
.noCode(false) .noCode(false)
.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(ApplicationDirs.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" +
" https://sourceforge.net/p/dex2jar/tickets/\n" + " https://sourceforge.net/p/dex2jar/tickets/\n" +
" https://bitbucket.org/pxb1988/dex2jar/issues\n" + " https://bitbucket.org/pxb1988/dex2jar/issues\n" +
" https://github.com/pxb1988/dex2jar/issues\n" + " https://github.com/pxb1988/dex2jar/issues\n" +
" dex2jar@googlegroups.com" " dex2jar@googlegroups.com"
) )
handler.dump(errorFile, emptyArray<String>()) handler.dump(errorFile, emptyArray<String>())
}
} }
}
/** /**
* loads the extension main class called $className from the jar located at $jarPath * loads the extension main class called $className from the jar located at $jarPath
* It may return an instance of HttpSource or SourceFactory depending on the extension. * It may return an instance of HttpSource or SourceFactory depending on the extension.
*/ */
fun loadExtensionInstance(jarPath: String, className: String): Any { fun loadExtensionInstance(jarPath: String, className: String): Any {
val classLoader = URLClassLoader(arrayOf<URL>(URL("file:$jarPath"))) val classLoader = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")))
val classToLoad = Class.forName(className, true, classLoader) val classToLoad = Class.forName(className, true, classLoader)
return classToLoad.getDeclaredConstructor().newInstance() return classToLoad.getDeclaredConstructor().newInstance()
} }
fun installExtension(pkgName: String): Int { fun installExtension(pkgName: String): Int {
logger.debug("Installing $pkgName") logger.debug("Installing $pkgName")
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName } val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
val fileNameWithoutType = extensionRecord.apkName.substringBefore(".apk") val fileNameWithoutType = extensionRecord.apkName.substringBefore(".apk")
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType" val dirPathWithoutType = "${ApplicationDirs.extensionsRoot}/$fileNameWithoutType"
// check if we don't have the dex file already downloaded // check if we don't have the dex file already downloaded
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar" val jarPath = "${ApplicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
if (!File(jarPath).exists()) { if (!File(jarPath).exists()) {
runBlocking { runBlocking {
val apkToDownload = ExtensionGithubApi.getApkUrl(extensionRecord) val apkToDownload = ExtensionGithubApi.getApkUrl(extensionRecord)
val apkFilePath = "$dirPathWithoutType.apk" val apkFilePath = "$dirPathWithoutType.apk"
val jarFilePath = "$dirPathWithoutType.jar" val jarFilePath = "$dirPathWithoutType.jar"
val dexFilePath = "$dirPathWithoutType.dex" val dexFilePath = "$dirPathWithoutType.dex"
// download apk file // download apk file
downloadAPKFile(apkToDownload, apkFilePath) downloadAPKFile(apkToDownload, apkFilePath)
val className: String = APKExtractor.extractDexAndReadClassname(apkFilePath, dexFilePath) val className: String = APKExtractor.extractDexAndReadClassname(apkFilePath, dexFilePath)
logger.debug(className) logger.debug(className)
// dex -> jar // dex -> jar
dex2jar(dexFilePath, jarFilePath, fileNameWithoutType) dex2jar(dexFilePath, jarFilePath, fileNameWithoutType)
// clean up // clean up
File(apkFilePath).delete() File(apkFilePath).delete()
File(dexFilePath).delete() File(dexFilePath).delete()
// update sources of the extension // update sources of the extension
val instance = loadExtensionInstance(jarFilePath, className) val instance = loadExtensionInstance(jarFilePath, className)
val extensionId = transaction { val extensionId = transaction {
ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.firstOrNull()!![ExtensionTable.id] ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.firstOrNull()!![ExtensionTable.id]
}
when (instance) {
is HttpSource -> { // single source
transaction {
if (SourceTable.select { SourceTable.id eq instance.id }.count() == 0L) {
SourceTable.insert {
it[this.id] = instance.id
it[name] = instance.name
it[this.lang] = instance.lang
it[extension] = extensionId
}
}
logger.debug("Installed source ${instance.name} with id ${instance.id}")
}
} }
is SourceFactory -> { // theme source or multi lang
transaction { when (instance) {
instance.createSources().forEachIndexed { index, source -> is HttpSource -> { // single source
val httpSource = source as HttpSource transaction {
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) { if (SourceTable.select { SourceTable.id eq instance.id }.count() == 0L) {
SourceTable.insert { SourceTable.insert {
it[this.id] = httpSource.id it[this.id] = instance.id
it[name] = httpSource.name it[name] = instance.name
it[this.lang] = httpSource.lang it[this.lang] = instance.lang
it[extension] = extensionId it[extension] = extensionId
it[partOfFactorySource] = true
} }
} }
logger.debug("Installed source ${httpSource.name} with id:${httpSource.id}") logger.debug("Installed source ${instance.name} with id ${instance.id}")
} }
} }
is SourceFactory -> { // theme source or multi lang
transaction {
instance.createSources().forEachIndexed { index, source ->
val httpSource = source as HttpSource
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
SourceTable.insert {
it[this.id] = httpSource.id
it[name] = httpSource.name
it[this.lang] = httpSource.lang
it[extension] = extensionId
it[partOfFactorySource] = true
}
}
logger.debug("Installed source ${httpSource.name} with id:${httpSource.id}")
}
}
}
else -> {
throw RuntimeException("Extension content is unexpected")
}
} }
else -> {
throw RuntimeException("Extension content is unexpected")
}
}
// update extension info // update extension info
transaction { transaction {
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) { ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
it[isInstalled] = true it[isInstalled] = true
it[classFQName] = className it[classFQName] = className
}
} }
} }
return 201 // we downloaded successfully
} else {
return 302
} }
return 201 // we downloaded successfully
} else {
return 302
} }
}
val networkHelper: NetworkHelper by injectLazy() val networkHelper: NetworkHelper by injectLazy()
private fun downloadAPKFile(url: String, apkPath: String) { private 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()
val downloadedFile = File(apkPath) val downloadedFile = File(apkPath)
val sink = downloadedFile.sink().buffer() val sink = downloadedFile.sink().buffer()
sink.writeAll(response.body!!.source()) sink.writeAll(response.body!!.source())
sink.close() sink.close()
} }
fun uninstallExtension(pkgName: String) { fun uninstallExtension(pkgName: String) {
logger.debug("Uninstalling $pkgName") logger.debug("Uninstalling $pkgName")
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 = "${ApplicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
transaction { transaction {
val extensionId = extensionRecord[ExtensionTable.id].value val extensionId = extensionRecord[ExtensionTable.id].value
SourceTable.deleteWhere { SourceTable.extension eq extensionId } SourceTable.deleteWhere { SourceTable.extension eq extensionId }
if (extensionRecord[ExtensionTable.isObsolete]) if (extensionRecord[ExtensionTable.isObsolete])
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName } ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
else else
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
it[isInstalled] = false
}
}
if (File(jarPath).exists()) {
File(jarPath).delete()
}
}
fun updateExtension(pkgName: String): Int {
val targetExtension = ExtensionsList.updateMap.remove(pkgName)!!
uninstallExtension(pkgName)
transaction {
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) { ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
it[isInstalled] = false it[name] = targetExtension.name
it[versionName] = targetExtension.versionName
it[versionCode] = targetExtension.versionCode
it[lang] = targetExtension.lang
it[isNsfw] = targetExtension.isNsfw
it[apkName] = targetExtension.apkName
it[iconUrl] = targetExtension.iconUrl
it[hasUpdate] = false
} }
}
return installExtension(pkgName)
} }
if (File(jarPath).exists()) { val network: NetworkHelper by injectLazy()
File(jarPath).delete()
}
}
fun updateExtension(pkgName: String): Int { fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val targetExtension = ExtensionsList.updateMap.remove(pkgName)!! val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
uninstallExtension(pkgName)
transaction { val saveDir = "${ApplicationDirs.extensionsRoot}/icon"
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
it[name] = targetExtension.name return getCachedImageResponse(saveDir, apkName) {
it[versionName] = targetExtension.versionName network.client.newCall(
it[versionCode] = targetExtension.versionCode GET(iconUrl)
it[lang] = targetExtension.lang ).execute()
it[isNsfw] = targetExtension.isNsfw
it[apkName] = targetExtension.apkName
it[iconUrl] = targetExtension.iconUrl
it[hasUpdate] = false
} }
} }
return installExtension(pkgName)
}
val network: NetworkHelper by injectLazy() fun getExtensionIconUrl(apkName: String): String {
return "/api/v1/extension/icon/$apkName"
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
val saveDir = "${applicationDirs.extensionsRoot}/icon"
return getCachedImageResponse(saveDir, apkName) {
network.client.newCall(
GET(iconUrl)
).execute()
} }
} }
fun getExtensionIconUrl(apkName: String): String {
return "/api/v1/extension/icon/$apkName"
}
}

View File

@ -23,75 +23,86 @@ import org.jetbrains.exposed.sql.update
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
object ExtensionsList { object ExtensionsList {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
var lastUpdateCheck: Long = 0 var lastUpdateCheck: Long = 0
var updateMap = ConcurrentHashMap<String, Extension.Available>() var updateMap = ConcurrentHashMap<String, Extension.Available>()
// const val ExtensionUpdateDelayTime = 60 * 1000 // 60,000 milliseconds = 60 seconds // const val ExtensionUpdateDelayTime = 60 * 1000 // 60,000 milliseconds = 60 seconds
const val ExtensionUpdateDelayTime = 0 const val ExtensionUpdateDelayTime = 0
fun getExtensionList(): List<ExtensionDataClass> { fun getExtensionList(): List<ExtensionDataClass> {
// update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty // update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty
if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) { if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) {
logger.debug("Getting extensions list from the internet") logger.debug("Getting extensions list from the internet")
lastUpdateCheck = System.currentTimeMillis() lastUpdateCheck = System.currentTimeMillis()
runBlocking { runBlocking {
val foundExtensions = ExtensionGithubApi.findExtensions() val foundExtensions = ExtensionGithubApi.findExtensions()
updateExtensionDatabase(foundExtensions) updateExtensionDatabase(foundExtensions)
}
} else {
logger.debug("used cached extension list")
} }
} else {
logger.debug("used cached extension list") return extensionTableAsDataClass()
} }
return extensionTableAsDataClass() fun extensionTableAsDataClass() = transaction {
} ExtensionTable.selectAll().map {
ExtensionDataClass(
fun extensionTableAsDataClass() = transaction { it[ExtensionTable.name],
ExtensionTable.selectAll().map { it[ExtensionTable.pkgName],
ExtensionDataClass( it[ExtensionTable.versionName],
it[ExtensionTable.name], it[ExtensionTable.versionCode],
it[ExtensionTable.pkgName], it[ExtensionTable.lang],
it[ExtensionTable.versionName], it[ExtensionTable.isNsfw],
it[ExtensionTable.versionCode], it[ExtensionTable.apkName],
it[ExtensionTable.lang], getExtensionIconUrl(it[ExtensionTable.apkName]),
it[ExtensionTable.isNsfw], it[ExtensionTable.isInstalled],
it[ExtensionTable.apkName], it[ExtensionTable.hasUpdate],
getExtensionIconUrl(it[ExtensionTable.apkName]), it[ExtensionTable.isObsolete],
it[ExtensionTable.isInstalled], )
it[ExtensionTable.hasUpdate], }
it[ExtensionTable.isObsolete],
)
} }
}
private fun updateExtensionDatabase(foundExtensions: List<Extension.Available>) { private fun updateExtensionDatabase(foundExtensions: List<Extension.Available>) {
transaction { transaction {
foundExtensions.forEach { foundExtension -> foundExtensions.forEach { foundExtension ->
val extensionRecord = ExtensionTable.select { ExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull() val extensionRecord = ExtensionTable.select { ExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
if (extensionRecord != null) { if (extensionRecord != null) {
if (extensionRecord[ExtensionTable.isInstalled]) { if (extensionRecord[ExtensionTable.isInstalled]) {
if (foundExtension.versionCode > extensionRecord[ExtensionTable.versionCode]) { if (foundExtension.versionCode > extensionRecord[ExtensionTable.versionCode]) {
// there is an update // there is an update
ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) { ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) {
it[hasUpdate] = true it[hasUpdate] = true
} }
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension) updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
} else if (foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode]) { } else if (foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode]) {
// some how the user installed an invalid version // some how the user installed an invalid version
ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) { ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) {
it[isObsolete] = true it[isObsolete] = true
}
} else {
// the two are equal
// NOOP
} }
} else { } else {
// the two are equal // extension is not installed so we can overwrite the data without a care
// NOOP ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) {
it[name] = foundExtension.name
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 { } else {
// extension is not installed so we can overwrite the data without a care // insert new record
ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) { ExtensionTable.insert {
it[name] = foundExtension.name it[name] = foundExtension.name
it[pkgName] = foundExtension.pkgName
it[versionName] = foundExtension.versionName it[versionName] = foundExtension.versionName
it[versionCode] = foundExtension.versionCode it[versionCode] = foundExtension.versionCode
it[lang] = foundExtension.lang it[lang] = foundExtension.lang
@ -100,37 +111,24 @@ private fun updateExtensionDatabase(foundExtensions: List<Extension.Available>)
it[iconUrl] = foundExtension.iconUrl it[iconUrl] = foundExtension.iconUrl
} }
} }
} else {
// insert new record
ExtensionTable.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
}
} }
}
// deal with obsolete extensions // deal with obsolete extensions
ExtensionTable.selectAll().forEach { extensionRecord -> ExtensionTable.selectAll().forEach { extensionRecord ->
val foundExtension = foundExtensions.find { it.pkgName == extensionRecord[ExtensionTable.pkgName] } val foundExtension = foundExtensions.find { it.pkgName == extensionRecord[ExtensionTable.pkgName] }
if (foundExtension == null) { if (foundExtension == null) {
// this extensions is obsolete // this extensions is obsolete
if (extensionRecord[ExtensionTable.isInstalled]) { if (extensionRecord[ExtensionTable.isInstalled]) {
// is installed so we should mark it as obsolete // is installed so we should mark it as obsolete
ExtensionTable.update({ ExtensionTable.pkgName eq extensionRecord[ExtensionTable.pkgName] }) { ExtensionTable.update({ ExtensionTable.pkgName eq extensionRecord[ExtensionTable.pkgName] }) {
it[isObsolete] = true it[isObsolete] = true
}
} else {
// is not installed so we can remove the record without a care
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq extensionRecord[ExtensionTable.pkgName] }
} }
} else {
// is not installed so we can remove the record without a care
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq extensionRecord[ExtensionTable.pkgName] }
} }
} }
} }
} }
} }
}

View File

@ -22,35 +22,35 @@ object Library {
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets, // TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
// ..implement that shit at some time... // ..implement that shit at some time...
// ..also Consider to rename it to `isDefault` // ..also Consider to rename it to `isDefault`
fun addMangaToLibrary(mangaId: Int) { fun addMangaToLibrary(mangaId: Int) {
val manga = getManga(mangaId) val manga = getManga(mangaId)
if (!manga.inLibrary) { if (!manga.inLibrary) {
transaction { transaction {
MangaTable.update({ MangaTable.id eq manga.id }) { MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = true it[inLibrary] = true
}
}
}
}
fun removeMangaFromLibrary(mangaId: Int) {
val manga = getManga(mangaId)
if (manga.inLibrary) {
transaction {
MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = false
it[defaultCategory] = true
}
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
}
}
}
fun getLibraryMangas(): List<MangaDataClass> {
return transaction {
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
MangaTable.toDataClass(it)
} }
} }
} }
} }
fun removeMangaFromLibrary(mangaId: Int) {
val manga = getManga(mangaId)
if (manga.inLibrary) {
transaction {
MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = false
it[defaultCategory] = true
}
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
}
}
}
fun getLibraryMangas(): List<MangaDataClass> {
return transaction {
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
MangaTable.toDataClass(it)
}
}
}
}

View File

@ -15,99 +15,99 @@ import ir.armor.tachidesk.impl.Source.getSource
import ir.armor.tachidesk.model.database.MangaStatus import ir.armor.tachidesk.model.database.MangaStatus
import ir.armor.tachidesk.model.database.MangaTable import ir.armor.tachidesk.model.database.MangaTable
import ir.armor.tachidesk.model.dataclass.MangaDataClass import ir.armor.tachidesk.model.dataclass.MangaDataClass
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.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import java.io.InputStream import java.io.InputStream
object Manga { object Manga {
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass { fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
return if (mangaEntry[MangaTable.initialized]) { return if (mangaEntry[MangaTable.initialized]) {
MangaDataClass( MangaDataClass(
mangaId, mangaId,
mangaEntry[MangaTable.sourceReference].toString(), mangaEntry[MangaTable.sourceReference].toString(),
mangaEntry[MangaTable.url], mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title], mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url], if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
true, true,
mangaEntry[MangaTable.artist], mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author], mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description], mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre], mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary], mangaEntry[MangaTable.inLibrary],
getSource(mangaEntry[MangaTable.sourceReference]) getSource(mangaEntry[MangaTable.sourceReference])
) )
} else { // initialize manga } else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val fetchedManga = source.fetchMangaDetails( val fetchedManga = source.fetchMangaDetails(
SManga.create().apply { SManga.create().apply {
url = mangaEntry[MangaTable.url] url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title] title = mangaEntry[MangaTable.title]
}
).toBlocking().first()
transaction {
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.initialized] = true
it[MangaTable.artist] = fetchedManga.artist
it[MangaTable.author] = fetchedManga.author
it[MangaTable.description] = fetchedManga.description
it[MangaTable.genre] = fetchedManga.genre
it[MangaTable.status] = fetchedManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
}
} }
).toBlocking().first()
transaction { mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
MangaTable.update({ MangaTable.id eq mangaId }) { val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
it[MangaTable.initialized] = true MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].toString(),
it[MangaTable.artist] = fetchedManga.artist mangaEntry[MangaTable.url],
it[MangaTable.author] = fetchedManga.author mangaEntry[MangaTable.title],
it[MangaTable.description] = fetchedManga.description if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
it[MangaTable.genre] = fetchedManga.genre
it[MangaTable.status] = fetchedManga.status true,
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url fetchedManga.artist,
} fetchedManga.author,
fetchedManga.description,
fetchedManga.genre,
MangaStatus.valueOf(fetchedManga.status).name,
false,
getSource(mangaEntry[MangaTable.sourceReference])
)
} }
}
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
val newThumbnail = mangaEntry[MangaTable.thumbnail_url] val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val saveDir = ApplicationDirs.thumbnailsRoot
val fileName = mangaId.toString()
MangaDataClass( return getCachedImageResponse(saveDir, fileName) {
mangaId, val sourceId = mangaEntry[MangaTable.sourceReference]
mangaEntry[MangaTable.sourceReference].toString(), val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
}
mangaEntry[MangaTable.url], source.client.newCall(
mangaEntry[MangaTable.title], GET(thumbnailUrl, source.headers)
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail, ).execute()
}
true,
fetchedManga.artist,
fetchedManga.author,
fetchedManga.description,
fetchedManga.genre,
MangaStatus.valueOf(fetchedManga.status).name,
false,
getSource(mangaEntry[MangaTable.sourceReference])
)
} }
} }
fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString()
return getCachedImageResponse(saveDir, fileName) {
val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
}
source.client.newCall(
GET(thumbnailUrl, source.headers)
).execute()
}
}
}

View File

@ -18,84 +18,84 @@ import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
object MangaList { object MangaList {
fun proxyThumbnailUrl(mangaId: Int): String { fun proxyThumbnailUrl(mangaId: Int): String {
return "/api/v1/manga/$mangaId/thumbnail" return "/api/v1/manga/$mangaId/thumbnail"
}
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId.toLong())
val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).toBlocking().first()
} else {
if (source.supportsLatest)
source.fetchLatestUpdates(pageNum).toBlocking().first()
else
throw Exception("Source $source doesn't support latest")
} }
return mangasPage.processEntries(sourceId)
}
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass { fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val mangasPage = this val source = getHttpSource(sourceId.toLong())
val mangaList = transaction { val mangasPage = if (popular) {
return@transaction mangasPage.mangas.map { manga -> source.fetchPopularManga(pageNum).toBlocking().first()
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull() } else {
if (mangaEntry == null) { // create manga entry if (source.supportsLatest)
val mangaId = MangaTable.insertAndGetId { source.fetchLatestUpdates(pageNum).toBlocking().first()
it[url] = manga.url else
it[title] = manga.title throw Exception("Source $source doesn't support latest")
}
return mangasPage.processEntries(sourceId)
}
it[artist] = manga.artist fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
it[author] = manga.author val mangasPage = this
it[description] = manga.description val mangaList = transaction {
it[genre] = manga.genre return@transaction mangasPage.mangas.map { manga ->
it[status] = manga.status var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
it[thumbnail_url] = manga.thumbnail_url if (mangaEntry == null) { // create manga entry
val mangaId = MangaTable.insertAndGetId {
it[url] = manga.url
it[title] = manga.title
it[sourceReference] = sourceId it[artist] = manga.artist
}.value it[author] = manga.author
it[description] = manga.description
it[genre] = manga.genre
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url
MangaDataClass( it[sourceReference] = sourceId
mangaId, }.value
sourceId.toString(),
manga.url, MangaDataClass(
manga.title, mangaId,
proxyThumbnailUrl(mangaId), sourceId.toString(),
manga.initialized, manga.url,
manga.title,
proxyThumbnailUrl(mangaId),
manga.artist, manga.initialized,
manga.author,
manga.description,
manga.genre,
MangaStatus.valueOf(manga.status).name
)
} else {
val mangaId = mangaEntry[MangaTable.id].value
MangaDataClass(
mangaId,
sourceId.toString(),
manga.url, manga.artist,
manga.title, manga.author,
proxyThumbnailUrl(mangaId), manga.description,
manga.genre,
MangaStatus.valueOf(manga.status).name
)
} else {
val mangaId = mangaEntry[MangaTable.id].value
MangaDataClass(
mangaId,
sourceId.toString(),
true, manga.url,
manga.title,
proxyThumbnailUrl(mangaId),
mangaEntry[MangaTable.artist], true,
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description], mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.genre], mangaEntry[MangaTable.author],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, mangaEntry[MangaTable.description],
mangaEntry[MangaTable.inLibrary] mangaEntry[MangaTable.genre],
) MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary]
)
}
} }
} }
return PagedMangaListDataClass(
mangaList,
mangasPage.hasNextPage
)
} }
return PagedMangaListDataClass(
mangaList,
mangasPage.hasNextPage
)
}
} }

View File

@ -14,7 +14,7 @@ import ir.armor.tachidesk.model.database.ChapterTable
import ir.armor.tachidesk.model.database.MangaTable import ir.armor.tachidesk.model.database.MangaTable
import ir.armor.tachidesk.model.database.PageTable import ir.armor.tachidesk.model.database.PageTable
import ir.armor.tachidesk.model.database.SourceTable import ir.armor.tachidesk.model.database.SourceTable
import ir.armor.tachidesk.server.applicationDirs import ir.armor.tachidesk.server.ApplicationDirs
import org.jetbrains.exposed.sql.and 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
@ -22,72 +22,72 @@ import org.jetbrains.exposed.sql.update
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
object Page{ object Page {
/** /**
* A page might have a imageUrl ready from the get go, or we might need to * A page might have a imageUrl ready from the get go, or we might need to
* go an extra step and call fetchImageUrl to get it. * go an extra step and call fetchImageUrl to get it.
*/ */
fun getTrueImageUrl(page: Page, source: HttpSource): String { fun getTrueImageUrl(page: Page, source: HttpSource): String {
if (page.imageUrl == null) { if (page.imageUrl == null) {
page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!! page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!!
}
return page.imageUrl!!
} }
return page.imageUrl!!
}
fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> { fun getPageImage(mangaId: Int, chapterIndex: Int, index: 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 source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction { val chapterEntry = transaction {
ChapterTable.select { ChapterTable.select {
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId) (ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.firstOrNull()!! }.firstOrNull()!!
} }
val chapterId = chapterEntry[ChapterTable.id].value val chapterId = chapterEntry[ChapterTable.id].value
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! } val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
val tachiPage = Page( val tachiPage = Page(
pageEntry[PageTable.index], pageEntry[PageTable.index],
pageEntry[PageTable.url], pageEntry[PageTable.url],
pageEntry[PageTable.imageUrl] pageEntry[PageTable.imageUrl]
) )
if (pageEntry[PageTable.imageUrl] == null) { if (pageEntry[PageTable.imageUrl] == null) {
transaction { transaction {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) { PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
it[imageUrl] = getTrueImageUrl(tachiPage, source) it[imageUrl] = getTrueImageUrl(tachiPage, source)
}
} }
} }
val saveDir = getChapterDir(mangaId, chapterId)
File(saveDir).mkdirs()
val fileName = index.toString()
return getCachedImageResponse(saveDir, fileName) {
source.fetchImage(tachiPage).toBlocking().first()
}
} }
val saveDir = getChapterDir(mangaId, chapterId)
File(saveDir).mkdirs()
val fileName = index.toString()
return getCachedImageResponse(saveDir, fileName) {
source.fetchImage(tachiPage).toBlocking().first()
}
}
// TODO: rewrite this to match tachiyomi // TODO: rewrite this to match tachiyomi
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]
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! } val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! } val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
val chapterDir = when { val chapterDir = when {
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}" chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
else -> chapterEntry[ChapterTable.name] else -> chapterEntry[ChapterTable.name]
}
val mangaTitle = mangaEntry[MangaTable.title]
val sourceName = source.toString()
val mangaDir = "${ApplicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
// make sure dirs exist
File(mangaDir).mkdirs()
return mangaDir
} }
val mangaTitle = mangaEntry[MangaTable.title]
val sourceName = source.toString()
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
// make sure dirs exist
File(mangaDir).mkdirs()
return mangaDir
}
} }

View File

@ -13,29 +13,29 @@ import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
object Search { object Search {
// TODO // TODO
fun sourceFilters(sourceId: Long) { fun sourceFilters(sourceId: Long) {
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
// source.getFilterList().toItems() // source.getFilterList().toItems()
} }
fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass { fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first() val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first()
return searchManga.processEntries(sourceId) return searchManga.processEntries(sourceId)
} }
fun sourceGlobalSearch(searchTerm: String) { fun sourceGlobalSearch(searchTerm: String) {
// TODO // TODO
} }
data class FilterWrapper( data class FilterWrapper(
val type: String, val type: String,
val filter: Any val filter: Any
) )
/** /**
* Note: Exhentai had a filter serializer (now in SY) that we might be able to steal * Note: Exhentai had a filter serializer (now in SY) that we might be able to steal
*/ */
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> { // private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
// return mapNotNull { filter -> // return mapNotNull { filter ->
// when (filter) { // when (filter) {

View File

@ -14,7 +14,7 @@ import ir.armor.tachidesk.impl.Extension.loadExtensionInstance
import ir.armor.tachidesk.model.database.ExtensionTable import ir.armor.tachidesk.model.database.ExtensionTable
import ir.armor.tachidesk.model.database.SourceTable import ir.armor.tachidesk.model.database.SourceTable
import ir.armor.tachidesk.model.dataclass.SourceDataClass import ir.armor.tachidesk.model.dataclass.SourceDataClass
import ir.armor.tachidesk.server.applicationDirs import ir.armor.tachidesk.server.ApplicationDirs
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
@ -22,67 +22,67 @@ import org.jetbrains.exposed.sql.transactions.transaction
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
object Source { object Source {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private val sourceCache = ConcurrentHashMap<Long, HttpSource>() private val sourceCache = ConcurrentHashMap<Long, HttpSource>()
fun getHttpSource(sourceId: Long): HttpSource { fun getHttpSource(sourceId: Long): HttpSource {
val cachedResult: HttpSource? = sourceCache[sourceId] val cachedResult: HttpSource? = sourceCache[sourceId]
if (cachedResult != null) { if (cachedResult != null) {
logger.debug("used cached HttpSource: ${cachedResult.name}") logger.debug("used cached HttpSource: ${cachedResult.name}")
return cachedResult return cachedResult
}
transaction {
val sourceRecord = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
val extensionId = sourceRecord[SourceTable.extension]
val extensionRecord = ExtensionTable.select { ExtensionTable.id eq extensionId }.firstOrNull()!!
val apkName = extensionRecord[ExtensionTable.apkName]
val className = extensionRecord[ExtensionTable.classFQName]
val jarName = apkName.substringBefore(".apk") + ".jar"
val jarPath = "${ApplicationDirs.extensionsRoot}/$jarName"
val extensionInstance = loadExtensionInstance(jarPath, className)
if (sourceRecord[SourceTable.partOfFactorySource]) {
(extensionInstance as SourceFactory).createSources().forEach {
sourceCache[it.id] = it as HttpSource
}
} else {
(extensionInstance as HttpSource).also {
sourceCache[it.id] = it
}
}
}
return sourceCache[sourceId]!!
} }
transaction { fun getSourceList(): List<SourceDataClass> {
val sourceRecord = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! return transaction {
SourceTable.selectAll().map {
val extensionId = sourceRecord[SourceTable.extension] SourceDataClass(
val extensionRecord = ExtensionTable.select { ExtensionTable.id eq extensionId }.firstOrNull()!! it[SourceTable.id].value.toString(),
val apkName = extensionRecord[ExtensionTable.apkName] it[SourceTable.name],
val className = extensionRecord[ExtensionTable.classFQName] it[SourceTable.lang],
val jarName = apkName.substringBefore(".apk") + ".jar" getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
val jarPath = "${applicationDirs.extensionsRoot}/$jarName" getHttpSource(it[SourceTable.id].value).supportsLatest
)
val extensionInstance = loadExtensionInstance(jarPath, className)
if (sourceRecord[SourceTable.partOfFactorySource]) {
(extensionInstance as SourceFactory).createSources().forEach {
sourceCache[it.id] = it as HttpSource
}
} else {
(extensionInstance as HttpSource).also {
sourceCache[it.id] = it
} }
} }
} }
return sourceCache[sourceId]!!
}
fun getSourceList(): List<SourceDataClass> { fun getSource(sourceId: Long): SourceDataClass {
return transaction { return transaction {
SourceTable.selectAll().map { val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
SourceDataClass( SourceDataClass(
it[SourceTable.id].value.toString(), sourceId.toString(),
it[SourceTable.name], source?.get(SourceTable.name),
it[SourceTable.lang], source?.get(SourceTable.lang),
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]), source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
getHttpSource(it[SourceTable.id].value).supportsLatest source?.let { getHttpSource(sourceId).supportsLatest }
) )
} }
} }
} }
fun getSource(sourceId: Long): SourceDataClass {
return transaction {
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
SourceDataClass(
sourceId.toString(),
source?.get(SourceTable.name),
source?.get(SourceTable.lang),
source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
source?.let { getHttpSource(sourceId).supportsLatest }
)
}
}
}

View File

@ -14,14 +14,14 @@ import ir.armor.tachidesk.model.database.ExtensionTable
import ir.armor.tachidesk.model.database.MangaTable import ir.armor.tachidesk.model.database.MangaTable
import ir.armor.tachidesk.model.database.PageTable import ir.armor.tachidesk.model.database.PageTable
import ir.armor.tachidesk.model.database.SourceTable import ir.armor.tachidesk.model.database.SourceTable
import ir.armor.tachidesk.server.applicationDirs 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
object DBMangaer { object DBMangaer {
val db by lazy { val db by lazy {
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver") Database.connect("jdbc:h2:${ApplicationDirs.dataRoot}/database", "org.h2.Driver")
} }
} }

View File

@ -41,248 +41,250 @@ import java.io.IOException
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
private val logger = KotlinLogging.logger {} object JavalinSetup {
private val logger = KotlinLogging.logger {}
fun javalinSetup() { fun javalinSetup() {
var hasWebUiBundled = false var hasWebUiBundled = false
val app = Javalin.create { config -> val app = Javalin.create { config ->
try { try {
Main::class.java.getResource("/react/index.html") Main::class.java.getResource("/react/index.html")
hasWebUiBundled = true hasWebUiBundled = true
config.addStaticFiles("/react") config.addStaticFiles("/react")
config.addSinglePageRoot("/", "/react/index.html") config.addSinglePageRoot("/", "/react/index.html")
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
logger.warn("react build files are missing.") logger.warn("react build files are missing.")
hasWebUiBundled = false hasWebUiBundled = false
}
config.enableCorsForAllOrigins()
}.start(serverConfig.ip, serverConfig.port)
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
openInBrowser()
} }
config.enableCorsForAllOrigins()
}.start(serverConfig.ip, serverConfig.port)
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
openInBrowser()
}
app.exception(NullPointerException::class.java) { e, ctx -> app.exception(NullPointerException::class.java) { e, ctx ->
logger.error("NullPointerException while handling the request", e) logger.error("NullPointerException while handling the request", e)
ctx.status(404) ctx.status(404)
} }
app.exception(IOException::class.java) { e, ctx -> app.exception(IOException::class.java) { e, ctx ->
logger.error("IOException while handling the request", e) logger.error("IOException while handling the request", e)
ctx.status(500) ctx.status(500)
ctx.result(e.message ?: "Internal Server Error") ctx.result(e.message ?: "Internal Server Error")
} }
app.get("/api/v1/extension/list") { ctx -> app.get("/api/v1/extension/list") { ctx ->
ctx.json(getExtensionList()) ctx.json(getExtensionList())
} }
app.get("/api/v1/extension/install/:pkgName") { ctx -> app.get("/api/v1/extension/install/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
ctx.status( ctx.status(
installExtension(pkgName) installExtension(pkgName)
) )
} }
app.get("/api/v1/extension/update/:pkgName") { ctx -> app.get("/api/v1/extension/update/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
ctx.status( ctx.status(
updateExtension(pkgName) updateExtension(pkgName)
) )
} }
app.get("/api/v1/extension/uninstall/:pkgName") { ctx -> app.get("/api/v1/extension/uninstall/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
uninstallExtension(pkgName) uninstallExtension(pkgName)
ctx.status(200) ctx.status(200)
} }
// icon for extension named `apkName` // icon for extension named `apkName`
app.get("/api/v1/extension/icon/:apkName") { ctx -> app.get("/api/v1/extension/icon/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName") val apkName = ctx.pathParam("apkName")
val result = getExtensionIcon(apkName) val result = getExtensionIcon(apkName)
ctx.result(result.first) ctx.result(result.first)
ctx.header("content-type", result.second) ctx.header("content-type", result.second)
} }
// list of sources // list of sources
app.get("/api/v1/source/list") { ctx -> app.get("/api/v1/source/list") { ctx ->
ctx.json(getSourceList()) ctx.json(getSourceList())
} }
// fetch source with id `sourceId` // fetch source with id `sourceId`
app.get("/api/v1/source/:sourceId") { ctx -> app.get("/api/v1/source/:sourceId") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(getSource(sourceId)) ctx.json(getSource(sourceId))
} }
// popular mangas from source with id `sourceId` // popular mangas from source with id `sourceId`
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx -> app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt() val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId, pageNum, popular = true)) ctx.json(getMangaList(sourceId, pageNum, popular = true))
} }
// latest mangas from source with id `sourceId` // latest mangas from source with id `sourceId`
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx -> app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt() val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId, pageNum, popular = false)) ctx.json(getMangaList(sourceId, pageNum, popular = false))
} }
// get manga info // get manga info
app.get("/api/v1/manga/:mangaId/") { ctx -> app.get("/api/v1/manga/:mangaId/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getManga(mangaId)) ctx.json(getManga(mangaId))
} }
// manga thumbnail // manga thumbnail
app.get("api/v1/manga/:mangaId/thumbnail") { ctx -> app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
val result = getMangaThumbnail(mangaId) val result = getMangaThumbnail(mangaId)
ctx.result(result.first) ctx.result(result.first)
ctx.header("content-type", result.second) ctx.header("content-type", result.second)
} }
// adds the manga to library // adds the manga to library
app.get("api/v1/manga/:mangaId/library") { ctx -> app.get("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
addMangaToLibrary(mangaId) addMangaToLibrary(mangaId)
ctx.status(200) ctx.status(200)
} }
// removes the manga from the library // removes the manga from the library
app.delete("api/v1/manga/:mangaId/library") { ctx -> app.delete("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
removeMangaFromLibrary(mangaId) removeMangaFromLibrary(mangaId)
ctx.status(200) ctx.status(200)
} }
// list manga's categories // list manga's categories
app.get("api/v1/manga/:mangaId/category/") { ctx -> app.get("api/v1/manga/:mangaId/category/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getMangaCategories(mangaId)) ctx.json(getMangaCategories(mangaId))
} }
// adds the manga to category // adds the manga to category
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx -> app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
val categoryId = ctx.pathParam("categoryId").toInt() val categoryId = ctx.pathParam("categoryId").toInt()
addMangaToCategory(mangaId, categoryId) addMangaToCategory(mangaId, categoryId)
ctx.status(200) ctx.status(200)
} }
// removes the manga from the category // removes the manga from the category
app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx -> app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
val categoryId = ctx.pathParam("categoryId").toInt() val categoryId = ctx.pathParam("categoryId").toInt()
removeMangaFromCategory(mangaId, categoryId) removeMangaFromCategory(mangaId, categoryId)
ctx.status(200) ctx.status(200)
} }
app.get("/api/v1/manga/:mangaId/chapters") { ctx -> app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapterList(mangaId)) ctx.json(getChapterList(mangaId))
} }
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx -> app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
val chapterIndex = ctx.pathParam("chapterIndex").toInt() val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapter(chapterIndex, mangaId)) ctx.json(getChapter(chapterIndex, mangaId))
} }
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx -> app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
val chapterIndex = ctx.pathParam("chapterIndex").toInt() val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val index = ctx.pathParam("index").toInt() val index = ctx.pathParam("index").toInt()
val result = getPageImage(mangaId, chapterIndex, index) val result = getPageImage(mangaId, chapterIndex, index)
ctx.result(result.first) ctx.result(result.first)
ctx.header("content-type", result.second) ctx.header("content-type", result.second)
} }
// global search // global search
app.get("/api/v1/search/:searchTerm") { ctx -> app.get("/api/v1/search/:searchTerm") { ctx ->
val searchTerm = ctx.pathParam("searchTerm") val searchTerm = ctx.pathParam("searchTerm")
ctx.json(sourceGlobalSearch(searchTerm)) ctx.json(sourceGlobalSearch(searchTerm))
} }
// single source search // single source search
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx -> app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm") val searchTerm = ctx.pathParam("searchTerm")
val pageNum = ctx.pathParam("pageNum").toInt() val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(sourceSearch(sourceId, searchTerm, pageNum)) ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
} }
// source filter list // source filter list
app.get("/api/v1/source/:sourceId/filters/") { ctx -> app.get("/api/v1/source/:sourceId/filters/") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(sourceFilters(sourceId)) ctx.json(sourceFilters(sourceId))
} }
// lists mangas that have no category assigned // lists mangas that have no category assigned
app.get("/api/v1/library/") { ctx -> app.get("/api/v1/library/") { ctx ->
ctx.json(getLibraryMangas()) ctx.json(getLibraryMangas())
} }
// category list // category list
app.get("/api/v1/category/") { ctx -> app.get("/api/v1/category/") { ctx ->
ctx.json(getCategoryList()) ctx.json(getCategoryList())
} }
// category create // category create
app.post("/api/v1/category/") { ctx -> app.post("/api/v1/category/") { ctx ->
val name = ctx.formParam("name")!! val name = ctx.formParam("name")!!
createCategory(name) createCategory(name)
ctx.status(200) ctx.status(200)
} }
// category modification // category modification
app.patch("/api/v1/category/:categoryId") { ctx -> app.patch("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt() val categoryId = ctx.pathParam("categoryId").toInt()
val name = ctx.formParam("name") val name = ctx.formParam("name")
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
updateCategory(categoryId, name, isLanding) updateCategory(categoryId, name, isLanding)
ctx.status(200) ctx.status(200)
} }
// category re-ordering // category re-ordering
app.patch("/api/v1/category/:categoryId/reorder") { ctx -> app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt() val categoryId = ctx.pathParam("categoryId").toInt()
val from = ctx.formParam("from")!!.toInt() val from = ctx.formParam("from")!!.toInt()
val to = ctx.formParam("to")!!.toInt() val to = ctx.formParam("to")!!.toInt()
reorderCategory(categoryId, from, to) reorderCategory(categoryId, from, to)
ctx.status(200) ctx.status(200)
} }
// category delete // category delete
app.delete("/api/v1/category/:categoryId") { ctx -> app.delete("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt() val categoryId = ctx.pathParam("categoryId").toInt()
removeCategory(categoryId) removeCategory(categoryId)
ctx.status(200) ctx.status(200)
} }
// returns the manga list associated with a category // returns the manga list associated with a category
app.get("/api/v1/category/:categoryId") { ctx -> app.get("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt() val categoryId = ctx.pathParam("categoryId").toInt()
ctx.json(getCategoryMangaList(categoryId)) ctx.json(getCategoryMangaList(categoryId))
} }
// expects a Tachiyomi legacy backup file to be uploaded // expects a Tachiyomi legacy backup file to be uploaded
app.get("/api/v1/backup/legacy/import") { ctx -> app.get("/api/v1/backup/legacy/import") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt() val categoryId = ctx.pathParam("categoryId").toInt()
ctx.json(getCategoryMangaList(categoryId)) ctx.json(getCategoryMangaList(categoryId))
} }
// returns a Tachiyomi legacy backup file created from the current database // returns a Tachiyomi legacy backup file created from the current database
app.get("/api/v1/backup/legacy/export") { ctx -> app.get("/api/v1/backup/legacy/export") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt() val categoryId = ctx.pathParam("categoryId").toInt()
ctx.json(getCategoryMangaList(categoryId)) ctx.json(getCategoryMangaList(categoryId))
}
} }
} }

View File

@ -24,7 +24,7 @@ import java.io.File
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
object applicationDirs { object ApplicationDirs {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!! val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!!
val extensionsRoot = "$dataRoot/extensions" val extensionsRoot = "$dataRoot/extensions"
val thumbnailsRoot = "$dataRoot/thumbnails" val thumbnailsRoot = "$dataRoot/thumbnails"
@ -50,17 +50,17 @@ fun applicationSetup() {
// make dirs we need // make dirs we need
listOf( listOf(
applicationDirs.dataRoot, ApplicationDirs.dataRoot,
applicationDirs.extensionsRoot, ApplicationDirs.extensionsRoot,
"${applicationDirs.extensionsRoot}/icon", "${ApplicationDirs.extensionsRoot}/icon",
applicationDirs.thumbnailsRoot ApplicationDirs.thumbnailsRoot
).forEach { ).forEach {
File(it).mkdirs() File(it).mkdirs()
} }
// 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("${ApplicationDirs.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 ->

View File

@ -20,8 +20,8 @@ import java.io.IOException
fun openInBrowser() { fun openInBrowser() {
try { try {
Desktop.browseURL("http://127.0.0.1:4567") Desktop.browseURL("http://127.0.0.1:4567")
} catch (e1: IOException) { } catch (e: Exception) {
e1.printStackTrace() e.printStackTrace()
} }
} }