mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-10 14:24:34 -05:00
feat(opds): Add option to skip metadata feed for direct access (#1879)
* feat(opds): add option to skip chapter metadata feed Introduces a new server configuration `server.opdsSkipChapterMetadataFeed` (default: false). When enabled, the OPDS chapter feed generates direct acquisition (CBZ download) and streaming (OPDS-PSE) links within the chapter list entries, bypassing the intermediate metadata subsection. This streamlines the user experience and improves compatibility with OPDS clients like KOReader that rely on direct links for automated downloading features. * fix: lint * fix(opds): enrich chapter data and refine sync logic for skip-metadata mode Refines the `opdsSkipChapterMetadataFeed` implementation to ensure necessary data is available for direct links and handles synchronization logic appropriate for a list view. - **Refactor ChapterForDownload:** Extract `refreshChapterPageList` and `updateChapterPersistence` to allow reusing page count verification logic outside the download flow. - **Enrich Chapter Repository:** When skipping metadata, asynchronously verify page counts and calculate CBZ file sizes for chapters in the list. This ensures direct stream/download links are valid even if the chapter wasn't previously fully indexed. - **KoSync Logic:** Implement synchronization logic in `OpdsEntryBuilder`. Since the user cannot be prompted in the chapter list view, `PROMPT` conflicts are explicitly ignored (prioritizing local progress), while updates are applied if non-conflicting. - **OPDS Attributes:** Add `length` (file size) to acquisition links and ensure download links only appear for actually downloaded chapters. - **Documentation:** Update `server.conf` description to clarify KoSync behavior in this mode. * feat(download): improve chapter download filenames * feat(opds): append language to source names * feat(opds): handle empty chapter titles * fix import org.jetbrains.exposed.v1.core.inList * refactor(opds): reorganize API routes and update facet count calculations based on active filters - **API Routing & Controllers**: Reorganize OPDS v1.2 route paths into logical groups in `OpdsAPI`. Centralize request filter extraction into `OpdsMangaFilter.fromContext`. - **Facet Counting**: Extract `Query.applyOpdsMangaFilter` to apply active filters to facet and navigation queries. Pass the active filters to `NavigationRepository` and `MangaRepository` count queries (using `excludeField` to calculate sibling counts). This ensures category, source, language, status, and genre counts (`thr:count`) are accurately computed based on active selections. - **Pagination**: Add pagination support to computed navigation feeds in `NavigationRepository` ( statuses and content languages). - **Builders**: Standardize parameter ordering in `FeedBuilderInternal` and `OpdsEntryBuilder` constructors. Simplify pagination and facet link URL generation. * fix(opds): remove redundant filter logic to avoid duplicate HAVING clauses Resolve IllegalStateException crash caused by applying content filters twice in MangaRepository. Filtering is now handled exclusively by `applyOpdsMangaFilter`, allowing `applyMangaLibrarySort` to focus solely on ordering operations. * revert(download): restore original CBZ filename scheme * refactor(opds): simplify persistence updates and clean up chapter mapping - Simplify page count and download checks in ChapterForDownload - Clean up enriched chapter mapping in ChapterRepository to improve readability * fix(opds): retrieve chapter archive size without leaving stream open * perf(opds): avoid redundant DB query when refreshing chapter page list
This commit is contained in:
@@ -64,6 +64,11 @@ object ChapterDownloadHelper {
|
||||
chapterId: Int,
|
||||
): Pair<InputStream, Long> = provider(mangaId, chapterId).getAsArchiveStream()
|
||||
|
||||
fun getChapterArchiveSize(
|
||||
mangaId: Int,
|
||||
chapterId: Int,
|
||||
): Long = provider(mangaId, chapterId).getArchiveSize()
|
||||
|
||||
private fun getChapterWithCbzFileName(chapterId: Int): Pair<ChapterDataClass, String> =
|
||||
transaction {
|
||||
val row =
|
||||
|
||||
@@ -31,6 +31,88 @@ import suwayomi.tachidesk.manga.model.table.PageTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Updates chapter download status and page count in the database if they differ from the file system.
|
||||
*/
|
||||
fun updateChapterPersistence(
|
||||
chapterId: Int,
|
||||
isMarkedAsDownloaded: Boolean,
|
||||
dbPageCount: Int,
|
||||
downloadPageCount: Int,
|
||||
lastPageRead: Int,
|
||||
logger: KLogger,
|
||||
): Boolean {
|
||||
if (isMarkedAsDownloaded && dbPageCount == downloadPageCount) {
|
||||
return false
|
||||
}
|
||||
|
||||
return transaction {
|
||||
var needsUpdate = false
|
||||
if (!isMarkedAsDownloaded) {
|
||||
logger.debug { "mark as downloaded" }
|
||||
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||
it[isDownloaded] = true
|
||||
}
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if (dbPageCount != downloadPageCount) {
|
||||
logger.debug { "use page count of downloaded chapter" }
|
||||
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||
it[pageCount] = downloadPageCount
|
||||
it[ChapterTable.lastPageRead] = lastPageRead.coerceAtMost(downloadPageCount - 1).coerceAtLeast(0)
|
||||
}
|
||||
needsUpdate = true
|
||||
}
|
||||
needsUpdate
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun refreshChapterPageList(
|
||||
mangaId: Int,
|
||||
chapterId: Int,
|
||||
existingChapterEntry: ResultRow? = null,
|
||||
): Int {
|
||||
val mutex = mutexByChapterId.get(chapterId) { Mutex() }
|
||||
return mutex.withLock {
|
||||
val chapterEntry = existingChapterEntry ?: transaction { ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first() }
|
||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
val pageList =
|
||||
source
|
||||
.getPageList(
|
||||
SChapter.create().apply {
|
||||
url = chapterEntry[ChapterTable.url]
|
||||
name = chapterEntry[ChapterTable.name]
|
||||
scanlator = chapterEntry[ChapterTable.scanlator]
|
||||
chapter_number = chapterEntry[ChapterTable.chapter_number]
|
||||
date_upload = chapterEntry[ChapterTable.date_upload]
|
||||
},
|
||||
).mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
||||
|
||||
transaction {
|
||||
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||
it[isDownloaded] = false
|
||||
}
|
||||
|
||||
PageTable.deleteWhere { PageTable.chapter eq chapterId }
|
||||
PageTable.batchInsert(pageList) { page ->
|
||||
this[PageTable.index] = page.index
|
||||
this[PageTable.url] = page.url
|
||||
this[PageTable.imageUrl] = page.imageUrl
|
||||
this[PageTable.chapter] = chapterId
|
||||
}
|
||||
|
||||
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||
it[pageCount] = pageList.size
|
||||
it[lastPageRead] = chapterEntry[ChapterTable.lastPageRead].coerceAtMost(pageList.size - 1).coerceAtLeast(0)
|
||||
}
|
||||
}
|
||||
pageList.size
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getChapterDownloadReady(
|
||||
chapterId: Int? = null,
|
||||
chapterIndex: Int? = null,
|
||||
@@ -68,56 +150,31 @@ private class ChapterForDownload(
|
||||
suspend fun asDownloadReady(): ChapterDataClass {
|
||||
val log = KotlinLogging.logger("${logger.name}::asDownloadReady")
|
||||
|
||||
val downloadPageCount =
|
||||
try {
|
||||
ChapterDownloadHelper.getImageCount(mangaId, chapterId)
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}
|
||||
val downloadPageCount = runCatching { ChapterDownloadHelper.getImageCount(mangaId, chapterId) }.getOrDefault(0)
|
||||
val isMarkedAsDownloaded = chapterEntry[ChapterTable.isDownloaded]
|
||||
val dbPageCount = chapterEntry[ChapterTable.pageCount]
|
||||
val doesDownloadExist = downloadPageCount != 0
|
||||
val doPageCountsMatch = dbPageCount == downloadPageCount
|
||||
|
||||
log.debug { "isMarkedAsDownloaded= $isMarkedAsDownloaded, dbPageCount= $dbPageCount, downloadPageCount= $downloadPageCount" }
|
||||
|
||||
return if (!doesDownloadExist) {
|
||||
log.debug { "reset download status and fetch page list" }
|
||||
updateDownloadStatusAndPageList(false)
|
||||
refreshChapterPageList(mangaId, chapterId, chapterEntry)
|
||||
chapterEntry = freshChapterEntry(optChapterId = chapterId)
|
||||
ChapterTable.toDataClass(chapterEntry)
|
||||
} else {
|
||||
transaction {
|
||||
var needsUpdate = false
|
||||
|
||||
if (!isMarkedAsDownloaded) {
|
||||
log.debug { "mark as downloaded" }
|
||||
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||
it[isDownloaded] = true
|
||||
}
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if (!doPageCountsMatch) {
|
||||
log.debug { "use page count of downloaded chapter" }
|
||||
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||
it[pageCount] = downloadPageCount
|
||||
it[lastPageRead] = chapterEntry[ChapterTable.lastPageRead].coerceAtMost(downloadPageCount - 1).coerceAtLeast(0)
|
||||
}
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
// Return updated chapter data
|
||||
val updatedRow =
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.id eq chapterId }
|
||||
.first()
|
||||
|
||||
if (needsUpdate) {
|
||||
chapterEntry = updatedRow
|
||||
}
|
||||
|
||||
ChapterTable.toDataClass(updatedRow)
|
||||
if (updateChapterPersistence(
|
||||
chapterId,
|
||||
isMarkedAsDownloaded,
|
||||
dbPageCount,
|
||||
downloadPageCount,
|
||||
chapterEntry[ChapterTable.lastPageRead],
|
||||
log,
|
||||
)
|
||||
) {
|
||||
chapterEntry = freshChapterEntry(optChapterId = chapterId)
|
||||
}
|
||||
ChapterTable.toDataClass(chapterEntry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,59 +207,4 @@ private class ChapterForDownload(
|
||||
}
|
||||
}.first()
|
||||
}
|
||||
|
||||
private suspend fun updateDownloadStatusAndPageList(downloaded: Boolean): ChapterDataClass {
|
||||
val mutex = mutexByChapterId.get(chapterId) { Mutex() }
|
||||
return mutex.withLock {
|
||||
val pageList = fetchPageList()
|
||||
|
||||
transaction {
|
||||
// Update download status
|
||||
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||
it[isDownloaded] = downloaded
|
||||
}
|
||||
|
||||
// Clear existing pages and insert new ones
|
||||
PageTable.deleteWhere { PageTable.chapter eq chapterId }
|
||||
PageTable.batchInsert(pageList) { page ->
|
||||
this[PageTable.index] = page.index
|
||||
this[PageTable.url] = page.url
|
||||
this[PageTable.imageUrl] = page.imageUrl
|
||||
this[PageTable.chapter] = chapterId
|
||||
}
|
||||
|
||||
// Update page count
|
||||
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||
it[pageCount] = pageList.size
|
||||
it[lastPageRead] = chapterEntry[ChapterTable.lastPageRead].coerceAtMost(pageList.size - 1).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
// Get updated chapter data
|
||||
val updatedRow =
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.id eq chapterId }
|
||||
.first()
|
||||
|
||||
chapterEntry = updatedRow
|
||||
ChapterTable.toDataClass(updatedRow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchPageList(): List<Page> {
|
||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
return source
|
||||
.getPageList(
|
||||
SChapter.create().apply {
|
||||
url = chapterEntry[ChapterTable.url]
|
||||
name = chapterEntry[ChapterTable.name]
|
||||
scanlator = chapterEntry[ChapterTable.scanlator]
|
||||
chapter_number = chapterEntry[ChapterTable.chapter_number]
|
||||
date_upload = chapterEntry[ChapterTable.date_upload]
|
||||
},
|
||||
).mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user