diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt index db9614a3..ca445c0a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt @@ -79,6 +79,10 @@ object MangaAPI { get("{mangaId}/chapter/{chapterIndex}/page/{index}", MangaController.pageRetrieve) } + path("chapters") { + post("batch", MangaController.anyChapterBatch) + } + path("category") { get("", CategoryController.categoryList) post("", CategoryController.categoryCreate) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt index 8769b362..cdc0e25f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt @@ -240,19 +240,43 @@ object MangaController { } ) - /** batch edit chapters */ + /** batch edit chapters of single manga */ val chapterBatch = handler( pathParam("mangaId"), documentWith = { withOperation { summary("Chapters update multiple") - description("Update multiple chapters. For batch marking as read, or bookmarking") + description("Update multiple chapters of single manga. For batch marking as read, or bookmarking") + } + body() + }, + behaviorOf = { ctx, mangaId -> + val input = json.decodeFromString(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() }, - behaviorOf = { ctx, mangaId -> + behaviorOf = { ctx -> val input = json.decodeFromString(ctx.body()) - Chapter.modifyChapters(input, mangaId) + Chapter.modifyChapters( + Chapter.MangaChapterBatchEditInput( + input.chapterIds, + null, + input.change + ) + ) }, withResults = { httpCode(HttpCode.OK) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt index 10dd1d5d..fa873929 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt @@ -15,6 +15,7 @@ import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SortOrder.ASC import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.manga.impl.Manga.getManga import suwayomi.tachidesk.manga.impl.util.getChapterDir @@ -198,29 +199,55 @@ object Chapter { data class ChapterChange( val isRead: Boolean? = null, val isBookmarked: Boolean? = null, - val lastPageRead: Int? = null // this probably won't be very useful, but for completion's sake + val lastPageRead: Int? = null, + val delete: Boolean? = null ) @Serializable - data class ChapterBatchEditInput( + data class MangaChapterBatchEditInput( val chapterIds: List? = null, val chapterIndexes: List? = null, val change: ChapterChange? ) - fun modifyChapters(input: ChapterBatchEditInput, mangaId: Int) { + @Serializable + data class ChapterBatchEditInput( + val chapterIds: List? = null, + val change: ChapterChange? + ) + + fun modifyChapters(input: MangaChapterBatchEditInput, mangaId: Int? = null) { // Make sure change is defined if (input.change == null) return - val (isRead, isBookmarked, lastPageRead) = input.change - if (isRead == null && isBookmarked == null) return + val (isRead, isBookmarked, lastPageRead, delete) = input.change + + // Handle deleting separately + if (delete == true) { + deleteChapters(input, mangaId) + } + + // return early if there are no other changes + if (listOfNotNull(isRead, isBookmarked, lastPageRead).isEmpty()) return // Make sure some filter is defined val condition = 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 + 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 + } + } } ?: return transaction { @@ -295,6 +322,42 @@ object Chapter { } } + private fun deleteChapters(input: MangaChapterBatchEditInput, mangaId: Int? = null) { + if (input.chapterIds != null) { + val chapterIds = input.chapterIds + + transaction { + ChapterTable.slice(ChapterTable.manga, ChapterTable.id) + .select { ChapterTable.id inList chapterIds } + .forEach { row -> + val chapterMangaId = row[ChapterTable.manga].value + val chapterId = row[ChapterTable.id].value + val chapterDir = getChapterDir(chapterMangaId, chapterId) + File(chapterDir).deleteRecursively() + } + + ChapterTable.update({ ChapterTable.id inList chapterIds }) { + it[isDownloaded] = false + } + } + } 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 + val chapterDir = getChapterDir(mangaId, chapterId) + File(chapterDir).deleteRecursively() + chapterId + } + + ChapterTable.update({ ChapterTable.id inList chapterIds }) { + it[isDownloaded] = false + } + } + } + } + fun getRecentChapters(pageNum: Int): PaginatedList { return paginatedFrom(pageNum) { transaction {