Skip to content

Commit

Permalink
feat(api): support custom covers for series
Browse files Browse the repository at this point in the history
Co-authored-by: Gauthier <gotson@users.noreply.github.com>
Co-authored-by: Gauthier Roebroeck <gauthier.roebroeck@gmail.com>
  • Loading branch information
3 people committed Sep 6, 2021
1 parent 1ba6822 commit d7470dd
Show file tree
Hide file tree
Showing 14 changed files with 253 additions and 38 deletions.
2 changes: 1 addition & 1 deletion komga-webui/src/components/ItemCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export default Vue.extend({
}
},
thumbnailSeriesAdded(event: ThumbnailSeriesSseDto) {
if (this.thumbnailError && (this.computedItem.type() === ItemTypes.SERIES && event.seriesId === this.item.id)) {
if (this.computedItem.type() === ItemTypes.SERIES && event.seriesId === this.item.id) {
this.thumbnailCacheBust = '?' + this.$_.random(1000)
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
ALTER TABLE THUMBNAIL_SERIES RENAME TO TMP_THUMBNAIL_SERIES;

CREATE TABLE THUMBNAIL_SERIES
(
ID varchar NOT NULL PRIMARY KEY,
URL varchar NULL DEFAULT NULL,
SELECTED boolean NOT NULL DEFAULT 0,
THUMBNAIL blob NULL DEFAULT NULL,
TYPE varchar not null,
CREATED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
LAST_MODIFIED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
SERIES_ID varchar NOT NULL,
FOREIGN KEY (SERIES_ID) REFERENCES SERIES (ID)
);

INSERT INTO THUMBNAIL_SERIES(ID, URL, SELECTED, CREATED_DATE, LAST_MODIFIED_DATE, SERIES_ID, TYPE)
SELECT ID, URL, SELECTED, CREATED_DATE, LAST_MODIFIED_DATE, SERIES_ID, "SIDECAR" AS TYPE
FROM TMP_THUMBNAIL_SERIES;

DROP TABLE TMP_THUMBNAIL_SERIES;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.gotson.komga.domain.model

enum class MarkSelectedPreference {
NO, YES, IF_NONE_EXIST
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package org.gotson.komga.domain.model
import com.github.f4b6a3.tsid.TsidCreator
import java.io.Serializable
import java.net.URL
import java.nio.file.Files
import java.nio.file.Paths
import java.time.LocalDateTime

data class ThumbnailBook(
Expand Down Expand Up @@ -51,4 +53,9 @@ data class ThumbnailBook(
result = 31 * result + lastModifiedDate.hashCode()
return result
}

fun exists(): Boolean {
if (url != null) return Files.exists(Paths.get(url.toURI()))
return thumbnail != null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,59 @@ package org.gotson.komga.domain.model
import com.github.f4b6a3.tsid.TsidCreator
import java.io.Serializable
import java.net.URL
import java.nio.file.Files
import java.nio.file.Paths
import java.time.LocalDateTime

data class ThumbnailSeries(
val url: URL,
val thumbnail: ByteArray? = null,
val url: URL? = null,
val selected: Boolean = false,
val type: Type,

val id: String = TsidCreator.getTsid256().toString(),
val seriesId: String = "",

override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
) : Auditable(), Serializable
) : Auditable(), Serializable {
enum class Type {
SIDECAR, USER_UPLOADED
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ThumbnailSeries) return false

if (thumbnail != null) {
if (other.thumbnail == null) return false
if (!thumbnail.contentEquals(other.thumbnail)) return false
} else if (other.thumbnail != null) return false
if (url != other.url) return false
if (selected != other.selected) return false
if (type != other.type) return false
if (id != other.id) return false
if (seriesId != other.seriesId) return false
if (createdDate != other.createdDate) return false
if (lastModifiedDate != other.lastModifiedDate) return false

return true
}

override fun hashCode(): Int {
var result = thumbnail?.contentHashCode() ?: 0
result = 31 * result + (url?.hashCode() ?: 0)
result = 31 * result + selected.hashCode()
result = 31 * result + type.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + seriesId.hashCode()
result = 31 * result + createdDate.hashCode()
result = 31 * result + lastModifiedDate.hashCode()
return result
}

fun exists(): Boolean {
if (url != null) return Files.exists(Paths.get(url.toURI()))
return thumbnail != null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package org.gotson.komga.domain.persistence
import org.gotson.komga.domain.model.ThumbnailSeries

interface ThumbnailSeriesRepository {
fun findByIdOrNull(thumbnailId: String): ThumbnailSeries?

fun findSelectedBySeriesIdOrNull(seriesId: String): ThumbnailSeries?

fun findAllBySeriesId(seriesId: String): Collection<ThumbnailSeries>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import org.gotson.komga.infrastructure.image.ImageType
import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionTemplate
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.time.LocalDateTime

private val logger = KotlinLogging.logger {}
Expand Down Expand Up @@ -161,15 +159,6 @@ class BookLifecycle(
}
}

private fun ThumbnailBook.exists(): Boolean {
if (type == ThumbnailBook.Type.SIDECAR) {
if (url != null)
return Files.exists(Paths.get(url.toURI()))
return false
}
return true
}

@Throws(
ImageConversionException::class,
MediaNotReadyException::class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.gotson.komga.domain.service

import mu.KotlinLogging
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.MarkSelectedPreference
import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.infrastructure.metadata.localartwork.LocalArtworkProvider
Expand Down Expand Up @@ -35,7 +36,7 @@ class LocalArtworkLifecycle(

if (library.importLocalArtwork)
localArtworkProvider.getSeriesThumbnails(series).forEach {
seriesLifecycle.addThumbnailForSeries(it)
seriesLifecycle.addThumbnailForSeries(it, if (it.selected) MarkSelectedPreference.IF_NONE_EXIST else MarkSelectedPreference.NO)
}
else
logger.info { "Library is not set to import local artwork, skipping" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import org.gotson.komga.domain.model.BookMetadataPatchCapability
import org.gotson.komga.domain.model.DomainEvent
import org.gotson.komga.domain.model.KomgaUser
import org.gotson.komga.domain.model.Library
import org.gotson.komga.domain.model.MarkSelectedPreference
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.ReadProgress
import org.gotson.komga.domain.model.Series
Expand All @@ -30,8 +31,6 @@ import org.gotson.komga.domain.persistence.ThumbnailSeriesRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionTemplate
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.time.LocalDateTime

private val logger = KotlinLogging.logger {}
Expand Down Expand Up @@ -194,20 +193,32 @@ class SeriesLifecycle(
eventPublisher.publishEvent(DomainEvent.ReadProgressSeriesDeleted(seriesId, user.id))
}

fun getThumbnail(seriesId: String): ThumbnailSeries? {
fun getSelectedThumbnail(seriesId: String): ThumbnailSeries? {
val selected = thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(seriesId)

if (selected == null || !selected.exists()) {
if (selected == null || (selected.type == ThumbnailSeries.Type.SIDECAR && !selected.exists())) {
thumbnailsHouseKeeping(seriesId)
return thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(seriesId)
}

return selected
}

private fun getBytesFromThumbnailSeries(thumbnail: ThumbnailSeries): ByteArray? =
when {
thumbnail.thumbnail != null -> thumbnail.thumbnail
thumbnail.url != null -> File(thumbnail.url.toURI()).readBytes()
else -> null
}

fun getThumbnailBytesByThumbnailId(thumbnailId: String): ByteArray? =
thumbnailsSeriesRepository.findByIdOrNull(thumbnailId)?.let {
getBytesFromThumbnailSeries(it)
}

fun getThumbnailBytes(seriesId: String, userId: String): ByteArray? {
getThumbnail(seriesId)?.let {
return File(it.url.toURI()).readBytes()
getSelectedThumbnail(seriesId)?.let {
return getBytesFromThumbnailSeries(it)
}

seriesRepository.findByIdOrNull(seriesId)?.let { series ->
Expand All @@ -225,19 +236,31 @@ class SeriesLifecycle(
return null
}

fun addThumbnailForSeries(thumbnail: ThumbnailSeries) {
fun addThumbnailForSeries(thumbnail: ThumbnailSeries, markSelected: MarkSelectedPreference) {
// delete existing thumbnail with the same url
thumbnailsSeriesRepository.findAllBySeriesId(thumbnail.seriesId)
.filter { it.url == thumbnail.url }
.forEach {
thumbnailsSeriesRepository.delete(it.id)
}
thumbnailsSeriesRepository.insert(thumbnail)

eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesAdded(thumbnail))
if (thumbnail.url != null) {
thumbnailsSeriesRepository.findAllBySeriesId(thumbnail.seriesId)
.filter { it.url == thumbnail.url }
.forEach {
thumbnailsSeriesRepository.delete(it.id)
}
}
thumbnailsSeriesRepository.insert(thumbnail.copy(selected = false))

if (thumbnail.selected)
if (markSelected == MarkSelectedPreference.YES ||
(
markSelected == MarkSelectedPreference.IF_NONE_EXIST &&
thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(thumbnail.seriesId) == null
)
) {
thumbnailsSeriesRepository.markSelected(thumbnail)
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesAdded(thumbnail))
}
}

fun deleteThumbnailForSeries(thumbnail: ThumbnailSeries) {
require(thumbnail.type == ThumbnailSeries.Type.USER_UPLOADED) { "Only uploaded thumbnails can be deleted" }
thumbnailsSeriesRepository.delete(thumbnail.id)
}

private fun thumbnailsHouseKeeping(seriesId: String) {
Expand All @@ -263,6 +286,4 @@ class SeriesLifecycle(
}
}
}

private fun ThumbnailSeries.exists(): Boolean = Files.exists(Paths.get(url.toURI()))
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ class ThumbnailSeriesDao(
) : ThumbnailSeriesRepository {
private val ts = Tables.THUMBNAIL_SERIES

override fun findByIdOrNull(thumbnailId: String): ThumbnailSeries? =
dsl.selectFrom(ts)
.where(ts.ID.eq(thumbnailId))
.fetchOneInto(ts)
?.toDomain()

override fun findAllBySeriesId(seriesId: String): Collection<ThumbnailSeries> =
dsl.selectFrom(ts)
.where(ts.SERIES_ID.eq(seriesId))
Expand All @@ -34,7 +40,9 @@ class ThumbnailSeriesDao(
dsl.insertInto(ts)
.set(ts.ID, thumbnail.id)
.set(ts.SERIES_ID, thumbnail.seriesId)
.set(ts.URL, thumbnail.url.toString())
.set(ts.URL, thumbnail.url?.toString())
.set(ts.THUMBNAIL, thumbnail.thumbnail)
.set(ts.TYPE, thumbnail.type.toString())
.set(ts.SELECTED, thumbnail.selected)
.execute()
}
Expand Down Expand Up @@ -68,8 +76,10 @@ class ThumbnailSeriesDao(

private fun ThumbnailSeriesRecord.toDomain() =
ThumbnailSeries(
url = URL(url),
thumbnail = thumbnail,
url = url?.let { URL(it) },
selected = selected,
type = ThumbnailSeries.Type.valueOf(type),
id = id,
seriesId = seriesId,
createdDate = createdDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ class LocalArtworkProvider(
ThumbnailSeries(
url = path.toUri().toURL(),
seriesId = series.id,
selected = index == 0
selected = index == 0,
type = ThumbnailSeries.Type.SIDECAR
)
}.toList()
}
Expand Down

0 comments on commit d7470dd

Please sign in to comment.