diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/opds/OpdsController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/opds/OpdsController.kt index 9018062d6..1fe2a8c37 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/opds/OpdsController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/opds/OpdsController.kt @@ -7,12 +7,14 @@ import org.gotson.komga.domain.model.BookSearch import org.gotson.komga.domain.model.Library import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.Series +import org.gotson.komga.domain.model.SeriesCollection import org.gotson.komga.domain.model.SeriesMetadata import org.gotson.komga.domain.model.SeriesSearch import org.gotson.komga.domain.persistence.BookMetadataRepository import org.gotson.komga.domain.persistence.BookRepository import org.gotson.komga.domain.persistence.LibraryRepository import org.gotson.komga.domain.persistence.MediaRepository +import org.gotson.komga.domain.persistence.SeriesCollectionRepository import org.gotson.komga.domain.persistence.SeriesMetadataRepository import org.gotson.komga.domain.persistence.SeriesRepository import org.gotson.komga.infrastructure.security.KomgaPrincipal @@ -54,17 +56,20 @@ private const val ROUTE_CATALOG = "catalog" private const val ROUTE_SERIES_ALL = "series" private const val ROUTE_SERIES_LATEST = "series/latest" private const val ROUTE_LIBRARIES_ALL = "libraries" +private const val ROUTE_COLLECTIONS_ALL = "collections" private const val ROUTE_SEARCH = "search" private const val ID_SERIES_ALL = "allSeries" private const val ID_SERIES_LATEST = "latestSeries" private const val ID_LIBRARIES_ALL = "allLibraries" +private const val ID_COLLECTIONS_ALL = "allCollections" @RestController @RequestMapping(value = [ROUTE_BASE], produces = [MediaType.APPLICATION_ATOM_XML_VALUE, MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE]) class OpdsController( servletContext: ServletContext, private val libraryRepository: LibraryRepository, + private val collectionRepository: SeriesCollectionRepository, private val seriesRepository: SeriesRepository, private val seriesMetadataRepository: SeriesMetadataRepository, private val bookRepository: BookRepository, @@ -113,6 +118,13 @@ class OpdsController( id = ID_LIBRARIES_ALL, content = "Browse by library", link = OpdsLinkFeedNavigation(OpdsLinkRel.SUBSECTION, "$routeBase$ROUTE_LIBRARIES_ALL") + ), + OpdsEntryNavigation( + title = "All collections", + updated = ZonedDateTime.now(), + id = ID_COLLECTIONS_ALL, + content = "Browse by collection", + link = OpdsLinkFeedNavigation(OpdsLinkRel.SUBSECTION, "$routeBase$ROUTE_COLLECTIONS_ALL") ) ) ) @@ -207,6 +219,29 @@ class OpdsController( ) } + @GetMapping(ROUTE_COLLECTIONS_ALL) + fun getCollections( + @AuthenticationPrincipal principal: KomgaPrincipal + ): OpdsFeed { + val collections = + if (principal.user.sharedAllLibraries) { + collectionRepository.findAll() + } else { + collectionRepository.findAllByLibraries(principal.user.sharedLibrariesIds, principal.user.sharedLibrariesIds) + } + return OpdsFeedNavigation( + id = ID_COLLECTIONS_ALL, + title = "All collections", + updated = ZonedDateTime.now(), + author = komgaAuthor, + links = listOf( + OpdsLinkFeedNavigation(OpdsLinkRel.SELF, "$routeBase$ROUTE_COLLECTIONS_ALL"), + linkStart + ), + entries = collections.map { it.toOpdsEntry() } + ) + } + @GetMapping("series/{id}") fun getOneSeries( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -268,6 +303,35 @@ class OpdsController( ) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @GetMapping("collections/{id}") + fun getOneCollection( + @AuthenticationPrincipal principal: KomgaPrincipal, + @PathVariable id: Long + ): OpdsFeed { + return collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { collection -> + val series = collection.seriesIds.mapNotNull { seriesRepository.findByIdOrNull(it) } + .map { SeriesWithInfo(it, seriesMetadataRepository.findById(it.id)) } + + val sorted = + if (!collection.ordered) series.sortedBy { it.metadata.titleSort } + else series + + val entries = sorted.map { it.toOpdsEntry() } + + OpdsFeedNavigation( + id = collection.id.toString(), + title = collection.name, + updated = collection.lastModifiedDate.atZone(ZoneId.systemDefault()) ?: ZonedDateTime.now(), + author = komgaAuthor, + links = listOf( + OpdsLinkFeedNavigation(OpdsLinkRel.SELF, "${routeBase}collections/$id"), + linkStart + ), + entries = entries + ) + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + } + private fun SeriesWithInfo.toOpdsEntry(): OpdsEntryNavigation = OpdsEntryNavigation( @@ -317,6 +381,16 @@ class OpdsController( ) } + private fun SeriesCollection.toOpdsEntry(): OpdsEntryNavigation { + return OpdsEntryNavigation( + title = name, + updated = lastModifiedDate.atZone(ZoneId.systemDefault()) ?: ZonedDateTime.now(), + id = id.toString(), + content = "", + link = OpdsLinkFeedNavigation(OpdsLinkRel.SUBSECTION, "${routeBase}collections/$id") + ) + } + private fun shouldPrependBookNumbers(userAgent: String) = userAgent.contains("chunky", ignoreCase = true)