Skip to content

Commit

Permalink
feat(api): metadata import settings per library
Browse files Browse the repository at this point in the history
ability to edit a library
fix filepath returned by API for Windows paths
series metadata import is now looking at all the files from all books, instead of being imported for each book separately

related to #199
  • Loading branch information
gotson committed Jul 3, 2020
1 parent 5760a06 commit 6824212
Show file tree
Hide file tree
Showing 25 changed files with 531 additions and 142 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
alter table library
add column import_comicinfo_book boolean default true;
alter table library
add column import_comicinfo_series boolean default true;
alter table library
add column import_comicinfo_collection boolean default true;
alter table library
add column import_epub_book boolean default true;
alter table library
add column import_epub_series boolean default true;
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ sealed class Task : Serializable {
override fun uniqueId() = "REFRESH_BOOK_METADATA_$bookId"
}

data class RefreshSeriesMetadata(val seriesId: Long) : Task() {
override fun uniqueId() = "REFRESH_SERIES_METADATA_$seriesId"
}

object BackupDatabase : Task() {
override fun uniqueId(): String = "BACKUP_DATABASE"
override fun toString(): String = "BackupDatabase"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.gotson.komga.application.tasks
import mu.KotlinLogging
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.domain.persistence.SeriesRepository
import org.gotson.komga.domain.service.BookLifecycle
import org.gotson.komga.domain.service.LibraryScanner
import org.gotson.komga.domain.service.MetadataLifecycle
Expand All @@ -20,6 +21,7 @@ class TaskHandler(
private val taskReceiver: TaskReceiver,
private val libraryRepository: LibraryRepository,
private val bookRepository: BookRepository,
private val seriesRepository: SeriesRepository,
private val libraryScanner: LibraryScanner,
private val bookLifecycle: BookLifecycle,
private val metadataLifecycle: MetadataLifecycle,
Expand Down Expand Up @@ -53,8 +55,14 @@ class TaskHandler(
is Task.RefreshBookMetadata ->
bookRepository.findByIdOrNull(task.bookId)?.let {
metadataLifecycle.refreshMetadata(it)
taskReceiver.refreshSeriesMetadata(it.seriesId)
} ?: logger.warn { "Cannot execute task $task: Book does not exist" }

is Task.RefreshSeriesMetadata ->
seriesRepository.findByIdOrNull(task.seriesId)?.let {
metadataLifecycle.refreshMetadata(it)
} ?: logger.warn { "Cannot execute task $task: Series does not exist" }

is Task.BackupDatabase -> {
databaseBackuper.backupDatabase()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ class TaskReceiver(
submitTask(Task.RefreshBookMetadata(book.id))
}

fun refreshSeriesMetadata(seriesId: Long) {
submitTask(Task.RefreshSeriesMetadata(seriesId))
}

fun databaseBackup() {
submitTask(Task.BackupDatabase)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,5 @@ data class BookMetadataPatch(
val publisher: String?,
val ageRating: Int?,
val releaseDate: LocalDate?,
val authors: List<Author>?,
val series: SeriesMetadataPatch?
val authors: List<Author>?
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import java.time.LocalDateTime
data class Library(
val name: String,
val root: URL,
val importComicInfoBook: Boolean = true,
val importComicInfoSeries: Boolean = true,
val importComicInfoCollection: Boolean = true,
val importEpubBook: Boolean = true,
val importEpubSeries: Boolean = true,

val id: Long = 0,

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

constructor(name: String, root: String) : this(name, Paths.get(root).toUri().toURL())

fun path(): Path = Paths.get(this.root.toURI())
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import org.gotson.komga.domain.model.Library

interface LibraryRepository {
fun findByIdOrNull(libraryId: Long): Library?
fun findById(libraryId: Long): Library
fun findAll(): Collection<Library>
fun findAllById(libraryIds: Collection<Long>): Collection<Library>

fun existsByName(name: String): Boolean

fun delete(libraryId: Long)
fun deleteAll()

fun insert(library: Library): Library
fun update(library: Library)

fun count(): Long
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,40 @@ class LibraryLifecycle(
fun addLibrary(library: Library): Library {
logger.info { "Adding new library: ${library.name} with root folder: ${library.root}" }

val existing = libraryRepository.findAll()
checkLibraryValidity(library, existing)

return libraryRepository.insert(library).also {
taskReceiver.scanLibrary(it.id)
}
}

fun updateLibrary(toUpdate: Library) {
logger.info { "Updating library: ${toUpdate.id}" }

val existing = libraryRepository.findAll().filter { it.id != toUpdate.id }
checkLibraryValidity(toUpdate, existing)

libraryRepository.update(toUpdate)
taskReceiver.scanLibrary(toUpdate.id)
}

private fun checkLibraryValidity(library: Library, existing: Collection<Library>) {
if (!Files.exists(library.path()))
throw FileNotFoundException("Library root folder does not exist: ${library.root}")

if (!Files.isDirectory(library.path()))
throw DirectoryNotFoundException("Library root folder is not a folder: ${library.root}")

if (libraryRepository.existsByName(library.name))
if (existing.map { it.name }.contains(library.name))
throw DuplicateNameException("Library name already exists")

libraryRepository.findAll().forEach {
existing.forEach {
if (library.path().startsWith(it.path()))
throw PathContainedInPath("Library path ${library.path()} is a child of existing library ${it.name}: ${it.path()}")
if (it.path().startsWith(library.path()))
throw PathContainedInPath("Library path ${library.path()} is a parent of existing library ${it.name}: ${it.path()}")
}

return libraryRepository.insert(library).let {
taskReceiver.scanLibrary(it.id)
it
}
}

fun deleteLibrary(library: Library) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,89 @@ package org.gotson.komga.domain.service

import mu.KotlinLogging
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.model.SeriesMetadataPatch
import org.gotson.komga.domain.persistence.BookMetadataRepository
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.domain.persistence.MediaRepository
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
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.springframework.stereotype.Service

private val logger = KotlinLogging.logger {}

@Service
class MetadataLifecycle(
private val bookMetadataProviders: List<BookMetadataProvider>,
private val seriesMetadataProviders: List<SeriesMetadataProvider>,
private val metadataApplier: MetadataApplier,
private val mediaRepository: MediaRepository,
private val bookMetadataRepository: BookMetadataRepository,
private val seriesMetadataRepository: SeriesMetadataRepository
private val seriesMetadataRepository: SeriesMetadataRepository,
private val libraryRepository: LibraryRepository,
private val bookRepository: BookRepository
) {

fun refreshMetadata(book: Book) {
logger.info { "Refresh metadata for book: $book" }
val media = mediaRepository.findById(book.id)

val library = libraryRepository.findById(book.libraryId)

bookMetadataProviders.forEach { provider ->
provider.getBookMetadataFromBook(book, media)?.let { bPatch ->
when {
provider is ComicInfoProvider && !library.importComicInfoBook -> logger.info { "Library is not set to import book metadata from ComicInfo, skipping" }
provider is EpubMetadataProvider && !library.importEpubBook -> logger.info { "Library is not set to import book metadata from Epub, skipping" }
else -> {
logger.debug { "Provider: $provider" }
provider.getBookMetadataFromBook(book, media)?.let { bPatch ->

bookMetadataRepository.findById(book.id).let {
logger.debug { "Original metadata: $it" }
val patched = metadataApplier.apply(bPatch, it)
logger.debug { "Patched metadata: $patched" }
bookMetadataRepository.findById(book.id).let {
logger.debug { "Original metadata: $it" }
val patched = metadataApplier.apply(bPatch, it)
logger.debug { "Patched metadata: $patched" }

bookMetadataRepository.update(patched)
bookMetadataRepository.update(patched)
}
}
}
}
}
}

fun refreshMetadata(series: Series) {
logger.info { "Refresh metadata for series: $series" }

val library = libraryRepository.findById(series.libraryId)

bPatch.series?.let { sPatch ->
seriesMetadataRepository.findById(book.seriesId).let {
logger.debug { "Apply metadata for series: ${book.seriesId}" }
seriesMetadataProviders.forEach { provider ->
when {
provider is ComicInfoProvider && !library.importComicInfoSeries -> logger.info { "Library is not set to import series metadata from ComicInfo, skipping" }
provider is EpubMetadataProvider && !library.importEpubSeries -> logger.info { "Library is not set to import series metadata from Epub, skipping" }
else -> {
logger.debug { "Provider: $provider" }
val patches = bookRepository.findBySeriesId(series.id)
.mapNotNull { provider.getSeriesMetadataFromBook(it, mediaRepository.findById(it.id)) }

val title = patches.uniqueOrNull { it.title }
val titleSort = patches.uniqueOrNull { it.titleSort }
val status = patches.uniqueOrNull { it.status }

if (title == null) logger.debug { "Ignoring title, values are not unique within series books" }
if (titleSort == null) logger.debug { "Ignoring sort title, values are not unique within series books" }
if (status == null) logger.debug { "Ignoring status, values are not unique within series books" }

val aggregatedPatch = SeriesMetadataPatch(title, titleSort, status)

seriesMetadataRepository.findById(series.id).let {
logger.debug { "Apply metadata for series: $series" }

logger.debug { "Original metadata: $it" }
val patched = metadataApplier.apply(sPatch, it)
val patched = metadataApplier.apply(aggregatedPatch, it)
logger.debug { "Patched metadata: $patched" }

seriesMetadataRepository.update(patched)
Expand All @@ -49,4 +94,13 @@ class MetadataLifecycle(
}
}

private fun <T, R : Any> Iterable<T>.uniqueOrNull(transform: (T) -> R?): R? {
return this
.mapNotNull(transform)
.distinct()
.let {
if (it.size == 1) it.first() else null
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Component
import toFilePath
import java.net.URL

@Component
Expand Down Expand Up @@ -207,7 +208,7 @@ class BookDtoDao(
seriesId = seriesId,
libraryId = libraryId,
name = name,
url = URL(url).toURI().path,
url = URL(url).toFilePath(),
number = number,
created = createdDate.toUTC(),
lastModified = lastModifiedDate.toUTC(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.gotson.komga.jooq.tables.records.LibraryRecord
import org.jooq.DSLContext
import org.springframework.stereotype.Component
import java.net.URL
import java.time.LocalDateTime

@Component
class LibraryDao(
Expand All @@ -18,10 +19,17 @@ class LibraryDao(
private val ul = Tables.USER_LIBRARY_SHARING

override fun findByIdOrNull(libraryId: Long): Library? =
findOne(libraryId)
?.toDomain()

override fun findById(libraryId: Long): Library =
findOne(libraryId)
.toDomain()

private fun findOne(libraryId: Long) =
dsl.selectFrom(l)
.where(l.ID.eq(libraryId))
.fetchOneInto(l)
?.toDomain()

override fun findAll(): Collection<Library> =
dsl.selectFrom(l)
Expand All @@ -34,12 +42,6 @@ class LibraryDao(
.fetchInto(l)
.map { it.toDomain() }

override fun existsByName(name: String): Boolean =
dsl.fetchExists(
dsl.selectFrom(l)
.where(l.NAME.equalIgnoreCase(name))
)

override fun delete(libraryId: Long) {
dsl.transaction { config ->
with(config.dsl())
Expand Down Expand Up @@ -67,9 +69,28 @@ class LibraryDao(
.set(l.ID, id)
.set(l.NAME, library.name)
.set(l.ROOT, library.root.toString())
.set(l.IMPORT_COMICINFO_BOOK, library.importComicInfoBook)
.set(l.IMPORT_COMICINFO_SERIES, library.importComicInfoSeries)
.set(l.IMPORT_COMICINFO_COLLECTION, library.importComicInfoCollection)
.set(l.IMPORT_EPUB_BOOK, library.importEpubBook)
.set(l.IMPORT_EPUB_SERIES, library.importEpubSeries)
.execute()

return findByIdOrNull(id)!!
return findById(id)
}

override fun update(library: Library) {
dsl.update(l)
.set(l.NAME, library.name)
.set(l.ROOT, library.root.toString())
.set(l.IMPORT_COMICINFO_BOOK, library.importComicInfoBook)
.set(l.IMPORT_COMICINFO_SERIES, library.importComicInfoSeries)
.set(l.IMPORT_COMICINFO_COLLECTION, library.importComicInfoCollection)
.set(l.IMPORT_EPUB_BOOK, library.importEpubBook)
.set(l.IMPORT_EPUB_SERIES, library.importEpubSeries)
.set(l.LAST_MODIFIED_DATE, LocalDateTime.now())
.where(l.ID.eq(library.id))
.execute()
}

override fun count(): Long = dsl.fetchCount(l).toLong()
Expand All @@ -79,6 +100,11 @@ class LibraryDao(
Library(
name = name,
root = URL(root),
importComicInfoBook = importComicinfoBook,
importComicInfoSeries = importComicinfoSeries,
importComicInfoCollection = importComicinfoCollection,
importEpubBook = importEpubBook,
importEpubSeries = importEpubSeries,
id = id,
createdDate = createdDate,
lastModifiedDate = lastModifiedDate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.stereotype.Component
import toFilePath
import java.math.BigDecimal
import java.net.URL

Expand Down Expand Up @@ -188,7 +189,7 @@ class SeriesDtoDao(
id = id,
libraryId = libraryId,
name = name,
url = URL(url).toURI().path,
url = URL(url).toFilePath(),
created = createdDate.toUTC(),
lastModified = lastModifiedDate.toUTC(),
fileLastModified = fileLastModified.toUTC(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.gotson.komga.infrastructure.metadata

import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.SeriesMetadataPatch

interface SeriesMetadataProvider {
fun getSeriesMetadataFromBook(book: Book, media: Media): SeriesMetadataPatch?
}

0 comments on commit 6824212

Please sign in to comment.