feat: add Series Metadata status

updatable via API

linked to #48
This commit is contained in:
Gauthier Roebroeck 2020-01-21 16:31:07 +08:00
parent f046bab6ab
commit f5221420fd
9 changed files with 168 additions and 44 deletions

View File

@ -0,0 +1,23 @@
package db.migration
import org.flywaydb.core.api.migration.BaseJavaMigration
import org.flywaydb.core.api.migration.Context
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.datasource.SingleConnectionDataSource
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class V20200121154334__create_series_metadata_from_series : BaseJavaMigration() {
override fun migrate(context: Context) {
val jdbcTemplate = JdbcTemplate(SingleConnectionDataSource(context.connection, true))
val seriesIds = jdbcTemplate.queryForList("SELECT id FROM series", Long::class.java)
val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"))
seriesIds.forEach { seriesId ->
val metadataId = jdbcTemplate.queryForObject("SELECT NEXTVAL('hibernate_sequence')", Int::class.java)
jdbcTemplate.execute("INSERT INTO series_metadata (ID, CREATED_DATE, LAST_MODIFIED_DATE, STATUS) VALUES ($metadataId, '$now', '$now', 'ONGOING')")
jdbcTemplate.execute("UPDATE series SET metadata_id = $metadataId WHERE id = $seriesId")
}
}
}

View File

@ -16,6 +16,7 @@ import javax.persistence.Id
import javax.persistence.JoinColumn
import javax.persistence.ManyToOne
import javax.persistence.OneToMany
import javax.persistence.OneToOne
import javax.persistence.Table
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
@ -63,6 +64,10 @@ class Series(
_books.forEachIndexed { index, book -> book.number = index + 1F }
}
@OneToOne(optional = false, orphanRemoval = true, cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
@JoinColumn(name = "metadata_id", nullable = false)
var metadata: SeriesMetadata = SeriesMetadata()
init {
this.books = books.toList()
}

View File

@ -0,0 +1,32 @@
package org.gotson.komga.domain.model
import org.hibernate.annotations.Cache
import org.hibernate.annotations.CacheConcurrencyStrategy
import javax.persistence.Cacheable
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.EnumType
import javax.persistence.Enumerated
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.persistence.Table
@Entity
@Table(name = "series_metadata")
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "cache.series_metadata")
class SeriesMetadata(
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
var status: Status = Status.ONGOING
) : AuditableEntity() {
@Id
@GeneratedValue
@Column(name = "id", nullable = false, unique = true)
val id: Long = 0
enum class Status {
ENDED, ONGOING, ABANDONED, HIATUS
}
}

View File

@ -13,6 +13,7 @@ import org.gotson.komga.domain.persistence.SeriesRepository
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.interfaces.rest.dto.BookDto
import org.gotson.komga.interfaces.rest.dto.SeriesDto
import org.gotson.komga.interfaces.rest.dto.SeriesMetadataUpdateDto
import org.gotson.komga.interfaces.rest.dto.toDto
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageRequest
@ -26,8 +27,10 @@ import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus
@ -49,16 +52,16 @@ class SeriesController(
@GetMapping
fun getAllSeries(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "search", required = false) searchTerm: String?,
@RequestParam(name = "library_id", required = false) libraryIds: List<Long>?,
page: Pageable
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "search", required = false) searchTerm: String?,
@RequestParam(name = "library_id", required = false) libraryIds: List<Long>?,
page: Pageable
): Page<SeriesDto> {
val pageRequest = PageRequest.of(
page.pageNumber,
page.pageSize,
if (page.sort.isSorted) page.sort
else Sort.by(Sort.Order.asc("name").ignoreCase())
page.pageNumber,
page.pageSize,
if (page.sort.isSorted) page.sort
else Sort.by(Sort.Order.asc("name").ignoreCase())
)
return mutableListOf<Specification<Series>>().let { specs ->
@ -94,13 +97,13 @@ class SeriesController(
// all updated series, whether newly added or updated
@GetMapping("/latest")
fun getLatestSeries(
@AuthenticationPrincipal principal: KomgaPrincipal,
page: Pageable
@AuthenticationPrincipal principal: KomgaPrincipal,
page: Pageable
): Page<SeriesDto> {
val pageRequest = PageRequest.of(
page.pageNumber,
page.pageSize,
Sort.by(Sort.Direction.DESC, "lastModifiedDate")
page.pageNumber,
page.pageSize,
Sort.by(Sort.Direction.DESC, "lastModifiedDate")
)
return if (principal.user.sharedAllLibraries) {
@ -113,13 +116,13 @@ class SeriesController(
// new series only, doesn't contain existing updated series
@GetMapping("/new")
fun getNewSeries(
@AuthenticationPrincipal principal: KomgaPrincipal,
page: Pageable
@AuthenticationPrincipal principal: KomgaPrincipal,
page: Pageable
): Page<SeriesDto> {
val pageRequest = PageRequest.of(
page.pageNumber,
page.pageSize,
Sort.by(Sort.Direction.DESC, "createdDate")
page.pageNumber,
page.pageSize,
Sort.by(Sort.Direction.DESC, "createdDate")
)
return if (principal.user.sharedAllLibraries) {
@ -132,13 +135,13 @@ class SeriesController(
// updated series only, doesn't contain new series
@GetMapping("/updated")
fun getUpdatedSeries(
@AuthenticationPrincipal principal: KomgaPrincipal,
page: Pageable
@AuthenticationPrincipal principal: KomgaPrincipal,
page: Pageable
): Page<SeriesDto> {
val pageRequest = PageRequest.of(
page.pageNumber,
page.pageSize,
Sort.by(Sort.Direction.DESC, "lastModifiedDate")
page.pageNumber,
page.pageSize,
Sort.by(Sort.Direction.DESC, "lastModifiedDate")
)
return if (principal.user.sharedAllLibraries) {
@ -150,41 +153,41 @@ class SeriesController(
@GetMapping("{seriesId}")
fun getOneSeries(
@AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable(name = "seriesId") id: Long
@AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable(name = "seriesId") id: Long
): SeriesDto =
seriesRepository.findByIdOrNull(id)?.let {
if (!principal.user.canAccessSeries(it)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
it.toDto(includeUrl = principal.user.isAdmin())
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
seriesRepository.findByIdOrNull(id)?.let {
if (!principal.user.canAccessSeries(it)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
it.toDto(includeUrl = principal.user.isAdmin())
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@GetMapping(value = ["{seriesId}/thumbnail"], produces = [MediaType.IMAGE_JPEG_VALUE])
fun getSeriesThumbnail(
@AuthenticationPrincipal principal: KomgaPrincipal,
request: WebRequest,
@PathVariable(name = "seriesId") id: Long
@AuthenticationPrincipal principal: KomgaPrincipal,
request: WebRequest,
@PathVariable(name = "seriesId") id: Long
): ResponseEntity<ByteArray> =
seriesRepository.findByIdOrNull(id)?.let { series ->
if (!principal.user.canAccessSeries(series)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
seriesRepository.findByIdOrNull(id)?.let { series ->
if (!principal.user.canAccessSeries(series)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
series.books.minBy { it.number }?.let { firstBook ->
bookController.getBookThumbnail(principal, request, firstBook.id)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
series.books.minBy { it.number }?.let { firstBook ->
bookController.getBookThumbnail(principal, request, firstBook.id)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@GetMapping("{seriesId}/books")
fun getAllBooksBySeries(
@AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable(name = "seriesId") id: Long,
@RequestParam(name = "media_status", required = false) mediaStatus: List<Media.Status>?,
page: Pageable
@AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable(name = "seriesId") id: Long,
@RequestParam(name = "media_status", required = false) mediaStatus: List<Media.Status>?,
page: Pageable
): Page<BookDto> {
seriesRepository.findByIdOrNull(id)?.let {
if (!principal.user.canAccessSeries(it)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
val pageRequest = PageRequest.of(
page.pageNumber,
page.pageNumber,
page.pageSize,
if (page.sort.isSorted) page.sort
else Sort.by(Sort.Order.asc("number"))
@ -208,4 +211,16 @@ class SeriesController(
}
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@PatchMapping("{seriesId}/metadata")
@PreAuthorize("hasRole('ROLE_ADMIN')")
fun updateMetadata(
@PathVariable seriesId: Long,
@RequestBody newMetadata: SeriesMetadataUpdateDto
): SeriesDto =
seriesRepository.findByIdOrNull(seriesId)?.let { series ->
newMetadata.status?.let { series.metadata.status = newMetadata.status }
seriesRepository.save(series).toDto(includeUrl = true)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}

View File

@ -15,7 +15,16 @@ data class SeriesDto(
val lastModified: LocalDateTime?,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val fileLastModified: LocalDateTime,
val booksCount: Int
val booksCount: Int,
val metadata: SeriesMetadataDto
)
data class SeriesMetadataDto(
val status: String,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val created: LocalDateTime?,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val lastModified: LocalDateTime?
)
fun Series.toDto(includeUrl: Boolean) = SeriesDto(
@ -26,5 +35,10 @@ fun Series.toDto(includeUrl: Boolean) = SeriesDto(
created = createdDate?.toUTC(),
lastModified = lastModifiedDate?.toUTC(),
fileLastModified = fileLastModified.toUTC(),
booksCount = books.size
booksCount = books.size,
metadata = SeriesMetadataDto(
status = metadata.status.name,
created = metadata.createdDate?.toUTC(),
lastModified = metadata.lastModifiedDate?.toUTC()
)
)

View File

@ -0,0 +1,7 @@
package org.gotson.komga.interfaces.rest.dto
import org.gotson.komga.domain.model.SeriesMetadata
data class SeriesMetadataUpdateDto(
val status: SeriesMetadata.Status?
)

View File

@ -65,6 +65,17 @@ caffeine.jcache {
}
}
cache.series_metadata {
monitoring {
statistics = true
}
policy {
maximum {
size = 500
}
}
}
default-update-timestamps-region {
monitoring {
statistics = true

View File

@ -0,0 +1,15 @@
create table series_metadata
(
id bigint not null,
created_date timestamp not null,
last_modified_date timestamp not null,
status varchar not null,
primary key (id)
);
alter table series
add (metadata_id bigint);
alter table series
add constraint fk_series_series_metadata_metadata_id foreign key (metadata_id) references series_metadata (id);

View File

@ -0,0 +1,2 @@
alter table series
alter column metadata_id set not null;