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

View file

@ -22,7 +22,8 @@ fun main(args: Array<String>) {
run(*args) run(*args)
} }
} catch (e: Exception) { } 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 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() else -> RB.getString("error_message.unexpected") to e.stackTraceToString()
} }

View file

@ -7,8 +7,13 @@ class RB private constructor() {
companion object { companion object {
private val BUNDLE: ResourceBundle = ResourceBundle.getBundle("org.gotson.komga.messages") private val BUNDLE: ResourceBundle = ResourceBundle.getBundle("org.gotson.komga.messages")
fun getString(key: String, vararg args: Any?): String = fun getString(
if (args.isEmpty()) BUNDLE.getString(key) key: String,
else MessageFormatter.arrayFormat(BUNDLE.getString(key), args).message 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 import org.springframework.core.io.ClassPathResource
@Preview @Preview
fun showErrorDialog(text: String, stackTrace: String? = null) { fun showErrorDialog(
text: String,
stackTrace: String? = null,
) {
application { application {
Window( Window(
title = RB.getString("dialog_error.title"), title = RB.getString("dialog_error.title"),
onCloseRequest = ::exitApplication, onCloseRequest = ::exitApplication,
visible = true, visible = true,
resizable = false, resizable = false,
state = WindowState( state =
WindowState(
placement = WindowPlacement.Floating, placement = WindowPlacement.Floating,
position = WindowPosition(alignment = Alignment.Center), position = WindowPosition(alignment = Alignment.Center),
size = DpSize( size =
DpSize(
if (stackTrace != null) 800.dp else Dp.Unspecified, if (stackTrace != null) 800.dp else Dp.Unspecified,
Dp.Unspecified, Dp.Unspecified,
), ),
@ -57,7 +62,8 @@ fun showErrorDialog(text: String, stackTrace: String? = null) {
Image( Image(
painter = loadSvgPainter(ClassPathResource("icons/komga-color.svg").inputStream, LocalDensity.current), painter = loadSvgPainter(ClassPathResource("icons/komga-color.svg").inputStream, LocalDensity.current),
contentDescription = "Komga logo", contentDescription = "Komga logo",
modifier = Modifier modifier =
Modifier
.size(96.dp) .size(96.dp)
.align(Alignment.Top), .align(Alignment.Top),
) )
@ -77,7 +83,8 @@ fun showErrorDialog(text: String, stackTrace: String? = null) {
Row( Row(
horizontalArrangement = if (stackTrace != null) Arrangement.SpaceBetween else Arrangement.End, horizontalArrangement = if (stackTrace != null) Arrangement.SpaceBetween else Arrangement.End,
modifier = if (stackTrace != null) modifier =
if (stackTrace != null)
Modifier.align(Alignment.End).fillMaxWidth() Modifier.align(Alignment.End).fillMaxWidth()
else else
Modifier.align(Alignment.End), Modifier.align(Alignment.End),

View file

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

View file

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

View file

@ -25,7 +25,8 @@ abstract class AbstractBenchmark {
fun executeJmhRunner() { fun executeJmhRunner() {
if (this.javaClass.simpleName.contains("jmhType")) return if (this.javaClass.simpleName.contains("jmhType")) return
Files.createDirectories(Paths.get(benchmarkProperties.resultFolder)) Files.createDirectories(Paths.get(benchmarkProperties.resultFolder))
val extension = when (benchmarkProperties.resultFormat) { val extension =
when (benchmarkProperties.resultFormat) {
ResultFormatType.TEXT -> "txt" ResultFormatType.TEXT -> "txt"
ResultFormatType.CSV -> "csv" ResultFormatType.CSV -> "csv"
ResultFormatType.SCSV -> "scsv" ResultFormatType.SCSV -> "scsv"
@ -33,7 +34,8 @@ abstract class AbstractBenchmark {
ResultFormatType.LATEX -> "tex" ResultFormatType.LATEX -> "tex"
} }
val resultFile = Paths.get(benchmarkProperties.resultFolder, "${this.javaClass.name}.$extension").absolutePathString() 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 .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) } .apply { if (benchmarkProperties.warmupIterations > 0) warmupIterations(benchmarkProperties.warmupIterations) }
.measurementIterations(benchmarkProperties.measurementIterations) .measurementIterations(benchmarkProperties.measurementIterations)

View file

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

View file

@ -11,7 +11,6 @@ import java.util.concurrent.TimeUnit
@OutputTimeUnit(TimeUnit.MILLISECONDS) @OutputTimeUnit(TimeUnit.MILLISECONDS)
class BrowseBenchmark : AbstractRestBenchmark() { class BrowseBenchmark : AbstractRestBenchmark() {
companion object { companion object {
private lateinit var biggestSeriesId: String private lateinit var biggestSeriesId: String
} }
@ -24,7 +23,8 @@ class BrowseBenchmark : AbstractRestBenchmark() {
super.prepareData() super.prepareData()
// find series with most books // 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() .content.first()
.id .id
} }

View file

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

View file

@ -11,7 +11,6 @@ import java.util.concurrent.TimeUnit
@OutputTimeUnit(TimeUnit.MILLISECONDS) @OutputTimeUnit(TimeUnit.MILLISECONDS)
class UnsortedBenchmark : AbstractRestBenchmark() { class UnsortedBenchmark : AbstractRestBenchmark() {
companion object { companion object {
private lateinit var biggestSeriesId: String private lateinit var biggestSeriesId: String
} }
@ -23,7 +22,8 @@ class UnsortedBenchmark : AbstractRestBenchmark() {
super.prepareData() super.prepareData()
// find series with most books // 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() .content.first()
.id .id
} }

View file

@ -28,7 +28,8 @@ class LibraryScanScheduler(
// map the libraryId to the scan scheduled task // map the libraryId to the scan scheduled task
private val registry = ConcurrentHashMap<String, ScheduledTask>() private val registry = ConcurrentHashMap<String, ScheduledTask>()
private val registrar = ScheduledTaskRegistrar().apply { private val registrar =
ScheduledTaskRegistrar().apply {
setTaskScheduler(taskScheduler) 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) { class ScanLibrary(val libraryId: String, val scanDeep: Boolean, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "SCAN_LIBRARY_${libraryId}_DEEP_$scanDeep" override val uniqueId = "SCAN_LIBRARY_${libraryId}_DEEP_$scanDeep"
override fun toString(): String = "ScanLibrary(libraryId='$libraryId', scanDeep='$scanDeep', priority='$priority')" override fun toString(): String = "ScanLibrary(libraryId='$libraryId', scanDeep='$scanDeep', priority='$priority')"
} }
class FindBooksToConvert(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class FindBooksToConvert(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "FIND_BOOKS_TO_CONVERT_$libraryId" override val uniqueId = "FIND_BOOKS_TO_CONVERT_$libraryId"
override fun toString(): String = "FindBooksToConvert(libraryId='$libraryId', priority='$priority')" override fun toString(): String = "FindBooksToConvert(libraryId='$libraryId', priority='$priority')"
} }
class FindBooksWithMissingPageHash(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class FindBooksWithMissingPageHash(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "FIND_BOOKS_WITH_MISSING_PAGE_HASH_$libraryId" override val uniqueId = "FIND_BOOKS_WITH_MISSING_PAGE_HASH_$libraryId"
override fun toString(): String = "FindBooksWithMissingPageHash(libraryId='$libraryId', priority='$priority')" override fun toString(): String = "FindBooksWithMissingPageHash(libraryId='$libraryId', priority='$priority')"
} }
class FindDuplicatePagesToDelete(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class FindDuplicatePagesToDelete(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "FIND_DUPLICATE_PAGES_TO_DELETE_$libraryId" override val uniqueId = "FIND_DUPLICATE_PAGES_TO_DELETE_$libraryId"
override fun toString(): String = "FindDuplicatePagesToDelete(libraryId='$libraryId', priority='$priority')" override fun toString(): String = "FindDuplicatePagesToDelete(libraryId='$libraryId', priority='$priority')"
} }
class EmptyTrash(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class EmptyTrash(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "EMPTY_TRASH_$libraryId" override val uniqueId = "EMPTY_TRASH_$libraryId"
override fun toString(): String = "EmptyTrash(libraryId='$libraryId', priority='$priority')" override fun toString(): String = "EmptyTrash(libraryId='$libraryId', priority='$priority')"
} }
class AnalyzeBook(val bookId: String, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) { class AnalyzeBook(val bookId: String, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) {
override val uniqueId = "ANALYZE_BOOK_$bookId" override val uniqueId = "ANALYZE_BOOK_$bookId"
override fun toString(): String = "AnalyzeBook(bookId='$bookId', priority='$priority')" override fun toString(): String = "AnalyzeBook(bookId='$bookId', priority='$priority')"
} }
class GenerateBookThumbnail(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class GenerateBookThumbnail(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "GENERATE_BOOK_THUMBNAIL_$bookId" override val uniqueId = "GENERATE_BOOK_THUMBNAIL_$bookId"
override fun toString(): String = "GenerateBookThumbnail(bookId='$bookId', priority='$priority')" 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) { 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 val uniqueId = "REFRESH_BOOK_METADATA_$bookId"
override fun toString(): String = "RefreshBookMetadata(bookId='$bookId', capabilities=$capabilities, priority='$priority')" override fun toString(): String = "RefreshBookMetadata(bookId='$bookId', capabilities=$capabilities, priority='$priority')"
} }
class HashBook(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class HashBook(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "HASH_BOOK_$bookId" override val uniqueId = "HASH_BOOK_$bookId"
override fun toString(): String = "HashBook(bookId='$bookId', priority='$priority')" override fun toString(): String = "HashBook(bookId='$bookId', priority='$priority')"
} }
class HashBookPages(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class HashBookPages(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "HASH_BOOK_PAGES_$bookId" override val uniqueId = "HASH_BOOK_PAGES_$bookId"
override fun toString(): String = "HashBookPages(bookId='$bookId', priority='$priority')" override fun toString(): String = "HashBookPages(bookId='$bookId', priority='$priority')"
} }
class RefreshSeriesMetadata(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority, seriesId) { class RefreshSeriesMetadata(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority, seriesId) {
override val uniqueId = "REFRESH_SERIES_METADATA_$seriesId" override val uniqueId = "REFRESH_SERIES_METADATA_$seriesId"
override fun toString(): String = "RefreshSeriesMetadata(seriesId='$seriesId', priority='$priority')" override fun toString(): String = "RefreshSeriesMetadata(seriesId='$seriesId', priority='$priority')"
} }
class AggregateSeriesMetadata(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority, seriesId) { class AggregateSeriesMetadata(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority, seriesId) {
override val uniqueId = "AGGREGATE_SERIES_METADATA_$seriesId" override val uniqueId = "AGGREGATE_SERIES_METADATA_$seriesId"
override fun toString(): String = "AggregateSeriesMetadata(seriesId='$seriesId', priority='$priority')" override fun toString(): String = "AggregateSeriesMetadata(seriesId='$seriesId', priority='$priority')"
} }
class RefreshBookLocalArtwork(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class RefreshBookLocalArtwork(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId: String = "REFRESH_BOOK_LOCAL_ARTWORK_$bookId" override val uniqueId: String = "REFRESH_BOOK_LOCAL_ARTWORK_$bookId"
override fun toString(): String = "RefreshBookLocalArtwork(bookId='$bookId', priority='$priority')" override fun toString(): String = "RefreshBookLocalArtwork(bookId='$bookId', priority='$priority')"
} }
class RefreshSeriesLocalArtwork(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class RefreshSeriesLocalArtwork(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId: String = "REFRESH_SERIES_LOCAL_ARTWORK_$seriesId" override val uniqueId: String = "REFRESH_SERIES_LOCAL_ARTWORK_$seriesId"
override fun toString(): String = "RefreshSeriesLocalArtwork(seriesId=$seriesId, priority='$priority')" 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) { 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 val uniqueId: String = "IMPORT_BOOK_${seriesId}_$sourceFile"
override fun toString(): String = override fun toString(): String =
"ImportBook(sourceFile='$sourceFile', seriesId='$seriesId', copyMode=$copyMode, destinationName=$destinationName, upgradeBookId=$upgradeBookId, priority='$priority')" "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) { class ConvertBook(val bookId: String, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) {
override val uniqueId: String = "CONVERT_BOOK_$bookId" override val uniqueId: String = "CONVERT_BOOK_$bookId"
override fun toString(): String = "ConvertBook(bookId='$bookId', priority='$priority')" override fun toString(): String = "ConvertBook(bookId='$bookId', priority='$priority')"
} }
class RepairExtension(val bookId: String, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) { class RepairExtension(val bookId: String, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) {
override val uniqueId: String = "REPAIR_EXTENSION_$bookId" override val uniqueId: String = "REPAIR_EXTENSION_$bookId"
override fun toString(): String = "RepairExtension(bookId='$bookId', priority='$priority')" override fun toString(): String = "RepairExtension(bookId='$bookId', priority='$priority')"
} }
class RemoveHashedPages(val bookId: String, val pages: Collection<BookPageNumbered>, priority: Int = DEFAULT_PRIORITY) : Task(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 val uniqueId: String = "REMOVE_HASHED_PAGES_$bookId"
override fun toString(): String = "RemoveHashedPages(bookId='$bookId', priority='$priority')" override fun toString(): String = "RemoveHashedPages(bookId='$bookId', priority='$priority')"
} }
class RebuildIndex(val entities: Set<LuceneEntity>?, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class RebuildIndex(val entities: Set<LuceneEntity>?, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "REBUILD_INDEX" override val uniqueId = "REBUILD_INDEX"
override fun toString(): String = "RebuildIndex(priority='$priority',entities='${entities?.map { it.type }}')" override fun toString(): String = "RebuildIndex(priority='$priority',entities='${entities?.map { it.type }}')"
} }
class UpgradeIndex(priority: Int = DEFAULT_PRIORITY) : Task(priority) { class UpgradeIndex(priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "UPGRADE_INDEX" override val uniqueId = "UPGRADE_INDEX"
override fun toString(): String = "UpgradeIndex(priority='$priority')" override fun toString(): String = "UpgradeIndex(priority='$priority')"
} }
class DeleteBook(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class DeleteBook(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "DELETE_BOOK_$bookId" override val uniqueId = "DELETE_BOOK_$bookId"
override fun toString(): String = "DeleteBook(bookId='$bookId', priority='$priority')" override fun toString(): String = "DeleteBook(bookId='$bookId', priority='$priority')"
} }
class DeleteSeries(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class DeleteSeries(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "DELETE_SERIES_$seriesId" override val uniqueId = "DELETE_SERIES_$seriesId"
override fun toString(): String = "DeleteSeries(seriesId='$seriesId', priority='$priority')" override fun toString(): String = "DeleteSeries(seriesId='$seriesId', priority='$priority')"
} }
class FixThumbnailsWithoutMetadata(priority: Int = DEFAULT_PRIORITY) : Task(priority, "FixThumbnailsWithoutMetadata") { class FixThumbnailsWithoutMetadata(priority: Int = DEFAULT_PRIORITY) : Task(priority, "FixThumbnailsWithoutMetadata") {
override val uniqueId = "FIX_THUMBNAILS_WITHOUT_METADATA_${LocalDateTime.now()}" override val uniqueId = "FIX_THUMBNAILS_WITHOUT_METADATA_${LocalDateTime.now()}"
override fun toString(): String = "FixThumbnailsWithoutMetadata(priority='$priority')" override fun toString(): String = "FixThumbnailsWithoutMetadata(priority='$priority')"
} }
class FindBookThumbnailsToRegenerate(val forBiggerResultOnly: Boolean, priority: Int = DEFAULT_PRIORITY) : Task(priority) { class FindBookThumbnailsToRegenerate(val forBiggerResultOnly: Boolean, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
override val uniqueId = "FIND_BOOK_THUMBNAILS_TO_REGENERATE" override val uniqueId = "FIND_BOOK_THUMBNAILS_TO_REGENERATE"
override fun toString(): String = "FindBookThumbnailsToRegenerate(forBiggerResultOnly='$forBiggerResultOnly', priority='$priority')" override fun toString(): String = "FindBookThumbnailsToRegenerate(forBiggerResultOnly='$forBiggerResultOnly', priority='$priority')"
} }
} }

View file

@ -25,11 +25,18 @@ class TaskEmitter(
private val tasksRepository: TasksRepository, private val tasksRepository: TasksRepository,
private val eventPublisher: ApplicationEventPublisher, 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)) 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)) submitTask(Task.EmptyTrash(libraryId, priority))
} }
@ -55,27 +62,42 @@ class TaskEmitter(
.let { submitTasks(it) } .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)) 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 bookIdToSeriesId
.map { Task.HashBookPages(it, priority) } .map { Task.HashBookPages(it, priority) }
.let { submitTasks(it) } .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)) 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 books
.map { Task.ConvertBook(it.id, priority, it.seriesId) } .map { Task.ConvertBook(it.id, priority, it.seriesId) }
.let { submitTasks(it) } .let { submitTasks(it) }
} }
fun repairExtensions(library: Library, priority: Int = DEFAULT_PRIORITY) { fun repairExtensions(
library: Library,
priority: Int = DEFAULT_PRIORITY,
) {
if (library.repairExtensions) if (library.repairExtensions)
bookConverter bookConverter
.getMismatchedExtensionBooks(library) .getMismatchedExtensionBooks(library)
@ -83,35 +105,57 @@ class TaskEmitter(
.let { submitTasks(it) } .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)) 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)) 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 bookIdToPages
.map { Task.RemoveHashedPages(it.key, it.value, priority) } .map { Task.RemoveHashedPages(it.key, it.value, priority) }
.let { submitTasks(it) } .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)) 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 books
.map { Task.AnalyzeBook(it.id, priority, it.seriesId) } .map { Task.AnalyzeBook(it.id, priority, it.seriesId) }
.let { submitTasks(it) } .let { submitTasks(it) }
} }
fun generateBookThumbnail(bookId: String, priority: Int = DEFAULT_PRIORITY) { fun generateBookThumbnail(
bookId: String,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.GenerateBookThumbnail(bookId, 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 bookIds
.map { Task.GenerateBookThumbnail(it, priority) } .map { Task.GenerateBookThumbnail(it, priority) }
.let { submitTasks(it) } .let { submitTasks(it) }
@ -135,39 +179,67 @@ class TaskEmitter(
.let { submitTasks(it) } .let { submitTasks(it) }
} }
fun refreshSeriesMetadata(seriesId: String, priority: Int = DEFAULT_PRIORITY) { fun refreshSeriesMetadata(
seriesId: String,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.RefreshSeriesMetadata(seriesId, 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)) 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)) 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 books
.map { Task.RefreshBookLocalArtwork(it.id, priority) } .map { Task.RefreshBookLocalArtwork(it.id, priority) }
.let { submitTasks(it) } .let { submitTasks(it) }
} }
fun refreshSeriesLocalArtwork(seriesId: String, priority: Int = DEFAULT_PRIORITY) { fun refreshSeriesLocalArtwork(
seriesId: String,
priority: Int = DEFAULT_PRIORITY,
) {
submitTask(Task.RefreshSeriesLocalArtwork(seriesId, 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 seriesIds
.map { Task.RefreshSeriesLocalArtwork(it, priority) } .map { Task.RefreshSeriesLocalArtwork(it, priority) }
.let { submitTasks(it) } .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)) 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)) submitTask(Task.RebuildIndex(entities, priority))
} }
@ -175,11 +247,17 @@ class TaskEmitter(
submitTask(Task.UpgradeIndex(priority)) 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)) 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)) submitTask(Task.DeleteSeries(seriesId, priority))
} }
@ -187,7 +265,10 @@ class TaskEmitter(
submitTask(Task.FixThumbnailsWithoutMetadata(priority)) 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)) submitTask(Task.FindBookThumbnailsToRegenerate(forBiggerResultOnly, priority))
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,46 +3,65 @@ package org.gotson.komga.domain.model
import java.net.URL import java.net.URL
sealed class DomainEvent { sealed class DomainEvent {
data class LibraryAdded(val library: Library) : DomainEvent() data class LibraryAdded(val library: Library) : DomainEvent()
data class LibraryUpdated(val library: Library) : DomainEvent() data class LibraryUpdated(val library: Library) : DomainEvent()
data class LibraryDeleted(val library: Library) : DomainEvent() data class LibraryDeleted(val library: Library) : DomainEvent()
data class LibraryScanned(val library: Library) : DomainEvent() data class LibraryScanned(val library: Library) : DomainEvent()
data class SeriesAdded(val series: Series) : DomainEvent() data class SeriesAdded(val series: Series) : DomainEvent()
data class SeriesUpdated(val series: Series) : DomainEvent() data class SeriesUpdated(val series: Series) : DomainEvent()
data class SeriesDeleted(val series: Series) : DomainEvent() data class SeriesDeleted(val series: Series) : DomainEvent()
data class BookAdded(val book: Book) : DomainEvent() data class BookAdded(val book: Book) : DomainEvent()
data class BookUpdated(val book: Book) : DomainEvent() data class BookUpdated(val book: Book) : DomainEvent()
data class BookDeleted(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 BookImported(val book: Book?, val sourceFile: URL, val success: Boolean, val message: String? = null) : DomainEvent()
data class CollectionAdded(val collection: SeriesCollection) : DomainEvent() data class CollectionAdded(val collection: SeriesCollection) : DomainEvent()
data class CollectionUpdated(val collection: SeriesCollection) : DomainEvent() data class CollectionUpdated(val collection: SeriesCollection) : DomainEvent()
data class CollectionDeleted(val collection: SeriesCollection) : DomainEvent() data class CollectionDeleted(val collection: SeriesCollection) : DomainEvent()
data class ReadListAdded(val readList: ReadList) : DomainEvent() data class ReadListAdded(val readList: ReadList) : DomainEvent()
data class ReadListUpdated(val readList: ReadList) : DomainEvent() data class ReadListUpdated(val readList: ReadList) : DomainEvent()
data class ReadListDeleted(val readList: ReadList) : DomainEvent() data class ReadListDeleted(val readList: ReadList) : DomainEvent()
data class ReadProgressChanged(val progress: ReadProgress) : DomainEvent() data class ReadProgressChanged(val progress: ReadProgress) : DomainEvent()
data class ReadProgressDeleted(val progress: ReadProgress) : DomainEvent() data class ReadProgressDeleted(val progress: ReadProgress) : DomainEvent()
data class ReadProgressSeriesChanged(val seriesId: String, val userId: String) : DomainEvent() data class ReadProgressSeriesChanged(val seriesId: String, val userId: String) : DomainEvent()
data class ReadProgressSeriesDeleted(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 ThumbnailBookAdded(val thumbnail: ThumbnailBook) : DomainEvent()
data class ThumbnailBookDeleted(val thumbnail: ThumbnailBook) : DomainEvent() data class ThumbnailBookDeleted(val thumbnail: ThumbnailBook) : DomainEvent()
data class ThumbnailSeriesAdded(val thumbnail: ThumbnailSeries) : DomainEvent() data class ThumbnailSeriesAdded(val thumbnail: ThumbnailSeries) : DomainEvent()
data class ThumbnailSeriesDeleted(val thumbnail: ThumbnailSeries) : DomainEvent() data class ThumbnailSeriesDeleted(val thumbnail: ThumbnailSeries) : DomainEvent()
data class ThumbnailSeriesCollectionAdded(val thumbnail: ThumbnailSeriesCollection) : DomainEvent() data class ThumbnailSeriesCollectionAdded(val thumbnail: ThumbnailSeriesCollection) : DomainEvent()
data class ThumbnailSeriesCollectionDeleted(val thumbnail: ThumbnailSeriesCollection) : DomainEvent() data class ThumbnailSeriesCollectionDeleted(val thumbnail: ThumbnailSeriesCollection) : DomainEvent()
data class ThumbnailReadListAdded(val thumbnail: ThumbnailReadList) : DomainEvent() data class ThumbnailReadListAdded(val thumbnail: ThumbnailReadList) : DomainEvent()
data class ThumbnailReadListDeleted(val thumbnail: ThumbnailReadList) : DomainEvent() data class ThumbnailReadListDeleted(val thumbnail: ThumbnailReadList) : DomainEvent()
data class UserUpdated(val user: KomgaUser, val expireSession: Boolean) : DomainEvent() data class UserUpdated(val user: KomgaUser, val expireSession: Boolean) : DomainEvent()
data class UserDeleted(val user: KomgaUser) : 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) fun Exception.withCode(code: String) = CodedException(this, code)
class MediaNotReadyException : Exception() class MediaNotReadyException : Exception()
class NoThumbnailFoundException : Exception() class NoThumbnailFoundException : Exception()
class MediaUnsupportedException(message: String, code: String = "") : CodedException(message, code) class MediaUnsupportedException(message: String, code: String = "") : CodedException(message, code)
class ImageConversionException(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 DirectoryNotFoundException(message: String, code: String = "") : CodedException(message, code)
class DuplicateNameException(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 PathContainedInPath(message: String, code: String = "") : CodedException(message, code)
class UserEmailAlreadyExistsException(message: String, code: String = "") : CodedException(message, code) class UserEmailAlreadyExistsException(message: String, code: String = "") : CodedException(message, code)
class BookConversionException(message: String) : Exception(message) class BookConversionException(message: String) : Exception(message)
class ComicRackListException(message: String, code: String = "") : CodedException(message, code) class ComicRackListException(message: String, code: String = "") : CodedException(message, code)
class EntryNotFoundException(message: String) : Exception(message) class EntryNotFoundException(message: String) : Exception(message)

View file

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

View file

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

View file

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

View file

@ -1,5 +1,7 @@
package org.gotson.komga.domain.model package org.gotson.komga.domain.model
enum class MarkSelectedPreference { 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 createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate, override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable { ) : Auditable {
@delegate:Transient @delegate:Transient
val profile: MediaProfile? by lazy { MediaType.fromMediaType(mediaType)?.profile } val profile: MediaProfile? by lazy { MediaType.fromMediaType(mediaType)?.profile }
enum class Status { enum class Status {
UNKNOWN, ERROR, READY, UNSUPPORTED, OUTDATED UNKNOWN,
ERROR,
READY,
UNSUPPORTED,
OUTDATED,
} }
override fun toString(): String { override fun toString(): String {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,18 +10,14 @@ data class Series(
val name: String, val name: String,
val url: URL, val url: URL,
val fileLastModified: LocalDateTime, val fileLastModified: LocalDateTime,
val id: String = TsidCreator.getTsid256().toString(), val id: String = TsidCreator.getTsid256().toString(),
val libraryId: String = "", val libraryId: String = "",
val bookCount: Int = 0, val bookCount: Int = 0,
val deletedDate: LocalDateTime? = null, val deletedDate: LocalDateTime? = null,
val oneshot: Boolean = false, val oneshot: Boolean = false,
override val createdDate: LocalDateTime = LocalDateTime.now(), override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate, override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable { ) : Auditable {
@delegate:Transient @delegate:Transient
val path: Path by lazy { this.url.toURI().toPath() } 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 * Indicates whether the collection is ordered manually
*/ */
val ordered: Boolean = false, val ordered: Boolean = false,
val seriesIds: List<String> = emptyList(), val seriesIds: List<String> = emptyList(),
val id: String = TsidCreator.getTsid256().toString(), val id: String = TsidCreator.getTsid256().toString(),
override val createdDate: LocalDateTime = LocalDateTime.now(), override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate, override val lastModifiedDate: LocalDateTime = createdDate,
/** /**
* Indicates that the seriesIds have been filtered and is not exhaustive. * Indicates that the seriesIds have been filtered and is not exhaustive.
*/ */

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,40 +10,85 @@ import java.net.URL
interface BookRepository { interface BookRepository {
fun findByIdOrNull(bookId: String): Book? fun findByIdOrNull(bookId: String): Book?
fun findNotDeletedByLibraryIdAndUrlOrNull(libraryId: String, url: URL): Book?
fun findNotDeletedByLibraryIdAndUrlOrNull(
libraryId: String,
url: URL,
): Book?
fun findAll(): Collection<Book> fun findAll(): Collection<Book>
fun findAllBySeriesId(seriesId: String): Collection<Book> fun findAllBySeriesId(seriesId: String): Collection<Book>
fun findAllBySeriesIds(seriesIds: Collection<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): 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 findAllDeletedByFileSize(fileSize: Long): Collection<Book>
fun findAllByLibraryIdAndWithEmptyHash(libraryId: String): 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 getLibraryIdOrNull(bookId: String): String?
fun getSeriesIdOrNull(bookId: String): String? fun getSeriesIdOrNull(bookId: String): String?
fun findFirstIdInSeriesOrNull(seriesId: String): String? fun findFirstIdInSeriesOrNull(seriesId: String): String?
fun findLastIdInSeriesOrNull(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 findAllIdsBySeriesId(seriesId: String): Collection<String>
fun findAllIdsBySeriesIds(seriesIds: Collection<String>): Collection<String> fun findAllIdsBySeriesIds(seriesIds: Collection<String>): Collection<String>
fun findAllIdsByLibraryId(libraryId: 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(book: Book)
fun insert(books: Collection<Book>) fun insert(books: Collection<Book>)
fun update(book: Book) fun update(book: Book)
fun update(books: Collection<Book>) fun update(books: Collection<Book>)
fun delete(bookId: String) fun delete(bookId: String)
fun delete(bookIds: Collection<String>) fun delete(bookIds: Collection<String>)
fun deleteAll() fun deleteAll()
fun count(): Long fun count(): Long
fun countGroupedByLibraryId(): Map<String, Int> fun countGroupedByLibraryId(): Map<String, Int>
fun getFilesizeGroupedByLibraryId(): Map<String, BigDecimal> fun getFilesizeGroupedByLibraryId(): Map<String, BigDecimal>

View file

@ -6,6 +6,7 @@ interface KomgaUserRepository {
fun count(): Long fun count(): Long
fun findByIdOrNull(id: String): KomgaUser? fun findByIdOrNull(id: String): KomgaUser?
fun findByEmailIgnoreCaseOrNull(email: String): KomgaUser? fun findByEmailIgnoreCaseOrNull(email: String): KomgaUser?
fun findAll(): Collection<KomgaUser> fun findAll(): Collection<KomgaUser>
@ -13,11 +14,17 @@ interface KomgaUserRepository {
fun existsByEmailIgnoreCase(email: String): Boolean fun existsByEmailIgnoreCase(email: String): Boolean
fun insert(user: KomgaUser) fun insert(user: KomgaUser)
fun update(user: KomgaUser) fun update(user: KomgaUser)
fun delete(userId: String) fun delete(userId: String)
fun deleteAll() fun deleteAll()
fun findAnnouncementIdsReadByUserId(userId: String): Set<String> 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 { interface LibraryRepository {
fun findById(libraryId: String): Library fun findById(libraryId: String): Library
fun findByIdOrNull(libraryId: String): Library? fun findByIdOrNull(libraryId: String): Library?
fun findAll(): Collection<Library> fun findAll(): Collection<Library>
fun findAllByIds(libraryIds: Collection<String>): Collection<Library> fun findAllByIds(libraryIds: Collection<String>): Collection<Library>
fun delete(libraryId: String) fun delete(libraryId: String)
fun deleteAll() fun deleteAll()
fun insert(library: Library) fun insert(library: Library)
fun update(library: Library) fun update(library: Library)
fun count(): Long fun count(): Long

View file

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

View file

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

View file

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

View file

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

View file

@ -6,51 +6,185 @@ import org.springframework.data.domain.Pageable
import java.time.LocalDate import java.time.LocalDate
interface ReferentialRepository { interface ReferentialRepository {
fun findAllAuthorsByName(search: String, filterOnLibraryIds: Collection<String>?): List<Author> fun findAllAuthorsByName(
fun findAllAuthorsByNameAndLibrary(search: String, libraryId: String, filterOnLibraryIds: Collection<String>?): List<Author> search: String,
fun findAllAuthorsByNameAndCollection(search: String, collectionId: String, filterOnLibraryIds: Collection<String>?): List<Author> filterOnLibraryIds: Collection<String>?,
fun findAllAuthorsByNameAndSeries(search: String, seriesId: String, filterOnLibraryIds: Collection<String>?): List<Author> ): List<Author>
fun findAllAuthorsNamesByName(search: String, filterOnLibraryIds: Collection<String>?): List<String>
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 findAllAuthorsRoles(filterOnLibraryIds: Collection<String>?): List<String>
fun findAllAuthorsByName(search: String?, role: String?, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author> fun findAllAuthorsByName(
fun findAllAuthorsByNameAndLibrary(search: String?, role: String?, libraryId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author> search: String?,
fun findAllAuthorsByNameAndCollection(search: String?, role: String?, collectionId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author> role: String?,
fun findAllAuthorsByNameAndSeries(search: String?, role: String?, seriesId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author> filterOnLibraryIds: Collection<String>?,
fun findAllAuthorsByNameAndReadList(search: String?, role: String?, readListId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author> 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 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 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 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 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 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>?): Set<String>
fun findAllPublishers(filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<String>
fun findAllPublishersByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String> fun findAllPublishers(
fun findAllPublishersByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String> 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 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 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 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], * Find one SeriesCollection by [collectionId],
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null. * 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 * Find all SeriesCollection
* optionally with at least one Series belonging to the provided [belongsToLibraryIds] if not null, * 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. * 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], * Find all SeriesCollection that contains the provided [containsSeriesId],
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null. * 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 findAllEmpty(): Collection<SeriesCollection>
fun findByNameOrNull(name: String): SeriesCollection? fun findByNameOrNull(name: String): SeriesCollection?
fun insert(collection: SeriesCollection) fun insert(collection: SeriesCollection)
fun update(collection: SeriesCollection) fun update(collection: SeriesCollection)
fun removeSeriesFromAll(seriesId: String) fun removeSeriesFromAll(seriesId: String)
fun removeSeriesFromAll(seriesIds: Collection<String>) fun removeSeriesFromAll(seriesIds: Collection<String>)
fun delete(collectionId: String) fun delete(collectionId: String)
fun delete(collectionIds: Collection<String>) fun delete(collectionIds: Collection<String>)
fun deleteAll() fun deleteAll()
fun existsByName(name: String): Boolean fun existsByName(name: String): Boolean

View file

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

View file

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

View file

@ -7,9 +7,16 @@ import java.net.URL
interface SidecarRepository { interface SidecarRepository {
fun findAll(): Collection<SidecarStored> 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 deleteByLibraryId(libraryId: String)
fun countGroupedByLibraryId(): Map<String, Int> fun countGroupedByLibraryId(): Map<String, Int>

View file

@ -6,20 +6,39 @@ import org.springframework.data.domain.Pageable
interface ThumbnailBookRepository { interface ThumbnailBookRepository {
fun findByIdOrNull(thumbnailId: String): ThumbnailBook? fun findByIdOrNull(thumbnailId: String): ThumbnailBook?
fun findSelectedByBookIdOrNull(bookId: String): ThumbnailBook? fun findSelectedByBookIdOrNull(bookId: String): ThumbnailBook?
fun findAllByBookId(bookId: String): Collection<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 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 insert(thumbnail: ThumbnailBook)
fun update(thumbnail: ThumbnailBook) fun update(thumbnail: ThumbnailBook)
fun updateMetadata(thumbnails: Collection<ThumbnailBook>) fun updateMetadata(thumbnails: Collection<ThumbnailBook>)
fun markSelected(thumbnail: ThumbnailBook) fun markSelected(thumbnail: ThumbnailBook)
fun delete(thumbnailBookId: String) fun delete(thumbnailBookId: String)
fun deleteByBookId(bookId: String) fun deleteByBookId(bookId: String)
fun deleteByBookIdAndType(bookId: String, type: ThumbnailBook.Type)
fun deleteByBookIdAndType(
bookId: String,
type: ThumbnailBook.Type,
)
fun deleteByBookIds(bookIds: Collection<String>) fun deleteByBookIds(bookIds: Collection<String>)
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,8 +26,10 @@ class BookMetadataLifecycle(
private val readListLifecycle: ReadListLifecycle, private val readListLifecycle: ReadListLifecycle,
private val eventPublisher: ApplicationEventPublisher, private val eventPublisher: ApplicationEventPublisher,
) { ) {
fun refreshMetadata(
fun refreshMetadata(book: Book, capabilities: Set<BookMetadataPatchCapability>) { book: Book,
capabilities: Set<BookMetadataPatchCapability>,
) {
logger.info { "Refresh metadata for book: $book with capabilities: $capabilities" } logger.info { "Refresh metadata for book: $book with capabilities: $capabilities" }
val media = mediaRepository.findById(book.id) val media = mediaRepository.findById(book.id)
@ -44,7 +46,8 @@ class BookMetadataLifecycle(
else -> { else -> {
logger.debug { "Provider: ${provider.javaClass.simpleName}" } logger.debug { "Provider: ${provider.javaClass.simpleName}" }
val patch = try { val patch =
try {
provider.getBookMetadataFromBook(BookWithMedia(book, media)) provider.getBookMetadataFromBook(BookWithMedia(book, media))
} catch (e: Exception) { } catch (e: Exception) {
logger.error(e) { "Error while getting metadata from ${provider.javaClass.simpleName} for book: $book" } 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>() private val failedPageRemoval = mutableListOf<String>()
fun removeHashedPages(book: Book, pagesToDelete: Collection<BookPageNumbered>): BookAction? { fun removeHashedPages(
book: Book,
pagesToDelete: Collection<BookPageNumbered>,
): BookAction? {
// perform various checks // perform various checks
if (failedPageRemoval.contains(book.id)) { if (failedPageRemoval.contains(book.id)) {
logger.info { "Book page removal already failed before, skipping" } logger.info { "Book page removal already failed before, skipping" }
@ -75,7 +78,8 @@ class BookPageEditor(
throw MediaNotReadyException() throw MediaNotReadyException()
// create a temp file with the pages removed // 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 -> pagesToDelete.find { candidate ->
candidate.fileHash == page.fileHash && candidate.fileHash == page.fileHash &&
candidate.mediaType == page.mediaType && candidate.mediaType == page.mediaType &&
@ -109,7 +113,8 @@ class BookPageEditor(
} }
// perform checks on new file // perform checks on new file
val createdBook = fileSystemScanner.scanFile(tempFile) val createdBook =
fileSystemScanner.scanFile(tempFile)
?.copy( ?.copy(
id = book.id, id = book.id,
seriesId = book.seriesId, seriesId = book.seriesId,
@ -142,7 +147,8 @@ class BookPageEditor(
} }
tempFile.moveTo(book.path, true) tempFile.moveTo(book.path, true)
val newBook = fileSystemScanner.scanFile(book.path) val newBook =
fileSystemScanner.scanFile(book.path)
?.copy( ?.copy(
id = book.id, id = book.id,
seriesId = book.seriesId, seriesId = book.seriesId,

View file

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

View file

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

View file

@ -61,11 +61,14 @@ class LibraryContentLifecycle(
private val thumbnailBookRepository: ThumbnailBookRepository, private val thumbnailBookRepository: ThumbnailBookRepository,
private val eventPublisher: ApplicationEventPublisher, private val eventPublisher: ApplicationEventPublisher,
) { ) {
fun scanRootFolder(
fun scanRootFolder(library: Library, scanDeep: Boolean = false) { library: Library,
scanDeep: Boolean = false,
) {
logger.info { "Scan root folder for library: $library" } logger.info { "Scan root folder for library: $library" }
measureTime { measureTime {
val scanResult = try { val scanResult =
try {
fileSystemScanner.scanRootFolder( fileSystemScanner.scanRootFolder(
Paths.get(library.root.toURI()), Paths.get(library.root.toURI()),
library.scanForceModifiedTime, 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 // 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) val books = bookRepository.findAllNotDeletedByLibraryIdAndUrlNotIn(library.id, urls)
if (books.isNotEmpty()) { if (books.isNotEmpty()) {
logger.info { "Soft deleting books not on disk anymore: $books" } logger.info { "Soft deleting books not on disk anymore: $books" }
bookLifecycle.softDeleteMany(books) bookLifecycle.softDeleteMany(books)
books.map { it.seriesId }.distinct().mapNotNull { seriesRepository.findByIdOrNull(it) }.toMutableList() 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 // 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 // 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 -> existingBooks.find { it.url == newBook.url && it.deletedDate == null }?.let { existingBook ->
logger.debug { "Matched existing book: $existingBook" } logger.debug { "Matched existing book: $existingBook" }
if (newBook.fileLastModified.notEquals(existingBook.fileLastModified)) { 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) hasher.computeHash(newBook.path)
} else null } else {
null
}
if (hash == existingBook.fileHash) { if (hash == existingBook.fileHash) {
logger.info { "Book changed on disk, but still has the same hash, no need to reset media status: $existingBook" } 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, fileLastModified = newBook.fileLastModified,
fileSize = newBook.fileSize, fileSize = newBook.fileSize,
fileHash = hash, fileHash = hash,
@ -168,7 +178,8 @@ class LibraryContentLifecycle(
bookRepository.update(updatedBook) bookRepository.update(updatedBook)
} else { } else {
logger.info { "Book changed on disk, update and reset media status: $existingBook" } logger.info { "Book changed on disk, update and reset media status: $existingBook" }
val updatedBook = existingBook.copy( val updatedBook =
existingBook.copy(
fileLastModified = newBook.fileLastModified, fileLastModified = newBook.fileLastModified,
fileSize = newBook.fileSize, fileSize = newBook.fileSize,
fileHash = hash ?: "", fileHash = hash ?: "",
@ -237,8 +248,10 @@ class LibraryContentLifecycle(
} }
} }
if (library.emptyTrashAfterScan) emptyTrash(library) if (library.emptyTrashAfterScan)
else cleanupEmptySets() emptyTrash(library)
else
cleanupEmptySets()
}.also { logger.info { "Library updated in $it" } } }.also { logger.info { "Library updated in $it" } }
eventPublisher.publishEvent(DomainEvent.LibraryScanned(library)) 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. * - Metadata. The metadata title will only be copied if locked. If not locked, the folder name is used.
* - all books, via #tryRestoreBooks * - 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" } logger.info { "Try to restore series: $newSeries" }
val bookSizes = newBooks.map { it.fileSize } val bookSizes = newBooks.map { it.fileSize }
val deletedCandidates = seriesRepository.findAll(SeriesSearch(deleted = true)) val deletedCandidates =
seriesRepository.findAll(SeriesSearch(deleted = true))
.mapNotNull { deletedCandidate -> .mapNotNull { deletedCandidate ->
val deletedBooks = bookRepository.findAllBySeriesId(deletedCandidate.id) val deletedBooks = bookRepository.findAllBySeriesId(deletedCandidate.id)
val deletedBooksSizes = deletedBooks.map { it.fileSize } val deletedBooksSizes = deletedBooks.map { it.fileSize }
if (newBooks.size == deletedBooks.size && bookSizes.containsAll(deletedBooksSizes) && deletedBooksSizes.containsAll(bookSizes) && deletedBooks.all { it.fileHash.isNotBlank() }) { if (newBooks.size == deletedBooks.size && bookSizes.containsAll(deletedBooksSizes) && deletedBooksSizes.containsAll(bookSizes) && deletedBooks.all { it.fileHash.isNotBlank() }) {
deletedCandidate to deletedBooks deletedCandidate to deletedBooks
} else null } else {
null
}
} }
logger.debug { "Deleted series candidates: $deletedCandidates" } 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)) } val newBooksWithHash = newBooks.map { book -> bookRepository.findByIdOrNull(book.id)!!.copy(fileHash = hasher.computeHash(book.path)) }
bookRepository.update(newBooksWithHash) 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 }) 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 (deletedCandidates.isNotEmpty()) {
// if the book has no hash, compute the hash and store it // if the book has no hash, compute the hash and store it
val bookWithHash = val bookWithHash =
if (bookToAdd.fileHash.isNotBlank()) bookToAdd if (bookToAdd.fileHash.isNotBlank())
else bookRepository.findByIdOrNull(bookToAdd.id)!!.copy(fileHash = hasher.computeHash(bookToAdd.path)).also { bookRepository.update(it) } bookToAdd
else
bookRepository.findByIdOrNull(bookToAdd.id)!!.copy(fileHash = hasher.computeHash(bookToAdd.path)).also { bookRepository.update(it) }
val match = deletedCandidates.find { it.fileHash == bookWithHash.fileHash } val match = deletedCandidates.find { it.fileHash == bookWithHash.fileHash }

View file

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

View file

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

View file

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

View file

@ -8,12 +8,20 @@ import org.springframework.stereotype.Service
@Service @Service
class MetadataApplier { 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 = fun apply(
if (patched != null && !lock) patched patch: BookMetadataPatch,
else original metadata: BookMetadata,
): BookMetadata =
fun apply(patch: BookMetadataPatch, metadata: BookMetadata): BookMetadata =
with(metadata) { with(metadata) {
copy( copy(
title = getIfNotLocked(title, patch.title, titleLock), 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) { with(metadata) {
copy( copy(
status = getIfNotLocked(status, patch.status, statusLock), status = getIfNotLocked(status, patch.status, statusLock),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -122,15 +122,19 @@ class ThumbnailLifecycle(
copier: (T, ThumbnailMetadata) -> T, copier: (T, ThumbnailMetadata) -> T,
updater: (Collection<T>) -> Unit, updater: (Collection<T>) -> Unit,
): Boolean { ): Boolean {
val (result, duration) = measureTimedValue { val (result, duration) =
measureTimedValue {
val thumbs = fetcher(Pageable.ofSize(1000)) val thumbs = fetcher(Pageable.ofSize(1000))
logger.info { "Fetched ${thumbs.numberOfElements} ${clazz.simpleName} to fix, total: ${thumbs.totalElements}" } logger.info { "Fetched ${thumbs.numberOfElements} ${clazz.simpleName} to fix, total: ${thumbs.totalElements}" }
val fixedThumbs = thumbs.mapNotNull { val fixedThumbs =
thumbs.mapNotNull {
try { try {
val meta = supplier(it) val meta = supplier(it)
if (meta == null) null if (meta == null)
else copier(it, meta) null
else
copier(it, meta)
} catch (e: Exception) { } catch (e: Exception) {
logger.error(e) { "Could not fix thumbnail: $it" } logger.error(e) { "Could not fix thumbnail: $it" }
null null
@ -159,5 +163,6 @@ class ThumbnailLifecycle(
) )
private data class Result(val processed: Int, val hasMore: Boolean) private data class Result(val processed: Int, val hasMore: Boolean)
private data class ThumbnailMetadata(val mediaType: String, val fileSize: Long, val dimension: Dimension) 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>, private val seriesMetadataProviders: List<SeriesMetadataFromBookProvider>,
bookMetadataProviders: List<BookMetadataProvider>, bookMetadataProviders: List<BookMetadataProvider>,
) { ) {
val bookMetadataProviders = bookMetadataProviders.filter { it.capabilities.contains(BookMetadataPatchCapability.NUMBER_SORT) } val bookMetadataProviders = bookMetadataProviders.filter { it.capabilities.contains(BookMetadataPatchCapability.NUMBER_SORT) }
fun scanAndPersist(filePath: String): List<TransientBook> { fun scanAndPersist(filePath: String): List<TransientBook> {
@ -60,7 +59,8 @@ class TransientBookLifecycle(
fun getMetadata(transientBook: TransientBook): Pair<String?, Float?> { fun getMetadata(transientBook: TransientBook): Pair<String?, Float?> {
val bookWithMedia = transientBook.toBookWithMedia() val bookWithMedia = transientBook.toBookWithMedia()
val number = bookMetadataProviders.firstNotNullOfOrNull { it.getBookMetadataFromBook(bookWithMedia)?.numberSort } val number = bookMetadataProviders.firstNotNullOfOrNull { it.getBookMetadataFromBook(bookWithMedia)?.numberSort }
val series = seriesMetadataProviders val series =
seriesMetadataProviders
.flatMap { .flatMap {
buildList { buildList {
if (it.supportsAppendVolume) add(it.getSeriesMetadataFromBook(bookWithMedia, true)?.title) if (it.supportsAppendVolume) add(it.getSeriesMetadataFromBook(bookWithMedia, true)?.title)
@ -77,11 +77,16 @@ class TransientBookLifecycle(
MediaNotReadyException::class, MediaNotReadyException::class,
IndexOutOfBoundsException::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 pageContent = bookAnalyzer.getPageContent(transientBook.toBookWithMedia(), number)
val pageMediaType = val pageMediaType =
if (transientBook.media.profile == MediaProfile.PDF) pdfImageType.mediaType if (transientBook.media.profile == MediaProfile.PDF)
else transientBook.media.pages[number - 1].mediaType pdfImageType.mediaType
else
transientBook.media.pages[number - 1].mediaType
return TypedBytes(pageContent, pageMediaType) return TypedBytes(pageContent, pageMediaType)
} }

View file

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

View file

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

View file

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

View file

@ -12,16 +12,18 @@ import java.sql.Connection
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
class SqliteUdfDataSource : SQLiteDataSource() { class SqliteUdfDataSource : SQLiteDataSource() {
companion object { companion object {
const val udfStripAccents = "UDF_STRIP_ACCENTS" const val UDF_STRIP_ACCENTS = "UDF_STRIP_ACCENTS"
const val collationUnicode3 = "COLLATION_UNICODE_3" const val COLLATION_UNICODE_3 = "COLLATION_UNICODE_3"
} }
override fun getConnection(): Connection = override fun getConnection(): Connection =
super.getConnection().also { addAllUdf(it as SQLiteConnection) } 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) } super.getConnection(username, password).also { addAllUdf(it) }
private fun addAllUdf(connection: SQLiteConnection) { private fun addAllUdf(connection: SQLiteConnection) {
@ -47,10 +49,10 @@ class SqliteUdfDataSource : SQLiteDataSource() {
} }
private fun createUdfStripAccents(connection: SQLiteConnection) { private fun createUdfStripAccents(connection: SQLiteConnection) {
log.debug { "Adding custom $udfStripAccents function" } log.debug { "Adding custom $UDF_STRIP_ACCENTS function" }
Function.create( Function.create(
connection, connection,
udfStripAccents, UDF_STRIP_ACCENTS,
object : Function() { object : Function() {
override fun xFunc() = override fun xFunc() =
when (val text = value_text(0)) { when (val text = value_text(0)) {
@ -62,17 +64,21 @@ class SqliteUdfDataSource : SQLiteDataSource() {
} }
private fun createUnicode3Collation(connection: SQLiteConnection) { private fun createUnicode3Collation(connection: SQLiteConnection) {
log.debug { "Adding custom $collationUnicode3 collation" } log.debug { "Adding custom $COLLATION_UNICODE_3 collation" }
Collation.create( Collation.create(
connection, connection,
collationUnicode3, COLLATION_UNICODE_3,
object : Collation() { object : Collation() {
val collator = Collator.getInstance().apply { val collator =
Collator.getInstance().apply {
strength = Collator.TERTIARY strength = Collator.TERTIARY
decomposition = Collator.CANONICAL_DECOMPOSITION 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 @Component
class Hasher { class Hasher {
fun computeHash(path: Path): String { fun computeHash(path: Path): String {
logger.debug { "Hashing: $path" } logger.debug { "Hashing: $path" }
@ -38,7 +37,8 @@ class Hasher {
} }
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
private fun ByteArray.toHexString(): String = asUByteArray().joinToString("") { private fun ByteArray.toHexString(): String =
asUByteArray().joinToString("") {
it.toString(16).padStart(2, '0') it.toString(16).padStart(2, '0')
} }
} }

View file

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

View file

@ -10,7 +10,6 @@ private val logger = KotlinLogging.logger {}
@Service @Service
class ImageAnalyzer { class ImageAnalyzer {
/** /**
* Returns the Dimension of the image contained in the stream. * Returns the Dimension of the image contained in the stream.
* The stream will not be closed, nor marked or reset. * 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 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 @Service
class ImageConverter( class ImageConverter(
private val imageAnalyzer: ImageAnalyzer, private val imageAnalyzer: ImageAnalyzer,
private val contentDetector: ContentDetector, private val contentDetector: ContentDetector,
) { ) {
val supportedReadFormats by lazy { ImageIO.getReaderFormatNames().toList() } val supportedReadFormats by lazy { ImageIO.getReaderFormatNames().toList() }
val supportedReadMediaTypes by lazy { ImageIO.getReaderMIMETypes().toList() } val supportedReadMediaTypes by lazy { ImageIO.getReaderMIMETypes().toList() }
val supportedWriteFormats by lazy { ImageIO.getWriterFormatNames().toList() } val supportedWriteFormats by lazy { ImageIO.getWriterFormatNames().toList() }
@ -38,7 +37,8 @@ class ImageConverter(
} }
private fun chooseWebpReader() { private fun chooseWebpReader() {
val providers = IIORegistry.getDefaultInstance().getServiceProviders( val providers =
IIORegistry.getDefaultInstance().getServiceProviders(
ImageReaderSpi::class.java, ImageReaderSpi::class.java,
{ it is ImageReaderSpi && it.mimeTypes.contains("image/webp") }, { it is ImageReaderSpi && it.mimeTypes.contains("image/webp") },
false, false,
@ -46,7 +46,7 @@ class ImageConverter(
if (providers.size > 1) { if (providers.size > 1) {
logger.debug { "WebP reader providers: ${providers.map { it.javaClass.canonicalName }}" } 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 { (providers - nightMonkeys).forEach {
logger.debug { "Deregister provider: ${it.javaClass.canonicalName}" } logger.debug { "Deregister provider: ${it.javaClass.canonicalName}" }
IIORegistry.getDefaultInstance().deregisterServiceProvider(it) IIORegistry.getDefaultInstance().deregisterServiceProvider(it)
@ -57,16 +57,25 @@ class ImageConverter(
private val supportsTransparency = listOf("png") private val supportsTransparency = listOf("png")
fun canConvertMediaType(from: String, to: String) = fun canConvertMediaType(
from: String,
to: String,
) =
supportedReadMediaTypes.contains(from) && supportedWriteMediaTypes.contains(to) supportedReadMediaTypes.contains(from) && supportedWriteMediaTypes.contains(to)
fun convertImage(imageBytes: ByteArray, format: String): ByteArray = fun convertImage(
imageBytes: ByteArray,
format: String,
): ByteArray =
ByteArrayOutputStream().use { baos -> ByteArrayOutputStream().use { baos ->
val image = ImageIO.read(imageBytes.inputStream()) val image = ImageIO.read(imageBytes.inputStream())
val result = if (!supportsTransparency.contains(format) && containsAlphaChannel(image)) { val result =
if (containsTransparency(image)) logger.info { "Image contains alpha channel but is not opaque, visual artifacts may appear" } if (!supportsTransparency.contains(format) && containsAlphaChannel(image)) {
else logger.info { "Image contains alpha channel but is opaque, conversion should not generate any visual artifacts" } 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 { BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_RGB).also {
it.createGraphics().drawImage(image, 0, 0, Color.WHITE, null) it.createGraphics().drawImage(image, 0, 0, Color.WHITE, null)
} }
@ -79,7 +88,11 @@ class ImageConverter(
baos.toByteArray() 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 val builder = resizeImageBuilder(imageBytes, format, size) ?: return imageBytes
return ByteArrayOutputStream().use { 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()) val builder = resizeImageBuilder(imageBytes, format, size) ?: return ImageIO.read(imageBytes.inputStream())
return builder.asBufferedImage() return builder.asBufferedImage()
} }
private fun resizeImageBuilder(imageBytes: ByteArray, format: ImageType, size: Int): Thumbnails.Builder<out InputStream>? { private fun resizeImageBuilder(
val longestEdge = imageAnalyzer.getDimension(imageBytes.inputStream())?.let { imageBytes: ByteArray,
format: ImageType,
size: Int,
): Thumbnails.Builder<out InputStream>? {
val longestEdge =
imageAnalyzer.getDimension(imageBytes.inputStream())?.let {
val mediaType = contentDetector.detectMediaType(imageBytes.inputStream()) val mediaType = contentDetector.detectMediaType(imageBytes.inputStream())
val longestEdge = max(it.height, it.width) val longestEdge = max(it.height, it.width)
// don't resize if source and target format is the same, and source is smaller than desired // don't resize if source and target format is the same, and source is smaller than desired

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