mirror of
https://github.com/gotson/komga.git
synced 2025-01-09 12:18:03 +08:00
parent
f046bab6ab
commit
f5221420fd
@ -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.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()
|
||||
}
|
||||
|
@ -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.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)
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
)
|
||||
|
@ -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 {
|
||||
monitoring {
|
||||
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 New Issue
Block a user