Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for configuring which categories are downloaded automatically #832

Merged
merged 2 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.Category.DEFAULT_CATEGORY_ID
import suwayomi.tachidesk.manga.impl.util.lang.isEmpty
import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty
import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryMetaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable
Expand Down Expand Up @@ -85,7 +85,8 @@ class CategoryMutation {
data class UpdateCategoryPatch(
val name: String? = null,
val default: Boolean? = null,
val includeInUpdate: IncludeInUpdate? = null,
val includeInUpdate: IncludeOrExclude? = null,
val includeInDownload: IncludeOrExclude? = null,
)

data class UpdateCategoryPayload(
Expand Down Expand Up @@ -136,6 +137,13 @@ class CategoryMutation {
}
}
}
if (patch.includeInDownload != null) {
CategoryTable.update({ CategoryTable.id inList ids }) { update ->
patch.includeInDownload.also {
update[includeInDownload] = it.value
}
}
}
}
}

Expand Down Expand Up @@ -229,7 +237,8 @@ class CategoryMutation {
val name: String,
val order: Int? = null,
val default: Boolean? = null,
val includeInUpdate: IncludeInUpdate? = null,
val includeInUpdate: IncludeOrExclude? = null,
val includeInDownload: IncludeOrExclude? = null,
)

data class CreateCategoryPayload(
Expand All @@ -238,7 +247,7 @@ class CategoryMutation {
)

fun createCategory(input: CreateCategoryInput): CreateCategoryPayload {
val (clientMutationId, name, order, default, includeInUpdate) = input
val (clientMutationId, name, order, default, includeInUpdate, includeInDownload) = input
transaction {
require(CategoryTable.select { CategoryTable.name eq input.name }.isEmpty()) {
"'name' must be unique"
Expand Down Expand Up @@ -271,6 +280,9 @@ class CategoryMutation {
if (includeInUpdate != null) {
it[CategoryTable.includeInUpdate] = includeInUpdate.value
}
if (includeInDownload != null) {
it[CategoryTable.includeInDownload] = includeInDownload.value
}
}

Category.normalizeCategories()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import suwayomi.tachidesk.graphql.server.primitives.Edge
import suwayomi.tachidesk.graphql.server.primitives.Node
import suwayomi.tachidesk.graphql.server.primitives.NodeList
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
import suwayomi.tachidesk.manga.model.table.CategoryTable
import java.util.concurrent.CompletableFuture

Expand All @@ -24,14 +24,16 @@ class CategoryType(
val order: Int,
val name: String,
val default: Boolean,
val includeInUpdate: IncludeInUpdate,
val includeInUpdate: IncludeOrExclude,
val includeInDownload: IncludeOrExclude,
) : Node {
constructor(row: ResultRow) : this(
row[CategoryTable.id].value,
row[CategoryTable.order],
row[CategoryTable.name],
row[CategoryTable.isDefault],
IncludeInUpdate.fromValue(row[CategoryTable.includeInUpdate]),
IncludeOrExclude.fromValue(row[CategoryTable.includeInUpdate]),
IncludeOrExclude.fromValue(row[CategoryTable.includeInDownload]),
)

fun mangas(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,15 @@ object CategoryController {
formParam<String?>("name"),
formParam<Boolean?>("default"),
formParam<Int?>("includeInUpdate"),
formParam<Int?>("includeInDownload"),
documentWith = {
withOperation {
summary("Category modify")
description("Modify a category")
}
},
behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate ->
Category.updateCategory(categoryId, name, isDefault, includeInUpdate)
behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate, includeInDownload ->
Category.updateCategory(categoryId, name, isDefault, includeInUpdate, includeInDownload)
ctx.status(200)
},
withResults = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ object Category {
name: String?,
isDefault: Boolean?,
includeInUpdate: Int?,
includeInDownload: Int?,
) {
transaction {
CategoryTable.update({ CategoryTable.id eq categoryId }) {
Expand All @@ -66,6 +67,7 @@ object Category {
}
if (categoryId != DEFAULT_CATEGORY_ID && isDefault != null) it[CategoryTable.isDefault] = isDefault
if (includeInUpdate != null) it[CategoryTable.includeInUpdate] = includeInUpdate
if (includeInDownload != null) it[CategoryTable.includeInDownload] = includeInDownload
}
}
}
Expand Down
57 changes: 52 additions & 5 deletions server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput
import suwayomi.tachidesk.manga.impl.track.Track
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
import suwayomi.tachidesk.manga.model.dataclass.paginatedFrom
Expand All @@ -49,6 +50,7 @@ import suwayomi.tachidesk.server.serverConfig
import java.time.Instant
import java.util.TreeSet
import java.util.concurrent.TimeUnit
import kotlin.collections.listOf
import kotlin.math.max

object Chapter {
Expand Down Expand Up @@ -309,20 +311,65 @@ object Chapter {
")",
)

if (!serverConfig.autoDownloadNewChapters.value) {
log.debug { "automatic download is not configured" }
return
}

// Only download if there are new chapters, or if this is the first fetch
val newNumberOfChapters = updatedChapterList.size
val numberOfNewChapters = newNumberOfChapters - prevNumberOfChapters

val areNewChaptersAvailable = numberOfNewChapters > 0
val wasInitialFetch = prevNumberOfChapters == 0

// make sure to ignore initial fetch
val isDownloadPossible =
serverConfig.autoDownloadNewChapters.value && areNewChaptersAvailable && !wasInitialFetch
if (!isDownloadPossible) {
log.debug { "download is not allowed/possible" }
if (!areNewChaptersAvailable) {
log.debug { "no new chapters available" }
return
}

chancez marked this conversation as resolved.
Show resolved Hide resolved
if (wasInitialFetch) {
log.debug { "skipping download on initial fetch" }
return
}

// Verify the manga is configured to be downloaded based on it's categories.
var mangaCategories = CategoryManga.getMangaCategories(mangaId).toSet()
// if the manga has no categories, then it's implicitly in the default category
if (mangaCategories.isEmpty()) {
val defaultCategory = Category.getCategoryById(Category.DEFAULT_CATEGORY_ID)
if (defaultCategory != null) {
mangaCategories = setOf(defaultCategory)
} else {
log.warn { "missing default category" }
}
}

if (mangaCategories.isNotEmpty()) {
chancez marked this conversation as resolved.
Show resolved Hide resolved
var downloadCategoriesMap = Category.getCategoryList().groupBy { it.includeInDownload }
val unsetCategories = downloadCategoriesMap[IncludeOrExclude.UNSET].orEmpty()
// We only download if it's in the include list, and not in the exclude list.
// Use the unset categories as the included categories if the included categories is
// empty
val includedCategories = downloadCategoriesMap[IncludeOrExclude.INCLUDE].orEmpty().ifEmpty { unsetCategories }
val excludedCategories = downloadCategoriesMap[IncludeOrExclude.EXCLUDE].orEmpty()
// Only download manga that aren't in any excluded categories
val mangaExcludeCategories = mangaCategories.intersect(excludedCategories)
if (mangaExcludeCategories.isNotEmpty()) {
log.debug { "download excluded by categories: '${mangaExcludeCategories.joinToString("', '") { it.name }}'" }
return
}
val mangaDownloadCategories = mangaCategories.intersect(includedCategories)
if (mangaDownloadCategories.isNotEmpty()) {
chancez marked this conversation as resolved.
Show resolved Hide resolved
log.debug { "download inluded by categories: '${mangaDownloadCategories.joinToString("', '") { it.name }}'" }
} else {
log.debug { "skipping download due to download categories configuration" }
return
}
} else {
log.debug { "no categories configured, skipping check for category download include/excludes" }
}

val newChapters = updatedChapterList.subList(0, numberOfNewChapters)

// make sure to only consider the latest chapters. e.g. old unread chapters should be ignored
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.impl.Manga
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.server.serverConfig
Expand Down Expand Up @@ -222,9 +222,9 @@ class Updater : IUpdater {
}

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 excludedCategories = includeInUpdateStatusToCategoryMap[IncludeOrExclude.EXCLUDE].orEmpty()
val includedCategories = includeInUpdateStatusToCategoryMap[IncludeOrExclude.INCLUDE].orEmpty()
val unsetCategories = includeInUpdateStatusToCategoryMap[IncludeOrExclude.UNSET].orEmpty()
val categoriesToUpdate =
if (forceAll) {
categories
Expand Down Expand Up @@ -277,6 +277,8 @@ class Updater : IUpdater {
// In case no manga gets updated and no update job was running before, the client would never receive an info about its update request
updateStatus(emptyList(), mangasToUpdate.isNotEmpty(), updateStatusCategories, skippedMangas)

logger.debug { "mangasToUpdate $mangasToUpdate" }

if (mangasToUpdate.isEmpty()) {
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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(
enum class IncludeOrExclude(
@JsonValue val value: Int,
) {
EXCLUDE(0),
Expand All @@ -18,7 +18,7 @@ enum class IncludeInUpdate(
;

companion object {
fun fromValue(value: Int) = IncludeInUpdate.values().find { it.value == value } ?: UNSET
fun fromValue(value: Int) = IncludeOrExclude.values().find { it.value == value } ?: UNSET
}
}

Expand All @@ -28,6 +28,7 @@ data class CategoryDataClass(
val name: String,
val default: Boolean,
val size: Int,
val includeInUpdate: IncludeInUpdate,
val includeInUpdate: IncludeOrExclude,
val includeInDownload: IncludeOrExclude,
val meta: Map<String, String> = emptyMap(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ 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
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude

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)
val includeInUpdate = integer("include_in_update").default(IncludeOrExclude.UNSET.value)
val includeInDownload = integer("include_in_download").default(IncludeOrExclude.UNSET.value)
}

fun CategoryTable.toDataClass(categoryEntry: ResultRow) =
Expand All @@ -27,6 +28,7 @@ fun CategoryTable.toDataClass(categoryEntry: ResultRow) =
categoryEntry[name],
categoryEntry[isDefault],
Category.getCategorySize(categoryEntry[id].value),
IncludeInUpdate.fromValue(categoryEntry[includeInUpdate]),
IncludeOrExclude.fromValue(categoryEntry[includeInUpdate]),
IncludeOrExclude.fromValue(categoryEntry[includeInDownload]),
Category.getCategoryMetaMap(categoryEntry[id].value),
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ package suwayomi.tachidesk.server.database.migration
* 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
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude

@Suppress("ClassName", "unused")
class M0026_CategoryIncludeInUpdate : AddColumnMigration(
"Category",
"include_in_update",
"INT",
IncludeInUpdate.UNSET.value.toString(),
IncludeOrExclude.UNSET.value.toString(),
)
Original file line number Diff line number Diff line change
@@ -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.IncludeOrExclude

@Suppress("ClassName", "unused")
class M0034_CategoryIncludeInDownload : AddColumnMigration(
"Category",
"include_in_download",
"INT",
IncludeOrExclude.UNSET.value.toString(),
)