mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-16 01:04:08 -05:00
Add support for OPDS v1.2 to browse stored CBZ files (#1257)
* Añadiendo algunos cambios iniciales para probar OPDS * Add suport to OPDS v1.2 * Added support for OPDS-PSE and reorganized controllers * Rename chapterIndex to chapterId in the API and controller, and update descriptions in OPDS * Refactor OPDS to use formatted timestamps and proxy thumbnail URLs * Refactor OPDS to use formatted timestamps and proxy thumbnail URLs * Update Manga API to download chapters cbz using only chapterId and improve chapter download query * Optimize OPDS queries * Update Manga API to download chapters cbz using only chapterId and improve chapter download query * Optimize OPDS queries * Use SourceDataClass to map sources and optimize thumbnail URL retrieval * Kotlin lint errors in ChapterDownloadHelper and Opds * Kotlin lint errors in ChapterDownloadHelper and Opds
This commit is contained in:
@@ -82,6 +82,7 @@ object MangaAPI {
|
||||
|
||||
path("chapter") {
|
||||
post("batch", MangaController.anyChapterBatch)
|
||||
get("{chapterId}/download", MangaController.downloadChapter)
|
||||
}
|
||||
|
||||
path("category") {
|
||||
|
||||
@@ -11,6 +11,7 @@ import io.javalin.http.HttpStatus
|
||||
import kotlinx.serialization.json.Json
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
|
||||
import suwayomi.tachidesk.manga.impl.Library
|
||||
import suwayomi.tachidesk.manga.impl.Manga
|
||||
import suwayomi.tachidesk.manga.impl.Page
|
||||
@@ -424,4 +425,29 @@ object MangaController {
|
||||
httpCode(HttpStatus.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
|
||||
val downloadChapter =
|
||||
handler(
|
||||
pathParam<Int>("chapterId"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Download chapter as CBZ")
|
||||
description("Get the CBZ file of the specified chapter")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, chapterId ->
|
||||
ctx.future {
|
||||
future { ChapterDownloadHelper.getCbzDownload(chapterId) }
|
||||
.thenApply { (inputStream, contentType, fileName) ->
|
||||
ctx.header("Content-Type", contentType)
|
||||
ctx.header("Content-Disposition", "attachment; filename=\"$fileName\"")
|
||||
ctx.result(inputStream)
|
||||
}
|
||||
}
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpStatus.OK)
|
||||
httpCode(HttpStatus.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package suwayomi.tachidesk.manga.impl
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.ChaptersFilesProvider
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.ArchiveProvider
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.FolderProvider
|
||||
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
|
||||
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
|
||||
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
object ChapterDownloadHelper {
|
||||
@@ -42,4 +47,27 @@ object ChapterDownloadHelper {
|
||||
if (!chapterFolder.exists() && serverConfig.downloadAsCbz.value) return ArchiveProvider(mangaId, chapterId)
|
||||
return FolderProvider(mangaId, chapterId)
|
||||
}
|
||||
|
||||
suspend fun getCbzDownload(chapterId: Int): Triple<InputStream, String, String> {
|
||||
val (chapterData, mangaTitle) =
|
||||
transaction {
|
||||
val row =
|
||||
(ChapterTable innerJoin MangaTable)
|
||||
.select(ChapterTable.columns + MangaTable.columns)
|
||||
.where { ChapterTable.id eq chapterId }
|
||||
.firstOrNull() ?: throw Exception("Chapter not found")
|
||||
val chapter = ChapterTable.toDataClass(row)
|
||||
val title = row[MangaTable.title]
|
||||
Pair(chapter, title)
|
||||
}
|
||||
|
||||
val provider = provider(chapterData.mangaId, chapterData.id)
|
||||
return if (provider is ArchiveProvider) {
|
||||
val cbzFile = File(getChapterCbzPath(chapterData.mangaId, chapterData.id))
|
||||
val fileName = "$mangaTitle - [${chapterData.scanlator}] ${chapterData.name}.cbz"
|
||||
Triple(cbzFile.inputStream(), "application/vnd.comicbook+zip", fileName)
|
||||
} else {
|
||||
throw IOException("Chapter not available as CBZ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package suwayomi.tachidesk.manga.model.dataclass
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import nl.adaptivity.xmlutil.serialization.XmlElement
|
||||
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
||||
import nl.adaptivity.xmlutil.serialization.XmlValue
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("feed", "", "")
|
||||
data class OpdsDataClass(
|
||||
@XmlElement(true)
|
||||
val id: String,
|
||||
@XmlElement(true)
|
||||
val title: String,
|
||||
@XmlElement(true)
|
||||
val icon: String? = null,
|
||||
@XmlElement(true)
|
||||
val updated: String, // ISO-8601
|
||||
@XmlElement(true)
|
||||
val author: Author? = null,
|
||||
@XmlElement(true)
|
||||
val links: List<Link>,
|
||||
@XmlElement(true)
|
||||
val entries: List<Entry>,
|
||||
@XmlSerialName("xmlns", "", "")
|
||||
val xmlns: String = "http://www.w3.org/2005/Atom",
|
||||
@XmlSerialName("xmlns:xsd", "", "")
|
||||
val xmlnsXsd: String = "http://www.w3.org/2001/XMLSchema",
|
||||
@XmlSerialName("xmlns:xsi", "", "")
|
||||
val xmlnsXsi: String = "http://www.w3.org/2001/XMLSchema-instance",
|
||||
@XmlSerialName("xmlns:opds", "", "")
|
||||
val xmlnsOpds: String = "http://opds-spec.org/2010/catalog",
|
||||
@XmlSerialName("xmlns:dcterms", "", "")
|
||||
val xmlnsDublinCore: String = "http://purl.org/dc/terms/",
|
||||
@XmlSerialName("xmlns:pse", "", "")
|
||||
val xmlnsPse: String = "http://vaemendis.net/opds-pse/ns",
|
||||
@XmlElement(true)
|
||||
@XmlSerialName("totalResults", "http://a9.com/-/spec/opensearch/1.1/", "")
|
||||
val totalResults: Long? = null,
|
||||
@XmlElement(true)
|
||||
@XmlSerialName("itemsPerPage", "http://a9.com/-/spec/opensearch/1.1/", "")
|
||||
val itemsPerPage: Int? = null,
|
||||
@XmlElement(true)
|
||||
@XmlSerialName("startIndex", "http://a9.com/-/spec/opensearch/1.1/", "")
|
||||
val startIndex: Int? = null,
|
||||
) {
|
||||
@Serializable
|
||||
@XmlSerialName("author", "", "")
|
||||
data class Author(
|
||||
@XmlElement(true)
|
||||
val name: String,
|
||||
@XmlElement(true)
|
||||
val uri: String? = null,
|
||||
@XmlElement(true)
|
||||
val email: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("link", "", "")
|
||||
data class Link(
|
||||
val rel: String,
|
||||
val href: String,
|
||||
val type: String? = null,
|
||||
val title: String? = null,
|
||||
@XmlSerialName("pse:count", "", "")
|
||||
val pseCount: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("entry", "", "")
|
||||
data class Entry(
|
||||
@XmlElement(true)
|
||||
val id: String,
|
||||
@XmlElement(true)
|
||||
val title: String,
|
||||
@XmlElement(true)
|
||||
val updated: String,
|
||||
@XmlElement(true)
|
||||
val summary: Summary? = null,
|
||||
@XmlElement(true)
|
||||
val content: Content? = null,
|
||||
@XmlElement(true)
|
||||
val link: List<Link>,
|
||||
@XmlElement(true)
|
||||
val authors: List<Author>? = null,
|
||||
@XmlElement(true)
|
||||
val categories: List<Category>? = null,
|
||||
@XmlElement(true)
|
||||
@XmlSerialName("language", "http://purl.org/dc/terms/", "dc")
|
||||
val extent: String? = null,
|
||||
@XmlElement(true)
|
||||
@XmlSerialName("format", "http://purl.org/dc/terms/format", "dc")
|
||||
val format: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("summary", "", "")
|
||||
data class Summary(
|
||||
val type: String = "text",
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("content", "", "")
|
||||
data class Content(
|
||||
val type: String = "text",
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("category", "", "")
|
||||
data class Category(
|
||||
val scheme: String? = null,
|
||||
val term: String,
|
||||
val label: String,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user