mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 14:52:05 +01:00
Feat: Adds OPDS Chapter Filtering/Ordering (#1392)
* Adds server level configs for OPDS * PR comments * Refactor server-reference.conf (itemsPerPage range) * Coerce itemsPerPage (10, 5000) and default invalid sort orders to DESC * Coerce itemsPerPage (10, 5000) and default invalid sort orders to DESC * Change opdsChapterSortOrder type to Enum(SortOrder) * Fix serialization of SortOrderEnum & Add `opdsShowOnlyDownloadedChapters` config
This commit is contained in:
@@ -112,7 +112,8 @@ open class ConfigManager {
|
||||
value: Any,
|
||||
) {
|
||||
mutex.withLock {
|
||||
val configValue = ConfigValueFactory.fromAnyRef(value)
|
||||
val actualValue = if (value is Enum<*>) value.name else value
|
||||
val configValue = ConfigValueFactory.fromAnyRef(actualValue)
|
||||
|
||||
updateUserConfigFile(path, configValue)
|
||||
internalConfig = internalConfig.withValue(path, configValue)
|
||||
|
||||
@@ -94,6 +94,9 @@ class SettingsMutation {
|
||||
|
||||
// local source
|
||||
validateFilePath(settings.localSourcePath, "localSourcePath")
|
||||
|
||||
// opds
|
||||
validateValue(settings.opdsItemsPerPage, "opdsItemsPerPage") { it in 10..5000 }
|
||||
}
|
||||
|
||||
private fun <SettingType : Any> updateSetting(
|
||||
@@ -177,6 +180,14 @@ class SettingsMutation {
|
||||
updateSetting(settings.flareSolverrSessionName, serverConfig.flareSolverrSessionName)
|
||||
updateSetting(settings.flareSolverrSessionTtl, serverConfig.flareSolverrSessionTtl)
|
||||
updateSetting(settings.flareSolverrAsResponseFallback, serverConfig.flareSolverrAsResponseFallback)
|
||||
|
||||
// opds
|
||||
updateSetting(settings.opdsItemsPerPage, serverConfig.opdsItemsPerPage)
|
||||
updateSetting(settings.opdsEnablePageReadProgress, serverConfig.opdsEnablePageReadProgress)
|
||||
updateSetting(settings.opdsMarkAsReadOnDownload, serverConfig.opdsMarkAsReadOnDownload)
|
||||
updateSetting(settings.opdsShowOnlyUnreadChapters, serverConfig.opdsShowOnlyUnreadChapters)
|
||||
updateSetting(settings.opdsShowOnlyDownloadedChapters, serverConfig.opdsShowOnlyDownloadedChapters)
|
||||
updateSetting(settings.opdsChapterSortOrder, serverConfig.opdsChapterSortOrder)
|
||||
}
|
||||
|
||||
fun setSettings(input: SetSettingsInput): SetSettingsPayload {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
package suwayomi.tachidesk.graphql.types
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Node
|
||||
import suwayomi.tachidesk.server.ServerConfig
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
@@ -92,6 +93,14 @@ interface Settings : Node {
|
||||
val flareSolverrSessionName: String?
|
||||
val flareSolverrSessionTtl: Int?
|
||||
val flareSolverrAsResponseFallback: Boolean?
|
||||
|
||||
// opds
|
||||
val opdsItemsPerPage: Int?
|
||||
val opdsEnablePageReadProgress: Boolean?
|
||||
val opdsMarkAsReadOnDownload: Boolean?
|
||||
val opdsShowOnlyUnreadChapters: Boolean?
|
||||
val opdsShowOnlyDownloadedChapters: Boolean?
|
||||
val opdsChapterSortOrder: SortOrder?
|
||||
}
|
||||
|
||||
data class PartialSettingsType(
|
||||
@@ -159,6 +168,13 @@ data class PartialSettingsType(
|
||||
override val flareSolverrSessionName: String?,
|
||||
override val flareSolverrSessionTtl: Int?,
|
||||
override val flareSolverrAsResponseFallback: Boolean?,
|
||||
// opds
|
||||
override val opdsItemsPerPage: Int?,
|
||||
override val opdsEnablePageReadProgress: Boolean?,
|
||||
override val opdsMarkAsReadOnDownload: Boolean?,
|
||||
override val opdsShowOnlyUnreadChapters: Boolean?,
|
||||
override val opdsShowOnlyDownloadedChapters: Boolean?,
|
||||
override val opdsChapterSortOrder: SortOrder?,
|
||||
) : Settings
|
||||
|
||||
class SettingsType(
|
||||
@@ -226,6 +242,13 @@ class SettingsType(
|
||||
override val flareSolverrSessionName: String,
|
||||
override val flareSolverrSessionTtl: Int,
|
||||
override val flareSolverrAsResponseFallback: Boolean,
|
||||
// opds
|
||||
override val opdsItemsPerPage: Int,
|
||||
override val opdsEnablePageReadProgress: Boolean,
|
||||
override val opdsMarkAsReadOnDownload: Boolean,
|
||||
override val opdsShowOnlyUnreadChapters: Boolean,
|
||||
override val opdsShowOnlyDownloadedChapters: Boolean,
|
||||
override val opdsChapterSortOrder: SortOrder,
|
||||
) : Settings {
|
||||
constructor(config: ServerConfig = serverConfig) : this(
|
||||
config.ip.value,
|
||||
@@ -287,5 +310,12 @@ class SettingsType(
|
||||
config.flareSolverrSessionName.value,
|
||||
config.flareSolverrSessionTtl.value,
|
||||
config.flareSolverrAsResponseFallback.value,
|
||||
// opds
|
||||
config.opdsItemsPerPage.value,
|
||||
config.opdsEnablePageReadProgress.value,
|
||||
config.opdsMarkAsReadOnDownload.value,
|
||||
config.opdsShowOnlyUnreadChapters.value,
|
||||
config.opdsShowOnlyDownloadedChapters.value,
|
||||
config.opdsChapterSortOrder.value,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ package suwayomi.tachidesk.manga
|
||||
|
||||
import io.javalin.apibuilder.ApiBuilder.delete
|
||||
import io.javalin.apibuilder.ApiBuilder.get
|
||||
import io.javalin.apibuilder.ApiBuilder.head
|
||||
import io.javalin.apibuilder.ApiBuilder.patch
|
||||
import io.javalin.apibuilder.ApiBuilder.path
|
||||
import io.javalin.apibuilder.ApiBuilder.post
|
||||
@@ -83,6 +84,7 @@ object MangaAPI {
|
||||
path("chapter") {
|
||||
post("batch", MangaController.anyChapterBatch)
|
||||
get("{chapterId}/download", MangaController.downloadChapter)
|
||||
head("{chapterId}/download", MangaController.downloadChapter)
|
||||
}
|
||||
|
||||
path("category") {
|
||||
|
||||
@@ -7,6 +7,7 @@ package suwayomi.tachidesk.manga.controller
|
||||
* 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 io.javalin.http.HandlerType
|
||||
import io.javalin.http.HttpStatus
|
||||
import kotlinx.serialization.json.Json
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||
@@ -434,22 +435,29 @@ object MangaController {
|
||||
val downloadChapter =
|
||||
handler(
|
||||
pathParam<Int>("chapterId"),
|
||||
queryParam<Boolean?>("markAsRead"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Download chapter as CBZ")
|
||||
description("Get the CBZ file of the specified chapter")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, chapterId ->
|
||||
behaviorOf = { ctx, chapterId, markAsRead ->
|
||||
val shouldMarkAsRead = if (ctx.method() == HandlerType.HEAD) false else markAsRead
|
||||
ctx.future {
|
||||
future { ChapterDownloadHelper.getCbzForDownload(chapterId) }
|
||||
future { ChapterDownloadHelper.getCbzForDownload(chapterId, shouldMarkAsRead) }
|
||||
.thenApply { (inputStream, fileName, fileSize) ->
|
||||
ctx.header("Content-Type", "application/vnd.comicbook+zip")
|
||||
ctx.header("Content-Disposition", "attachment; filename=\"$fileName\"")
|
||||
ctx.header("Content-Length", fileSize.toString())
|
||||
if (ctx.method() == HandlerType.HEAD) {
|
||||
inputStream.close()
|
||||
ctx.status(200)
|
||||
} else {
|
||||
ctx.result(inputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpStatus.OK)
|
||||
|
||||
@@ -57,7 +57,10 @@ object ChapterDownloadHelper {
|
||||
chapterId: Int,
|
||||
): Pair<InputStream, Long> = provider(mangaId, chapterId).getAsArchiveStream()
|
||||
|
||||
fun getCbzForDownload(chapterId: Int): Triple<InputStream, String, Long> {
|
||||
fun getCbzForDownload(
|
||||
chapterId: Int,
|
||||
markAsRead: Boolean?,
|
||||
): Triple<InputStream, String, Long> {
|
||||
val (chapterData, mangaTitle) =
|
||||
transaction {
|
||||
val row =
|
||||
@@ -74,6 +77,17 @@ object ChapterDownloadHelper {
|
||||
|
||||
val cbzFile = provider(chapterData.mangaId, chapterData.id).getAsArchiveStream()
|
||||
|
||||
if (markAsRead == true) {
|
||||
Chapter.modifyChapter(
|
||||
chapterData.mangaId,
|
||||
chapterData.index,
|
||||
isRead = true,
|
||||
isBookmarked = null,
|
||||
markPrevRead = null,
|
||||
lastPageRead = null,
|
||||
)
|
||||
}
|
||||
|
||||
return Triple(cbzFile.first, fileName, cbzFile.second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import nl.adaptivity.xmlutil.serialization.XML
|
||||
import org.jetbrains.exposed.sql.JoinType
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.lowerCase
|
||||
import org.jetbrains.exposed.sql.or
|
||||
@@ -30,6 +31,7 @@ import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
import suwayomi.tachidesk.opds.model.OpdsXmlModels
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.Instant
|
||||
@@ -37,13 +39,11 @@ import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
object Opds {
|
||||
private const val ITEMS_PER_PAGE = 20
|
||||
private val opdsItemsPerPageBounded: Int
|
||||
get() = serverConfig.opdsItemsPerPage.value.coerceIn(10, 5000)
|
||||
|
||||
fun getRootFeed(baseUrl: String): String {
|
||||
val builder =
|
||||
FeedBuilder(baseUrl, 1, "opds", "Suwayomi OPDS Catalog").apply {
|
||||
totalResults = 6
|
||||
entries +=
|
||||
val rootSection =
|
||||
listOf(
|
||||
"mangas" to "All Manga",
|
||||
"sources" to "Sources",
|
||||
@@ -52,7 +52,12 @@ object Opds {
|
||||
"status" to "Status",
|
||||
"languages" to "Languages",
|
||||
"library-updates" to "Library Update History",
|
||||
).map { (id, title) ->
|
||||
)
|
||||
val builder =
|
||||
FeedBuilder(baseUrl, 1, "opds", "Suwayomi OPDS Catalog").apply {
|
||||
totalResults = rootSection.size.toLong()
|
||||
entries +=
|
||||
rootSection.map { (id, title) ->
|
||||
OpdsXmlModels.Entry(
|
||||
id = id,
|
||||
title = title,
|
||||
@@ -84,6 +89,7 @@ object Opds {
|
||||
.select(MangaTable.columns)
|
||||
.where {
|
||||
val conditions = mutableListOf<Op<Boolean>>()
|
||||
conditions += (MangaTable.inLibrary eq true)
|
||||
|
||||
criteria?.query?.takeIf { it.isNotBlank() }?.let { q ->
|
||||
val lowerQ = q.lowercase()
|
||||
@@ -102,14 +108,14 @@ object Opds {
|
||||
conditions += (MangaTable.title.lowerCase() like "%${title.lowercase()}%")
|
||||
}
|
||||
|
||||
if (conditions.isEmpty()) (MangaTable.inLibrary eq true) else conditions.reduce { acc, op -> acc and op }
|
||||
conditions.reduce { acc, op -> acc and op }
|
||||
}.groupBy(MangaTable.id)
|
||||
.orderBy(MangaTable.title to SortOrder.ASC)
|
||||
val totalCount = query.count()
|
||||
val mangas =
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.limit(opdsItemsPerPageBounded)
|
||||
.offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong())
|
||||
.map { MangaTable.toDataClass(it, includeMangaMeta = false) }
|
||||
Pair(mangas, totalCount)
|
||||
}
|
||||
@@ -146,8 +152,8 @@ object Opds {
|
||||
val totalCount = query.count()
|
||||
val sources =
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.limit(opdsItemsPerPageBounded)
|
||||
.offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong())
|
||||
.map {
|
||||
SourceDataClass(
|
||||
id = it[SourceTable.id].value.toString(),
|
||||
@@ -165,7 +171,7 @@ object Opds {
|
||||
|
||||
return FeedBuilder(baseUrl, pageNum, "sources", "Sources")
|
||||
.apply {
|
||||
totalResults = totalCount.toLong()
|
||||
totalResults = totalCount
|
||||
entries +=
|
||||
sourceList.map {
|
||||
OpdsXmlModels.Entry(
|
||||
@@ -191,8 +197,9 @@ object Opds {
|
||||
pageNum: Int,
|
||||
): String {
|
||||
val formattedNow = opdsDateFormatter.format(Instant.now())
|
||||
val categoryList =
|
||||
val (categoryList, total) =
|
||||
transaction {
|
||||
val query =
|
||||
CategoryTable
|
||||
.join(CategoryMangaTable, JoinType.INNER, onColumn = CategoryTable.id, otherColumn = CategoryMangaTable.category)
|
||||
.join(MangaTable, JoinType.INNER, onColumn = CategoryMangaTable.manga, otherColumn = MangaTable.id)
|
||||
@@ -200,21 +207,23 @@ object Opds {
|
||||
.select(CategoryTable.id, CategoryTable.name)
|
||||
.groupBy(CategoryTable.id)
|
||||
.orderBy(CategoryTable.order to SortOrder.ASC)
|
||||
.map { row ->
|
||||
Pair(row[CategoryTable.id].value, row[CategoryTable.name])
|
||||
}
|
||||
}
|
||||
|
||||
val totalCount = categoryList.size
|
||||
val fromIndex = (pageNum - 1) * ITEMS_PER_PAGE
|
||||
val toIndex = minOf(fromIndex + ITEMS_PER_PAGE, totalCount)
|
||||
val paginatedCategories = if (fromIndex < totalCount) categoryList.subList(fromIndex, toIndex) else emptyList()
|
||||
val total = query.count()
|
||||
|
||||
val paginated =
|
||||
query
|
||||
.limit(opdsItemsPerPageBounded)
|
||||
.offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong())
|
||||
.map { row -> Pair(row[CategoryTable.id].value, row[CategoryTable.name]) }
|
||||
|
||||
Pair(paginated, total)
|
||||
}
|
||||
|
||||
return FeedBuilder(baseUrl, pageNum, "categories", "Categories")
|
||||
.apply {
|
||||
totalResults = totalCount.toLong()
|
||||
totalResults = total
|
||||
entries +=
|
||||
paginatedCategories.map { (id, name) ->
|
||||
categoryList.map { (id, name) ->
|
||||
OpdsXmlModels.Entry(
|
||||
id = "category/$id",
|
||||
title = name,
|
||||
@@ -252,8 +261,8 @@ object Opds {
|
||||
}
|
||||
|
||||
val totalCount = genres.size
|
||||
val fromIndex = (pageNum - 1) * ITEMS_PER_PAGE
|
||||
val toIndex = minOf(fromIndex + ITEMS_PER_PAGE, totalCount)
|
||||
val fromIndex = (pageNum - 1) * opdsItemsPerPageBounded
|
||||
val toIndex = minOf(fromIndex + opdsItemsPerPageBounded, totalCount)
|
||||
val paginatedGenres = if (fromIndex < totalCount) genres.subList(fromIndex, toIndex) else emptyList()
|
||||
|
||||
return serialize(
|
||||
@@ -263,7 +272,7 @@ object Opds {
|
||||
updated = formattedNow,
|
||||
author = OpdsXmlModels.Author("Suwayomi", "https://suwayomi.org/"),
|
||||
totalResults = totalCount.toLong(),
|
||||
itemsPerPage = ITEMS_PER_PAGE,
|
||||
itemsPerPage = opdsItemsPerPageBounded,
|
||||
startIndex = fromIndex + 1,
|
||||
links =
|
||||
listOf(
|
||||
@@ -306,8 +315,8 @@ object Opds {
|
||||
|
||||
val statuses = MangaStatus.entries.sortedBy { it.value }
|
||||
val totalCount = statuses.size
|
||||
val fromIndex = (pageNum - 1) * ITEMS_PER_PAGE
|
||||
val toIndex = minOf(fromIndex + ITEMS_PER_PAGE, totalCount)
|
||||
val fromIndex = (pageNum - 1) * opdsItemsPerPageBounded
|
||||
val toIndex = minOf(fromIndex + opdsItemsPerPageBounded, totalCount)
|
||||
val paginatedStatuses = if (fromIndex < totalCount) statuses.subList(fromIndex, toIndex) else emptyList()
|
||||
|
||||
return FeedBuilder(baseUrl, pageNum, "status", "Status")
|
||||
@@ -378,6 +387,8 @@ object Opds {
|
||||
baseUrl: String,
|
||||
pageNum: Int,
|
||||
): String {
|
||||
val sortOrder = serverConfig.opdsChapterSortOrder.value
|
||||
|
||||
val (manga, chapters, totalCount) =
|
||||
transaction {
|
||||
val mangaEntry =
|
||||
@@ -386,18 +397,30 @@ object Opds {
|
||||
.where { MangaTable.id eq mangaId }
|
||||
.first()
|
||||
val mangaData = MangaTable.toDataClass(mangaEntry, includeMangaMeta = false)
|
||||
|
||||
val chapterConditions =
|
||||
buildList {
|
||||
if (serverConfig.opdsShowOnlyUnreadChapters.value) {
|
||||
add(ChapterTable.isRead eq false)
|
||||
}
|
||||
|
||||
if (serverConfig.opdsShowOnlyDownloadedChapters.value) {
|
||||
add(ChapterTable.isDownloaded eq true)
|
||||
}
|
||||
add(ChapterTable.manga eq mangaId)
|
||||
}.reduce { acc, op -> acc and op }
|
||||
|
||||
val chaptersQuery =
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where {
|
||||
(ChapterTable.manga eq mangaId)
|
||||
}.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
|
||||
.where { chapterConditions }
|
||||
.orderBy(ChapterTable.sourceOrder to sortOrder)
|
||||
|
||||
val total = chaptersQuery.count()
|
||||
val chaptersData =
|
||||
chaptersQuery
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.limit(opdsItemsPerPageBounded)
|
||||
.offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong())
|
||||
.map { ChapterTable.toDataClass(it, includeChapterCount = false, includeChapterMeta = false) }
|
||||
Triple(mangaData, chaptersData, total)
|
||||
}
|
||||
@@ -489,6 +512,7 @@ object Opds {
|
||||
|
||||
val entryTitle =
|
||||
when {
|
||||
isMetaDataEntry -> "⬇"
|
||||
chapter.read -> "✅"
|
||||
chapter.lastPageRead > 0 -> "⌛"
|
||||
chapter.pageCount == 0 -> "❌"
|
||||
@@ -506,7 +530,9 @@ object Opds {
|
||||
add(
|
||||
OpdsXmlModels.Link(
|
||||
rel = "http://opds-spec.org/acquisition/open-access",
|
||||
href = "/api/v1/chapter/${chapter.id}/download",
|
||||
href =
|
||||
"/api/v1/chapter/${chapter.id}/download" +
|
||||
"?markAsRead=${serverConfig.opdsMarkAsReadOnDownload.value}",
|
||||
type = "application/vnd.comicbook+zip",
|
||||
),
|
||||
)
|
||||
@@ -515,7 +541,9 @@ object Opds {
|
||||
add(
|
||||
OpdsXmlModels.Link(
|
||||
rel = "http://vaemendis.net/opds-pse/stream",
|
||||
href = "/api/v1/manga/${manga.id}/chapter/${chapter.index}/page/{pageNumber}?updateProgress=true",
|
||||
href =
|
||||
"/api/v1/manga/${manga.id}/chapter/${chapter.index}/page/{pageNumber}" +
|
||||
"?updateProgress=${serverConfig.opdsEnablePageReadProgress.value}",
|
||||
type = "image/jpeg",
|
||||
pseCount = chapter.pageCount,
|
||||
pseLastRead = chapter.lastPageRead.takeIf { it != 0 },
|
||||
@@ -584,8 +612,8 @@ object Opds {
|
||||
val totalCount = query.count()
|
||||
val paginatedResults =
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.limit(opdsItemsPerPageBounded)
|
||||
.offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong())
|
||||
.map { MangaTable.toDataClass(it, includeMangaMeta = false) }
|
||||
|
||||
Triple(paginatedResults, totalCount, sourceRow)
|
||||
@@ -624,8 +652,8 @@ object Opds {
|
||||
val totalCount = query.count()
|
||||
val mangas =
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.limit(opdsItemsPerPageBounded)
|
||||
.offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong())
|
||||
.map { MangaTable.toDataClass(it, includeMangaMeta = false) }
|
||||
Triple(mangas, totalCount, categoryName)
|
||||
}
|
||||
@@ -655,8 +683,8 @@ object Opds {
|
||||
val totalCount = query.count()
|
||||
val mangas =
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.limit(opdsItemsPerPageBounded)
|
||||
.offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong())
|
||||
.map { MangaTable.toDataClass(it, includeMangaMeta = false) }
|
||||
Pair(mangas, totalCount)
|
||||
}
|
||||
@@ -692,8 +720,8 @@ object Opds {
|
||||
val totalCount = query.count()
|
||||
val mangas =
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.limit(opdsItemsPerPageBounded)
|
||||
.offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong())
|
||||
.map { MangaTable.toDataClass(it, includeMangaMeta = false) }
|
||||
Pair(mangas, totalCount)
|
||||
}
|
||||
@@ -724,8 +752,8 @@ object Opds {
|
||||
val totalCount = query.count()
|
||||
val mangas =
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.limit(opdsItemsPerPageBounded)
|
||||
.offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong())
|
||||
.map { MangaTable.toDataClass(it, includeMangaMeta = false) }
|
||||
Pair(mangas, totalCount)
|
||||
}
|
||||
@@ -753,8 +781,8 @@ object Opds {
|
||||
val totalCount = query.count()
|
||||
val chapters =
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.limit(opdsItemsPerPageBounded)
|
||||
.offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong())
|
||||
.map {
|
||||
ChapterTable.toDataClass(
|
||||
it,
|
||||
@@ -833,7 +861,7 @@ object Opds {
|
||||
type = "application/atom+xml;profile=opds-catalog;kind=navigation",
|
||||
)
|
||||
},
|
||||
(totalResults > pageNum * ITEMS_PER_PAGE).takeIf { it }?.let {
|
||||
(totalResults > pageNum * opdsItemsPerPageBounded).takeIf { it }?.let {
|
||||
OpdsXmlModels.Link(
|
||||
rel = "next",
|
||||
href = "$baseUrl/$id?pageNumber=${pageNum + 1}",
|
||||
@@ -843,8 +871,8 @@ object Opds {
|
||||
),
|
||||
entries = entries,
|
||||
totalResults = totalResults,
|
||||
itemsPerPage = ITEMS_PER_PAGE,
|
||||
startIndex = (pageNum - 1) * ITEMS_PER_PAGE + 1,
|
||||
itemsPerPage = opdsItemsPerPageBounded,
|
||||
startIndex = (pageNum - 1) * opdsItemsPerPageBounded + 1,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package suwayomi.tachidesk.server
|
||||
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
|
||||
interface ConfigAdapter<T> {
|
||||
fun toType(configValue: String): T
|
||||
}
|
||||
@@ -19,3 +21,7 @@ object BooleanConfigAdapter : ConfigAdapter<Boolean> {
|
||||
object DoubleConfigAdapter : ConfigAdapter<Double> {
|
||||
override fun toType(configValue: String): Double = configValue.toDouble()
|
||||
}
|
||||
|
||||
object SortOrderConfigAdapter : ConfigAdapter<SortOrder> {
|
||||
override fun toType(configValue: String): SortOrder = SortOrder.valueOf(configValue)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule
|
||||
import kotlin.reflect.KProperty
|
||||
@@ -155,6 +156,14 @@ class ServerConfig(
|
||||
val flareSolverrSessionTtl: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val flareSolverrAsResponseFallback: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
|
||||
// opds settings
|
||||
val opdsItemsPerPage: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val opdsEnablePageReadProgress: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val opdsMarkAsReadOnDownload: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val opdsShowOnlyUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val opdsShowOnlyDownloadedChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val opdsChapterSortOrder: MutableStateFlow<SortOrder> by OverrideConfigValue(SortOrderConfigAdapter)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun <T> subscribeTo(
|
||||
flow: Flow<T>,
|
||||
|
||||
@@ -70,3 +70,11 @@ server.flareSolverrTimeout = 60 # time in seconds
|
||||
server.flareSolverrSessionName = "suwayomi"
|
||||
server.flareSolverrSessionTtl = 15 # time in minutes
|
||||
server.flareSolverrAsResponseFallback = false
|
||||
|
||||
# OPDS
|
||||
server.opdsItemsPerPage = 50 # Range (10 - 5000)
|
||||
server.opdsEnablePageReadProgress = true
|
||||
server.opdsMarkAsReadOnDownload = false
|
||||
server.opdsShowOnlyUnreadChapters = false
|
||||
server.opdsShowOnlyDownloadedChapters = false
|
||||
server.opdsChapterSortOrder = "DESC" # "ASC", "DESC"
|
||||
|
||||
@@ -67,3 +67,11 @@ server.flareSolverrTimeout = 60 # time in seconds
|
||||
server.flareSolverrSessionName = "suwayomi"
|
||||
server.flareSolverrSessionTtl = 15 # time in minutes
|
||||
server.flareSolverrAsResponseFallback = false
|
||||
|
||||
# OPDS
|
||||
server.opdsItemsPerPage = 50 # Range (10 - 5000)
|
||||
server.opdsEnablePageReadProgress = true
|
||||
server.opdsMarkAsReadOnDownload = false
|
||||
server.opdsShowOnlyUnreadChapters = false
|
||||
server.opdsShowOnlyDownloadedChapters = false
|
||||
server.opdsChapterSortOrder = "DESC" # "ASC", "DESC"
|
||||
|
||||
Reference in New Issue
Block a user