mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-16 09:14:09 -05:00
Switch to a new Ktlint Formatter (#705)
* Switch to new Ktlint plugin * Add ktlintCheck to PR builds * Run formatter * Put ktlint version in libs toml * Fix lint * Use Zip4Java from libs.toml
This commit is contained in:
@@ -17,154 +17,163 @@ import suwayomi.tachidesk.server.util.withOperation
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
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")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.future(
|
||||
future {
|
||||
ProtoBackupImport.performRestore(ctx.bodyAsInputStream())
|
||||
val protobufImport =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Restore a backup")
|
||||
description("Expects a Tachiyomi protobuf backup in the body")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
},
|
||||
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\"")
|
||||
}
|
||||
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)
|
||||
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\"")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
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
|
||||
)
|
||||
)
|
||||
val protobufExport =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Create a backup")
|
||||
description("Returns a Tachiyomi protobuf backup created from the current database as a body")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
stream(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.contentType("application/octet-stream")
|
||||
ctx.future(
|
||||
future {
|
||||
ProtoBackupExport.createBackup(
|
||||
BackupFlags(
|
||||
includeManga = true,
|
||||
includeCategories = true,
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
stream(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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")
|
||||
|
||||
ctx.header("Content-Disposition", """attachment; filename="${ProtoBackupExport.getBackupFilename()}"""")
|
||||
ctx.future(
|
||||
future {
|
||||
ProtoBackupExport.createBackup(
|
||||
BackupFlags(
|
||||
includeManga = true,
|
||||
includeCategories = true,
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true
|
||||
)
|
||||
)
|
||||
val protobufExportFile =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Create a backup file")
|
||||
description("Returns a Tachiyomi protobuf backup created from the current database as a file")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
stream(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.contentType("application/octet-stream")
|
||||
|
||||
ctx.header("Content-Disposition", """attachment; filename="${ProtoBackupExport.getBackupFilename()}"""")
|
||||
ctx.future(
|
||||
future {
|
||||
ProtoBackupExport.createBackup(
|
||||
BackupFlags(
|
||||
includeManga = true,
|
||||
includeCategories = true,
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
stream(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.future(
|
||||
future {
|
||||
ProtoBackupValidator.validate(ctx.bodyAsInputStream())
|
||||
val protobufValidate =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Validate a backup")
|
||||
description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<ProtoBackupValidator.ValidationResult>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.future(
|
||||
future {
|
||||
ProtoBackupValidator.validate(ctx.bodyAsInputStream())
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<ProtoBackupValidator.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\"")
|
||||
}
|
||||
uploadedFile("backup.proto.gz") {
|
||||
it.description("Protobuf backup")
|
||||
it.required(true)
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.future(
|
||||
future {
|
||||
ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content)
|
||||
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\"",
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<ProtoBackupValidator.ValidationResult>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
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<ProtoBackupValidator.ValidationResult>(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,136 +19,143 @@ 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<Array<CategoryDataClass>>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
val categoryList =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Category list")
|
||||
description("get a list of categories")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.json(Category.getCategoryList())
|
||||
},
|
||||
withResults = {
|
||||
json<Array<CategoryDataClass>>(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
}
|
||||
)
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
/** category modification */
|
||||
val categoryModify = handler(
|
||||
pathParam<Int>("categoryId"),
|
||||
formParam<String?>("name"),
|
||||
formParam<Boolean?>("default"),
|
||||
formParam<Int?>("includeInUpdate"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Category modify")
|
||||
description("Modify a category")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate ->
|
||||
Category.updateCategory(categoryId, name, isDefault, includeInUpdate)
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
val categoryModify =
|
||||
handler(
|
||||
pathParam<Int>("categoryId"),
|
||||
formParam<String?>("name"),
|
||||
formParam<Boolean?>("default"),
|
||||
formParam<Int?>("includeInUpdate"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Category modify")
|
||||
description("Modify a category")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate ->
|
||||
Category.updateCategory(categoryId, name, isDefault, includeInUpdate)
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
}
|
||||
)
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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<Array<MangaDataClass>>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
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<Array<MangaDataClass>>(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
}
|
||||
)
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
/** used to modify a category's meta parameters */
|
||||
val meta = handler(
|
||||
pathParam<Int>("categoryId"),
|
||||
formParam<String>("key"),
|
||||
formParam<String>("value"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Add meta data to category")
|
||||
description("A simple Key-Value storage in the manga object, you can set values for whatever you want inside it.")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, categoryId, key, value ->
|
||||
Category.modifyMeta(categoryId, key, value)
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
val meta =
|
||||
handler(
|
||||
pathParam<Int>("categoryId"),
|
||||
formParam<String>("key"),
|
||||
formParam<String>("value"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Add meta data to category")
|
||||
description("A simple Key-Value storage in the manga object, you can set values for whatever you want inside it.")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, categoryId, key, value ->
|
||||
Category.modifyMeta(categoryId, key, value)
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,159 +39,167 @@ object DownloadController {
|
||||
}
|
||||
|
||||
/** Start the downloader */
|
||||
val start = handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader start")
|
||||
description("Start the downloader")
|
||||
}
|
||||
},
|
||||
behaviorOf = {
|
||||
DownloadManager.start()
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
val start =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader start")
|
||||
description("Start the downloader")
|
||||
}
|
||||
},
|
||||
behaviorOf = {
|
||||
DownloadManager.start()
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** Stop the downloader */
|
||||
val stop = handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader stop")
|
||||
description("Stop the downloader")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.future(
|
||||
future { DownloadManager.stop() }
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
val stop =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader stop")
|
||||
description("Stop the downloader")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.future(
|
||||
future { DownloadManager.stop() },
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** clear download queue */
|
||||
val clear = handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader clear")
|
||||
description("Clear download queue")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.future(
|
||||
future { DownloadManager.clear() }
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
val clear =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader clear")
|
||||
description("Clear download queue")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.future(
|
||||
future { DownloadManager.clear() },
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** Queue single chapter for download */
|
||||
val queueChapter = handler(
|
||||
pathParam<Int>("chapterIndex"),
|
||||
pathParam<Int>("mangaId"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader add single chapter")
|
||||
description("Queue single chapter for download")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, chapterIndex, mangaId ->
|
||||
ctx.future(
|
||||
future {
|
||||
DownloadManager.enqueueWithChapterIndex(mangaId, chapterIndex)
|
||||
val queueChapter =
|
||||
handler(
|
||||
pathParam<Int>("chapterIndex"),
|
||||
pathParam<Int>("mangaId"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader add single chapter")
|
||||
description("Queue single chapter for download")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
},
|
||||
behaviorOf = { ctx, chapterIndex, mangaId ->
|
||||
ctx.future(
|
||||
future {
|
||||
DownloadManager.enqueueWithChapterIndex(mangaId, chapterIndex)
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
|
||||
val queueChapters = handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader add multiple chapters")
|
||||
description("Queue multiple chapters for download")
|
||||
}
|
||||
body<EnqueueInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
val inputs = json.decodeFromString<EnqueueInput>(ctx.body())
|
||||
ctx.future(
|
||||
future {
|
||||
DownloadManager.enqueue(inputs)
|
||||
val queueChapters =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader add multiple chapters")
|
||||
description("Queue multiple chapters for download")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
body<EnqueueInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
val inputs = json.decodeFromString<EnqueueInput>(ctx.body())
|
||||
ctx.future(
|
||||
future {
|
||||
DownloadManager.enqueue(inputs)
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** delete multiple chapters from download queue */
|
||||
val unqueueChapters = handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader remove multiple downloads")
|
||||
description("Remove multiple chapters downloads from queue")
|
||||
}
|
||||
body<EnqueueInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
val input = json.decodeFromString<EnqueueInput>(ctx.body())
|
||||
ctx.future(
|
||||
future {
|
||||
DownloadManager.dequeue(input)
|
||||
val unqueueChapters =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader remove multiple downloads")
|
||||
description("Remove multiple chapters downloads from queue")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
body<EnqueueInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
val input = json.decodeFromString<EnqueueInput>(ctx.body())
|
||||
ctx.future(
|
||||
future {
|
||||
DownloadManager.dequeue(input)
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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.dequeue(chapterIndex, mangaId)
|
||||
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.dequeue(chapterIndex, mangaId)
|
||||
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** clear download queue */
|
||||
val reorderChapter = handler(
|
||||
pathParam<Int>("chapterIndex"),
|
||||
pathParam<Int>("mangaId"),
|
||||
pathParam<Int>("to"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader reorder chapter")
|
||||
description("Reorder chapter in download queue")
|
||||
}
|
||||
},
|
||||
behaviorOf = { _, chapterIndex, mangaId, to ->
|
||||
DownloadManager.reorder(chapterIndex, mangaId, to)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
val reorderChapter =
|
||||
handler(
|
||||
pathParam<Int>("chapterIndex"),
|
||||
pathParam<Int>("mangaId"),
|
||||
pathParam<Int>("to"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Downloader reorder chapter")
|
||||
description("Reorder chapter in download queue")
|
||||
}
|
||||
},
|
||||
behaviorOf = { _, chapterIndex, mangaId, to ->
|
||||
DownloadManager.reorder(chapterIndex, mangaId, to)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,143 +21,149 @@ object ExtensionController {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/** list all extensions */
|
||||
val list = handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Extension list")
|
||||
description("List all extensions")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.future(
|
||||
future {
|
||||
ExtensionsList.getExtensionList()
|
||||
val list =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Extension list")
|
||||
description("List all extensions")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<Array<ExtensionDataClass>>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.future(
|
||||
future {
|
||||
ExtensionsList.getExtensionList()
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<Array<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\"")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pkgName ->
|
||||
ctx.future(
|
||||
future {
|
||||
Extension.installExtension(pkgName)
|
||||
val install =
|
||||
handler(
|
||||
pathParam<String>("pkgName"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Extension install")
|
||||
description("install extension identified with \"pkgName\"")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.CREATED)
|
||||
httpCode(HttpCode.FOUND)
|
||||
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
)
|
||||
},
|
||||
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 }
|
||||
|
||||
ctx.future(
|
||||
future {
|
||||
Extension.installExternalExtension(uploadedFile.content, uploadedFile.filename)
|
||||
val installFile =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Extension install apk")
|
||||
description("Install the uploaded apk file")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.CREATED)
|
||||
httpCode(HttpCode.FOUND)
|
||||
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
)
|
||||
uploadedFile("file") {
|
||||
it.description("Extension apk")
|
||||
it.required(true)
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
val uploadedFile = ctx.uploadedFile("file")!!
|
||||
logger.debug { "Uploaded extension file name: " + uploadedFile.filename }
|
||||
|
||||
ctx.future(
|
||||
future {
|
||||
Extension.installExternalExtension(uploadedFile.content, uploadedFile.filename)
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.CREATED)
|
||||
httpCode(HttpCode.FOUND)
|
||||
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
|
||||
},
|
||||
)
|
||||
|
||||
/** update extension identified with "pkgName" */
|
||||
val update = handler(
|
||||
pathParam<String>("pkgName"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Extension update")
|
||||
description("Update extension identified with \"pkgName\"")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pkgName ->
|
||||
ctx.future(
|
||||
future {
|
||||
Extension.updateExtension(pkgName)
|
||||
val update =
|
||||
handler(
|
||||
pathParam<String>("pkgName"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Extension update")
|
||||
description("Update extension identified with \"pkgName\"")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.CREATED)
|
||||
httpCode(HttpCode.FOUND)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
)
|
||||
},
|
||||
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)
|
||||
}
|
||||
)
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
/** icon for extension named `apkName` */
|
||||
val icon = handler(
|
||||
pathParam<String>("apkName"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Extension icon")
|
||||
description("Icon for extension named `apkName`")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, apkName ->
|
||||
ctx.future(
|
||||
future { Extension.getExtensionIcon(apkName) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
it.first
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
image(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
val icon =
|
||||
handler(
|
||||
pathParam<String>("apkName"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Extension icon")
|
||||
description("Icon for extension named `apkName`")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, apkName ->
|
||||
ctx.future(
|
||||
future { Extension.getExtensionIcon(apkName) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
it.first
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
image(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,367 +32,390 @@ import kotlin.time.Duration.Companion.days
|
||||
object MangaController {
|
||||
private val json by DI.global.instance<Json>()
|
||||
|
||||
val retrieve = handler(
|
||||
pathParam<Int>("mangaId"),
|
||||
queryParam("onlineFetch", false),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Get manga info")
|
||||
description("Get a manga from the database using a specific id.")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, onlineFetch ->
|
||||
ctx.future(
|
||||
future {
|
||||
Manga.getManga(mangaId, onlineFetch)
|
||||
val retrieve =
|
||||
handler(
|
||||
pathParam<Int>("mangaId"),
|
||||
queryParam("onlineFetch", false),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Get manga info")
|
||||
description("Get a manga from the database using a specific id.")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<MangaDataClass>(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, onlineFetch ->
|
||||
ctx.future(
|
||||
future {
|
||||
Manga.getManga(mangaId, onlineFetch)
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<MangaDataClass>(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
|
||||
/** get manga info with all data filled in */
|
||||
val retrieveFull = handler(
|
||||
pathParam<Int>("mangaId"),
|
||||
queryParam("onlineFetch", false),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Get manga info with all data filled in")
|
||||
description("Get a manga from the database using a specific id.")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, onlineFetch ->
|
||||
ctx.future(
|
||||
future {
|
||||
Manga.getMangaFull(mangaId, onlineFetch)
|
||||
val retrieveFull =
|
||||
handler(
|
||||
pathParam<Int>("mangaId"),
|
||||
queryParam("onlineFetch", false),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Get manga info with all data filled in")
|
||||
description("Get a manga from the database using a specific id.")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<MangaDataClass>(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, onlineFetch ->
|
||||
ctx.future(
|
||||
future {
|
||||
Manga.getMangaFull(mangaId, onlineFetch)
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<MangaDataClass>(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
|
||||
/** manga thumbnail */
|
||||
val thumbnail = handler(
|
||||
pathParam<Int>("mangaId"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Get a manga thumbnail")
|
||||
description("Get a manga thumbnail from the source or the cache.")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId ->
|
||||
ctx.future(
|
||||
future { Manga.getMangaThumbnail(mangaId) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
val httpCacheSeconds = 1.days.inWholeSeconds
|
||||
ctx.header("cache-control", "max-age=$httpCacheSeconds")
|
||||
it.first
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
image(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
val thumbnail =
|
||||
handler(
|
||||
pathParam<Int>("mangaId"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Get a manga thumbnail")
|
||||
description("Get a manga thumbnail from the source or the cache.")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId ->
|
||||
ctx.future(
|
||||
future { Manga.getMangaThumbnail(mangaId) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
val httpCacheSeconds = 1.days.inWholeSeconds
|
||||
ctx.header("cache-control", "max-age=$httpCacheSeconds")
|
||||
it.first
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
image(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
}
|
||||
)
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
}
|
||||
)
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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<Array<CategoryDataClass>>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
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<Array<CategoryDataClass>>(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
}
|
||||
)
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
}
|
||||
)
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
}
|
||||
)
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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<Array<ChapterDataClass>>(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
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<Array<ChapterDataClass>>(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
|
||||
/** batch edit chapters of single manga */
|
||||
val chapterBatch = handler(
|
||||
pathParam<Int>("mangaId"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Chapters update multiple")
|
||||
description("Update multiple chapters of single manga. For batch marking as read, or bookmarking")
|
||||
}
|
||||
body<Chapter.MangaChapterBatchEditInput>()
|
||||
},
|
||||
behaviorOf = { ctx, mangaId ->
|
||||
val input = json.decodeFromString<Chapter.MangaChapterBatchEditInput>(ctx.body())
|
||||
Chapter.modifyChapters(input, mangaId)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
val chapterBatch =
|
||||
handler(
|
||||
pathParam<Int>("mangaId"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Chapters update multiple")
|
||||
description("Update multiple chapters of single manga. For batch marking as read, or bookmarking")
|
||||
}
|
||||
body<Chapter.MangaChapterBatchEditInput>()
|
||||
},
|
||||
behaviorOf = { ctx, mangaId ->
|
||||
val input = json.decodeFromString<Chapter.MangaChapterBatchEditInput>(ctx.body())
|
||||
Chapter.modifyChapters(input, mangaId)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** batch edit chapters from multiple manga */
|
||||
val anyChapterBatch = handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Chapters update multiple")
|
||||
description("Update multiple chapters on any manga. For batch marking as read, or bookmarking")
|
||||
}
|
||||
body<Chapter.ChapterBatchEditInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
val input = json.decodeFromString<Chapter.ChapterBatchEditInput>(ctx.body())
|
||||
Chapter.modifyChapters(
|
||||
Chapter.MangaChapterBatchEditInput(
|
||||
input.chapterIds,
|
||||
null,
|
||||
input.change
|
||||
val anyChapterBatch =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Chapters update multiple")
|
||||
description("Update multiple chapters on any manga. For batch marking as read, or bookmarking")
|
||||
}
|
||||
body<Chapter.ChapterBatchEditInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
val input = json.decodeFromString<Chapter.ChapterBatchEditInput>(ctx.body())
|
||||
Chapter.modifyChapters(
|
||||
Chapter.MangaChapterBatchEditInput(
|
||||
input.chapterIds,
|
||||
null,
|
||||
input.change,
|
||||
),
|
||||
)
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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 { getChapterDownloadReadyByIndex(chapterIndex, mangaId) })
|
||||
},
|
||||
withResults = {
|
||||
json<ChapterDataClass>(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
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 { getChapterDownloadReadyByIndex(chapterIndex, mangaId) })
|
||||
},
|
||||
withResults = {
|
||||
json<ChapterDataClass>(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
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)
|
||||
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
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)
|
||||
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
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)
|
||||
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
|
||||
/** get page at index "index" */
|
||||
val pageRetrieve = handler(
|
||||
pathParam<Int>("mangaId"),
|
||||
pathParam<Int>("chapterIndex"),
|
||||
pathParam<Int>("index"),
|
||||
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 ->
|
||||
ctx.future(
|
||||
future { Page.getPageImage(mangaId, chapterIndex, index) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
val httpCacheSeconds = 1.days.inWholeSeconds
|
||||
ctx.header("cache-control", "max-age=$httpCacheSeconds")
|
||||
it.first
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
image(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
val pageRetrieve =
|
||||
handler(
|
||||
pathParam<Int>("mangaId"),
|
||||
pathParam<Int>("chapterIndex"),
|
||||
pathParam<Int>("index"),
|
||||
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 ->
|
||||
ctx.future(
|
||||
future { Page.getPageImage(mangaId, chapterIndex, index) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
val httpCacheSeconds = 1.days.inWholeSeconds
|
||||
ctx.header("cache-control", "max-age=$httpCacheSeconds")
|
||||
it.first
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
image(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,217 +29,229 @@ import suwayomi.tachidesk.server.util.withOperation
|
||||
|
||||
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<Array<SourceDataClass>>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
val list =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Sources list")
|
||||
description("List of sources")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.json(Source.getSourceList())
|
||||
},
|
||||
withResults = {
|
||||
json<Array<SourceDataClass>>(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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<SourceDataClass>(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
}
|
||||
)
|
||||
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<SourceDataClass>(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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`")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, sourceId, pageNum ->
|
||||
ctx.future(
|
||||
future {
|
||||
MangaList.getMangaList(sourceId, pageNum, popular = true)
|
||||
val popular =
|
||||
handler(
|
||||
pathParam<Long>("sourceId"),
|
||||
pathParam<Int>("pageNum"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Source popular manga")
|
||||
description("Popular mangas from source with id `sourceId`")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<PagedMangaListDataClass>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
},
|
||||
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`")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, sourceId, pageNum ->
|
||||
ctx.future(
|
||||
future {
|
||||
MangaList.getMangaList(sourceId, pageNum, popular = false)
|
||||
val latest =
|
||||
handler(
|
||||
pathParam<Long>("sourceId"),
|
||||
pathParam<Int>("pageNum"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Source latest manga")
|
||||
description("Latest mangas from source with id `sourceId`")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<PagedMangaListDataClass>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
},
|
||||
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<Array<Source.PreferenceObject>>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
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<Array<Source.PreferenceObject>>(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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`")
|
||||
}
|
||||
body<SourcePreferenceChange>()
|
||||
},
|
||||
behaviorOf = { ctx, sourceId ->
|
||||
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
|
||||
ctx.json(Source.setSourcePreference(sourceId, preferenceChange.position, preferenceChange.value))
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
val setPreference =
|
||||
handler(
|
||||
pathParam<Long>("sourceId"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Source preference set")
|
||||
description("Set one preference of source with id `sourceId`")
|
||||
}
|
||||
body<SourcePreferenceChange>()
|
||||
},
|
||||
behaviorOf = { ctx, sourceId ->
|
||||
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
|
||||
ctx.json(Source.setSourcePreference(sourceId, preferenceChange.position, preferenceChange.value))
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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<Array<Search.FilterObject>>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
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<Array<Search.FilterObject>>(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
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`")
|
||||
}
|
||||
body<FilterChange>()
|
||||
body<Array<FilterChange>>()
|
||||
},
|
||||
behaviorOf = { ctx, sourceId ->
|
||||
val filterChange = try {
|
||||
json.decodeFromString<List<FilterChange>>(ctx.body())
|
||||
} catch (e: Exception) {
|
||||
listOf(json.decodeFromString<FilterChange>(ctx.body()))
|
||||
}
|
||||
val setFilters =
|
||||
handler(
|
||||
pathParam<Long>("sourceId"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Source filters set")
|
||||
description("Change filters of source with id `sourceId`")
|
||||
}
|
||||
body<FilterChange>()
|
||||
body<Array<FilterChange>>()
|
||||
},
|
||||
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)
|
||||
}
|
||||
)
|
||||
ctx.json(Search.setFilter(sourceId, filterChange))
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
}
|
||||
)
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
/** quick search single source filter */
|
||||
val quickSearchSingle = handler(
|
||||
pathParam<Long>("sourceId"),
|
||||
queryParam("pageNum", 1),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Source manga quick search")
|
||||
description("Returns list of manga from source matching posted searchTerm and filter")
|
||||
}
|
||||
body<FilterData>()
|
||||
},
|
||||
behaviorOf = { ctx, sourceId, pageNum ->
|
||||
val filter = json.decodeFromString<FilterData>(ctx.body())
|
||||
ctx.future(future { Search.sourceFilter(sourceId, pageNum, filter) })
|
||||
},
|
||||
withResults = {
|
||||
json<PagedMangaListDataClass>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
val quickSearchSingle =
|
||||
handler(
|
||||
pathParam<Long>("sourceId"),
|
||||
queryParam("pageNum", 1),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Source manga quick search")
|
||||
description("Returns list of manga from source matching posted searchTerm and filter")
|
||||
}
|
||||
body<FilterData>()
|
||||
},
|
||||
behaviorOf = { ctx, sourceId, pageNum ->
|
||||
val filter = json.decodeFromString<FilterData>(ctx.body())
|
||||
ctx.future(future { Search.sourceFilter(sourceId, pageNum, filter) })
|
||||
},
|
||||
withResults = {
|
||||
json<PagedMangaListDataClass>(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/** 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)
|
||||
}
|
||||
)
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,25 +30,26 @@ 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)
|
||||
val recentChapters =
|
||||
handler(
|
||||
pathParam<Int>("pageNum"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Updates fetch")
|
||||
description("Get recently updated manga chapters")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<PagedMangaChapterListDataClass>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
},
|
||||
behaviorOf = { ctx, pageNum ->
|
||||
ctx.future(
|
||||
future {
|
||||
Chapter.getRecentChapters(pageNum)
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<PagedMangaChapterListDataClass>(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Class made for handling return type in the documentation for [recentChapters],
|
||||
@@ -56,42 +57,43 @@ object UpdateController {
|
||||
*/
|
||||
private class PagedMangaChapterListDataClass : PaginatedList<MangaChapterDataClass>(emptyList(), false)
|
||||
|
||||
val categoryUpdate = handler(
|
||||
formParam<Int?>("categoryId"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Updater start")
|
||||
description("Starts the updater")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, categoryId ->
|
||||
val updater by DI.global.instance<IUpdater>()
|
||||
if (categoryId == null) {
|
||||
logger.info { "Adding Library to Update Queue" }
|
||||
updater.addCategoriesToUpdateQueue(
|
||||
Category.getCategoryList(),
|
||||
clear = true,
|
||||
forceAll = false
|
||||
)
|
||||
} else {
|
||||
val category = Category.getCategoryById(categoryId)
|
||||
if (category != null) {
|
||||
val categoryUpdate =
|
||||
handler(
|
||||
formParam<Int?>("categoryId"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Updater start")
|
||||
description("Starts the updater")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, categoryId ->
|
||||
val updater by DI.global.instance<IUpdater>()
|
||||
if (categoryId == null) {
|
||||
logger.info { "Adding Library to Update Queue" }
|
||||
updater.addCategoriesToUpdateQueue(
|
||||
listOf(category),
|
||||
Category.getCategoryList(),
|
||||
clear = true,
|
||||
forceAll = true
|
||||
forceAll = false,
|
||||
)
|
||||
} else {
|
||||
logger.info { "No Category found" }
|
||||
ctx.status(HttpCode.BAD_REQUEST)
|
||||
val category = Category.getCategoryById(categoryId)
|
||||
if (category != null) {
|
||||
updater.addCategoriesToUpdateQueue(
|
||||
listOf(category),
|
||||
clear = true,
|
||||
forceAll = true,
|
||||
)
|
||||
} else {
|
||||
logger.info { "No Category found" }
|
||||
ctx.status(HttpCode.BAD_REQUEST)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
httpCode(HttpCode.BAD_REQUEST)
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
httpCode(HttpCode.BAD_REQUEST)
|
||||
},
|
||||
)
|
||||
|
||||
fun categoryUpdateWS(ws: WsConfig) {
|
||||
ws.onConnect { ctx ->
|
||||
@@ -105,42 +107,44 @@ 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.status.value)
|
||||
},
|
||||
withResults = {
|
||||
json<UpdateStatus>(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
|
||||
val reset = handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Updater reset")
|
||||
description("Stops and resets the Updater")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
val updater by DI.global.instance<IUpdater>()
|
||||
logger.info { "Resetting Updater" }
|
||||
ctx.future(
|
||||
future {
|
||||
updater.reset()
|
||||
}.thenApply {
|
||||
ctx.status(HttpCode.OK)
|
||||
val updateSummary =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Updater summary")
|
||||
description("Gets the latest updater summary")
|
||||
}
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
}
|
||||
)
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
val updater by DI.global.instance<IUpdater>()
|
||||
ctx.json(updater.status.value)
|
||||
},
|
||||
withResults = {
|
||||
json<UpdateStatus>(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
val reset =
|
||||
handler(
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Updater reset")
|
||||
description("Stops and resets the Updater")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
val updater by DI.global.instance<IUpdater>()
|
||||
logger.info { "Resetting Updater" }
|
||||
ctx.future(
|
||||
future {
|
||||
updater.reset()
|
||||
}.thenApply {
|
||||
ctx.status(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,10 +35,11 @@ object Category {
|
||||
|
||||
return transaction {
|
||||
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null) {
|
||||
val newCategoryId = CategoryTable.insertAndGetId {
|
||||
it[CategoryTable.name] = name
|
||||
it[CategoryTable.order] = Int.MAX_VALUE
|
||||
}.value
|
||||
val newCategoryId =
|
||||
CategoryTable.insertAndGetId {
|
||||
it[CategoryTable.name] = name
|
||||
it[CategoryTable.order] = Int.MAX_VALUE
|
||||
}.value
|
||||
|
||||
normalizeCategories()
|
||||
|
||||
@@ -49,10 +50,20 @@ object Category {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?, includeInUpdate: Int?) {
|
||||
fun updateCategory(
|
||||
categoryId: Int,
|
||||
name: String?,
|
||||
isDefault: Boolean?,
|
||||
includeInUpdate: Int?,
|
||||
) {
|
||||
transaction {
|
||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||
if (categoryId != DEFAULT_CATEGORY_ID && name != null && !name.equals(DEFAULT_CATEGORY_NAME, ignoreCase = true)) it[CategoryTable.name] = name
|
||||
if (
|
||||
categoryId != DEFAULT_CATEGORY_ID && name != null &&
|
||||
!name.equals(DEFAULT_CATEGORY_NAME, ignoreCase = true)
|
||||
) {
|
||||
it[CategoryTable.name] = name
|
||||
}
|
||||
if (categoryId != DEFAULT_CATEGORY_ID && isDefault != null) it[CategoryTable.isDefault] = isDefault
|
||||
if (includeInUpdate != null) it[CategoryTable.includeInUpdate] = includeInUpdate
|
||||
}
|
||||
@@ -62,10 +73,16 @@ object Category {
|
||||
/**
|
||||
* Move the category from order number `from` to `to`
|
||||
*/
|
||||
fun reorderCategory(from: Int, to: Int) {
|
||||
fun reorderCategory(
|
||||
from: Int,
|
||||
to: Int,
|
||||
) {
|
||||
if (from == 0 || to == 0) return
|
||||
transaction {
|
||||
val categories = CategoryTable.select { CategoryTable.id neq DEFAULT_CATEGORY_ID }.orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
||||
val categories =
|
||||
CategoryTable.select {
|
||||
CategoryTable.id neq DEFAULT_CATEGORY_ID
|
||||
}.orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
||||
categories.add(to - 1, categories.removeAt(from - 1))
|
||||
categories.forEachIndexed { index, cat ->
|
||||
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
|
||||
@@ -98,14 +115,15 @@ object Category {
|
||||
}
|
||||
}
|
||||
|
||||
private fun needsDefaultCategory() = transaction {
|
||||
MangaTable
|
||||
.leftJoin(CategoryMangaTable)
|
||||
.select { MangaTable.inLibrary eq true }
|
||||
.andWhere { CategoryMangaTable.manga.isNull() }
|
||||
.empty()
|
||||
.not()
|
||||
}
|
||||
private fun needsDefaultCategory() =
|
||||
transaction {
|
||||
MangaTable
|
||||
.leftJoin(CategoryMangaTable)
|
||||
.select { MangaTable.inLibrary eq true }
|
||||
.andWhere { CategoryMangaTable.manga.isNull() }
|
||||
.empty()
|
||||
.not()
|
||||
}
|
||||
|
||||
const val DEFAULT_CATEGORY_ID = 0
|
||||
const val DEFAULT_CATEGORY_NAME = "Default"
|
||||
@@ -156,11 +174,16 @@ object Category {
|
||||
}
|
||||
}
|
||||
|
||||
fun modifyMeta(categoryId: Int, key: String, value: String) {
|
||||
fun modifyMeta(
|
||||
categoryId: Int,
|
||||
key: String,
|
||||
value: String,
|
||||
) {
|
||||
transaction {
|
||||
val meta = transaction {
|
||||
CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
|
||||
}.firstOrNull()
|
||||
val meta =
|
||||
transaction {
|
||||
CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
|
||||
}.firstOrNull()
|
||||
|
||||
if (meta == null) {
|
||||
CategoryMetaTable.insert {
|
||||
|
||||
@@ -31,9 +31,16 @@ import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
|
||||
object CategoryManga {
|
||||
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
||||
fun addMangaToCategory(
|
||||
mangaId: Int,
|
||||
categoryId: Int,
|
||||
) {
|
||||
if (categoryId == DEFAULT_CATEGORY_ID) return
|
||||
fun notAlreadyInCategory() = CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.isEmpty()
|
||||
|
||||
fun notAlreadyInCategory() =
|
||||
CategoryMangaTable.select {
|
||||
(CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId)
|
||||
}.isEmpty()
|
||||
|
||||
transaction {
|
||||
if (notAlreadyInCategory()) {
|
||||
@@ -45,7 +52,10 @@ object CategoryManga {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
||||
fun removeMangaFromCategory(
|
||||
mangaId: Int,
|
||||
categoryId: Int,
|
||||
) {
|
||||
if (categoryId == DEFAULT_CATEGORY_ID) return
|
||||
transaction {
|
||||
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
|
||||
@@ -57,12 +67,18 @@ object CategoryManga {
|
||||
*/
|
||||
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
||||
// Select the required columns from the MangaTable and add the aggregate functions to compute unread, download, and chapter counts
|
||||
val unreadCount = wrapAsExpression<Long>(
|
||||
ChapterTable.slice(ChapterTable.id.count()).select((ChapterTable.isRead eq false) and (ChapterTable.manga eq MangaTable.id))
|
||||
)
|
||||
val downloadedCount = wrapAsExpression<Long>(
|
||||
ChapterTable.slice(ChapterTable.id.count()).select((ChapterTable.isDownloaded eq true) and (ChapterTable.manga eq MangaTable.id))
|
||||
)
|
||||
val unreadCount =
|
||||
wrapAsExpression<Long>(
|
||||
ChapterTable.slice(
|
||||
ChapterTable.id.count(),
|
||||
).select((ChapterTable.isRead eq false) and (ChapterTable.manga eq MangaTable.id)),
|
||||
)
|
||||
val downloadedCount =
|
||||
wrapAsExpression<Long>(
|
||||
ChapterTable.slice(
|
||||
ChapterTable.id.count(),
|
||||
).select((ChapterTable.isDownloaded eq true) and (ChapterTable.manga eq MangaTable.id)),
|
||||
)
|
||||
|
||||
val chapterCount = ChapterTable.id.count().alias("chapter_count")
|
||||
val lastReadAt = ChapterTable.lastReadAt.max().alias("last_read_at")
|
||||
@@ -80,19 +96,20 @@ object CategoryManga {
|
||||
|
||||
return transaction {
|
||||
// Fetch data from the MangaTable and join with the CategoryMangaTable, if a category is specified
|
||||
val query = if (categoryId == DEFAULT_CATEGORY_ID) {
|
||||
MangaTable
|
||||
.leftJoin(ChapterTable, { MangaTable.id }, { ChapterTable.manga })
|
||||
.leftJoin(CategoryMangaTable)
|
||||
.slice(columns = selectedColumns)
|
||||
.select { (MangaTable.inLibrary eq true) and CategoryMangaTable.category.isNull() }
|
||||
} else {
|
||||
MangaTable
|
||||
.innerJoin(CategoryMangaTable)
|
||||
.leftJoin(ChapterTable, { MangaTable.id }, { ChapterTable.manga })
|
||||
.slice(columns = selectedColumns)
|
||||
.select { (MangaTable.inLibrary eq true) and (CategoryMangaTable.category eq categoryId) }
|
||||
}
|
||||
val query =
|
||||
if (categoryId == DEFAULT_CATEGORY_ID) {
|
||||
MangaTable
|
||||
.leftJoin(ChapterTable, { MangaTable.id }, { ChapterTable.manga })
|
||||
.leftJoin(CategoryMangaTable)
|
||||
.slice(columns = selectedColumns)
|
||||
.select { (MangaTable.inLibrary eq true) and CategoryMangaTable.category.isNull() }
|
||||
} else {
|
||||
MangaTable
|
||||
.innerJoin(CategoryMangaTable)
|
||||
.leftJoin(ChapterTable, { MangaTable.id }, { ChapterTable.manga })
|
||||
.slice(columns = selectedColumns)
|
||||
.select { (MangaTable.inLibrary eq true) and (CategoryMangaTable.category eq categoryId) }
|
||||
}
|
||||
|
||||
// Join with the ChapterTable to fetch the last read chapter for each manga
|
||||
query.groupBy(*MangaTable.columns.toTypedArray()).map(transform)
|
||||
@@ -104,7 +121,9 @@ object CategoryManga {
|
||||
*/
|
||||
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
|
||||
return transaction {
|
||||
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||
CategoryMangaTable.innerJoin(CategoryTable).select {
|
||||
CategoryMangaTable.manga eq mangaId
|
||||
}.orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||
CategoryTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,10 @@ object Chapter {
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
/** get chapter list when showing a manga */
|
||||
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean = false): List<ChapterDataClass> {
|
||||
suspend fun getChapterList(
|
||||
mangaId: Int,
|
||||
onlineFetch: Boolean = false,
|
||||
): List<ChapterDataClass> {
|
||||
return if (onlineFetch) {
|
||||
getSourceChapters(mangaId)
|
||||
} else {
|
||||
@@ -68,10 +71,11 @@ object Chapter {
|
||||
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
|
||||
val chapterList = fetchChapterList(mangaId)
|
||||
|
||||
val dbChapterMap = transaction {
|
||||
ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||
.associateBy({ it[ChapterTable.url] }, { it })
|
||||
}
|
||||
val dbChapterMap =
|
||||
transaction {
|
||||
ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||
.associateBy({ it[ChapterTable.url] }, { it })
|
||||
}
|
||||
|
||||
val chapterIds = chapterList.map { dbChapterMap.getValue(it.url)[ChapterTable.id] }
|
||||
val chapterMetas = getChaptersMetaMaps(chapterIds)
|
||||
@@ -88,21 +92,17 @@ object Chapter {
|
||||
chapterNumber = it.chapter_number,
|
||||
scanlator = it.scanlator,
|
||||
mangaId = mangaId,
|
||||
|
||||
read = dbChapter[ChapterTable.isRead],
|
||||
bookmarked = dbChapter[ChapterTable.isBookmarked],
|
||||
lastPageRead = dbChapter[ChapterTable.lastPageRead],
|
||||
lastReadAt = dbChapter[ChapterTable.lastReadAt],
|
||||
|
||||
index = chapterList.size - index,
|
||||
fetchedAt = dbChapter[ChapterTable.fetchedAt],
|
||||
realUrl = dbChapter[ChapterTable.realUrl],
|
||||
downloaded = dbChapter[ChapterTable.isDownloaded],
|
||||
|
||||
pageCount = dbChapter[ChapterTable.pageCount],
|
||||
|
||||
chapterCount = chapterList.size,
|
||||
meta = chapterMetas.getValue(dbChapter[ChapterTable.id])
|
||||
meta = chapterMetas.getValue(dbChapter[ChapterTable.id]),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -111,10 +111,11 @@ object Chapter {
|
||||
val manga = getManga(mangaId)
|
||||
val source = getCatalogueSourceOrStub(manga.sourceId.toLong())
|
||||
|
||||
val sManga = SManga.create().apply {
|
||||
title = manga.title
|
||||
url = manga.url
|
||||
}
|
||||
val sManga =
|
||||
SManga.create().apply {
|
||||
title = manga.title
|
||||
url = manga.url
|
||||
}
|
||||
|
||||
val numberOfCurrentChapters = getCountOfMangaChapters(mangaId)
|
||||
val chapterList = source.getChapterList(sManga)
|
||||
@@ -143,9 +144,10 @@ object Chapter {
|
||||
it[fetchedAt] = now++
|
||||
it[ChapterTable.manga] = mangaId
|
||||
|
||||
it[realUrl] = runCatching {
|
||||
(source as? HttpSource)?.getChapterUrl(fetchedChapter)
|
||||
}.getOrNull()
|
||||
it[realUrl] =
|
||||
runCatching {
|
||||
(source as? HttpSource)?.getChapterUrl(fetchedChapter)
|
||||
}.getOrNull()
|
||||
}
|
||||
} else {
|
||||
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
|
||||
@@ -157,9 +159,10 @@ object Chapter {
|
||||
it[sourceOrder] = index + 1
|
||||
it[ChapterTable.manga] = mangaId
|
||||
|
||||
it[realUrl] = runCatching {
|
||||
(source as? HttpSource)?.getChapterUrl(fetchedChapter)
|
||||
}.getOrNull()
|
||||
it[realUrl] =
|
||||
runCatching {
|
||||
(source as? HttpSource)?.getChapterUrl(fetchedChapter)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,18 +172,20 @@ object Chapter {
|
||||
}
|
||||
}
|
||||
|
||||
val newChapters = transaction {
|
||||
ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC).toList()
|
||||
}
|
||||
val newChapters =
|
||||
transaction {
|
||||
ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC).toList()
|
||||
}
|
||||
|
||||
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
|
||||
val dbChapterCount = newChapters.count()
|
||||
if (dbChapterCount > chapterList.size) { // we got some clean up due
|
||||
val dbChapterList = transaction {
|
||||
ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||
.orderBy(ChapterTable.url to ASC).toList()
|
||||
}
|
||||
val dbChapterList =
|
||||
transaction {
|
||||
ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||
.orderBy(ChapterTable.url to ASC).toList()
|
||||
}
|
||||
val chapterUrls = chapterList.map { it.url }.toSet()
|
||||
|
||||
dbChapterList.forEachIndexed { index, dbChapter ->
|
||||
@@ -203,7 +208,11 @@ object Chapter {
|
||||
return chapterList
|
||||
}
|
||||
|
||||
private fun downloadNewChapters(mangaId: Int, prevNumberOfChapters: Int, updatedChapterList: List<ResultRow>) {
|
||||
private fun downloadNewChapters(
|
||||
mangaId: Int,
|
||||
prevNumberOfChapters: Int,
|
||||
updatedChapterList: List<ResultRow>,
|
||||
) {
|
||||
// convert numbers to be index based
|
||||
val currentNumberOfChapters = (prevNumberOfChapters - 1).coerceAtLeast(0)
|
||||
val updatedNumberOfChapters = (updatedChapterList.size - 1).coerceAtLeast(0)
|
||||
@@ -224,8 +233,9 @@ object Chapter {
|
||||
// make sure to only consider the latest chapters. e.g. old unread chapters should be ignored
|
||||
val latestReadChapterIndex =
|
||||
updatedChapterList.indexOfFirst { it[ChapterTable.isRead] }.takeIf { it > -1 } ?: return
|
||||
val unreadChapters = updatedChapterList.subList(numberOfNewChapters, latestReadChapterIndex)
|
||||
.filter { !it[ChapterTable.isRead] }
|
||||
val unreadChapters =
|
||||
updatedChapterList.subList(numberOfNewChapters, latestReadChapterIndex)
|
||||
.filter { !it[ChapterTable.isRead] }
|
||||
|
||||
val skipDueToUnreadChapters = serverConfig.excludeEntryWithUnreadChapters.value && unreadChapters.isNotEmpty()
|
||||
if (skipDueToUnreadChapters) {
|
||||
@@ -235,9 +245,10 @@ object Chapter {
|
||||
val firstChapterToDownloadIndex =
|
||||
(numberOfNewChapters - serverConfig.autoDownloadAheadLimit.value).coerceAtLeast(0)
|
||||
|
||||
val chapterIdsToDownload = newChapters.subList(firstChapterToDownloadIndex, numberOfNewChapters)
|
||||
.filter { !it[ChapterTable.isRead] && !it[ChapterTable.isDownloaded] }
|
||||
.map { it[ChapterTable.id].value }
|
||||
val chapterIdsToDownload =
|
||||
newChapters.subList(firstChapterToDownloadIndex, numberOfNewChapters)
|
||||
.filter { !it[ChapterTable.isRead] && !it[ChapterTable.isDownloaded] }
|
||||
.map { it[ChapterTable.id].value }
|
||||
|
||||
if (chapterIdsToDownload.isEmpty()) {
|
||||
return
|
||||
@@ -254,7 +265,7 @@ object Chapter {
|
||||
isRead: Boolean?,
|
||||
isBookmarked: Boolean?,
|
||||
markPrevRead: Boolean?,
|
||||
lastPageRead: Int?
|
||||
lastPageRead: Int?,
|
||||
) {
|
||||
transaction {
|
||||
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
|
||||
@@ -285,23 +296,26 @@ object Chapter {
|
||||
val isRead: Boolean? = null,
|
||||
val isBookmarked: Boolean? = null,
|
||||
val lastPageRead: Int? = null,
|
||||
val delete: Boolean? = null
|
||||
val delete: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MangaChapterBatchEditInput(
|
||||
val chapterIds: List<Int>? = null,
|
||||
val chapterIndexes: List<Int>? = null,
|
||||
val change: ChapterChange?
|
||||
val change: ChapterChange?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterBatchEditInput(
|
||||
val chapterIds: List<Int>? = null,
|
||||
val change: ChapterChange?
|
||||
val change: ChapterChange?,
|
||||
)
|
||||
|
||||
fun modifyChapters(input: MangaChapterBatchEditInput, mangaId: Int? = null) {
|
||||
fun modifyChapters(
|
||||
input: MangaChapterBatchEditInput,
|
||||
mangaId: Int? = null,
|
||||
) {
|
||||
// Make sure change is defined
|
||||
if (input.change == null) return
|
||||
val (isRead, isBookmarked, lastPageRead, delete) = input.change
|
||||
@@ -315,25 +329,26 @@ object Chapter {
|
||||
if (listOfNotNull(isRead, isBookmarked, lastPageRead).isEmpty()) return
|
||||
|
||||
// Make sure some filter is defined
|
||||
val condition = when {
|
||||
mangaId != null ->
|
||||
// mangaId is not null, scope query under manga
|
||||
when {
|
||||
input.chapterIds != null ->
|
||||
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.id inList input.chapterIds) }
|
||||
input.chapterIndexes != null ->
|
||||
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder inList input.chapterIndexes) }
|
||||
else -> null
|
||||
val condition =
|
||||
when {
|
||||
mangaId != null ->
|
||||
// mangaId is not null, scope query under manga
|
||||
when {
|
||||
input.chapterIds != null ->
|
||||
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.id inList input.chapterIds) }
|
||||
input.chapterIndexes != null ->
|
||||
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder inList input.chapterIndexes) }
|
||||
else -> null
|
||||
}
|
||||
else -> {
|
||||
// mangaId is null, only chapterIndexes is valid for this case
|
||||
when {
|
||||
input.chapterIds != null ->
|
||||
Op.build { (ChapterTable.id inList input.chapterIds) }
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// mangaId is null, only chapterIndexes is valid for this case
|
||||
when {
|
||||
input.chapterIds != null ->
|
||||
Op.build { (ChapterTable.id inList input.chapterIds) }
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
} ?: return
|
||||
} ?: return
|
||||
|
||||
transaction {
|
||||
val now = Instant.now().epochSecond
|
||||
@@ -368,7 +383,12 @@ object Chapter {
|
||||
}
|
||||
}
|
||||
|
||||
fun modifyChapterMeta(mangaId: Int, chapterIndex: Int, key: String, value: String) {
|
||||
fun modifyChapterMeta(
|
||||
mangaId: Int,
|
||||
chapterIndex: Int,
|
||||
key: String,
|
||||
value: String,
|
||||
) {
|
||||
transaction {
|
||||
val chapterId =
|
||||
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
|
||||
@@ -377,7 +397,11 @@ object Chapter {
|
||||
}
|
||||
}
|
||||
|
||||
fun modifyChapterMeta(chapterId: Int, key: String, value: String) {
|
||||
fun modifyChapterMeta(
|
||||
chapterId: Int,
|
||||
key: String,
|
||||
value: String,
|
||||
) {
|
||||
transaction {
|
||||
val meta =
|
||||
ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
|
||||
@@ -397,7 +421,10 @@ object Chapter {
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteChapter(mangaId: Int, chapterIndex: Int) {
|
||||
fun deleteChapter(
|
||||
mangaId: Int,
|
||||
chapterIndex: Int,
|
||||
) {
|
||||
transaction {
|
||||
val chapterId =
|
||||
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
|
||||
@@ -411,19 +438,23 @@ object Chapter {
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteChapters(input: MangaChapterBatchEditInput, mangaId: Int? = null) {
|
||||
private fun deleteChapters(
|
||||
input: MangaChapterBatchEditInput,
|
||||
mangaId: Int? = null,
|
||||
) {
|
||||
if (input.chapterIds != null) {
|
||||
deleteChapters(input.chapterIds)
|
||||
} else if (input.chapterIndexes != null && mangaId != null) {
|
||||
transaction {
|
||||
val chapterIds = ChapterTable.slice(ChapterTable.manga, ChapterTable.id)
|
||||
.select { (ChapterTable.sourceOrder inList input.chapterIndexes) and (ChapterTable.manga eq mangaId) }
|
||||
.map { row ->
|
||||
val chapterId = row[ChapterTable.id].value
|
||||
ChapterDownloadHelper.delete(mangaId, chapterId)
|
||||
val chapterIds =
|
||||
ChapterTable.slice(ChapterTable.manga, ChapterTable.id)
|
||||
.select { (ChapterTable.sourceOrder inList input.chapterIndexes) and (ChapterTable.manga eq mangaId) }
|
||||
.map { row ->
|
||||
val chapterId = row[ChapterTable.id].value
|
||||
ChapterDownloadHelper.delete(mangaId, chapterId)
|
||||
|
||||
chapterId
|
||||
}
|
||||
chapterId
|
||||
}
|
||||
|
||||
ChapterTable.update({ ChapterTable.id inList chapterIds }) {
|
||||
it[isDownloaded] = false
|
||||
@@ -457,7 +488,7 @@ object Chapter {
|
||||
.map {
|
||||
MangaChapterDataClass(
|
||||
MangaTable.toDataClass(it),
|
||||
ChapterTable.toDataClass(it)
|
||||
ChapterTable.toDataClass(it),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,18 @@ import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
object ChapterDownloadHelper {
|
||||
fun getImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
|
||||
fun getImage(
|
||||
mangaId: Int,
|
||||
chapterId: Int,
|
||||
index: Int,
|
||||
): Pair<InputStream, String> {
|
||||
return provider(mangaId, chapterId).getImage().execute(index)
|
||||
}
|
||||
|
||||
fun delete(mangaId: Int, chapterId: Int): Boolean {
|
||||
fun delete(
|
||||
mangaId: Int,
|
||||
chapterId: Int,
|
||||
): Boolean {
|
||||
return provider(mangaId, chapterId).delete()
|
||||
}
|
||||
|
||||
@@ -25,13 +32,16 @@ object ChapterDownloadHelper {
|
||||
chapterId: Int,
|
||||
download: DownloadChapter,
|
||||
scope: CoroutineScope,
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit,
|
||||
): Boolean {
|
||||
return provider(mangaId, chapterId).download().execute(download, scope, step)
|
||||
}
|
||||
|
||||
// return the appropriate provider based on how the download was saved. For the logic is simple but will evolve when new types of downloads are available
|
||||
private fun provider(mangaId: Int, chapterId: Int): ChaptersFilesProvider {
|
||||
private fun provider(
|
||||
mangaId: Int,
|
||||
chapterId: Int,
|
||||
): ChaptersFilesProvider {
|
||||
val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId))
|
||||
val cbzFile = File(getChapterCbzPath(mangaId, chapterId))
|
||||
if (cbzFile.exists()) return ArchiveProvider(mangaId, chapterId)
|
||||
|
||||
@@ -29,9 +29,10 @@ object Library {
|
||||
val manga = getManga(mangaId)
|
||||
if (!manga.inLibrary) {
|
||||
transaction {
|
||||
val defaultCategories = CategoryTable.select {
|
||||
(CategoryTable.isDefault eq true) and (CategoryTable.id neq Category.DEFAULT_CATEGORY_ID)
|
||||
}.toList()
|
||||
val defaultCategories =
|
||||
CategoryTable.select {
|
||||
(CategoryTable.isDefault eq true) and (CategoryTable.id neq Category.DEFAULT_CATEGORY_ID)
|
||||
}.toList()
|
||||
val existingCategories = CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.toList()
|
||||
|
||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||
@@ -66,7 +67,10 @@ object Library {
|
||||
}
|
||||
}
|
||||
|
||||
fun handleMangaThumbnail(mangaId: Int, inLibrary: Boolean) {
|
||||
fun handleMangaThumbnail(
|
||||
mangaId: Int,
|
||||
inLibrary: Boolean,
|
||||
) {
|
||||
scope.launch {
|
||||
try {
|
||||
if (inLibrary) {
|
||||
|
||||
@@ -58,7 +58,10 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
object Manga {
|
||||
private fun truncate(text: String?, maxLength: Int): String? {
|
||||
private fun truncate(
|
||||
text: String?,
|
||||
maxLength: Int,
|
||||
): String? {
|
||||
return if (text?.length ?: 0 > maxLength) {
|
||||
text?.take(maxLength - 3) + "..."
|
||||
} else {
|
||||
@@ -66,7 +69,10 @@ object Manga {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
|
||||
suspend fun getManga(
|
||||
mangaId: Int,
|
||||
onlineFetch: Boolean = false,
|
||||
): MangaDataClass {
|
||||
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||
|
||||
return if (!onlineFetch && mangaEntry[MangaTable.initialized]) {
|
||||
@@ -79,14 +85,11 @@ object Manga {
|
||||
MangaDataClass(
|
||||
id = mangaId,
|
||||
sourceId = mangaEntry[MangaTable.sourceReference].toString(),
|
||||
|
||||
url = mangaEntry[MangaTable.url],
|
||||
title = mangaEntry[MangaTable.title],
|
||||
thumbnailUrl = proxyThumbnailUrl(mangaId),
|
||||
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
|
||||
|
||||
initialized = true,
|
||||
|
||||
artist = sManga.artist,
|
||||
author = sManga.author,
|
||||
description = sManga.description,
|
||||
@@ -100,7 +103,7 @@ object Manga {
|
||||
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
|
||||
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
|
||||
freshData = true
|
||||
freshData = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -108,12 +111,14 @@ object Manga {
|
||||
suspend fun fetchManga(mangaId: Int): SManga? {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||
|
||||
val source = getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
||||
?: return null
|
||||
val sManga = SManga.create().apply {
|
||||
url = mangaEntry[MangaTable.url]
|
||||
title = mangaEntry[MangaTable.title]
|
||||
}
|
||||
val source =
|
||||
getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
||||
?: return null
|
||||
val sManga =
|
||||
SManga.create().apply {
|
||||
url = mangaEntry[MangaTable.url]
|
||||
title = mangaEntry[MangaTable.title]
|
||||
}
|
||||
val networkManga = source.getMangaDetails(sManga)
|
||||
sManga.copyFrom(networkManga)
|
||||
|
||||
@@ -139,9 +144,10 @@ object Manga {
|
||||
clearThumbnail(mangaId)
|
||||
}
|
||||
|
||||
it[MangaTable.realUrl] = runCatching {
|
||||
(source as? HttpSource)?.getMangaUrl(sManga)
|
||||
}.getOrNull()
|
||||
it[MangaTable.realUrl] =
|
||||
runCatching {
|
||||
(source as? HttpSource)?.getMangaUrl(sManga)
|
||||
}.getOrNull()
|
||||
|
||||
it[MangaTable.lastFetchedAt] = Instant.now().epochSecond
|
||||
|
||||
@@ -152,7 +158,10 @@ object Manga {
|
||||
return sManga
|
||||
}
|
||||
|
||||
suspend fun getMangaFull(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
|
||||
suspend fun getMangaFull(
|
||||
mangaId: Int,
|
||||
onlineFetch: Boolean = false,
|
||||
): MangaDataClass {
|
||||
val mangaDaaClass = getManga(mangaId, onlineFetch)
|
||||
|
||||
return transaction {
|
||||
@@ -186,17 +195,17 @@ object Manga {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMangaDataClass(mangaId: Int, mangaEntry: ResultRow) = MangaDataClass(
|
||||
private fun getMangaDataClass(
|
||||
mangaId: Int,
|
||||
mangaEntry: ResultRow,
|
||||
) = MangaDataClass(
|
||||
id = mangaId,
|
||||
sourceId = mangaEntry[MangaTable.sourceReference].toString(),
|
||||
|
||||
url = mangaEntry[MangaTable.url],
|
||||
title = mangaEntry[MangaTable.title],
|
||||
thumbnailUrl = proxyThumbnailUrl(mangaId),
|
||||
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
|
||||
|
||||
initialized = true,
|
||||
|
||||
artist = mangaEntry[MangaTable.artist],
|
||||
author = mangaEntry[MangaTable.author],
|
||||
description = mangaEntry[MangaTable.description],
|
||||
@@ -210,7 +219,7 @@ object Manga {
|
||||
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
|
||||
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
|
||||
freshData = false
|
||||
freshData = false,
|
||||
)
|
||||
|
||||
fun getMangaMetaMap(mangaId: Int): Map<String, String> {
|
||||
@@ -220,7 +229,11 @@ object Manga {
|
||||
}
|
||||
}
|
||||
|
||||
fun modifyMangaMeta(mangaId: Int, key: String, value: String) {
|
||||
fun modifyMangaMeta(
|
||||
mangaId: Int,
|
||||
key: String,
|
||||
value: String,
|
||||
) {
|
||||
transaction {
|
||||
val meta =
|
||||
MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
|
||||
@@ -251,45 +264,51 @@ object Manga {
|
||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||
|
||||
return when (val source = getCatalogueSourceOrStub(sourceId)) {
|
||||
is HttpSource -> getImageResponse(cacheSaveDir, fileName) {
|
||||
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
||||
?: if (!mangaEntry[MangaTable.initialized]) {
|
||||
// initialize then try again
|
||||
getManga(mangaId)
|
||||
transaction {
|
||||
MangaTable.select { MangaTable.id eq mangaId }.first()
|
||||
}[MangaTable.thumbnail_url]!!
|
||||
} else {
|
||||
// source provides no thumbnail url for this manga
|
||||
throw NullPointerException("No thumbnail found")
|
||||
}
|
||||
is HttpSource ->
|
||||
getImageResponse(cacheSaveDir, fileName) {
|
||||
val thumbnailUrl =
|
||||
mangaEntry[MangaTable.thumbnail_url]
|
||||
?: if (!mangaEntry[MangaTable.initialized]) {
|
||||
// initialize then try again
|
||||
getManga(mangaId)
|
||||
transaction {
|
||||
MangaTable.select { MangaTable.id eq mangaId }.first()
|
||||
}[MangaTable.thumbnail_url]!!
|
||||
} else {
|
||||
// source provides no thumbnail url for this manga
|
||||
throw NullPointerException("No thumbnail found")
|
||||
}
|
||||
|
||||
source.client.newCall(
|
||||
GET(thumbnailUrl, source.headers)
|
||||
).await()
|
||||
}
|
||||
source.client.newCall(
|
||||
GET(thumbnailUrl, source.headers),
|
||||
).await()
|
||||
}
|
||||
|
||||
is LocalSource -> {
|
||||
val imageFile = mangaEntry[MangaTable.thumbnail_url]?.let {
|
||||
val file = File(it)
|
||||
if (file.exists()) {
|
||||
file
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: throw IOException("Thumbnail does not exist")
|
||||
val contentType = ImageUtil.findImageType { imageFile.inputStream() }?.mime
|
||||
?: "image/jpeg"
|
||||
val imageFile =
|
||||
mangaEntry[MangaTable.thumbnail_url]?.let {
|
||||
val file = File(it)
|
||||
if (file.exists()) {
|
||||
file
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: throw IOException("Thumbnail does not exist")
|
||||
val contentType =
|
||||
ImageUtil.findImageType { imageFile.inputStream() }?.mime
|
||||
?: "image/jpeg"
|
||||
imageFile.inputStream() to contentType
|
||||
}
|
||||
|
||||
is StubSource -> getImageResponse(cacheSaveDir, fileName) {
|
||||
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
||||
?: throw NullPointerException("No thumbnail found")
|
||||
network.client.newCall(
|
||||
GET(thumbnailUrl)
|
||||
).await()
|
||||
}
|
||||
is StubSource ->
|
||||
getImageResponse(cacheSaveDir, fileName) {
|
||||
val thumbnailUrl =
|
||||
mangaEntry[MangaTable.thumbnail_url]
|
||||
?: throw NullPointerException("No thumbnail found")
|
||||
network.client.newCall(
|
||||
GET(thumbnailUrl),
|
||||
).await()
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown source")
|
||||
}
|
||||
@@ -319,14 +338,18 @@ object Manga {
|
||||
|
||||
private val downloadAheadQueue = ConcurrentHashMap<String, ConcurrentHashMap.KeySetView<Int, Boolean>>()
|
||||
private var downloadAheadTimer: Timer? = null
|
||||
fun downloadAhead(mangaIds: List<Int>, latestReadChapterIds: List<Int> = emptyList()) {
|
||||
|
||||
private const val MANGAS_KEY = "mangaIds"
|
||||
private const val CHAPTERS_KEY = "chapterIds"
|
||||
|
||||
fun downloadAhead(
|
||||
mangaIds: List<Int>,
|
||||
latestReadChapterIds: List<Int> = emptyList(),
|
||||
) {
|
||||
if (serverConfig.autoDownloadAheadLimit.value == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
val MANGAS_KEY = "mangaIds"
|
||||
val CHAPTERS_KEY = "chapterIds"
|
||||
|
||||
val updateDownloadAheadQueue = { key: String, ids: List<Int> ->
|
||||
val idSet = downloadAheadQueue[key] ?: ConcurrentHashMap.newKeySet()
|
||||
idSet.addAll(ids)
|
||||
@@ -339,17 +362,21 @@ object Manga {
|
||||
// handle cases where this function gets called multiple times in quick succession.
|
||||
// this could happen in case e.g. multiple chapters get marked as read without batching the operation
|
||||
downloadAheadTimer?.cancel()
|
||||
downloadAheadTimer = Timer().apply {
|
||||
schedule(
|
||||
object : TimerTask() {
|
||||
override fun run() {
|
||||
downloadAheadChapters(downloadAheadQueue[MANGAS_KEY]!!.toList(), downloadAheadQueue[CHAPTERS_KEY]!!.toList())
|
||||
downloadAheadQueue.clear()
|
||||
}
|
||||
},
|
||||
5000
|
||||
)
|
||||
}
|
||||
downloadAheadTimer =
|
||||
Timer().apply {
|
||||
schedule(
|
||||
object : TimerTask() {
|
||||
override fun run() {
|
||||
downloadAheadChapters(
|
||||
downloadAheadQueue[MANGAS_KEY]?.toList().orEmpty(),
|
||||
downloadAheadQueue[CHAPTERS_KEY]?.toList().orEmpty(),
|
||||
)
|
||||
downloadAheadQueue.clear()
|
||||
}
|
||||
},
|
||||
5000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,29 +397,38 @@ object Manga {
|
||||
*
|
||||
* will download the unread chapters starting from chapter 15
|
||||
*/
|
||||
private fun downloadAheadChapters(mangaIds: List<Int>, latestReadChapterIds: List<Int>) {
|
||||
val mangaToLatestReadChapterIndex = transaction {
|
||||
ChapterTable.select { (ChapterTable.manga inList mangaIds) and (ChapterTable.isRead eq true) }
|
||||
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC).groupBy { it[ChapterTable.manga].value }
|
||||
}.mapValues { (_, chapters) -> chapters.firstOrNull()?.let { it[ChapterTable.sourceOrder] } ?: 0 }
|
||||
private fun downloadAheadChapters(
|
||||
mangaIds: List<Int>,
|
||||
latestReadChapterIds: List<Int>,
|
||||
) {
|
||||
val mangaToLatestReadChapterIndex =
|
||||
transaction {
|
||||
ChapterTable.select { (ChapterTable.manga inList mangaIds) and (ChapterTable.isRead eq true) }
|
||||
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC).groupBy { it[ChapterTable.manga].value }
|
||||
}.mapValues { (_, chapters) -> chapters.firstOrNull()?.let { it[ChapterTable.sourceOrder] } ?: 0 }
|
||||
|
||||
val mangaToUnreadChaptersMap = transaction {
|
||||
ChapterTable.select { (ChapterTable.manga inList mangaIds) and (ChapterTable.isRead eq false) }
|
||||
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
|
||||
.groupBy { it[ChapterTable.manga].value }
|
||||
}
|
||||
val mangaToUnreadChaptersMap =
|
||||
transaction {
|
||||
ChapterTable.select { (ChapterTable.manga inList mangaIds) and (ChapterTable.isRead eq false) }
|
||||
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
|
||||
.groupBy { it[ChapterTable.manga].value }
|
||||
}
|
||||
|
||||
val chapterIdsToDownload = mangaToUnreadChaptersMap.map { (mangaId, unreadChapters) ->
|
||||
val latestReadChapterIndex = mangaToLatestReadChapterIndex[mangaId] ?: 0
|
||||
val lastChapterToDownloadIndex =
|
||||
unreadChapters.indexOfLast { it[ChapterTable.sourceOrder] > latestReadChapterIndex && it[ChapterTable.id].value !in latestReadChapterIds }
|
||||
val unreadChaptersToConsider = unreadChapters.subList(0, lastChapterToDownloadIndex + 1)
|
||||
val firstChapterToDownloadIndex =
|
||||
(unreadChaptersToConsider.size - serverConfig.autoDownloadAheadLimit.value).coerceAtLeast(0)
|
||||
unreadChaptersToConsider.subList(firstChapterToDownloadIndex, lastChapterToDownloadIndex + 1)
|
||||
.filter { !it[ChapterTable.isDownloaded] }
|
||||
.map { it[ChapterTable.id].value }
|
||||
}.flatten()
|
||||
val chapterIdsToDownload =
|
||||
mangaToUnreadChaptersMap.map { (mangaId, unreadChapters) ->
|
||||
val latestReadChapterIndex = mangaToLatestReadChapterIndex[mangaId] ?: 0
|
||||
val lastChapterToDownloadIndex =
|
||||
unreadChapters.indexOfLast {
|
||||
it[ChapterTable.sourceOrder] > latestReadChapterIndex &&
|
||||
it[ChapterTable.id].value !in latestReadChapterIds
|
||||
}
|
||||
val unreadChaptersToConsider = unreadChapters.subList(0, lastChapterToDownloadIndex + 1)
|
||||
val firstChapterToDownloadIndex =
|
||||
(unreadChaptersToConsider.size - serverConfig.autoDownloadAheadLimit.value).coerceAtLeast(0)
|
||||
unreadChaptersToConsider.subList(firstChapterToDownloadIndex, lastChapterToDownloadIndex + 1)
|
||||
.filter { !it[ChapterTable.isDownloaded] }
|
||||
.map { it[ChapterTable.id].value }
|
||||
}.flatten()
|
||||
|
||||
logger.info { "downloadAheadChapters: download chapters [${chapterIdsToDownload.joinToString(", ")}]" }
|
||||
|
||||
|
||||
@@ -26,29 +26,35 @@ object MangaList {
|
||||
return "/api/v1/manga/$mangaId/thumbnail"
|
||||
}
|
||||
|
||||
suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
|
||||
suspend fun getMangaList(
|
||||
sourceId: Long,
|
||||
pageNum: Int = 1,
|
||||
popular: Boolean,
|
||||
): PagedMangaListDataClass {
|
||||
require(pageNum > 0) {
|
||||
"pageNum = $pageNum is not in valid range"
|
||||
}
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val mangasPage = if (popular) {
|
||||
source.getPopularManga(pageNum)
|
||||
} else {
|
||||
if (source.supportsLatest) {
|
||||
source.getLatestUpdates(pageNum)
|
||||
val mangasPage =
|
||||
if (popular) {
|
||||
source.getPopularManga(pageNum)
|
||||
} else {
|
||||
throw Exception("Source $source doesn't support latest")
|
||||
if (source.supportsLatest) {
|
||||
source.getLatestUpdates(pageNum)
|
||||
} else {
|
||||
throw Exception("Source $source doesn't support latest")
|
||||
}
|
||||
}
|
||||
}
|
||||
return mangasPage.processEntries(sourceId)
|
||||
}
|
||||
|
||||
fun MangasPage.insertOrGet(sourceId: Long): List<Int> {
|
||||
return transaction {
|
||||
mangas.map { manga ->
|
||||
val mangaEntry = MangaTable.select {
|
||||
(MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId)
|
||||
}.firstOrNull()
|
||||
val mangaEntry =
|
||||
MangaTable.select {
|
||||
(MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId)
|
||||
}.firstOrNull()
|
||||
if (mangaEntry == null) { // create manga entry
|
||||
MangaTable.insertAndGetId {
|
||||
it[url] = manga.url
|
||||
@@ -73,89 +79,87 @@ object MangaList {
|
||||
|
||||
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
||||
val mangasPage = this
|
||||
val mangaList = transaction {
|
||||
return@transaction mangasPage.mangas.map { manga ->
|
||||
var mangaEntry = MangaTable.select {
|
||||
(MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId)
|
||||
}.firstOrNull()
|
||||
if (mangaEntry == null) { // create manga entry
|
||||
val mangaId = MangaTable.insertAndGetId {
|
||||
it[url] = manga.url
|
||||
it[title] = manga.title
|
||||
val mangaList =
|
||||
transaction {
|
||||
return@transaction mangasPage.mangas.map { manga ->
|
||||
var mangaEntry =
|
||||
MangaTable.select {
|
||||
(MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId)
|
||||
}.firstOrNull()
|
||||
if (mangaEntry == null) { // create manga entry
|
||||
val mangaId =
|
||||
MangaTable.insertAndGetId {
|
||||
it[url] = manga.url
|
||||
it[title] = manga.title
|
||||
|
||||
it[artist] = manga.artist
|
||||
it[author] = manga.author
|
||||
it[description] = manga.description
|
||||
it[genre] = manga.genre
|
||||
it[status] = manga.status
|
||||
it[thumbnail_url] = manga.thumbnail_url
|
||||
it[updateStrategy] = manga.update_strategy.name
|
||||
it[artist] = manga.artist
|
||||
it[author] = manga.author
|
||||
it[description] = manga.description
|
||||
it[genre] = manga.genre
|
||||
it[status] = manga.status
|
||||
it[thumbnail_url] = manga.thumbnail_url
|
||||
it[updateStrategy] = manga.update_strategy.name
|
||||
|
||||
it[sourceReference] = sourceId
|
||||
}.value
|
||||
it[sourceReference] = sourceId
|
||||
}.value
|
||||
|
||||
mangaEntry = MangaTable.select {
|
||||
(MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId)
|
||||
}.first()
|
||||
mangaEntry =
|
||||
MangaTable.select {
|
||||
(MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId)
|
||||
}.first()
|
||||
|
||||
MangaDataClass(
|
||||
id = mangaId,
|
||||
sourceId = sourceId.toString(),
|
||||
|
||||
url = manga.url,
|
||||
title = manga.title,
|
||||
thumbnailUrl = proxyThumbnailUrl(mangaId),
|
||||
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
|
||||
|
||||
initialized = manga.initialized,
|
||||
|
||||
artist = manga.artist,
|
||||
author = manga.author,
|
||||
description = manga.description,
|
||||
genre = manga.genre.toGenreList(),
|
||||
status = MangaStatus.valueOf(manga.status).name,
|
||||
inLibrary = false, // It's a new manga entry
|
||||
inLibraryAt = 0,
|
||||
meta = getMangaMetaMap(mangaId),
|
||||
realUrl = mangaEntry[MangaTable.realUrl],
|
||||
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
|
||||
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
|
||||
freshData = true
|
||||
)
|
||||
} else {
|
||||
val mangaId = mangaEntry[MangaTable.id].value
|
||||
MangaDataClass(
|
||||
id = mangaId,
|
||||
sourceId = sourceId.toString(),
|
||||
|
||||
url = manga.url,
|
||||
title = manga.title,
|
||||
thumbnailUrl = proxyThumbnailUrl(mangaId),
|
||||
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
|
||||
|
||||
initialized = true,
|
||||
|
||||
artist = mangaEntry[MangaTable.artist],
|
||||
author = mangaEntry[MangaTable.author],
|
||||
description = mangaEntry[MangaTable.description],
|
||||
genre = mangaEntry[MangaTable.genre].toGenreList(),
|
||||
status = MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
inLibrary = mangaEntry[MangaTable.inLibrary],
|
||||
inLibraryAt = mangaEntry[MangaTable.inLibraryAt],
|
||||
meta = getMangaMetaMap(mangaId),
|
||||
realUrl = mangaEntry[MangaTable.realUrl],
|
||||
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
|
||||
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
|
||||
freshData = false
|
||||
)
|
||||
MangaDataClass(
|
||||
id = mangaId,
|
||||
sourceId = sourceId.toString(),
|
||||
url = manga.url,
|
||||
title = manga.title,
|
||||
thumbnailUrl = proxyThumbnailUrl(mangaId),
|
||||
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
|
||||
initialized = manga.initialized,
|
||||
artist = manga.artist,
|
||||
author = manga.author,
|
||||
description = manga.description,
|
||||
genre = manga.genre.toGenreList(),
|
||||
status = MangaStatus.valueOf(manga.status).name,
|
||||
inLibrary = false, // It's a new manga entry
|
||||
inLibraryAt = 0,
|
||||
meta = getMangaMetaMap(mangaId),
|
||||
realUrl = mangaEntry[MangaTable.realUrl],
|
||||
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
|
||||
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
|
||||
freshData = true,
|
||||
)
|
||||
} else {
|
||||
val mangaId = mangaEntry[MangaTable.id].value
|
||||
MangaDataClass(
|
||||
id = mangaId,
|
||||
sourceId = sourceId.toString(),
|
||||
url = manga.url,
|
||||
title = manga.title,
|
||||
thumbnailUrl = proxyThumbnailUrl(mangaId),
|
||||
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
|
||||
initialized = true,
|
||||
artist = mangaEntry[MangaTable.artist],
|
||||
author = mangaEntry[MangaTable.author],
|
||||
description = mangaEntry[MangaTable.description],
|
||||
genre = mangaEntry[MangaTable.genre].toGenreList(),
|
||||
status = MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
inLibrary = mangaEntry[MangaTable.inLibrary],
|
||||
inLibraryAt = mangaEntry[MangaTable.inLibraryAt],
|
||||
meta = getMangaMetaMap(mangaId),
|
||||
realUrl = mangaEntry[MangaTable.realUrl],
|
||||
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
|
||||
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
|
||||
freshData = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return PagedMangaListDataClass(
|
||||
mangaList,
|
||||
mangasPage.hasNextPage
|
||||
mangasPage.hasNextPage,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,21 +31,30 @@ object Page {
|
||||
* A page might have a imageUrl ready from the get go, or we might need to
|
||||
* go an extra step and call fetchImageUrl to get it.
|
||||
*/
|
||||
suspend fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
||||
suspend fun getTrueImageUrl(
|
||||
page: Page,
|
||||
source: HttpSource,
|
||||
): String {
|
||||
if (page.imageUrl == null) {
|
||||
page.imageUrl = source.getImageUrl(page)
|
||||
}
|
||||
return page.imageUrl!!
|
||||
}
|
||||
|
||||
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int, progressFlow: ((StateFlow<Int>) -> Unit)? = null): Pair<InputStream, String> {
|
||||
suspend fun getPageImage(
|
||||
mangaId: Int,
|
||||
chapterIndex: Int,
|
||||
index: Int,
|
||||
progressFlow: ((StateFlow<Int>) -> Unit)? = null,
|
||||
): Pair<InputStream, String> {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val chapterEntry = transaction {
|
||||
ChapterTable.select {
|
||||
(ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||
}.first()
|
||||
}
|
||||
val chapterEntry =
|
||||
transaction {
|
||||
ChapterTable.select {
|
||||
(ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||
}.first()
|
||||
}
|
||||
val chapterId = chapterEntry[ChapterTable.id].value
|
||||
|
||||
val pageEntry =
|
||||
@@ -54,11 +63,12 @@ object Page {
|
||||
.orderBy(PageTable.index to SortOrder.ASC)
|
||||
.limit(1, index.toLong()).first()
|
||||
}
|
||||
val tachiyomiPage = Page(
|
||||
pageEntry[PageTable.index],
|
||||
pageEntry[PageTable.url],
|
||||
pageEntry[PageTable.imageUrl]
|
||||
)
|
||||
val tachiyomiPage =
|
||||
Page(
|
||||
pageEntry[PageTable.index],
|
||||
pageEntry[PageTable.url],
|
||||
pageEntry[PageTable.imageUrl],
|
||||
)
|
||||
progressFlow?.invoke(tachiyomiPage.progress)
|
||||
|
||||
// we treat Local source differently
|
||||
|
||||
@@ -20,13 +20,21 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogue
|
||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||
|
||||
object Search {
|
||||
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
|
||||
suspend fun sourceSearch(
|
||||
sourceId: Long,
|
||||
searchTerm: String,
|
||||
pageNum: Int,
|
||||
): PagedMangaListDataClass {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val searchManga = source.getSearchManga(pageNum, searchTerm, getFilterListOf(source))
|
||||
return searchManga.processEntries(sourceId)
|
||||
}
|
||||
|
||||
suspend fun sourceFilter(sourceId: Long, pageNum: Int, filter: FilterData): PagedMangaListDataClass {
|
||||
suspend fun sourceFilter(
|
||||
sourceId: Long,
|
||||
pageNum: Int,
|
||||
filter: FilterData,
|
||||
): PagedMangaListDataClass {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val filterList = if (filter.filter != null) buildFilterList(sourceId, filter.filter) else source.getFilterList()
|
||||
val searchManga = source.getSearchManga(pageNum, filter.searchTerm ?: "", filterList)
|
||||
@@ -35,14 +43,20 @@ object Search {
|
||||
|
||||
private val filterListCache = mutableMapOf<Long, FilterList>()
|
||||
|
||||
private fun getFilterListOf(source: CatalogueSource, reset: Boolean = false): FilterList {
|
||||
private fun getFilterListOf(
|
||||
source: CatalogueSource,
|
||||
reset: Boolean = false,
|
||||
): FilterList {
|
||||
if (reset || !filterListCache.containsKey(source.id)) {
|
||||
filterListCache[source.id] = source.getFilterList()
|
||||
}
|
||||
return filterListCache[source.id]!!
|
||||
}
|
||||
|
||||
fun getFilterList(sourceId: Long, reset: Boolean): List<FilterObject> {
|
||||
fun getFilterList(
|
||||
sourceId: Long,
|
||||
reset: Boolean,
|
||||
): List<FilterObject> {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
|
||||
return getFilterListOf(source, reset).list.map {
|
||||
@@ -70,30 +84,37 @@ object Search {
|
||||
is Filter.Select<*> -> FilterObject("Select", item)
|
||||
else -> throw RuntimeException("Illegal Group item type!")
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
else -> it
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Filter.Select<*>.getValuesType(): String = values::class.java.componentType!!.simpleName
|
||||
|
||||
class SerializableGroup(name: String, state: List<FilterObject>) : Filter<List<FilterObject>>(name, state)
|
||||
|
||||
data class FilterObject(
|
||||
val type: String,
|
||||
val filter: Filter<*>
|
||||
val filter: Filter<*>,
|
||||
)
|
||||
|
||||
fun setFilter(sourceId: Long, changes: List<FilterChange>) {
|
||||
fun setFilter(
|
||||
sourceId: Long,
|
||||
changes: List<FilterChange>,
|
||||
) {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val filterList = getFilterListOf(source, false)
|
||||
updateFilterList(filterList, changes)
|
||||
}
|
||||
|
||||
private fun updateFilterList(filterList: FilterList, changes: List<FilterChange>): FilterList {
|
||||
private fun updateFilterList(
|
||||
filterList: FilterList,
|
||||
changes: List<FilterChange>,
|
||||
): FilterList {
|
||||
changes.forEach { change ->
|
||||
when (val filter = filterList[change.position]) {
|
||||
is Filter.Header -> {
|
||||
@@ -124,7 +145,10 @@ object Search {
|
||||
return filterList
|
||||
}
|
||||
|
||||
fun buildFilterList(sourceId: Long, changes: List<FilterChange>): FilterList {
|
||||
fun buildFilterList(
|
||||
sourceId: Long,
|
||||
changes: List<FilterChange>,
|
||||
): FilterList {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val filterList = source.getFilterList()
|
||||
return updateFilterList(filterList, changes)
|
||||
@@ -135,13 +159,13 @@ object Search {
|
||||
@Serializable
|
||||
data class FilterChange(
|
||||
val position: Int,
|
||||
val state: String
|
||||
val state: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FilterData(
|
||||
val searchTerm: String?,
|
||||
val filter: List<FilterChange>?
|
||||
val filter: List<FilterChange>?,
|
||||
)
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
|
||||
@@ -46,7 +46,7 @@ object Source {
|
||||
catalogueSource.supportsLatest,
|
||||
catalogueSource is ConfigurableSource,
|
||||
it[SourceTable.isNsfw],
|
||||
catalogueSource.toString()
|
||||
catalogueSource.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -63,12 +63,12 @@ object Source {
|
||||
source[SourceTable.name],
|
||||
source[SourceTable.lang],
|
||||
getExtensionIconUrl(
|
||||
extension[ExtensionTable.apkName]
|
||||
extension[ExtensionTable.apkName],
|
||||
),
|
||||
catalogueSource.supportsLatest,
|
||||
catalogueSource is ConfigurableSource,
|
||||
source[SourceTable.isNsfw],
|
||||
catalogueSource.toString()
|
||||
catalogueSource.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ object Source {
|
||||
*/
|
||||
data class PreferenceObject(
|
||||
val type: String,
|
||||
val props: Any
|
||||
val props: Any,
|
||||
)
|
||||
|
||||
var preferenceScreenMap: MutableMap<Long, PreferenceScreen> = mutableMapOf()
|
||||
@@ -119,7 +119,7 @@ object Source {
|
||||
|
||||
data class SourcePreferenceChange(
|
||||
val position: Int,
|
||||
val value: String
|
||||
val value: String,
|
||||
)
|
||||
|
||||
private val jsonMapper by DI.global.instance<JsonMapper>()
|
||||
@@ -137,7 +137,7 @@ object Source {
|
||||
"Set<String>" -> jsonMapper.fromJsonString(value, List::class.java as Class<List<String>>).toSet()
|
||||
else -> throw RuntimeException("Unsupported type conversion")
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
val screen = preferenceScreenMap[sourceId]!!
|
||||
val pref = screen.preferences[position]
|
||||
|
||||
@@ -12,5 +12,5 @@ data class BackupFlags(
|
||||
val includeCategories: Boolean,
|
||||
val includeChapters: Boolean,
|
||||
val includeTracking: Boolean,
|
||||
val includeHistory: Boolean
|
||||
val includeHistory: Boolean,
|
||||
)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
interface Category : Serializable {
|
||||
|
||||
var id: Int?
|
||||
|
||||
var name: String
|
||||
|
||||
var order: Int
|
||||
|
||||
var flags: Int
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(name: String): Category = CategoryImpl().apply {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun createDefault(): Category = create("Default").apply { id = 0 }
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
class CategoryImpl : Category {
|
||||
|
||||
override var id: Int? = null
|
||||
|
||||
override lateinit var name: String
|
||||
|
||||
override var order: Int = 0
|
||||
|
||||
override var flags: Int = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val category = other as Category
|
||||
return name == category.name
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return name.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import java.io.Serializable
|
||||
|
||||
interface Chapter : SChapter, Serializable {
|
||||
|
||||
var id: Long?
|
||||
|
||||
var manga_id: Long?
|
||||
@@ -23,9 +24,9 @@ interface Chapter : SChapter, Serializable {
|
||||
get() = chapter_number >= 0f
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(): Chapter = ChapterImpl().apply {
|
||||
chapter_number = -1f
|
||||
}
|
||||
fun create(): Chapter =
|
||||
ChapterImpl().apply {
|
||||
chapter_number = -1f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
|
||||
class ChapterImpl : Chapter {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
override var manga_id: Long? = null
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Object containing the history statistics of a chapter
|
||||
*/
|
||||
interface History : Serializable {
|
||||
|
||||
/**
|
||||
* Id of history object.
|
||||
*/
|
||||
var id: Long?
|
||||
|
||||
/**
|
||||
* Chapter id of history object.
|
||||
*/
|
||||
var chapter_id: Long
|
||||
|
||||
/**
|
||||
* Last time chapter was read in time long format
|
||||
*/
|
||||
var last_read: Long
|
||||
|
||||
/**
|
||||
* Total time chapter was read - todo not yet implemented
|
||||
*/
|
||||
var time_read: Long
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* History constructor
|
||||
*
|
||||
* @param chapter chapter object
|
||||
* @return history object
|
||||
*/
|
||||
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
||||
this.chapter_id = chapter.id!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
/**
|
||||
* Object containing the history statistics of a chapter
|
||||
*/
|
||||
class HistoryImpl : History {
|
||||
|
||||
/**
|
||||
* Id of history object.
|
||||
*/
|
||||
override var id: Long? = null
|
||||
|
||||
/**
|
||||
* Chapter id of history object.
|
||||
*/
|
||||
override var chapter_id: Long = 0
|
||||
|
||||
/**
|
||||
* Last time chapter was read in time long format
|
||||
*/
|
||||
override var last_read: Long = 0
|
||||
|
||||
/**
|
||||
* Total time chapter was read - todo not yet implemented
|
||||
*/
|
||||
override var time_read: Long = 0
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@@ -15,7 +17,6 @@ object ReadingModeType {
|
||||
}
|
||||
|
||||
interface Manga : SManga {
|
||||
|
||||
var id: Long?
|
||||
|
||||
var source: Long
|
||||
@@ -48,11 +49,17 @@ interface Manga : SManga {
|
||||
return genre?.split(", ")?.map { it.trim() }
|
||||
}
|
||||
|
||||
private fun setChapterFlags(flag: Int, mask: Int) {
|
||||
private fun setChapterFlags(
|
||||
flag: Int,
|
||||
mask: Int,
|
||||
) {
|
||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||
}
|
||||
|
||||
private fun setViewerFlags(flag: Int, mask: Int) {
|
||||
private fun setViewerFlags(
|
||||
flag: Int,
|
||||
mask: Int,
|
||||
) {
|
||||
viewer_flags = viewer_flags and mask.inv() or (flag and mask)
|
||||
}
|
||||
|
||||
@@ -86,7 +93,6 @@ interface Manga : SManga {
|
||||
set(rotationType) = setViewerFlags(rotationType, OrientationType.MASK)
|
||||
|
||||
companion object {
|
||||
|
||||
// Generic filter that does not filter anything
|
||||
const val SHOW_ALL = 0x00000000
|
||||
|
||||
@@ -115,15 +121,21 @@ interface Manga : SManga {
|
||||
const val CHAPTER_DISPLAY_NUMBER = 0x00100000
|
||||
const val CHAPTER_DISPLAY_MASK = 0x00100000
|
||||
|
||||
fun create(source: Long): Manga = MangaImpl().apply {
|
||||
this.source = source
|
||||
}
|
||||
fun create(source: Long): Manga =
|
||||
MangaImpl().apply {
|
||||
this.source = source
|
||||
}
|
||||
|
||||
fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply {
|
||||
url = pathUrl
|
||||
this.title = title
|
||||
this.source = source
|
||||
}
|
||||
fun create(
|
||||
pathUrl: String,
|
||||
title: String,
|
||||
source: Long = 0,
|
||||
): Manga =
|
||||
MangaImpl().apply {
|
||||
url = pathUrl
|
||||
this.title = title
|
||||
this.source = source
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
class MangaCategory {
|
||||
|
||||
var id: Long? = null
|
||||
|
||||
var manga_id: Long = 0
|
||||
|
||||
var category_id: Int = 0
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(manga: Manga, category: Category): MangaCategory {
|
||||
val mc = MangaCategory()
|
||||
mc.manga_id = manga.id!!
|
||||
mc.category_id = category.id!!
|
||||
return mc
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
class MangaChapter(val manga: Manga, val chapter: Chapter)
|
||||
@@ -1,10 +0,0 @@
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
/**
|
||||
* Object containing manga, chapter and history
|
||||
*
|
||||
* @param manga object containing manga
|
||||
* @param chapter object containing chater
|
||||
* @param history object containing history
|
||||
*/
|
||||
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
@@ -5,7 +7,6 @@ import org.jetbrains.exposed.sql.ResultRow
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
|
||||
open class MangaImpl : Manga {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
override var source: Long = -1
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
interface Track : Serializable {
|
||||
|
||||
var id: Long?
|
||||
|
||||
var manga_id: Long
|
||||
@@ -39,8 +40,9 @@ interface Track : Serializable {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(serviceId: Int): Track = TrackImpl().apply {
|
||||
sync_id = serviceId
|
||||
}
|
||||
fun create(serviceId: Int): Track =
|
||||
TrackImpl().apply {
|
||||
sync_id = serviceId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package suwayomi.tachidesk.manga.impl.backup.models
|
||||
|
||||
class TrackImpl : Track {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
override var manga_id: Long = 0
|
||||
|
||||
@@ -51,7 +51,7 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
private val logger = KotlinLogging.logger { }
|
||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||
private var backupSchedulerJobId: String = ""
|
||||
private const val lastAutomatedBackupKey = "lastAutomatedBackupKey"
|
||||
private const val LAST_AUTOMATED_BACKUP_KEY = "lastAutomatedBackupKey"
|
||||
private val preferences = Preferences.userNodeForPackage(ProtoBackupExport::class.java)
|
||||
|
||||
init {
|
||||
@@ -59,10 +59,10 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
combine(serverConfig.backupInterval, serverConfig.backupTime) { interval, timeOfDay ->
|
||||
Pair(
|
||||
interval,
|
||||
timeOfDay
|
||||
timeOfDay,
|
||||
)
|
||||
},
|
||||
::scheduleAutomatedBackupTask
|
||||
::scheduleAutomatedBackupTask,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
val task = {
|
||||
cleanupAutomatedBackups()
|
||||
createAutomatedBackup()
|
||||
preferences.putLong(lastAutomatedBackupKey, System.currentTimeMillis())
|
||||
preferences.putLong(LAST_AUTOMATED_BACKUP_KEY, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
val (hour, minute) = serverConfig.backupTime.value.split(":").map { it.toInt() }
|
||||
@@ -86,7 +86,7 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
val backupInterval = serverConfig.backupInterval.value.days.coerceAtLeast(1.days)
|
||||
|
||||
// trigger last backup in case the server wasn't running on the scheduled time
|
||||
val lastAutomatedBackup = preferences.getLong(lastAutomatedBackupKey, System.currentTimeMillis())
|
||||
val lastAutomatedBackup = preferences.getLong(LAST_AUTOMATED_BACKUP_KEY, System.currentTimeMillis())
|
||||
val wasPreviousBackupTriggered =
|
||||
(System.currentTimeMillis() - lastAutomatedBackup) < backupInterval.inWholeMilliseconds
|
||||
if (!wasPreviousBackupTriggered) {
|
||||
@@ -105,8 +105,8 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
includeCategories = true,
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true
|
||||
)
|
||||
includeHistory = true,
|
||||
),
|
||||
).use { input ->
|
||||
val automatedBackupDir = File(applicationDirs.automatedBackupRoot)
|
||||
automatedBackupDir.mkdirs()
|
||||
@@ -162,14 +162,15 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
|
||||
val databaseManga = transaction { MangaTable.select { MangaTable.inLibrary eq true } }
|
||||
|
||||
val backup: Backup = transaction {
|
||||
Backup(
|
||||
backupManga(databaseManga, flags),
|
||||
backupCategories(),
|
||||
emptyList(),
|
||||
backupExtensionInfo(databaseManga)
|
||||
)
|
||||
}
|
||||
val backup: Backup =
|
||||
transaction {
|
||||
Backup(
|
||||
backupManga(databaseManga, flags),
|
||||
backupCategories(),
|
||||
emptyList(),
|
||||
backupExtensionInfo(databaseManga),
|
||||
)
|
||||
}
|
||||
|
||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
||||
|
||||
@@ -179,48 +180,54 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
return byteStream.toByteArray().inputStream()
|
||||
}
|
||||
|
||||
private fun backupManga(databaseManga: Query, flags: BackupFlags): List<BackupManga> {
|
||||
private fun backupManga(
|
||||
databaseManga: Query,
|
||||
flags: BackupFlags,
|
||||
): List<BackupManga> {
|
||||
return databaseManga.map { mangaRow ->
|
||||
val backupManga = BackupManga(
|
||||
source = mangaRow[MangaTable.sourceReference],
|
||||
url = mangaRow[MangaTable.url],
|
||||
title = mangaRow[MangaTable.title],
|
||||
artist = mangaRow[MangaTable.artist],
|
||||
author = mangaRow[MangaTable.author],
|
||||
description = mangaRow[MangaTable.description],
|
||||
genre = mangaRow[MangaTable.genre]?.split(", ") ?: emptyList(),
|
||||
status = MangaStatus.valueOf(mangaRow[MangaTable.status]).value,
|
||||
thumbnailUrl = mangaRow[MangaTable.thumbnail_url],
|
||||
dateAdded = TimeUnit.SECONDS.toMillis(mangaRow[MangaTable.inLibraryAt]),
|
||||
viewer = 0, // not supported in Tachidesk
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaRow[MangaTable.updateStrategy])
|
||||
)
|
||||
val backupManga =
|
||||
BackupManga(
|
||||
source = mangaRow[MangaTable.sourceReference],
|
||||
url = mangaRow[MangaTable.url],
|
||||
title = mangaRow[MangaTable.title],
|
||||
artist = mangaRow[MangaTable.artist],
|
||||
author = mangaRow[MangaTable.author],
|
||||
description = mangaRow[MangaTable.description],
|
||||
genre = mangaRow[MangaTable.genre]?.split(", ") ?: emptyList(),
|
||||
status = MangaStatus.valueOf(mangaRow[MangaTable.status]).value,
|
||||
thumbnailUrl = mangaRow[MangaTable.thumbnail_url],
|
||||
dateAdded = TimeUnit.SECONDS.toMillis(mangaRow[MangaTable.inLibraryAt]),
|
||||
viewer = 0, // not supported in Tachidesk
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaRow[MangaTable.updateStrategy]),
|
||||
)
|
||||
|
||||
val mangaId = mangaRow[MangaTable.id].value
|
||||
|
||||
if (flags.includeChapters) {
|
||||
val chapters = transaction {
|
||||
ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
|
||||
.map {
|
||||
ChapterTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
val chapters =
|
||||
transaction {
|
||||
ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
|
||||
.map {
|
||||
ChapterTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
|
||||
backupManga.chapters = chapters.map {
|
||||
BackupChapter(
|
||||
it.url,
|
||||
it.name,
|
||||
it.scanlator,
|
||||
it.read,
|
||||
it.bookmarked,
|
||||
it.lastPageRead,
|
||||
TimeUnit.SECONDS.toMillis(it.fetchedAt),
|
||||
it.uploadDate,
|
||||
it.chapterNumber,
|
||||
chapters.size - it.index
|
||||
)
|
||||
}
|
||||
backupManga.chapters =
|
||||
chapters.map {
|
||||
BackupChapter(
|
||||
it.url,
|
||||
it.name,
|
||||
it.scanlator,
|
||||
it.read,
|
||||
it.bookmarked,
|
||||
it.lastPageRead,
|
||||
TimeUnit.SECONDS.toMillis(it.fetchedAt),
|
||||
it.uploadDate,
|
||||
it.chapterNumber,
|
||||
chapters.size - it.index,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.includeCategories) {
|
||||
@@ -246,7 +253,7 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
BackupCategory(
|
||||
it.name,
|
||||
it.order,
|
||||
0 // not supported in Tachidesk
|
||||
0, // not supported in Tachidesk
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -260,7 +267,7 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
val sourceRow = SourceTable.select { SourceTable.id eq it }.firstOrNull()
|
||||
BackupSource(
|
||||
sourceRow?.get(SourceTable.name) ?: "",
|
||||
it
|
||||
it,
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
|
||||
@@ -47,9 +47,12 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
private val errors = mutableListOf<Pair<Date, String>>()
|
||||
|
||||
private val backupMutex = Mutex()
|
||||
|
||||
sealed class BackupRestoreState {
|
||||
data object Idle : BackupRestoreState()
|
||||
|
||||
data class RestoringCategories(val totalManga: Int) : BackupRestoreState()
|
||||
|
||||
data class RestoringManga(val current: Int, val totalManga: Int, val title: String) : BackupRestoreState()
|
||||
}
|
||||
|
||||
@@ -70,26 +73,28 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
restoreCategories(backup.backupCategories)
|
||||
}
|
||||
|
||||
val categoryMapping = transaction {
|
||||
backup.backupCategories.associate {
|
||||
it.order to CategoryTable.select { CategoryTable.name eq it.name }.first()[CategoryTable.id].value
|
||||
val categoryMapping =
|
||||
transaction {
|
||||
backup.backupCategories.associate {
|
||||
it.order to CategoryTable.select { CategoryTable.name eq it.name }.first()[CategoryTable.id].value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store source mapping for error messages
|
||||
sourceMapping = backup.getSourceMap()
|
||||
|
||||
// Restore individual manga
|
||||
backup.backupManga.forEachIndexed { index, manga ->
|
||||
backupRestoreState.value = BackupRestoreState.RestoringManga(
|
||||
current = index + 1,
|
||||
totalManga = backup.backupManga.size,
|
||||
title = manga.title
|
||||
)
|
||||
backupRestoreState.value =
|
||||
BackupRestoreState.RestoringManga(
|
||||
current = index + 1,
|
||||
totalManga = backup.backupManga.size,
|
||||
title = manga.title,
|
||||
)
|
||||
restoreManga(
|
||||
backupManga = manga,
|
||||
backupCategories = backup.backupCategories,
|
||||
categoryMapping = categoryMapping
|
||||
categoryMapping = categoryMapping,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,7 +131,7 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
private fun restoreManga(
|
||||
backupManga: BackupManga,
|
||||
backupCategories: List<BackupCategory>,
|
||||
categoryMapping: Map<Int, Int>
|
||||
categoryMapping: Map<Int, Int>,
|
||||
) {
|
||||
val manga = backupManga.getMangaImpl()
|
||||
val chapters = backupManga.getChaptersImpl()
|
||||
@@ -150,36 +155,38 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
categoryMapping: Map<Int, Int>
|
||||
categoryMapping: Map<Int, Int>,
|
||||
) {
|
||||
val dbManga = transaction {
|
||||
MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }
|
||||
.firstOrNull()
|
||||
}
|
||||
val dbManga =
|
||||
transaction {
|
||||
MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
if (dbManga == null) { // Manga not in database
|
||||
transaction {
|
||||
// insert manga to database
|
||||
val mangaId = MangaTable.insertAndGetId {
|
||||
it[url] = manga.url
|
||||
it[title] = manga.title
|
||||
val mangaId =
|
||||
MangaTable.insertAndGetId {
|
||||
it[url] = manga.url
|
||||
it[title] = manga.title
|
||||
|
||||
it[artist] = manga.artist
|
||||
it[author] = manga.author
|
||||
it[description] = manga.description
|
||||
it[genre] = manga.genre
|
||||
it[status] = manga.status
|
||||
it[thumbnail_url] = manga.thumbnail_url
|
||||
it[updateStrategy] = manga.update_strategy.name
|
||||
it[artist] = manga.artist
|
||||
it[author] = manga.author
|
||||
it[description] = manga.description
|
||||
it[genre] = manga.genre
|
||||
it[status] = manga.status
|
||||
it[thumbnail_url] = manga.thumbnail_url
|
||||
it[updateStrategy] = manga.update_strategy.name
|
||||
|
||||
it[sourceReference] = manga.source
|
||||
it[sourceReference] = manga.source
|
||||
|
||||
it[initialized] = manga.description != null
|
||||
it[initialized] = manga.description != null
|
||||
|
||||
it[inLibrary] = manga.favorite
|
||||
it[inLibrary] = manga.favorite
|
||||
|
||||
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
|
||||
}.value
|
||||
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
|
||||
}.value
|
||||
|
||||
// insert chapter data
|
||||
val chaptersLength = chapters.size
|
||||
|
||||
@@ -24,7 +24,7 @@ object ProtoBackupValidator {
|
||||
val missingTrackers: List<String>,
|
||||
val mangasMissingSources: List<String>,
|
||||
@JsonIgnore
|
||||
val missingSourceIds: List<Pair<Long, String>>
|
||||
val missingSourceIds: List<Pair<Long, String>>,
|
||||
)
|
||||
|
||||
fun validate(backup: Backup): ValidationResult {
|
||||
@@ -34,9 +34,10 @@ object ProtoBackupValidator {
|
||||
|
||||
val sources = backup.getSourceMap()
|
||||
|
||||
val missingSources = transaction {
|
||||
sources.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
|
||||
}
|
||||
val missingSources =
|
||||
transaction {
|
||||
sources.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
|
||||
}
|
||||
|
||||
// val trackers = backup.backupManga
|
||||
// .flatMap { it.tracking }
|
||||
@@ -56,7 +57,7 @@ object ProtoBackupValidator {
|
||||
.sorted(),
|
||||
missingTrackers,
|
||||
emptyList(),
|
||||
missingSources.toList()
|
||||
missingSources.toList(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ data class Backup(
|
||||
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||
// Bump by 100 to specify this is a 0.x value
|
||||
@ProtoNumber(100) var brokenBackupSources: List<BrokenBackupSource> = emptyList(),
|
||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList()
|
||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||
) {
|
||||
fun getSourceMap(): Map<Long, String> {
|
||||
return (brokenBackupSources.map { BackupSource(it.name, it.sourceId) } + backupSources)
|
||||
|
||||
@@ -2,8 +2,6 @@ package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import suwayomi.tachidesk.manga.impl.backup.models.Category
|
||||
import suwayomi.tachidesk.manga.impl.backup.models.CategoryImpl
|
||||
|
||||
@Serializable
|
||||
class BackupCategory(
|
||||
@@ -11,23 +9,5 @@ class BackupCategory(
|
||||
@ProtoNumber(2) var order: Int = 0,
|
||||
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
||||
// Bump by 100 to specify this is a 0.x value
|
||||
@ProtoNumber(100) var flags: Int = 0
|
||||
) {
|
||||
fun getCategoryImpl(): CategoryImpl {
|
||||
return CategoryImpl().apply {
|
||||
name = this@BackupCategory.name
|
||||
flags = this@BackupCategory.flags
|
||||
order = this@BackupCategory.order
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun copyFrom(category: Category): BackupCategory {
|
||||
return BackupCategory(
|
||||
name = category.name,
|
||||
order = category.order,
|
||||
flags = category.flags
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ProtoNumber(100) var flags: Int = 0,
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@ data class BackupChapter(
|
||||
@ProtoNumber(8) var dateUpload: Long = 0,
|
||||
// chapterNumber is called number is 1.x
|
||||
@ProtoNumber(9) var chapterNumber: Float = 0F,
|
||||
@ProtoNumber(10) var sourceOrder: Int = 0
|
||||
@ProtoNumber(10) var sourceOrder: Int = 0,
|
||||
) {
|
||||
fun toChapterImpl(): ChapterImpl {
|
||||
return ChapterImpl().apply {
|
||||
@@ -49,7 +49,7 @@ data class BackupChapter(
|
||||
lastPageRead = chapter.last_page_read,
|
||||
dateFetch = chapter.date_fetch,
|
||||
dateUpload = chapter.date_upload,
|
||||
sourceOrder = chapter.source_order
|
||||
sourceOrder = chapter.source_order,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
||||
@Serializable
|
||||
data class BrokenBackupHistory(
|
||||
@ProtoNumber(0) var url: String,
|
||||
@ProtoNumber(1) var lastRead: Long
|
||||
@ProtoNumber(1) var lastRead: Long,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BackupHistory(
|
||||
@ProtoNumber(1) var url: String,
|
||||
@ProtoNumber(2) var lastRead: Long
|
||||
@ProtoNumber(2) var lastRead: Long,
|
||||
)
|
||||
|
||||
@@ -37,7 +37,7 @@ data class BackupManga(
|
||||
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(),
|
||||
@ProtoNumber(103) var viewer_flags: Int? = null,
|
||||
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
|
||||
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE
|
||||
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||
) {
|
||||
fun getMangaImpl(): MangaImpl {
|
||||
return MangaImpl().apply {
|
||||
@@ -86,7 +86,7 @@ data class BackupManga(
|
||||
dateAdded = manga.date_added,
|
||||
viewer = manga.readingModeType,
|
||||
viewer_flags = manga.viewer_flags,
|
||||
chapterFlags = manga.chapter_flags
|
||||
chapterFlags = manga.chapter_flags,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,19 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
||||
@Serializable
|
||||
data class BrokenBackupSource(
|
||||
@ProtoNumber(0) var name: String = "",
|
||||
@ProtoNumber(1) var sourceId: Long
|
||||
@ProtoNumber(1) var sourceId: Long,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BackupSource(
|
||||
@ProtoNumber(1) var name: String = "",
|
||||
@ProtoNumber(2) var sourceId: Long
|
||||
@ProtoNumber(2) var sourceId: Long,
|
||||
) {
|
||||
companion object {
|
||||
fun copyFrom(source: Source): BackupSource {
|
||||
return BackupSource(
|
||||
name = source.name,
|
||||
sourceId = source.id
|
||||
sourceId = source.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ data class BackupTracking(
|
||||
// startedReadingDate is called startReadTime in 1.x
|
||||
@ProtoNumber(10) var startedReadingDate: Long = 0,
|
||||
// finishedReadingDate is called endReadTime in 1.x
|
||||
@ProtoNumber(11) var finishedReadingDate: Long = 0
|
||||
@ProtoNumber(11) var finishedReadingDate: Long = 0,
|
||||
) {
|
||||
fun getTrackingImpl(): TrackImpl {
|
||||
return TrackImpl().apply {
|
||||
@@ -58,7 +58,7 @@ data class BackupTracking(
|
||||
status = track.status,
|
||||
startedReadingDate = track.started_reading_date,
|
||||
finishedReadingDate = track.finished_reading_date,
|
||||
trackingUrl = track.tracking_url
|
||||
trackingUrl = track.tracking_url,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,11 @@ import suwayomi.tachidesk.manga.model.table.PageTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
import java.io.File
|
||||
|
||||
suspend fun getChapterDownloadReady(chapterId: Int? = null, chapterIndex: Int? = null, mangaId: Int? = null): ChapterDataClass {
|
||||
suspend fun getChapterDownloadReady(
|
||||
chapterId: Int? = null,
|
||||
chapterIndex: Int? = null,
|
||||
mangaId: Int? = null,
|
||||
): ChapterDataClass {
|
||||
val chapter = ChapterForDownload(chapterId, chapterIndex, mangaId)
|
||||
|
||||
return chapter.asDownloadReady()
|
||||
@@ -39,14 +43,17 @@ suspend fun getChapterDownloadReadyById(chapterId: Int): ChapterDataClass {
|
||||
return getChapterDownloadReady(chapterId = chapterId)
|
||||
}
|
||||
|
||||
suspend fun getChapterDownloadReadyByIndex(chapterIndex: Int, mangaId: Int): ChapterDataClass {
|
||||
suspend fun getChapterDownloadReadyByIndex(
|
||||
chapterIndex: Int,
|
||||
mangaId: Int,
|
||||
): ChapterDataClass {
|
||||
return getChapterDownloadReady(chapterIndex = chapterIndex, mangaId = mangaId)
|
||||
}
|
||||
|
||||
private class ChapterForDownload(
|
||||
optChapterId: Int? = null,
|
||||
optChapterIndex: Int? = null,
|
||||
optMangaId: Int? = null
|
||||
optMangaId: Int? = null,
|
||||
) {
|
||||
suspend fun asDownloadReady(): ChapterDataClass {
|
||||
if (isNotCompletelyDownloaded()) {
|
||||
@@ -74,7 +81,11 @@ private class ChapterForDownload(
|
||||
mangaId = chapterEntry[ChapterTable.manga].value
|
||||
}
|
||||
|
||||
private fun freshChapterEntry(optChapterId: Int? = null, optChapterIndex: Int? = null, optMangaId: Int? = null) = transaction {
|
||||
private fun freshChapterEntry(
|
||||
optChapterId: Int? = null,
|
||||
optChapterIndex: Int? = null,
|
||||
optMangaId: Int? = null,
|
||||
) = transaction {
|
||||
ChapterTable.select {
|
||||
if (optChapterId != null) {
|
||||
ChapterTable.id eq optChapterId
|
||||
@@ -94,7 +105,7 @@ private class ChapterForDownload(
|
||||
SChapter.create().apply {
|
||||
url = chapterEntry[ChapterTable.url]
|
||||
name = chapterEntry[ChapterTable.name]
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,7 +137,7 @@ private class ChapterForDownload(
|
||||
|
||||
private fun updatePageCount(
|
||||
pageList: List<Page>,
|
||||
chapterId: Int
|
||||
chapterId: Int,
|
||||
) {
|
||||
transaction {
|
||||
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||
@@ -141,7 +152,7 @@ private class ChapterForDownload(
|
||||
return !(
|
||||
chapterEntry[ChapterTable.isDownloaded] &&
|
||||
(firstPageExists() || File(getChapterCbzPath(mangaId, chapterEntry[ChapterTable.id].value)).exists())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun firstPageExists(): Boolean {
|
||||
@@ -154,7 +165,7 @@ private class ChapterForDownload(
|
||||
|
||||
return ImageResponse.findFileNameStartingWith(
|
||||
chapterDir,
|
||||
getPageName(0)
|
||||
getPageName(0),
|
||||
) != null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,16 +57,16 @@ object DownloadManager {
|
||||
private val downloadQueue = CopyOnWriteArrayList<DownloadChapter>()
|
||||
private val downloaders = ConcurrentHashMap<String, Downloader>()
|
||||
|
||||
private const val downloadQueueKey = "downloadQueueKey"
|
||||
private const val DOWNLOAD_QUEUE_KEY = "downloadQueueKey"
|
||||
private val sharedPreferences =
|
||||
Injekt.get<Application>().getSharedPreferences(DownloadManager::class.jvmName, Context.MODE_PRIVATE)
|
||||
|
||||
private fun loadDownloadQueue(): List<Int> {
|
||||
return sharedPreferences.getStringSet(downloadQueueKey, emptySet())?.mapNotNull { it.toInt() } ?: emptyList()
|
||||
return sharedPreferences.getStringSet(DOWNLOAD_QUEUE_KEY, emptySet())?.mapNotNull { it.toInt() } ?: emptyList()
|
||||
}
|
||||
|
||||
private fun saveDownloadQueue() {
|
||||
sharedPreferences.edit().putStringSet(downloadQueueKey, downloadQueue.map { it.chapter.id.toString() }.toSet())
|
||||
sharedPreferences.edit().putStringSet(DOWNLOAD_QUEUE_KEY, downloadQueue.map { it.chapter.id.toString() }.toSet())
|
||||
.apply()
|
||||
}
|
||||
|
||||
@@ -95,30 +95,32 @@ object DownloadManager {
|
||||
|
||||
fun notifyClient(ctx: WsContext) {
|
||||
ctx.send(
|
||||
getStatus()
|
||||
getStatus(),
|
||||
)
|
||||
}
|
||||
|
||||
fun handleRequest(ctx: WsMessageContext) {
|
||||
when (ctx.message()) {
|
||||
"STATUS" -> notifyClient(ctx)
|
||||
else -> ctx.send(
|
||||
"""
|
||||
else ->
|
||||
ctx.send(
|
||||
"""
|
||||
|Invalid command.
|
||||
|Supported commands are:
|
||||
| - STATUS
|
||||
| sends the current download status
|
||||
|
|
||||
""".trimMargin()
|
||||
)
|
||||
""".trimMargin(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val notifyFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
val status = notifyFlow.sample(1.seconds)
|
||||
.onStart { emit(Unit) }
|
||||
.map { getStatus() }
|
||||
val status =
|
||||
notifyFlow.sample(1.seconds)
|
||||
.onStart { emit(Unit) }
|
||||
.map { getStatus() }
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
@@ -129,6 +131,7 @@ object DownloadManager {
|
||||
}
|
||||
|
||||
private val saveQueueFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
init {
|
||||
saveQueueFlow.onEach { saveDownloadQueue() }.launchIn(scope)
|
||||
}
|
||||
@@ -159,11 +162,12 @@ object DownloadManager {
|
||||
} else {
|
||||
Status.Started
|
||||
},
|
||||
downloadQueue.toList()
|
||||
downloadQueue.toList(),
|
||||
)
|
||||
}
|
||||
|
||||
private val downloaderWatch = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
init {
|
||||
serverConfig.subscribeTo(serverConfig.maxSourcesInParallel, { maxSourcesInParallel ->
|
||||
val runningDownloaders = downloaders.values.filter { it.isActive }
|
||||
@@ -186,14 +190,18 @@ object DownloadManager {
|
||||
val runningDownloaders = downloaders.values.filter { it.isActive }
|
||||
val availableDownloads = downloadQueue.filter { it.state != Error }
|
||||
|
||||
logger.info { "Running: ${runningDownloaders.size}, Queued: ${availableDownloads.size}, Failed: ${downloadQueue.size - availableDownloads.size}" }
|
||||
logger.info {
|
||||
"Running: ${runningDownloaders.size}, " +
|
||||
"Queued: ${availableDownloads.size}, " +
|
||||
"Failed: ${downloadQueue.size - availableDownloads.size}"
|
||||
}
|
||||
|
||||
if (runningDownloaders.size < serverConfig.maxSourcesInParallel.value) {
|
||||
availableDownloads.asSequence()
|
||||
.map { it.manga.sourceId }
|
||||
.distinct()
|
||||
.minus(
|
||||
runningDownloaders.map { it.sourceId }.toSet()
|
||||
runningDownloaders.map { it.sourceId }.toSet(),
|
||||
)
|
||||
.take(serverConfig.maxSourcesInParallel.value - runningDownloaders.size)
|
||||
.map { getDownloader(it) }
|
||||
@@ -212,24 +220,29 @@ object DownloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDownloader(sourceId: String) = downloaders.getOrPut(sourceId) {
|
||||
Downloader(
|
||||
scope = scope,
|
||||
sourceId = sourceId,
|
||||
downloadQueue = downloadQueue,
|
||||
notifier = ::notifyAllClients,
|
||||
onComplete = ::refreshDownloaders,
|
||||
onDownloadFinished = ::triggerSaveDownloadQueue
|
||||
)
|
||||
}
|
||||
|
||||
fun enqueueWithChapterIndex(mangaId: Int, chapterIndex: Int) {
|
||||
val chapter = transaction {
|
||||
ChapterTable
|
||||
.slice(ChapterTable.id)
|
||||
.select { ChapterTable.manga.eq(mangaId) and ChapterTable.sourceOrder.eq(chapterIndex) }
|
||||
.first()
|
||||
private fun getDownloader(sourceId: String) =
|
||||
downloaders.getOrPut(sourceId) {
|
||||
Downloader(
|
||||
scope = scope,
|
||||
sourceId = sourceId,
|
||||
downloadQueue = downloadQueue,
|
||||
notifier = ::notifyAllClients,
|
||||
onComplete = ::refreshDownloaders,
|
||||
onDownloadFinished = ::triggerSaveDownloadQueue,
|
||||
)
|
||||
}
|
||||
|
||||
fun enqueueWithChapterIndex(
|
||||
mangaId: Int,
|
||||
chapterIndex: Int,
|
||||
) {
|
||||
val chapter =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.slice(ChapterTable.id)
|
||||
.select { ChapterTable.manga.eq(mangaId) and ChapterTable.sourceOrder.eq(chapterIndex) }
|
||||
.first()
|
||||
}
|
||||
enqueue(EnqueueInput(chapterIds = listOf(chapter[ChapterTable.id].value)))
|
||||
}
|
||||
|
||||
@@ -237,35 +250,38 @@ object DownloadManager {
|
||||
// Input might have additional formats in the future, such as "All for mangaID" or "Unread for categoryID"
|
||||
// Having this input format is just future-proofing
|
||||
data class EnqueueInput(
|
||||
val chapterIds: List<Int>?
|
||||
val chapterIds: List<Int>?,
|
||||
)
|
||||
|
||||
fun enqueue(input: EnqueueInput) {
|
||||
if (input.chapterIds.isNullOrEmpty()) return
|
||||
|
||||
val chapters = transaction {
|
||||
(ChapterTable innerJoin MangaTable)
|
||||
.select { ChapterTable.id inList input.chapterIds }
|
||||
.orderBy(ChapterTable.manga)
|
||||
.orderBy(ChapterTable.sourceOrder)
|
||||
.toList()
|
||||
}
|
||||
|
||||
val mangas = transaction {
|
||||
chapters.distinctBy { chapter -> chapter[MangaTable.id] }
|
||||
.map { MangaTable.toDataClass(it) }
|
||||
.associateBy { it.id }
|
||||
}
|
||||
|
||||
val inputPairs = transaction {
|
||||
chapters.map {
|
||||
Pair(
|
||||
// this should be safe because mangas is created above from chapters
|
||||
mangas[it[ChapterTable.manga].value]!!,
|
||||
ChapterTable.toDataClass(it)
|
||||
)
|
||||
val chapters =
|
||||
transaction {
|
||||
(ChapterTable innerJoin MangaTable)
|
||||
.select { ChapterTable.id inList input.chapterIds }
|
||||
.orderBy(ChapterTable.manga)
|
||||
.orderBy(ChapterTable.sourceOrder)
|
||||
.toList()
|
||||
}
|
||||
|
||||
val mangas =
|
||||
transaction {
|
||||
chapters.distinctBy { chapter -> chapter[MangaTable.id] }
|
||||
.map { MangaTable.toDataClass(it) }
|
||||
.associateBy { it.id }
|
||||
}
|
||||
|
||||
val inputPairs =
|
||||
transaction {
|
||||
chapters.map {
|
||||
Pair(
|
||||
// this should be safe because mangas is created above from chapters
|
||||
mangas[it[ChapterTable.manga].value]!!,
|
||||
ChapterTable.toDataClass(it),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addMultipleToQueue(inputPairs)
|
||||
}
|
||||
@@ -289,17 +305,21 @@ object DownloadManager {
|
||||
* Tries to add chapter to queue.
|
||||
* If chapter is added, returns the created DownloadChapter, otherwise returns null
|
||||
*/
|
||||
private fun addToQueue(manga: MangaDataClass, chapter: ChapterDataClass): DownloadChapter? {
|
||||
private fun addToQueue(
|
||||
manga: MangaDataClass,
|
||||
chapter: ChapterDataClass,
|
||||
): DownloadChapter? {
|
||||
val downloadChapter = downloadQueue.firstOrNull { it.mangaId == manga.id && it.chapterIndex == chapter.index }
|
||||
|
||||
val addToQueue = downloadChapter == null
|
||||
if (addToQueue) {
|
||||
val newDownloadChapter = DownloadChapter(
|
||||
chapter.index,
|
||||
manga.id,
|
||||
chapter,
|
||||
manga
|
||||
)
|
||||
val newDownloadChapter =
|
||||
DownloadChapter(
|
||||
chapter.index,
|
||||
manga.id,
|
||||
chapter,
|
||||
manga,
|
||||
)
|
||||
downloadQueue.add(newDownloadChapter)
|
||||
triggerSaveDownloadQueue()
|
||||
logger.debug { "Added chapter ${chapter.id} to download queue ($newDownloadChapter)" }
|
||||
@@ -325,11 +345,17 @@ object DownloadManager {
|
||||
dequeue(downloadQueue.filter { it.chapter.id in input.chapterIds }.toSet())
|
||||
}
|
||||
|
||||
fun dequeue(chapterIndex: Int, mangaId: Int) {
|
||||
fun dequeue(
|
||||
chapterIndex: Int,
|
||||
mangaId: Int,
|
||||
) {
|
||||
dequeue(downloadQueue.filter { it.mangaId == mangaId && it.chapterIndex == chapterIndex }.toSet())
|
||||
}
|
||||
|
||||
fun dequeue(mangaIds: List<Int>, chaptersToIgnore: List<Int> = emptyList()) {
|
||||
fun dequeue(
|
||||
mangaIds: List<Int>,
|
||||
chaptersToIgnore: List<Int> = emptyList(),
|
||||
) {
|
||||
dequeue(downloadQueue.filter { it.mangaId in mangaIds && it.chapter.id !in chaptersToIgnore }.toSet())
|
||||
}
|
||||
|
||||
@@ -342,21 +368,33 @@ object DownloadManager {
|
||||
notifyAllClients()
|
||||
}
|
||||
|
||||
fun reorder(chapterIndex: Int, mangaId: Int, to: Int) {
|
||||
val download = downloadQueue.find { it.mangaId == mangaId && it.chapterIndex == chapterIndex }
|
||||
?: return
|
||||
fun reorder(
|
||||
chapterIndex: Int,
|
||||
mangaId: Int,
|
||||
to: Int,
|
||||
) {
|
||||
val download =
|
||||
downloadQueue.find { it.mangaId == mangaId && it.chapterIndex == chapterIndex }
|
||||
?: return
|
||||
|
||||
reorder(download, to)
|
||||
}
|
||||
|
||||
fun reorder(chapterId: Int, to: Int) {
|
||||
val download = downloadQueue.find { it.chapter.id == chapterId }
|
||||
?: return
|
||||
fun reorder(
|
||||
chapterId: Int,
|
||||
to: Int,
|
||||
) {
|
||||
val download =
|
||||
downloadQueue.find { it.chapter.id == chapterId }
|
||||
?: return
|
||||
|
||||
reorder(download, to)
|
||||
}
|
||||
|
||||
private fun reorder(download: DownloadChapter, to: Int) {
|
||||
private fun reorder(
|
||||
download: DownloadChapter,
|
||||
to: Int,
|
||||
) {
|
||||
require(to >= 0) { "'to' must be over or equal to 0" }
|
||||
|
||||
logger.debug { "reorder download $download from ${downloadQueue.indexOf(download)} to $to" }
|
||||
@@ -400,5 +438,5 @@ object DownloadManager {
|
||||
enum class DownloaderState(val state: Int) {
|
||||
Stopped(0),
|
||||
Running(1),
|
||||
Paused(2)
|
||||
Paused(2),
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class Downloader(
|
||||
private val downloadQueue: CopyOnWriteArrayList<DownloadChapter>,
|
||||
private val notifier: (immediate: Boolean) -> Unit,
|
||||
private val onComplete: () -> Unit,
|
||||
private val onDownloadFinished: () -> Unit
|
||||
private val onDownloadFinished: () -> Unit,
|
||||
) {
|
||||
private val logger = KotlinLogging.logger("${Downloader::class.java.name} source($sourceId)")
|
||||
|
||||
@@ -45,9 +45,13 @@ class Downloader(
|
||||
get() = downloadQueue.filter { it.manga.sourceId == sourceId }
|
||||
|
||||
class StopDownloadException : Exception("Cancelled download")
|
||||
|
||||
class PauseDownloadException : Exception("Pause download")
|
||||
|
||||
private suspend fun step(download: DownloadChapter?, immediate: Boolean) {
|
||||
private suspend fun step(
|
||||
download: DownloadChapter?,
|
||||
immediate: Boolean,
|
||||
) {
|
||||
notifier(immediate)
|
||||
currentCoroutineContext().ensureActive()
|
||||
if (download != null && download != availableSourceDownloads.firstOrNull { it.state != Error }) {
|
||||
@@ -64,16 +68,17 @@ class Downloader(
|
||||
|
||||
fun start() {
|
||||
if (!isActive) {
|
||||
job = scope.launch {
|
||||
run()
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion {
|
||||
if (it !is CancellationException) {
|
||||
logger.debug { "completed" }
|
||||
onComplete()
|
||||
job =
|
||||
scope.launch {
|
||||
run()
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion {
|
||||
if (it !is CancellationException) {
|
||||
logger.debug { "completed" }
|
||||
onComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug { "started" }
|
||||
notifier(false)
|
||||
}
|
||||
@@ -84,7 +89,10 @@ class Downloader(
|
||||
logger.debug { "stopped" }
|
||||
}
|
||||
|
||||
private suspend fun finishDownload(logger: KLogger, download: DownloadChapter) {
|
||||
private suspend fun finishDownload(
|
||||
logger: KLogger,
|
||||
download: DownloadChapter,
|
||||
) {
|
||||
downloadQueue.removeIf { it.mangaId == download.mangaId && it.chapterIndex == download.chapterIndex }
|
||||
step(null, false)
|
||||
logger.debug { "finished" }
|
||||
@@ -93,9 +101,10 @@ class Downloader(
|
||||
|
||||
private suspend fun run() {
|
||||
while (downloadQueue.isNotEmpty() && currentCoroutineContext().isActive) {
|
||||
val download = availableSourceDownloads.firstOrNull {
|
||||
(it.state == Queued || it.state == Finished || (it.state == Error && it.tries < 3)) // 3 re-tries per download
|
||||
} ?: break
|
||||
val download =
|
||||
availableSourceDownloads.firstOrNull {
|
||||
(it.state == Queued || it.state == Finished || (it.state == Error && it.tries < 3)) // 3 re-tries per download
|
||||
} ?: break
|
||||
|
||||
val logContext = "${logger.name} - downloadChapter($download))"
|
||||
val downloadLogger = KotlinLogging.logger(logContext)
|
||||
@@ -120,7 +129,9 @@ class Downloader(
|
||||
ChapterDownloadHelper.download(download.mangaId, download.chapter.id, download, scope, this::step)
|
||||
download.state = Finished
|
||||
transaction {
|
||||
ChapterTable.update({ (ChapterTable.manga eq download.mangaId) and (ChapterTable.sourceOrder eq download.chapterIndex) }) {
|
||||
ChapterTable.update(
|
||||
{ (ChapterTable.manga eq download.mangaId) and (ChapterTable.sourceOrder eq download.chapterIndex) },
|
||||
) {
|
||||
it[isDownloaded] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ abstract class ChaptersFilesProvider(val mangaId: Int, val chapterId: Int) : Dow
|
||||
open suspend fun downloadImpl(
|
||||
download: DownloadChapter,
|
||||
scope: CoroutineScope,
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit,
|
||||
): Boolean {
|
||||
val pageCount = download.chapter.pageCount
|
||||
val chapterDir = getChapterCachePath(mangaId, chapterId)
|
||||
@@ -42,16 +42,17 @@ abstract class ChaptersFilesProvider(val mangaId: Int, val chapterId: Int) : Dow
|
||||
Page.getPageImage(
|
||||
mangaId = download.mangaId,
|
||||
chapterIndex = download.chapterIndex,
|
||||
index = pageNum
|
||||
index = pageNum,
|
||||
) { flow ->
|
||||
pageProgressJob = flow
|
||||
.sample(100)
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
download.progress = (pageNum.toFloat() + (it.toFloat() * 0.01f)) / pageCount
|
||||
step(null, false) // don't throw on canceled download here since we can't do anything
|
||||
}
|
||||
.launchIn(scope)
|
||||
pageProgressJob =
|
||||
flow
|
||||
.sample(100)
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
download.progress = (pageNum.toFloat() + (it.toFloat() * 0.01f)) / pageCount
|
||||
step(null, false) // don't throw on canceled download here since we can't do anything
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
} finally {
|
||||
// always cancel the page progress job even if it throws an exception to avoid memory leaks
|
||||
|
||||
@@ -13,7 +13,11 @@ fun interface FileDownload0Args : FileDownload {
|
||||
}
|
||||
|
||||
fun interface FileDownload3Args<A, B, C> : FileDownload {
|
||||
suspend fun execute(a: A, b: B, c: C): Boolean
|
||||
suspend fun execute(
|
||||
a: A,
|
||||
b: B,
|
||||
c: C,
|
||||
): Boolean
|
||||
|
||||
override suspend fun executeDownload(vararg args: Any): Boolean {
|
||||
return execute(args[0] as A, args[1] as B, args[2] as C)
|
||||
|
||||
@@ -28,7 +28,7 @@ class ArchiveProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(mang
|
||||
override suspend fun downloadImpl(
|
||||
download: DownloadChapter,
|
||||
scope: CoroutineScope,
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit,
|
||||
): Boolean {
|
||||
val mangaDownloadFolder = File(getMangaDownloadDir(mangaId))
|
||||
val outputFile = File(getChapterCbzPath(mangaId, chapterId))
|
||||
@@ -71,7 +71,10 @@ class ArchiveProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(mang
|
||||
return false
|
||||
}
|
||||
|
||||
private fun handleExistingCbzFile(cbzFile: File, chapterFolder: File) {
|
||||
private fun handleExistingCbzFile(
|
||||
cbzFile: File,
|
||||
chapterFolder: File,
|
||||
) {
|
||||
if (!chapterFolder.exists()) chapterFolder.mkdirs()
|
||||
ZipArchiveInputStream(cbzFile.inputStream()).use { zipInputStream ->
|
||||
var zipEntry = zipInputStream.nextEntry
|
||||
|
||||
@@ -25,7 +25,7 @@ class FolderProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(manga
|
||||
override suspend fun downloadImpl(
|
||||
download: DownloadChapter,
|
||||
scope: CoroutineScope,
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit,
|
||||
): Boolean {
|
||||
val chapterDir = getChapterDownloadPath(mangaId, chapterId)
|
||||
val folder = File(chapterDir)
|
||||
@@ -51,10 +51,14 @@ class FolderProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(manga
|
||||
return File(chapterDir).deleteRecursively()
|
||||
}
|
||||
|
||||
private fun isExistingFile(folder: File, fileName: String): Boolean {
|
||||
val existingFile = folder.listFiles { file ->
|
||||
file.isFile && file.name.startsWith(fileName)
|
||||
}?.firstOrNull()
|
||||
private fun isExistingFile(
|
||||
folder: File,
|
||||
fileName: String,
|
||||
): Boolean {
|
||||
val existingFile =
|
||||
folder.listFiles { file ->
|
||||
file.isFile && file.name.startsWith(fileName)
|
||||
}?.firstOrNull()
|
||||
return existingFile?.exists() == true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class DownloadChapter(
|
||||
var manga: MangaDataClass,
|
||||
var state: DownloadState = Queued,
|
||||
var progress: Float = 0f,
|
||||
var tries: Int = 0
|
||||
var tries: Int = 0,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "${manga.title} ($mangaId) - ${chapter.name} (${chapter.id}) | state= $state, tries= $tries, progress= $progress"
|
||||
|
||||
@@ -11,5 +11,5 @@ enum class DownloadState(val state: Int) {
|
||||
Queued(0),
|
||||
Downloading(1),
|
||||
Finished(2),
|
||||
Error(3)
|
||||
Error(3),
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ package suwayomi.tachidesk.manga.impl.download.model
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
enum class Status {
|
||||
Stopped, Started;
|
||||
Stopped,
|
||||
Started,
|
||||
}
|
||||
|
||||
data class DownloadStatus(
|
||||
val status: Status,
|
||||
val queue: List<DownloadChapter>
|
||||
val queue: List<DownloadChapter>,
|
||||
)
|
||||
|
||||
@@ -71,7 +71,10 @@ object Extension {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun installExternalExtension(inputStream: InputStream, apkName: String): Int {
|
||||
suspend fun installExternalExtension(
|
||||
inputStream: InputStream,
|
||||
apkName: String,
|
||||
): Int {
|
||||
return installAPK(true) {
|
||||
val savePath = "${applicationDirs.extensionsRoot}/$apkName"
|
||||
logger.debug { "Saving apk at $apkName" }
|
||||
@@ -87,15 +90,19 @@ object Extension {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun installAPK(forceReinstall: Boolean = false, fetcher: suspend () -> String): Int {
|
||||
suspend fun installAPK(
|
||||
forceReinstall: Boolean = false,
|
||||
fetcher: suspend () -> String,
|
||||
): Int {
|
||||
val apkFilePath = fetcher()
|
||||
val apkName = File(apkFilePath).name
|
||||
|
||||
// check if we don't have the extension already installed
|
||||
// if it's installed and we want to update, it first has to be uninstalled
|
||||
val isInstalled = transaction {
|
||||
ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()
|
||||
}?.get(ExtensionTable.isInstalled) ?: false
|
||||
val isInstalled =
|
||||
transaction {
|
||||
ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()
|
||||
}?.get(ExtensionTable.isInstalled) ?: false
|
||||
|
||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||
|
||||
@@ -119,7 +126,7 @@ object Extension {
|
||||
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
||||
throw Exception(
|
||||
"Lib version is $libVersion, while only versions " +
|
||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -148,18 +155,20 @@ object Extension {
|
||||
|
||||
// collect sources from the extension
|
||||
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
||||
val sources: List<CatalogueSource> = when (extensionMainClassInstance) {
|
||||
is Source -> listOf(extensionMainClassInstance)
|
||||
is SourceFactory -> extensionMainClassInstance.createSources()
|
||||
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
||||
}.map { it as CatalogueSource }
|
||||
val sources: List<CatalogueSource> =
|
||||
when (extensionMainClassInstance) {
|
||||
is Source -> listOf(extensionMainClassInstance)
|
||||
is SourceFactory -> extensionMainClassInstance.createSources()
|
||||
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
||||
}.map { it as CatalogueSource }
|
||||
|
||||
val langs = sources.map { it.lang }.toSet()
|
||||
val extensionLang = when (langs.size) {
|
||||
0 -> ""
|
||||
1 -> langs.first()
|
||||
else -> "all"
|
||||
}
|
||||
val extensionLang =
|
||||
when (langs.size) {
|
||||
0 -> ""
|
||||
1 -> langs.first()
|
||||
else -> "all"
|
||||
}
|
||||
|
||||
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ")
|
||||
|
||||
@@ -205,7 +214,10 @@ object Extension {
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractAssetsFromApk(apkPath: String, jarPath: String) {
|
||||
private fun extractAssetsFromApk(
|
||||
apkPath: String,
|
||||
jarPath: String,
|
||||
) {
|
||||
val apkFile = File(apkPath)
|
||||
val jarFile = File(jarPath)
|
||||
|
||||
@@ -256,7 +268,10 @@ object Extension {
|
||||
|
||||
private val network: NetworkHelper by injectLazy()
|
||||
|
||||
private suspend fun downloadAPKFile(url: String, savePath: String) {
|
||||
private suspend fun downloadAPKFile(
|
||||
url: String,
|
||||
savePath: String,
|
||||
) {
|
||||
val request = Request.Builder().url(url).build()
|
||||
val response = network.client.newCall(request).await()
|
||||
|
||||
@@ -275,23 +290,24 @@ object Extension {
|
||||
val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.first() }
|
||||
val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk")
|
||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||
val sources = transaction {
|
||||
val extensionId = extensionRecord[ExtensionTable.id].value
|
||||
val sources =
|
||||
transaction {
|
||||
val extensionId = extensionRecord[ExtensionTable.id].value
|
||||
|
||||
val sources = SourceTable.select { SourceTable.extension eq extensionId }.map { it[SourceTable.id].value }
|
||||
val sources = SourceTable.select { SourceTable.extension eq extensionId }.map { it[SourceTable.id].value }
|
||||
|
||||
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
||||
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
||||
|
||||
if (extensionRecord[ExtensionTable.isObsolete]) {
|
||||
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
|
||||
} else {
|
||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||
it[isInstalled] = false
|
||||
if (extensionRecord[ExtensionTable.isObsolete]) {
|
||||
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
|
||||
} else {
|
||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||
it[isInstalled] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sources
|
||||
}
|
||||
sources
|
||||
}
|
||||
|
||||
if (File(jarPath).exists()) {
|
||||
// free up the file descriptor if exists
|
||||
@@ -323,17 +339,18 @@ object Extension {
|
||||
}
|
||||
|
||||
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||
val iconUrl = if (apkName == "localSource") {
|
||||
""
|
||||
} else {
|
||||
transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.first() }[ExtensionTable.iconUrl]
|
||||
}
|
||||
val iconUrl =
|
||||
if (apkName == "localSource") {
|
||||
""
|
||||
} else {
|
||||
transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.first() }[ExtensionTable.iconUrl]
|
||||
}
|
||||
|
||||
val cacheSaveDir = "${applicationDirs.extensionsRoot}/icon"
|
||||
|
||||
return getImageResponse(cacheSaveDir, apkName) {
|
||||
network.client.newCall(
|
||||
GET(iconUrl)
|
||||
GET(iconUrl),
|
||||
).await()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,23 +48,24 @@ object ExtensionsList {
|
||||
return extensionTableAsDataClass()
|
||||
}
|
||||
|
||||
fun extensionTableAsDataClass() = transaction {
|
||||
ExtensionTable.selectAll().filter { it[ExtensionTable.name] != LocalSource.EXTENSION_NAME }.map {
|
||||
ExtensionDataClass(
|
||||
it[ExtensionTable.apkName],
|
||||
getExtensionIconUrl(it[ExtensionTable.apkName]),
|
||||
it[ExtensionTable.name],
|
||||
it[ExtensionTable.pkgName],
|
||||
it[ExtensionTable.versionName],
|
||||
it[ExtensionTable.versionCode],
|
||||
it[ExtensionTable.lang],
|
||||
it[ExtensionTable.isNsfw],
|
||||
it[ExtensionTable.isInstalled],
|
||||
it[ExtensionTable.hasUpdate],
|
||||
it[ExtensionTable.isObsolete]
|
||||
)
|
||||
fun extensionTableAsDataClass() =
|
||||
transaction {
|
||||
ExtensionTable.selectAll().filter { it[ExtensionTable.name] != LocalSource.EXTENSION_NAME }.map {
|
||||
ExtensionDataClass(
|
||||
it[ExtensionTable.apkName],
|
||||
getExtensionIconUrl(it[ExtensionTable.apkName]),
|
||||
it[ExtensionTable.name],
|
||||
it[ExtensionTable.pkgName],
|
||||
it[ExtensionTable.versionName],
|
||||
it[ExtensionTable.versionCode],
|
||||
it[ExtensionTable.lang],
|
||||
it[ExtensionTable.isNsfw],
|
||||
it[ExtensionTable.isInstalled],
|
||||
it[ExtensionTable.hasUpdate],
|
||||
it[ExtensionTable.isObsolete],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
|
||||
transaction {
|
||||
|
||||
@@ -36,7 +36,7 @@ object ExtensionGithubApi {
|
||||
val nsfw: Int,
|
||||
val hasReadme: Int = 0,
|
||||
val hasChangelog: Int = 0,
|
||||
val sources: List<ExtensionSourceJsonObject>?
|
||||
val sources: List<ExtensionSourceJsonObject>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -44,27 +44,29 @@ object ExtensionGithubApi {
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val id: Long,
|
||||
val baseUrl: String
|
||||
val baseUrl: String,
|
||||
)
|
||||
|
||||
private var requiresFallbackSource = false
|
||||
|
||||
suspend fun findExtensions(): List<OnlineExtension> {
|
||||
val githubResponse = if (requiresFallbackSource) {
|
||||
null
|
||||
} else {
|
||||
try {
|
||||
client.newCall(GET("${REPO_URL_PREFIX}index.min.json")).awaitSuccess()
|
||||
} catch (e: Throwable) {
|
||||
logger.error(e) { "Failed to get extensions from GitHub" }
|
||||
requiresFallbackSource = true
|
||||
val githubResponse =
|
||||
if (requiresFallbackSource) {
|
||||
null
|
||||
} else {
|
||||
try {
|
||||
client.newCall(GET("${REPO_URL_PREFIX}index.min.json")).awaitSuccess()
|
||||
} catch (e: Throwable) {
|
||||
logger.error(e) { "Failed to get extensions from GitHub" }
|
||||
requiresFallbackSource = true
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val response = githubResponse ?: run {
|
||||
client.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json")).awaitSuccess()
|
||||
}
|
||||
val response =
|
||||
githubResponse ?: run {
|
||||
client.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json")).awaitSuccess()
|
||||
}
|
||||
|
||||
return with(json) {
|
||||
response
|
||||
@@ -107,7 +109,7 @@ object ExtensionGithubApi {
|
||||
hasChangelog = it.hasChangelog == 1,
|
||||
sources = it.sources?.toExtensionSources() ?: emptyList(),
|
||||
apkName = it.apk,
|
||||
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}"
|
||||
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -118,7 +120,7 @@ object ExtensionGithubApi {
|
||||
name = it.name,
|
||||
lang = it.lang,
|
||||
id = it.id,
|
||||
baseUrl = it.baseUrl
|
||||
baseUrl = it.baseUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ data class OnlineExtensionSource(
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val id: Long,
|
||||
val baseUrl: String
|
||||
val baseUrl: String,
|
||||
)
|
||||
|
||||
data class OnlineExtension(
|
||||
@@ -25,5 +25,5 @@ data class OnlineExtension(
|
||||
val hasReadme: Boolean,
|
||||
val hasChangelog: Boolean,
|
||||
val sources: List<OnlineExtensionSource>,
|
||||
val iconUrl: String
|
||||
val iconUrl: String,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,14 @@ import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
||||
|
||||
interface IUpdater {
|
||||
fun getLastUpdateTimestamp(): Long
|
||||
fun addCategoriesToUpdateQueue(categories: List<CategoryDataClass>, clear: Boolean?, forceAll: Boolean)
|
||||
|
||||
fun addCategoriesToUpdateQueue(
|
||||
categories: List<CategoryDataClass>,
|
||||
clear: Boolean?,
|
||||
forceAll: Boolean,
|
||||
)
|
||||
|
||||
val status: StateFlow<UpdateStatus>
|
||||
|
||||
fun reset()
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ enum class JobStatus {
|
||||
RUNNING,
|
||||
COMPLETE,
|
||||
FAILED,
|
||||
SKIPPED
|
||||
SKIPPED,
|
||||
}
|
||||
|
||||
data class UpdateJob(
|
||||
val manga: MangaDataClass,
|
||||
val status: JobStatus = JobStatus.PENDING
|
||||
val status: JobStatus = JobStatus.PENDING,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,8 @@ import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||
|
||||
enum class CategoryUpdateStatus {
|
||||
UPDATING, SKIPPED
|
||||
UPDATING,
|
||||
SKIPPED,
|
||||
}
|
||||
|
||||
data class UpdateStatus(
|
||||
@@ -13,16 +14,21 @@ data class UpdateStatus(
|
||||
val mangaStatusMap: Map<JobStatus, List<MangaDataClass>> = emptyMap(),
|
||||
val running: Boolean = false,
|
||||
@JsonIgnore
|
||||
val numberOfJobs: Int = 0
|
||||
val numberOfJobs: Int = 0,
|
||||
) {
|
||||
|
||||
constructor(categories: Map<CategoryUpdateStatus, List<CategoryDataClass>>, jobs: List<UpdateJob>, skippedMangas: List<MangaDataClass>, running: Boolean) : this(
|
||||
constructor(
|
||||
categories: Map<CategoryUpdateStatus, List<CategoryDataClass>>,
|
||||
jobs: List<UpdateJob>,
|
||||
skippedMangas: List<MangaDataClass>,
|
||||
running: Boolean,
|
||||
) : this(
|
||||
categories,
|
||||
mangaStatusMap = jobs.groupBy { it.status }
|
||||
.mapValues { entry ->
|
||||
entry.value.map { it.manga }
|
||||
}.plus(Pair(JobStatus.SKIPPED, skippedMangas)),
|
||||
mangaStatusMap =
|
||||
jobs.groupBy { it.status }
|
||||
.mapValues { entry ->
|
||||
entry.value.map { it.manga }
|
||||
}.plus(Pair(JobStatus.SKIPPED, skippedMangas)),
|
||||
running = running,
|
||||
numberOfJobs = jobs.size
|
||||
numberOfJobs = jobs.size,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class Updater : IUpdater {
|
||||
}
|
||||
}
|
||||
},
|
||||
ignoreInitialValue = false
|
||||
ignoreInitialValue = false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -87,7 +87,11 @@ class Updater : IUpdater {
|
||||
return
|
||||
}
|
||||
|
||||
logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval.value}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" }
|
||||
logger.info {
|
||||
"Trigger global update (interval= ${serverConfig.globalUpdateInterval.value}h, lastAutomatedUpdate= ${Date(
|
||||
lastAutomatedUpdate,
|
||||
)})"
|
||||
}
|
||||
addCategoriesToUpdateQueue(Category.getCategoryList(), clear = true, forceAll = false)
|
||||
}
|
||||
|
||||
@@ -103,7 +107,10 @@ class Updater : IUpdater {
|
||||
val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0)
|
||||
val timeToNextExecution = (updateInterval - (System.currentTimeMillis() - lastAutomatedUpdate)).mod(updateInterval)
|
||||
|
||||
val wasPreviousUpdateTriggered = System.currentTimeMillis() - (if (lastAutomatedUpdate > 0) lastAutomatedUpdate else System.currentTimeMillis()) < updateInterval
|
||||
val wasPreviousUpdateTriggered =
|
||||
System.currentTimeMillis() - (
|
||||
if (lastAutomatedUpdate > 0) lastAutomatedUpdate else System.currentTimeMillis()
|
||||
) < updateInterval
|
||||
if (!wasPreviousUpdateTriggered) {
|
||||
autoUpdateTask()
|
||||
}
|
||||
@@ -114,7 +121,12 @@ class Updater : IUpdater {
|
||||
/**
|
||||
* Updates the status and sustains the "skippedMangas"
|
||||
*/
|
||||
private fun updateStatus(jobs: List<UpdateJob>, running: Boolean, categories: Map<CategoryUpdateStatus, List<CategoryDataClass>>? = null, skippedMangas: List<MangaDataClass>? = null) {
|
||||
private fun updateStatus(
|
||||
jobs: List<UpdateJob>,
|
||||
running: Boolean,
|
||||
categories: Map<CategoryUpdateStatus, List<CategoryDataClass>>? = null,
|
||||
skippedMangas: List<MangaDataClass>? = null,
|
||||
) {
|
||||
val updateStatusCategories = categories ?: _status.value.categoryStatusMap
|
||||
val tmpSkippedMangas = skippedMangas ?: _status.value.mangaStatusMap[JobStatus.SKIPPED] ?: emptyList()
|
||||
_status.update { UpdateStatus(updateStatusCategories, jobs, tmpSkippedMangas, running) }
|
||||
@@ -136,7 +148,7 @@ class Updater : IUpdater {
|
||||
process(job),
|
||||
tracker.any { (_, job) ->
|
||||
job.status == JobStatus.PENDING || job.status == JobStatus.RUNNING
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -148,19 +160,24 @@ class Updater : IUpdater {
|
||||
private suspend fun process(job: UpdateJob): List<UpdateJob> {
|
||||
tracker[job.manga.id] = job.copy(status = JobStatus.RUNNING)
|
||||
updateStatus(tracker.values.toList(), true)
|
||||
tracker[job.manga.id] = try {
|
||||
logger.info { "Updating \"${job.manga.title}\" (source: ${job.manga.sourceId})" }
|
||||
Chapter.getChapterList(job.manga.id, true)
|
||||
job.copy(status = JobStatus.COMPLETE)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
logger.error(e) { "Error while updating ${job.manga.title}" }
|
||||
job.copy(status = JobStatus.FAILED)
|
||||
}
|
||||
tracker[job.manga.id] =
|
||||
try {
|
||||
logger.info { "Updating \"${job.manga.title}\" (source: ${job.manga.sourceId})" }
|
||||
Chapter.getChapterList(job.manga.id, true)
|
||||
job.copy(status = JobStatus.COMPLETE)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
logger.error(e) { "Error while updating ${job.manga.title}" }
|
||||
job.copy(status = JobStatus.FAILED)
|
||||
}
|
||||
return tracker.values.toList()
|
||||
}
|
||||
|
||||
override fun addCategoriesToUpdateQueue(categories: List<CategoryDataClass>, clear: Boolean?, forceAll: Boolean) {
|
||||
override fun addCategoriesToUpdateQueue(
|
||||
categories: List<CategoryDataClass>,
|
||||
clear: Boolean?,
|
||||
forceAll: Boolean,
|
||||
) {
|
||||
preferences.putLong(lastUpdateKey, System.currentTimeMillis())
|
||||
|
||||
if (clear == true) {
|
||||
@@ -171,31 +188,53 @@ class Updater : IUpdater {
|
||||
val excludedCategories = includeInUpdateStatusToCategoryMap[IncludeInUpdate.EXCLUDE].orEmpty()
|
||||
val includedCategories = includeInUpdateStatusToCategoryMap[IncludeInUpdate.INCLUDE].orEmpty()
|
||||
val unsetCategories = includeInUpdateStatusToCategoryMap[IncludeInUpdate.UNSET].orEmpty()
|
||||
val categoriesToUpdate = if (forceAll) {
|
||||
categories
|
||||
} else {
|
||||
includedCategories.ifEmpty { unsetCategories }
|
||||
}
|
||||
val categoriesToUpdate =
|
||||
if (forceAll) {
|
||||
categories
|
||||
} else {
|
||||
includedCategories.ifEmpty { unsetCategories }
|
||||
}
|
||||
val skippedCategories = categories.subtract(categoriesToUpdate.toSet()).toList()
|
||||
val updateStatusCategories = mapOf(
|
||||
Pair(CategoryUpdateStatus.UPDATING, categoriesToUpdate),
|
||||
Pair(CategoryUpdateStatus.SKIPPED, skippedCategories)
|
||||
)
|
||||
val updateStatusCategories =
|
||||
mapOf(
|
||||
Pair(CategoryUpdateStatus.UPDATING, categoriesToUpdate),
|
||||
Pair(CategoryUpdateStatus.SKIPPED, skippedCategories),
|
||||
)
|
||||
|
||||
logger.debug { "Updating categories: '${categoriesToUpdate.joinToString("', '") { it.name }}'" }
|
||||
|
||||
val categoriesToUpdateMangas = categoriesToUpdate
|
||||
.flatMap { CategoryManga.getCategoryMangaList(it.id) }
|
||||
.distinctBy { it.id }
|
||||
val categoriesToUpdateMangas =
|
||||
categoriesToUpdate
|
||||
.flatMap { CategoryManga.getCategoryMangaList(it.id) }
|
||||
.distinctBy { it.id }
|
||||
val mangasToCategoriesMap = CategoryManga.getMangasCategories(categoriesToUpdateMangas.map { it.id })
|
||||
val mangasToUpdate = categoriesToUpdateMangas
|
||||
.asSequence()
|
||||
.filter { it.updateStrategy == UpdateStrategy.ALWAYS_UPDATE }
|
||||
.filter { if (serverConfig.excludeUnreadChapters.value) { (it.unreadCount ?: 0L) == 0L } else true }
|
||||
.filter { if (serverConfig.excludeNotStarted.value) { it.lastReadAt != null } else true }
|
||||
.filter { if (serverConfig.excludeCompleted.value) { it.status != MangaStatus.COMPLETED.name } else true }
|
||||
.filter { forceAll || !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } }
|
||||
.toList()
|
||||
val mangasToUpdate =
|
||||
categoriesToUpdateMangas
|
||||
.asSequence()
|
||||
.filter { it.updateStrategy == UpdateStrategy.ALWAYS_UPDATE }
|
||||
.filter {
|
||||
if (serverConfig.excludeUnreadChapters.value) {
|
||||
(it.unreadCount ?: 0L) == 0L
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
if (serverConfig.excludeNotStarted.value) {
|
||||
it.lastReadAt != null
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
if (serverConfig.excludeCompleted.value) {
|
||||
it.status != MangaStatus.COMPLETED.name
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
.filter { forceAll || !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } }
|
||||
.toList()
|
||||
val skippedMangas = categoriesToUpdateMangas.subtract(mangasToUpdate.toSet()).toList()
|
||||
|
||||
// In case no manga gets updated and no update job was running before, the client would never receive an info about its update request
|
||||
@@ -207,7 +246,7 @@ class Updater : IUpdater {
|
||||
|
||||
addMangasToQueue(
|
||||
mangasToUpdate
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title))
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,22 +19,26 @@ object UpdaterSocket : Websocket<UpdateStatus>() {
|
||||
private val updater by DI.global.instance<IUpdater>()
|
||||
private var job: Job? = null
|
||||
|
||||
override fun notifyClient(ctx: WsContext, value: UpdateStatus?) {
|
||||
override fun notifyClient(
|
||||
ctx: WsContext,
|
||||
value: UpdateStatus?,
|
||||
) {
|
||||
ctx.send(value ?: updater.status.value)
|
||||
}
|
||||
|
||||
override fun handleRequest(ctx: WsMessageContext) {
|
||||
when (ctx.message()) {
|
||||
"STATUS" -> notifyClient(ctx, updater.status.value)
|
||||
else -> ctx.send(
|
||||
"""
|
||||
else ->
|
||||
ctx.send(
|
||||
"""
|
||||
|Invalid command.
|
||||
|Supported commands are:
|
||||
| - STATUS
|
||||
| sends the current update status
|
||||
|
|
||||
""".trimMargin()
|
||||
)
|
||||
""".trimMargin(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,16 +6,24 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
abstract class Websocket<T> {
|
||||
protected val clients = ConcurrentHashMap<String, WsContext>()
|
||||
|
||||
open fun addClient(ctx: WsContext) {
|
||||
clients[ctx.sessionId] = ctx
|
||||
notifyClient(ctx, null)
|
||||
}
|
||||
|
||||
open fun removeClient(ctx: WsContext) {
|
||||
clients.remove(ctx.sessionId)
|
||||
}
|
||||
|
||||
open fun notifyAllClients(value: T) {
|
||||
clients.values.forEach { notifyClient(it, value) }
|
||||
}
|
||||
abstract fun notifyClient(ctx: WsContext, value: T?)
|
||||
|
||||
abstract fun notifyClient(
|
||||
ctx: WsContext,
|
||||
value: T?,
|
||||
)
|
||||
|
||||
abstract fun handleRequest(ctx: WsMessageContext)
|
||||
}
|
||||
|
||||
@@ -55,13 +55,14 @@ object BytecodeEditor {
|
||||
// Invalid class size
|
||||
return null
|
||||
}
|
||||
val cafebabe = String.format(
|
||||
"%02X%02X%02X%02X",
|
||||
bytes[0],
|
||||
bytes[1],
|
||||
bytes[2],
|
||||
bytes[3]
|
||||
)
|
||||
val cafebabe =
|
||||
String.format(
|
||||
"%02X%02X%02X%02X",
|
||||
bytes[0],
|
||||
bytes[1],
|
||||
bytes[2],
|
||||
bytes[3],
|
||||
)
|
||||
if (cafebabe.lowercase() != "cafebabe") {
|
||||
// Corrupted class
|
||||
return null
|
||||
@@ -80,14 +81,15 @@ object BytecodeEditor {
|
||||
/**
|
||||
* The path where replacement classes will reside
|
||||
*/
|
||||
private const val replacementPath = "xyz/nulldev/androidcompat/replace"
|
||||
private const val REPLACEMENT_PATH = "xyz/nulldev/androidcompat/replace"
|
||||
|
||||
/**
|
||||
* List of classes that will be replaced
|
||||
*/
|
||||
private val classesToReplace = listOf(
|
||||
"java/text/SimpleDateFormat"
|
||||
)
|
||||
private val classesToReplace =
|
||||
listOf(
|
||||
"java/text/SimpleDateFormat",
|
||||
)
|
||||
|
||||
/**
|
||||
* Replace direct references to the class, used on places
|
||||
@@ -95,11 +97,12 @@ object BytecodeEditor {
|
||||
*
|
||||
* @return [String] of class or null if [String] was null
|
||||
*/
|
||||
private fun String?.replaceDirectly() = when (this) {
|
||||
null -> this
|
||||
in classesToReplace -> "$replacementPath/$this"
|
||||
else -> this
|
||||
}
|
||||
private fun String?.replaceDirectly() =
|
||||
when (this) {
|
||||
null -> this
|
||||
in classesToReplace -> "$REPLACEMENT_PATH/$this"
|
||||
else -> this
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace references to the class, used in places that have
|
||||
@@ -112,7 +115,7 @@ object BytecodeEditor {
|
||||
var classReference = this
|
||||
if (classReference != null) {
|
||||
classesToReplace.forEach {
|
||||
classReference = classReference?.replace(it, "$replacementPath/$it")
|
||||
classReference = classReference?.replace(it, "$REPLACEMENT_PATH/$it")
|
||||
}
|
||||
}
|
||||
return classReference
|
||||
@@ -142,7 +145,7 @@ object BytecodeEditor {
|
||||
name: String?,
|
||||
desc: String?,
|
||||
signature: String?,
|
||||
cst: Any?
|
||||
cst: Any?,
|
||||
): FieldVisitor? {
|
||||
logger.trace { "CLass Field" to "${desc.replaceIndirectly()}: ${cst?.let { it::class.java.simpleName }}: $cst" }
|
||||
return super.visitField(access, name, desc.replaceIndirectly(), signature, cst)
|
||||
@@ -154,7 +157,7 @@ object BytecodeEditor {
|
||||
name: String?,
|
||||
signature: String?,
|
||||
superName: String?,
|
||||
interfaces: Array<out String>?
|
||||
interfaces: Array<out String>?,
|
||||
) {
|
||||
logger.trace { "Visiting $name: $signature: $superName" }
|
||||
super.visit(version, access, name, signature, superName, interfaces)
|
||||
@@ -171,16 +174,17 @@ object BytecodeEditor {
|
||||
name: String,
|
||||
desc: String,
|
||||
signature: String?,
|
||||
exceptions: Array<String?>?
|
||||
exceptions: Array<String?>?,
|
||||
): MethodVisitor {
|
||||
logger.trace { "Processing method $name: ${desc.replaceIndirectly()}: $signature" }
|
||||
val mv: MethodVisitor? = super.visitMethod(
|
||||
access,
|
||||
name,
|
||||
desc.replaceIndirectly(),
|
||||
signature,
|
||||
exceptions
|
||||
)
|
||||
val mv: MethodVisitor? =
|
||||
super.visitMethod(
|
||||
access,
|
||||
name,
|
||||
desc.replaceIndirectly(),
|
||||
signature,
|
||||
exceptions,
|
||||
)
|
||||
return object : MethodVisitor(Opcodes.ASM5, mv) {
|
||||
override fun visitLdcInsn(cst: Any?) {
|
||||
logger.trace { "Ldc" to "${cst?.let { "${it::class.java.simpleName}: $it" }}" }
|
||||
@@ -192,13 +196,16 @@ object BytecodeEditor {
|
||||
// fun fetchChapterList() {
|
||||
// if (format is SimpleDateFormat)
|
||||
// }
|
||||
override fun visitTypeInsn(opcode: Int, type: String?) {
|
||||
override fun visitTypeInsn(
|
||||
opcode: Int,
|
||||
type: String?,
|
||||
) {
|
||||
logger.trace {
|
||||
"Type" to "$opcode: ${type.replaceDirectly()}"
|
||||
}
|
||||
super.visitTypeInsn(
|
||||
opcode,
|
||||
type.replaceDirectly()
|
||||
type.replaceDirectly(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -211,7 +218,7 @@ object BytecodeEditor {
|
||||
owner: String?,
|
||||
name: String?,
|
||||
desc: String?,
|
||||
itf: Boolean
|
||||
itf: Boolean,
|
||||
) {
|
||||
logger.trace {
|
||||
"Method" to "$opcode: ${owner.replaceDirectly()}: $name: ${desc.replaceIndirectly()}"
|
||||
@@ -221,7 +228,7 @@ object BytecodeEditor {
|
||||
owner.replaceDirectly(),
|
||||
name,
|
||||
desc.replaceIndirectly(),
|
||||
itf
|
||||
itf,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -234,7 +241,7 @@ object BytecodeEditor {
|
||||
opcode: Int,
|
||||
owner: String?,
|
||||
name: String?,
|
||||
desc: String?
|
||||
desc: String?,
|
||||
) {
|
||||
logger.trace { "Field" to "$opcode: $owner: $name: ${desc.replaceIndirectly()}" }
|
||||
super.visitFieldInsn(opcode, owner, name, desc.replaceIndirectly())
|
||||
@@ -244,7 +251,7 @@ object BytecodeEditor {
|
||||
name: String?,
|
||||
desc: String?,
|
||||
bsm: Handle?,
|
||||
vararg bsmArgs: Any?
|
||||
vararg bsmArgs: Any?,
|
||||
) {
|
||||
logger.trace { "InvokeDynamic" to "$name: $desc" }
|
||||
super.visitInvokeDynamicInsn(name, desc, bsm, *bsmArgs)
|
||||
@@ -252,7 +259,7 @@ object BytecodeEditor {
|
||||
}
|
||||
}
|
||||
},
|
||||
0
|
||||
0,
|
||||
)
|
||||
return pair.first to cw.toByteArray()
|
||||
}
|
||||
@@ -262,7 +269,7 @@ object BytecodeEditor {
|
||||
pair.first,
|
||||
pair.second,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING
|
||||
StandardOpenOption.TRUNCATE_EXISTING,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +31,19 @@ private fun getMangaDir(mangaId: Int): String {
|
||||
return "$sourceDir/$mangaDir"
|
||||
}
|
||||
|
||||
private fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||
private fun getChapterDir(
|
||||
mangaId: Int,
|
||||
chapterId: Int,
|
||||
): String {
|
||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() }
|
||||
|
||||
val chapterDir = SafePath.buildValidFilename(
|
||||
when {
|
||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||
else -> chapterEntry[ChapterTable.name]
|
||||
}
|
||||
)
|
||||
val chapterDir =
|
||||
SafePath.buildValidFilename(
|
||||
when {
|
||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||
else -> chapterEntry[ChapterTable.name]
|
||||
},
|
||||
)
|
||||
|
||||
return getMangaDir(mangaId) + "/$chapterDir"
|
||||
}
|
||||
@@ -52,20 +56,32 @@ fun getMangaDownloadDir(mangaId: Int): String {
|
||||
return applicationDirs.mangaDownloadsRoot + "/" + getMangaDir(mangaId)
|
||||
}
|
||||
|
||||
fun getChapterDownloadPath(mangaId: Int, chapterId: Int): String {
|
||||
fun getChapterDownloadPath(
|
||||
mangaId: Int,
|
||||
chapterId: Int,
|
||||
): String {
|
||||
return applicationDirs.mangaDownloadsRoot + "/" + getChapterDir(mangaId, chapterId)
|
||||
}
|
||||
|
||||
fun getChapterCbzPath(mangaId: Int, chapterId: Int): String {
|
||||
fun getChapterCbzPath(
|
||||
mangaId: Int,
|
||||
chapterId: Int,
|
||||
): String {
|
||||
return getChapterDownloadPath(mangaId, chapterId) + ".cbz"
|
||||
}
|
||||
|
||||
fun getChapterCachePath(mangaId: Int, chapterId: Int): String {
|
||||
fun getChapterCachePath(
|
||||
mangaId: Int,
|
||||
chapterId: Int,
|
||||
): String {
|
||||
return applicationDirs.tempMangaCacheRoot + "/" + getChapterDir(mangaId, chapterId)
|
||||
}
|
||||
|
||||
/** return value says if rename/move was successful */
|
||||
fun updateMangaDownloadDir(mangaId: Int, newTitle: String): Boolean {
|
||||
fun updateMangaDownloadDir(
|
||||
mangaId: Int,
|
||||
newTitle: String,
|
||||
): Boolean {
|
||||
val mangaEntry = getMangaEntry(mangaId)
|
||||
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
|
||||
@@ -43,14 +43,18 @@ object PackageTools {
|
||||
const val LIB_VERSION_MIN = 1.3
|
||||
const val LIB_VERSION_MAX = 1.5
|
||||
|
||||
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" // inorichi's key
|
||||
private const val unofficialSignature = "64feb21075ba97ebc9cc981243645b331595c111cef1b0d084236a0403b00581" // ArMor's key
|
||||
val trustedSignatures = mutableSetOf<String>() + officialSignature + unofficialSignature
|
||||
private const val OFFICIAL_SIGNATURE = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" // inorichi's key
|
||||
private const val UNOFFICIAL_SIGNATURE = "64feb21075ba97ebc9cc981243645b331595c111cef1b0d084236a0403b00581" // ArMor's key
|
||||
val trustedSignatures = setOf(OFFICIAL_SIGNATURE, UNOFFICIAL_SIGNATURE)
|
||||
|
||||
/**
|
||||
* Convert dex to jar, a wrapper for the dex2jar library
|
||||
*/
|
||||
fun dex2jar(dexFile: String, jarFile: String, fileNameWithoutType: String) {
|
||||
fun dex2jar(
|
||||
dexFile: String,
|
||||
jarFile: String,
|
||||
fileNameWithoutType: String,
|
||||
) {
|
||||
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
|
||||
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
|
||||
|
||||
@@ -78,7 +82,7 @@ object PackageTools {
|
||||
https://bitbucket.org/pxb1988/dex2jar/issues
|
||||
https://github.com/pxb1988/dex2jar/issues
|
||||
dex2jar@googlegroups.com
|
||||
""".trimIndent()
|
||||
""".trimIndent(),
|
||||
)
|
||||
handler.dump(errorFile, emptyArray<String>())
|
||||
} else {
|
||||
@@ -93,37 +97,40 @@ object PackageTools {
|
||||
val parsed = ApkFile(apk)
|
||||
val dbFactory = DocumentBuilderFactory.newInstance()
|
||||
val dBuilder = dbFactory.newDocumentBuilder()
|
||||
val doc = parsed.manifestXml.byteInputStream().use {
|
||||
dBuilder.parse(it)
|
||||
}
|
||||
val doc =
|
||||
parsed.manifestXml.byteInputStream().use {
|
||||
dBuilder.parse(it)
|
||||
}
|
||||
|
||||
logger.trace(parsed.manifestXml)
|
||||
|
||||
applicationInfo.metaData = Bundle().apply {
|
||||
val appTag = doc.getElementsByTagName("application").item(0)
|
||||
applicationInfo.metaData =
|
||||
Bundle().apply {
|
||||
val appTag = doc.getElementsByTagName("application").item(0)
|
||||
|
||||
appTag?.childNodes?.toList()
|
||||
.orEmpty()
|
||||
.asSequence()
|
||||
.filter {
|
||||
it.nodeType == Node.ELEMENT_NODE
|
||||
}.map {
|
||||
it as Element
|
||||
}.filter {
|
||||
it.tagName == "meta-data"
|
||||
}.forEach {
|
||||
putString(
|
||||
it.attributes.getNamedItem("android:name").nodeValue,
|
||||
it.attributes.getNamedItem("android:value").nodeValue
|
||||
)
|
||||
}
|
||||
}
|
||||
appTag?.childNodes?.toList()
|
||||
.orEmpty()
|
||||
.asSequence()
|
||||
.filter {
|
||||
it.nodeType == Node.ELEMENT_NODE
|
||||
}.map {
|
||||
it as Element
|
||||
}.filter {
|
||||
it.tagName == "meta-data"
|
||||
}.forEach {
|
||||
putString(
|
||||
it.attributes.getNamedItem("android:name").nodeValue,
|
||||
it.attributes.getNamedItem("android:value").nodeValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
signatures = (
|
||||
parsed.apkSingers.flatMap { it.certificateMetas }
|
||||
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
|
||||
signatures =
|
||||
(
|
||||
parsed.apkSingers.flatMap { it.certificateMetas }
|
||||
// + parsed.apkV2Singers.flatMap { it.certificateMetas }
|
||||
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
|
||||
.map { Signature(it.data) }.toTypedArray()
|
||||
.map { Signature(it.data) }.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +149,10 @@ object PackageTools {
|
||||
* loads the extension main class called [className] from the jar located at [jarPath]
|
||||
* It may return an instance of HttpSource or SourceFactory depending on the extension.
|
||||
*/
|
||||
fun loadExtensionSources(jarPath: String, className: String): Any {
|
||||
fun loadExtensionSources(
|
||||
jarPath: String,
|
||||
className: String,
|
||||
): Any {
|
||||
try {
|
||||
logger.debug { "loading jar with path: $jarPath" }
|
||||
val classLoader = jarLoaderMap[jarPath] ?: URLClassLoader(arrayOf<URL>(URL("file:$jarPath")))
|
||||
|
||||
@@ -17,50 +17,48 @@ import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
// source: https://github.com/jobobby04/TachiyomiSY/blob/9320221a4e8b118ef68deb60d8c4c32bcbb9e06f/app/src/main/java/eu/kanade/tachiyomi/util/lang/RxCoroutineBridge.kt
|
||||
/*
|
||||
* Util functions for bridging RxJava and coroutines. Taken from TachiyomiEH/SY.
|
||||
*/
|
||||
// Util functions for bridging RxJava and coroutines. Taken from TachiyomiEH/SY.
|
||||
|
||||
suspend fun <T> Observable<T>.awaitSingle(): T = single().awaitOne()
|
||||
|
||||
@OptIn(InternalCoroutinesApi::class)
|
||||
private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont ->
|
||||
cont.unsubscribeOnCancellation(
|
||||
subscribe(
|
||||
object : Subscriber<T>() {
|
||||
override fun onStart() {
|
||||
request(1)
|
||||
}
|
||||
private suspend fun <T> Observable<T>.awaitOne(): T =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
cont.unsubscribeOnCancellation(
|
||||
subscribe(
|
||||
object : Subscriber<T>() {
|
||||
override fun onStart() {
|
||||
request(1)
|
||||
}
|
||||
|
||||
override fun onNext(t: T) {
|
||||
cont.resume(t)
|
||||
}
|
||||
override fun onNext(t: T) {
|
||||
cont.resume(t)
|
||||
}
|
||||
|
||||
override fun onCompleted() {
|
||||
if (cont.isActive) {
|
||||
cont.resumeWithException(
|
||||
IllegalStateException(
|
||||
"Should have invoked onNext"
|
||||
override fun onCompleted() {
|
||||
if (cont.isActive) {
|
||||
cont.resumeWithException(
|
||||
IllegalStateException(
|
||||
"Should have invoked onNext",
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
override fun onError(e: Throwable) {
|
||||
/*
|
||||
* Rx1 observable throws NoSuchElementException if cancellation happened before
|
||||
* element emission. To mitigate this we try to atomically resume continuation with exception:
|
||||
* if resume failed, then we know that continuation successfully cancelled itself
|
||||
*/
|
||||
val token = cont.tryResumeWithException(e)
|
||||
if (token != null) {
|
||||
cont.completeResume(token)
|
||||
* Rx1 observable throws NoSuchElementException if cancellation happened before
|
||||
* element emission. To mitigate this we try to atomically resume continuation with exception:
|
||||
* if resume failed, then we know that continuation successfully cancelled itself
|
||||
*/
|
||||
val token = cont.tryResumeWithException(e)
|
||||
if (token != null) {
|
||||
cont.completeResume(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) =
|
||||
invokeOnCancellation { sub.unsubscribe() }
|
||||
private fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) = invokeOnCancellation { sub.unsubscribe() }
|
||||
|
||||
@@ -20,7 +20,10 @@ suspend fun Call.await(): Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
enqueue(
|
||||
object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
override fun onResponse(
|
||||
call: Call,
|
||||
response: Response,
|
||||
) {
|
||||
if (!response.isSuccessful) {
|
||||
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
||||
return
|
||||
@@ -31,12 +34,15 @@ suspend fun Call.await(): Response {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
override fun onFailure(
|
||||
call: Call,
|
||||
e: IOException,
|
||||
) {
|
||||
// Don't bother with resuming the continuation if it is already cancelled.
|
||||
if (continuation.isCancelled) return
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
|
||||
@@ -35,14 +35,16 @@ object GetCatalogueSource {
|
||||
return cachedResult
|
||||
}
|
||||
|
||||
val sourceRecord = transaction {
|
||||
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
|
||||
} ?: return null
|
||||
val sourceRecord =
|
||||
transaction {
|
||||
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
|
||||
} ?: return null
|
||||
|
||||
val extensionId = sourceRecord[SourceTable.extension]
|
||||
val extensionRecord = transaction {
|
||||
ExtensionTable.select { ExtensionTable.id eq extensionId }.first()
|
||||
}
|
||||
val extensionRecord =
|
||||
transaction {
|
||||
ExtensionTable.select { ExtensionTable.id eq extensionId }.first()
|
||||
}
|
||||
|
||||
val apkName = extensionRecord[ExtensionTable.apkName]
|
||||
val className = extensionRecord[ExtensionTable.classFQName]
|
||||
|
||||
@@ -27,7 +27,11 @@ open class StubSource(override val id: Long) : CatalogueSource {
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
override fun fetchSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> {
|
||||
return Observable.error(getSourceNotInstalledException())
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ object ImageResponse {
|
||||
}
|
||||
|
||||
/** find file with name when file extension is not known */
|
||||
fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
||||
fun findFileNameStartingWith(
|
||||
directoryPath: String,
|
||||
fileName: String,
|
||||
): String? {
|
||||
val target = "$fileName."
|
||||
File(directoryPath).listFiles().orEmpty().forEach { file ->
|
||||
if (file.name.startsWith(target)) {
|
||||
@@ -30,11 +33,14 @@ object ImageResponse {
|
||||
return null
|
||||
}
|
||||
|
||||
fun getCachedImageResponse(cachedFile: String, filePath: String): Pair<InputStream, String> {
|
||||
fun getCachedImageResponse(
|
||||
cachedFile: String,
|
||||
filePath: String,
|
||||
): Pair<InputStream, String> {
|
||||
val fileType = cachedFile.substringAfter("$filePath.")
|
||||
return Pair(
|
||||
pathToInputStream(cachedFile),
|
||||
"image/$fileType"
|
||||
"image/$fileType",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,7 +55,7 @@ object ImageResponse {
|
||||
suspend fun getImageResponse(
|
||||
saveDir: String,
|
||||
fileName: String,
|
||||
fetcher: suspend () -> Response
|
||||
fetcher: suspend () -> Response,
|
||||
): Pair<InputStream, String> {
|
||||
File(saveDir).mkdirs()
|
||||
|
||||
@@ -80,14 +86,18 @@ object ImageResponse {
|
||||
}
|
||||
|
||||
/** Save image safely */
|
||||
fun saveImage(filePath: String, image: InputStream): Pair<String, String> {
|
||||
fun saveImage(
|
||||
filePath: String,
|
||||
image: InputStream,
|
||||
): Pair<String, String> {
|
||||
val tmpSavePath = "$filePath.tmp"
|
||||
val tmpSaveFile = File(tmpSavePath)
|
||||
image.use { input -> tmpSaveFile.outputStream().use { output -> input.copyTo(output) } }
|
||||
|
||||
// find image type
|
||||
val imageType = ImageUtil.findImageType { tmpSaveFile.inputStream() }?.mime
|
||||
?: "image/jpeg"
|
||||
val imageType =
|
||||
ImageUtil.findImageType { tmpSaveFile.inputStream() }?.mime
|
||||
?: "image/jpeg"
|
||||
|
||||
val actualSavePath = "$filePath.${imageType.substringAfter("/")}"
|
||||
|
||||
@@ -95,7 +105,10 @@ object ImageResponse {
|
||||
return Pair(actualSavePath, imageType)
|
||||
}
|
||||
|
||||
fun clearCachedImage(saveDir: String, fileName: String) {
|
||||
fun clearCachedImage(
|
||||
saveDir: String,
|
||||
fileName: String,
|
||||
) {
|
||||
val cachedFile = findFileNameStartingWith(saveDir, fileName)
|
||||
cachedFile?.also {
|
||||
File(it).delete()
|
||||
|
||||
@@ -19,12 +19,16 @@ import java.net.URLConnection
|
||||
|
||||
// adopted from: eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
object ImageUtil {
|
||||
fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean {
|
||||
val contentType = try {
|
||||
URLConnection.guessContentTypeFromName(name)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} ?: openStream?.let { findImageType(it)?.mime }
|
||||
fun isImage(
|
||||
name: String,
|
||||
openStream: (() -> InputStream)? = null,
|
||||
): Boolean {
|
||||
val contentType =
|
||||
try {
|
||||
URLConnection.guessContentTypeFromName(name)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} ?: openStream?.let { findImageType(it)?.mime }
|
||||
return contentType?.startsWith("image/") ?: false
|
||||
}
|
||||
|
||||
@@ -36,12 +40,13 @@ object ImageUtil {
|
||||
try {
|
||||
val bytes = ByteArray(12)
|
||||
|
||||
val length = if (stream.markSupported()) {
|
||||
stream.mark(bytes.size)
|
||||
stream.read(bytes, 0, bytes.size).also { stream.reset() }
|
||||
} else {
|
||||
stream.read(bytes, 0, bytes.size)
|
||||
}
|
||||
val length =
|
||||
if (stream.markSupported()) {
|
||||
stream.mark(bytes.size)
|
||||
stream.read(bytes, 0, bytes.size).also { stream.reset() }
|
||||
} else {
|
||||
stream.read(bytes, 0, bytes.size)
|
||||
}
|
||||
|
||||
if (length == -1) {
|
||||
return null
|
||||
@@ -160,6 +165,6 @@ object ImageUtil {
|
||||
JPEG("image/jpeg", "jpg"),
|
||||
JXL("image/jxl", "jxl"),
|
||||
PNG("image/png", "png"),
|
||||
WEBP("image/webp", "webp")
|
||||
WEBP("image/webp", "webp"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ package suwayomi.tachidesk.manga.impl.util.storage
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
fun ZipEntry.use(stream: ZipInputStream, block: (ZipEntry) -> Unit) {
|
||||
fun ZipEntry.use(
|
||||
stream: ZipInputStream,
|
||||
block: (ZipEntry) -> Unit,
|
||||
) {
|
||||
var exception: Throwable? = null
|
||||
try {
|
||||
return block(this)
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.io.File
|
||||
import java.io.OutputStream
|
||||
|
||||
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/util/storage/OkioExtensions.kt
|
||||
|
||||
/**
|
||||
* Saves the given source to a file and closes it. Directories will be created if needed.
|
||||
*
|
||||
|
||||
@@ -9,8 +9,13 @@ import com.fasterxml.jackson.annotation.JsonValue
|
||||
* 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/. */
|
||||
|
||||
enum class IncludeInUpdate(@JsonValue val value: Int) {
|
||||
EXCLUDE(0), INCLUDE(1), UNSET(-1);
|
||||
enum class IncludeInUpdate(
|
||||
@JsonValue val value: Int,
|
||||
) {
|
||||
EXCLUDE(0),
|
||||
INCLUDE(1),
|
||||
UNSET(-1),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int) = IncludeInUpdate.values().find { it.value == value } ?: UNSET
|
||||
@@ -24,5 +29,5 @@ data class CategoryDataClass(
|
||||
val default: Boolean,
|
||||
val size: Int,
|
||||
val includeInUpdate: IncludeInUpdate,
|
||||
val meta: Map<String, String> = emptyMap()
|
||||
val meta: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
@@ -15,38 +15,27 @@ data class ChapterDataClass(
|
||||
val chapterNumber: Float,
|
||||
val scanlator: String?,
|
||||
val mangaId: Int,
|
||||
|
||||
/** chapter is read */
|
||||
val read: Boolean,
|
||||
|
||||
/** chapter is bookmarked */
|
||||
val bookmarked: Boolean,
|
||||
|
||||
/** last read page, zero means not read/no data */
|
||||
val lastPageRead: Int,
|
||||
|
||||
/** last read page, zero means not read/no data */
|
||||
val lastReadAt: Long,
|
||||
|
||||
// TODO(v0.6.0): rename to sourceOrder
|
||||
/** this chapter's index, starts with 1 */
|
||||
val index: Int,
|
||||
|
||||
/** the date we fist saw this chapter*/
|
||||
val fetchedAt: Long,
|
||||
|
||||
/** the website url of this chapter*/
|
||||
val realUrl: String? = null,
|
||||
|
||||
/** is chapter downloaded */
|
||||
val downloaded: Boolean,
|
||||
|
||||
/** used to construct pages in the front-end */
|
||||
val pageCount: Int = -1,
|
||||
|
||||
/** total chapter count, used to calculate if there's a next and prev chapter */
|
||||
val chapterCount: Int? = null,
|
||||
|
||||
/** used to store client specific values */
|
||||
val meta: Map<String, String> = emptyMap()
|
||||
val meta: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
@@ -10,15 +10,13 @@ package suwayomi.tachidesk.manga.model.dataclass
|
||||
data class ExtensionDataClass(
|
||||
val apkName: String,
|
||||
val iconUrl: String,
|
||||
|
||||
val name: String,
|
||||
val pkgName: String,
|
||||
val versionName: String,
|
||||
val versionCode: Int,
|
||||
val lang: String,
|
||||
val isNsfw: Boolean,
|
||||
|
||||
val installed: Boolean,
|
||||
val hasUpdate: Boolean,
|
||||
val obsolete: Boolean
|
||||
val obsolete: Boolean,
|
||||
)
|
||||
|
||||
@@ -9,5 +9,5 @@ package suwayomi.tachidesk.manga.model.dataclass
|
||||
|
||||
data class MangaChapterDataClass(
|
||||
val manga: MangaDataClass,
|
||||
val chapter: ChapterDataClass
|
||||
val chapter: ChapterDataClass,
|
||||
)
|
||||
|
||||
@@ -15,14 +15,11 @@ import java.time.Instant
|
||||
data class MangaDataClass(
|
||||
val id: Int,
|
||||
val sourceId: String,
|
||||
|
||||
val url: String,
|
||||
val title: String,
|
||||
val thumbnailUrl: String? = null,
|
||||
val thumbnailUrlLastFetched: Long = 0,
|
||||
|
||||
val initialized: Boolean = false,
|
||||
|
||||
val artist: String? = null,
|
||||
val author: String? = null,
|
||||
val description: String? = null,
|
||||
@@ -31,30 +28,25 @@ data class MangaDataClass(
|
||||
val inLibrary: Boolean = false,
|
||||
val inLibraryAt: Long = 0,
|
||||
val source: SourceDataClass? = null,
|
||||
|
||||
/** meta data for clients */
|
||||
val meta: Map<String, String> = emptyMap(),
|
||||
|
||||
val realUrl: String? = null,
|
||||
var lastFetchedAt: Long? = 0,
|
||||
var chaptersLastFetchedAt: Long? = 0,
|
||||
|
||||
var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||
|
||||
val freshData: Boolean = false,
|
||||
var unreadCount: Long? = null,
|
||||
var downloadCount: Long? = null,
|
||||
var chapterCount: Long? = null,
|
||||
var lastReadAt: Long? = null,
|
||||
var lastChapterRead: ChapterDataClass? = null,
|
||||
|
||||
val age: Long? = if (lastFetchedAt == null) 0 else Instant.now().epochSecond.minus(lastFetchedAt),
|
||||
val chaptersAge: Long? = if (chaptersLastFetchedAt == null) null else Instant.now().epochSecond.minus(chaptersLastFetchedAt)
|
||||
val chaptersAge: Long? = if (chaptersLastFetchedAt == null) null else Instant.now().epochSecond.minus(chaptersLastFetchedAt),
|
||||
)
|
||||
|
||||
data class PagedMangaListDataClass(
|
||||
val mangaList: List<MangaDataClass>,
|
||||
val hasNextPage: Boolean
|
||||
val hasNextPage: Boolean,
|
||||
)
|
||||
|
||||
internal fun String?.toGenreList() = this?.split(",")?.trimAll().orEmpty()
|
||||
|
||||
@@ -9,5 +9,5 @@ package suwayomi.tachidesk.manga.model.dataclass
|
||||
|
||||
data class PageDataClass(
|
||||
val index: Int,
|
||||
var imageUrl: String
|
||||
var imageUrl: String,
|
||||
)
|
||||
|
||||
@@ -11,15 +11,15 @@ import kotlin.math.min
|
||||
|
||||
open class PaginatedList<T>(
|
||||
val page: List<T>,
|
||||
val hasNextPage: Boolean
|
||||
val hasNextPage: Boolean,
|
||||
)
|
||||
|
||||
const val PaginationFactor = 50
|
||||
const val PAGINATION_FACTOR = 50
|
||||
|
||||
fun <T> paginatedFrom(
|
||||
pageNum: Int,
|
||||
paginationFactor: Int = PaginationFactor,
|
||||
lister: () -> List<T>
|
||||
paginationFactor: Int = PAGINATION_FACTOR,
|
||||
lister: () -> List<T>,
|
||||
): PaginatedList<T> {
|
||||
val list = lister()
|
||||
val lastIndex = list.size - 1
|
||||
@@ -35,6 +35,6 @@ fun <T> paginatedFrom(
|
||||
|
||||
return PaginatedList(
|
||||
sliced,
|
||||
higherIndex < lastIndex
|
||||
higherIndex < lastIndex,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,16 +14,12 @@ data class SourceDataClass(
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val iconUrl: String,
|
||||
|
||||
/** The Source provides a latest listing */
|
||||
val supportsLatest: Boolean,
|
||||
|
||||
/** The Source implements [ConfigurableSource] */
|
||||
val isConfigurable: Boolean,
|
||||
|
||||
/** The Source class has a @Nsfw annotation */
|
||||
val isNsfw: Boolean,
|
||||
|
||||
/** A nicer version of [name] */
|
||||
val displayName: String
|
||||
val displayName: String,
|
||||
)
|
||||
|
||||
@@ -20,12 +20,13 @@ object CategoryTable : IntIdTable() {
|
||||
val includeInUpdate = integer("include_in_update").default(IncludeInUpdate.UNSET.value)
|
||||
}
|
||||
|
||||
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
||||
categoryEntry[id].value,
|
||||
categoryEntry[order],
|
||||
categoryEntry[name],
|
||||
categoryEntry[isDefault],
|
||||
Category.getCategorySize(categoryEntry[id].value),
|
||||
IncludeInUpdate.fromValue(categoryEntry[includeInUpdate]),
|
||||
Category.getCategoryMetaMap(categoryEntry[id].value)
|
||||
)
|
||||
fun CategoryTable.toDataClass(categoryEntry: ResultRow) =
|
||||
CategoryDataClass(
|
||||
categoryEntry[id].value,
|
||||
categoryEntry[order],
|
||||
categoryEntry[name],
|
||||
categoryEntry[isDefault],
|
||||
Category.getCategorySize(categoryEntry[id].value),
|
||||
IncludeInUpdate.fromValue(categoryEntry[includeInUpdate]),
|
||||
Category.getCategoryMetaMap(categoryEntry[id].value),
|
||||
)
|
||||
|
||||
@@ -60,5 +60,5 @@ fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
|
||||
downloaded = chapterEntry[isDownloaded],
|
||||
pageCount = chapterEntry[pageCount],
|
||||
chapterCount = transaction { ChapterTable.select { manga eq chapterEntry[manga].value }.count().toInt() },
|
||||
meta = getChapterMetaMap(chapterEntry[id])
|
||||
meta = getChapterMetaMap(chapterEntry[id]),
|
||||
)
|
||||
|
||||
@@ -13,8 +13,12 @@ object ExtensionTable : IntIdTable() {
|
||||
val apkName = varchar("apk_name", 1024)
|
||||
|
||||
// default is the local source icon from tachiyomi
|
||||
val iconUrl = varchar("icon_url", 2048)
|
||||
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
||||
@Suppress("ktlint:standard:max-line-length")
|
||||
val iconUrl =
|
||||
varchar("icon_url", 2048)
|
||||
.default(
|
||||
"https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp",
|
||||
)
|
||||
|
||||
val name = varchar("name", 128)
|
||||
val pkgName = varchar("pkg_name", 128)
|
||||
|
||||
@@ -50,14 +50,11 @@ fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||
MangaDataClass(
|
||||
id = mangaEntry[this.id].value,
|
||||
sourceId = mangaEntry[sourceReference].toString(),
|
||||
|
||||
url = mangaEntry[url],
|
||||
title = mangaEntry[title],
|
||||
thumbnailUrl = proxyThumbnailUrl(mangaEntry[this.id].value),
|
||||
thumbnailUrlLastFetched = mangaEntry[thumbnailUrlLastFetched],
|
||||
|
||||
initialized = mangaEntry[initialized],
|
||||
|
||||
artist = mangaEntry[artist],
|
||||
author = mangaEntry[author],
|
||||
description = mangaEntry[description],
|
||||
@@ -69,7 +66,7 @@ fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||
realUrl = mangaEntry[realUrl],
|
||||
lastFetchedAt = mangaEntry[lastFetchedAt],
|
||||
chaptersLastFetchedAt = mangaEntry[chaptersLastFetchedAt],
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[updateStrategy])
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[updateStrategy]),
|
||||
)
|
||||
|
||||
enum class MangaStatus(val value: Int) {
|
||||
@@ -79,7 +76,8 @@ enum class MangaStatus(val value: Int) {
|
||||
LICENSED(3),
|
||||
PUBLISHING_FINISHED(4),
|
||||
CANCELLED(5),
|
||||
ON_HIATUS(6);
|
||||
ON_HIATUS(6),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun valueOf(value: Int): MangaStatus = values().find { it.value == value } ?: UNKNOWN
|
||||
|
||||
Reference in New Issue
Block a user