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,7 +26,8 @@ kotlin {
jvmToolchain(17)
}
val benchmarkSourceSet = sourceSets.create("benchmark") {
val benchmarkSourceSet =
sourceSets.create("benchmark") {
java {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().runtimeClasspath
@ -147,7 +148,8 @@ tasks {
withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs = listOf(
freeCompilerArgs =
listOf(
"-Xjsr305=strict",
"-opt-in=kotlin.time.ExperimentalTime",
)
@ -243,16 +245,20 @@ 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",
),
@ -262,7 +268,8 @@ 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

@ -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,7 +9,8 @@ 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,
}
}

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

View file

@ -19,7 +19,6 @@ import javax.sql.DataSource
// as advised in https://docs.spring.io/spring-boot/docs/3.1.4/reference/htmlsingle/#howto.data-access.configure-jooq-with-multiple-datasources
@Configuration
class JooqConfiguration {
@Bean("dslContext")
@Primary
fun mainDslContext(
@ -37,7 +36,12 @@ class JooqConfiguration {
): DSLContext =
createDslContext(dataSource, transactionProvider, executeListenerProviders)
private fun createDslContext(dataSource: DataSource, transactionProvider: ObjectProvider<TransactionProvider?>, executeListenerProviders: ObjectProvider<ExecuteListenerProvider?>) = DefaultDSLContext(
private fun createDslContext(
dataSource: DataSource,
transactionProvider: ObjectProvider<TransactionProvider?>,
executeListenerProviders: ObjectProvider<ExecuteListenerProvider?>,
) =
DefaultDSLContext(
DefaultConfiguration().also { configuration ->
configuration.set(SQLDialect.SQLITE)
configuration.set(DataSourceConnectionProvider(TransactionAwareDataSourceProxy(dataSource)))

View file

@ -6,7 +6,6 @@ import org.springframework.data.domain.Sort
class UnpagedSorted(
private val sort: Sort,
) : Pageable {
override fun getPageNumber(): Int {
throw UnsupportedOperationException()
}

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