fix duplicates in v2 api for series release years

This commit is contained in:
Gauthier Roebroeck 2026-03-20 11:12:52 +08:00
parent a08c78c95f
commit ac2d3346b4
6 changed files with 81 additions and 35 deletions

View file

@ -234,7 +234,7 @@
"/api/v1/age-ratings": {
"get": {
"deprecated": true,
"description": "Use GET /v2/genres instead. Deprecated since 1.x.0",
"description": "Use GET /v2/age-ratings instead. Deprecated since 1.x.0",
"operationId": "getAgeRatings_1",
"parameters": [
{
@ -4760,7 +4760,7 @@
"/api/v1/publishers": {
"get": {
"deprecated": true,
"description": "Use GET /v2/genres instead. Deprecated since 1.x.0",
"description": "Use GET /v2/publishers instead. Deprecated since 1.x.0",
"operationId": "getPublishers_1",
"parameters": [
{
@ -6575,8 +6575,8 @@
"/api/v1/series/release-dates": {
"get": {
"deprecated": true,
"description": "Use GET /v2/genres instead. Deprecated since 1.x.0",
"operationId": "getSeriesReleaseDates_1",
"description": "Use GET /v2/series/release-years instead. Deprecated since 1.x.0",
"operationId": "getSeriesReleaseDates",
"parameters": [
{
"in": "query",
@ -7675,7 +7675,7 @@
"/api/v1/tags": {
"get": {
"deprecated": true,
"description": "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0",
"description": "Use GET /v2/tags instead. Deprecated since 1.x.0",
"operationId": "getTags_1",
"parameters": [
{
@ -7735,7 +7735,7 @@
"/api/v1/tags/book": {
"get": {
"deprecated": true,
"description": "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0",
"description": "Use GET /v2/tags instead. Deprecated since 1.x.0",
"operationId": "getBookTags",
"parameters": [
{
@ -7803,7 +7803,7 @@
"/api/v1/tags/series": {
"get": {
"deprecated": true,
"description": "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0",
"description": "Use GET /v2/tags instead. Deprecated since 1.x.0",
"operationId": "getSeriesTags",
"parameters": [
{
@ -8736,10 +8736,10 @@
]
}
},
"/api/v2/series/release-dates": {
"/api/v2/series/release-years": {
"get": {
"description": "Can be filtered by various criteria",
"operationId": "getSeriesReleaseDates",
"operationId": "getSeriesReleaseYears",
"parameters": [
{
"in": "query",
@ -8812,7 +8812,7 @@
"description": "Bad Request"
}
},
"summary": "List series release dates",
"summary": "List series release years",
"tags": [
"Referential metadata"
]
@ -15342,4 +15342,4 @@
]
}
]
}
}

View file

@ -228,7 +228,7 @@ interface ReferentialRepository {
filterOnLibraryIds: Collection<String>?,
): Set<LocalDate>
fun findSeriesReleaseDates(
fun findSeriesReleaseYears(
context: SearchContext,
filterBy: FilterBy?,
pageable: Pageable,

View file

@ -641,14 +641,60 @@ class ReferentialDao(
.orderBy(bma.RELEASE_DATE.desc())
.fetchSet(bma.RELEASE_DATE)
override fun findSeriesReleaseDates(
override fun findSeriesReleaseYears(
context: SearchContext,
filterBy: FilterBy?,
pageable: Pageable,
): Page<String> {
filterBy?.let { require(it.type in setOf(FilterByEntity.LIBRARY, FilterByEntity.COLLECTION)) }
return findGeneric(context, null, filterBy, pageable, bma, null, bma.SERIES_ID, { it?.releaseDate?.year?.toString() }, Sort.by(Order.desc("year")), listOf(bma.RELEASE_DATE), sortField = bma.RELEASE_DATE.desc())
val sortField = bma.RELEASE_DATE.desc()
val restrictionCondition = ContentRestrictionsSearchHelper(context.restrictions).toCondition()
val query =
dslRO
.selectDistinct(DSL.year(bma.RELEASE_DATE))
.from(bma)
.apply {
restrictionCondition.second.forEach { join ->
when (join) {
RequiredJoin.SeriesMetadata -> innerJoin(sd).on(bma.SERIES_ID.eq(sd.SERIES_ID))
// shouldn't be required
RequiredJoin.BookMetadata -> Unit
RequiredJoin.BookMetadataAggregation -> Unit
is RequiredJoin.Collection -> Unit
RequiredJoin.Media -> Unit
is RequiredJoin.ReadList -> Unit
is RequiredJoin.ReadProgress -> Unit
}
}
}.apply { if (!context.libraryIds.isNullOrEmpty() || filterBy?.type == FilterByEntity.LIBRARY) leftJoin(s).on(bma.SERIES_ID.eq(s.ID)) }
.apply { if (filterBy?.type == FilterByEntity.COLLECTION) leftJoin(cs).on(bma.SERIES_ID.eq(cs.SERIES_ID)) }
.apply {
if (filterBy?.type == FilterByEntity.READLIST)
leftJoin(b)
.on(bma.SERIES_ID.eq(b.SERIES_ID))
.leftJoin(rb)
.on(b.ID.eq(rb.BOOK_ID))
}.where(restrictionCondition.first)
.apply { context.libraryIds?.let { this.and(s.LIBRARY_ID.`in`(it)) } }
.apply {
filterBy?.let {
when (it.type) {
FilterByEntity.LIBRARY -> this.and(s.LIBRARY_ID.`in`(it.ids))
FilterByEntity.COLLECTION -> this.and(cs.COLLECTION_ID.`in`(it.ids))
FilterByEntity.SERIES -> this.and(bma.SERIES_ID.`in`(it.ids))
FilterByEntity.READLIST -> this.and(rb.READLIST_ID.`in`(it.ids))
}
}
}
val count = dslRO.fetchCount(query)
val items =
query
.orderBy(sortField)
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
.fetchArray(0)
.mapNotNull { it?.toString() }
return buildPage(items, pageable, count, Sort.by(Order.desc("year")))
}
@Deprecated("Use findSharingLabels instead")

View file

@ -88,7 +88,7 @@ class ReferentialV1Controller(
@GetMapping("tags")
@Deprecated("Use GET /v2/tags instead")
// TODO: add deprecation release
@Operation(summary = "List tags", description = "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
@Operation(summary = "List tags", description = "Use GET /v2/tags instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
fun getTags(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryIds: Set<String> = emptySet(),
@ -103,7 +103,7 @@ class ReferentialV1Controller(
@GetMapping("tags/book")
@Deprecated("Use GET /v2/tags instead")
// TODO: add deprecation release
@Operation(summary = "List book tags", description = "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
@Operation(summary = "List book tags", description = "Use GET /v2/tags instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
fun getBookTags(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "series_id", required = false) seriesId: String?,
@ -120,7 +120,7 @@ class ReferentialV1Controller(
@GetMapping("tags/series")
@Deprecated("Use GET /v2/tags instead")
// TODO: add deprecation release
@Operation(summary = "List series tags", description = "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
@Operation(summary = "List series tags", description = "Use GET /v2/tags instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
fun getSeriesTags(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryId: String?,
@ -150,7 +150,7 @@ class ReferentialV1Controller(
@GetMapping("publishers")
@Deprecated("Use GET /v2/publishers instead")
// TODO: add deprecation release
@Operation(summary = "List publishers", description = "Use GET /v2/genres instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
@Operation(summary = "List publishers", description = "Use GET /v2/publishers instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
fun getPublishers(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryIds: Set<String> = emptySet(),
@ -165,7 +165,7 @@ class ReferentialV1Controller(
@GetMapping("age-ratings")
@Deprecated("Use GET /v2/age-ratings instead")
// TODO: add deprecation release
@Operation(summary = "List age ratings", description = "Use GET /v2/genres instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
@Operation(summary = "List age ratings", description = "Use GET /v2/age-ratings instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
fun getAgeRatings(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryIds: Set<String> = emptySet(),
@ -178,9 +178,9 @@ class ReferentialV1Controller(
}.map { it?.toString() ?: "None" }.toSet()
@GetMapping("series/release-dates")
@Deprecated("Use GET /v2/age-ratings instead")
@Deprecated("Use GET /v2/series/release-years instead")
// TODO: add deprecation release
@Operation(summary = "List series release dates", description = "Use GET /v2/genres instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
@Operation(summary = "List series release dates", description = "Use GET /v2/series/release-years instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
fun getSeriesReleaseDates(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryIds: Set<String> = emptySet(),

View file

@ -292,9 +292,9 @@ class ReferentialV2Controller(
}
@PageableWithoutSortAsQueryParam
@GetMapping("series/release-dates")
@Operation(summary = "List series release dates", description = "Can be filtered by various criteria")
fun getSeriesReleaseDates(
@GetMapping("series/release-years")
@Operation(summary = "List series release years", description = "Can be filtered by various criteria")
fun getSeriesReleaseYears(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryIds: Set<String> = emptySet(),
@RequestParam(name = "collection_id", required = false) collectionIds: Set<String> = emptySet(),
@ -318,7 +318,7 @@ class ReferentialV2Controller(
else -> null
}
return referentialRepository.findSeriesReleaseDates(SearchContext(principal.user), filterBy, pageRequest)
return referentialRepository.findSeriesReleaseYears(SearchContext(principal.user), filterBy, pageRequest)
}
@PageableWithoutSortAsQueryParam

View file

@ -98,7 +98,7 @@ class ReferentialDaoTest(
makeBook("1", libraryId = library1.id, seriesId = series1.id).let { book ->
seriesLifecycle.addBooks(series1, listOf(book))
bookMetadataRepository.findById(book.id).let {
bookMetadataRepository.update(it.copy(authors = listOf(Author("item1", "writer")), releaseDate = LocalDate.of(2001, 1, 1), tags = setOf("bt1")))
bookMetadataRepository.update(it.copy(authors = listOf(Author("item1", "writer")), releaseDate = LocalDate.of(2002, 1, 1), tags = setOf("bt1")))
}
seriesMetadataRepository.findById(series1.id).let {
seriesMetadataRepository.update(it.copy(genres = setOf("item1"), sharingLabels = setOf("item1"), language = "fr", publisher = "item1", ageRating = 18, tags = setOf("st1")))
@ -109,7 +109,7 @@ class ReferentialDaoTest(
makeBook("2", libraryId = library2.id, seriesId = series2.id).let { book ->
seriesLifecycle.addBooks(series2, listOf(book))
bookMetadataRepository.findById(book.id).let {
bookMetadataRepository.update(it.copy(authors = listOf(Author("item2", "inker")), releaseDate = LocalDate.of(2002, 1, 1), tags = setOf("bt2")))
bookMetadataRepository.update(it.copy(authors = listOf(Author("item2", "inker")), releaseDate = LocalDate.of(2002, 2, 1), tags = setOf("bt2")))
}
seriesMetadataRepository.findById(series2.id).let {
seriesMetadataRepository.update(it.copy(genres = setOf("item2"), sharingLabels = setOf("item2"), language = "en", publisher = "item2", ageRating = 19, tags = setOf("st2")))
@ -729,11 +729,11 @@ class ReferentialDaoTest(
}
@Nested
inner class SeriesReleaseDate {
inner class SeriesReleaseYear {
@Test
fun `given filter by library when getting series release dates then only matching series release dates are returned`() {
val context = SearchContext(userAll)
val items = referentialDao.findSeriesReleaseDates(context, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), Pageable.unpaged()).content
val items = referentialDao.findSeriesReleaseYears(context, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), Pageable.unpaged()).content
assertThat(items).containsExactly("2002")
}
@ -741,23 +741,23 @@ class ReferentialDaoTest(
@Test
fun `given user without restrictions when getting series release dates then all series release dates are returned`() {
val context = SearchContext(userAll)
val items = referentialDao.findSeriesReleaseDates(context, null, Pageable.unpaged()).content
val items = referentialDao.findSeriesReleaseYears(context, null, Pageable.unpaged()).content
assertThat(items).containsExactly("2004", "2003", "2002", "2001")
assertThat(items).containsExactly("2004", "2003", "2002")
}
@Test
fun `given user with restricted library access when getting series release dates then only allowed series release dates are returned`() {
val context = SearchContext(userLib1)
val items = referentialDao.findSeriesReleaseDates(context, null, Pageable.unpaged()).content
val items = referentialDao.findSeriesReleaseYears(context, null, Pageable.unpaged()).content
assertThat(items).containsExactly("2004", "2003", "2001")
assertThat(items).containsExactly("2004", "2003", "2002")
}
@Test
fun `given user with restricted label access when getting series release dates then only allowed series release dates are returned`() {
val context = SearchContext(userLabelAllow)
val items = referentialDao.findSeriesReleaseDates(context, null, Pageable.unpaged()).content
val items = referentialDao.findSeriesReleaseYears(context, null, Pageable.unpaged()).content
assertThat(items).containsExactly("2003")
}
@ -765,7 +765,7 @@ class ReferentialDaoTest(
@Test
fun `given user with restricted age access when getting series release dates then only allowed series release dates are returned`() {
val context = SearchContext(userAge10)
val items = referentialDao.findSeriesReleaseDates(context, null, Pageable.unpaged()).content
val items = referentialDao.findSeriesReleaseYears(context, null, Pageable.unpaged()).content
assertThat(items).containsExactly("2004")
}