mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-16 17:24:07 -05:00
Add support for opds-pse for undownloaded chapters (#1278)
* Add OPDS page streaming for undownloaded chapters * Add [D] in chapter title prefix when isDownloaded * Removed Chapter.isDownloaded check in query for other opds endpoints * Add chapter progression tracking for streaming and refactor code * dd ❌ Unicode for chapters with 0 pages [post pageRefresh] * Add Library Updates feed and remove redundant metadata fetching for OPDS chapters and manga * Address PR comments & add chapter markAsRead for cbzDownload * Address PR comment/s * Rem. markAsRead for chapter download * Rem. markAsRead for chapter download --------- Co-authored-by: ShowY <showypro@gmail.com>
This commit is contained in:
@@ -23,12 +23,17 @@ object OpdsAPI {
|
||||
get("genres", OpdsV1Controller.genresFeed)
|
||||
get("status", OpdsV1Controller.statusFeed)
|
||||
get("languages", OpdsV1Controller.languagesFeed)
|
||||
get("library-updates", OpdsV1Controller.libraryUpdatesFeed)
|
||||
|
||||
// Faceted feeds (Acquisition Feeds)
|
||||
path("manga/{mangaId}") {
|
||||
get(OpdsV1Controller.mangaFeed)
|
||||
}
|
||||
|
||||
path("manga/{mangaId}/chapter/{chapterId}/fetch") {
|
||||
get(OpdsV1Controller.chapterMetadataFeed)
|
||||
}
|
||||
|
||||
path("source/{sourceId}") {
|
||||
get(OpdsV1Controller.sourceFeed)
|
||||
}
|
||||
|
||||
@@ -278,6 +278,31 @@ object OpdsV1Controller {
|
||||
},
|
||||
)
|
||||
|
||||
var chapterMetadataFeed =
|
||||
handler(
|
||||
pathParam<Int>("mangaId"),
|
||||
pathParam<Int>("chapterId"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("OPDS Chapter Details Feed")
|
||||
description("OPDS feed for a specific undownloaded chapter of a manga")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, chapterId ->
|
||||
ctx.future {
|
||||
future {
|
||||
Opds.getChapterMetadataFeed(mangaId, chapterId, BASE_URL)
|
||||
}.thenApply { xml ->
|
||||
ctx.contentType(OPDS_MIME).result(xml)
|
||||
}
|
||||
}
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpStatus.OK)
|
||||
httpCode(HttpStatus.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
|
||||
// Specific Source Feed
|
||||
val sourceFeed =
|
||||
handler(
|
||||
@@ -407,4 +432,28 @@ object OpdsV1Controller {
|
||||
httpCode(HttpStatus.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
|
||||
// Main Library Updates Feed
|
||||
val libraryUpdatesFeed =
|
||||
handler(
|
||||
queryParam<Int?>("pageNumber"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("OPDS Library Updates Feed")
|
||||
description("OPDS feed listing recent manga chapter updates")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pageNumber ->
|
||||
ctx.future {
|
||||
future {
|
||||
Opds.getLibraryUpdatesFeed(BASE_URL, pageNumber ?: 1)
|
||||
}.thenApply { xml ->
|
||||
ctx.contentType(OPDS_MIME).result(xml)
|
||||
}
|
||||
}
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpStatus.OK)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package suwayomi.tachidesk.opds.impl
|
||||
|
||||
import SearchCriteria
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.adaptivity.xmlutil.XmlDeclMode
|
||||
import nl.adaptivity.xmlutil.core.XmlVersion
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
@@ -8,12 +10,14 @@ import org.jetbrains.exposed.sql.JoinType
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.lowerCase
|
||||
import org.jetbrains.exposed.sql.or
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper.getArchiveStreamWithSize
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
||||
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
|
||||
@@ -26,7 +30,6 @@ 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 java.io.File
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.Instant
|
||||
@@ -48,6 +51,7 @@ object Opds {
|
||||
"genres" to "Genres",
|
||||
"status" to "Status",
|
||||
"languages" to "Languages",
|
||||
"library-updates" to "Library Update History",
|
||||
).map { (id, title) ->
|
||||
OpdsXmlModels.Entry(
|
||||
id = id,
|
||||
@@ -79,26 +83,26 @@ object Opds {
|
||||
.join(ChapterTable, JoinType.INNER, onColumn = MangaTable.id, otherColumn = ChapterTable.manga)
|
||||
.select(MangaTable.columns)
|
||||
.where {
|
||||
val baseCondition = ChapterTable.isDownloaded eq true
|
||||
if (criteria == null) {
|
||||
baseCondition
|
||||
} else {
|
||||
val conditions = mutableListOf<Op<Boolean>>()
|
||||
criteria.query?.takeIf { it.isNotBlank() }?.let { q ->
|
||||
conditions += (
|
||||
(MangaTable.title like "%$q%") or
|
||||
(MangaTable.author like "%$q%") or
|
||||
(MangaTable.genre like "%$q%")
|
||||
)
|
||||
}
|
||||
criteria.author?.takeIf { it.isNotBlank() }?.let { author ->
|
||||
conditions += (MangaTable.author like "%$author%")
|
||||
}
|
||||
criteria.title?.takeIf { it.isNotBlank() }?.let { title ->
|
||||
conditions += (MangaTable.title like "%$title%")
|
||||
}
|
||||
baseCondition and (if (conditions.isEmpty()) Op.TRUE else conditions.reduce { acc, op -> acc and op })
|
||||
val conditions = mutableListOf<Op<Boolean>>()
|
||||
|
||||
criteria?.query?.takeIf { it.isNotBlank() }?.let { q ->
|
||||
val lowerQ = q.lowercase()
|
||||
conditions += (
|
||||
(MangaTable.title.lowerCase() like "%$lowerQ%") or
|
||||
(MangaTable.author.lowerCase() like "%$lowerQ%") or
|
||||
(MangaTable.genre.lowerCase() like "%$lowerQ%")
|
||||
)
|
||||
}
|
||||
|
||||
criteria?.author?.takeIf { it.isNotBlank() }?.let { author ->
|
||||
conditions += (MangaTable.author.lowerCase() like "%${author.lowercase()}%")
|
||||
}
|
||||
|
||||
criteria?.title?.takeIf { it.isNotBlank() }?.let { title ->
|
||||
conditions += (MangaTable.title.lowerCase() like "%${title.lowercase()}%")
|
||||
}
|
||||
|
||||
if (conditions.isEmpty()) (MangaTable.inLibrary eq true) else conditions.reduce { acc, op -> acc and op }
|
||||
}.groupBy(MangaTable.id)
|
||||
.orderBy(MangaTable.title to SortOrder.ASC)
|
||||
val totalCount = query.count()
|
||||
@@ -106,7 +110,7 @@ object Opds {
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.map { MangaTable.toDataClass(it) }
|
||||
.map { MangaTable.toDataClass(it, includeMangaMeta = false) }
|
||||
Pair(mangas, totalCount)
|
||||
}
|
||||
|
||||
@@ -136,7 +140,6 @@ object Opds {
|
||||
}.join(ChapterTable, JoinType.INNER) {
|
||||
ChapterTable.manga eq MangaTable.id
|
||||
}.select(SourceTable.columns)
|
||||
.where { ChapterTable.isDownloaded eq true }
|
||||
.groupBy(SourceTable.id)
|
||||
.orderBy(SourceTable.name to SortOrder.ASC)
|
||||
|
||||
@@ -195,7 +198,6 @@ object Opds {
|
||||
.join(MangaTable, JoinType.INNER, onColumn = CategoryMangaTable.manga, otherColumn = MangaTable.id)
|
||||
.join(ChapterTable, JoinType.INNER, onColumn = MangaTable.id, otherColumn = ChapterTable.manga)
|
||||
.select(CategoryTable.id, CategoryTable.name)
|
||||
.where { ChapterTable.isDownloaded eq true }
|
||||
.groupBy(CategoryTable.id)
|
||||
.orderBy(CategoryTable.order to SortOrder.ASC)
|
||||
.map { row ->
|
||||
@@ -241,7 +243,6 @@ object Opds {
|
||||
MangaTable
|
||||
.join(ChapterTable, JoinType.INNER, onColumn = MangaTable.id, otherColumn = ChapterTable.manga)
|
||||
.select(MangaTable.genre)
|
||||
.where { ChapterTable.isDownloaded eq true }
|
||||
.map { it[MangaTable.genre] }
|
||||
.flatMap { it?.split(", ")?.filterNot { g -> g.isBlank() } ?: emptyList() }
|
||||
.groupingBy { it }
|
||||
@@ -344,7 +345,6 @@ object Opds {
|
||||
.join(MangaTable, JoinType.INNER, onColumn = SourceTable.id, otherColumn = MangaTable.sourceReference)
|
||||
.join(ChapterTable, JoinType.INNER, onColumn = MangaTable.id, otherColumn = ChapterTable.manga)
|
||||
.select(SourceTable.lang)
|
||||
.where { ChapterTable.isDownloaded eq true }
|
||||
.groupBy(SourceTable.lang)
|
||||
.orderBy(SourceTable.lang to SortOrder.ASC)
|
||||
.map { row -> row[SourceTable.lang] }
|
||||
@@ -378,7 +378,6 @@ object Opds {
|
||||
baseUrl: String,
|
||||
pageNum: Int,
|
||||
): String {
|
||||
val formattedNow = opdsDateFormatter.format(Instant.now())
|
||||
val (manga, chapters, totalCount) =
|
||||
transaction {
|
||||
val mangaEntry =
|
||||
@@ -386,21 +385,20 @@ object Opds {
|
||||
.selectAll()
|
||||
.where { MangaTable.id eq mangaId }
|
||||
.first()
|
||||
val mangaData = MangaTable.toDataClass(mangaEntry)
|
||||
val mangaData = MangaTable.toDataClass(mangaEntry, includeMangaMeta = false)
|
||||
val chaptersQuery =
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where {
|
||||
(ChapterTable.manga eq mangaId) and
|
||||
(ChapterTable.isDownloaded eq true) and
|
||||
(ChapterTable.pageCount greater 0)
|
||||
(ChapterTable.manga eq mangaId)
|
||||
}.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
|
||||
|
||||
val total = chaptersQuery.count()
|
||||
val chaptersData =
|
||||
chaptersQuery
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.map { ChapterTable.toDataClass(it) }
|
||||
.map { ChapterTable.toDataClass(it, includeChapterCount = false, includeChapterMeta = false) }
|
||||
Triple(mangaData, chaptersData, total)
|
||||
}
|
||||
|
||||
@@ -422,56 +420,140 @@ object Opds {
|
||||
type = "image/jpeg",
|
||||
)
|
||||
}
|
||||
entries += chapters.map { createChapterEntry(it, manga) }
|
||||
entries += chapters.map { createChapterEntry(it, manga, baseUrl, isMetaDataEntry = false) }
|
||||
}.build()
|
||||
.let(::serialize)
|
||||
}
|
||||
|
||||
suspend fun getChapterMetadataFeed(
|
||||
mangaId: Int,
|
||||
chapterIndex: Int,
|
||||
baseUrl: String,
|
||||
): String {
|
||||
val mangaData =
|
||||
withContext(Dispatchers.IO) {
|
||||
transaction {
|
||||
val mangaEntry =
|
||||
MangaTable
|
||||
.selectAll()
|
||||
.where { MangaTable.id eq mangaId }
|
||||
.first()
|
||||
MangaTable.toDataClass(mangaEntry, includeMangaMeta = false)
|
||||
}
|
||||
}
|
||||
|
||||
val updatedChapterData = getChapterDownloadReady(chapterIndex = chapterIndex, mangaId = mangaId)
|
||||
val updatedEntry = createChapterEntry(updatedChapterData, mangaData, baseUrl, isMetaDataEntry = true)
|
||||
|
||||
return FeedBuilder(
|
||||
baseUrl = baseUrl,
|
||||
pageNum = 1,
|
||||
id = "manga/$mangaId/chapter/$chapterIndex",
|
||||
title = "${mangaData.title} | ${updatedChapterData.name} | Details",
|
||||
).apply {
|
||||
totalResults = 1
|
||||
icon = mangaData.thumbnailUrl
|
||||
mangaData.thumbnailUrl?.let { url ->
|
||||
links +=
|
||||
OpdsXmlModels.Link(
|
||||
rel = "http://opds-spec.org/image",
|
||||
href = url,
|
||||
type = "image/jpeg",
|
||||
)
|
||||
links +=
|
||||
OpdsXmlModels.Link(
|
||||
rel = "http://opds-spec.org/image/thumbnail",
|
||||
href = url,
|
||||
type = "image/jpeg",
|
||||
)
|
||||
}
|
||||
entries += listOf(updatedEntry)
|
||||
}.build()
|
||||
.let(::serialize)
|
||||
}
|
||||
|
||||
private fun createChapterEntry(
|
||||
chapter: ChapterDataClass,
|
||||
manga: MangaDataClass,
|
||||
baseUrl: String,
|
||||
isMetaDataEntry: Boolean,
|
||||
addMangaTitleInEntry: Boolean = false,
|
||||
): OpdsXmlModels.Entry {
|
||||
val cbzFile = File(getChapterCbzPath(manga.id, chapter.id))
|
||||
val isCbzAvailable = cbzFile.exists()
|
||||
val chapterDetails =
|
||||
buildString {
|
||||
append("${manga.title} | ${chapter.name} | By ${chapter.scanlator}")
|
||||
if (isMetaDataEntry) {
|
||||
append(" | Progress (${chapter.lastPageRead} / ${chapter.pageCount})")
|
||||
}
|
||||
}
|
||||
|
||||
return OpdsXmlModels.Entry(
|
||||
id = "chapter/${chapter.id}",
|
||||
title = chapter.name,
|
||||
updated = opdsDateFormatter.format(Instant.ofEpochMilli(chapter.uploadDate)),
|
||||
content = OpdsXmlModels.Content(value = "${chapter.scanlator}"),
|
||||
summary = manga.description?.let { OpdsXmlModels.Summary(value = it) },
|
||||
extent =
|
||||
cbzFile.takeIf { it.exists() }?.let {
|
||||
formatFileSize(it.length())
|
||||
},
|
||||
format = cbzFile.takeIf { it.exists() }?.let { "CBZ" },
|
||||
authors =
|
||||
listOfNotNull(
|
||||
manga.author?.let { OpdsXmlModels.Author(name = it) },
|
||||
manga.artist?.takeIf { it != manga.author }?.let { OpdsXmlModels.Author(name = it) },
|
||||
),
|
||||
link =
|
||||
listOfNotNull(
|
||||
if (isCbzAvailable) {
|
||||
val entryTitle =
|
||||
when {
|
||||
chapter.read -> "✅"
|
||||
chapter.lastPageRead > 0 -> "⌛"
|
||||
chapter.pageCount == 0 -> "❌"
|
||||
else -> "⭕"
|
||||
} + (if (addMangaTitleInEntry) " ${manga.title} :" else "") + " ${chapter.name}"
|
||||
|
||||
val cbzInputStreamPair =
|
||||
runCatching {
|
||||
if (isMetaDataEntry && chapter.downloaded) getArchiveStreamWithSize(manga.id, chapter.id) else null
|
||||
}.getOrNull()
|
||||
|
||||
val links =
|
||||
mutableListOf<OpdsXmlModels.Link>().apply {
|
||||
if (cbzInputStreamPair != null) {
|
||||
add(
|
||||
OpdsXmlModels.Link(
|
||||
rel = "http://opds-spec.org/acquisition/open-access",
|
||||
href = "/api/v1/chapter/${chapter.id}/download",
|
||||
type = "application/vnd.comicbook+zip",
|
||||
)
|
||||
} else {
|
||||
),
|
||||
)
|
||||
}
|
||||
if (isMetaDataEntry) {
|
||||
add(
|
||||
OpdsXmlModels.Link(
|
||||
rel = "http://vaemendis.net/opds-pse/stream",
|
||||
href = "/api/v1/manga/${manga.id}/chapter/${chapter.index}/page/{pageNumber}",
|
||||
href = "/api/v1/manga/${manga.id}/chapter/${chapter.index}/page/{pageNumber}?updateProgress=true",
|
||||
type = "image/jpeg",
|
||||
pseCount = chapter.pageCount,
|
||||
)
|
||||
},
|
||||
OpdsXmlModels.Link(
|
||||
rel = "http://opds-spec.org/image",
|
||||
href = "/api/v1/manga/${manga.id}/chapter/${chapter.index}/page/0",
|
||||
type = "image/jpeg",
|
||||
),
|
||||
pseLastRead = chapter.lastPageRead.takeIf { it != 0 },
|
||||
),
|
||||
)
|
||||
add(
|
||||
OpdsXmlModels.Link(
|
||||
rel = "http://opds-spec.org/image",
|
||||
href = "/api/v1/manga/${manga.id}/chapter/${chapter.index}/page/0",
|
||||
type = "image/jpeg",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
add(
|
||||
OpdsXmlModels.Link(
|
||||
rel = "subsection",
|
||||
href = "$baseUrl/manga/${manga.id}/chapter/${chapter.index}/fetch",
|
||||
type = "application/atom+xml;profile=opds-catalog;kind=acquisition",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return OpdsXmlModels.Entry(
|
||||
id = "chapter/${chapter.id}",
|
||||
title = entryTitle,
|
||||
updated = opdsDateFormatter.format(Instant.ofEpochMilli(chapter.uploadDate)),
|
||||
content = OpdsXmlModels.Content(value = chapterDetails),
|
||||
summary = OpdsXmlModels.Summary(value = chapterDetails),
|
||||
extent = cbzInputStreamPair?.second?.let { formatFileSize(it) },
|
||||
format = cbzInputStreamPair?.second?.let { "CBZ" },
|
||||
authors =
|
||||
listOfNotNull(
|
||||
manga.author?.let { OpdsXmlModels.Author(name = it) },
|
||||
manga.artist?.takeIf { it != manga.author }?.let { OpdsXmlModels.Author(name = it) },
|
||||
chapter.scanlator?.let { OpdsXmlModels.Author(name = it) },
|
||||
),
|
||||
link = links,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -495,7 +577,7 @@ object Opds {
|
||||
.join(ChapterTable, JoinType.INNER, onColumn = MangaTable.id, otherColumn = ChapterTable.manga)
|
||||
.select(MangaTable.columns)
|
||||
.where {
|
||||
(MangaTable.sourceReference eq sourceId) and (ChapterTable.isDownloaded eq true)
|
||||
(MangaTable.sourceReference eq sourceId)
|
||||
}.groupBy(MangaTable.id)
|
||||
.orderBy(MangaTable.title to SortOrder.ASC)
|
||||
|
||||
@@ -504,7 +586,7 @@ object Opds {
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.map { MangaTable.toDataClass(it) }
|
||||
.map { MangaTable.toDataClass(it, includeMangaMeta = false) }
|
||||
|
||||
Triple(paginatedResults, totalCount, sourceRow)
|
||||
}
|
||||
@@ -536,7 +618,7 @@ object Opds {
|
||||
.join(MangaTable, JoinType.INNER, onColumn = CategoryMangaTable.manga, otherColumn = MangaTable.id)
|
||||
.join(ChapterTable, JoinType.INNER, onColumn = MangaTable.id, otherColumn = ChapterTable.manga)
|
||||
.select(MangaTable.columns)
|
||||
.where { (CategoryMangaTable.category eq categoryId) and (ChapterTable.isDownloaded eq true) }
|
||||
.where { (CategoryMangaTable.category eq categoryId) }
|
||||
.groupBy(MangaTable.id)
|
||||
.orderBy(MangaTable.title to SortOrder.ASC)
|
||||
val totalCount = query.count()
|
||||
@@ -544,7 +626,7 @@ object Opds {
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.map { MangaTable.toDataClass(it) }
|
||||
.map { MangaTable.toDataClass(it, includeMangaMeta = false) }
|
||||
Triple(mangas, totalCount, categoryName)
|
||||
}
|
||||
return FeedBuilder(baseUrl, pageNum, "category/$categoryId", "Category: $categoryName")
|
||||
@@ -567,7 +649,7 @@ object Opds {
|
||||
MangaTable
|
||||
.join(ChapterTable, JoinType.INNER, onColumn = MangaTable.id, otherColumn = ChapterTable.manga)
|
||||
.select(MangaTable.columns)
|
||||
.where { (MangaTable.genre like "%$genre%") and (ChapterTable.isDownloaded eq true) }
|
||||
.where { (MangaTable.genre like "%$genre%") }
|
||||
.groupBy(MangaTable.id)
|
||||
.orderBy(MangaTable.title to SortOrder.ASC)
|
||||
val totalCount = query.count()
|
||||
@@ -575,7 +657,7 @@ object Opds {
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.map { MangaTable.toDataClass(it) }
|
||||
.map { MangaTable.toDataClass(it, includeMangaMeta = false) }
|
||||
Pair(mangas, totalCount)
|
||||
}
|
||||
return FeedBuilder(baseUrl, pageNum, "genre/${genre.encodeURL()}", "Genre: $genre")
|
||||
@@ -604,7 +686,7 @@ object Opds {
|
||||
MangaTable
|
||||
.join(ChapterTable, JoinType.INNER, onColumn = MangaTable.id, otherColumn = ChapterTable.manga)
|
||||
.select(MangaTable.columns)
|
||||
.where { (MangaTable.status eq statusId.toInt()) and (ChapterTable.isDownloaded eq true) }
|
||||
.where { (MangaTable.status eq statusId.toInt()) }
|
||||
.groupBy(MangaTable.id)
|
||||
.orderBy(MangaTable.title to SortOrder.ASC)
|
||||
val totalCount = query.count()
|
||||
@@ -612,7 +694,7 @@ object Opds {
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.map { MangaTable.toDataClass(it) }
|
||||
.map { MangaTable.toDataClass(it, includeMangaMeta = false) }
|
||||
Pair(mangas, totalCount)
|
||||
}
|
||||
return FeedBuilder(baseUrl, pageNum, "status/$statusId", "Status: $statusName")
|
||||
@@ -636,7 +718,7 @@ object Opds {
|
||||
.join(MangaTable, JoinType.INNER, onColumn = SourceTable.id, otherColumn = MangaTable.sourceReference)
|
||||
.join(ChapterTable, JoinType.INNER, onColumn = MangaTable.id, otherColumn = ChapterTable.manga)
|
||||
.select(MangaTable.columns)
|
||||
.where { (SourceTable.lang eq langCode) and (ChapterTable.isDownloaded eq true) }
|
||||
.where { (SourceTable.lang eq langCode) }
|
||||
.groupBy(MangaTable.id)
|
||||
.orderBy(MangaTable.title to SortOrder.ASC)
|
||||
val totalCount = query.count()
|
||||
@@ -644,7 +726,7 @@ object Opds {
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.map { MangaTable.toDataClass(it) }
|
||||
.map { MangaTable.toDataClass(it, includeMangaMeta = false) }
|
||||
Pair(mangas, totalCount)
|
||||
}
|
||||
return FeedBuilder(baseUrl, pageNum, "language/$langCode", "Language: $langCode")
|
||||
@@ -655,6 +737,52 @@ object Opds {
|
||||
.let(::serialize)
|
||||
}
|
||||
|
||||
fun getLibraryUpdatesFeed(
|
||||
baseUrl: String,
|
||||
pageNum: Int,
|
||||
): String {
|
||||
val (chapterToMangaMap, total) =
|
||||
transaction {
|
||||
val query =
|
||||
ChapterTable
|
||||
.join(MangaTable, JoinType.INNER, onColumn = ChapterTable.manga, otherColumn = MangaTable.id)
|
||||
.selectAll()
|
||||
.where { (MangaTable.inLibrary eq true) }
|
||||
.orderBy(ChapterTable.fetchedAt to SortOrder.DESC, ChapterTable.sourceOrder to SortOrder.DESC)
|
||||
|
||||
val totalCount = query.count()
|
||||
val chapters =
|
||||
query
|
||||
.limit(ITEMS_PER_PAGE)
|
||||
.offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong())
|
||||
.map {
|
||||
ChapterTable.toDataClass(
|
||||
it,
|
||||
includeChapterCount = false,
|
||||
includeChapterMeta = false,
|
||||
) to MangaTable.toDataClass(it, includeMangaMeta = false)
|
||||
}
|
||||
|
||||
Pair(chapters, totalCount)
|
||||
}
|
||||
|
||||
return FeedBuilder(baseUrl, pageNum, "library-updates", "Library Updates")
|
||||
.apply {
|
||||
totalResults = total
|
||||
entries +=
|
||||
chapterToMangaMap.map {
|
||||
createChapterEntry(
|
||||
it.first,
|
||||
it.second,
|
||||
baseUrl,
|
||||
isMetaDataEntry = false,
|
||||
addMangaTitleInEntry = true,
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
.let(::serialize)
|
||||
}
|
||||
|
||||
private class FeedBuilder(
|
||||
val baseUrl: String,
|
||||
val pageNum: Int,
|
||||
|
||||
@@ -64,6 +64,8 @@ data class OpdsXmlModels(
|
||||
val title: String? = null,
|
||||
@XmlSerialName("pse:count", "", "")
|
||||
val pseCount: Int? = null,
|
||||
@XmlSerialName("pse:lastRead", "", "")
|
||||
val pseLastRead: Int? = null,
|
||||
@XmlSerialName("opds:facetGroup", "", "")
|
||||
val facetGroup: String? = null,
|
||||
@XmlSerialName("opds:activeFacet", "", "")
|
||||
|
||||
Reference in New Issue
Block a user