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
## TL;DR
- Changes in Server

View File

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

View File

@@ -54,14 +54,11 @@ dependencies {
// Disk & File
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
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
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
// implementation("tachiyomi.sourceapi:source-api:1.1")
@@ -78,9 +75,6 @@ dependencies {
}
application {
applicationDefaultJvmArgs = listOf(
"-Djunrar.extractor.thread-keep-alive-seconds=30"
)
mainClass.set(MainClass)
}
@@ -110,9 +104,6 @@ buildConfig {
buildConfigField("String", "WEBUI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-WebUI-preview"))
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", "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.util.lang.compareToCaseInsensitiveNaturalOrder
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
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.
@@ -21,40 +22,20 @@ class RarPageLoader(file: File) : PageLoader {
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
* comparator.
*/
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
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, header ->
val streamFn = { archive.getInputStream(header) }
val streamFn = { getStream(header) }
ReaderPage(i).apply {
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 Header(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) {
val displayValues get() = values.map { it.toString() }
}
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(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 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 {
fun defineEndpoints() {
path("settings") {
get("about", SettingsController.about)
get("check-update", SettingsController.checkUpdate)
get("about", SettingsController::about)
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
* 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.AboutDataClass
import suwayomi.tachidesk.global.impl.AppUpdate
import suwayomi.tachidesk.global.impl.UpdateDataClass
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.withOperation
/** Settings Page/Screen */
object SettingsController {
/** returns some static info about the current app build */
val about = handler(
documentWith = {
withOperation {
summary("About Tachidesk")
description("Returns some static info about the current app build")
}
},
behaviorOf = { ctx ->
ctx.json(About.getAbout())
},
withResults = {
json<AboutDataClass>(HttpCode.OK)
}
)
fun about(ctx: Context) {
ctx.json(About.getAbout())
}
/** check for app updates */
val checkUpdate = handler(
documentWith = {
withOperation {
summary("Tachidesk update check")
description("Check for app updates")
}
},
behaviorOf = { ctx ->
ctx.json(
future { AppUpdate.checkUpdate() }
)
},
withResults = {
json<UpdateDataClass>(HttpCode.OK)
}
)
fun checkUpdate(ctx: Context) {
ctx.json(
future { AppUpdate.checkUpdate() }
)
}
}

View File

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

View File

@@ -1,14 +1,11 @@
package suwayomi.tachidesk.manga.controller
import io.javalin.http.HttpCode
import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator
import io.javalin.http.Context
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.withOperation
import java.text.SimpleDateFormat
import java.util.Date
@@ -22,155 +19,78 @@ import java.util.Date
object BackupController {
/** expects a Tachiyomi protobuf backup in the body */
val protobufImport = handler(
documentWith = {
withOperation {
summary("Restore a backup")
description("Expects a Tachiyomi protobuf backup in the body")
fun protobufImport(ctx: Context) {
ctx.future(
future {
ProtoBackupImport.performRestore(ctx.bodyAsInputStream())
}
},
behaviorOf = { ctx ->
ctx.future(
future {
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" */
val protobufImportFile = handler(
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\"")
fun protobufImportFile(ctx: Context) {
// TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz"
ctx.future(
future {
ProtoBackupImport.performRestore(ctx.uploadedFile("backup.proto.gz")!!.content)
}
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"
ctx.future(
future {
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 */
val protobufExport = handler(
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.future(
future {
ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true,
)
fun protobufExport(ctx: Context) {
ctx.contentType("application/octet-stream")
ctx.future(
future {
ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true,
)
}
)
},
withResults = {
mime(HttpCode.OK, "application/octet-stream")
}
)
)
}
)
}
/** returns a Tachiyomi protobuf backup created from the current database as a file */
val protobufExportFile = handler(
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")
val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date())
fun protobufExportFile(ctx: Context) {
ctx.contentType("application/octet-stream")
val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date())
ctx.header("Content-Disposition", """attachment; filename="tachidesk_$currentDate.proto.gz"""")
ctx.future(
future {
ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true,
)
ctx.header("Content-Disposition", """attachment; filename="tachidesk_$currentDate.proto.gz"""")
ctx.future(
future {
ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true,
)
}
)
},
withResults = {
mime(HttpCode.OK, "application/octet-stream")
}
)
)
}
)
}
/** Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body */
val protobufValidate = handler(
documentWith = {
withOperation {
summary("Validate a backup")
description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body")
fun protobufValidate(ctx: Context) {
ctx.future(
future {
ProtoBackupValidator.validate(ctx.bodyAsInputStream())
}
body<ByteArray>("") {
}
},
behaviorOf = { ctx ->
ctx.future(
future {
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" */
val protobufValidateFile = handler(
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\"")
fun protobufValidateFile(ctx: Context) {
ctx.future(
future {
ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content)
}
uploadedFile("backup.proto.gz") {
it.description("Protobuf backup")
it.required(true)
}
},
behaviorOf = { ctx ->
ctx.future(
future {
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
* 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.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 {
/** category list */
val categoryList = handler(
documentWith = {
withOperation {
summary("Category list")
description("get a list of categories")
}
},
behaviorOf = { ctx ->
ctx.json(Category.getCategoryList())
},
withResults = {
json<List<CategoryDataClass>>(HttpCode.OK)
}
)
fun categoryList(ctx: Context) {
ctx.json(Category.getCategoryList())
}
/** category create */
val categoryCreate = handler(
formParam<String>("name"),
documentWith = {
withOperation {
summary("Category create")
description("Create a category")
}
},
behaviorOf = { ctx, name ->
if (Category.createCategory(name) != -1) {
ctx.status(200)
} else {
ctx.status(HttpCode.BAD_REQUEST)
}
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.BAD_REQUEST)
}
)
fun categoryCreate(ctx: Context) {
val name = ctx.formParam("name")!!
Category.createCategory(name)
ctx.status(200)
}
/** category modification */
val categoryModify = handler(
pathParam<Int>("categoryId"),
formParam<String?>("name"),
formParam<Boolean?>("default"),
documentWith = {
withOperation {
summary("Category modify")
description("Modify a category")
}
},
behaviorOf = { ctx, categoryId, name, isDefault ->
Category.updateCategory(categoryId, name, isDefault)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
fun categoryModify(ctx: Context) {
val categoryId = ctx.pathParam("categoryId").toInt()
val name = ctx.formParam("name")
val isDefault = ctx.formParam("default")?.toBoolean()
Category.updateCategory(categoryId, name, isDefault)
ctx.status(200)
}
/** category delete */
val categoryDelete = handler(
pathParam<Int>("categoryId"),
documentWith = {
withOperation {
summary("Category delete")
description("Delete a category")
}
},
behaviorOf = { ctx, categoryId ->
Category.removeCategory(categoryId)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
fun categoryDelete(ctx: Context) {
val categoryId = ctx.pathParam("categoryId").toInt()
Category.removeCategory(categoryId)
ctx.status(200)
}
/** returns the manga list associated with a category */
val categoryMangas = handler(
pathParam<Int>("categoryId"),
documentWith = {
withOperation {
summary("Category manga")
description("Returns the manga list associated with a category")
}
},
behaviorOf = { ctx, categoryId ->
ctx.json(CategoryManga.getCategoryMangaList(categoryId))
},
withResults = {
json<List<MangaDataClass>>(HttpCode.OK)
}
)
fun categoryMangas(ctx: Context) {
val categoryId = ctx.pathParam("categoryId").toInt()
ctx.json(CategoryManga.getCategoryMangaList(categoryId))
}
/** category re-ordering */
val categoryReorder = handler(
formParam<Int>("from"),
formParam<Int>("to"),
documentWith = {
withOperation {
summary("Category re-ordering")
description("Re-order a category")
}
},
behaviorOf = { ctx, from, to ->
Category.reorderCategory(from, to)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
fun categoryReorder(ctx: Context) {
val from = ctx.formParam("from")!!.toInt()
val to = ctx.formParam("to")!!.toInt()
Category.reorderCategory(from, to)
ctx.status(200)
}
}

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

View File

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

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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.http.Context
import io.javalin.http.HttpCode
import suwayomi.tachidesk.manga.impl.CategoryManga
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.Page
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.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.formParam
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam
@@ -32,7 +30,7 @@ object MangaController {
documentWith = {
withOperation {
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 ->
@@ -49,278 +47,140 @@ object MangaController {
)
/** manga thumbnail */
val thumbnail = handler(
pathParam<Int>("mangaId"),
queryParam("useCache", 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(
future { Manga.getMangaThumbnail(mangaId, useCache) }
.thenApply {
ctx.header("content-type", it.second)
val httpCacheSeconds = 60 * 60 * 24
ctx.header("cache-control", "max-age=$httpCacheSeconds")
it.first
}
)
},
withResults = {
mime(HttpCode.OK, "image/*")
httpCode(HttpCode.NOT_FOUND)
}
)
fun thumbnail(ctx: Context) {
val mangaId = ctx.pathParam("mangaId").toInt()
val useCache = ctx.queryParam("useCache")?.toBoolean() ?: true
ctx.future(
future { Manga.getMangaThumbnail(mangaId, useCache) }
.thenApply {
ctx.header("content-type", it.second)
val httpCacheSeconds = 60 * 60 * 24
ctx.header("cache-control", "max-age=$httpCacheSeconds")
it.first
}
)
}
/** adds the manga to library */
val addToLibrary = handler(
pathParam<Int>("mangaId"),
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(
future { Library.addMangaToLibrary(mangaId) }
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
fun addToLibrary(ctx: Context) {
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.future(
future { Library.addMangaToLibrary(mangaId) }
)
}
/** removes the manga from the library */
val removeFromLibrary = handler(
pathParam<Int>("mangaId"),
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(
future { Library.removeMangaFromLibrary(mangaId) }
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
fun removeFromLibrary(ctx: Context) {
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.future(
future { Library.removeMangaFromLibrary(mangaId) }
)
}
/** list manga's categories */
val categoryList = handler(
pathParam<Int>("mangaId"),
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))
},
withResults = {
json<List<CategoryDataClass>>(HttpCode.OK)
}
)
fun categoryList(ctx: Context) {
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(CategoryManga.getMangaCategories(mangaId))
}
/** adds the manga to category */
val addToCategory = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("categoryId"),
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)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
fun addToCategory(ctx: Context) {
val mangaId = ctx.pathParam("mangaId").toInt()
val categoryId = ctx.pathParam("categoryId").toInt()
CategoryManga.addMangaToCategory(mangaId, categoryId)
ctx.status(200)
}
/** removes the manga from the category */
val removeFromCategory = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("categoryId"),
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)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
fun removeFromCategory(ctx: Context) {
val mangaId = ctx.pathParam("mangaId").toInt()
val categoryId = ctx.pathParam("categoryId").toInt()
CategoryManga.removeMangaFromCategory(mangaId, categoryId)
ctx.status(200)
}
/** used to modify a manga's meta parameters */
val meta = handler(
pathParam<Int>("mangaId"),
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
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)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
fun meta(ctx: Context) {
val mangaId = ctx.pathParam("mangaId").toInt()
val key = ctx.formParam("key")!!
val value = ctx.formParam("value")!!
Manga.modifyMangaMeta(mangaId, key, value)
ctx.status(200)
}
/** get chapter list when showing a manga */
val chapterList = handler(
pathParam<Int>("mangaId"),
queryParam("onlineFetch", false),
documentWith = {
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) })
},
withResults = {
json<List<ChapterDataClass>>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
fun chapterList(ctx: Context) {
val mangaId = ctx.pathParam("mangaId").toInt()
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean() ?: false
ctx.future(future { Chapter.getChapterList(mangaId, onlineFetch) })
}
/** used to display a chapter, get a chapter in order to show its pages */
val chapterRetrieve = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
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) })
},
withResults = {
json<ChapterDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
fun chapterRetrieve(ctx: Context) {
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.future(future { getChapterDownloadReady(chapterIndex, mangaId) })
}
/** used to modify a chapter's parameters */
val chapterModify = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
formParam<Boolean?>("read"),
formParam<Boolean?>("bookmarked"),
formParam<Boolean?>("markPrevRead"),
formParam<Int?>("lastPageRead"),
documentWith = {
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)
fun chapterModify(ctx: Context) {
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
val read = ctx.formParam("read")?.toBoolean()
val bookmarked = ctx.formParam("bookmarked")?.toBoolean()
val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean()
val lastPageRead = ctx.formParam("lastPageRead")?.toInt()
Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead)
ctx.status(200)
}
/** delete a downloaded chapter */
val chapterDelete = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
documentWith = {
withOperation {
summary("Delete a chapter download")
description("Delete the downloaded chapter and its files.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex ->
Chapter.deleteChapter(mangaId, chapterIndex)
fun chapterDelete(ctx: Context) {
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
Chapter.deleteChapter(mangaId, chapterIndex)
ctx.status(200)
}
/** used to modify a chapter's meta parameters */
val chapterMeta = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
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)
fun chapterMeta(ctx: Context) {
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
val key = ctx.formParam("key")!!
val value = ctx.formParam("value")!!
Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value)
ctx.status(200)
}
/** get page at index "index" */
val pageRetrieve = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
pathParam<Int>("index"),
queryParam("useCache", 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(
future { Page.getPageImage(mangaId, chapterIndex, index, useCache) }
.thenApply {
ctx.header("content-type", it.second)
it.first
}
)
},
withResults = {
mime(HttpCode.OK, "image/*")
httpCode(HttpCode.NOT_FOUND)
}
)
fun pageRetrieve(ctx: Context) {
val mangaId = ctx.pathParam("mangaId").toInt()
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val index = ctx.pathParam("index").toInt()
val useCache = ctx.queryParam("useCache")?.toBoolean() ?: true
ctx.future(
future { Page.getPageImage(mangaId, chapterIndex, index, useCache) }
.thenApply {
ctx.header("content-type", it.second)
it.first
}
)
}
}

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

View File

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

View File

@@ -70,19 +70,12 @@ object CategoryManga {
.slice(ChapterTable.id.count())
.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 dataClass = MangaTable.toDataClass(it)
dataClass.unreadCount = it[unreadExpression]?.toInt()
dataClass.downloadCount = it[downloadExpression]?.toInt()
dataClass.chapterCount = it[chapterCountExpression]?.toInt()
dataClass
}
@@ -97,7 +90,7 @@ object CategoryManga {
return transaction {
CategoryMangaTable.innerJoin(MangaTable)
.slice(selectedColumns)
.select { (MangaTable.inLibrary eq true) and (CategoryMangaTable.category eq categoryId) }
.select { CategoryMangaTable.category eq categoryId }
.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
* 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.select
import org.jetbrains.exposed.sql.transactions.transaction
@@ -23,20 +24,17 @@ object Library {
if (!manga.inLibrary) {
transaction {
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 }) {
it[inLibrary] = true
it[inLibraryAt] = Instant.now().epochSecond
it[defaultCategory] = defaultCategories.isEmpty() && existingCategories.isEmpty()
it[defaultCategory] = defaultCategories.isEmpty()
}
if (existingCategories.isEmpty()) {
defaultCategories.forEach { category ->
CategoryMangaTable.insert {
it[CategoryMangaTable.category] = category[CategoryTable.id].value
it[CategoryMangaTable.manga] = mangaId
}
defaultCategories.forEach { category ->
CategoryMangaTable.insert {
it[CategoryMangaTable.category] = category[CategoryTable.id].value
it[CategoryMangaTable.manga] = mangaId
}
}
}
@@ -49,7 +47,9 @@ object Library {
transaction {
MangaTable.update({ MangaTable.id eq manga.id }) {
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,
var unreadCount: Int? = null,
var downloadCount: Int? = null,
var chapterCount: Int? = null
var downloadCount: Int? = null
)
data class PagedMangaListDataClass(

View File

@@ -26,7 +26,7 @@ import org.kodein.di.instance
import suwayomi.tachidesk.global.GlobalAPI
import suwayomi.tachidesk.manga.MangaAPI
import suwayomi.tachidesk.server.util.Browser
import suwayomi.tachidesk.server.util.setupWebInterface
import suwayomi.tachidesk.server.util.setupWebUI
import java.io.IOException
import java.util.concurrent.CompletableFuture
import kotlin.concurrent.thread
@@ -45,9 +45,9 @@ object JavalinSetup {
fun javalinSetup() {
val app = Javalin.create { config ->
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.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL)
config.registerPlugin(OpenApiPlugin(getOpenApiOptions()))
@@ -56,14 +56,16 @@ object JavalinSetup {
config.enableCorsForAllOrigins()
config.accessManager { handler, ctx, _ ->
fun credentialsValid(): Boolean {
fun basicAuthCredentialsValid(): Boolean {
val (username, password) = ctx.basicAuthCredentials()
return username == serverConfig.basicAuthUsername && password == serverConfig.basicAuthPassword
}
if (serverConfig.basicAuthEnabled && !(ctx.basicAuthCredentialsExist() && credentialsValid())) {
ctx.header("WWW-Authenticate", "Basic")
ctx.status(401).json("Unauthorized")
if (serverConfig.authType != "none") {
if (serverConfig.authType == "basicAuth" && !(ctx.basicAuthCredentialsExist() && basicAuthCredentialsValid())) {
ctx.header("WWW-Authenticate", "Basic")
ctx.status(401).json("Unauthorized")
}
} else {
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.SystemPropertyOverridableConfigModule
import xyz.nulldev.ts.config.debugLogsEnabled
import kotlin.reflect.KProperty
private const val MODULE_NAME = "server"
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
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
val systemTrayEnabled: Boolean by overridableConfig
val downloadsPath: String by overridableConfig
// webUI
val webUIEnabled: Boolean by overridableConfig
val webUIFlavor: String by overridableConfig
val initialOpenInBrowserEnabled: Boolean by overridableConfig
val webUIInterface: String by overridableConfig
val electronPath: String by overridableConfig
// 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 basicAuthUsername: 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 kotlinx.serialization.json.Json
import mu.KotlinLogging
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.kodein.di.DI
import org.kodein.di.bind
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.GlobalConfigManager
import java.io.File
import java.security.Security
import java.util.Locale
private val logger = KotlinLogging.logger {}
@@ -40,7 +38,7 @@ class ApplicationDirs(
) {
val extensionsRoot = "$dataRoot/extensions"
val thumbnailsRoot = "$dataRoot/thumbnails"
val mangaDownloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" }
val mangaDownloadsRoot = "$dataRoot/downloads"
val localMangaRoot = "$dataRoot/local"
val webUIRoot = "$dataRoot/webUI"
}
@@ -54,11 +52,6 @@ val androidCompat by lazy { AndroidCompat() }
fun applicationSetup() {
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
val applicationDirs = ApplicationDirs()
@@ -76,6 +69,7 @@ fun applicationSetup() {
// Migrate Directories from old versions
File("$ApplicationRootDir/manga-thumbnails").renameTo(applicationDirs.thumbnailsRoot)
File("$ApplicationRootDir/manga-local").renameTo(applicationDirs.localMangaRoot)
File("$ApplicationRootDir/manga").renameTo(applicationDirs.mangaDownloadsRoot)
File("$ApplicationRootDir/anime-thumbnails").delete()
// make dirs we need
@@ -90,6 +84,11 @@ fun applicationSetup() {
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
handleAppMutex()
@@ -155,7 +154,4 @@ fun applicationSetup() {
System.getProperties()["socksProxyPort"] = 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 {
val results = mutableListOf<ResultType>()
val results = mutableListOf<ResultType<*>>()
inline fun <reified T> json(code: HttpCode) {
results += ResultType.MimeType(code, "application/json", T::class.java)
@@ -121,22 +121,19 @@ class ResultsBuilder {
fun plainText(code: HttpCode) {
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) {
results += ResultType.StatusCode(code)
}
}
sealed class ResultType {
sealed class ResultType <T> {
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) {
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) {
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
* 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 net.lingala.zip4j.ZipFile
import org.kodein.di.DI
@@ -17,8 +14,6 @@ import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.BuildConfig
import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
import java.net.HttpURLConnection
@@ -28,7 +23,6 @@ import java.security.MessageDigest
private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
private val json: Json by injectLazy()
private val tmpDir = System.getProperty("java.io.tmpdir")
private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
@@ -50,19 +44,6 @@ private fun directoryMD5(fileDir: String): String {
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() {
// check if we have webUI installed and is correct version
val webUIRevisionFile = File(applicationDirs.webUIRoot + "/revision")
@@ -136,63 +117,3 @@ fun setupWebUI() {
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
server.webUIEnabled = true
server.webUIFlavor = "WebUI" # "WebUI" or "Sorayomi" or "Custom"
server.initialOpenInBrowserEnabled = true
server.webUIInterface = "browser" # "browser" or "electron"
server.electronPath = ""
# Authentication
server.basicAuthEnabled = false
server.authType = "none" # "none" or "basicAuth" or "token"
server.basicAuthEnabled = false # This is deprecated, use server.authType
server.basicAuthUsername = ""
server.basicAuthPassword = ""
# misc
server.debugLogsEnabled = false
server.systemTrayEnabled = true
server.downloadsPath = ""