Skip to content

Commit

Permalink
feat: custom thumbnails for series
Browse files Browse the repository at this point in the history
closes gotson#63
  • Loading branch information
gotson committed Aug 12, 2020
1 parent f0854a8 commit f5f423f
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE THUMBNAIL_SERIES
(
ID varchar NOT NULL PRIMARY KEY,
URL varchar NOT NULL,
SELECTED boolean NOT NULL DEFAULT 0,
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)
);
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package org.gotson.komga.domain.model

import com.github.f4b6a3.tsid.TsidCreator
import java.net.URL
import java.nio.file.Path
import java.nio.file.Paths
import java.time.LocalDateTime

data class Series(
Expand All @@ -14,4 +16,7 @@ data class Series(

override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
) : Auditable()
) : Auditable() {

fun path(): Path = Paths.get(this.url.toURI())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.gotson.komga.domain.model

import com.github.f4b6a3.tsid.TsidCreator
import java.net.URL
import java.time.LocalDateTime

data class ThumbnailSeries(
val url: URL,
val selected: Boolean = false,

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

override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
) : Auditable()
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.gotson.komga.domain.persistence

import org.gotson.komga.domain.model.ThumbnailSeries

interface ThumbnailSeriesRepository {
fun findBySeriesId(seriesId: String): Collection<ThumbnailSeries>
fun findSelectedBySeriesId(seriesId: String): ThumbnailSeries?

fun insert(thumbnail: ThumbnailSeries)
fun markSelected(thumbnail: ThumbnailSeries)

fun delete(thumbnailSeriesId: String)
fun deleteBySeriesId(seriesId: String)
fun deleteBySeriesIds(seriesIds: Collection<String>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class BookLifecycle(
fun getThumbnail(bookId: String): ThumbnailBook? {
val selected = thumbnailBookRepository.findSelectedByBookId(bookId)

if (selected == null || !thumbnailExists(selected)) {
if (selected == null || !selected.exists()) {
thumbnailsHouseKeeping(bookId)
return thumbnailBookRepository.findSelectedByBookId(bookId)
}
Expand All @@ -115,7 +115,7 @@ class BookLifecycle(
logger.info { "House keeping thumbnails for book: $bookId" }
val all = thumbnailBookRepository.findByBookId(bookId)
.mapNotNull {
if (!thumbnailExists(it)) {
if (!it.exists()) {
logger.warn { "Thumbnail doesn't exist, removing entry" }
thumbnailBookRepository.delete(it.id)
null
Expand All @@ -135,10 +135,10 @@ class BookLifecycle(
}
}

private fun thumbnailExists(thumbnailBook: ThumbnailBook): Boolean {
if (thumbnailBook.type == ThumbnailBook.Type.SIDECAR) {
if (thumbnailBook.url != null)
return Files.exists(Paths.get(thumbnailBook.url.toURI()))
private fun ThumbnailBook.exists(): Boolean {
if (type == ThumbnailBook.Type.SIDECAR) {
if (url != null)
return Files.exists(Paths.get(url.toURI()))
return false
}
return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import org.gotson.komga.infrastructure.metadata.BookMetadataProvider
import org.gotson.komga.infrastructure.metadata.SeriesMetadataProvider
import org.gotson.komga.infrastructure.metadata.comicinfo.ComicInfoProvider
import org.gotson.komga.infrastructure.metadata.epub.EpubMetadataProvider
import org.gotson.komga.infrastructure.metadata.localmediaassets.LocalMediaAssetsProvider
import org.gotson.komga.infrastructure.metadata.localartwork.LocalArtworkProvider
import org.springframework.stereotype.Service

private val logger = KotlinLogging.logger {}
Expand All @@ -31,9 +31,10 @@ class MetadataLifecycle(
private val libraryRepository: LibraryRepository,
private val bookRepository: BookRepository,
private val bookLifecycle: BookLifecycle,
private val seriesLifecycle: SeriesLifecycle,
private val collectionRepository: SeriesCollectionRepository,
private val collectionLifecycle: SeriesCollectionLifecycle,
private val localMediaAssetsProvider: LocalMediaAssetsProvider
private val localArtworkProvider: LocalArtworkProvider
) {

fun refreshMetadata(book: Book) {
Expand Down Expand Up @@ -62,7 +63,7 @@ class MetadataLifecycle(
}
}

localMediaAssetsProvider.getBookThumbnails(book).forEach {
localArtworkProvider.getBookThumbnails(book).forEach {
bookLifecycle.addThumbnailForBook(it)
}
}
Expand Down Expand Up @@ -131,6 +132,10 @@ class MetadataLifecycle(
}
}
}

localArtworkProvider.getSeriesThumbnails(series).forEach {
seriesLifecycle.addThumbnailForSeries(it)
}
}

private fun <T, R : Any> Iterable<T>.uniqueOrNull(transform: (T) -> R?): R? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.model.ThumbnailSeries
import org.gotson.komga.domain.persistence.BookMetadataRepository
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.MediaRepository
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
import org.gotson.komga.domain.persistence.SeriesRepository
import org.gotson.komga.domain.persistence.ThumbnailSeriesRepository
import org.springframework.stereotype.Service
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.util.Comparator

private val logger = KotlinLogging.logger {}
Expand All @@ -27,6 +32,7 @@ class SeriesLifecycle(
private val mediaRepository: MediaRepository,
private val bookMetadataRepository: BookMetadataRepository,
private val seriesRepository: SeriesRepository,
private val thumbnailsSeriesRepository: ThumbnailSeriesRepository,
private val seriesMetadataRepository: SeriesMetadataRepository,
private val collectionRepository: SeriesCollectionRepository
) {
Expand Down Expand Up @@ -96,6 +102,7 @@ class SeriesLifecycle(
bookLifecycle.deleteMany(bookIds)

collectionRepository.removeSeriesFromAll(seriesId)
thumbnailsSeriesRepository.deleteBySeriesId(seriesId)

seriesRepository.delete(seriesId)
}
Expand All @@ -107,14 +114,69 @@ class SeriesLifecycle(
bookLifecycle.deleteMany(bookIds)

collectionRepository.removeSeriesFromAll(seriesIds)
thumbnailsSeriesRepository.deleteBySeriesIds(seriesIds)

seriesRepository.deleteAll(seriesIds)
}

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

if (selected == null || !selected.exists()) {
thumbnailsHouseKeeping(seriesId)
return thumbnailsSeriesRepository.findSelectedBySeriesId(seriesId)
}

return selected
}

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

bookRepository.findFirstIdInSeries(seriesId)?.let { bookId ->
return bookLifecycle.getThumbnailBytes(bookId)
}
return null
}

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

if (thumbnail.selected)
thumbnailsSeriesRepository.markSelected(thumbnail)
}

private fun thumbnailsHouseKeeping(seriesId: String) {
logger.info { "House keeping thumbnails for series: $seriesId" }
val all = thumbnailsSeriesRepository.findBySeriesId(seriesId)
.mapNotNull {
if (!it.exists()) {
logger.warn { "Thumbnail doesn't exist, removing entry" }
thumbnailsSeriesRepository.delete(it.id)
null
} else it
}

val selected = all.filter { it.selected }
when {
selected.size > 1 -> {
logger.info { "More than one thumbnail is selected, removing extra ones" }
thumbnailsSeriesRepository.markSelected(selected[0])
}
selected.isEmpty() && all.isNotEmpty() -> {
logger.info { "Series has bo selected thumbnail, choosing one automatically" }
thumbnailsSeriesRepository.markSelected(all.first())
}
}
}

private fun ThumbnailSeries.exists(): Boolean = Files.exists(Paths.get(url.toURI()))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.gotson.komga.infrastructure.jooq

import org.gotson.komga.domain.model.ThumbnailSeries
import org.gotson.komga.domain.persistence.ThumbnailSeriesRepository
import org.gotson.komga.jooq.Tables
import org.gotson.komga.jooq.tables.records.ThumbnailSeriesRecord
import org.jooq.DSLContext
import org.springframework.stereotype.Component
import java.net.URL

@Component
class ThumbnailSeriesDao(
private val dsl: DSLContext
) : ThumbnailSeriesRepository {
private val ts = Tables.THUMBNAIL_SERIES

override fun findBySeriesId(seriesId: String): Collection<ThumbnailSeries> =
dsl.selectFrom(ts)
.where(ts.SERIES_ID.eq(seriesId))
.fetchInto(ts)
.map { it.toDomain() }

override fun findSelectedBySeriesId(seriesId: String): ThumbnailSeries? =
dsl.selectFrom(ts)
.where(ts.SERIES_ID.eq(seriesId))
.and(ts.SELECTED.isTrue)
.limit(1)
.fetchInto(ts)
.map { it.toDomain() }
.firstOrNull()

override fun insert(thumbnail: ThumbnailSeries) {
dsl.insertInto(ts)
.set(ts.ID, thumbnail.id)
.set(ts.SERIES_ID, thumbnail.seriesId)
.set(ts.URL, thumbnail.url.toString())
.set(ts.SELECTED, thumbnail.selected)
.execute()
}

override fun markSelected(thumbnail: ThumbnailSeries) {
dsl.transaction { config ->
config.dsl().update(ts)
.set(ts.SELECTED, false)
.where(ts.SERIES_ID.eq(thumbnail.seriesId))
.and(ts.ID.ne(thumbnail.id))
.execute()

config.dsl().update(ts)
.set(ts.SELECTED, true)
.where(ts.SERIES_ID.eq(thumbnail.seriesId))
.and(ts.ID.eq(thumbnail.id))
.execute()
}
}

override fun delete(thumbnailSeriesId: String) {
dsl.deleteFrom(ts).where(ts.ID.eq(thumbnailSeriesId)).execute()
}

override fun deleteBySeriesId(seriesId: String) {
dsl.deleteFrom(ts).where(ts.SERIES_ID.eq(seriesId)).execute()
}

override fun deleteBySeriesIds(seriesIds: Collection<String>) {
dsl.deleteFrom(ts).where(ts.SERIES_ID.`in`(seriesIds)).execute()
}

private fun ThumbnailSeriesRecord.toDomain() =
ThumbnailSeries(
url = URL(url),
selected = selected,
id = id,
seriesId = seriesId,
createdDate = createdDate,
lastModifiedDate = lastModifiedDate
)
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.gotson.komga.infrastructure.metadata.localmediaassets
package org.gotson.komga.infrastructure.metadata.localartwork

import mu.KotlinLogging
import org.apache.commons.io.FilenameUtils
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.model.ThumbnailBook
import org.gotson.komga.domain.model.ThumbnailSeries
import org.gotson.komga.infrastructure.mediacontainer.ContentDetector
import org.springframework.stereotype.Service
import java.nio.file.Files
Expand All @@ -12,11 +14,12 @@ import kotlin.streams.asSequence
private val logger = KotlinLogging.logger {}

@Service
class LocalMediaAssetsProvider(
class LocalArtworkProvider(
private val contentDetector: ContentDetector
) {

val supportedExtensions = listOf("png", "jpeg", "jpg", "tbn")
val supportedSeriesFiles = listOf("cover", "default", "folder", "poster", "series")

fun getBookThumbnails(book: Book): List<ThumbnailBook> {
logger.info { "Looking for local thumbnails for book: $book" }
Expand All @@ -39,8 +42,27 @@ class LocalMediaAssetsProvider(
bookId = book.id,
selected = index == 0
)
}.sortedBy { it.url.toString() }
.toList()
}.toList()
}
}

fun getSeriesThumbnails(series: Series): List<ThumbnailSeries> {
logger.info { "Looking for local thumbnails for series: $series" }

return Files.list(series.path()).use { dirStream ->
dirStream.asSequence()
.filter { Files.isRegularFile(it) }
.filter { supportedSeriesFiles.contains(FilenameUtils.getBaseName(it.toString().toLowerCase())) }
.filter { supportedExtensions.contains(FilenameUtils.getExtension(it.fileName.toString()).toLowerCase()) }
.filter { contentDetector.isImage(contentDetector.detectMediaType(it)) }
.mapIndexed { index, path ->
logger.info { "Found file: $path" }
ThumbnailSeries(
url = path.toUri().toURL(),
seriesId = series.id,
selected = index == 0
)
}.toList()
}
}

Expand Down

0 comments on commit f5f423f

Please sign in to comment.