style: ktlint format

This commit is contained in:
Gauthier Roebroeck 2024-01-22 15:26:37 +08:00
parent e01b32446b
commit d9bba60578
312 changed files with 8845 additions and 6494 deletions

View file

@ -126,7 +126,8 @@ jreleaser {
enabled = true
title = "# [{{projectVersion}}]({{repoUrl}}/compare/{{previousTagName}}...{{tagName}}) ({{#f_now}}YYYY-MM-dd{{/f_now}})"
target = rootDir.resolve("CHANGELOG.md")
content = """
content =
"""
{{changelogTitle}}
{{changelogChanges}}
""".trimIndent()
@ -173,7 +174,8 @@ jreleaser {
templateDirectory = rootDir.resolve("komga/docker")
repository.active = Active.NEVER
buildArgs = listOf("--cache-from", "gotson/komga:latest")
imageNames = listOf(
imageNames =
listOf(
"komga:latest",
"komga:{{projectVersion}}",
"komga:{{projectVersionMajor}}.x",
@ -185,7 +187,8 @@ jreleaser {
buildx {
enabled = true
createBuilder = false
platforms = listOf(
platforms =
listOf(
"linux/amd64",
"linux/arm/v7",
"linux/arm64/v8",

View file

@ -22,7 +22,8 @@ fun main(args: Array<String>) {
run(*args)
}
} catch (e: Exception) {
val (message, stackTrace) = when (e.cause) {
val (message, stackTrace) =
when (e.cause) {
is PortInUseException -> RB.getString("error_message.port_in_use", (e.cause as PortInUseException).port) to null
else -> RB.getString("error_message.unexpected") to e.stackTraceToString()
}

View file

@ -7,8 +7,13 @@ class RB private constructor() {
companion object {
private val BUNDLE: ResourceBundle = ResourceBundle.getBundle("org.gotson.komga.messages")
fun getString(key: String, vararg args: Any?): String =
if (args.isEmpty()) BUNDLE.getString(key)
else MessageFormatter.arrayFormat(BUNDLE.getString(key), args).message
fun getString(
key: String,
vararg args: Any?,
): String =
if (args.isEmpty())
BUNDLE.getString(key)
else
MessageFormatter.arrayFormat(BUNDLE.getString(key), args).message
}
}

View file

@ -30,17 +30,22 @@ import org.gotson.komga.RB
import org.springframework.core.io.ClassPathResource
@Preview
fun showErrorDialog(text: String, stackTrace: String? = null) {
fun showErrorDialog(
text: String,
stackTrace: String? = null,
) {
application {
Window(
title = RB.getString("dialog_error.title"),
onCloseRequest = ::exitApplication,
visible = true,
resizable = false,
state = WindowState(
state =
WindowState(
placement = WindowPlacement.Floating,
position = WindowPosition(alignment = Alignment.Center),
size = DpSize(
size =
DpSize(
if (stackTrace != null) 800.dp else Dp.Unspecified,
Dp.Unspecified,
),
@ -57,7 +62,8 @@ fun showErrorDialog(text: String, stackTrace: String? = null) {
Image(
painter = loadSvgPainter(ClassPathResource("icons/komga-color.svg").inputStream, LocalDensity.current),
contentDescription = "Komga logo",
modifier = Modifier
modifier =
Modifier
.size(96.dp)
.align(Alignment.Top),
)
@ -77,7 +83,8 @@ fun showErrorDialog(text: String, stackTrace: String? = null) {
Row(
horizontalArrangement = if (stackTrace != null) Arrangement.SpaceBetween else Arrangement.End,
modifier = if (stackTrace != null)
modifier =
if (stackTrace != null)
Modifier.align(Alignment.End).fillMaxWidth()
else
Modifier.align(Alignment.End),

View file

@ -26,7 +26,6 @@ class TrayIconRunner(
@Value("\${server.port}") serverPort: Int,
env: Environment,
) : ApplicationRunner {
val komgaUrl = "http://localhost:$serverPort$servletContextPath"
val komgaConfigDir = File(komgaConfigDir)
val logFile = File(logFileName)

View file

@ -26,12 +26,13 @@ kotlin {
jvmToolchain(17)
}
val benchmarkSourceSet = sourceSets.create("benchmark") {
val benchmarkSourceSet =
sourceSets.create("benchmark") {
java {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().runtimeClasspath
}
}
}
val benchmarkImplementation by configurations.getting {
extendsFrom(configurations.testImplementation.get())
@ -147,7 +148,8 @@ tasks {
withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs = listOf(
freeCompilerArgs =
listOf(
"-Xjsr305=strict",
"-opt-in=kotlin.time.ExperimentalTime",
)
@ -243,26 +245,31 @@ springBoot {
}
}
val sqliteUrls = mapOf(
val sqliteUrls =
mapOf(
"main" to "jdbc:sqlite:${project.layout.buildDirectory.get()}/generated/flyway/main/database.sqlite",
"tasks" to "jdbc:sqlite:${project.layout.buildDirectory.get()}/generated/flyway/tasks/tasks.sqlite",
)
val sqliteMigrationDirs = mapOf(
"main" to listOf(
)
val sqliteMigrationDirs =
mapOf(
"main" to
listOf(
"$projectDir/src/flyway/resources/db/migration/sqlite",
"$projectDir/src/flyway/kotlin/db/migration/sqlite",
),
"tasks" to listOf(
"tasks" to
listOf(
"$projectDir/src/flyway/resources/tasks/migration/sqlite",
// "$projectDir/src/flyway/kotlin/tasks/migration/sqlite",
),
)
)
task("flywayMigrateMain", FlywayMigrateTask::class) {
val id = "main"
url = sqliteUrls[id]
locations = arrayOf("classpath:db/migration/sqlite")
placeholders = mapOf(
placeholders =
mapOf(
"library-file-hashing" to "true",
"library-scan-startup" to "false",
"delete-empty-collections" to "true",

View file

@ -25,7 +25,8 @@ abstract class AbstractBenchmark {
fun executeJmhRunner() {
if (this.javaClass.simpleName.contains("jmhType")) return
Files.createDirectories(Paths.get(benchmarkProperties.resultFolder))
val extension = when (benchmarkProperties.resultFormat) {
val extension =
when (benchmarkProperties.resultFormat) {
ResultFormatType.TEXT -> "txt"
ResultFormatType.CSV -> "csv"
ResultFormatType.SCSV -> "scsv"
@ -33,7 +34,8 @@ abstract class AbstractBenchmark {
ResultFormatType.LATEX -> "tex"
}
val resultFile = Paths.get(benchmarkProperties.resultFolder, "${this.javaClass.name}.$extension").absolutePathString()
val opt: Options = OptionsBuilder()
val opt: Options =
OptionsBuilder()
.include("\\." + this.javaClass.simpleName + "\\.") // set the class name regex for benchmarks to search for to the current class
.apply { if (benchmarkProperties.warmupIterations > 0) warmupIterations(benchmarkProperties.warmupIterations) }
.measurementIterations(benchmarkProperties.measurementIterations)

View file

@ -12,7 +12,6 @@ import org.springframework.beans.factory.annotation.Autowired
internal const val DEFAULT_PAGE_SIZE = 20
abstract class AbstractRestBenchmark : AbstractBenchmark() {
companion object {
lateinit var userRepository: KomgaUserRepository
lateinit var seriesController: SeriesController
@ -38,7 +37,8 @@ abstract class AbstractRestBenchmark : AbstractBenchmark() {
@Setup(Level.Trial)
fun prepareData() {
principal = KomgaPrincipal(
principal =
KomgaPrincipal(
userRepository.findByEmailIgnoreCaseOrNull("admin@example.org")
?: throw IllegalStateException("no user found"),
)

View file

@ -11,7 +11,6 @@ import java.util.concurrent.TimeUnit
@OutputTimeUnit(TimeUnit.MILLISECONDS)
class BrowseBenchmark : AbstractRestBenchmark() {
companion object {
private lateinit var biggestSeriesId: String
}
@ -24,7 +23,8 @@ class BrowseBenchmark : AbstractRestBenchmark() {
super.prepareData()
// find series with most books
biggestSeriesId = seriesController.getAllSeries(principal, page = PageRequest.of(0, 1, Sort.by(Sort.Order.desc("booksCount"))))
biggestSeriesId =
seriesController.getAllSeries(principal, page = PageRequest.of(0, 1, Sort.by(Sort.Order.desc("booksCount"))))
.content.first()
.id
}

View file

@ -19,7 +19,6 @@ import java.util.concurrent.TimeUnit
@OutputTimeUnit(TimeUnit.MILLISECONDS)
class DashboardBenchmark : AbstractRestBenchmark() {
companion object {
lateinit var bookLatestReleaseDate: LocalDate
}

View file

@ -11,7 +11,6 @@ import java.util.concurrent.TimeUnit
@OutputTimeUnit(TimeUnit.MILLISECONDS)
class UnsortedBenchmark : AbstractRestBenchmark() {
companion object {
private lateinit var biggestSeriesId: String
}
@ -23,7 +22,8 @@ class UnsortedBenchmark : AbstractRestBenchmark() {
super.prepareData()
// find series with most books
biggestSeriesId = seriesController.getAllSeries(principal, page = PageRequest.of(0, 1, Sort.by(Sort.Order.desc("booksCount"))))
biggestSeriesId =
seriesController.getAllSeries(principal, page = PageRequest.of(0, 1, Sort.by(Sort.Order.desc("booksCount"))))
.content.first()
.id
}

View file

@ -28,7 +28,8 @@ class LibraryScanScheduler(
// map the libraryId to the scan scheduled task
private val registry = ConcurrentHashMap<String, ScheduledTask>()
private val registrar = ScheduledTaskRegistrar().apply {
private val registrar =
ScheduledTaskRegistrar().apply {
setTaskScheduler(taskScheduler)
}

View file

@ -17,122 +17,146 @@ sealed class Task(val priority: Int = DEFAULT_PRIORITY, val groupId: String? = n
class ScanLibrary(val libraryId: String, val scanDeep: Boolean, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "SCAN_LIBRARY_${libraryId}_DEEP_$scanDeep"
override fun toString(): String = "ScanLibrary(libraryId='$libraryId', scanDeep='$scanDeep', priority='$priority')"
}
class FindBooksToConvert(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "FIND_BOOKS_TO_CONVERT_$libraryId"
override fun toString(): String = "FindBooksToConvert(libraryId='$libraryId', priority='$priority')"
}
class FindBooksWithMissingPageHash(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "FIND_BOOKS_WITH_MISSING_PAGE_HASH_$libraryId"
override fun toString(): String = "FindBooksWithMissingPageHash(libraryId='$libraryId', priority='$priority')"
}
class FindDuplicatePagesToDelete(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "FIND_DUPLICATE_PAGES_TO_DELETE_$libraryId"
override fun toString(): String = "FindDuplicatePagesToDelete(libraryId='$libraryId', priority='$priority')"
}
class EmptyTrash(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "EMPTY_TRASH_$libraryId"
override fun toString(): String = "EmptyTrash(libraryId='$libraryId', priority='$priority')"
}
class AnalyzeBook(val bookId: String, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) {
override val uniqueId = "ANALYZE_BOOK_$bookId"
override fun toString(): String = "AnalyzeBook(bookId='$bookId', priority='$priority')"
}
class GenerateBookThumbnail(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "GENERATE_BOOK_THUMBNAIL_$bookId"
override fun toString(): String = "GenerateBookThumbnail(bookId='$bookId', priority='$priority')"
}
class RefreshBookMetadata(val bookId: String, val capabilities: Set<BookMetadataPatchCapability>, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) {
override val uniqueId = "REFRESH_BOOK_METADATA_$bookId"
override fun toString(): String = "RefreshBookMetadata(bookId='$bookId', capabilities=$capabilities, priority='$priority')"
}
class HashBook(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "HASH_BOOK_$bookId"
override fun toString(): String = "HashBook(bookId='$bookId', priority='$priority')"
}
class HashBookPages(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "HASH_BOOK_PAGES_$bookId"
override fun toString(): String = "HashBookPages(bookId='$bookId', priority='$priority')"
}
class RefreshSeriesMetadata(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority, seriesId) {
override val uniqueId = "REFRESH_SERIES_METADATA_$seriesId"
override fun toString(): String = "RefreshSeriesMetadata(seriesId='$seriesId', priority='$priority')"
}
class AggregateSeriesMetadata(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority, seriesId) {
override val uniqueId = "AGGREGATE_SERIES_METADATA_$seriesId"
override fun toString(): String = "AggregateSeriesMetadata(seriesId='$seriesId', priority='$priority')"
}
class RefreshBookLocalArtwork(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId: String = "REFRESH_BOOK_LOCAL_ARTWORK_$bookId"
override fun toString(): String = "RefreshBookLocalArtwork(bookId='$bookId', priority='$priority')"
}
class RefreshSeriesLocalArtwork(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId: String = "REFRESH_SERIES_LOCAL_ARTWORK_$seriesId"
override fun toString(): String = "RefreshSeriesLocalArtwork(seriesId=$seriesId, priority='$priority')"
}
class ImportBook(val sourceFile: String, val seriesId: String, val copyMode: CopyMode, val destinationName: String?, val upgradeBookId: String?, priority: Int = DEFAULT_PRIORITY) : Task(priority, seriesId) {
override val uniqueId: String = "IMPORT_BOOK_${seriesId}_$sourceFile"
override fun toString(): String =
"ImportBook(sourceFile='$sourceFile', seriesId='$seriesId', copyMode=$copyMode, destinationName=$destinationName, upgradeBookId=$upgradeBookId, priority='$priority')"
}
class ConvertBook(val bookId: String, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) {
override val uniqueId: String = "CONVERT_BOOK_$bookId"
override fun toString(): String = "ConvertBook(bookId='$bookId', priority='$priority')"
}
class RepairExtension(val bookId: String, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) {
override val uniqueId: String = "REPAIR_EXTENSION_$bookId"
override fun toString(): String = "RepairExtension(bookId='$bookId', priority='$priority')"
}
class RemoveHashedPages(val bookId: String, val pages: Collection<BookPageNumbered>, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId: String = "REMOVE_HASHED_PAGES_$bookId"
override fun toString(): String = "RemoveHashedPages(bookId='$bookId', priority='$priority')"
}
class RebuildIndex(val entities: Set<LuceneEntity>?, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "REBUILD_INDEX"
override fun toString(): String = "RebuildIndex(priority='$priority',entities='${entities?.map { it.type }}')"
}
class UpgradeIndex(priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "UPGRADE_INDEX"
override fun toString(): String = "UpgradeIndex(priority='$priority')"
}
class DeleteBook(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "DELETE_BOOK_$bookId"
override fun toString(): String = "DeleteBook(bookId='$bookId', priority='$priority')"
}
class DeleteSeries(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "DELETE_SERIES_$seriesId"
override fun toString(): String = "DeleteSeries(seriesId='$seriesId', priority='$priority')"
}
class FixThumbnailsWithoutMetadata(priority: Int = DEFAULT_PRIORITY) : Task(priority, "FixThumbnailsWithoutMetadata") {
override val uniqueId = "FIX_THUMBNAILS_WITHOUT_METADATA_${LocalDateTime.now()}"
override fun toString(): String = "FixThumbnailsWithoutMetadata(priority='$priority')"
}
class FindBookThumbnailsToRegenerate(val forBiggerResultOnly: Boolean, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "FIND_BOOK_THUMBNAILS_TO_REGENERATE"
override fun toString(): String = "FindBookThumbnailsToRegenerate(forBiggerResultOnly='$forBiggerResultOnly', priority='$priority')"
}
}

View file

@ -25,11 +25,18 @@ class TaskEmitter(
private val tasksRepository: TasksRepository,
private val eventPublisher: ApplicationEventPublisher,
) {
fun scanLibrary(libraryId: String, scanDeep: Boolean = false, priority: Int = DEFAULT_PRIORITY) {
fun scanLibrary(
libraryId: String,
scanDeep: Boolean = false,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.ScanLibrary(libraryId, scanDeep, priority))
}
fun emptyTrash(libraryId: String, priority: Int = DEFAULT_PRIORITY) {
fun emptyTrash(
libraryId: String,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.EmptyTrash(libraryId, priority))
}
@ -55,27 +62,42 @@ class TaskEmitter(
.let { submitTasks(it) }
}
fun findBooksWithMissingPageHash(library: Library, priority: Int = DEFAULT_PRIORITY) {
fun findBooksWithMissingPageHash(
library: Library,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.FindBooksWithMissingPageHash(library.id, priority))
}
fun hashBookPages(bookIdToSeriesId: Collection<String>, priority: Int = DEFAULT_PRIORITY) {
fun hashBookPages(
bookIdToSeriesId: Collection<String>,
priority: Int = DEFAULT_PRIORITY,
) {
bookIdToSeriesId
.map { Task.HashBookPages(it, priority) }
.let { submitTasks(it) }
}
fun findBooksToConvert(library: Library, priority: Int = DEFAULT_PRIORITY) {
fun findBooksToConvert(
library: Library,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.FindBooksToConvert(library.id, priority))
}
fun convertBookToCbz(books: Collection<Book>, priority: Int = DEFAULT_PRIORITY) {
fun convertBookToCbz(
books: Collection<Book>,
priority: Int = DEFAULT_PRIORITY,
) {
books
.map { Task.ConvertBook(it.id, priority, it.seriesId) }
.let { submitTasks(it) }
}
fun repairExtensions(library: Library, priority: Int = DEFAULT_PRIORITY) {
fun repairExtensions(
library: Library,
priority: Int = DEFAULT_PRIORITY,
) {
if (library.repairExtensions)
bookConverter
.getMismatchedExtensionBooks(library)
@ -83,35 +105,57 @@ class TaskEmitter(
.let { submitTasks(it) }
}
fun findDuplicatePagesToDelete(library: Library, priority: Int = DEFAULT_PRIORITY) {
fun findDuplicatePagesToDelete(
library: Library,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.FindDuplicatePagesToDelete(library.id, priority))
}
fun removeDuplicatePages(bookId: String, pages: Collection<BookPageNumbered>, priority: Int = DEFAULT_PRIORITY) {
fun removeDuplicatePages(
bookId: String,
pages: Collection<BookPageNumbered>,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.RemoveHashedPages(bookId, pages, priority))
}
fun removeDuplicatePages(bookIdToPages: Map<String, Collection<BookPageNumbered>>, priority: Int = DEFAULT_PRIORITY) {
fun removeDuplicatePages(
bookIdToPages: Map<String, Collection<BookPageNumbered>>,
priority: Int = DEFAULT_PRIORITY,
) {
bookIdToPages
.map { Task.RemoveHashedPages(it.key, it.value, priority) }
.let { submitTasks(it) }
}
fun analyzeBook(book: Book, priority: Int = DEFAULT_PRIORITY) {
fun analyzeBook(
book: Book,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.AnalyzeBook(book.id, priority, book.seriesId))
}
fun analyzeBook(books: Collection<Book>, priority: Int = DEFAULT_PRIORITY) {
fun analyzeBook(
books: Collection<Book>,
priority: Int = DEFAULT_PRIORITY,
) {
books
.map { Task.AnalyzeBook(it.id, priority, it.seriesId) }
.let { submitTasks(it) }
}
fun generateBookThumbnail(bookId: String, priority: Int = DEFAULT_PRIORITY) {
fun generateBookThumbnail(
bookId: String,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.GenerateBookThumbnail(bookId, priority))
}
fun generateBookThumbnail(bookIds: Collection<String>, priority: Int = DEFAULT_PRIORITY) {
fun generateBookThumbnail(
bookIds: Collection<String>,
priority: Int = DEFAULT_PRIORITY,
) {
bookIds
.map { Task.GenerateBookThumbnail(it, priority) }
.let { submitTasks(it) }
@ -135,39 +179,67 @@ class TaskEmitter(
.let { submitTasks(it) }
}
fun refreshSeriesMetadata(seriesId: String, priority: Int = DEFAULT_PRIORITY) {
fun refreshSeriesMetadata(
seriesId: String,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.RefreshSeriesMetadata(seriesId, priority))
}
fun aggregateSeriesMetadata(seriesId: String, priority: Int = DEFAULT_PRIORITY) {
fun aggregateSeriesMetadata(
seriesId: String,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.AggregateSeriesMetadata(seriesId, priority))
}
fun refreshBookLocalArtwork(book: Book, priority: Int = DEFAULT_PRIORITY) {
fun refreshBookLocalArtwork(
book: Book,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.RefreshBookLocalArtwork(book.id, priority))
}
fun refreshBookLocalArtwork(books: Collection<Book>, priority: Int = DEFAULT_PRIORITY) {
fun refreshBookLocalArtwork(
books: Collection<Book>,
priority: Int = DEFAULT_PRIORITY,
) {
books
.map { Task.RefreshBookLocalArtwork(it.id, priority) }
.let { submitTasks(it) }
}
fun refreshSeriesLocalArtwork(seriesId: String, priority: Int = DEFAULT_PRIORITY) {
fun refreshSeriesLocalArtwork(
seriesId: String,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.RefreshSeriesLocalArtwork(seriesId, priority))
}
fun refreshSeriesLocalArtwork(seriesIds: Collection<String>, priority: Int = DEFAULT_PRIORITY) {
fun refreshSeriesLocalArtwork(
seriesIds: Collection<String>,
priority: Int = DEFAULT_PRIORITY,
) {
seriesIds
.map { Task.RefreshSeriesLocalArtwork(it, priority) }
.let { submitTasks(it) }
}
fun importBook(sourceFile: String, seriesId: String, copyMode: CopyMode, destinationName: String?, upgradeBookId: String?, priority: Int = DEFAULT_PRIORITY) {
fun importBook(
sourceFile: String,
seriesId: String,
copyMode: CopyMode,
destinationName: String?,
upgradeBookId: String?,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.ImportBook(sourceFile, seriesId, copyMode, destinationName, upgradeBookId, priority))
}
fun rebuildIndex(priority: Int = DEFAULT_PRIORITY, entities: Set<LuceneEntity>? = null) {
fun rebuildIndex(
priority: Int = DEFAULT_PRIORITY,
entities: Set<LuceneEntity>? = null,
) {
submitTask(Task.RebuildIndex(entities, priority))
}
@ -175,11 +247,17 @@ class TaskEmitter(
submitTask(Task.UpgradeIndex(priority))
}
fun deleteBook(bookId: String, priority: Int = DEFAULT_PRIORITY) {
fun deleteBook(
bookId: String,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.DeleteBook(bookId, priority))
}
fun deleteSeries(seriesId: String, priority: Int = DEFAULT_PRIORITY) {
fun deleteSeries(
seriesId: String,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.DeleteSeries(seriesId, priority))
}
@ -187,7 +265,10 @@ class TaskEmitter(
submitTask(Task.FixThumbnailsWithoutMetadata(priority))
}
fun findBookThumbnailsToRegenerate(forBiggerResultOnly: Boolean, priority: Int = DEFAULT_PRIORITY) {
fun findBookThumbnailsToRegenerate(
forBiggerResultOnly: Boolean,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.FindBookThumbnailsToRegenerate(forBiggerResultOnly, priority))
}

View file

@ -1,4 +1,5 @@
package org.gotson.komga.application.tasks
class TaskAddedEvent
class TaskPoolSizeChangedEvent

View file

@ -47,7 +47,6 @@ class TaskHandler(
private val thumbnailLifecycle: ThumbnailLifecycle,
private val meterRegistry: MeterRegistry,
) {
fun handleTask(task: Task) {
logger.info { "Executing task: $task" }
try {
@ -162,8 +161,10 @@ class TaskHandler(
is Task.DeleteBook -> {
bookRepository.findByIdOrNull(task.bookId)?.let { book ->
if (book.oneshot) seriesLifecycle.deleteSeriesFiles(seriesRepository.findByIdOrNull(book.seriesId)!!)
else bookLifecycle.deleteBookFiles(book)
if (book.oneshot)
seriesLifecycle.deleteSeriesFiles(seriesRepository.findByIdOrNull(book.seriesId)!!)
else
bookLifecycle.deleteBookFiles(book)
}
}

View file

@ -43,10 +43,13 @@ class TaskProcessor(
fun processAvailableTask() {
if (processTasks) {
logger.debug { "Active count: ${executor.activeCount}, Core Pool Size: ${executor.corePoolSize}, Pool Size: ${executor.poolSize}" }
if (executor.corePoolSize == 1) executor.execute { takeAndProcess() }
// fan out while threads are available
else while (tasksRepository.hasAvailable() && executor.activeCount < executor.corePoolSize)
if (executor.corePoolSize == 1) {
executor.execute { takeAndProcess() }
} else {
// fan out while threads are available
while (tasksRepository.hasAvailable() && executor.activeCount < executor.corePoolSize)
executor.execute { takeAndProcess() }
}
} else {
logger.debug { "Not processing tasks" }
}

View file

@ -2,15 +2,26 @@ package org.gotson.komga.application.tasks
interface TasksRepository {
fun hasAvailable(): Boolean
fun takeFirst(owner: String = Thread.currentThread().name): Task?
fun findAll(): List<Task>
fun findAllGroupedByOwner(): Map<String?, List<Task>>
fun count(): Int
fun countBySimpleType(): Map<String, Int>
fun save(task: Task)
fun save(tasks: Collection<Task>)
fun delete(taskId: String)
fun deleteAll()
fun deleteAllWithoutOwner(): Int
fun disown(): Int
}

View file

@ -6,5 +6,6 @@ data class AgeRestriction(
)
enum class AllowExclude {
ALLOW_ONLY, EXCLUDE,
ALLOW_ONLY,
EXCLUDE,
}

View file

@ -13,18 +13,14 @@ data class Book(
val fileSize: Long = 0,
val fileHash: String = "",
val number: Int = 0,
val id: String = TsidCreator.getTsid256().toString(),
val seriesId: String = "",
val libraryId: String = "",
val deletedDate: LocalDateTime? = null,
val oneshot: Boolean = false,
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable {
@delegate:Transient
val path: Path by lazy { this.url.toURI().toPath() }
}

View file

@ -14,7 +14,6 @@ class BookMetadata(
tags: Set<String> = emptySet(),
val isbn: String = "",
val links: List<WebLink> = emptyList(),
val titleLock: Boolean = false,
val summaryLock: Boolean = false,
val numberLock: Boolean = false,
@ -24,13 +23,10 @@ class BookMetadata(
val tagsLock: Boolean = false,
val isbnLock: Boolean = false,
val linksLock: Boolean = false,
val bookId: String = "",
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable {
val title = title.trim()
val summary = summary.trim()
val number = number.trim()

View file

@ -9,9 +9,7 @@ data class BookMetadataAggregation(
val releaseDate: LocalDate? = null,
val summary: String = "",
val summaryNumber: String = "",
val seriesId: String = "",
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable

View file

@ -12,7 +12,6 @@ data class BookMetadataPatch(
val isbn: String? = null,
val links: List<WebLink>? = null,
val tags: Set<String>? = null,
val readLists: List<ReadListEntry> = emptyList(),
) {
data class ReadListEntry(

View file

@ -13,6 +13,6 @@ class BookPageNumbered(
dimension = dimension,
fileHash = fileHash,
fileSize = fileSize,
) {
) {
override fun toString(): String = "BookPageNumbered(fileName='$fileName', mediaType='$mediaType', dimension=$dimension, fileHash='$fileHash', fileSize=$fileSize, pageNumber=$pageNumber)"
}

View file

@ -28,4 +28,4 @@ class BookSearchWithReadProgress(
mediaStatus = mediaStatus,
deleted = deleted,
releasedAfter = releasedAfter,
)
)

View file

@ -3,46 +3,65 @@ package org.gotson.komga.domain.model
import java.net.URL
sealed class DomainEvent {
data class LibraryAdded(val library: Library) : DomainEvent()
data class LibraryUpdated(val library: Library) : DomainEvent()
data class LibraryDeleted(val library: Library) : DomainEvent()
data class LibraryScanned(val library: Library) : DomainEvent()
data class SeriesAdded(val series: Series) : DomainEvent()
data class SeriesUpdated(val series: Series) : DomainEvent()
data class SeriesDeleted(val series: Series) : DomainEvent()
data class BookAdded(val book: Book) : DomainEvent()
data class BookUpdated(val book: Book) : DomainEvent()
data class BookDeleted(val book: Book) : DomainEvent()
data class BookImported(val book: Book?, val sourceFile: URL, val success: Boolean, val message: String? = null) : DomainEvent()
data class CollectionAdded(val collection: SeriesCollection) : DomainEvent()
data class CollectionUpdated(val collection: SeriesCollection) : DomainEvent()
data class CollectionDeleted(val collection: SeriesCollection) : DomainEvent()
data class ReadListAdded(val readList: ReadList) : DomainEvent()
data class ReadListUpdated(val readList: ReadList) : DomainEvent()
data class ReadListDeleted(val readList: ReadList) : DomainEvent()
data class ReadProgressChanged(val progress: ReadProgress) : DomainEvent()
data class ReadProgressDeleted(val progress: ReadProgress) : DomainEvent()
data class ReadProgressSeriesChanged(val seriesId: String, val userId: String) : DomainEvent()
data class ReadProgressSeriesDeleted(val seriesId: String, val userId: String) : DomainEvent()
data class ThumbnailBookAdded(val thumbnail: ThumbnailBook) : DomainEvent()
data class ThumbnailBookDeleted(val thumbnail: ThumbnailBook) : DomainEvent()
data class ThumbnailSeriesAdded(val thumbnail: ThumbnailSeries) : DomainEvent()
data class ThumbnailSeriesDeleted(val thumbnail: ThumbnailSeries) : DomainEvent()
data class ThumbnailSeriesCollectionAdded(val thumbnail: ThumbnailSeriesCollection) : DomainEvent()
data class ThumbnailSeriesCollectionDeleted(val thumbnail: ThumbnailSeriesCollection) : DomainEvent()
data class ThumbnailReadListAdded(val thumbnail: ThumbnailReadList) : DomainEvent()
data class ThumbnailReadListDeleted(val thumbnail: ThumbnailReadList) : DomainEvent()
data class UserUpdated(val user: KomgaUser, val expireSession: Boolean) : DomainEvent()
data class UserDeleted(val user: KomgaUser) : DomainEvent()
}

View file

@ -15,13 +15,23 @@ open class CodedException : Exception {
fun Exception.withCode(code: String) = CodedException(this, code)
class MediaNotReadyException : Exception()
class NoThumbnailFoundException : Exception()
class MediaUnsupportedException(message: String, code: String = "") : CodedException(message, code)
class ImageConversionException(message: String, code: String = "") : CodedException(message, code)
class DirectoryNotFoundException(message: String, code: String = "") : CodedException(message, code)
class DuplicateNameException(message: String, code: String = "") : CodedException(message, code)
class PathContainedInPath(message: String, code: String = "") : CodedException(message, code)
class UserEmailAlreadyExistsException(message: String, code: String = "") : CodedException(message, code)
class BookConversionException(message: String) : Exception(message)
class ComicRackListException(message: String, code: String = "") : CodedException(message, code)
class EntryNotFoundException(message: String) : Exception(message)

View file

@ -16,7 +16,8 @@ sealed class HistoricalEvent(
type = "BookFileDeleted",
bookId = book.id,
seriesId = book.seriesId,
properties = mapOf(
properties =
mapOf(
"reason" to reason,
"name" to book.path.toString(),
),
@ -25,7 +26,8 @@ sealed class HistoricalEvent(
class SeriesFolderDeleted(seriesId: String, seriesPath: Path, reason: String) : HistoricalEvent(
type = "SeriesFolderDeleted",
seriesId = seriesId,
properties = mapOf(
properties =
mapOf(
"reason" to reason,
"name" to seriesPath.toString(),
),
@ -37,7 +39,8 @@ sealed class HistoricalEvent(
type = "BookConverted",
bookId = book.id,
seriesId = book.seriesId,
properties = mapOf(
properties =
mapOf(
"name" to book.path.toString(),
"former file" to previous.path.toString(),
),
@ -47,7 +50,8 @@ sealed class HistoricalEvent(
type = "BookImported",
bookId = book.id,
seriesId = series.id,
properties = mapOf(
properties =
mapOf(
"name" to book.path.toString(),
"source" to source.toString(),
"upgrade" to if (upgrade) "Yes" else "No",
@ -58,7 +62,8 @@ sealed class HistoricalEvent(
type = "DuplicatePageDeleted",
bookId = book.id,
seriesId = book.seriesId,
properties = mapOf(
properties =
mapOf(
"name" to book.path.toString(),
"page number" to page.pageNumber.toString(),
"page file name" to page.fileName,

View file

@ -27,7 +27,6 @@ data class KomgaUser(
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable {
@delegate:Transient
val roles: Set<String> by lazy {
buildSet {
@ -65,20 +64,26 @@ data class KomgaUser(
return sharedAllLibraries || sharedLibrariesIds.any { it == library.id }
}
fun isContentAllowed(ageRating: Int? = null, sharingLabels: Set<String> = emptySet()): Boolean {
fun isContentAllowed(
ageRating: Int? = null,
sharingLabels: Set<String> = emptySet(),
): Boolean {
val labels = sharingLabels.lowerNotBlank().toSet()
val ageAllowed =
if (restrictions.ageRestriction?.restriction == AllowExclude.ALLOW_ONLY)
ageRating != null && ageRating <= restrictions.ageRestriction.age
else null
else
null
val labelAllowed =
if (restrictions.labelsAllow.isNotEmpty())
restrictions.labelsAllow.intersect(labels).isNotEmpty()
else null
else
null
val allowed = when {
val allowed =
when {
ageAllowed == null -> labelAllowed != false
labelAllowed == null -> ageAllowed != false
else -> ageAllowed != false || labelAllowed != false
@ -88,12 +93,14 @@ data class KomgaUser(
val ageDenied =
if (restrictions.ageRestriction?.restriction == AllowExclude.EXCLUDE)
ageRating != null && ageRating >= restrictions.ageRestriction.age
else false
else
false
val labelDenied =
if (restrictions.labelsExclude.isNotEmpty())
restrictions.labelsExclude.intersect(labels).isNotEmpty()
else false
else
false
return !ageDenied && !labelDenied
}

View file

@ -34,15 +34,11 @@ data class Library(
val hashPages: Boolean = false,
val analyzeDimensions: Boolean = true,
val oneshotsDirectory: String? = null,
val unavailableDate: LocalDateTime? = null,
val id: String = TsidCreator.getTsid256().toString(),
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable {
enum class SeriesCover {
FIRST,
FIRST_UNREAD_OR_FIRST,

View file

@ -1,5 +1,7 @@
package org.gotson.komga.domain.model
enum class MarkSelectedPreference {
NO, YES, IF_NONE_OR_GENERATED
NO,
YES,
IF_NONE_OR_GENERATED,
}

View file

@ -15,12 +15,15 @@ data class Media(
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable {
@delegate:Transient
val profile: MediaProfile? by lazy { MediaType.fromMediaType(mediaType)?.profile }
enum class Status {
UNKNOWN, ERROR, READY, UNSUPPORTED, OUTDATED
UNKNOWN,
ERROR,
READY,
UNSUPPORTED,
OUTDATED,
}
override fun toString(): String {

View file

@ -8,17 +8,19 @@ interface MediaExtension
class ProxyExtension private constructor(
val extensionClassName: String,
) : MediaExtension {
companion object {
fun of(extensionClass: String?): ProxyExtension? =
extensionClass?.let {
val kClass = Class.forName(extensionClass).kotlin
if (kClass.qualifiedName != MediaExtension::class.qualifiedName && kClass.isSubclassOf(MediaExtension::class)) ProxyExtension(extensionClass)
else null
if (kClass.qualifiedName != MediaExtension::class.qualifiedName && kClass.isSubclassOf(MediaExtension::class))
ProxyExtension(extensionClass)
else
null
}
}
inline fun <reified T> proxyForType(): Boolean = T::class.qualifiedName == extensionClassName
fun proxyForType(clazz: KClass<out Any>): Boolean = clazz.qualifiedName == extensionClassName
}

View file

@ -7,6 +7,7 @@ data class MediaFile(
val fileSize: Long? = null,
) {
enum class SubType {
EPUB_PAGE, EPUB_ASSET
EPUB_PAGE,
EPUB_ASSET,
}
}

View file

@ -1,5 +1,8 @@
package org.gotson.komga.domain.model
enum class MetadataPatchTarget {
BOOK, SERIES, READLIST, COLLECTION
BOOK,
SERIES,
READLIST,
COLLECTION,
}

View file

@ -8,7 +8,6 @@ class PageHashKnown(
val action: Action,
val deleteCount: Int = 0,
val matchCount: Int = 0,
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable, PageHash(hash, size) {

View file

@ -44,7 +44,6 @@ data class R2Locator(
*/
val text: Text? = null,
) {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
data class Location(
/**

View file

@ -9,8 +9,9 @@ data class R2Progression(
val locator: R2Locator,
)
fun ReadProgress.toR2Progression() = R2Progression(
fun ReadProgress.toR2Progression() =
R2Progression(
modified = readDate.toZonedDateTime(),
device = R2Device(deviceId, deviceName),
locator = locator ?: R2Locator("", ""),
)
)

View file

@ -11,14 +11,10 @@ data class ReadList(
* Indicates whether the read list is ordered manually
*/
val ordered: Boolean = true,
val bookIds: SortedMap<Int, String> = sortedMapOf(),
val id: String = TsidCreator.getTsid256().toString(),
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
/**
* Indicates that the bookIds have been filtered and is not exhaustive.
*/

View file

@ -11,7 +11,6 @@ data class ReadProgress(
val deviceId: String = "",
val deviceName: String = "",
val locator: R2Locator? = null,
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable

View file

@ -1,5 +1,7 @@
package org.gotson.komga.domain.model
enum class ReadStatus {
UNREAD, READ, IN_PROGRESS
UNREAD,
READ,
IN_PROGRESS,
}

View file

@ -10,18 +10,14 @@ data class Series(
val name: String,
val url: URL,
val fileLastModified: LocalDateTime,
val id: String = TsidCreator.getTsid256().toString(),
val libraryId: String = "",
val bookCount: Int = 0,
val deletedDate: LocalDateTime? = null,
val oneshot: Boolean = false,
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable {
@delegate:Transient
val path: Path by lazy { this.url.toURI().toPath() }
}

View file

@ -9,14 +9,10 @@ data class SeriesCollection(
* Indicates whether the collection is ordered manually
*/
val ordered: Boolean = false,
val seriesIds: List<String> = emptyList(),
val id: String = TsidCreator.getTsid256().toString(),
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
/**
* Indicates that the seriesIds have been filtered and is not exhaustive.
*/

View file

@ -18,7 +18,6 @@ class SeriesMetadata(
sharingLabels: Set<String> = emptySet(),
val links: List<WebLink> = emptyList(),
val alternateTitles: List<AlternateTitle> = emptyList(),
val statusLock: Boolean = false,
val titleLock: Boolean = false,
val titleSortLock: Boolean = false,
@ -33,9 +32,7 @@ class SeriesMetadata(
val sharingLabelsLock: Boolean = false,
val linksLock: Boolean = false,
val alternateTitlesLock: Boolean = false,
val seriesId: String = "",
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable {
@ -116,7 +113,10 @@ class SeriesMetadata(
)
enum class Status {
ENDED, ONGOING, ABANDONED, HIATUS
ENDED,
ONGOING,
ABANDONED,
HIATUS,
}
enum class ReadingDirection {

View file

@ -11,6 +11,5 @@ data class SeriesMetadataPatch(
val language: String?,
val genres: Set<String>?,
val totalBookCount: Int?,
val collections: Set<String>,
)

View file

@ -12,7 +12,9 @@ open class SeriesSearch(
val oneshot: Boolean? = null,
) {
enum class SearchField {
NAME, TITLE, TITLE_SORT
NAME,
TITLE,
TITLE_SORT,
}
}
@ -44,4 +46,4 @@ class SeriesSearchWithReadProgress(
deleted = deleted,
complete = complete,
oneshot = oneshot,
)
)

View file

@ -10,13 +10,14 @@ data class Sidecar(
val type: Type,
val source: Source,
) {
enum class Type {
ARTWORK, METADATA
ARTWORK,
METADATA,
}
enum class Source {
SERIES, BOOK
SERIES,
BOOK,
}
}

View file

@ -14,15 +14,15 @@ data class ThumbnailBook(
val mediaType: String,
val fileSize: Long,
val dimension: Dimension,
val id: String = TsidCreator.getTsid256().toString(),
val bookId: String = "",
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable {
enum class Type {
GENERATED, SIDECAR, USER_UPLOADED
GENERATED,
SIDECAR,
USER_UPLOADED,
}
fun exists(): Boolean {
@ -39,7 +39,8 @@ data class ThumbnailBook(
if (thumbnail != null) {
if (other.thumbnail == null) return false
if (!thumbnail.contentEquals(other.thumbnail)) return false
} else if (other.thumbnail != null) 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

View file

@ -10,10 +10,8 @@ data class ThumbnailReadList(
val mediaType: String,
val fileSize: Long,
val dimension: Dimension,
val id: String = TsidCreator.getTsid256().toString(),
val readListId: String = "",
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable {

View file

@ -14,15 +14,14 @@ data class ThumbnailSeries(
val mediaType: String,
val fileSize: Long,
val dimension: Dimension,
val id: String = TsidCreator.getTsid256().toString(),
val seriesId: String = "",
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable {
enum class Type {
SIDECAR, USER_UPLOADED
SIDECAR,
USER_UPLOADED,
}
fun exists(): Boolean {
@ -39,7 +38,8 @@ data class ThumbnailSeries(
if (thumbnail != null) {
if (other.thumbnail == null) return false
if (!thumbnail.contentEquals(other.thumbnail)) return false
} else if (other.thumbnail != null) 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

View file

@ -10,10 +10,8 @@ data class ThumbnailSeriesCollection(
val mediaType: String,
val fileSize: Long,
val dimension: Dimension,
val id: String = TsidCreator.getTsid256().toString(),
val collectionId: String = "",
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable {

View file

@ -8,11 +8,17 @@ import java.time.LocalDateTime
interface AuthenticationActivityRepository {
fun findAll(pageable: Pageable): Page<AuthenticationActivity>
fun findAllByUser(user: KomgaUser, pageable: Pageable): Page<AuthenticationActivity>
fun findAllByUser(
user: KomgaUser,
pageable: Pageable,
): Page<AuthenticationActivity>
fun findMostRecentByUser(user: KomgaUser): AuthenticationActivity?
fun insert(activity: AuthenticationActivity)
fun deleteByUser(user: KomgaUser)
fun deleteOlderThan(dateTime: LocalDateTime)
}

View file

@ -4,12 +4,15 @@ import org.gotson.komga.domain.model.BookMetadataAggregation
interface BookMetadataAggregationRepository {
fun findById(seriesId: String): BookMetadataAggregation
fun findByIdOrNull(seriesId: String): BookMetadataAggregation?
fun insert(metadata: BookMetadataAggregation)
fun update(metadata: BookMetadataAggregation)
fun delete(seriesId: String)
fun delete(seriesIds: Collection<String>)
fun count(): Long

View file

@ -4,17 +4,21 @@ import org.gotson.komga.domain.model.BookMetadata
interface BookMetadataRepository {
fun findById(bookId: String): BookMetadata
fun findByIdOrNull(bookId: String): BookMetadata?
fun findAllByIds(bookIds: Collection<String>): Collection<BookMetadata>
fun insert(metadata: BookMetadata)
fun insert(metadatas: Collection<BookMetadata>)
fun update(metadata: BookMetadata)
fun update(metadatas: Collection<BookMetadata>)
fun delete(bookId: String)
fun delete(bookIds: Collection<String>)
fun count(): Long

View file

@ -10,40 +10,85 @@ import java.net.URL
interface BookRepository {
fun findByIdOrNull(bookId: String): Book?
fun findNotDeletedByLibraryIdAndUrlOrNull(libraryId: String, url: URL): Book?
fun findNotDeletedByLibraryIdAndUrlOrNull(
libraryId: String,
url: URL,
): Book?
fun findAll(): Collection<Book>
fun findAllBySeriesId(seriesId: String): Collection<Book>
fun findAllBySeriesIds(seriesIds: Collection<String>): Collection<Book>
fun findAllNotDeletedByLibraryIdAndUrlNotIn(libraryId: String, urls: Collection<URL>): Collection<Book>
fun findAllNotDeletedByLibraryIdAndUrlNotIn(
libraryId: String,
urls: Collection<URL>,
): Collection<Book>
fun findAll(bookSearch: BookSearch): Collection<Book>
fun findAll(bookSearch: BookSearch, pageable: Pageable): Page<Book>
fun findAll(
bookSearch: BookSearch,
pageable: Pageable,
): Page<Book>
fun findAllDeletedByFileSize(fileSize: Long): Collection<Book>
fun findAllByLibraryIdAndWithEmptyHash(libraryId: String): Collection<Book>
fun findAllByLibraryIdAndMediaTypes(libraryId: String, mediaTypes: Collection<String>): Collection<Book>
fun findAllByLibraryIdAndMismatchedExtension(libraryId: String, mediaType: String, extension: String): Collection<Book>
fun findAllByLibraryIdAndMediaTypes(
libraryId: String,
mediaTypes: Collection<String>,
): Collection<Book>
fun findAllByLibraryIdAndMismatchedExtension(
libraryId: String,
mediaType: String,
extension: String,
): Collection<Book>
fun getLibraryIdOrNull(bookId: String): String?
fun getSeriesIdOrNull(bookId: String): String?
fun findFirstIdInSeriesOrNull(seriesId: String): String?
fun findLastIdInSeriesOrNull(seriesId: String): String?
fun findFirstUnreadIdInSeriesOrNull(seriesId: String, userId: String): String?
fun findFirstUnreadIdInSeriesOrNull(
seriesId: String,
userId: String,
): String?
fun findAllIdsBySeriesId(seriesId: String): Collection<String>
fun findAllIdsBySeriesIds(seriesIds: Collection<String>): Collection<String>
fun findAllIdsByLibraryId(libraryId: String): Collection<String>
fun findAllIds(bookSearch: BookSearch, sort: Sort): Collection<String>
fun findAllIds(
bookSearch: BookSearch,
sort: Sort,
): Collection<String>
fun insert(book: Book)
fun insert(books: Collection<Book>)
fun update(book: Book)
fun update(books: Collection<Book>)
fun delete(bookId: String)
fun delete(bookIds: Collection<String>)
fun deleteAll()
fun count(): Long
fun countGroupedByLibraryId(): Map<String, Int>
fun getFilesizeGroupedByLibraryId(): Map<String, BigDecimal>

View file

@ -6,6 +6,7 @@ interface KomgaUserRepository {
fun count(): Long
fun findByIdOrNull(id: String): KomgaUser?
fun findByEmailIgnoreCaseOrNull(email: String): KomgaUser?
fun findAll(): Collection<KomgaUser>
@ -13,11 +14,17 @@ interface KomgaUserRepository {
fun existsByEmailIgnoreCase(email: String): Boolean
fun insert(user: KomgaUser)
fun update(user: KomgaUser)
fun delete(userId: String)
fun deleteAll()
fun findAnnouncementIdsReadByUserId(userId: String): Set<String>
fun saveAnnouncementIdsRead(user: KomgaUser, announcementIds: Set<String>)
fun saveAnnouncementIdsRead(
user: KomgaUser,
announcementIds: Set<String>,
)
}

View file

@ -4,15 +4,19 @@ import org.gotson.komga.domain.model.Library
interface LibraryRepository {
fun findById(libraryId: String): Library
fun findByIdOrNull(libraryId: String): Library?
fun findAll(): Collection<Library>
fun findAllByIds(libraryIds: Collection<String>): Collection<Library>
fun delete(libraryId: String)
fun deleteAll()
fun insert(library: Library)
fun update(library: Library)
fun count(): Long

View file

@ -5,20 +5,29 @@ import org.gotson.komga.domain.model.MediaExtension
interface MediaRepository {
fun findById(bookId: String): Media
fun findByIdOrNull(bookId: String): Media?
fun findAllBookIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(libraryId: String, mediaTypes: Collection<String>, pageHashing: Int): Collection<String>
fun findAllBookIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(
libraryId: String,
mediaTypes: Collection<String>,
pageHashing: Int,
): Collection<String>
fun getPagesSize(bookId: String): Int
fun getPagesSizes(bookIds: Collection<String>): Collection<Pair<String, Int>>
fun findExtensionByIdOrNull(bookId: String): MediaExtension?
fun insert(media: Media)
fun insert(medias: Collection<Media>)
fun update(media: Media)
fun delete(bookId: String)
fun deleteByBookIds(bookIds: Collection<String>)
fun count(): Long

View file

@ -9,14 +9,30 @@ import org.springframework.data.domain.Pageable
interface PageHashRepository {
fun findKnown(pageHash: String): PageHashKnown?
fun findAllKnown(actions: List<PageHashKnown.Action>?, pageable: Pageable): Page<PageHashKnown>
fun findAllKnown(
actions: List<PageHashKnown.Action>?,
pageable: Pageable,
): Page<PageHashKnown>
fun findAllUnknown(pageable: Pageable): Page<PageHashUnknown>
fun findMatchesByHash(pageHash: String, pageable: Pageable): Page<PageHashMatch>
fun findMatchesByKnownHashAction(actions: List<PageHashKnown.Action>?, libraryId: String?): Map<String, Collection<BookPageNumbered>>
fun findMatchesByHash(
pageHash: String,
pageable: Pageable,
): Page<PageHashMatch>
fun findMatchesByKnownHashAction(
actions: List<PageHashKnown.Action>?,
libraryId: String?,
): Map<String, Collection<BookPageNumbered>>
fun getKnownThumbnail(pageHash: String): ByteArray?
fun insert(pageHash: PageHashKnown, thumbnail: ByteArray?)
fun insert(
pageHash: PageHashKnown,
thumbnail: ByteArray?,
)
fun update(pageHash: PageHashKnown)
}

View file

@ -10,33 +10,51 @@ interface ReadListRepository {
* Find one ReadList by [readListId],
* optionally with only bookIds filtered by the provided [filterOnLibraryIds] it not null.
*/
fun findByIdOrNull(readListId: String, filterOnLibraryIds: Collection<String>? = null, restrictions: ContentRestrictions = ContentRestrictions()): ReadList?
fun findByIdOrNull(
readListId: String,
filterOnLibraryIds: Collection<String>? = null,
restrictions: ContentRestrictions = ContentRestrictions(),
): ReadList?
/**
* Find all ReadList
* optionally with at least one Book belonging to the provided [belongsToLibraryIds] if not null,
* optionally with only bookIds filtered by the provided [filterOnLibraryIds] if not null.
*/
fun findAll(belongsToLibraryIds: Collection<String>? = null, filterOnLibraryIds: Collection<String>? = null, search: String? = null, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page<ReadList>
fun findAll(
belongsToLibraryIds: Collection<String>? = null,
filterOnLibraryIds: Collection<String>? = null,
search: String? = null,
pageable: Pageable,
restrictions: ContentRestrictions = ContentRestrictions(),
): Page<ReadList>
/**
* Find all ReadList that contains the provided [containsBookId],
* optionally with only bookIds filtered by the provided [filterOnLibraryIds] if not null.
*/
fun findAllContainingBookId(containsBookId: String, filterOnLibraryIds: Collection<String>?, restrictions: ContentRestrictions = ContentRestrictions()): Collection<ReadList>
fun findAllContainingBookId(
containsBookId: String,
filterOnLibraryIds: Collection<String>?,
restrictions: ContentRestrictions = ContentRestrictions(),
): Collection<ReadList>
fun findAllEmpty(): Collection<ReadList>
fun findByNameOrNull(name: String): ReadList?
fun insert(readList: ReadList)
fun update(readList: ReadList)
fun removeBookFromAll(bookId: String)
fun removeBooksFromAll(bookIds: Collection<String>)
fun delete(readListId: String)
fun delete(readListIds: Collection<String>)
fun deleteAll()
fun existsByName(name: String): Boolean

View file

@ -3,21 +3,43 @@ package org.gotson.komga.domain.persistence
import org.gotson.komga.domain.model.ReadProgress
interface ReadProgressRepository {
fun findByBookIdAndUserIdOrNull(bookId: String, userId: String): ReadProgress?
fun findByBookIdAndUserIdOrNull(
bookId: String,
userId: String,
): ReadProgress?
fun findAll(): Collection<ReadProgress>
fun findAllByUserId(userId: String): Collection<ReadProgress>
fun findAllByBookId(bookId: String): Collection<ReadProgress>
fun findAllByBookIdsAndUserId(bookIds: Collection<String>, userId: String): Collection<ReadProgress>
fun findAllByBookIdsAndUserId(
bookIds: Collection<String>,
userId: String,
): Collection<ReadProgress>
fun save(readProgress: ReadProgress)
fun save(readProgresses: Collection<ReadProgress>)
fun delete(bookId: String, userId: String)
fun delete(
bookId: String,
userId: String,
)
fun deleteByUserId(userId: String)
fun deleteByBookId(bookId: String)
fun deleteByBookIds(bookIds: Collection<String>)
fun deleteByBookIdsAndUserId(bookIds: Collection<String>, userId: String)
fun deleteByBookIdsAndUserId(
bookIds: Collection<String>,
userId: String,
)
fun deleteBySeriesIds(seriesIds: Collection<String>)
fun deleteAll()
}

View file

@ -6,51 +6,185 @@ import org.springframework.data.domain.Pageable
import java.time.LocalDate
interface ReferentialRepository {
fun findAllAuthorsByName(search: String, filterOnLibraryIds: Collection<String>?): List<Author>
fun findAllAuthorsByNameAndLibrary(search: String, libraryId: String, filterOnLibraryIds: Collection<String>?): List<Author>
fun findAllAuthorsByNameAndCollection(search: String, collectionId: String, filterOnLibraryIds: Collection<String>?): List<Author>
fun findAllAuthorsByNameAndSeries(search: String, seriesId: String, filterOnLibraryIds: Collection<String>?): List<Author>
fun findAllAuthorsNamesByName(search: String, filterOnLibraryIds: Collection<String>?): List<String>
fun findAllAuthorsByName(
search: String,
filterOnLibraryIds: Collection<String>?,
): List<Author>
fun findAllAuthorsByNameAndLibrary(
search: String,
libraryId: String,
filterOnLibraryIds: Collection<String>?,
): List<Author>
fun findAllAuthorsByNameAndCollection(
search: String,
collectionId: String,
filterOnLibraryIds: Collection<String>?,
): List<Author>
fun findAllAuthorsByNameAndSeries(
search: String,
seriesId: String,
filterOnLibraryIds: Collection<String>?,
): List<Author>
fun findAllAuthorsNamesByName(
search: String,
filterOnLibraryIds: Collection<String>?,
): List<String>
fun findAllAuthorsRoles(filterOnLibraryIds: Collection<String>?): List<String>
fun findAllAuthorsByName(search: String?, role: String?, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author>
fun findAllAuthorsByNameAndLibrary(search: String?, role: String?, libraryId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author>
fun findAllAuthorsByNameAndCollection(search: String?, role: String?, collectionId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author>
fun findAllAuthorsByNameAndSeries(search: String?, role: String?, seriesId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author>
fun findAllAuthorsByNameAndReadList(search: String?, role: String?, readListId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author>
fun findAllAuthorsByName(
search: String?,
role: String?,
filterOnLibraryIds: Collection<String>?,
pageable: Pageable,
): Page<Author>
fun findAllAuthorsByNameAndLibrary(
search: String?,
role: String?,
libraryId: String,
filterOnLibraryIds: Collection<String>?,
pageable: Pageable,
): Page<Author>
fun findAllAuthorsByNameAndCollection(
search: String?,
role: String?,
collectionId: String,
filterOnLibraryIds: Collection<String>?,
pageable: Pageable,
): Page<Author>
fun findAllAuthorsByNameAndSeries(
search: String?,
role: String?,
seriesId: String,
filterOnLibraryIds: Collection<String>?,
pageable: Pageable,
): Page<Author>
fun findAllAuthorsByNameAndReadList(
search: String?,
role: String?,
readListId: String,
filterOnLibraryIds: Collection<String>?,
pageable: Pageable,
): Page<Author>
fun findAllGenres(filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllGenresByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllGenresByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllGenresByLibrary(
libraryId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllGenresByCollection(
collectionId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllSeriesAndBookTags(filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllSeriesAndBookTagsByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllSeriesAndBookTagsByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllSeriesAndBookTagsByLibrary(
libraryId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllSeriesAndBookTagsByCollection(
collectionId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllSeriesTags(filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllSeriesTagsByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllSeriesTagsByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllSeriesTagsByLibrary(
libraryId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllSeriesTagsByCollection(
collectionId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllBookTags(filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllBookTagsBySeries(seriesId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllBookTagsByReadList(readListId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllBookTagsBySeries(
seriesId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllBookTagsByReadList(
readListId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllLanguages(filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllLanguagesByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllLanguagesByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllLanguagesByLibrary(
libraryId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllLanguagesByCollection(
collectionId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllPublishers(filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllPublishers(filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<String>
fun findAllPublishersByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllPublishersByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllPublishers(
filterOnLibraryIds: Collection<String>?,
pageable: Pageable,
): Page<String>
fun findAllPublishersByLibrary(
libraryId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllPublishersByCollection(
collectionId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllAgeRatings(filterOnLibraryIds: Collection<String>?): Set<Int?>
fun findAllAgeRatingsByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<Int?>
fun findAllAgeRatingsByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<Int?>
fun findAllAgeRatingsByLibrary(
libraryId: String,
filterOnLibraryIds: Collection<String>?,
): Set<Int?>
fun findAllAgeRatingsByCollection(
collectionId: String,
filterOnLibraryIds: Collection<String>?,
): Set<Int?>
fun findAllSeriesReleaseDates(filterOnLibraryIds: Collection<String>?): Set<LocalDate>
fun findAllSeriesReleaseDatesByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<LocalDate>
fun findAllSeriesReleaseDatesByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<LocalDate>
fun findAllSeriesReleaseDatesByLibrary(
libraryId: String,
filterOnLibraryIds: Collection<String>?,
): Set<LocalDate>
fun findAllSeriesReleaseDatesByCollection(
collectionId: String,
filterOnLibraryIds: Collection<String>?,
): Set<LocalDate>
fun findAllSharingLabels(filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllSharingLabelsByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllSharingLabelsByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
fun findAllSharingLabelsByLibrary(
libraryId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
fun findAllSharingLabelsByCollection(
collectionId: String,
filterOnLibraryIds: Collection<String>?,
): Set<String>
}

View file

@ -10,33 +10,51 @@ interface SeriesCollectionRepository {
* Find one SeriesCollection by [collectionId],
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null.
*/
fun findByIdOrNull(collectionId: String, filterOnLibraryIds: Collection<String>? = null, restrictions: ContentRestrictions = ContentRestrictions()): SeriesCollection?
fun findByIdOrNull(
collectionId: String,
filterOnLibraryIds: Collection<String>? = null,
restrictions: ContentRestrictions = ContentRestrictions(),
): SeriesCollection?
/**
* Find all SeriesCollection
* optionally with at least one Series belonging to the provided [belongsToLibraryIds] if not null,
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null.
*/
fun findAll(belongsToLibraryIds: Collection<String>? = null, filterOnLibraryIds: Collection<String>? = null, search: String? = null, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page<SeriesCollection>
fun findAll(
belongsToLibraryIds: Collection<String>? = null,
filterOnLibraryIds: Collection<String>? = null,
search: String? = null,
pageable: Pageable,
restrictions: ContentRestrictions = ContentRestrictions(),
): Page<SeriesCollection>
/**
* Find all SeriesCollection that contains the provided [containsSeriesId],
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null.
*/
fun findAllContainingSeriesId(containsSeriesId: String, filterOnLibraryIds: Collection<String>?, restrictions: ContentRestrictions = ContentRestrictions()): Collection<SeriesCollection>
fun findAllContainingSeriesId(
containsSeriesId: String,
filterOnLibraryIds: Collection<String>?,
restrictions: ContentRestrictions = ContentRestrictions(),
): Collection<SeriesCollection>
fun findAllEmpty(): Collection<SeriesCollection>
fun findByNameOrNull(name: String): SeriesCollection?
fun insert(collection: SeriesCollection)
fun update(collection: SeriesCollection)
fun removeSeriesFromAll(seriesId: String)
fun removeSeriesFromAll(seriesIds: Collection<String>)
fun delete(collectionId: String)
fun delete(collectionIds: Collection<String>)
fun deleteAll()
fun existsByName(name: String): Boolean

View file

@ -4,12 +4,15 @@ import org.gotson.komga.domain.model.SeriesMetadata
interface SeriesMetadataRepository {
fun findById(seriesId: String): SeriesMetadata
fun findByIdOrNull(seriesId: String): SeriesMetadata?
fun insert(metadata: SeriesMetadata)
fun update(metadata: SeriesMetadata)
fun delete(seriesId: String)
fun delete(seriesIds: Collection<String>)
fun count(): Long

View file

@ -6,12 +6,23 @@ import java.net.URL
interface SeriesRepository {
fun findByIdOrNull(seriesId: String): Series?
fun findNotDeletedByLibraryIdAndUrlOrNull(libraryId: String, url: URL): Series?
fun findNotDeletedByLibraryIdAndUrlOrNull(
libraryId: String,
url: URL,
): Series?
fun findAll(): Collection<Series>
fun findAllByLibraryId(libraryId: String): Collection<Series>
fun findAllNotDeletedByLibraryIdAndUrlNotIn(libraryId: String, urls: Collection<URL>): Collection<Series>
fun findAllNotDeletedByLibraryIdAndUrlNotIn(
libraryId: String,
urls: Collection<URL>,
): Collection<Series>
fun findAllByTitleContaining(title: String): Collection<Series>
fun findAll(search: SeriesSearch): Collection<Series>
fun getLibraryId(seriesId: String): String?
@ -19,12 +30,19 @@ interface SeriesRepository {
fun findAllIdsByLibraryId(libraryId: String): Collection<String>
fun insert(series: Series)
fun update(series: Series, updateModifiedTime: Boolean = true)
fun update(
series: Series,
updateModifiedTime: Boolean = true,
)
fun delete(seriesId: String)
fun delete(seriesIds: Collection<String>)
fun deleteAll()
fun count(): Long
fun countGroupedByLibraryId(): Map<String, Int>
}

View file

@ -7,9 +7,16 @@ import java.net.URL
interface SidecarRepository {
fun findAll(): Collection<SidecarStored>
fun save(libraryId: String, sidecar: Sidecar)
fun save(
libraryId: String,
sidecar: Sidecar,
)
fun deleteByLibraryIdAndUrls(
libraryId: String,
urls: Collection<URL>,
)
fun deleteByLibraryIdAndUrls(libraryId: String, urls: Collection<URL>)
fun deleteByLibraryId(libraryId: String)
fun countGroupedByLibraryId(): Map<String, Int>

View file

@ -6,20 +6,39 @@ import org.springframework.data.domain.Pageable
interface ThumbnailBookRepository {
fun findByIdOrNull(thumbnailId: String): ThumbnailBook?
fun findSelectedByBookIdOrNull(bookId: String): ThumbnailBook?
fun findAllByBookId(bookId: String): Collection<ThumbnailBook>
fun findAllByBookIdAndType(bookId: String, type: ThumbnailBook.Type): Collection<ThumbnailBook>
fun findAllByBookIdAndType(
bookId: String,
type: ThumbnailBook.Type,
): Collection<ThumbnailBook>
fun findAllWithoutMetadata(pageable: Pageable): Page<ThumbnailBook>
fun findAllBookIdsByThumbnailTypeAndDimensionSmallerThan(type: ThumbnailBook.Type, size: Int): Collection<String>
fun findAllBookIdsByThumbnailTypeAndDimensionSmallerThan(
type: ThumbnailBook.Type,
size: Int,
): Collection<String>
fun insert(thumbnail: ThumbnailBook)
fun update(thumbnail: ThumbnailBook)
fun updateMetadata(thumbnails: Collection<ThumbnailBook>)
fun markSelected(thumbnail: ThumbnailBook)
fun delete(thumbnailBookId: String)
fun deleteByBookId(bookId: String)
fun deleteByBookIdAndType(bookId: String, type: ThumbnailBook.Type)
fun deleteByBookIdAndType(
bookId: String,
type: ThumbnailBook.Type,
)
fun deleteByBookIds(bookIds: Collection<String>)
}

View file

@ -6,17 +6,24 @@ import org.springframework.data.domain.Pageable
interface ThumbnailReadListRepository {
fun findByIdOrNull(thumbnailId: String): ThumbnailReadList?
fun findSelectedByReadListIdOrNull(readListId: String): ThumbnailReadList?
fun findAllByReadListId(readListId: String): Collection<ThumbnailReadList>
fun findAllWithoutMetadata(pageable: Pageable): Page<ThumbnailReadList>
fun insert(thumbnail: ThumbnailReadList)
fun update(thumbnail: ThumbnailReadList)
fun updateMetadata(thumbnails: Collection<ThumbnailReadList>)
fun markSelected(thumbnail: ThumbnailReadList)
fun delete(thumbnailReadListId: String)
fun deleteByReadListId(readListId: String)
fun deleteByReadListIds(readListIds: Collection<String>)
}

View file

@ -6,17 +6,24 @@ import org.springframework.data.domain.Pageable
interface ThumbnailSeriesCollectionRepository {
fun findByIdOrNull(thumbnailId: String): ThumbnailSeriesCollection?
fun findSelectedByCollectionIdOrNull(collectionId: String): ThumbnailSeriesCollection?
fun findAllByCollectionId(collectionId: String): Collection<ThumbnailSeriesCollection>
fun findAllWithoutMetadata(pageable: Pageable): Page<ThumbnailSeriesCollection>
fun insert(thumbnail: ThumbnailSeriesCollection)
fun update(thumbnail: ThumbnailSeriesCollection)
fun updateMetadata(thumbnails: Collection<ThumbnailSeriesCollection>)
fun markSelected(thumbnail: ThumbnailSeriesCollection)
fun delete(thumbnailCollectionId: String)
fun deleteByCollectionId(collectionId: String)
fun deleteByCollectionIds(collectionIds: Collection<String>)
}

View file

@ -6,17 +6,27 @@ import org.springframework.data.domain.Pageable
interface ThumbnailSeriesRepository {
fun findByIdOrNull(thumbnailId: String): ThumbnailSeries?
fun findSelectedBySeriesIdOrNull(seriesId: String): ThumbnailSeries?
fun findAllBySeriesId(seriesId: String): Collection<ThumbnailSeries>
fun findAllBySeriesIdIdAndType(seriesId: String, type: ThumbnailSeries.Type): Collection<ThumbnailSeries>
fun findAllBySeriesIdIdAndType(
seriesId: String,
type: ThumbnailSeries.Type,
): Collection<ThumbnailSeries>
fun findAllWithoutMetadata(pageable: Pageable): Page<ThumbnailSeries>
fun insert(thumbnail: ThumbnailSeries)
fun updateMetadata(thumbnails: Collection<ThumbnailSeries>)
fun markSelected(thumbnail: ThumbnailSeries)
fun delete(thumbnailSeriesId: String)
fun deleteBySeriesId(seriesId: String)
fun deleteBySeriesIds(seriesIds: Collection<String>)
}

View file

@ -4,6 +4,8 @@ import org.gotson.komga.domain.model.TransientBook
interface TransientBookRepository {
fun findByIdOrNull(transientBookId: String): TransientBook?
fun save(transientBook: TransientBook)
fun save(transientBooks: Collection<TransientBook>)
}

View file

@ -51,22 +51,27 @@ class BookAnalyzer(
@Qualifier("pdfImageType")
private val pdfImageType: ImageType,
) {
val divinaExtractors = extractors
val divinaExtractors =
extractors
.flatMap { e -> e.mediaTypes().map { it to e } }
.toMap()
fun analyze(book: Book, analyzeDimensions: Boolean): Media {
fun analyze(
book: Book,
analyzeDimensions: Boolean,
): Media {
logger.info { "Trying to analyze book: $book" }
return try {
var mediaType = contentDetector.detectMediaType(book.path).let {
var mediaType =
contentDetector.detectMediaType(book.path).let {
logger.info { "Detected media type: $it" }
MediaType.fromMediaType(it) ?: return Media(mediaType = it, status = Media.Status.UNSUPPORTED, comment = "ERR_1001", bookId = book.id)
}
if (book.path.extension.lowercase() == "epub" && mediaType != MediaType.EPUB) {
if (epubExtractor.isEpub(book.path)) mediaType = MediaType.EPUB
else {
if (epubExtractor.isEpub(book.path)) {
mediaType = MediaType.EPUB
} else {
logger.warn { "Epub file is malformed, file is probably broken: ${book.path}" }
return Media(mediaType = mediaType.type, status = Media.Status.ERROR, comment = "ERR_1032", bookId = book.id)
}
@ -89,8 +94,13 @@ class BookAnalyzer(
}.copy(bookId = book.id)
}
private fun analyzeDivina(book: Book, mediaType: MediaType, analyzeDimensions: Boolean): Media {
val entries = try {
private fun analyzeDivina(
book: Book,
mediaType: MediaType,
analyzeDimensions: Boolean,
): Media {
val entries =
try {
divinaExtractors[mediaType.type]?.getEntries(book.path, analyzeDimensions)
?: return Media(status = Media.Status.UNSUPPORTED)
} catch (ex: MediaUnsupportedException) {
@ -100,7 +110,8 @@ class BookAnalyzer(
return Media(status = Media.Status.ERROR, comment = "ERR_1008")
}
val (pages, others) = entries
val (pages, others) =
entries
.partition { entry ->
entry.mediaType?.let { contentDetector.isImage(it) } ?: false
}.let { (images, others) ->
@ -110,7 +121,8 @@ class BookAnalyzer(
)
}
val entriesErrorSummary = others
val entriesErrorSummary =
others
.filter { it.mediaType.isNullOrBlank() }
.map { it.name }
.ifEmpty { null }
@ -127,9 +139,13 @@ class BookAnalyzer(
return Media(status = Media.Status.READY, pages = pages, pageCount = pages.size, files = files, comment = entriesErrorSummary)
}
private fun analyzeEpub(book: Book, analyzeDimensions: Boolean): Media {
private fun analyzeEpub(
book: Book,
analyzeDimensions: Boolean,
): Media {
val manifest = epubExtractor.getManifest(book.path, analyzeDimensions)
val entriesErrorSummary = manifest.missingResources
val entriesErrorSummary =
manifest.missingResources
.map { it.fileName }
.ifEmpty { null }
?.joinToString(prefix = "ERR_1033 [", postfix = "]") { it }
@ -139,7 +155,8 @@ class BookAnalyzer(
files = manifest.resources,
pageCount = manifest.pageCount,
epubDivinaCompatible = manifest.divinaPages.isNotEmpty(),
extension = MediaExtensionEpub(
extension =
MediaExtensionEpub(
toc = manifest.toc,
landmarks = manifest.landmarks,
pageList = manifest.pageList,
@ -150,7 +167,10 @@ class BookAnalyzer(
)
}
private fun analyzePdf(book: Book, analyzeDimensions: Boolean): Media {
private fun analyzePdf(
book: Book,
analyzeDimensions: Boolean,
): Media {
val pages = pdfExtractor.getPages(book.path, analyzeDimensions).map { BookPage(it.name, "", it.dimension) }
return Media(status = Media.Status.READY, pages = pages)
}
@ -167,7 +187,8 @@ class BookAnalyzer(
throw MediaNotReadyException()
}
val thumbnail = getPoster(book)?.let { cover ->
val thumbnail =
getPoster(book)?.let { cover ->
imageConverter.resizeImageToByteArray(cover.bytes, thumbnailType, komgaSettingsProvider.thumbnailSize.maxEdge)
} ?: throw NoThumbnailFoundException()
@ -181,8 +202,10 @@ class BookAnalyzer(
)
}
fun getPoster(book: BookWithMedia): TypedBytes? = when (book.media.profile) {
MediaProfile.DIVINA -> divinaExtractors[book.media.mediaType]?.getEntryStream(book.book.path, book.media.pages.first().fileName)?.let {
fun getPoster(book: BookWithMedia): TypedBytes? =
when (book.media.profile) {
MediaProfile.DIVINA ->
divinaExtractors[book.media.mediaType]?.getEntryStream(book.book.path, book.media.pages.first().fileName)?.let {
TypedBytes(
it,
book.media.pages.first().mediaType,
@ -198,7 +221,10 @@ class BookAnalyzer(
MediaNotReadyException::class,
IndexOutOfBoundsException::class,
)
fun getPageContent(book: BookWithMedia, number: Int): ByteArray {
fun getPageContent(
book: BookWithMedia,
number: Int,
): ByteArray {
logger.debug { "Get page #$number for book: $book" }
if (book.media.status != Media.Status.READY) {
@ -215,8 +241,10 @@ class BookAnalyzer(
MediaProfile.DIVINA -> divinaExtractors.getValue(book.media.mediaType!!).getEntryStream(book.book.path, book.media.pages[number - 1].fileName)
MediaProfile.PDF -> pdfExtractor.getPageContentAsImage(book.book.path, number).bytes
MediaProfile.EPUB ->
if (book.media.epubDivinaCompatible) epubExtractor.getEntryStream(book.book.path, book.media.pages[number - 1].fileName)
else throw MediaUnsupportedException("Epub profile does not support getting page content")
if (book.media.epubDivinaCompatible)
epubExtractor.getEntryStream(book.book.path, book.media.pages[number - 1].fileName)
else
throw MediaUnsupportedException("Epub profile does not support getting page content")
null -> throw MediaNotReadyException()
}
@ -226,7 +254,10 @@ class BookAnalyzer(
MediaNotReadyException::class,
IndexOutOfBoundsException::class,
)
fun getPageContentRaw(book: BookWithMedia, number: Int): TypedBytes {
fun getPageContentRaw(
book: BookWithMedia,
number: Int,
): TypedBytes {
logger.debug { "Get raw page #$number for book: $book" }
if (book.media.profile != MediaProfile.PDF) throw MediaUnsupportedException("Extractor does not support raw extraction of pages")
@ -246,7 +277,10 @@ class BookAnalyzer(
@Throws(
MediaNotReadyException::class,
)
fun getFileContent(book: BookWithMedia, fileName: String): ByteArray {
fun getFileContent(
book: BookWithMedia,
fileName: String,
): ByteArray {
logger.debug { "Get file $fileName for book: $book" }
if (book.media.status != Media.Status.READY) {
@ -268,12 +302,15 @@ class BookAnalyzer(
* See [org.gotson.komga.infrastructure.configuration.KomgaProperties.pageHashing]
*/
fun hashPages(book: BookWithMedia): Media {
val hashedPages = book.media.pages.mapIndexed { index, bookPage ->
val hashedPages =
book.media.pages.mapIndexed { index, bookPage ->
if (bookPage.fileHash.isBlank() && (index < pageHashing || index >= (book.media.pageCount - pageHashing))) {
val content = getPageContent(book, index + 1)
val hash = hashPage(bookPage, content)
bookPage.copy(fileHash = hash)
} else bookPage
} else {
bookPage
}
}
return book.media.copy(pages = hashedPages)
@ -284,7 +321,10 @@ class BookAnalyzer(
*
* For JPEG, the image is read/written to remove the metadata.
*/
fun hashPage(page: BookPage, content: ByteArray): String {
fun hashPage(
page: BookPage,
content: ByteArray,
): String {
val bytes =
if (page.mediaType == ImageType.JPEG.mediaType) {
// JPEG could contain different EXIF data, reading and writing back the image will get rid of it
@ -292,7 +332,9 @@ class BookAnalyzer(
ImageIO.write(ImageIO.read(content.inputStream()), ImageType.JPEG.imageIOFormat, buffer)
buffer.toByteArray()
}
} else content
} else {
content
}
return hasher.computeHash(bytes.inputStream())
}

View file

@ -49,7 +49,6 @@ class BookConverter(
private val eventPublisher: ApplicationEventPublisher,
private val historicalEventRepository: HistoricalEventRepository,
) {
private val convertibleTypes = listOf(MediaType.RAR_4.type, MediaType.RAR_5.type)
private val mediaTypeToExtension =
@ -60,10 +59,10 @@ class BookConverter(
private val skippedRepairs = mutableListOf<String>()
fun getConvertibleBooks(library: Library): Collection<Book> =
if (library.convertToCbz)
if (library.convertToCbz) {
bookRepository.findAllByLibraryIdAndMediaTypes(library.id, convertibleTypes)
.also { logger.info { "Found ${it.size} books to convert" } }
else {
} else {
logger.info { "CBZ conversion is not enabled, skipping" }
emptyList()
}
@ -111,7 +110,8 @@ class BookConverter(
}
// perform checks on new file
val convertedBook = fileSystemScanner.scanFile(destinationPath)
val convertedBook =
fileSystemScanner.scanFile(destinationPath)
?.copy(
id = book.id,
seriesId = book.seriesId,
@ -200,7 +200,8 @@ class BookConverter(
logger.info { "Renaming ${book.path} to $destinationPath" }
book.path.moveTo(destinationPath)
val repairedBook = fileSystemScanner.scanFile(destinationPath)
val repairedBook =
fileSystemScanner.scanFile(destinationPath)
?.copy(
id = book.id,
seriesId = book.seriesId,

View file

@ -60,8 +60,13 @@ class BookImporter(
private val taskEmitter: TaskEmitter,
private val historicalEventRepository: HistoricalEventRepository,
) {
fun importBook(sourceFile: Path, series: Series, copyMode: CopyMode, destinationName: String? = null, upgradeBookId: String? = null): Book {
fun importBook(
sourceFile: Path,
series: Series,
copyMode: CopyMode,
destinationName: String? = null,
upgradeBookId: String? = null,
): Book {
try {
if (sourceFile.notExists()) throw FileNotFoundException("File not found: $sourceFile").withCode("ERR_1018")
if (series.oneshot) throw IllegalArgumentException("Destination series is oneshot")
@ -70,14 +75,20 @@ class BookImporter(
if (sourceFile.startsWith(library.path)) throw PathContainedInPath("Cannot import file that is part of an existing library", "ERR_1019")
}
val destFile = series.path.resolve(
if (destinationName != null) Paths.get("$destinationName.${sourceFile.extension}").name
else sourceFile.name,
)
val sidecars = fileSystemScanner.scanBookSidecars(sourceFile).associateWith {
val destFile =
series.path.resolve(
if (destinationName != null) it.url.toURI().toPath().name.replace(sourceFile.nameWithoutExtension, destinationName, true)
else it.url.toURI().toPath().name,
if (destinationName != null)
Paths.get("$destinationName.${sourceFile.extension}").name
else
sourceFile.name,
)
val sidecars =
fileSystemScanner.scanBookSidecars(sourceFile).associateWith {
series.path.resolve(
if (destinationName != null)
it.url.toURI().toPath().name.replace(sourceFile.nameWithoutExtension, destinationName, true)
else
it.url.toURI().toPath().name,
)
}
@ -86,7 +97,9 @@ class BookImporter(
bookRepository.findByIdOrNull(upgradeBookId)?.also {
if (it.seriesId != series.id) throw IllegalArgumentException("Book to upgrade ($upgradeBookId) does not belong to series: $series").withCode("ERR_1020")
}
} else null
} else {
null
}
var deletedUpgradedFile = false
when {
@ -135,7 +148,8 @@ class BookImporter(
}
}
CopyMode.HARDLINK -> try {
CopyMode.HARDLINK ->
try {
logger.info { "Hardlink file $sourceFile to $destFile" }
Files.createLink(destFile, sourceFile)
sidecars.forEach {
@ -154,7 +168,8 @@ class BookImporter(
}
}
val importedBook = fileSystemScanner.scanFile(destFile)
val importedBook =
fileSystemScanner.scanFile(destFile)
?.copy(libraryId = series.libraryId)
?: throw IllegalStateException("Newly imported book could not be scanned: $destFile").withCode("ERR_1022")
@ -208,7 +223,8 @@ class BookImporter(
Sidecar.Type.ARTWORK -> taskEmitter.refreshBookLocalArtwork(importedBook)
Sidecar.Type.METADATA -> taskEmitter.refreshBookMetadata(importedBook)
}
val destSidecar = sourceSidecar.copy(
val destSidecar =
sourceSidecar.copy(
url = destPath.toUri().toURL(),
parentUrl = destPath.parent.toUri().toURL(),
lastModifiedTime = destPath.readAttributes<BasicFileAttributes>().getUpdatedTime(),

View file

@ -69,7 +69,6 @@ class BookLifecycle(
@Qualifier("pdfImageType")
private val pdfImageType: ImageType,
) {
private val resizeTargetFormat = ImageType.JPEG
fun analyzeAndPersist(book: Book): Set<BookAction> {
@ -80,7 +79,8 @@ class BookLifecycle(
// if the number of pages has changed, delete all read progress for that book
mediaRepository.findById(book.id).let { previous ->
if (previous.status == Media.Status.OUTDATED && previous.pageCount != media.pageCount) {
val adjustedProgress = readProgressRepository.findAllByBookId(book.id)
val adjustedProgress =
readProgressRepository.findAllByBookId(book.id)
.map { it.copy(page = if (it.completed) media.pageCount else 1) }
if (adjustedProgress.isNotEmpty()) {
logger.info { "Number of pages differ, adjust read progress for book" }
@ -130,7 +130,10 @@ class BookLifecycle(
}
}
fun addThumbnailForBook(thumbnail: ThumbnailBook, markSelected: MarkSelectedPreference): ThumbnailBook {
fun addThumbnailForBook(
thumbnail: ThumbnailBook,
markSelected: MarkSelectedPreference,
): ThumbnailBook {
when (thumbnail.type) {
ThumbnailBook.Type.GENERATED -> {
// only one generated thumbnail is allowed
@ -153,7 +156,8 @@ class BookLifecycle(
}
}
val selected = when (markSelected) {
val selected =
when (markSelected) {
MarkSelectedPreference.YES -> true
MarkSelectedPreference.IF_NONE_OR_GENERATED -> {
val selectedThumbnail = thumbnailBookRepository.findSelectedByBookIdOrNull(thumbnail.bookId)
@ -163,8 +167,10 @@ class BookLifecycle(
MarkSelectedPreference.NO -> false
}
if (selected) thumbnailBookRepository.markSelected(thumbnail)
else thumbnailsHouseKeeping(thumbnail.bookId)
if (selected)
thumbnailBookRepository.markSelected(thumbnail)
else
thumbnailsHouseKeeping(thumbnail.bookId)
val newThumbnail = thumbnail.copy(selected = selected)
eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(newThumbnail))
@ -189,9 +195,13 @@ class BookLifecycle(
return selected
}
fun getThumbnailBytes(bookId: String, resizeTo: Int? = null): TypedBytes? {
fun getThumbnailBytes(
bookId: String,
resizeTo: Int? = null,
): TypedBytes? {
getThumbnail(bookId)?.let {
val thumbnailBytes = when {
val thumbnailBytes =
when {
it.thumbnail != null -> it.thumbnail
it.url != null -> File(it.url.toURI()).readBytes()
else -> return null
@ -238,13 +248,16 @@ class BookLifecycle(
private fun thumbnailsHouseKeeping(bookId: String) {
logger.info { "House keeping thumbnails for book: $bookId" }
val all = thumbnailBookRepository.findAllByBookId(bookId)
val all =
thumbnailBookRepository.findAllByBookId(bookId)
.mapNotNull {
if (!it.exists()) {
logger.warn { "Thumbnail doesn't exist, removing entry" }
thumbnailBookRepository.delete(it.id)
null
} else it
} else {
it
}
}
val selected = all.filter { it.selected }
@ -274,15 +287,23 @@ class BookLifecycle(
MediaNotReadyException::class,
IndexOutOfBoundsException::class,
)
fun getBookPage(book: Book, number: Int, convertTo: ImageType? = null, resizeTo: Int? = null): TypedBytes {
fun getBookPage(
book: Book,
number: Int,
convertTo: ImageType? = null,
resizeTo: Int? = null,
): TypedBytes {
val media = mediaRepository.findById(book.id)
val pageContent = bookAnalyzer.getPageContent(BookWithMedia(book, media), number)
val pageMediaType =
if (media.profile == MediaProfile.PDF) pdfImageType.mediaType
else media.pages[number - 1].mediaType
if (media.profile == MediaProfile.PDF)
pdfImageType.mediaType
else
media.pages[number - 1].mediaType
if (resizeTo != null) {
val convertedPage = try {
val convertedPage =
try {
imageConverter.resizeImageToByteArray(pageContent, resizeTargetFormat, resizeTo)
} catch (e: Exception) {
logger.error(e) { "Resize page #$number of book $book to $resizeTo: failed" }
@ -304,7 +325,8 @@ class BookLifecycle(
}
logger.info { msg }
val convertedPage = try {
val convertedPage =
try {
imageConverter.convertImage(pageContent, it.imageIOFormat)
} catch (e: Exception) {
logger.error(e) { "$msg: conversion failed" }
@ -360,7 +382,11 @@ class BookLifecycle(
books.forEach { eventPublisher.publishEvent(DomainEvent.BookDeleted(it)) }
}
fun markReadProgress(book: Book, user: KomgaUser, page: Int) {
fun markReadProgress(
book: Book,
user: KomgaUser,
page: Int,
) {
val pages = mediaRepository.getPagesSize(book.id)
require(page in 1..pages) { "Page argument ($page) must be within 1 and book page count ($pages)" }
@ -369,7 +395,10 @@ class BookLifecycle(
eventPublisher.publishEvent(DomainEvent.ReadProgressChanged(progress))
}
fun markReadProgressCompleted(bookId: String, user: KomgaUser) {
fun markReadProgressCompleted(
bookId: String,
user: KomgaUser,
) {
val media = mediaRepository.findById(bookId)
val progress = ReadProgress(bookId, user.id, media.pageCount, true)
@ -377,21 +406,29 @@ class BookLifecycle(
eventPublisher.publishEvent(DomainEvent.ReadProgressChanged(progress))
}
fun deleteReadProgress(book: Book, user: KomgaUser) {
fun deleteReadProgress(
book: Book,
user: KomgaUser,
) {
readProgressRepository.findByBookIdAndUserIdOrNull(book.id, user.id)?.let { progress ->
readProgressRepository.delete(book.id, user.id)
eventPublisher.publishEvent(DomainEvent.ReadProgressDeleted(progress))
}
}
fun markProgression(book: Book, user: KomgaUser, newProgression: R2Progression) {
fun markProgression(
book: Book,
user: KomgaUser,
newProgression: R2Progression,
) {
readProgressRepository.findByBookIdAndUserIdOrNull(book.id, user.id)?.let { savedProgress ->
check(newProgression.modified.toLocalDateTime().toCurrentTimeZone().isAfter(savedProgress.readDate)) { "Progression is older than existing" }
}
val media = mediaRepository.findById(book.id)
requireNotNull(media.profile) { "Media has no profile" }
val progress = when (media.profile!!) {
val progress =
when (media.profile!!) {
MediaProfile.DIVINA,
MediaProfile.PDF,
-> {
@ -409,14 +446,16 @@ class BookLifecycle(
}
MediaProfile.EPUB -> {
val href = newProgression.locator.href
val href =
newProgression.locator.href
.replaceBefore("/resource/", "").removePrefix("/resource/")
.replaceAfter("#", "").removeSuffix("#")
.let { UriUtils.decode(it, Charsets.UTF_8) }
require(href in media.files.map { it.fileName }) { "Resource does not exist in book: $href" }
requireNotNull(newProgression.locator.locations?.progression) { "location.progression is required" }
val extension = mediaRepository.findExtensionByIdOrNull(book.id) as? MediaExtensionEpub
val extension =
mediaRepository.findExtensionByIdOrNull(book.id) as? MediaExtensionEpub
?: throw IllegalArgumentException("Epub extension not found")
// match progression with positions
val matchingPositions = extension.positions.filter { it.href == href }
@ -453,7 +492,8 @@ class BookLifecycle(
if (book.path.notExists()) return logger.info { "Cannot delete book file, path does not exist: ${book.path}" }
if (!book.path.isWritable()) return logger.info { "Cannot delete book file, path is not writable: ${book.path}" }
val thumbnails = thumbnailBookRepository.findAllByBookIdAndType(book.id, ThumbnailBook.Type.SIDECAR)
val thumbnails =
thumbnailBookRepository.findAllByBookIdAndType(book.id, ThumbnailBook.Type.SIDECAR)
.mapNotNull { it.url?.toURI()?.toPath() }
.filter { it.exists() && it.isWritable() }

View file

@ -26,8 +26,10 @@ class BookMetadataLifecycle(
private val readListLifecycle: ReadListLifecycle,
private val eventPublisher: ApplicationEventPublisher,
) {
fun refreshMetadata(book: Book, capabilities: Set<BookMetadataPatchCapability>) {
fun refreshMetadata(
book: Book,
capabilities: Set<BookMetadataPatchCapability>,
) {
logger.info { "Refresh metadata for book: $book with capabilities: $capabilities" }
val media = mediaRepository.findById(book.id)
@ -44,7 +46,8 @@ class BookMetadataLifecycle(
else -> {
logger.debug { "Provider: ${provider.javaClass.simpleName}" }
val patch = try {
val patch =
try {
provider.getBookMetadataFromBook(BookWithMedia(book, media))
} catch (e: Exception) {
logger.error(e) { "Error while getting metadata from ${provider.javaClass.simpleName} for book: $book" }

View file

@ -52,7 +52,10 @@ class BookPageEditor(
private val failedPageRemoval = mutableListOf<String>()
fun removeHashedPages(book: Book, pagesToDelete: Collection<BookPageNumbered>): BookAction? {
fun removeHashedPages(
book: Book,
pagesToDelete: Collection<BookPageNumbered>,
): BookAction? {
// perform various checks
if (failedPageRemoval.contains(book.id)) {
logger.info { "Book page removal already failed before, skipping" }
@ -75,7 +78,8 @@ class BookPageEditor(
throw MediaNotReadyException()
// create a temp file with the pages removed
val pagesToKeep = media.pages.filterIndexed { index, page ->
val pagesToKeep =
media.pages.filterIndexed { index, page ->
pagesToDelete.find { candidate ->
candidate.fileHash == page.fileHash &&
candidate.mediaType == page.mediaType &&
@ -109,7 +113,8 @@ class BookPageEditor(
}
// perform checks on new file
val createdBook = fileSystemScanner.scanFile(tempFile)
val createdBook =
fileSystemScanner.scanFile(tempFile)
?.copy(
id = book.id,
seriesId = book.seriesId,
@ -142,7 +147,8 @@ class BookPageEditor(
}
tempFile.moveTo(book.path, true)
val newBook = fileSystemScanner.scanFile(book.path)
val newBook =
fileSystemScanner.scanFile(book.path)
?.copy(
id = book.id,
seriesId = book.seriesId,

View file

@ -36,7 +36,6 @@ class FileSystemScanner(
private val sidecarBookConsumers: List<SidecarBookConsumer>,
private val sidecarSeriesConsumers: List<SidecarSeriesConsumer>,
) {
private data class TempSidecar(
val name: String,
val url: URL,
@ -55,7 +54,8 @@ class FileSystemScanner(
scanEpub: Boolean = true,
directoryExclusions: Set<String> = emptySet(),
): ScanResult {
val scanForExtensions = buildList {
val scanForExtensions =
buildList {
if (scanCbx) addAll(listOf("cbz", "zip", "cbr", "rar"))
if (scanPdf) add("pdf")
if (scanEpub) add("epub")
@ -84,15 +84,20 @@ class FileSystemScanner(
setOf(FileVisitOption.FOLLOW_LINKS),
Integer.MAX_VALUE,
object : FileVisitor<Path> {
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
override fun preVisitDirectory(
dir: Path,
attrs: BasicFileAttributes,
): FileVisitResult {
logger.trace { "preVisit: $dir (regularFile:${attrs.isRegularFile}, directory:${attrs.isDirectory}, symbolicLink:${attrs.isSymbolicLink}, other:${attrs.isOther})" }
if (dir.name.startsWith(".") ||
directoryExclusions.any { exclude ->
dir.pathString.contains(exclude, true)
}
) return FileVisitResult.SKIP_SUBTREE
)
return FileVisitResult.SKIP_SUBTREE
pathToSeries[dir] = Series(
pathToSeries[dir] =
Series(
name = dir.name.ifBlank { dir.pathString },
url = dir.toUri().toURL(),
fileLastModified = attrs.getUpdatedTime(),
@ -101,7 +106,10 @@ class FileSystemScanner(
return FileVisitResult.CONTINUE
}
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
override fun visitFile(
file: Path,
attrs: BasicFileAttributes,
): FileVisitResult {
logger.trace { "visitFile: $file (regularFile:${attrs.isRegularFile}, directory:${attrs.isDirectory}, symbolicLink:${attrs.isSymbolicLink}, other:${attrs.isOther})" }
if (!attrs.isSymbolicLink && !attrs.isDirectory) {
if (scanForExtensions.contains(file.extension.lowercase()) &&
@ -131,19 +139,26 @@ class FileSystemScanner(
return FileVisitResult.CONTINUE
}
override fun visitFileFailed(file: Path?, exc: IOException?): FileVisitResult {
override fun visitFileFailed(
file: Path?,
exc: IOException?,
): FileVisitResult {
logger.warn { "Could not access: $file" }
return FileVisitResult.SKIP_SUBTREE
}
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
override fun postVisitDirectory(
dir: Path,
exc: IOException?,
): FileVisitResult {
logger.trace { "postVisit: $dir" }
val books = pathToBooks[dir]
val tempSeries = pathToSeries[dir]
if (!books.isNullOrEmpty() && tempSeries !== null) {
if (!oneshotsDir.isNullOrBlank() && dir.pathString.contains(oneshotsDir, true)) {
books.forEach { book ->
val series = Series(
val series =
Series(
name = book.name,
url = book.url,
fileLastModified = book.fileLastModified,
@ -166,7 +181,8 @@ class FileSystemScanner(
// book sidecars are matched here, with the actual list of books
books.forEach { book ->
val sidecars = pathToBookSidecars[dir]
val sidecars =
pathToBookSidecars[dir]
?.mapNotNull { sidecar ->
sidecarBookConsumers.firstOrNull { it.isSidecarBookMatch(book.name, sidecar.name) }?.let {
sidecar to it.getSidecarBookType()
@ -210,7 +226,10 @@ class FileSystemScanner(
}
}
private fun pathToBook(path: Path, attrs: BasicFileAttributes): Book =
private fun pathToBook(
path: Path,
attrs: BasicFileAttributes,
): Book =
Book(
name = path.nameWithoutExtension,
url = path.toUri().toURL(),

View file

@ -26,8 +26,11 @@ class KomgaUserLifecycle(
private val transactionTemplate: TransactionTemplate,
private val eventPublisher: ApplicationEventPublisher,
) {
fun updatePassword(user: KomgaUser, newPassword: String, expireSessions: Boolean) {
fun updatePassword(
user: KomgaUser,
newPassword: String,
expireSessions: Boolean,
) {
logger.info { "Changing password for user ${user.email}" }
val updatedUser = user.copy(password = passwordEncoder.encode(newPassword))
userRepository.update(updatedUser)
@ -45,7 +48,8 @@ class KomgaUserLifecycle(
logger.info { "Update user: $toUpdate" }
userRepository.update(toUpdate)
val expireSessions = existing.roles != user.roles ||
val expireSessions =
existing.roles != user.roles ||
existing.restrictions != user.restrictions ||
existing.sharedAllLibraries != user.sharedAllLibraries ||
existing.sharedLibrariesIds != user.sharedLibrariesIds

View file

@ -61,11 +61,14 @@ class LibraryContentLifecycle(
private val thumbnailBookRepository: ThumbnailBookRepository,
private val eventPublisher: ApplicationEventPublisher,
) {
fun scanRootFolder(library: Library, scanDeep: Boolean = false) {
fun scanRootFolder(
library: Library,
scanDeep: Boolean = false,
) {
logger.info { "Scan root folder for library: $library" }
measureTime {
val scanResult = try {
val scanResult =
try {
fileSystemScanner.scanRootFolder(
Paths.get(library.root.toURI()),
library.scanForceModifiedTime,
@ -113,13 +116,16 @@ class LibraryContentLifecycle(
}
// delete books that don't exist anymore. We need to do this now, so trash bin can work
val seriesToSortAndRefresh = scannedSeries.values.flatten().map { it.url }.let { urls ->
val seriesToSortAndRefresh =
scannedSeries.values.flatten().map { it.url }.let { urls ->
val books = bookRepository.findAllNotDeletedByLibraryIdAndUrlNotIn(library.id, urls)
if (books.isNotEmpty()) {
logger.info { "Soft deleting books not on disk anymore: $books" }
bookLifecycle.softDeleteMany(books)
books.map { it.seriesId }.distinct().mapNotNull { seriesRepository.findByIdOrNull(it) }.toMutableList()
} else mutableListOf()
} else {
mutableListOf()
}
}
// we store the url of all the series that had deleted books
// this can be used to detect changed series even if their file modified date did not change, for example because of NFS/SMB cache
@ -155,12 +161,16 @@ class LibraryContentLifecycle(
existingBooks.find { it.url == newBook.url && it.deletedDate == null }?.let { existingBook ->
logger.debug { "Matched existing book: $existingBook" }
if (newBook.fileLastModified.notEquals(existingBook.fileLastModified)) {
val hash = if (existingBook.fileSize == newBook.fileSize && existingBook.fileHash.isNotBlank()) {
val hash =
if (existingBook.fileSize == newBook.fileSize && existingBook.fileHash.isNotBlank()) {
hasher.computeHash(newBook.path)
} else null
} else {
null
}
if (hash == existingBook.fileHash) {
logger.info { "Book changed on disk, but still has the same hash, no need to reset media status: $existingBook" }
val updatedBook = existingBook.copy(
val updatedBook =
existingBook.copy(
fileLastModified = newBook.fileLastModified,
fileSize = newBook.fileSize,
fileHash = hash,
@ -168,7 +178,8 @@ class LibraryContentLifecycle(
bookRepository.update(updatedBook)
} else {
logger.info { "Book changed on disk, update and reset media status: $existingBook" }
val updatedBook = existingBook.copy(
val updatedBook =
existingBook.copy(
fileLastModified = newBook.fileLastModified,
fileSize = newBook.fileSize,
fileHash = hash ?: "",
@ -237,8 +248,10 @@ class LibraryContentLifecycle(
}
}
if (library.emptyTrashAfterScan) emptyTrash(library)
else cleanupEmptySets()
if (library.emptyTrashAfterScan)
emptyTrash(library)
else
cleanupEmptySets()
}.also { logger.info { "Library updated in $it" } }
eventPublisher.publishEvent(DomainEvent.LibraryScanned(library))
@ -255,17 +268,23 @@ class LibraryContentLifecycle(
* - Metadata. The metadata title will only be copied if locked. If not locked, the folder name is used.
* - all books, via #tryRestoreBooks
*/
private fun tryRestoreSeries(newSeries: Series, newBooks: List<Book>) {
private fun tryRestoreSeries(
newSeries: Series,
newBooks: List<Book>,
) {
logger.info { "Try to restore series: $newSeries" }
val bookSizes = newBooks.map { it.fileSize }
val deletedCandidates = seriesRepository.findAll(SeriesSearch(deleted = true))
val deletedCandidates =
seriesRepository.findAll(SeriesSearch(deleted = true))
.mapNotNull { deletedCandidate ->
val deletedBooks = bookRepository.findAllBySeriesId(deletedCandidate.id)
val deletedBooksSizes = deletedBooks.map { it.fileSize }
if (newBooks.size == deletedBooks.size && bookSizes.containsAll(deletedBooksSizes) && deletedBooksSizes.containsAll(bookSizes) && deletedBooks.all { it.fileHash.isNotBlank() }) {
deletedCandidate to deletedBooks
} else null
} else {
null
}
}
logger.debug { "Deleted series candidates: $deletedCandidates" }
@ -273,7 +292,8 @@ class LibraryContentLifecycle(
val newBooksWithHash = newBooks.map { book -> bookRepository.findByIdOrNull(book.id)!!.copy(fileHash = hasher.computeHash(book.path)) }
bookRepository.update(newBooksWithHash)
val match = deletedCandidates.find { (_, books) ->
val match =
deletedCandidates.find { (_, books) ->
books.map { it.fileHash }.containsAll(newBooksWithHash.map { it.fileHash }) && newBooksWithHash.map { it.fileHash }.containsAll(books.map { it.fileHash })
}
@ -332,8 +352,10 @@ class LibraryContentLifecycle(
if (deletedCandidates.isNotEmpty()) {
// if the book has no hash, compute the hash and store it
val bookWithHash =
if (bookToAdd.fileHash.isNotBlank()) bookToAdd
else bookRepository.findByIdOrNull(bookToAdd.id)!!.copy(fileHash = hasher.computeHash(bookToAdd.path)).also { bookRepository.update(it) }
if (bookToAdd.fileHash.isNotBlank())
bookToAdd
else
bookRepository.findByIdOrNull(bookToAdd.id)!!.copy(fileHash = hasher.computeHash(bookToAdd.path)).also { bookRepository.update(it) }
val match = deletedCandidates.find { it.fileHash == bookWithHash.fileHash }

View file

@ -30,7 +30,6 @@ class LibraryLifecycle(
private val transactionTemplate: TransactionTemplate,
private val libraryScanScheduler: LibraryScanScheduler,
) {
@Throws(
FileNotFoundException::class,
DirectoryNotFoundException::class,
@ -70,7 +69,10 @@ class LibraryLifecycle(
eventPublisher.publishEvent(DomainEvent.LibraryUpdated(toUpdate))
}
private fun checkLibraryShouldRescan(existing: Library, updated: Library): Boolean {
private fun checkLibraryShouldRescan(
existing: Library,
updated: Library,
): Boolean {
if (existing.root != updated.root) return true
if (existing.oneshotsDirectory != updated.oneshotsDirectory) return true
if (existing.scanCbx != updated.scanCbx) return true
@ -81,7 +83,10 @@ class LibraryLifecycle(
return false
}
private fun checkLibraryValidity(library: Library, existing: Collection<Library>) {
private fun checkLibraryValidity(
library: Library,
existing: Collection<Library>,
) {
if (!Files.exists(library.path))
throw FileNotFoundException("Library root folder does not exist: ${library.root}")

View file

@ -17,7 +17,6 @@ class LocalArtworkLifecycle(
private val seriesLifecycle: SeriesLifecycle,
private val localArtworkProvider: LocalArtworkProvider,
) {
fun refreshLocalArtwork(book: Book) {
logger.info { "Refresh local artwork for book: $book" }
val library = libraryRepository.findById(book.libraryId)

View file

@ -6,11 +6,11 @@ import org.springframework.stereotype.Service
@Service
class MetadataAggregator {
fun aggregate(metadatas: Collection<BookMetadata>): BookMetadataAggregation {
val authors = metadatas.flatMap { it.authors }.distinctBy { "${it.role}__${it.name}" }
val tags = metadatas.flatMap { it.tags }.toSet()
val (summary, summaryNumber) = metadatas
val (summary, summaryNumber) =
metadatas
.sortedBy { it.numberSort }
.find { it.summary.isNotBlank() }
?.let {

View file

@ -8,12 +8,20 @@ import org.springframework.stereotype.Service
@Service
class MetadataApplier {
private fun <T> getIfNotLocked(
original: T,
patched: T?,
lock: Boolean,
): T =
if (patched != null && !lock)
patched
else
original
private fun <T> getIfNotLocked(original: T, patched: T?, lock: Boolean): T =
if (patched != null && !lock) patched
else original
fun apply(patch: BookMetadataPatch, metadata: BookMetadata): BookMetadata =
fun apply(
patch: BookMetadataPatch,
metadata: BookMetadata,
): BookMetadata =
with(metadata) {
copy(
title = getIfNotLocked(title, patch.title, titleLock),
@ -28,7 +36,10 @@ class MetadataApplier {
)
}
fun apply(patch: SeriesMetadataPatch, metadata: SeriesMetadata): SeriesMetadata =
fun apply(
patch: SeriesMetadataPatch,
metadata: SeriesMetadata,
): SeriesMetadata =
with(metadata) {
copy(
status = getIfNotLocked(status, patch.status, statusLock),

View file

@ -23,19 +23,21 @@ class PageHashLifecycle(
private val bookRepository: BookRepository,
private val komgaProperties: KomgaProperties,
) {
private val hashableMediaTypes = listOf(MediaType.ZIP.type)
fun getBookIdsWithMissingPageHash(library: Library): Collection<String> =
if (library.hashPages)
if (library.hashPages) {
mediaRepository.findAllBookIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(library.id, hashableMediaTypes, komgaProperties.pageHashing)
.also { logger.info { "Found ${it.size} books with missing page hash" } }
else {
} else {
logger.info { "Page hashing is not enabled, skipping" }
emptyList()
}
fun getPage(pageHash: String, resizeTo: Int? = null): TypedBytes? {
fun getPage(
pageHash: String,
resizeTo: Int? = null,
): TypedBytes? {
val match = pageHashRepository.findMatchesByHash(pageHash, Pageable.ofSize(1)).firstOrNull() ?: return null
val book = bookRepository.findByIdOrNull(match.bookId) ?: return null

View file

@ -29,7 +29,6 @@ class ReadListLifecycle(
private val eventPublisher: ApplicationEventPublisher,
private val transactionTemplate: TransactionTemplate,
) {
@Throws(
DuplicateNameException::class,
)
@ -50,7 +49,8 @@ class ReadListLifecycle(
@Transactional
fun updateReadList(toUpdate: ReadList) {
logger.info { "Update read list: $toUpdate" }
val existing = readListRepository.findByIdOrNull(toUpdate.id)
val existing =
readListRepository.findByIdOrNull(toUpdate.id)
?: throw IllegalArgumentException("Cannot update read list that does not exist")
if (!existing.name.equals(toUpdate.name, true) && readListRepository.existsByName(toUpdate.name))
@ -75,14 +75,19 @@ class ReadListLifecycle(
* Read list will be created if it doesn't exist.
*/
@Transactional
fun addBookToReadList(readListName: String, book: Book, numberInList: Int?) {
fun addBookToReadList(
readListName: String,
book: Book,
numberInList: Int?,
) {
readListRepository.findByNameOrNull(readListName).let { existing ->
if (existing != null) {
if (existing.bookIds.containsValue(book.id))
if (existing.bookIds.containsValue(book.id)) {
logger.debug { "Book is already in existing read list '${existing.name}'" }
else {
} else {
val map = existing.bookIds.toSortedMap()
val key = if (numberInList != null && existing.bookIds.containsKey(numberInList)) {
val key =
if (numberInList != null && existing.bookIds.containsKey(numberInList)) {
logger.debug { "Existing read list '${existing.name}' already contains a book at position $numberInList, adding book '${book.name}' at the end" }
existing.bookIds.lastKey() + 1
} else {
@ -150,7 +155,8 @@ class ReadListLifecycle(
return it.thumbnail
}
val ids = with(mutableListOf<String>()) {
val ids =
with(mutableListOf<String>()) {
while (size < 4) {
this += readList.bookIds.values.take(4)
}

View file

@ -19,8 +19,10 @@ class ReadListMatcher(
logger.info { "Trying to match $request" }
val readListMatch =
if (readListRepository.existsByName(request.name)) ReadListMatch(request.name, "ERR_1009")
else ReadListMatch(request.name)
if (readListRepository.existsByName(request.name))
ReadListMatch(request.name, "ERR_1009")
else
ReadListMatch(request.name)
val matches = readListRequestRepository.matchBookRequests(request.books)

View file

@ -25,7 +25,6 @@ class SeriesCollectionLifecycle(
private val eventPublisher: ApplicationEventPublisher,
private val transactionTemplate: TransactionTemplate,
) {
@Throws(
DuplicateNameException::class,
)
@ -47,7 +46,8 @@ class SeriesCollectionLifecycle(
fun updateCollection(toUpdate: SeriesCollection) {
logger.info { "Update collection: $toUpdate" }
val existing = collectionRepository.findByIdOrNull(toUpdate.id)
val existing =
collectionRepository.findByIdOrNull(toUpdate.id)
?: throw IllegalArgumentException("Cannot update collection that does not exist")
if (!existing.name.equals(toUpdate.name, true) && collectionRepository.existsByName(toUpdate.name))
@ -71,12 +71,15 @@ class SeriesCollectionLifecycle(
* Collection will be created if it doesn't exist.
*/
@Transactional
fun addSeriesToCollection(collectionName: String, series: Series) {
fun addSeriesToCollection(
collectionName: String,
series: Series,
) {
collectionRepository.findByNameOrNull(collectionName).let { existing ->
if (existing != null) {
if (existing.seriesIds.contains(series.id))
if (existing.seriesIds.contains(series.id)) {
logger.debug { "Series is already in existing collection '${existing.name}'" }
else {
} else {
logger.debug { "Adding series '${series.name}' to existing collection '${existing.name}'" }
updateCollection(
existing.copy(seriesIds = existing.seriesIds + series.id),
@ -133,12 +136,16 @@ class SeriesCollectionLifecycle(
fun getThumbnailBytes(thumbnailId: String): ByteArray? =
thumbnailSeriesCollectionRepository.findByIdOrNull(thumbnailId)?.thumbnail
fun getThumbnailBytes(collection: SeriesCollection, userId: String): ByteArray {
fun getThumbnailBytes(
collection: SeriesCollection,
userId: String,
): ByteArray {
thumbnailSeriesCollectionRepository.findSelectedByCollectionIdOrNull(collection.id)?.let {
return it.thumbnail
}
val ids = with(mutableListOf<String>()) {
val ids =
with(mutableListOf<String>()) {
while (size < 4) {
this += collection.seriesIds.take(4)
}

View file

@ -62,7 +62,6 @@ class SeriesLifecycle(
private val transactionTemplate: TransactionTemplate,
private val historicalEventRepository: HistoricalEventRepository,
) {
private val whitespacePattern = """\s+""".toRegex()
fun sortBooks(series: Series) {
@ -73,7 +72,8 @@ class SeriesLifecycle(
logger.debug { "Existing books: $books" }
logger.debug { "Existing metadata: $metadatas" }
val sorted = books
val sorted =
books
.sortedWith(
compareBy(natSortComparator) {
it.name
@ -89,9 +89,12 @@ class SeriesLifecycle(
sorted.mapIndexed { index, (book, _) -> book.copy(number = index + 1) },
)
val oldToNew = sorted.mapIndexedNotNull { index, (book, metadata) ->
if (metadata.numberLock && metadata.numberSortLock) null
else Triple(
val oldToNew =
sorted.mapIndexedNotNull { index, (book, metadata) ->
if (metadata.numberLock && metadata.numberSortLock)
null
else
Triple(
book,
metadata,
metadata.copy(
@ -116,7 +119,10 @@ class SeriesLifecycle(
}
}
fun addBooks(series: Series, booksToAdd: Collection<Book>) {
fun addBooks(
series: Series,
booksToAdd: Collection<Book>,
) {
booksToAdd.forEach {
check(it.libraryId == series.libraryId) { "Cannot add book to series if they don't share the same libraryId" }
}
@ -197,13 +203,18 @@ class SeriesLifecycle(
series.forEach { eventPublisher.publishEvent(DomainEvent.SeriesDeleted(it)) }
}
fun markReadProgressCompleted(seriesId: String, user: KomgaUser) {
val bookIds = bookRepository.findAllIdsBySeriesId(seriesId)
fun markReadProgressCompleted(
seriesId: String,
user: KomgaUser,
) {
val bookIds =
bookRepository.findAllIdsBySeriesId(seriesId)
.filter { bookId ->
val readProgress = readProgressRepository.findByBookIdAndUserIdOrNull(bookId, user.id)
readProgress == null || !readProgress.completed
}
val progresses = mediaRepository.getPagesSizes(bookIds)
val progresses =
mediaRepository.getPagesSizes(bookIds)
.map { (bookId, pageSize) -> ReadProgress(bookId, user.id, pageSize, true) }
readProgressRepository.save(progresses)
@ -211,7 +222,10 @@ class SeriesLifecycle(
eventPublisher.publishEvent(DomainEvent.ReadProgressSeriesChanged(seriesId, user.id))
}
fun deleteReadProgress(seriesId: String, user: KomgaUser) {
fun deleteReadProgress(
seriesId: String,
user: KomgaUser,
) {
val bookIds = bookRepository.findAllIdsBySeriesId(seriesId)
val progresses = readProgressRepository.findAllByBookIdsAndUserId(bookIds, user.id)
readProgressRepository.deleteByBookIdsAndUserId(bookIds, user.id)
@ -243,18 +257,26 @@ class SeriesLifecycle(
getBytesFromThumbnailSeries(it)
}
fun getThumbnailBytes(seriesId: String, userId: String): ByteArray? {
fun getThumbnailBytes(
seriesId: String,
userId: String,
): ByteArray? {
getSelectedThumbnail(seriesId)?.let {
return getBytesFromThumbnailSeries(it)
}
seriesRepository.findByIdOrNull(seriesId)?.let { series ->
val bookId = when (libraryRepository.findById(series.libraryId).seriesCover) {
val bookId =
when (libraryRepository.findById(series.libraryId).seriesCover) {
Library.SeriesCover.FIRST -> bookRepository.findFirstIdInSeriesOrNull(seriesId)
Library.SeriesCover.FIRST_UNREAD_OR_FIRST -> bookRepository.findFirstUnreadIdInSeriesOrNull(seriesId, userId)
Library.SeriesCover.FIRST_UNREAD_OR_FIRST ->
bookRepository.findFirstUnreadIdInSeriesOrNull(seriesId, userId)
?: bookRepository.findFirstIdInSeriesOrNull(seriesId)
Library.SeriesCover.FIRST_UNREAD_OR_LAST -> bookRepository.findFirstUnreadIdInSeriesOrNull(seriesId, userId)
Library.SeriesCover.FIRST_UNREAD_OR_LAST ->
bookRepository.findFirstUnreadIdInSeriesOrNull(seriesId, userId)
?: bookRepository.findLastIdInSeriesOrNull(seriesId)
Library.SeriesCover.LAST -> bookRepository.findLastIdInSeriesOrNull(seriesId)
}
if (bookId != null) return bookLifecycle.getThumbnailBytes(bookId)?.bytes
@ -263,7 +285,10 @@ class SeriesLifecycle(
return null
}
fun addThumbnailForSeries(thumbnail: ThumbnailSeries, markSelected: MarkSelectedPreference): ThumbnailSeries {
fun addThumbnailForSeries(
thumbnail: ThumbnailSeries,
markSelected: MarkSelectedPreference,
): ThumbnailSeries {
// delete existing thumbnail with the same url
if (thumbnail.url != null) {
thumbnailsSeriesRepository.findAllBySeriesId(thumbnail.seriesId)
@ -274,11 +299,13 @@ class SeriesLifecycle(
}
thumbnailsSeriesRepository.insert(thumbnail.copy(selected = false))
val selected = when (markSelected) {
val selected =
when (markSelected) {
MarkSelectedPreference.YES -> true
MarkSelectedPreference.IF_NONE_OR_GENERATED -> {
thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(thumbnail.seriesId) == null
}
MarkSelectedPreference.NO -> false
}
@ -299,7 +326,8 @@ class SeriesLifecycle(
if (series.path.notExists()) return logger.info { "Cannot delete series folder, path does not exist: ${series.path}" }
if (!series.path.isWritable()) return logger.info { "Cannot delete series folder, path is not writable: ${series.path}" }
val thumbnails = thumbnailsSeriesRepository.findAllBySeriesIdIdAndType(series.id, ThumbnailSeries.Type.SIDECAR)
val thumbnails =
thumbnailsSeriesRepository.findAllBySeriesIdIdAndType(series.id, ThumbnailSeries.Type.SIDECAR)
.mapNotNull { it.url?.toURI()?.toPath() }
.filter { it.exists() && it.isWritable() }
@ -320,13 +348,16 @@ class SeriesLifecycle(
private fun thumbnailsHouseKeeping(seriesId: String) {
logger.info { "House keeping thumbnails for series: $seriesId" }
val all = thumbnailsSeriesRepository.findAllBySeriesId(seriesId)
val all =
thumbnailsSeriesRepository.findAllBySeriesId(seriesId)
.mapNotNull {
if (!it.exists()) {
logger.warn { "Thumbnail doesn't exist, removing entry" }
thumbnailsSeriesRepository.delete(it.id)
null
} else it
} else {
it
}
}
val selected = all.filter { it.selected }

View file

@ -35,7 +35,6 @@ class SeriesMetadataLifecycle(
private val collectionLifecycle: SeriesCollectionLifecycle,
private val eventPublisher: ApplicationEventPublisher,
) {
fun refreshMetadata(series: Series) {
logger.info { "Refresh metadata for series: $series" }
@ -49,7 +48,8 @@ class SeriesMetadataLifecycle(
else -> {
logger.debug { "Provider: ${provider.javaClass.simpleName}" }
val patches = bookRepository.findAllBySeriesId(series.id)
val patches =
bookRepository.findAllBySeriesId(series.id)
.mapNotNull { book ->
try {
provider.getSeriesMetadataFromBook(BookWithMedia(book, mediaRepository.findById(book.id)), library.importComicInfoSeriesAppendVolume)
@ -79,7 +79,8 @@ class SeriesMetadataLifecycle(
logger.info { "Library is not set to import series metadata for this provider, skipping: ${provider.javaClass.simpleName}" }
else -> {
logger.debug { "Provider: ${provider.javaClass.simpleName}" }
val patch = try {
val patch =
try {
provider.getSeriesMetadata(series)
} catch (e: Exception) {
logger.error(e) { "Error while getting metadata from ${provider::class.simpleName} for series: $series" }
@ -101,7 +102,8 @@ class SeriesMetadataLifecycle(
patches: List<SeriesMetadataPatch>,
series: Series,
) {
val aggregatedPatch = SeriesMetadataPatch(
val aggregatedPatch =
SeriesMetadataPatch(
title = patches.mostFrequent { it.title },
titleSort = patches.mostFrequent { it.titleSort },
status = patches.mostFrequent { it.status },

View file

@ -122,15 +122,19 @@ class ThumbnailLifecycle(
copier: (T, ThumbnailMetadata) -> T,
updater: (Collection<T>) -> Unit,
): Boolean {
val (result, duration) = measureTimedValue {
val (result, duration) =
measureTimedValue {
val thumbs = fetcher(Pageable.ofSize(1000))
logger.info { "Fetched ${thumbs.numberOfElements} ${clazz.simpleName} to fix, total: ${thumbs.totalElements}" }
val fixedThumbs = thumbs.mapNotNull {
val fixedThumbs =
thumbs.mapNotNull {
try {
val meta = supplier(it)
if (meta == null) null
else copier(it, meta)
if (meta == null)
null
else
copier(it, meta)
} catch (e: Exception) {
logger.error(e) { "Could not fix thumbnail: $it" }
null
@ -159,5 +163,6 @@ class ThumbnailLifecycle(
)
private data class Result(val processed: Int, val hasMore: Boolean)
private data class ThumbnailMetadata(val mediaType: String, val fileSize: Long, val dimension: Dimension)
}

View file

@ -30,7 +30,6 @@ class TransientBookLifecycle(
private val seriesMetadataProviders: List<SeriesMetadataFromBookProvider>,
bookMetadataProviders: List<BookMetadataProvider>,
) {
val bookMetadataProviders = bookMetadataProviders.filter { it.capabilities.contains(BookMetadataPatchCapability.NUMBER_SORT) }
fun scanAndPersist(filePath: String): List<TransientBook> {
@ -60,7 +59,8 @@ class TransientBookLifecycle(
fun getMetadata(transientBook: TransientBook): Pair<String?, Float?> {
val bookWithMedia = transientBook.toBookWithMedia()
val number = bookMetadataProviders.firstNotNullOfOrNull { it.getBookMetadataFromBook(bookWithMedia)?.numberSort }
val series = seriesMetadataProviders
val series =
seriesMetadataProviders
.flatMap {
buildList {
if (it.supportsAppendVolume) add(it.getSeriesMetadataFromBook(bookWithMedia, true)?.title)
@ -77,11 +77,16 @@ class TransientBookLifecycle(
MediaNotReadyException::class,
IndexOutOfBoundsException::class,
)
fun getBookPage(transientBook: TransientBook, number: Int): TypedBytes {
fun getBookPage(
transientBook: TransientBook,
number: Int,
): TypedBytes {
val pageContent = bookAnalyzer.getPageContent(transientBook.toBookWithMedia(), number)
val pageMediaType =
if (transientBook.media.profile == MediaProfile.PDF) pdfImageType.mediaType
else transientBook.media.pages[number - 1].mediaType
if (transientBook.media.profile == MediaProfile.PDF)
pdfImageType.mediaType
else
transientBook.media.pages[number - 1].mediaType
return TypedBytes(pageContent, pageMediaType)
}

View file

@ -11,7 +11,8 @@ private val logger = KotlinLogging.logger {}
@Service
class TransientBookCache : TransientBookRepository {
private val cache = Caffeine.newBuilder()
private val cache =
Caffeine.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build<String, TransientBook>()

View file

@ -14,7 +14,6 @@ import javax.sql.DataSource
class DataSourcesConfiguration(
private val komgaProperties: KomgaProperties,
) {
@Bean("sqliteDataSource")
@Primary
fun sqliteDataSource(): DataSource =
@ -28,13 +27,21 @@ class DataSourcesConfiguration(
this.maximumPoolSize = 1
}
private fun buildDataSource(poolName: String, dataSourceClass: Class<out SQLiteDataSource>, databaseProps: KomgaProperties.Database): HikariDataSource {
val extraPragmas = databaseProps.pragmas.let {
if (it.isEmpty()) ""
else "?" + it.map { (key, value) -> "$key=$value" }.joinToString(separator = "&")
private fun buildDataSource(
poolName: String,
dataSourceClass: Class<out SQLiteDataSource>,
databaseProps: KomgaProperties.Database,
): HikariDataSource {
val extraPragmas =
databaseProps.pragmas.let {
if (it.isEmpty())
""
else
"?" + it.map { (key, value) -> "$key=$value" }.joinToString(separator = "&")
}
val dataSource = DataSourceBuilder.create()
val dataSource =
DataSourceBuilder.create()
.driverClassName("org.sqlite.JDBC")
.url("jdbc:sqlite:${databaseProps.file}$extraPragmas")
.type(dataSourceClass)
@ -47,9 +54,12 @@ class DataSourcesConfiguration(
}
val poolSize =
if (databaseProps.file.contains(":memory:") || databaseProps.file.contains("mode=memory")) 1
else if (databaseProps.poolSize != null) databaseProps.poolSize!!
else Runtime.getRuntime().availableProcessors().coerceAtMost(databaseProps.maxPoolSize)
if (databaseProps.file.contains(":memory:") || databaseProps.file.contains("mode=memory"))
1
else if (databaseProps.poolSize != null)
databaseProps.poolSize!!
else
Runtime.getRuntime().availableProcessors().coerceAtMost(databaseProps.maxPoolSize)
return HikariDataSource(
HikariConfig().apply {

View file

@ -11,7 +11,6 @@ class FlywaySecondaryMigrationInitializer(
@Qualifier("tasksDataSource")
private val tasksDataSource: DataSource,
) : InitializingBean {
// by default Spring Boot will perform migration only on the @Primary datasource
override fun afterPropertiesSet() {
Flyway.configure()

View file

@ -12,16 +12,18 @@ import java.sql.Connection
private val log = KotlinLogging.logger {}
class SqliteUdfDataSource : SQLiteDataSource() {
companion object {
const val udfStripAccents = "UDF_STRIP_ACCENTS"
const val collationUnicode3 = "COLLATION_UNICODE_3"
const val UDF_STRIP_ACCENTS = "UDF_STRIP_ACCENTS"
const val COLLATION_UNICODE_3 = "COLLATION_UNICODE_3"
}
override fun getConnection(): Connection =
super.getConnection().also { addAllUdf(it as SQLiteConnection) }
override fun getConnection(username: String?, password: String?): SQLiteConnection =
override fun getConnection(
username: String?,
password: String?,
): SQLiteConnection =
super.getConnection(username, password).also { addAllUdf(it) }
private fun addAllUdf(connection: SQLiteConnection) {
@ -47,10 +49,10 @@ class SqliteUdfDataSource : SQLiteDataSource() {
}
private fun createUdfStripAccents(connection: SQLiteConnection) {
log.debug { "Adding custom $udfStripAccents function" }
log.debug { "Adding custom $UDF_STRIP_ACCENTS function" }
Function.create(
connection,
udfStripAccents,
UDF_STRIP_ACCENTS,
object : Function() {
override fun xFunc() =
when (val text = value_text(0)) {
@ -62,17 +64,21 @@ class SqliteUdfDataSource : SQLiteDataSource() {
}
private fun createUnicode3Collation(connection: SQLiteConnection) {
log.debug { "Adding custom $collationUnicode3 collation" }
log.debug { "Adding custom $COLLATION_UNICODE_3 collation" }
Collation.create(
connection,
collationUnicode3,
COLLATION_UNICODE_3,
object : Collation() {
val collator = Collator.getInstance().apply {
val collator =
Collator.getInstance().apply {
strength = Collator.TERTIARY
decomposition = Collator.CANONICAL_DECOMPOSITION
}
override fun xCompare(str1: String, str2: String): Int = collator.compare(str1, str2)
override fun xCompare(
str1: String,
str2: String,
): Int = collator.compare(str1, str2)
},
)
}

View file

@ -14,7 +14,6 @@ private const val SEED = 0
@Component
class Hasher {
fun computeHash(path: Path): String {
logger.debug { "Hashing: $path" }
@ -38,7 +37,8 @@ class Hasher {
}
@OptIn(ExperimentalUnsignedTypes::class)
private fun ByteArray.toHexString(): String = asUByteArray().joinToString("") {
private fun ByteArray.toHexString(): String =
asUByteArray().joinToString("") {
it.toString(16).padStart(2, '0')
}
}

View file

@ -6,7 +6,6 @@ import org.springframework.context.annotation.Configuration
@Configuration
class HttpExchangeConfiguration {
private val httpExchangeRepository = InMemoryHttpExchangeRepository()
@Bean

View file

@ -10,7 +10,6 @@ private val logger = KotlinLogging.logger {}
@Service
class ImageAnalyzer {
/**
* Returns the Dimension of the image contained in the stream.
* The stream will not be closed, nor marked or reset.

View file

@ -16,14 +16,13 @@ import kotlin.math.min
private val logger = KotlinLogging.logger {}
private const val webpNightMonkeys = "com.github.gotson.nightmonkeys.webp.imageio.plugins.WebpImageReaderSpi"
private const val WEBP_NIGHT_MONKEYS = "com.github.gotson.nightmonkeys.webp.imageio.plugins.WebpImageReaderSpi"
@Service
class ImageConverter(
private val imageAnalyzer: ImageAnalyzer,
private val contentDetector: ContentDetector,
) {
val supportedReadFormats by lazy { ImageIO.getReaderFormatNames().toList() }
val supportedReadMediaTypes by lazy { ImageIO.getReaderMIMETypes().toList() }
val supportedWriteFormats by lazy { ImageIO.getWriterFormatNames().toList() }
@ -38,7 +37,8 @@ class ImageConverter(
}
private fun chooseWebpReader() {
val providers = IIORegistry.getDefaultInstance().getServiceProviders(
val providers =
IIORegistry.getDefaultInstance().getServiceProviders(
ImageReaderSpi::class.java,
{ it is ImageReaderSpi && it.mimeTypes.contains("image/webp") },
false,
@ -46,7 +46,7 @@ class ImageConverter(
if (providers.size > 1) {
logger.debug { "WebP reader providers: ${providers.map { it.javaClass.canonicalName }}" }
providers.firstOrNull { it.javaClass.canonicalName == webpNightMonkeys }?.let { nightMonkeys ->
providers.firstOrNull { it.javaClass.canonicalName == WEBP_NIGHT_MONKEYS }?.let { nightMonkeys ->
(providers - nightMonkeys).forEach {
logger.debug { "Deregister provider: ${it.javaClass.canonicalName}" }
IIORegistry.getDefaultInstance().deregisterServiceProvider(it)
@ -57,16 +57,25 @@ class ImageConverter(
private val supportsTransparency = listOf("png")
fun canConvertMediaType(from: String, to: String) =
fun canConvertMediaType(
from: String,
to: String,
) =
supportedReadMediaTypes.contains(from) && supportedWriteMediaTypes.contains(to)
fun convertImage(imageBytes: ByteArray, format: String): ByteArray =
fun convertImage(
imageBytes: ByteArray,
format: String,
): ByteArray =
ByteArrayOutputStream().use { baos ->
val image = ImageIO.read(imageBytes.inputStream())
val result = if (!supportsTransparency.contains(format) && containsAlphaChannel(image)) {
if (containsTransparency(image)) logger.info { "Image contains alpha channel but is not opaque, visual artifacts may appear" }
else logger.info { "Image contains alpha channel but is opaque, conversion should not generate any visual artifacts" }
val result =
if (!supportsTransparency.contains(format) && containsAlphaChannel(image)) {
if (containsTransparency(image))
logger.info { "Image contains alpha channel but is not opaque, visual artifacts may appear" }
else
logger.info { "Image contains alpha channel but is opaque, conversion should not generate any visual artifacts" }
BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_RGB).also {
it.createGraphics().drawImage(image, 0, 0, Color.WHITE, null)
}
@ -79,7 +88,11 @@ class ImageConverter(
baos.toByteArray()
}
fun resizeImageToByteArray(imageBytes: ByteArray, format: ImageType, size: Int): ByteArray {
fun resizeImageToByteArray(
imageBytes: ByteArray,
format: ImageType,
size: Int,
): ByteArray {
val builder = resizeImageBuilder(imageBytes, format, size) ?: return imageBytes
return ByteArrayOutputStream().use {
@ -88,14 +101,23 @@ class ImageConverter(
}
}
fun resizeImageToBufferedImage(imageBytes: ByteArray, format: ImageType, size: Int): BufferedImage {
fun resizeImageToBufferedImage(
imageBytes: ByteArray,
format: ImageType,
size: Int,
): BufferedImage {
val builder = resizeImageBuilder(imageBytes, format, size) ?: return ImageIO.read(imageBytes.inputStream())
return builder.asBufferedImage()
}
private fun resizeImageBuilder(imageBytes: ByteArray, format: ImageType, size: Int): Thumbnails.Builder<out InputStream>? {
val longestEdge = imageAnalyzer.getDimension(imageBytes.inputStream())?.let {
private fun resizeImageBuilder(
imageBytes: ByteArray,
format: ImageType,
size: Int,
): Thumbnails.Builder<out InputStream>? {
val longestEdge =
imageAnalyzer.getDimension(imageBytes.inputStream())?.let {
val mediaType = contentDetector.detectMediaType(imageBytes.inputStream())
val longestEdge = max(it.height, it.width)
// don't resize if source and target format is the same, and source is smaller than desired

Some files were not shown because too many files have changed in this diff Show more