mirror of
https://github.com/gotson/komga.git
synced 2026-04-16 12:01:09 +02:00
parent
f046bab6ab
commit
f5221420fd
9 changed files with 168 additions and 44 deletions
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ import javax.persistence.Id
|
||||||
import javax.persistence.JoinColumn
|
import javax.persistence.JoinColumn
|
||||||
import javax.persistence.ManyToOne
|
import javax.persistence.ManyToOne
|
||||||
import javax.persistence.OneToMany
|
import javax.persistence.OneToMany
|
||||||
|
import javax.persistence.OneToOne
|
||||||
import javax.persistence.Table
|
import javax.persistence.Table
|
||||||
import javax.validation.constraints.NotBlank
|
import javax.validation.constraints.NotBlank
|
||||||
import javax.validation.constraints.NotNull
|
import javax.validation.constraints.NotNull
|
||||||
|
|
@ -63,6 +64,10 @@ class Series(
|
||||||
_books.forEachIndexed { index, book -> book.number = index + 1F }
|
_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 {
|
init {
|
||||||
this.books = books.toList()
|
this.books = books.toList()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import org.gotson.komga.domain.persistence.SeriesRepository
|
||||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||||
import org.gotson.komga.interfaces.rest.dto.BookDto
|
import org.gotson.komga.interfaces.rest.dto.BookDto
|
||||||
import org.gotson.komga.interfaces.rest.dto.SeriesDto
|
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.gotson.komga.interfaces.rest.dto.toDto
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
import org.springframework.data.domain.PageRequest
|
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.access.prepost.PreAuthorize
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
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.PathVariable
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
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.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus
|
import org.springframework.web.bind.annotation.ResponseStatus
|
||||||
|
|
@ -49,16 +52,16 @@ class SeriesController(
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
fun getAllSeries(
|
fun getAllSeries(
|
||||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
@RequestParam(name = "search", required = false) searchTerm: String?,
|
@RequestParam(name = "search", required = false) searchTerm: String?,
|
||||||
@RequestParam(name = "library_id", required = false) libraryIds: List<Long>?,
|
@RequestParam(name = "library_id", required = false) libraryIds: List<Long>?,
|
||||||
page: Pageable
|
page: Pageable
|
||||||
): Page<SeriesDto> {
|
): Page<SeriesDto> {
|
||||||
val pageRequest = PageRequest.of(
|
val pageRequest = PageRequest.of(
|
||||||
page.pageNumber,
|
page.pageNumber,
|
||||||
page.pageSize,
|
page.pageSize,
|
||||||
if (page.sort.isSorted) page.sort
|
if (page.sort.isSorted) page.sort
|
||||||
else Sort.by(Sort.Order.asc("name").ignoreCase())
|
else Sort.by(Sort.Order.asc("name").ignoreCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
return mutableListOf<Specification<Series>>().let { specs ->
|
return mutableListOf<Specification<Series>>().let { specs ->
|
||||||
|
|
@ -94,13 +97,13 @@ class SeriesController(
|
||||||
// all updated series, whether newly added or updated
|
// all updated series, whether newly added or updated
|
||||||
@GetMapping("/latest")
|
@GetMapping("/latest")
|
||||||
fun getLatestSeries(
|
fun getLatestSeries(
|
||||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
page: Pageable
|
page: Pageable
|
||||||
): Page<SeriesDto> {
|
): Page<SeriesDto> {
|
||||||
val pageRequest = PageRequest.of(
|
val pageRequest = PageRequest.of(
|
||||||
page.pageNumber,
|
page.pageNumber,
|
||||||
page.pageSize,
|
page.pageSize,
|
||||||
Sort.by(Sort.Direction.DESC, "lastModifiedDate")
|
Sort.by(Sort.Direction.DESC, "lastModifiedDate")
|
||||||
)
|
)
|
||||||
|
|
||||||
return if (principal.user.sharedAllLibraries) {
|
return if (principal.user.sharedAllLibraries) {
|
||||||
|
|
@ -113,13 +116,13 @@ class SeriesController(
|
||||||
// new series only, doesn't contain existing updated series
|
// new series only, doesn't contain existing updated series
|
||||||
@GetMapping("/new")
|
@GetMapping("/new")
|
||||||
fun getNewSeries(
|
fun getNewSeries(
|
||||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
page: Pageable
|
page: Pageable
|
||||||
): Page<SeriesDto> {
|
): Page<SeriesDto> {
|
||||||
val pageRequest = PageRequest.of(
|
val pageRequest = PageRequest.of(
|
||||||
page.pageNumber,
|
page.pageNumber,
|
||||||
page.pageSize,
|
page.pageSize,
|
||||||
Sort.by(Sort.Direction.DESC, "createdDate")
|
Sort.by(Sort.Direction.DESC, "createdDate")
|
||||||
)
|
)
|
||||||
|
|
||||||
return if (principal.user.sharedAllLibraries) {
|
return if (principal.user.sharedAllLibraries) {
|
||||||
|
|
@ -132,13 +135,13 @@ class SeriesController(
|
||||||
// updated series only, doesn't contain new series
|
// updated series only, doesn't contain new series
|
||||||
@GetMapping("/updated")
|
@GetMapping("/updated")
|
||||||
fun getUpdatedSeries(
|
fun getUpdatedSeries(
|
||||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
page: Pageable
|
page: Pageable
|
||||||
): Page<SeriesDto> {
|
): Page<SeriesDto> {
|
||||||
val pageRequest = PageRequest.of(
|
val pageRequest = PageRequest.of(
|
||||||
page.pageNumber,
|
page.pageNumber,
|
||||||
page.pageSize,
|
page.pageSize,
|
||||||
Sort.by(Sort.Direction.DESC, "lastModifiedDate")
|
Sort.by(Sort.Direction.DESC, "lastModifiedDate")
|
||||||
)
|
)
|
||||||
|
|
||||||
return if (principal.user.sharedAllLibraries) {
|
return if (principal.user.sharedAllLibraries) {
|
||||||
|
|
@ -150,41 +153,41 @@ class SeriesController(
|
||||||
|
|
||||||
@GetMapping("{seriesId}")
|
@GetMapping("{seriesId}")
|
||||||
fun getOneSeries(
|
fun getOneSeries(
|
||||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
@PathVariable(name = "seriesId") id: Long
|
@PathVariable(name = "seriesId") id: Long
|
||||||
): SeriesDto =
|
): SeriesDto =
|
||||||
seriesRepository.findByIdOrNull(id)?.let {
|
seriesRepository.findByIdOrNull(id)?.let {
|
||||||
if (!principal.user.canAccessSeries(it)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
|
if (!principal.user.canAccessSeries(it)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
|
||||||
it.toDto(includeUrl = principal.user.isAdmin())
|
it.toDto(includeUrl = principal.user.isAdmin())
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
@GetMapping(value = ["{seriesId}/thumbnail"], produces = [MediaType.IMAGE_JPEG_VALUE])
|
@GetMapping(value = ["{seriesId}/thumbnail"], produces = [MediaType.IMAGE_JPEG_VALUE])
|
||||||
fun getSeriesThumbnail(
|
fun getSeriesThumbnail(
|
||||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
request: WebRequest,
|
request: WebRequest,
|
||||||
@PathVariable(name = "seriesId") id: Long
|
@PathVariable(name = "seriesId") id: Long
|
||||||
): ResponseEntity<ByteArray> =
|
): ResponseEntity<ByteArray> =
|
||||||
seriesRepository.findByIdOrNull(id)?.let { series ->
|
seriesRepository.findByIdOrNull(id)?.let { series ->
|
||||||
if (!principal.user.canAccessSeries(series)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
|
if (!principal.user.canAccessSeries(series)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
series.books.minBy { it.number }?.let { firstBook ->
|
series.books.minBy { it.number }?.let { firstBook ->
|
||||||
bookController.getBookThumbnail(principal, request, firstBook.id)
|
bookController.getBookThumbnail(principal, request, firstBook.id)
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
@GetMapping("{seriesId}/books")
|
@GetMapping("{seriesId}/books")
|
||||||
fun getAllBooksBySeries(
|
fun getAllBooksBySeries(
|
||||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
@PathVariable(name = "seriesId") id: Long,
|
@PathVariable(name = "seriesId") id: Long,
|
||||||
@RequestParam(name = "media_status", required = false) mediaStatus: List<Media.Status>?,
|
@RequestParam(name = "media_status", required = false) mediaStatus: List<Media.Status>?,
|
||||||
page: Pageable
|
page: Pageable
|
||||||
): Page<BookDto> {
|
): Page<BookDto> {
|
||||||
seriesRepository.findByIdOrNull(id)?.let {
|
seriesRepository.findByIdOrNull(id)?.let {
|
||||||
if (!principal.user.canAccessSeries(it)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
|
if (!principal.user.canAccessSeries(it)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
val pageRequest = PageRequest.of(
|
val pageRequest = PageRequest.of(
|
||||||
page.pageNumber,
|
page.pageNumber,
|
||||||
page.pageSize,
|
page.pageSize,
|
||||||
if (page.sort.isSorted) page.sort
|
if (page.sort.isSorted) page.sort
|
||||||
else Sort.by(Sort.Order.asc("number"))
|
else Sort.by(Sort.Order.asc("number"))
|
||||||
|
|
@ -208,4 +211,16 @@ class SeriesController(
|
||||||
}
|
}
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: 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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,16 @@ data class SeriesDto(
|
||||||
val lastModified: LocalDateTime?,
|
val lastModified: LocalDateTime?,
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||||
val fileLastModified: LocalDateTime,
|
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(
|
fun Series.toDto(includeUrl: Boolean) = SeriesDto(
|
||||||
|
|
@ -26,5 +35,10 @@ fun Series.toDto(includeUrl: Boolean) = SeriesDto(
|
||||||
created = createdDate?.toUTC(),
|
created = createdDate?.toUTC(),
|
||||||
lastModified = lastModifiedDate?.toUTC(),
|
lastModified = lastModifiedDate?.toUTC(),
|
||||||
fileLastModified = fileLastModified.toUTC(),
|
fileLastModified = fileLastModified.toUTC(),
|
||||||
booksCount = books.size
|
booksCount = books.size,
|
||||||
|
metadata = SeriesMetadataDto(
|
||||||
|
status = metadata.status.name,
|
||||||
|
created = metadata.createdDate?.toUTC(),
|
||||||
|
lastModified = metadata.lastModifiedDate?.toUTC()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
)
|
||||||
|
|
@ -65,6 +65,17 @@ caffeine.jcache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cache.series_metadata {
|
||||||
|
monitoring {
|
||||||
|
statistics = true
|
||||||
|
}
|
||||||
|
policy {
|
||||||
|
maximum {
|
||||||
|
size = 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default-update-timestamps-region {
|
default-update-timestamps-region {
|
||||||
monitoring {
|
monitoring {
|
||||||
statistics = true
|
statistics = true
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
alter table series
|
||||||
|
alter column metadata_id set not null;
|
||||||
Loading…
Reference in a new issue