Batch editing and deleting any chapter (#449)

* Add new endpoint for batch editing any chapter

* Add option to batch editing chapters to delete chapter (remove downloaded content)

* Rename the endpoint to match single manga batch endpoint

* Do not return early, in case there are other changes

* PR changes
This commit is contained in:
Valter Martinek
2022-11-15 11:49:20 +01:00
committed by GitHub
parent c0948209be
commit 8fbc24c751
3 changed files with 105 additions and 14 deletions

View File

@@ -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)

View File

@@ -240,19 +240,43 @@ object MangaController {
}
)
/** batch edit chapters */
/** batch edit chapters of single manga */
val chapterBatch = handler(
pathParam<Int>("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<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, mangaId ->
behaviorOf = { ctx ->
val input = json.decodeFromString<Chapter.ChapterBatchEditInput>(ctx.body())
Chapter.modifyChapters(input, mangaId)
Chapter.modifyChapters(
Chapter.MangaChapterBatchEditInput(
input.chapterIds,
null,
input.change
)
)
},
withResults = {
httpCode(HttpCode.OK)

View File

@@ -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<Int>? = null,
val chapterIndexes: List<Int>? = null,
val change: ChapterChange?
)
fun modifyChapters(input: ChapterBatchEditInput, mangaId: Int) {
@Serializable
data class ChapterBatchEditInput(
val chapterIds: List<Int>? = 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<MangaChapterDataClass> {
return paginatedFrom(pageNum) {
transaction {