diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1354beff..28923d57f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,9 +2,10 @@ ## Where should I start? Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap. -**Note 1:** Notify the developers on [Suwayomi discord](https://discord.gg/DDZdqZWaHA) (#tachidesk-server and #tachidesk-webui channels) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature. - -**Note 2:** Your pull request will be squashed into a single commit. +### Important notes +- Notify the developers on [Suwayomi discord](https://discord.gg/DDZdqZWaHA) (#tachidesk-server and #tachidesk-webui channels) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature. +- Your pull request will be squashed into a single commit. +- We hate big pull requests, make them as small as possible, change one meaningful thing. Spam pull requests, we don't mind. ### Project goals and vision - Porting Tachiyomi and covering it's features diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/CategoryController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/CategoryController.kt index 96b56b5ac..c4ec3ef3f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/CategoryController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/CategoryController.kt @@ -61,14 +61,15 @@ object CategoryController { pathParam("categoryId"), formParam("name"), formParam("default"), + formParam("includeInUpdate"), documentWith = { withOperation { summary("Category modify") description("Modify a category") } }, - behaviorOf = { ctx, categoryId, name, isDefault -> - Category.updateCategory(categoryId, name, isDefault) + behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate -> + Category.updateCategory(categoryId, name, isDefault, includeInUpdate) ctx.status(200) }, withResults = { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt index a86da540c..06f055a68 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt @@ -13,10 +13,7 @@ import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.update.IUpdater import suwayomi.tachidesk.manga.impl.update.UpdateStatus import suwayomi.tachidesk.manga.impl.update.UpdaterSocket -import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass -import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass -import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass -import suwayomi.tachidesk.manga.model.dataclass.PaginatedList +import suwayomi.tachidesk.manga.model.dataclass.* import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.util.formParam import suwayomi.tachidesk.server.util.handler @@ -93,14 +90,33 @@ object UpdateController { if (clear) { updater.reset() } - categories + + val includeInUpdateStatusToCategoryMap = categories.groupBy { it.includeInUpdate } + val excludedCategories = includeInUpdateStatusToCategoryMap[IncludeInUpdate.EXCLUDE].orEmpty() + val includedCategories = includeInUpdateStatusToCategoryMap[IncludeInUpdate.INCLUDE].orEmpty() + val unsetCategories = includeInUpdateStatusToCategoryMap[IncludeInUpdate.UNSET].orEmpty() + val categoriesToUpdate = includedCategories.ifEmpty { unsetCategories } + + logger.debug { "Updating categories: '${categoriesToUpdate.joinToString("', '") { it.name }}'" } + + val categoriesToUpdateMangas = categoriesToUpdate .flatMap { CategoryManga.getCategoryMangaList(it.id) } .distinctBy { it.id } - .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title)) + val mangasToCategoriesMap = CategoryManga.getMangasCategories(categoriesToUpdateMangas.map { it.id }) + val mangasToUpdate = categoriesToUpdateMangas .filter { it.updateStrategy == UpdateStrategy.ALWAYS_UPDATE } - .forEach { manga -> - updater.addMangaToQueue(manga) - } + .filter { !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } } + + // In case no manga gets updated and no update job was running before, the client would never receive an info about its update request + if (mangasToUpdate.isEmpty()) { + UpdaterSocket.notifyAllClients(UpdateStatus()) + return + } + + updater.addMangasToQueue( + mangasToUpdate + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title)), + ) } fun categoryUpdateWS(ws: WsConfig) { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Category.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Category.kt index ad684c04e..6e6ac6491 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Category.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Category.kt @@ -18,8 +18,8 @@ import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.manga.impl.CategoryManga.removeMangaFromCategory -import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass +import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryMetaTable import suwayomi.tachidesk.manga.model.table.CategoryTable @@ -50,11 +50,12 @@ object Category { } } - fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?) { + fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?, includeInUpdate: Int?) { transaction { CategoryTable.update({ CategoryTable.id eq categoryId }) { if (name != null && !name.equals(DEFAULT_CATEGORY_NAME, ignoreCase = true)) it[CategoryTable.name] = name if (isDefault != null) it[CategoryTable.isDefault] = isDefault + if (includeInUpdate != null) it[CategoryTable.includeInUpdate] = includeInUpdate } } } @@ -98,12 +99,14 @@ object Category { const val DEFAULT_CATEGORY_ID = 0 const val DEFAULT_CATEGORY_NAME = "Default" - private fun addDefaultIfNecessary(categories: List): List = - if (MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.isNotEmpty()) { - listOf(CategoryDataClass(DEFAULT_CATEGORY_ID, 0, DEFAULT_CATEGORY_NAME, true)) + categories + private fun addDefaultIfNecessary(categories: List): List { + val defaultCategorySize = MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.count().toInt() + return if (defaultCategorySize > 0) { + listOf(CategoryDataClass(DEFAULT_CATEGORY_ID, 0, DEFAULT_CATEGORY_NAME, true, defaultCategorySize, IncludeInUpdate.UNSET)) + categories } else { categories } + } fun getCategoryList(): List { return transaction { @@ -123,6 +126,14 @@ object Category { } } + fun getCategorySize(categoryId: Int): Int { + return transaction { + CategoryMangaTable.select { + CategoryMangaTable.category eq categoryId + }.count().toInt() + } + } + fun getCategoryMetaMap(categoryId: Int): Map { return transaction { CategoryMetaTable.select { CategoryMetaTable.ref eq categoryId } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/CategoryManga.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/CategoryManga.kt index 8316dcc75..b571c53d1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/CategoryManga.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/CategoryManga.kt @@ -116,4 +116,20 @@ object CategoryManga { } } } + + fun getMangasCategories(mangaIDs: List): Map> { + return buildMap { + transaction { + CategoryMangaTable.innerJoin(CategoryTable) + .select { CategoryMangaTable.manga inList mangaIDs } + .groupBy { it[CategoryMangaTable.manga] } + .forEach { + val mangaId = it.key.value + val categories = it.value + + set(mangaId, categories.map { category -> CategoryTable.toDataClass(category) }) + } + } + } + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/IUpdater.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/IUpdater.kt index 33c20bf8d..a69225727 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/IUpdater.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/IUpdater.kt @@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.StateFlow import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass interface IUpdater { - fun addMangaToQueue(manga: MangaDataClass) + fun addMangasToQueue(mangas: List) val status: StateFlow fun reset() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt index 7316038a4..446011d73 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt @@ -74,13 +74,17 @@ class Updater : IUpdater { return tracker.values.toList() } - override fun addMangaToQueue(manga: MangaDataClass) { + override fun addMangasToQueue(mangas: List) { + mangas.forEach { tracker[it.id] = UpdateJob(it) } + _status.update { UpdateStatus(tracker.values.toList(), mangas.isNotEmpty()) } + mangas.forEach { addMangaToQueue(it) } + } + + private fun addMangaToQueue(manga: MangaDataClass) { val updateChannel = getOrCreateUpdateChannelFor(manga.sourceId) scope.launch { updateChannel.send(UpdateJob(manga)) } - tracker[manga.id] = UpdateJob(manga) - _status.update { UpdateStatus(tracker.values.toList(), true) } } override fun reset() { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/dataclass/CategoryDataClass.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/dataclass/CategoryDataClass.kt index 3e7fd1fb6..641aeea19 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/dataclass/CategoryDataClass.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/dataclass/CategoryDataClass.kt @@ -1,5 +1,7 @@ package suwayomi.tachidesk.manga.model.dataclass +import com.fasterxml.jackson.annotation.JsonValue + /* * Copyright (C) Contributors to the Suwayomi project * @@ -7,10 +9,20 @@ package suwayomi.tachidesk.manga.model.dataclass * 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); + + companion object { + fun fromValue(value: Int) = IncludeInUpdate.values().find { it.value == value } ?: UNSET + } +} + data class CategoryDataClass( val id: Int, val order: Int, val name: String, val default: Boolean, + val size: Int, + val includeInUpdate: IncludeInUpdate, val meta: Map = emptyMap() ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryTable.kt index 8d8a08ab5..47e86a39a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryTable.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryTable.kt @@ -11,11 +11,13 @@ import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.ResultRow import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass +import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate object CategoryTable : IntIdTable() { val name = varchar("name", 64) val order = integer("order").default(0) val isDefault = bool("is_default").default(false) + val includeInUpdate = integer("include_in_update").default(IncludeInUpdate.UNSET.value) } fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass( @@ -23,5 +25,7 @@ fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass( categoryEntry[order], categoryEntry[name], categoryEntry[isDefault], + Category.getCategorySize(categoryEntry[id].value), + IncludeInUpdate.fromValue(categoryEntry[includeInUpdate]), Category.getCategoryMetaMap(categoryEntry[id].value) ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0026_CategoryIncludeInUpdate.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0026_CategoryIncludeInUpdate.kt new file mode 100644 index 000000000..15f92ef21 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0026_CategoryIncludeInUpdate.kt @@ -0,0 +1,19 @@ +package suwayomi.tachidesk.server.database.migration + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import de.neonew.exposed.migrations.helpers.AddColumnMigration +import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate + +@Suppress("ClassName", "unused") +class M0026_CategoryIncludeInUpdate : AddColumnMigration( + "Category", + "include_in_update", + "INT", + IncludeInUpdate.UNSET.value.toString() +) diff --git a/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/update/TestUpdater.kt b/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/update/TestUpdater.kt index b4c7f8a17..cd1a66dba 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/update/TestUpdater.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/update/TestUpdater.kt @@ -13,8 +13,8 @@ class TestUpdater : IUpdater { private val _status = MutableStateFlow(UpdateStatus()) override val status: StateFlow = _status.asStateFlow() - override fun addMangaToQueue(manga: MangaDataClass) { - updateQueue.add(UpdateJob(manga)) + override fun addMangasToQueue(mangas: List) { + mangas.forEach { updateQueue.add(UpdateJob(it)) } isRunning = true updateStatus() }