mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-03 10:54:38 -05:00
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:
@@ -79,6 +79,10 @@ object MangaAPI {
|
|||||||
get("{mangaId}/chapter/{chapterIndex}/page/{index}", MangaController.pageRetrieve)
|
get("{mangaId}/chapter/{chapterIndex}/page/{index}", MangaController.pageRetrieve)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path("chapters") {
|
||||||
|
post("batch", MangaController.anyChapterBatch)
|
||||||
|
}
|
||||||
|
|
||||||
path("category") {
|
path("category") {
|
||||||
get("", CategoryController.categoryList)
|
get("", CategoryController.categoryList)
|
||||||
post("", CategoryController.categoryCreate)
|
post("", CategoryController.categoryCreate)
|
||||||
|
|||||||
@@ -240,19 +240,43 @@ object MangaController {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/** batch edit chapters */
|
/** batch edit chapters of single manga */
|
||||||
val chapterBatch = handler(
|
val chapterBatch = handler(
|
||||||
pathParam<Int>("mangaId"),
|
pathParam<Int>("mangaId"),
|
||||||
documentWith = {
|
documentWith = {
|
||||||
withOperation {
|
withOperation {
|
||||||
summary("Chapters update multiple")
|
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>()
|
body<Chapter.ChapterBatchEditInput>()
|
||||||
},
|
},
|
||||||
behaviorOf = { ctx, mangaId ->
|
behaviorOf = { ctx ->
|
||||||
val input = json.decodeFromString<Chapter.ChapterBatchEditInput>(ctx.body())
|
val input = json.decodeFromString<Chapter.ChapterBatchEditInput>(ctx.body())
|
||||||
Chapter.modifyChapters(input, mangaId)
|
Chapter.modifyChapters(
|
||||||
|
Chapter.MangaChapterBatchEditInput(
|
||||||
|
input.chapterIds,
|
||||||
|
null,
|
||||||
|
input.change
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
withResults = {
|
withResults = {
|
||||||
httpCode(HttpCode.OK)
|
httpCode(HttpCode.OK)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.jetbrains.exposed.dao.id.EntityID
|
|||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.SortOrder.ASC
|
import org.jetbrains.exposed.sql.SortOrder.ASC
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import suwayomi.tachidesk.manga.impl.Manga.getManga
|
import suwayomi.tachidesk.manga.impl.Manga.getManga
|
||||||
import suwayomi.tachidesk.manga.impl.util.getChapterDir
|
import suwayomi.tachidesk.manga.impl.util.getChapterDir
|
||||||
@@ -198,29 +199,55 @@ object Chapter {
|
|||||||
data class ChapterChange(
|
data class ChapterChange(
|
||||||
val isRead: Boolean? = null,
|
val isRead: Boolean? = null,
|
||||||
val isBookmarked: 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
|
@Serializable
|
||||||
data class ChapterBatchEditInput(
|
data class MangaChapterBatchEditInput(
|
||||||
val chapterIds: List<Int>? = null,
|
val chapterIds: List<Int>? = null,
|
||||||
val chapterIndexes: List<Int>? = null,
|
val chapterIndexes: List<Int>? = null,
|
||||||
val change: ChapterChange?
|
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
|
// Make sure change is defined
|
||||||
if (input.change == null) return
|
if (input.change == null) return
|
||||||
val (isRead, isBookmarked, lastPageRead) = input.change
|
val (isRead, isBookmarked, lastPageRead, delete) = input.change
|
||||||
if (isRead == null && isBookmarked == null) return
|
|
||||||
|
// 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
|
// Make sure some filter is defined
|
||||||
val condition = when {
|
val condition = when {
|
||||||
input.chapterIds != null ->
|
mangaId != null ->
|
||||||
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.id inList input.chapterIds) }
|
// mangaId is not null, scope query under manga
|
||||||
input.chapterIndexes != null ->
|
when {
|
||||||
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder inList input.chapterIndexes) }
|
input.chapterIds != null ->
|
||||||
else -> 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
|
} ?: return
|
||||||
|
|
||||||
transaction {
|
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> {
|
fun getRecentChapters(pageNum: Int): PaginatedList<MangaChapterDataClass> {
|
||||||
return paginatedFrom(pageNum) {
|
return paginatedFrom(pageNum) {
|
||||||
transaction {
|
transaction {
|
||||||
|
|||||||
Reference in New Issue
Block a user