Compare commits

..

1 Commits

Author SHA1 Message Date
Aria Moradi
53c3ac5676 token auth 2022-04-16 18:26:44 +04:30
24 changed files with 496 additions and 1232 deletions

View File

@@ -1,25 +1,3 @@
# Server: v0.6.3-next + WebUI: r944
## TL;DR
- N/A
## Tachidesk-Server Changelog
- (r1087) v0.6.3 (by @AriaMoradi)
- (r1088) Save categories when manga is unfavorited ([#335](https://github.com/Suwayomi/Tachidesk-Server/pull/335) by @Syer10)
- (r1089) handle solid RAR archives ([#339](https://github.com/Suwayomi/Tachidesk-Server/pull/339)) cfso100@gmail.com
- (r1090) add support for changing downloads dir ([#343](https://github.com/Suwayomi/Tachidesk-Server/pull/343) by @AriaMoradi)
- (r1091) fix Applications dir dependency ([#344](https://github.com/Suwayomi/Tachidesk-Server/pull/344) by @AriaMoradi)
- (r1092) add support for alternative web interfaces ([#342](https://github.com/Suwayomi/Tachidesk-Server/pull/342) by @AriaMoradi)
- (r1093) Add displayValues json field for select filter ([#347](https://github.com/Suwayomi/Tachidesk-Server/pull/347) by @Syer10)
- (r1094) document manga endpoints ([#348](https://github.com/Suwayomi/Tachidesk-Server/pull/348) by @Syer10)
- (r1095) add ChapterCount to manga object in categoryMangas endpoint ([#349](https://github.com/Suwayomi/Tachidesk-Server/pull/349) by @abhijeetChawla)
- (r1096) document all endpoints ([#350](https://github.com/Suwayomi/Tachidesk-Server/pull/350) by @Syer10)
- (r1097) fix copymanga ([#354](https://github.com/Suwayomi/Tachidesk-Server/pull/354) by @AriaMoradi)
- (r1098) fix formatting by kotlinter (by @AriaMoradi)
## Tachidesk-WebUI Changelog
- (r943) fix default width ([#171](https://github.com/Suwayomi/Tachidesk-WebUI/pull/171) by @Robonau)
- (r944) added an update checker button for library ([#172](https://github.com/Suwayomi/Tachidesk-WebUI/pull/172) by @infix)
# Server: v0.6.3 + WebUI: r942 # Server: v0.6.3 + WebUI: r942
## TL;DR ## TL;DR
- Changes in Server - Changes in Server

View File

@@ -14,8 +14,7 @@ const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release // should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.6.3" val tachideskVersion = System.getenv("ProductVersion") ?: "v0.6.3"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r944" val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r942"
val sorayomiRevisionTag = System.getenv("SorayomiRevision") ?: "0.1.5"
// counts commits on the master branch // counts commits on the master branch
val tachideskRevision = runCatching { val tachideskRevision = runCatching {

View File

@@ -54,14 +54,11 @@ dependencies {
// Disk & File // Disk & File
implementation("net.lingala.zip4j:zip4j:2.9.1") implementation("net.lingala.zip4j:zip4j:2.9.1")
implementation("com.github.junrar:junrar:7.5.0") implementation("com.github.junrar:junrar:7.4.0")
// CloudflareInterceptor // CloudflareInterceptor
implementation("net.sourceforge.htmlunit:htmlunit:2.56.0") implementation("net.sourceforge.htmlunit:htmlunit:2.56.0")
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
implementation("org.bouncycastle:bcprov-jdk18on:1.71")
// Source models and interfaces from Tachiyomi 1.x // Source models and interfaces from Tachiyomi 1.x
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi // using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
// implementation("tachiyomi.sourceapi:source-api:1.1") // implementation("tachiyomi.sourceapi:source-api:1.1")
@@ -78,9 +75,6 @@ dependencies {
} }
application { application {
applicationDefaultJvmArgs = listOf(
"-Djunrar.extractor.thread-keep-alive-seconds=30"
)
mainClass.set(MainClass) mainClass.set(MainClass)
} }
@@ -110,9 +104,6 @@ buildConfig {
buildConfigField("String", "WEBUI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-WebUI-preview")) buildConfigField("String", "WEBUI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-WebUI-preview"))
buildConfigField("String", "WEBUI_TAG", quoteWrap(webUIRevisionTag)) buildConfigField("String", "WEBUI_TAG", quoteWrap(webUIRevisionTag))
buildConfigField("String", "SORAYOMI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-Sorayomi"))
buildConfigField("String", "SORAYOMI_TAG", quoteWrap(sorayomiRevisionTag))
buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Tachidesk-Server")) buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Tachidesk-Server"))
buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA")) buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA"))

View File

@@ -5,10 +5,11 @@ import com.github.junrar.rarfile.FileHeader
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.util.concurrent.Executors
/** /**
* Loader used to load a chapter from a .rar or .cbr file. * Loader used to load a chapter from a .rar or .cbr file.
@@ -21,40 +22,20 @@ class RarPageLoader(file: File) : PageLoader {
private val archive = Archive(file) private val archive = Archive(file)
/** /**
* The fully uncompressed files, to be used in case archive is solid. * Pool for copying compressed files to an input stream.
*/ */
private var archiveMap = mutableMapOf<FileHeader, InputStream>() private val pool = Executors.newFixedThreadPool(1)
/** /**
* Returns an observable containing the pages found on this rar archive ordered with a natural * Returns an observable containing the pages found on this rar archive ordered with a natural
* comparator. * comparator.
*/ */
override fun getPages(): List<ReaderPage> { override fun getPages(): List<ReaderPage> {
if (archive.mainHeader.isSolid) {
// Solid means that we need to read all the file sequentially
for (header in archive.fileHeaders) {
val baos = ByteArrayOutputStream()
archive.extractFile(header, baos)
archiveMap[header] = ByteArrayInputStream(baos.toByteArray())
}
// After reading the full archive, proceed to filter and transform
return archive.fileHeaders
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { archiveMap.getValue(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, header ->
val streamFn = { archiveMap.getValue(header) }
ReaderPage(i).apply {
stream = streamFn
status = Page.READY
}
}
}
return archive.fileHeaders return archive.fileHeaders
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, header -> .mapIndexed { i, header ->
val streamFn = { archive.getInputStream(header) } val streamFn = { getStream(header) }
ReaderPage(i).apply { ReaderPage(i).apply {
stream = streamFn stream = streamFn
@@ -62,4 +43,21 @@ class RarPageLoader(file: File) : PageLoader {
} }
} }
} }
/**
* Returns an input stream for the given [header].
*/
private fun getStream(header: FileHeader): InputStream {
val pipeIn = PipedInputStream()
val pipeOut = PipedOutputStream(pipeIn)
pool.execute {
try {
pipeOut.use {
archive.extractFile(header, it)
}
} catch (e: Exception) {
}
}
return pipeIn
}
} }

View File

@@ -5,9 +5,7 @@ package eu.kanade.tachiyomi.source.model
open class Filter<T>(val name: String, var state: T) { open class Filter<T>(val name: String, var state: T) {
open class Header(name: String) : Filter<Any>(name, 0) open class Header(name: String) : Filter<Any>(name, 0)
open class Separator(name: String = "") : Filter<Any>(name, 0) open class Separator(name: String = "") : Filter<Any>(name, 0)
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state) { abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state)
val displayValues get() = values.map { it.toString() }
}
abstract class Text(name: String, state: String = "") : Filter<String>(name, state) abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state) abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) { abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {

View File

@@ -14,8 +14,8 @@ import suwayomi.tachidesk.global.controller.SettingsController
object GlobalAPI { object GlobalAPI {
fun defineEndpoints() { fun defineEndpoints() {
path("settings") { path("settings") {
get("about", SettingsController.about) get("about", SettingsController::about)
get("check-update", SettingsController.checkUpdate) get("check-update", SettingsController::checkUpdate)
} }
} }
} }

View File

@@ -7,48 +7,22 @@ package suwayomi.tachidesk.global.controller
* 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 io.javalin.http.HttpCode import io.javalin.http.Context
import suwayomi.tachidesk.global.impl.About import suwayomi.tachidesk.global.impl.About
import suwayomi.tachidesk.global.impl.AboutDataClass
import suwayomi.tachidesk.global.impl.AppUpdate import suwayomi.tachidesk.global.impl.AppUpdate
import suwayomi.tachidesk.global.impl.UpdateDataClass
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.withOperation
/** Settings Page/Screen */ /** Settings Page/Screen */
object SettingsController { object SettingsController {
/** returns some static info about the current app build */ /** returns some static info about the current app build */
val about = handler( fun about(ctx: Context) {
documentWith = {
withOperation {
summary("About Tachidesk")
description("Returns some static info about the current app build")
}
},
behaviorOf = { ctx ->
ctx.json(About.getAbout()) ctx.json(About.getAbout())
},
withResults = {
json<AboutDataClass>(HttpCode.OK)
} }
)
/** check for app updates */ /** check for app updates */
val checkUpdate = handler( fun checkUpdate(ctx: Context) {
documentWith = {
withOperation {
summary("Tachidesk update check")
description("Check for app updates")
}
},
behaviorOf = { ctx ->
ctx.json( ctx.json(
future { AppUpdate.checkUpdate() } future { AppUpdate.checkUpdate() }
) )
},
withResults = {
json<UpdateDataClass>(HttpCode.OK)
} }
)
} }

View File

@@ -24,98 +24,98 @@ import suwayomi.tachidesk.manga.controller.UpdateController
object MangaAPI { object MangaAPI {
fun defineEndpoints() { fun defineEndpoints() {
path("extension") { path("extension") {
get("list", ExtensionController.list) get("list", ExtensionController::list)
get("install/{pkgName}", ExtensionController.install) get("install/{pkgName}", ExtensionController::install)
post("install", ExtensionController.installFile) post("install", ExtensionController::installFile)
get("update/{pkgName}", ExtensionController.update) get("update/{pkgName}", ExtensionController::update)
get("uninstall/{pkgName}", ExtensionController.uninstall) get("uninstall/{pkgName}", ExtensionController::uninstall)
get("icon/{apkName}", ExtensionController.icon) get("icon/{apkName}", ExtensionController::icon)
} }
path("source") { path("source") {
get("list", SourceController.list) get("list", SourceController::list)
get("{sourceId}", SourceController.retrieve) get("{sourceId}", SourceController::retrieve)
get("{sourceId}/popular/{pageNum}", SourceController.popular) get("{sourceId}/popular/{pageNum}", SourceController::popular)
get("{sourceId}/latest/{pageNum}", SourceController.latest) get("{sourceId}/latest/{pageNum}", SourceController::latest)
get("{sourceId}/preferences", SourceController.getPreferences) get("{sourceId}/preferences", SourceController::getPreferences)
post("{sourceId}/preferences", SourceController.setPreference) post("{sourceId}/preferences", SourceController::setPreference)
get("{sourceId}/filters", SourceController.getFilters) get("{sourceId}/filters", SourceController::getFilters)
post("{sourceId}/filters", SourceController.setFilters) post("{sourceId}/filters", SourceController::setFilters)
get("{sourceId}/search", SourceController.searchSingle) get("{sourceId}/search", SourceController::searchSingle)
// get("all/search", SourceController.searchGlobal) // TODO // get("all/search", SourceController::searchGlobal) // TODO
} }
path("manga") { path("manga") {
get("{mangaId}", MangaController.retrieve) get("{mangaId}", MangaController.retrieve)
get("{mangaId}/thumbnail", MangaController.thumbnail) get("{mangaId}/thumbnail", MangaController::thumbnail)
get("{mangaId}/category", MangaController.categoryList) get("{mangaId}/category", MangaController::categoryList)
get("{mangaId}/category/{categoryId}", MangaController.addToCategory) get("{mangaId}/category/{categoryId}", MangaController::addToCategory)
delete("{mangaId}/category/{categoryId}", MangaController.removeFromCategory) delete("{mangaId}/category/{categoryId}", MangaController::removeFromCategory)
get("{mangaId}/library", MangaController.addToLibrary) get("{mangaId}/library", MangaController::addToLibrary)
delete("{mangaId}/library", MangaController.removeFromLibrary) delete("{mangaId}/library", MangaController::removeFromLibrary)
patch("{mangaId}/meta", MangaController.meta) patch("{mangaId}/meta", MangaController::meta)
get("{mangaId}/chapters", MangaController.chapterList) get("{mangaId}/chapters", MangaController::chapterList)
get("{mangaId}/chapter/{chapterIndex}", MangaController.chapterRetrieve) get("{mangaId}/chapter/{chapterIndex}", MangaController::chapterRetrieve)
patch("{mangaId}/chapter/{chapterIndex}", MangaController.chapterModify) patch("{mangaId}/chapter/{chapterIndex}", MangaController::chapterModify)
delete("{mangaId}/chapter/{chapterIndex}", MangaController.chapterDelete) delete("{mangaId}/chapter/{chapterIndex}", MangaController::chapterDelete)
patch("{mangaId}/chapter/{chapterIndex}/meta", MangaController.chapterMeta) patch("{mangaId}/chapter/{chapterIndex}/meta", MangaController::chapterMeta)
get("{mangaId}/chapter/{chapterIndex}/page/{index}", MangaController.pageRetrieve) get("{mangaId}/chapter/{chapterIndex}/page/{index}", MangaController::pageRetrieve)
} }
path("category") { path("category") {
get("", CategoryController.categoryList) get("", CategoryController::categoryList)
post("", CategoryController.categoryCreate) post("", CategoryController::categoryCreate)
// The order here is important {categoryId} needs to be applied last // The order here is important {categoryId} needs to be applied last
// or throws a NumberFormatException // or throws a NumberFormatException
patch("reorder", CategoryController.categoryReorder) patch("reorder", CategoryController::categoryReorder)
get("{categoryId}", CategoryController.categoryMangas) get("{categoryId}", CategoryController::categoryMangas)
patch("{categoryId}", CategoryController.categoryModify) patch("{categoryId}", CategoryController::categoryModify)
delete("{categoryId}", CategoryController.categoryDelete) delete("{categoryId}", CategoryController::categoryDelete)
} }
path("backup") { path("backup") {
post("import", BackupController.protobufImport) post("import", BackupController::protobufImport)
post("import/file", BackupController.protobufImportFile) post("import/file", BackupController::protobufImportFile)
post("validate", BackupController.protobufValidate) post("validate", BackupController::protobufValidate)
post("validate/file", BackupController.protobufValidateFile) post("validate/file", BackupController::protobufValidateFile)
get("export", BackupController.protobufExport) get("export", BackupController::protobufExport)
get("export/file", BackupController.protobufExportFile) get("export/file", BackupController::protobufExportFile)
} }
path("downloads") { path("downloads") {
ws("", DownloadController::downloadsWS) ws("", DownloadController::downloadsWS)
get("start", DownloadController.start) get("start", DownloadController::start)
get("stop", DownloadController.stop) get("stop", DownloadController::stop)
get("clear", DownloadController.stop) get("clear", DownloadController::stop)
} }
path("download") { path("download") {
get("{mangaId}/chapter/{chapterIndex}", DownloadController.queueChapter) get("{mangaId}/chapter/{chapterIndex}", DownloadController::queueChapter)
delete("{mangaId}/chapter/{chapterIndex}", DownloadController.unqueueChapter) delete("{mangaId}/chapter/{chapterIndex}", DownloadController::unqueueChapter)
} }
path("update") { path("update") {
get("recentChapters/{pageNum}", UpdateController.recentChapters) get("recentChapters/{pageNum}", UpdateController::recentChapters)
post("fetch", UpdateController.categoryUpdate) post("fetch", UpdateController::categoryUpdate)
post("reset", UpdateController.reset) post("reset", UpdateController.reset)
get("summary", UpdateController.updateSummary) get("summary", UpdateController::updateSummary)
ws("", UpdateController::categoryUpdateWS) ws("", UpdateController::categoryUpdateWS)
} }
} }

View File

@@ -1,14 +1,11 @@
package suwayomi.tachidesk.manga.controller package suwayomi.tachidesk.manga.controller
import io.javalin.http.HttpCode import io.javalin.http.Context
import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator
import suwayomi.tachidesk.manga.impl.backup.BackupFlags import suwayomi.tachidesk.manga.impl.backup.BackupFlags
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.withOperation
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@@ -22,60 +19,26 @@ import java.util.Date
object BackupController { object BackupController {
/** expects a Tachiyomi protobuf backup in the body */ /** expects a Tachiyomi protobuf backup in the body */
val protobufImport = handler( fun protobufImport(ctx: Context) {
documentWith = {
withOperation {
summary("Restore a backup")
description("Expects a Tachiyomi protobuf backup in the body")
}
},
behaviorOf = { ctx ->
ctx.future( ctx.future(
future { future {
ProtoBackupImport.performRestore(ctx.bodyAsInputStream()) ProtoBackupImport.performRestore(ctx.bodyAsInputStream())
} }
) )
},
withResults = {
httpCode(HttpCode.OK)
} }
)
/** expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */ /** expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */
val protobufImportFile = handler( fun protobufImportFile(ctx: Context) {
documentWith = {
withOperation {
summary("Restore a backup file")
description("Expects a Tachiyomi protobuf backup as a file upload, the file must be named \"backup.proto.gz\"")
}
uploadedFile("backup.proto.gz") {
it.description("Protobuf backup")
it.required(true)
}
},
behaviorOf = { ctx ->
// TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz" // TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz"
ctx.future( ctx.future(
future { future {
ProtoBackupImport.performRestore(ctx.uploadedFile("backup.proto.gz")!!.content) ProtoBackupImport.performRestore(ctx.uploadedFile("backup.proto.gz")!!.content)
} }
) )
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
} }
)
/** returns a Tachiyomi protobuf backup created from the current database as a body */ /** returns a Tachiyomi protobuf backup created from the current database as a body */
val protobufExport = handler( fun protobufExport(ctx: Context) {
documentWith = {
withOperation {
summary("Create a backup")
description("Returns a Tachiyomi protobuf backup created from the current database as a body")
}
},
behaviorOf = { ctx ->
ctx.contentType("application/octet-stream") ctx.contentType("application/octet-stream")
ctx.future( ctx.future(
future { future {
@@ -90,21 +53,10 @@ object BackupController {
) )
} }
) )
},
withResults = {
mime(HttpCode.OK, "application/octet-stream")
} }
)
/** returns a Tachiyomi protobuf backup created from the current database as a file */ /** returns a Tachiyomi protobuf backup created from the current database as a file */
val protobufExportFile = handler( fun protobufExportFile(ctx: Context) {
documentWith = {
withOperation {
summary("Create a backup file")
description("Returns a Tachiyomi protobuf backup created from the current database as a file")
}
},
behaviorOf = { ctx ->
ctx.contentType("application/octet-stream") ctx.contentType("application/octet-stream")
val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date()) val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date())
@@ -122,55 +74,23 @@ object BackupController {
) )
} }
) )
},
withResults = {
mime(HttpCode.OK, "application/octet-stream")
} }
)
/** Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body */ /** Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body */
val protobufValidate = handler( fun protobufValidate(ctx: Context) {
documentWith = {
withOperation {
summary("Validate a backup")
description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body")
}
body<ByteArray>("") {
}
},
behaviorOf = { ctx ->
ctx.future( ctx.future(
future { future {
ProtoBackupValidator.validate(ctx.bodyAsInputStream()) ProtoBackupValidator.validate(ctx.bodyAsInputStream())
} }
) )
},
withResults = {
json<AbstractBackupValidator.ValidationResult>(HttpCode.OK)
} }
)
/** Reports missing sources and trackers, expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */ /** Reports missing sources and trackers, expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */
val protobufValidateFile = handler( fun protobufValidateFile(ctx: Context) {
documentWith = {
withOperation {
summary("Validate a backup file")
description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup as a file upload, the file must be named \"backup.proto.gz\"")
}
uploadedFile("backup.proto.gz") {
it.description("Protobuf backup")
it.required(true)
}
},
behaviorOf = { ctx ->
ctx.future( ctx.future(
future { future {
ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content) ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content)
} }
) )
},
withResults = {
json<AbstractBackupValidator.ValidationResult>(HttpCode.OK)
} }
)
} }

View File

@@ -7,126 +7,50 @@ package suwayomi.tachidesk.manga.controller
* 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 io.javalin.http.HttpCode import io.javalin.http.Context
import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.server.util.formParam
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.withOperation
object CategoryController { object CategoryController {
/** category list */ /** category list */
val categoryList = handler( fun categoryList(ctx: Context) {
documentWith = {
withOperation {
summary("Category list")
description("get a list of categories")
}
},
behaviorOf = { ctx ->
ctx.json(Category.getCategoryList()) ctx.json(Category.getCategoryList())
},
withResults = {
json<List<CategoryDataClass>>(HttpCode.OK)
} }
)
/** category create */ /** category create */
val categoryCreate = handler( fun categoryCreate(ctx: Context) {
formParam<String>("name"), val name = ctx.formParam("name")!!
documentWith = { Category.createCategory(name)
withOperation {
summary("Category create")
description("Create a category")
}
},
behaviorOf = { ctx, name ->
if (Category.createCategory(name) != -1) {
ctx.status(200) ctx.status(200)
} else {
ctx.status(HttpCode.BAD_REQUEST)
} }
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.BAD_REQUEST)
}
)
/** category modification */ /** category modification */
val categoryModify = handler( fun categoryModify(ctx: Context) {
pathParam<Int>("categoryId"), val categoryId = ctx.pathParam("categoryId").toInt()
formParam<String?>("name"), val name = ctx.formParam("name")
formParam<Boolean?>("default"), val isDefault = ctx.formParam("default")?.toBoolean()
documentWith = {
withOperation {
summary("Category modify")
description("Modify a category")
}
},
behaviorOf = { ctx, categoryId, name, isDefault ->
Category.updateCategory(categoryId, name, isDefault) Category.updateCategory(categoryId, name, isDefault)
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
} }
)
/** category delete */ /** category delete */
val categoryDelete = handler( fun categoryDelete(ctx: Context) {
pathParam<Int>("categoryId"), val categoryId = ctx.pathParam("categoryId").toInt()
documentWith = {
withOperation {
summary("Category delete")
description("Delete a category")
}
},
behaviorOf = { ctx, categoryId ->
Category.removeCategory(categoryId) Category.removeCategory(categoryId)
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
} }
)
/** returns the manga list associated with a category */ /** returns the manga list associated with a category */
val categoryMangas = handler( fun categoryMangas(ctx: Context) {
pathParam<Int>("categoryId"), val categoryId = ctx.pathParam("categoryId").toInt()
documentWith = {
withOperation {
summary("Category manga")
description("Returns the manga list associated with a category")
}
},
behaviorOf = { ctx, categoryId ->
ctx.json(CategoryManga.getCategoryMangaList(categoryId)) ctx.json(CategoryManga.getCategoryMangaList(categoryId))
},
withResults = {
json<List<MangaDataClass>>(HttpCode.OK)
} }
)
/** category re-ordering */ /** category re-ordering */
val categoryReorder = handler( fun categoryReorder(ctx: Context) {
formParam<Int>("from"), val from = ctx.formParam("from")!!.toInt()
formParam<Int>("to"), val to = ctx.formParam("to")!!.toInt()
documentWith = {
withOperation {
summary("Category re-ordering")
description("Re-order a category")
}
},
behaviorOf = { ctx, from, to ->
Category.reorderCategory(from, to) Category.reorderCategory(from, to)
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
} }
)
} }

View File

@@ -7,13 +7,10 @@ package suwayomi.tachidesk.manga.controller
* 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 io.javalin.http.HttpCode import io.javalin.http.Context
import io.javalin.websocket.WsConfig import io.javalin.websocket.WsConfig
import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.DownloadManager
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.withOperation
object DownloadController { object DownloadController {
/** Download queue stats */ /** Download queue stats */
@@ -31,99 +28,45 @@ object DownloadController {
} }
/** Start the downloader */ /** Start the downloader */
val start = handler( fun start(ctx: Context) {
documentWith = {
withOperation {
summary("Downloader start")
description("Start the downloader")
}
},
behaviorOf = { ctx ->
DownloadManager.start() DownloadManager.start()
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
} }
)
/** Stop the downloader */ /** Stop the downloader */
val stop = handler( fun stop(ctx: Context) {
documentWith = {
withOperation {
summary("Downloader stop")
description("Stop the downloader")
}
},
behaviorOf = { ctx ->
DownloadManager.stop() DownloadManager.stop()
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
} }
)
/** clear download queue */ /** clear download queue */
val clear = handler( fun clear(ctx: Context) {
documentWith = {
withOperation {
summary("Downloader clear")
description("Clear download queue")
}
},
behaviorOf = { ctx ->
DownloadManager.clear() DownloadManager.clear()
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
} }
)
/** Queue chapter for download */ /** Queue chapter for download */
val queueChapter = handler( fun queueChapter(ctx: Context) {
pathParam<Int>("chapterIndex"), val chapterIndex = ctx.pathParam("chapterIndex").toInt()
pathParam<Int>("mangaId"), val mangaId = ctx.pathParam("mangaId").toInt()
documentWith = {
withOperation {
summary("Downloader add chapter")
description("Queue chapter for download")
}
},
behaviorOf = { ctx, chapterIndex, mangaId ->
ctx.future( ctx.future(
future { future {
DownloadManager.enqueue(chapterIndex, mangaId) DownloadManager.enqueue(chapterIndex, mangaId)
} }
) )
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
} }
)
/** delete chapter from download queue */ /** delete chapter from download queue */
val unqueueChapter = handler( fun unqueueChapter(ctx: Context) {
pathParam<Int>("chapterIndex"), val chapterIndex = ctx.pathParam("chapterIndex").toInt()
pathParam<Int>("mangaId"), val mangaId = ctx.pathParam("mangaId").toInt()
documentWith = {
withOperation {
summary("Downloader remove chapter")
description("Delete chapter from download queue")
}
},
behaviorOf = { ctx, chapterIndex, mangaId ->
DownloadManager.unqueue(chapterIndex, mangaId) DownloadManager.unqueue(chapterIndex, mangaId)
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
} }
)
} }

View File

@@ -7,76 +7,38 @@ package suwayomi.tachidesk.manga.controller
* 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 io.javalin.http.HttpCode import io.javalin.http.Context
import mu.KotlinLogging import mu.KotlinLogging
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam
import suwayomi.tachidesk.server.util.withOperation
object ExtensionController { object ExtensionController {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
/** list all extensions */ /** list all extensions */
val list = handler( fun list(ctx: Context) {
documentWith = {
withOperation {
summary("Extension list")
description("List all extensions")
}
},
behaviorOf = { ctx ->
ctx.future( ctx.future(
future { future {
ExtensionsList.getExtensionList() ExtensionsList.getExtensionList()
} }
) )
},
withResults = {
json<List<ExtensionDataClass>>(HttpCode.OK)
} }
)
/** install extension identified with "pkgName" */ /** install extension identified with "pkgName" */
val install = handler( fun install(ctx: Context) {
pathParam<String>("pkgName"), val pkgName = ctx.pathParam("pkgName")
documentWith = {
withOperation {
summary("Extension install")
description("install extension identified with \"pkgName\"")
}
},
behaviorOf = { ctx, pkgName ->
ctx.future( ctx.future(
future { future {
Extension.installExtension(pkgName) Extension.installExtension(pkgName)
} }
) )
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
} }
)
/** install the uploaded apk file */ /** install the uploaded apk file */
val installFile = handler( fun installFile(ctx: Context) {
documentWith = {
withOperation {
summary("Extension install apk")
description("Install the uploaded apk file")
}
uploadedFile("file") {
it.description("Extension apk")
it.required(true)
}
},
behaviorOf = { ctx ->
val uploadedFile = ctx.uploadedFile("file")!! val uploadedFile = ctx.uploadedFile("file")!!
logger.debug { "Uploaded extension file name: " + uploadedFile.filename } logger.debug { "Uploaded extension file name: " + uploadedFile.filename }
@@ -85,70 +47,32 @@ object ExtensionController {
Extension.installExternalExtension(uploadedFile.content, uploadedFile.filename) Extension.installExternalExtension(uploadedFile.content, uploadedFile.filename)
} }
) )
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
} }
)
/** update extension identified with "pkgName" */ /** update extension identified with "pkgName" */
val update = handler( fun update(ctx: Context) {
pathParam<String>("pkgName"), val pkgName = ctx.pathParam("pkgName")
documentWith = {
withOperation {
summary("Extension update")
description("Update extension identified with \"pkgName\"")
}
},
behaviorOf = { ctx, pkgName ->
ctx.future( ctx.future(
future { future {
Extension.updateExtension(pkgName) Extension.updateExtension(pkgName)
} }
) )
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.NOT_FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
} }
)
/** uninstall extension identified with "pkgName" */ /** uninstall extension identified with "pkgName" */
val uninstall = handler( fun uninstall(ctx: Context) {
pathParam<String>("pkgName"), val pkgName = ctx.pathParam("pkgName")
documentWith = {
withOperation {
summary("Extension uninstall")
description("Uninstall extension identified with \"pkgName\"")
}
},
behaviorOf = { ctx, pkgName ->
Extension.uninstallExtension(pkgName) Extension.uninstallExtension(pkgName)
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.NOT_FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
} }
)
/** icon for extension named `apkName` */ /** icon for extension named `apkName` */
val icon = handler( fun icon(ctx: Context) {
pathParam<String>("apkName"), val apkName = ctx.pathParam("apkName")
queryParam("useCache", true), val useCache = ctx.queryParam("useCache")?.toBoolean() ?: true
documentWith = {
withOperation {
summary("Extension icon")
description("Icon for extension named `apkName`")
}
},
behaviorOf = { ctx, apkName, useCache ->
ctx.future( ctx.future(
future { Extension.getExtensionIcon(apkName, useCache) } future { Extension.getExtensionIcon(apkName, useCache) }
.thenApply { .thenApply {
@@ -156,10 +80,5 @@ object ExtensionController {
it.first it.first
} }
) )
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
} }
)
} }

View File

@@ -7,6 +7,7 @@ package suwayomi.tachidesk.manga.controller
* 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 io.javalin.http.Context
import io.javalin.http.HttpCode import io.javalin.http.HttpCode
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.Chapter
@@ -14,11 +15,8 @@ import suwayomi.tachidesk.manga.impl.Library
import suwayomi.tachidesk.manga.impl.Manga import suwayomi.tachidesk.manga.impl.Manga
import suwayomi.tachidesk.manga.impl.Page import suwayomi.tachidesk.manga.impl.Page
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.formParam
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam import suwayomi.tachidesk.server.util.queryParam
@@ -32,7 +30,7 @@ object MangaController {
documentWith = { documentWith = {
withOperation { withOperation {
summary("Get a manga") summary("Get a manga")
description("Get a manga from the database using a specific id.") description("Get a manga from the database using a specific id")
} }
}, },
behaviorOf = { ctx, mangaId, onlineFetch -> behaviorOf = { ctx, mangaId, onlineFetch ->
@@ -49,16 +47,10 @@ object MangaController {
) )
/** manga thumbnail */ /** manga thumbnail */
val thumbnail = handler( fun thumbnail(ctx: Context) {
pathParam<Int>("mangaId"), val mangaId = ctx.pathParam("mangaId").toInt()
queryParam("useCache", true), val useCache = ctx.queryParam("useCache")?.toBoolean() ?: true
documentWith = {
withOperation {
summary("Get a manga thumbnail")
description("Get a manga thumbnail from the source or the cache.")
}
},
behaviorOf = { ctx, mangaId, useCache ->
ctx.future( ctx.future(
future { Manga.getMangaThumbnail(mangaId, useCache) } future { Manga.getMangaThumbnail(mangaId, useCache) }
.thenApply { .thenApply {
@@ -68,248 +60,121 @@ object MangaController {
it.first it.first
} }
) )
},
withResults = {
mime(HttpCode.OK, "image/*")
httpCode(HttpCode.NOT_FOUND)
} }
)
/** adds the manga to library */ /** adds the manga to library */
val addToLibrary = handler( fun addToLibrary(ctx: Context) {
pathParam<Int>("mangaId"), val mangaId = ctx.pathParam("mangaId").toInt()
documentWith = {
withOperation {
summary("Add manga to library")
description("Use a manga id to add the manga to your library.\nWill do nothing if manga is already in your library.")
}
},
behaviorOf = { ctx, mangaId ->
ctx.future( ctx.future(
future { Library.addMangaToLibrary(mangaId) } future { Library.addMangaToLibrary(mangaId) }
) )
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
} }
)
/** removes the manga from the library */ /** removes the manga from the library */
val removeFromLibrary = handler( fun removeFromLibrary(ctx: Context) {
pathParam<Int>("mangaId"), val mangaId = ctx.pathParam("mangaId").toInt()
documentWith = {
withOperation {
summary("Remove manga to library")
description("Use a manga id to remove the manga to your library.\nWill do nothing if manga not in your library.")
}
},
behaviorOf = { ctx, mangaId ->
ctx.future( ctx.future(
future { Library.removeMangaFromLibrary(mangaId) } future { Library.removeMangaFromLibrary(mangaId) }
) )
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
} }
)
/** list manga's categories */ /** list manga's categories */
val categoryList = handler( fun categoryList(ctx: Context) {
pathParam<Int>("mangaId"), val mangaId = ctx.pathParam("mangaId").toInt()
documentWith = {
withOperation {
summary("Get a manga's categories")
description("Get the list of categories for this manga")
}
},
behaviorOf = { ctx, mangaId ->
ctx.json(CategoryManga.getMangaCategories(mangaId)) ctx.json(CategoryManga.getMangaCategories(mangaId))
},
withResults = {
json<List<CategoryDataClass>>(HttpCode.OK)
} }
)
/** adds the manga to category */ /** adds the manga to category */
val addToCategory = handler( fun addToCategory(ctx: Context) {
pathParam<Int>("mangaId"), val mangaId = ctx.pathParam("mangaId").toInt()
pathParam<Int>("categoryId"), val categoryId = ctx.pathParam("categoryId").toInt()
documentWith = {
withOperation {
summary("Add manga to category")
description("Add a manga to a category using their ids.")
}
},
behaviorOf = { ctx, mangaId, categoryId ->
CategoryManga.addMangaToCategory(mangaId, categoryId) CategoryManga.addMangaToCategory(mangaId, categoryId)
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
} }
)
/** removes the manga from the category */ /** removes the manga from the category */
val removeFromCategory = handler( fun removeFromCategory(ctx: Context) {
pathParam<Int>("mangaId"), val mangaId = ctx.pathParam("mangaId").toInt()
pathParam<Int>("categoryId"), val categoryId = ctx.pathParam("categoryId").toInt()
documentWith = {
withOperation {
summary("Remove manga from category")
description("Remove a manga from a category using their ids.")
}
},
behaviorOf = { ctx, mangaId, categoryId ->
CategoryManga.removeMangaFromCategory(mangaId, categoryId) CategoryManga.removeMangaFromCategory(mangaId, categoryId)
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
} }
)
/** used to modify a manga's meta parameters */ /** used to modify a manga's meta parameters */
val meta = handler( fun meta(ctx: Context) {
pathParam<Int>("mangaId"), val mangaId = ctx.pathParam("mangaId").toInt()
formParam<String>("key"),
formParam<String>("value"), val key = ctx.formParam("key")!!
documentWith = { val value = ctx.formParam("value")!!
withOperation {
summary("Add data to manga")
description("A simple Key-Value storage in the manga object, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, mangaId, key, value ->
Manga.modifyMangaMeta(mangaId, key, value) Manga.modifyMangaMeta(mangaId, key, value)
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
} }
)
/** get chapter list when showing a manga */ /** get chapter list when showing a manga */
val chapterList = handler( fun chapterList(ctx: Context) {
pathParam<Int>("mangaId"), val mangaId = ctx.pathParam("mangaId").toInt()
queryParam("onlineFetch", false),
documentWith = { val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean() ?: false
withOperation {
summary("Get manga chapter list")
description("Get the manga chapter list from the database or online. If there is no chapters in the database it fetches the chapters online. Use onlineFetch to update chapter list.")
}
},
behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future(future { Chapter.getChapterList(mangaId, onlineFetch) }) ctx.future(future { Chapter.getChapterList(mangaId, onlineFetch) })
},
withResults = {
json<List<ChapterDataClass>>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
} }
)
/** used to display a chapter, get a chapter in order to show its pages */ /** used to display a chapter, get a chapter in order to show its pages */
val chapterRetrieve = handler( fun chapterRetrieve(ctx: Context) {
pathParam<Int>("mangaId"), val chapterIndex = ctx.pathParam("chapterIndex").toInt()
pathParam<Int>("chapterIndex"), val mangaId = ctx.pathParam("mangaId").toInt()
documentWith = {
withOperation {
summary("Get a chapter")
description("Get the chapter from the manga id and chapter index. It will also retrieve the pages for this chapter.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex ->
ctx.future(future { getChapterDownloadReady(chapterIndex, mangaId) }) ctx.future(future { getChapterDownloadReady(chapterIndex, mangaId) })
},
withResults = {
json<ChapterDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
} }
)
/** used to modify a chapter's parameters */ /** used to modify a chapter's parameters */
val chapterModify = handler( fun chapterModify(ctx: Context) {
pathParam<Int>("mangaId"), val chapterIndex = ctx.pathParam("chapterIndex").toInt()
pathParam<Int>("chapterIndex"), val mangaId = ctx.pathParam("mangaId").toInt()
formParam<Boolean?>("read"),
formParam<Boolean?>("bookmarked"), val read = ctx.formParam("read")?.toBoolean()
formParam<Boolean?>("markPrevRead"), val bookmarked = ctx.formParam("bookmarked")?.toBoolean()
formParam<Int?>("lastPageRead"), val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean()
documentWith = { val lastPageRead = ctx.formParam("lastPageRead")?.toInt()
withOperation {
summary("Modify a chapter")
description("Update user info for a given chapter, such as read status, bookmarked, and more.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead ->
Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead) Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead)
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
} }
)
/** delete a downloaded chapter */ /** delete a downloaded chapter */
val chapterDelete = handler( fun chapterDelete(ctx: Context) {
pathParam<Int>("mangaId"), val chapterIndex = ctx.pathParam("chapterIndex").toInt()
pathParam<Int>("chapterIndex"), val mangaId = ctx.pathParam("mangaId").toInt()
documentWith = {
withOperation {
summary("Delete a chapter download")
description("Delete the downloaded chapter and its files.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex ->
Chapter.deleteChapter(mangaId, chapterIndex) Chapter.deleteChapter(mangaId, chapterIndex)
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
} }
)
/** used to modify a chapter's meta parameters */ /** used to modify a chapter's meta parameters */
val chapterMeta = handler( fun chapterMeta(ctx: Context) {
pathParam<Int>("mangaId"), val chapterIndex = ctx.pathParam("chapterIndex").toInt()
pathParam<Int>("chapterIndex"), val mangaId = ctx.pathParam("mangaId").toInt()
formParam<String>("key"),
formParam<String>("value"), val key = ctx.formParam("key")!!
documentWith = { val value = ctx.formParam("value")!!
withOperation {
summary("Add data to chapter")
description("A simple Key-Value storage in the chapter object, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex, key, value ->
Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value) Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value)
ctx.status(200) ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
} }
)
/** get page at index "index" */ /** get page at index "index" */
val pageRetrieve = handler( fun pageRetrieve(ctx: Context) {
pathParam<Int>("mangaId"), val mangaId = ctx.pathParam("mangaId").toInt()
pathParam<Int>("chapterIndex"), val chapterIndex = ctx.pathParam("chapterIndex").toInt()
pathParam<Int>("index"), val index = ctx.pathParam("index").toInt()
queryParam("useCache", true), val useCache = ctx.queryParam("useCache")?.toBoolean() ?: true
documentWith = {
withOperation {
summary("Get a chapter page")
description("Get a chapter page for a given index. Cache use can be disabled so it only retrieves it directly from the source.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex, index, useCache ->
ctx.future( ctx.future(
future { Page.getPageImage(mangaId, chapterIndex, index, useCache) } future { Page.getPageImage(mangaId, chapterIndex, index, useCache) }
.thenApply { .thenApply {
@@ -317,10 +182,5 @@ object MangaController {
it.first it.first
} }
) )
},
withResults = {
mime(HttpCode.OK, "image/*")
httpCode(HttpCode.NOT_FOUND)
} }
)
} }

View File

@@ -7,7 +7,7 @@ package suwayomi.tachidesk.manga.controller
* 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 io.javalin.http.HttpCode import io.javalin.http.Context
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.kodein.di.DI import org.kodein.di.DI
@@ -18,158 +18,67 @@ import suwayomi.tachidesk.manga.impl.Search
import suwayomi.tachidesk.manga.impl.Search.FilterChange import suwayomi.tachidesk.manga.impl.Search.FilterChange
import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam
import suwayomi.tachidesk.server.util.withOperation
import javax.sound.sampled.SourceDataLine
object SourceController { object SourceController {
/** list of sources */ /** list of sources */
val list = handler( fun list(ctx: Context) {
documentWith = {
withOperation {
summary("Sources list")
description("List of sources")
}
},
behaviorOf = { ctx ->
ctx.json(Source.getSourceList()) ctx.json(Source.getSourceList())
},
withResults = {
json<List<SourceDataLine>>(HttpCode.OK)
} }
)
/** fetch source with id `sourceId` */ /** fetch source with id `sourceId` */
val retrieve = handler( fun retrieve(ctx: Context) {
pathParam<Long>("sourceId"), val sourceId = ctx.pathParam("sourceId").toLong()
documentWith = {
withOperation {
summary("Source fetch")
description("Fetch source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId ->
ctx.json(Source.getSource(sourceId)!!) ctx.json(Source.getSource(sourceId)!!)
},
withResults = {
json<SourceDataLine>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
} }
)
/** popular mangas from source with id `sourceId` */ /** popular mangas from source with id `sourceId` */
val popular = handler( fun popular(ctx: Context) {
pathParam<Long>("sourceId"), val sourceId = ctx.pathParam("sourceId").toLong()
pathParam<Int>("pageNum"), val pageNum = ctx.pathParam("pageNum").toInt()
documentWith = {
withOperation {
summary("Source popular manga")
description("Popular mangas from source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId, pageNum ->
ctx.future( ctx.future(
future { future {
MangaList.getMangaList(sourceId, pageNum, popular = true) MangaList.getMangaList(sourceId, pageNum, popular = true)
} }
) )
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
} }
)
/** latest mangas from source with id `sourceId` */ /** latest mangas from source with id `sourceId` */
val latest = handler( fun latest(ctx: Context) {
pathParam<Long>("sourceId"), val sourceId = ctx.pathParam("sourceId").toLong()
pathParam<Int>("pageNum"), val pageNum = ctx.pathParam("pageNum").toInt()
documentWith = {
withOperation {
summary("Source latest manga")
description("Latest mangas from source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId, pageNum ->
ctx.future( ctx.future(
future { future {
MangaList.getMangaList(sourceId, pageNum, popular = false) MangaList.getMangaList(sourceId, pageNum, popular = false)
} }
) )
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
} }
)
/** fetch preferences of source with id `sourceId` */ /** fetch preferences of source with id `sourceId` */
val getPreferences = handler( fun getPreferences(ctx: Context) {
pathParam<Long>("sourceId"), val sourceId = ctx.pathParam("sourceId").toLong()
documentWith = {
withOperation {
summary("Source preferences")
description("Fetch preferences of source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId ->
ctx.json(Source.getSourcePreferences(sourceId)) ctx.json(Source.getSourcePreferences(sourceId))
},
withResults = {
json<List<Source.PreferenceObject>>(HttpCode.OK)
} }
)
/** set one preference of source with id `sourceId` */ /** set one preference of source with id `sourceId` */
val setPreference = handler( fun setPreference(ctx: Context) {
pathParam<Long>("sourceId"), val sourceId = ctx.pathParam("sourceId").toLong()
documentWith = {
withOperation {
summary("Source preference set")
description("Set one preference of source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId ->
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java) val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
ctx.json(Source.setSourcePreference(sourceId, preferenceChange)) ctx.json(Source.setSourcePreference(sourceId, preferenceChange))
},
withResults = {
httpCode(HttpCode.OK)
} }
)
/** fetch filters of source with id `sourceId` */ /** fetch filters of source with id `sourceId` */
val getFilters = handler( fun getFilters(ctx: Context) {
pathParam<Long>("sourceId"), val sourceId = ctx.pathParam("sourceId").toLong()
queryParam("reset", false), val reset = ctx.queryParam("reset")?.toBoolean() ?: false
documentWith = {
withOperation {
summary("Source filters")
description("Fetch filters of source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId, reset ->
ctx.json(Search.getFilterList(sourceId, reset)) ctx.json(Search.getFilterList(sourceId, reset))
},
withResults = {
json<List<Search.FilterObject>>(HttpCode.OK)
} }
)
private val json by DI.global.instance<Json>() private val json by DI.global.instance<Json>()
/** change filters of source with id `sourceId` */ /** change filters of source with id `sourceId` */
val setFilters = handler( fun setFilters(ctx: Context) {
pathParam<Long>("sourceId"), val sourceId = ctx.pathParam("sourceId").toLong()
documentWith = {
withOperation {
summary("Source filters set")
description("Change filters of source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId ->
val filterChange = try { val filterChange = try {
json.decodeFromString<List<FilterChange>>(ctx.body()) json.decodeFromString<List<FilterChange>>(ctx.body())
} catch (e: Exception) { } catch (e: Exception) {
@@ -177,45 +86,19 @@ object SourceController {
} }
ctx.json(Search.setFilter(sourceId, filterChange)) ctx.json(Search.setFilter(sourceId, filterChange))
},
withResults = {
httpCode(HttpCode.OK)
} }
)
/** single source search */ /** single source search */
val searchSingle = handler( fun searchSingle(ctx: Context) {
pathParam<Long>("sourceId"), val sourceId = ctx.pathParam("sourceId").toLong()
queryParam("searchTerm", ""), val searchTerm = ctx.queryParam("searchTerm") ?: ""
queryParam("pageNum", 1), val pageNum = ctx.queryParam("pageNum")?.toInt() ?: 1
documentWith = {
withOperation {
summary("Source search")
description("Single source search")
}
},
behaviorOf = { ctx, sourceId, searchTerm, pageNum ->
ctx.future(future { Search.sourceSearch(sourceId, searchTerm, pageNum) }) ctx.future(future { Search.sourceSearch(sourceId, searchTerm, pageNum) })
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
} }
)
/** all source search */ /** all source search */
val searchAll = handler( fun searchAll(ctx: Context) { // TODO
pathParam<String>("searchTerm"), val searchTerm = ctx.pathParam("searchTerm")
documentWith = {
withOperation {
summary("Source global search")
description("All source search")
}
},
behaviorOf = { ctx, searchTerm -> // TODO
ctx.json(Search.sourceGlobalSearch(searchTerm)) ctx.json(Search.sourceGlobalSearch(searchTerm))
},
withResults = {
httpCode(HttpCode.OK)
} }
)
} }

View File

@@ -1,5 +1,6 @@
package suwayomi.tachidesk.manga.controller package suwayomi.tachidesk.manga.controller
import io.javalin.http.Context
import io.javalin.http.HttpCode import io.javalin.http.HttpCode
import io.javalin.websocket.WsConfig import io.javalin.websocket.WsConfig
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@@ -11,15 +12,10 @@ import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.impl.update.IUpdater import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.manga.impl.update.UpdateStatus
import suwayomi.tachidesk.manga.impl.update.UpdaterSocket import suwayomi.tachidesk.manga.impl.update.UpdaterSocket
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.formParam
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
/* /*
@@ -33,35 +29,18 @@ object UpdateController {
private val logger = KotlinLogging.logger { } private val logger = KotlinLogging.logger { }
/** get recently updated manga chapters */ /** get recently updated manga chapters */
val recentChapters = handler( fun recentChapters(ctx: Context) {
pathParam<Int>("pageNum"), val pageNum = ctx.pathParam("pageNum").toInt()
documentWith = {
withOperation {
summary("Updates fetch")
description("Get recently updated manga chapters")
}
},
behaviorOf = { ctx, pageNum ->
ctx.future( ctx.future(
future { future {
Chapter.getRecentChapters(pageNum) Chapter.getRecentChapters(pageNum)
} }
) )
},
withResults = {
json<PaginatedList<MangaDataClass>>(HttpCode.OK)
} }
)
val categoryUpdate = handler( fun categoryUpdate(ctx: Context) {
formParam<Int?>("categoryId"), val categoryId = ctx.formParam("category")?.toIntOrNull()
documentWith = {
withOperation {
summary("Updater start")
description("Starts the updater")
}
},
behaviorOf = { ctx, categoryId ->
val categoriesForUpdate = ArrayList<CategoryDataClass>() val categoriesForUpdate = ArrayList<CategoryDataClass>()
if (categoryId == null) { if (categoryId == null) {
logger.info { "Adding Library to Update Queue" } logger.info { "Adding Library to Update Queue" }
@@ -73,17 +52,12 @@ object UpdateController {
} else { } else {
logger.info { "No Category found" } logger.info { "No Category found" }
ctx.status(HttpCode.BAD_REQUEST) ctx.status(HttpCode.BAD_REQUEST)
return@handler return
} }
} }
addCategoriesToUpdateQueue(categoriesForUpdate, true) addCategoriesToUpdateQueue(categoriesForUpdate, true)
ctx.status(HttpCode.OK) ctx.status(HttpCode.OK)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.BAD_REQUEST)
} }
)
private fun addCategoriesToUpdateQueue(categories: List<CategoryDataClass>, clear: Boolean = false) { private fun addCategoriesToUpdateQueue(categories: List<CategoryDataClass>, clear: Boolean = false) {
val updater by DI.global.instance<IUpdater>() val updater by DI.global.instance<IUpdater>()
@@ -110,27 +84,15 @@ object UpdateController {
} }
} }
val updateSummary = handler( fun updateSummary(ctx: Context) {
documentWith = {
withOperation {
summary("Updater summary")
description("Gets the latest updater summary")
}
},
behaviorOf = { ctx ->
val updater by DI.global.instance<IUpdater>() val updater by DI.global.instance<IUpdater>()
ctx.json(updater.getStatus().value.getJsonSummary()) ctx.json(updater.getStatus().value.getJsonSummary())
},
withResults = {
json<UpdateStatus>(HttpCode.OK)
} }
)
val reset = handler( val reset = handler(
documentWith = { documentWith = {
withOperation { withOperation {
summary("Updater reset") summary("Stops and resets the Updater")
description("Stops and resets the Updater")
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->

View File

@@ -70,19 +70,12 @@ object CategoryManga {
.slice(ChapterTable.id.count()) .slice(ChapterTable.id.count())
.select { (MangaTable.id eq ChapterTable.manga) and (ChapterTable.isDownloaded eq true) } .select { (MangaTable.id eq ChapterTable.manga) and (ChapterTable.isDownloaded eq true) }
) )
val chapterCountExpression = wrapAsExpression<Long>(
ChapterTable
.slice(ChapterTable.id.count())
.select { (MangaTable.id eq ChapterTable.manga) }
)
val selectedColumns = MangaTable.columns + unreadExpression + downloadExpression + chapterCountExpression
val selectedColumns = MangaTable.columns + unreadExpression + downloadExpression
val transform: (ResultRow) -> MangaDataClass = { val transform: (ResultRow) -> MangaDataClass = {
val dataClass = MangaTable.toDataClass(it) val dataClass = MangaTable.toDataClass(it)
dataClass.unreadCount = it[unreadExpression]?.toInt() dataClass.unreadCount = it[unreadExpression]?.toInt()
dataClass.downloadCount = it[downloadExpression]?.toInt() dataClass.downloadCount = it[downloadExpression]?.toInt()
dataClass.chapterCount = it[chapterCountExpression]?.toInt()
dataClass dataClass
} }
@@ -97,7 +90,7 @@ object CategoryManga {
return transaction { return transaction {
CategoryMangaTable.innerJoin(MangaTable) CategoryMangaTable.innerJoin(MangaTable)
.slice(selectedColumns) .slice(selectedColumns)
.select { (MangaTable.inLibrary eq true) and (CategoryMangaTable.category eq categoryId) } .select { CategoryMangaTable.category eq categoryId }
.map(transform) .map(transform)
} }
} }

View File

@@ -7,6 +7,7 @@ package suwayomi.tachidesk.manga.impl
* 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 org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert 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
@@ -23,15 +24,13 @@ object Library {
if (!manga.inLibrary) { if (!manga.inLibrary) {
transaction { transaction {
val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList() val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList()
val existingCategories = CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.toList()
MangaTable.update({ MangaTable.id eq manga.id }) { MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = true it[inLibrary] = true
it[inLibraryAt] = Instant.now().epochSecond it[inLibraryAt] = Instant.now().epochSecond
it[defaultCategory] = defaultCategories.isEmpty() && existingCategories.isEmpty() it[defaultCategory] = defaultCategories.isEmpty()
} }
if (existingCategories.isEmpty()) {
defaultCategories.forEach { category -> defaultCategories.forEach { category ->
CategoryMangaTable.insert { CategoryMangaTable.insert {
it[CategoryMangaTable.category] = category[CategoryTable.id].value it[CategoryMangaTable.category] = category[CategoryTable.id].value
@@ -41,7 +40,6 @@ object Library {
} }
} }
} }
}
suspend fun removeMangaFromLibrary(mangaId: Int) { suspend fun removeMangaFromLibrary(mangaId: Int) {
val manga = getManga(mangaId) val manga = getManga(mangaId)
@@ -49,7 +47,9 @@ object Library {
transaction { transaction {
MangaTable.update({ MangaTable.id eq manga.id }) { MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = false it[inLibrary] = false
} it[defaultCategory] = true
}
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
} }
} }
} }

View File

@@ -36,8 +36,7 @@ data class MangaDataClass(
val freshData: Boolean = false, val freshData: Boolean = false,
var unreadCount: Int? = null, var unreadCount: Int? = null,
var downloadCount: Int? = null, var downloadCount: Int? = null
var chapterCount: Int? = null
) )
data class PagedMangaListDataClass( data class PagedMangaListDataClass(

View File

@@ -26,7 +26,7 @@ import org.kodein.di.instance
import suwayomi.tachidesk.global.GlobalAPI import suwayomi.tachidesk.global.GlobalAPI
import suwayomi.tachidesk.manga.MangaAPI import suwayomi.tachidesk.manga.MangaAPI
import suwayomi.tachidesk.server.util.Browser import suwayomi.tachidesk.server.util.Browser
import suwayomi.tachidesk.server.util.setupWebInterface import suwayomi.tachidesk.server.util.setupWebUI
import java.io.IOException import java.io.IOException
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.concurrent.thread import kotlin.concurrent.thread
@@ -45,9 +45,9 @@ object JavalinSetup {
fun javalinSetup() { fun javalinSetup() {
val app = Javalin.create { config -> val app = Javalin.create { config ->
if (serverConfig.webUIEnabled) { if (serverConfig.webUIEnabled) {
setupWebInterface() setupWebUI()
logger.info { "Serving web static files for ${serverConfig.webUIFlavor}" } logger.info { "Serving webUI static files" }
config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL) config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL)
config.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL) config.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL)
config.registerPlugin(OpenApiPlugin(getOpenApiOptions())) config.registerPlugin(OpenApiPlugin(getOpenApiOptions()))
@@ -56,14 +56,16 @@ object JavalinSetup {
config.enableCorsForAllOrigins() config.enableCorsForAllOrigins()
config.accessManager { handler, ctx, _ -> config.accessManager { handler, ctx, _ ->
fun credentialsValid(): Boolean { fun basicAuthCredentialsValid(): Boolean {
val (username, password) = ctx.basicAuthCredentials() val (username, password) = ctx.basicAuthCredentials()
return username == serverConfig.basicAuthUsername && password == serverConfig.basicAuthPassword return username == serverConfig.basicAuthUsername && password == serverConfig.basicAuthPassword
} }
if (serverConfig.basicAuthEnabled && !(ctx.basicAuthCredentialsExist() && credentialsValid())) { if (serverConfig.authType != "none") {
if (serverConfig.authType == "basicAuth" && !(ctx.basicAuthCredentialsExist() && basicAuthCredentialsValid())) {
ctx.header("WWW-Authenticate", "Basic") ctx.header("WWW-Authenticate", "Basic")
ctx.status(401).json("Unauthorized") ctx.status(401).json("Unauthorized")
}
} else { } else {
handler.handle(ctx) handler.handle(ctx)
} }

View File

@@ -11,6 +11,7 @@ import com.typesafe.config.Config
import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.GlobalConfigManager
import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule
import xyz.nulldev.ts.config.debugLogsEnabled import xyz.nulldev.ts.config.debugLogsEnabled
import kotlin.reflect.KProperty
private const val MODULE_NAME = "server" private const val MODULE_NAME = "server"
class ServerConfig(config: Config, moduleName: String = MODULE_NAME) : SystemPropertyOverridableConfigModule(config, moduleName) { class ServerConfig(config: Config, moduleName: String = MODULE_NAME) : SystemPropertyOverridableConfigModule(config, moduleName) {
@@ -26,16 +27,23 @@ class ServerConfig(config: Config, moduleName: String = MODULE_NAME) : SystemPro
// misc // misc
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config) val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
val systemTrayEnabled: Boolean by overridableConfig val systemTrayEnabled: Boolean by overridableConfig
val downloadsPath: String by overridableConfig
// webUI // webUI
val webUIEnabled: Boolean by overridableConfig val webUIEnabled: Boolean by overridableConfig
val webUIFlavor: String by overridableConfig
val initialOpenInBrowserEnabled: Boolean by overridableConfig val initialOpenInBrowserEnabled: Boolean by overridableConfig
val webUIInterface: String by overridableConfig val webUIInterface: String by overridableConfig
val electronPath: String by overridableConfig val electronPath: String by overridableConfig
// Authentication // Authentication
val authType: String by object {
operator fun <R> getValue(thisRef: R, property: KProperty<*>): String {
val propValue: String = overridableConfig.getValue(thisRef, property)
if (basicAuthEnabled) {
return "basicAuth"
}
return propValue
}
}
val basicAuthEnabled: Boolean by overridableConfig val basicAuthEnabled: Boolean by overridableConfig
val basicAuthUsername: String by overridableConfig val basicAuthUsername: String by overridableConfig
val basicAuthPassword: String by overridableConfig val basicAuthPassword: String by overridableConfig

View File

@@ -13,7 +13,6 @@ import io.javalin.plugin.json.JavalinJackson
import io.javalin.plugin.json.JsonMapper import io.javalin.plugin.json.JsonMapper
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import mu.KotlinLogging import mu.KotlinLogging
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.bind import org.kodein.di.bind
import org.kodein.di.conf.global import org.kodein.di.conf.global
@@ -30,7 +29,6 @@ import xyz.nulldev.ts.config.ApplicationRootDir
import xyz.nulldev.ts.config.ConfigKodeinModule import xyz.nulldev.ts.config.ConfigKodeinModule
import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.GlobalConfigManager
import java.io.File import java.io.File
import java.security.Security
import java.util.Locale import java.util.Locale
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -40,7 +38,7 @@ class ApplicationDirs(
) { ) {
val extensionsRoot = "$dataRoot/extensions" val extensionsRoot = "$dataRoot/extensions"
val thumbnailsRoot = "$dataRoot/thumbnails" val thumbnailsRoot = "$dataRoot/thumbnails"
val mangaDownloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" } val mangaDownloadsRoot = "$dataRoot/downloads"
val localMangaRoot = "$dataRoot/local" val localMangaRoot = "$dataRoot/local"
val webUIRoot = "$dataRoot/webUI" val webUIRoot = "$dataRoot/webUI"
} }
@@ -54,11 +52,6 @@ val androidCompat by lazy { AndroidCompat() }
fun applicationSetup() { fun applicationSetup() {
logger.info("Running Tachidesk ${BuildConfig.VERSION} revision ${BuildConfig.REVISION}") logger.info("Running Tachidesk ${BuildConfig.VERSION} revision ${BuildConfig.REVISION}")
// register Tachidesk's config which is dubbed "ServerConfig"
GlobalConfigManager.registerModule(
ServerConfig.register(GlobalConfigManager.config)
)
// Application dirs // Application dirs
val applicationDirs = ApplicationDirs() val applicationDirs = ApplicationDirs()
@@ -76,6 +69,7 @@ fun applicationSetup() {
// Migrate Directories from old versions // Migrate Directories from old versions
File("$ApplicationRootDir/manga-thumbnails").renameTo(applicationDirs.thumbnailsRoot) File("$ApplicationRootDir/manga-thumbnails").renameTo(applicationDirs.thumbnailsRoot)
File("$ApplicationRootDir/manga-local").renameTo(applicationDirs.localMangaRoot) File("$ApplicationRootDir/manga-local").renameTo(applicationDirs.localMangaRoot)
File("$ApplicationRootDir/manga").renameTo(applicationDirs.mangaDownloadsRoot)
File("$ApplicationRootDir/anime-thumbnails").delete() File("$ApplicationRootDir/anime-thumbnails").delete()
// make dirs we need // make dirs we need
@@ -90,6 +84,11 @@ fun applicationSetup() {
File(it).mkdirs() File(it).mkdirs()
} }
// register Tachidesk's config which is dubbed "ServerConfig"
GlobalConfigManager.registerModule(
ServerConfig.register(GlobalConfigManager.config)
)
// Make sure only one instance of the app is running // Make sure only one instance of the app is running
handleAppMutex() handleAppMutex()
@@ -155,7 +154,4 @@ fun applicationSetup() {
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
} }
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
Security.addProvider(BouncyCastleProvider())
} }

View File

@@ -113,7 +113,7 @@ sealed class Param<T> {
} }
class ResultsBuilder { class ResultsBuilder {
val results = mutableListOf<ResultType>() val results = mutableListOf<ResultType<*>>()
inline fun <reified T> json(code: HttpCode) { inline fun <reified T> json(code: HttpCode) {
results += ResultType.MimeType(code, "application/json", T::class.java) results += ResultType.MimeType(code, "application/json", T::class.java)
@@ -121,22 +121,19 @@ class ResultsBuilder {
fun plainText(code: HttpCode) { fun plainText(code: HttpCode) {
results += ResultType.MimeType(code, "text/plain", String::class.java) results += ResultType.MimeType(code, "text/plain", String::class.java)
} }
fun mime(code: HttpCode, mime: String) {
results += ResultType.MimeType(code, mime, null)
}
fun httpCode(code: HttpCode) { fun httpCode(code: HttpCode) {
results += ResultType.StatusCode(code) results += ResultType.StatusCode(code)
} }
} }
sealed class ResultType { sealed class ResultType <T> {
abstract fun applyTo(documentation: OpenApiDocumentation) abstract fun applyTo(documentation: OpenApiDocumentation)
data class MimeType(val code: HttpCode, val mime: String, private val clazz: Class<*>?) : ResultType() { data class MimeType<T>(val code: HttpCode, val mime: String, private val clazz: Class<T>) : ResultType<T>() {
override fun applyTo(documentation: OpenApiDocumentation) { override fun applyTo(documentation: OpenApiDocumentation) {
documentation.result(code.status.toString(), clazz) documentation.result(code.status.toString(), clazz)
} }
} }
data class StatusCode(val code: HttpCode) : ResultType() { data class StatusCode(val code: HttpCode) : ResultType<Unit>() {
override fun applyTo(documentation: OpenApiDocumentation) { override fun applyTo(documentation: OpenApiDocumentation) {
documentation.result<Unit>(code.status.toString()) documentation.result<Unit>(code.status.toString())
} }

View File

@@ -7,9 +7,6 @@ package suwayomi.tachidesk.server.util
* 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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import mu.KotlinLogging import mu.KotlinLogging
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import org.kodein.di.DI import org.kodein.di.DI
@@ -17,8 +14,6 @@ import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.BuildConfig import suwayomi.tachidesk.server.BuildConfig
import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
@@ -28,7 +23,6 @@ import java.security.MessageDigest
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
private val json: Json by injectLazy()
private val tmpDir = System.getProperty("java.io.tmpdir") private val tmpDir = System.getProperty("java.io.tmpdir")
private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
@@ -50,19 +44,6 @@ private fun directoryMD5(fileDir: String): String {
return digest.toHex() return digest.toHex()
} }
/** Make sure a valid web interface installation is available */
fun setupWebInterface() {
when (serverConfig.webUIFlavor) {
"WebUI" -> setupWebUI()
"Sorayomi" -> setupSorayomi()
"Custom" -> {
/* do nothing */
}
else -> setupWebUI()
}
}
/** Make sure a valid copy of WebUI is available */
fun setupWebUI() { fun setupWebUI() {
// check if we have webUI installed and is correct version // check if we have webUI installed and is correct version
val webUIRevisionFile = File(applicationDirs.webUIRoot + "/revision") val webUIRevisionFile = File(applicationDirs.webUIRoot + "/revision")
@@ -136,63 +117,3 @@ fun setupWebUI() {
logger.info { "Extracting WebUI zip Done." } logger.info { "Extracting WebUI zip Done." }
} }
} }
/** Make sure a valid copy of Sorayomi is available */
fun setupSorayomi() {
// check if we have Sorayomi installed and is correct version
val sorayomiVersionFile = File(applicationDirs.webUIRoot + "/version.json")
if (sorayomiVersionFile.exists() && json.parseToJsonElement(
sorayomiVersionFile.readText()
).jsonObject["version"]!!.jsonPrimitive.content == BuildConfig.SORAYOMI_TAG
) {
logger.info { "Sorayomi Static files exists and is the correct revision" }
logger.info { "Verifying Sorayomi Static files..." }
logger.info { "md5: " + directoryMD5(applicationDirs.webUIRoot) }
} else {
File(applicationDirs.webUIRoot).deleteRecursively()
val sorayomiZip = "tachidesk-sorayomi-${BuildConfig.SORAYOMI_TAG}-web.zip"
val sorayomiZipPath = "$tmpDir/$sorayomiZip"
val sorayomiZipFile = File(sorayomiZipPath)
// download sorayomi zip
val sorayomiZipURL = "${BuildConfig.SORAYOMI_REPO}/releases/download/${BuildConfig.SORAYOMI_TAG}/$sorayomiZip"
sorayomiZipFile.delete()
logger.info { "Downloading Sorayomi zip from the Internet..." }
val data = ByteArray(1024)
sorayomiZipFile.outputStream().use { sorayomiZipFileOut ->
val connection = URL(sorayomiZipURL).openConnection() as HttpURLConnection
connection.connect()
val contentLength = connection.contentLength
connection.inputStream.buffered().use { inp ->
var totalCount = 0
print("Download progress: % 00")
while (true) {
val count = inp.read(data, 0, 1024)
if (count == -1)
break
totalCount += count
val percentage = (totalCount.toFloat() / contentLength * 100).toInt().toString().padStart(2, '0')
print("\b\b$percentage")
sorayomiZipFileOut.write(data, 0, count)
}
println()
logger.info { "Downloading Sorayomi Done." }
}
}
// extract Sorayomi zip
logger.info { "Extracting Sorayomi zip..." }
File(applicationDirs.webUIRoot).mkdirs()
ZipFile(sorayomiZipPath).extractAll(applicationDirs.webUIRoot)
logger.info { "Extracting Sorayomi zip Done." }
}
}

View File

@@ -9,17 +9,16 @@ server.socksProxyPort = ""
# webUI # webUI
server.webUIEnabled = true server.webUIEnabled = true
server.webUIFlavor = "WebUI" # "WebUI" or "Sorayomi" or "Custom"
server.initialOpenInBrowserEnabled = true server.initialOpenInBrowserEnabled = true
server.webUIInterface = "browser" # "browser" or "electron" server.webUIInterface = "browser" # "browser" or "electron"
server.electronPath = "" server.electronPath = ""
# Authentication # Authentication
server.basicAuthEnabled = false server.authType = "none" # "none" or "basicAuth" or "token"
server.basicAuthEnabled = false # This is deprecated, use server.authType
server.basicAuthUsername = "" server.basicAuthUsername = ""
server.basicAuthPassword = "" server.basicAuthPassword = ""
# misc # misc
server.debugLogsEnabled = false server.debugLogsEnabled = false
server.systemTrayEnabled = true server.systemTrayEnabled = true
server.downloadsPath = ""