mirror of
https://github.com/gotson/komga.git
synced 2025-12-06 08:32:25 +01:00
style: ktlint format
This commit is contained in:
parent
e01b32446b
commit
d9bba60578
312 changed files with 8845 additions and 6494 deletions
|
|
@ -126,7 +126,8 @@ jreleaser {
|
|||
enabled = true
|
||||
title = "# [{{projectVersion}}]({{repoUrl}}/compare/{{previousTagName}}...{{tagName}}) ({{#f_now}}YYYY-MM-dd{{/f_now}})"
|
||||
target = rootDir.resolve("CHANGELOG.md")
|
||||
content = """
|
||||
content =
|
||||
"""
|
||||
{{changelogTitle}}
|
||||
{{changelogChanges}}
|
||||
""".trimIndent()
|
||||
|
|
@ -173,7 +174,8 @@ jreleaser {
|
|||
templateDirectory = rootDir.resolve("komga/docker")
|
||||
repository.active = Active.NEVER
|
||||
buildArgs = listOf("--cache-from", "gotson/komga:latest")
|
||||
imageNames = listOf(
|
||||
imageNames =
|
||||
listOf(
|
||||
"komga:latest",
|
||||
"komga:{{projectVersion}}",
|
||||
"komga:{{projectVersionMajor}}.x",
|
||||
|
|
@ -185,7 +187,8 @@ jreleaser {
|
|||
buildx {
|
||||
enabled = true
|
||||
createBuilder = false
|
||||
platforms = listOf(
|
||||
platforms =
|
||||
listOf(
|
||||
"linux/amd64",
|
||||
"linux/arm/v7",
|
||||
"linux/arm64/v8",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ fun main(args: Array<String>) {
|
|||
run(*args)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val (message, stackTrace) = when (e.cause) {
|
||||
val (message, stackTrace) =
|
||||
when (e.cause) {
|
||||
is PortInUseException -> RB.getString("error_message.port_in_use", (e.cause as PortInUseException).port) to null
|
||||
else -> RB.getString("error_message.unexpected") to e.stackTraceToString()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,13 @@ class RB private constructor() {
|
|||
companion object {
|
||||
private val BUNDLE: ResourceBundle = ResourceBundle.getBundle("org.gotson.komga.messages")
|
||||
|
||||
fun getString(key: String, vararg args: Any?): String =
|
||||
if (args.isEmpty()) BUNDLE.getString(key)
|
||||
else MessageFormatter.arrayFormat(BUNDLE.getString(key), args).message
|
||||
fun getString(
|
||||
key: String,
|
||||
vararg args: Any?,
|
||||
): String =
|
||||
if (args.isEmpty())
|
||||
BUNDLE.getString(key)
|
||||
else
|
||||
MessageFormatter.arrayFormat(BUNDLE.getString(key), args).message
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,17 +30,22 @@ import org.gotson.komga.RB
|
|||
import org.springframework.core.io.ClassPathResource
|
||||
|
||||
@Preview
|
||||
fun showErrorDialog(text: String, stackTrace: String? = null) {
|
||||
fun showErrorDialog(
|
||||
text: String,
|
||||
stackTrace: String? = null,
|
||||
) {
|
||||
application {
|
||||
Window(
|
||||
title = RB.getString("dialog_error.title"),
|
||||
onCloseRequest = ::exitApplication,
|
||||
visible = true,
|
||||
resizable = false,
|
||||
state = WindowState(
|
||||
state =
|
||||
WindowState(
|
||||
placement = WindowPlacement.Floating,
|
||||
position = WindowPosition(alignment = Alignment.Center),
|
||||
size = DpSize(
|
||||
size =
|
||||
DpSize(
|
||||
if (stackTrace != null) 800.dp else Dp.Unspecified,
|
||||
Dp.Unspecified,
|
||||
),
|
||||
|
|
@ -57,7 +62,8 @@ fun showErrorDialog(text: String, stackTrace: String? = null) {
|
|||
Image(
|
||||
painter = loadSvgPainter(ClassPathResource("icons/komga-color.svg").inputStream, LocalDensity.current),
|
||||
contentDescription = "Komga logo",
|
||||
modifier = Modifier
|
||||
modifier =
|
||||
Modifier
|
||||
.size(96.dp)
|
||||
.align(Alignment.Top),
|
||||
)
|
||||
|
|
@ -77,7 +83,8 @@ fun showErrorDialog(text: String, stackTrace: String? = null) {
|
|||
|
||||
Row(
|
||||
horizontalArrangement = if (stackTrace != null) Arrangement.SpaceBetween else Arrangement.End,
|
||||
modifier = if (stackTrace != null)
|
||||
modifier =
|
||||
if (stackTrace != null)
|
||||
Modifier.align(Alignment.End).fillMaxWidth()
|
||||
else
|
||||
Modifier.align(Alignment.End),
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ class TrayIconRunner(
|
|||
@Value("\${server.port}") serverPort: Int,
|
||||
env: Environment,
|
||||
) : ApplicationRunner {
|
||||
|
||||
val komgaUrl = "http://localhost:$serverPort$servletContextPath"
|
||||
val komgaConfigDir = File(komgaConfigDir)
|
||||
val logFile = File(logFileName)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ kotlin {
|
|||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
val benchmarkSourceSet = sourceSets.create("benchmark") {
|
||||
val benchmarkSourceSet =
|
||||
sourceSets.create("benchmark") {
|
||||
java {
|
||||
compileClasspath += sourceSets.main.get().output
|
||||
runtimeClasspath += sourceSets.main.get().runtimeClasspath
|
||||
|
|
@ -147,7 +148,8 @@ tasks {
|
|||
withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
freeCompilerArgs = listOf(
|
||||
freeCompilerArgs =
|
||||
listOf(
|
||||
"-Xjsr305=strict",
|
||||
"-opt-in=kotlin.time.ExperimentalTime",
|
||||
)
|
||||
|
|
@ -243,16 +245,20 @@ springBoot {
|
|||
}
|
||||
}
|
||||
|
||||
val sqliteUrls = mapOf(
|
||||
val sqliteUrls =
|
||||
mapOf(
|
||||
"main" to "jdbc:sqlite:${project.layout.buildDirectory.get()}/generated/flyway/main/database.sqlite",
|
||||
"tasks" to "jdbc:sqlite:${project.layout.buildDirectory.get()}/generated/flyway/tasks/tasks.sqlite",
|
||||
)
|
||||
val sqliteMigrationDirs = mapOf(
|
||||
"main" to listOf(
|
||||
val sqliteMigrationDirs =
|
||||
mapOf(
|
||||
"main" to
|
||||
listOf(
|
||||
"$projectDir/src/flyway/resources/db/migration/sqlite",
|
||||
"$projectDir/src/flyway/kotlin/db/migration/sqlite",
|
||||
),
|
||||
"tasks" to listOf(
|
||||
"tasks" to
|
||||
listOf(
|
||||
"$projectDir/src/flyway/resources/tasks/migration/sqlite",
|
||||
// "$projectDir/src/flyway/kotlin/tasks/migration/sqlite",
|
||||
),
|
||||
|
|
@ -262,7 +268,8 @@ task("flywayMigrateMain", FlywayMigrateTask::class) {
|
|||
val id = "main"
|
||||
url = sqliteUrls[id]
|
||||
locations = arrayOf("classpath:db/migration/sqlite")
|
||||
placeholders = mapOf(
|
||||
placeholders =
|
||||
mapOf(
|
||||
"library-file-hashing" to "true",
|
||||
"library-scan-startup" to "false",
|
||||
"delete-empty-collections" to "true",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ abstract class AbstractBenchmark {
|
|||
fun executeJmhRunner() {
|
||||
if (this.javaClass.simpleName.contains("jmhType")) return
|
||||
Files.createDirectories(Paths.get(benchmarkProperties.resultFolder))
|
||||
val extension = when (benchmarkProperties.resultFormat) {
|
||||
val extension =
|
||||
when (benchmarkProperties.resultFormat) {
|
||||
ResultFormatType.TEXT -> "txt"
|
||||
ResultFormatType.CSV -> "csv"
|
||||
ResultFormatType.SCSV -> "scsv"
|
||||
|
|
@ -33,7 +34,8 @@ abstract class AbstractBenchmark {
|
|||
ResultFormatType.LATEX -> "tex"
|
||||
}
|
||||
val resultFile = Paths.get(benchmarkProperties.resultFolder, "${this.javaClass.name}.$extension").absolutePathString()
|
||||
val opt: Options = OptionsBuilder()
|
||||
val opt: Options =
|
||||
OptionsBuilder()
|
||||
.include("\\." + this.javaClass.simpleName + "\\.") // set the class name regex for benchmarks to search for to the current class
|
||||
.apply { if (benchmarkProperties.warmupIterations > 0) warmupIterations(benchmarkProperties.warmupIterations) }
|
||||
.measurementIterations(benchmarkProperties.measurementIterations)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import org.springframework.beans.factory.annotation.Autowired
|
|||
internal const val DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
abstract class AbstractRestBenchmark : AbstractBenchmark() {
|
||||
|
||||
companion object {
|
||||
lateinit var userRepository: KomgaUserRepository
|
||||
lateinit var seriesController: SeriesController
|
||||
|
|
@ -38,7 +37,8 @@ abstract class AbstractRestBenchmark : AbstractBenchmark() {
|
|||
|
||||
@Setup(Level.Trial)
|
||||
fun prepareData() {
|
||||
principal = KomgaPrincipal(
|
||||
principal =
|
||||
KomgaPrincipal(
|
||||
userRepository.findByEmailIgnoreCaseOrNull("admin@example.org")
|
||||
?: throw IllegalStateException("no user found"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import java.util.concurrent.TimeUnit
|
|||
|
||||
@OutputTimeUnit(TimeUnit.MILLISECONDS)
|
||||
class BrowseBenchmark : AbstractRestBenchmark() {
|
||||
|
||||
companion object {
|
||||
private lateinit var biggestSeriesId: String
|
||||
}
|
||||
|
|
@ -24,7 +23,8 @@ class BrowseBenchmark : AbstractRestBenchmark() {
|
|||
super.prepareData()
|
||||
|
||||
// find series with most books
|
||||
biggestSeriesId = seriesController.getAllSeries(principal, page = PageRequest.of(0, 1, Sort.by(Sort.Order.desc("booksCount"))))
|
||||
biggestSeriesId =
|
||||
seriesController.getAllSeries(principal, page = PageRequest.of(0, 1, Sort.by(Sort.Order.desc("booksCount"))))
|
||||
.content.first()
|
||||
.id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import java.util.concurrent.TimeUnit
|
|||
|
||||
@OutputTimeUnit(TimeUnit.MILLISECONDS)
|
||||
class DashboardBenchmark : AbstractRestBenchmark() {
|
||||
|
||||
companion object {
|
||||
lateinit var bookLatestReleaseDate: LocalDate
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import java.util.concurrent.TimeUnit
|
|||
|
||||
@OutputTimeUnit(TimeUnit.MILLISECONDS)
|
||||
class UnsortedBenchmark : AbstractRestBenchmark() {
|
||||
|
||||
companion object {
|
||||
private lateinit var biggestSeriesId: String
|
||||
}
|
||||
|
|
@ -23,7 +22,8 @@ class UnsortedBenchmark : AbstractRestBenchmark() {
|
|||
super.prepareData()
|
||||
|
||||
// find series with most books
|
||||
biggestSeriesId = seriesController.getAllSeries(principal, page = PageRequest.of(0, 1, Sort.by(Sort.Order.desc("booksCount"))))
|
||||
biggestSeriesId =
|
||||
seriesController.getAllSeries(principal, page = PageRequest.of(0, 1, Sort.by(Sort.Order.desc("booksCount"))))
|
||||
.content.first()
|
||||
.id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ class LibraryScanScheduler(
|
|||
// map the libraryId to the scan scheduled task
|
||||
private val registry = ConcurrentHashMap<String, ScheduledTask>()
|
||||
|
||||
private val registrar = ScheduledTaskRegistrar().apply {
|
||||
private val registrar =
|
||||
ScheduledTaskRegistrar().apply {
|
||||
setTaskScheduler(taskScheduler)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,122 +17,146 @@ sealed class Task(val priority: Int = DEFAULT_PRIORITY, val groupId: String? = n
|
|||
|
||||
class ScanLibrary(val libraryId: String, val scanDeep: Boolean, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "SCAN_LIBRARY_${libraryId}_DEEP_$scanDeep"
|
||||
|
||||
override fun toString(): String = "ScanLibrary(libraryId='$libraryId', scanDeep='$scanDeep', priority='$priority')"
|
||||
}
|
||||
|
||||
class FindBooksToConvert(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "FIND_BOOKS_TO_CONVERT_$libraryId"
|
||||
|
||||
override fun toString(): String = "FindBooksToConvert(libraryId='$libraryId', priority='$priority')"
|
||||
}
|
||||
|
||||
class FindBooksWithMissingPageHash(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "FIND_BOOKS_WITH_MISSING_PAGE_HASH_$libraryId"
|
||||
|
||||
override fun toString(): String = "FindBooksWithMissingPageHash(libraryId='$libraryId', priority='$priority')"
|
||||
}
|
||||
|
||||
class FindDuplicatePagesToDelete(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "FIND_DUPLICATE_PAGES_TO_DELETE_$libraryId"
|
||||
|
||||
override fun toString(): String = "FindDuplicatePagesToDelete(libraryId='$libraryId', priority='$priority')"
|
||||
}
|
||||
|
||||
class EmptyTrash(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "EMPTY_TRASH_$libraryId"
|
||||
|
||||
override fun toString(): String = "EmptyTrash(libraryId='$libraryId', priority='$priority')"
|
||||
}
|
||||
|
||||
class AnalyzeBook(val bookId: String, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) {
|
||||
override val uniqueId = "ANALYZE_BOOK_$bookId"
|
||||
|
||||
override fun toString(): String = "AnalyzeBook(bookId='$bookId', priority='$priority')"
|
||||
}
|
||||
|
||||
class GenerateBookThumbnail(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "GENERATE_BOOK_THUMBNAIL_$bookId"
|
||||
|
||||
override fun toString(): String = "GenerateBookThumbnail(bookId='$bookId', priority='$priority')"
|
||||
}
|
||||
|
||||
class RefreshBookMetadata(val bookId: String, val capabilities: Set<BookMetadataPatchCapability>, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) {
|
||||
override val uniqueId = "REFRESH_BOOK_METADATA_$bookId"
|
||||
|
||||
override fun toString(): String = "RefreshBookMetadata(bookId='$bookId', capabilities=$capabilities, priority='$priority')"
|
||||
}
|
||||
|
||||
class HashBook(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "HASH_BOOK_$bookId"
|
||||
|
||||
override fun toString(): String = "HashBook(bookId='$bookId', priority='$priority')"
|
||||
}
|
||||
|
||||
class HashBookPages(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "HASH_BOOK_PAGES_$bookId"
|
||||
|
||||
override fun toString(): String = "HashBookPages(bookId='$bookId', priority='$priority')"
|
||||
}
|
||||
|
||||
class RefreshSeriesMetadata(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority, seriesId) {
|
||||
override val uniqueId = "REFRESH_SERIES_METADATA_$seriesId"
|
||||
|
||||
override fun toString(): String = "RefreshSeriesMetadata(seriesId='$seriesId', priority='$priority')"
|
||||
}
|
||||
|
||||
class AggregateSeriesMetadata(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority, seriesId) {
|
||||
override val uniqueId = "AGGREGATE_SERIES_METADATA_$seriesId"
|
||||
|
||||
override fun toString(): String = "AggregateSeriesMetadata(seriesId='$seriesId', priority='$priority')"
|
||||
}
|
||||
|
||||
class RefreshBookLocalArtwork(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId: String = "REFRESH_BOOK_LOCAL_ARTWORK_$bookId"
|
||||
|
||||
override fun toString(): String = "RefreshBookLocalArtwork(bookId='$bookId', priority='$priority')"
|
||||
}
|
||||
|
||||
class RefreshSeriesLocalArtwork(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId: String = "REFRESH_SERIES_LOCAL_ARTWORK_$seriesId"
|
||||
|
||||
override fun toString(): String = "RefreshSeriesLocalArtwork(seriesId=$seriesId, priority='$priority')"
|
||||
}
|
||||
|
||||
class ImportBook(val sourceFile: String, val seriesId: String, val copyMode: CopyMode, val destinationName: String?, val upgradeBookId: String?, priority: Int = DEFAULT_PRIORITY) : Task(priority, seriesId) {
|
||||
override val uniqueId: String = "IMPORT_BOOK_${seriesId}_$sourceFile"
|
||||
|
||||
override fun toString(): String =
|
||||
"ImportBook(sourceFile='$sourceFile', seriesId='$seriesId', copyMode=$copyMode, destinationName=$destinationName, upgradeBookId=$upgradeBookId, priority='$priority')"
|
||||
}
|
||||
|
||||
class ConvertBook(val bookId: String, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) {
|
||||
override val uniqueId: String = "CONVERT_BOOK_$bookId"
|
||||
|
||||
override fun toString(): String = "ConvertBook(bookId='$bookId', priority='$priority')"
|
||||
}
|
||||
|
||||
class RepairExtension(val bookId: String, priority: Int = DEFAULT_PRIORITY, groupId: String) : Task(priority, groupId) {
|
||||
override val uniqueId: String = "REPAIR_EXTENSION_$bookId"
|
||||
|
||||
override fun toString(): String = "RepairExtension(bookId='$bookId', priority='$priority')"
|
||||
}
|
||||
|
||||
class RemoveHashedPages(val bookId: String, val pages: Collection<BookPageNumbered>, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId: String = "REMOVE_HASHED_PAGES_$bookId"
|
||||
|
||||
override fun toString(): String = "RemoveHashedPages(bookId='$bookId', priority='$priority')"
|
||||
}
|
||||
|
||||
class RebuildIndex(val entities: Set<LuceneEntity>?, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "REBUILD_INDEX"
|
||||
|
||||
override fun toString(): String = "RebuildIndex(priority='$priority',entities='${entities?.map { it.type }}')"
|
||||
}
|
||||
|
||||
class UpgradeIndex(priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "UPGRADE_INDEX"
|
||||
|
||||
override fun toString(): String = "UpgradeIndex(priority='$priority')"
|
||||
}
|
||||
|
||||
class DeleteBook(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "DELETE_BOOK_$bookId"
|
||||
|
||||
override fun toString(): String = "DeleteBook(bookId='$bookId', priority='$priority')"
|
||||
}
|
||||
|
||||
class DeleteSeries(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "DELETE_SERIES_$seriesId"
|
||||
|
||||
override fun toString(): String = "DeleteSeries(seriesId='$seriesId', priority='$priority')"
|
||||
}
|
||||
|
||||
class FixThumbnailsWithoutMetadata(priority: Int = DEFAULT_PRIORITY) : Task(priority, "FixThumbnailsWithoutMetadata") {
|
||||
override val uniqueId = "FIX_THUMBNAILS_WITHOUT_METADATA_${LocalDateTime.now()}"
|
||||
|
||||
override fun toString(): String = "FixThumbnailsWithoutMetadata(priority='$priority')"
|
||||
}
|
||||
|
||||
class FindBookThumbnailsToRegenerate(val forBiggerResultOnly: Boolean, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override val uniqueId = "FIND_BOOK_THUMBNAILS_TO_REGENERATE"
|
||||
|
||||
override fun toString(): String = "FindBookThumbnailsToRegenerate(forBiggerResultOnly='$forBiggerResultOnly', priority='$priority')"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,18 @@ class TaskEmitter(
|
|||
private val tasksRepository: TasksRepository,
|
||||
private val eventPublisher: ApplicationEventPublisher,
|
||||
) {
|
||||
fun scanLibrary(libraryId: String, scanDeep: Boolean = false, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun scanLibrary(
|
||||
libraryId: String,
|
||||
scanDeep: Boolean = false,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.ScanLibrary(libraryId, scanDeep, priority))
|
||||
}
|
||||
|
||||
fun emptyTrash(libraryId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun emptyTrash(
|
||||
libraryId: String,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.EmptyTrash(libraryId, priority))
|
||||
}
|
||||
|
||||
|
|
@ -55,27 +62,42 @@ class TaskEmitter(
|
|||
.let { submitTasks(it) }
|
||||
}
|
||||
|
||||
fun findBooksWithMissingPageHash(library: Library, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun findBooksWithMissingPageHash(
|
||||
library: Library,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.FindBooksWithMissingPageHash(library.id, priority))
|
||||
}
|
||||
|
||||
fun hashBookPages(bookIdToSeriesId: Collection<String>, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun hashBookPages(
|
||||
bookIdToSeriesId: Collection<String>,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
bookIdToSeriesId
|
||||
.map { Task.HashBookPages(it, priority) }
|
||||
.let { submitTasks(it) }
|
||||
}
|
||||
|
||||
fun findBooksToConvert(library: Library, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun findBooksToConvert(
|
||||
library: Library,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.FindBooksToConvert(library.id, priority))
|
||||
}
|
||||
|
||||
fun convertBookToCbz(books: Collection<Book>, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun convertBookToCbz(
|
||||
books: Collection<Book>,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
books
|
||||
.map { Task.ConvertBook(it.id, priority, it.seriesId) }
|
||||
.let { submitTasks(it) }
|
||||
}
|
||||
|
||||
fun repairExtensions(library: Library, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun repairExtensions(
|
||||
library: Library,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
if (library.repairExtensions)
|
||||
bookConverter
|
||||
.getMismatchedExtensionBooks(library)
|
||||
|
|
@ -83,35 +105,57 @@ class TaskEmitter(
|
|||
.let { submitTasks(it) }
|
||||
}
|
||||
|
||||
fun findDuplicatePagesToDelete(library: Library, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun findDuplicatePagesToDelete(
|
||||
library: Library,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.FindDuplicatePagesToDelete(library.id, priority))
|
||||
}
|
||||
|
||||
fun removeDuplicatePages(bookId: String, pages: Collection<BookPageNumbered>, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun removeDuplicatePages(
|
||||
bookId: String,
|
||||
pages: Collection<BookPageNumbered>,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.RemoveHashedPages(bookId, pages, priority))
|
||||
}
|
||||
|
||||
fun removeDuplicatePages(bookIdToPages: Map<String, Collection<BookPageNumbered>>, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun removeDuplicatePages(
|
||||
bookIdToPages: Map<String, Collection<BookPageNumbered>>,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
bookIdToPages
|
||||
.map { Task.RemoveHashedPages(it.key, it.value, priority) }
|
||||
.let { submitTasks(it) }
|
||||
}
|
||||
|
||||
fun analyzeBook(book: Book, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun analyzeBook(
|
||||
book: Book,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.AnalyzeBook(book.id, priority, book.seriesId))
|
||||
}
|
||||
|
||||
fun analyzeBook(books: Collection<Book>, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun analyzeBook(
|
||||
books: Collection<Book>,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
books
|
||||
.map { Task.AnalyzeBook(it.id, priority, it.seriesId) }
|
||||
.let { submitTasks(it) }
|
||||
}
|
||||
|
||||
fun generateBookThumbnail(bookId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun generateBookThumbnail(
|
||||
bookId: String,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.GenerateBookThumbnail(bookId, priority))
|
||||
}
|
||||
|
||||
fun generateBookThumbnail(bookIds: Collection<String>, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun generateBookThumbnail(
|
||||
bookIds: Collection<String>,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
bookIds
|
||||
.map { Task.GenerateBookThumbnail(it, priority) }
|
||||
.let { submitTasks(it) }
|
||||
|
|
@ -135,39 +179,67 @@ class TaskEmitter(
|
|||
.let { submitTasks(it) }
|
||||
}
|
||||
|
||||
fun refreshSeriesMetadata(seriesId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun refreshSeriesMetadata(
|
||||
seriesId: String,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.RefreshSeriesMetadata(seriesId, priority))
|
||||
}
|
||||
|
||||
fun aggregateSeriesMetadata(seriesId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun aggregateSeriesMetadata(
|
||||
seriesId: String,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.AggregateSeriesMetadata(seriesId, priority))
|
||||
}
|
||||
|
||||
fun refreshBookLocalArtwork(book: Book, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun refreshBookLocalArtwork(
|
||||
book: Book,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.RefreshBookLocalArtwork(book.id, priority))
|
||||
}
|
||||
|
||||
fun refreshBookLocalArtwork(books: Collection<Book>, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun refreshBookLocalArtwork(
|
||||
books: Collection<Book>,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
books
|
||||
.map { Task.RefreshBookLocalArtwork(it.id, priority) }
|
||||
.let { submitTasks(it) }
|
||||
}
|
||||
|
||||
fun refreshSeriesLocalArtwork(seriesId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun refreshSeriesLocalArtwork(
|
||||
seriesId: String,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.RefreshSeriesLocalArtwork(seriesId, priority))
|
||||
}
|
||||
|
||||
fun refreshSeriesLocalArtwork(seriesIds: Collection<String>, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun refreshSeriesLocalArtwork(
|
||||
seriesIds: Collection<String>,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
seriesIds
|
||||
.map { Task.RefreshSeriesLocalArtwork(it, priority) }
|
||||
.let { submitTasks(it) }
|
||||
}
|
||||
|
||||
fun importBook(sourceFile: String, seriesId: String, copyMode: CopyMode, destinationName: String?, upgradeBookId: String?, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun importBook(
|
||||
sourceFile: String,
|
||||
seriesId: String,
|
||||
copyMode: CopyMode,
|
||||
destinationName: String?,
|
||||
upgradeBookId: String?,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.ImportBook(sourceFile, seriesId, copyMode, destinationName, upgradeBookId, priority))
|
||||
}
|
||||
|
||||
fun rebuildIndex(priority: Int = DEFAULT_PRIORITY, entities: Set<LuceneEntity>? = null) {
|
||||
fun rebuildIndex(
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
entities: Set<LuceneEntity>? = null,
|
||||
) {
|
||||
submitTask(Task.RebuildIndex(entities, priority))
|
||||
}
|
||||
|
||||
|
|
@ -175,11 +247,17 @@ class TaskEmitter(
|
|||
submitTask(Task.UpgradeIndex(priority))
|
||||
}
|
||||
|
||||
fun deleteBook(bookId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun deleteBook(
|
||||
bookId: String,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.DeleteBook(bookId, priority))
|
||||
}
|
||||
|
||||
fun deleteSeries(seriesId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun deleteSeries(
|
||||
seriesId: String,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.DeleteSeries(seriesId, priority))
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +265,10 @@ class TaskEmitter(
|
|||
submitTask(Task.FixThumbnailsWithoutMetadata(priority))
|
||||
}
|
||||
|
||||
fun findBookThumbnailsToRegenerate(forBiggerResultOnly: Boolean, priority: Int = DEFAULT_PRIORITY) {
|
||||
fun findBookThumbnailsToRegenerate(
|
||||
forBiggerResultOnly: Boolean,
|
||||
priority: Int = DEFAULT_PRIORITY,
|
||||
) {
|
||||
submitTask(Task.FindBookThumbnailsToRegenerate(forBiggerResultOnly, priority))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
package org.gotson.komga.application.tasks
|
||||
|
||||
class TaskAddedEvent
|
||||
|
||||
class TaskPoolSizeChangedEvent
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ class TaskHandler(
|
|||
private val thumbnailLifecycle: ThumbnailLifecycle,
|
||||
private val meterRegistry: MeterRegistry,
|
||||
) {
|
||||
|
||||
fun handleTask(task: Task) {
|
||||
logger.info { "Executing task: $task" }
|
||||
try {
|
||||
|
|
@ -162,8 +161,10 @@ class TaskHandler(
|
|||
|
||||
is Task.DeleteBook -> {
|
||||
bookRepository.findByIdOrNull(task.bookId)?.let { book ->
|
||||
if (book.oneshot) seriesLifecycle.deleteSeriesFiles(seriesRepository.findByIdOrNull(book.seriesId)!!)
|
||||
else bookLifecycle.deleteBookFiles(book)
|
||||
if (book.oneshot)
|
||||
seriesLifecycle.deleteSeriesFiles(seriesRepository.findByIdOrNull(book.seriesId)!!)
|
||||
else
|
||||
bookLifecycle.deleteBookFiles(book)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,10 +43,13 @@ class TaskProcessor(
|
|||
fun processAvailableTask() {
|
||||
if (processTasks) {
|
||||
logger.debug { "Active count: ${executor.activeCount}, Core Pool Size: ${executor.corePoolSize}, Pool Size: ${executor.poolSize}" }
|
||||
if (executor.corePoolSize == 1) executor.execute { takeAndProcess() }
|
||||
// fan out while threads are available
|
||||
else while (tasksRepository.hasAvailable() && executor.activeCount < executor.corePoolSize)
|
||||
if (executor.corePoolSize == 1) {
|
||||
executor.execute { takeAndProcess() }
|
||||
} else {
|
||||
// fan out while threads are available
|
||||
while (tasksRepository.hasAvailable() && executor.activeCount < executor.corePoolSize)
|
||||
executor.execute { takeAndProcess() }
|
||||
}
|
||||
} else {
|
||||
logger.debug { "Not processing tasks" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,26 @@ package org.gotson.komga.application.tasks
|
|||
|
||||
interface TasksRepository {
|
||||
fun hasAvailable(): Boolean
|
||||
|
||||
fun takeFirst(owner: String = Thread.currentThread().name): Task?
|
||||
|
||||
fun findAll(): List<Task>
|
||||
|
||||
fun findAllGroupedByOwner(): Map<String?, List<Task>>
|
||||
|
||||
fun count(): Int
|
||||
|
||||
fun countBySimpleType(): Map<String, Int>
|
||||
|
||||
fun save(task: Task)
|
||||
|
||||
fun save(tasks: Collection<Task>)
|
||||
|
||||
fun delete(taskId: String)
|
||||
|
||||
fun deleteAll()
|
||||
|
||||
fun deleteAllWithoutOwner(): Int
|
||||
|
||||
fun disown(): Int
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@ data class AgeRestriction(
|
|||
)
|
||||
|
||||
enum class AllowExclude {
|
||||
ALLOW_ONLY, EXCLUDE,
|
||||
ALLOW_ONLY,
|
||||
EXCLUDE,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,18 +13,14 @@ data class Book(
|
|||
val fileSize: Long = 0,
|
||||
val fileHash: String = "",
|
||||
val number: Int = 0,
|
||||
|
||||
val id: String = TsidCreator.getTsid256().toString(),
|
||||
val seriesId: String = "",
|
||||
val libraryId: String = "",
|
||||
|
||||
val deletedDate: LocalDateTime? = null,
|
||||
val oneshot: Boolean = false,
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable {
|
||||
|
||||
@delegate:Transient
|
||||
val path: Path by lazy { this.url.toURI().toPath() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ class BookMetadata(
|
|||
tags: Set<String> = emptySet(),
|
||||
val isbn: String = "",
|
||||
val links: List<WebLink> = emptyList(),
|
||||
|
||||
val titleLock: Boolean = false,
|
||||
val summaryLock: Boolean = false,
|
||||
val numberLock: Boolean = false,
|
||||
|
|
@ -24,13 +23,10 @@ class BookMetadata(
|
|||
val tagsLock: Boolean = false,
|
||||
val isbnLock: Boolean = false,
|
||||
val linksLock: Boolean = false,
|
||||
|
||||
val bookId: String = "",
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable {
|
||||
|
||||
val title = title.trim()
|
||||
val summary = summary.trim()
|
||||
val number = number.trim()
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ data class BookMetadataAggregation(
|
|||
val releaseDate: LocalDate? = null,
|
||||
val summary: String = "",
|
||||
val summaryNumber: String = "",
|
||||
|
||||
val seriesId: String = "",
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ data class BookMetadataPatch(
|
|||
val isbn: String? = null,
|
||||
val links: List<WebLink>? = null,
|
||||
val tags: Set<String>? = null,
|
||||
|
||||
val readLists: List<ReadListEntry> = emptyList(),
|
||||
) {
|
||||
data class ReadListEntry(
|
||||
|
|
|
|||
|
|
@ -3,46 +3,65 @@ package org.gotson.komga.domain.model
|
|||
import java.net.URL
|
||||
|
||||
sealed class DomainEvent {
|
||||
|
||||
data class LibraryAdded(val library: Library) : DomainEvent()
|
||||
|
||||
data class LibraryUpdated(val library: Library) : DomainEvent()
|
||||
|
||||
data class LibraryDeleted(val library: Library) : DomainEvent()
|
||||
|
||||
data class LibraryScanned(val library: Library) : DomainEvent()
|
||||
|
||||
data class SeriesAdded(val series: Series) : DomainEvent()
|
||||
|
||||
data class SeriesUpdated(val series: Series) : DomainEvent()
|
||||
|
||||
data class SeriesDeleted(val series: Series) : DomainEvent()
|
||||
|
||||
data class BookAdded(val book: Book) : DomainEvent()
|
||||
|
||||
data class BookUpdated(val book: Book) : DomainEvent()
|
||||
|
||||
data class BookDeleted(val book: Book) : DomainEvent()
|
||||
|
||||
data class BookImported(val book: Book?, val sourceFile: URL, val success: Boolean, val message: String? = null) : DomainEvent()
|
||||
|
||||
data class CollectionAdded(val collection: SeriesCollection) : DomainEvent()
|
||||
|
||||
data class CollectionUpdated(val collection: SeriesCollection) : DomainEvent()
|
||||
|
||||
data class CollectionDeleted(val collection: SeriesCollection) : DomainEvent()
|
||||
|
||||
data class ReadListAdded(val readList: ReadList) : DomainEvent()
|
||||
|
||||
data class ReadListUpdated(val readList: ReadList) : DomainEvent()
|
||||
|
||||
data class ReadListDeleted(val readList: ReadList) : DomainEvent()
|
||||
|
||||
data class ReadProgressChanged(val progress: ReadProgress) : DomainEvent()
|
||||
|
||||
data class ReadProgressDeleted(val progress: ReadProgress) : DomainEvent()
|
||||
|
||||
data class ReadProgressSeriesChanged(val seriesId: String, val userId: String) : DomainEvent()
|
||||
|
||||
data class ReadProgressSeriesDeleted(val seriesId: String, val userId: String) : DomainEvent()
|
||||
|
||||
data class ThumbnailBookAdded(val thumbnail: ThumbnailBook) : DomainEvent()
|
||||
|
||||
data class ThumbnailBookDeleted(val thumbnail: ThumbnailBook) : DomainEvent()
|
||||
|
||||
data class ThumbnailSeriesAdded(val thumbnail: ThumbnailSeries) : DomainEvent()
|
||||
|
||||
data class ThumbnailSeriesDeleted(val thumbnail: ThumbnailSeries) : DomainEvent()
|
||||
|
||||
data class ThumbnailSeriesCollectionAdded(val thumbnail: ThumbnailSeriesCollection) : DomainEvent()
|
||||
|
||||
data class ThumbnailSeriesCollectionDeleted(val thumbnail: ThumbnailSeriesCollection) : DomainEvent()
|
||||
|
||||
data class ThumbnailReadListAdded(val thumbnail: ThumbnailReadList) : DomainEvent()
|
||||
|
||||
data class ThumbnailReadListDeleted(val thumbnail: ThumbnailReadList) : DomainEvent()
|
||||
|
||||
data class UserUpdated(val user: KomgaUser, val expireSession: Boolean) : DomainEvent()
|
||||
|
||||
data class UserDeleted(val user: KomgaUser) : DomainEvent()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,13 +15,23 @@ open class CodedException : Exception {
|
|||
fun Exception.withCode(code: String) = CodedException(this, code)
|
||||
|
||||
class MediaNotReadyException : Exception()
|
||||
|
||||
class NoThumbnailFoundException : Exception()
|
||||
|
||||
class MediaUnsupportedException(message: String, code: String = "") : CodedException(message, code)
|
||||
|
||||
class ImageConversionException(message: String, code: String = "") : CodedException(message, code)
|
||||
|
||||
class DirectoryNotFoundException(message: String, code: String = "") : CodedException(message, code)
|
||||
|
||||
class DuplicateNameException(message: String, code: String = "") : CodedException(message, code)
|
||||
|
||||
class PathContainedInPath(message: String, code: String = "") : CodedException(message, code)
|
||||
|
||||
class UserEmailAlreadyExistsException(message: String, code: String = "") : CodedException(message, code)
|
||||
|
||||
class BookConversionException(message: String) : Exception(message)
|
||||
|
||||
class ComicRackListException(message: String, code: String = "") : CodedException(message, code)
|
||||
|
||||
class EntryNotFoundException(message: String) : Exception(message)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ sealed class HistoricalEvent(
|
|||
type = "BookFileDeleted",
|
||||
bookId = book.id,
|
||||
seriesId = book.seriesId,
|
||||
properties = mapOf(
|
||||
properties =
|
||||
mapOf(
|
||||
"reason" to reason,
|
||||
"name" to book.path.toString(),
|
||||
),
|
||||
|
|
@ -25,7 +26,8 @@ sealed class HistoricalEvent(
|
|||
class SeriesFolderDeleted(seriesId: String, seriesPath: Path, reason: String) : HistoricalEvent(
|
||||
type = "SeriesFolderDeleted",
|
||||
seriesId = seriesId,
|
||||
properties = mapOf(
|
||||
properties =
|
||||
mapOf(
|
||||
"reason" to reason,
|
||||
"name" to seriesPath.toString(),
|
||||
),
|
||||
|
|
@ -37,7 +39,8 @@ sealed class HistoricalEvent(
|
|||
type = "BookConverted",
|
||||
bookId = book.id,
|
||||
seriesId = book.seriesId,
|
||||
properties = mapOf(
|
||||
properties =
|
||||
mapOf(
|
||||
"name" to book.path.toString(),
|
||||
"former file" to previous.path.toString(),
|
||||
),
|
||||
|
|
@ -47,7 +50,8 @@ sealed class HistoricalEvent(
|
|||
type = "BookImported",
|
||||
bookId = book.id,
|
||||
seriesId = series.id,
|
||||
properties = mapOf(
|
||||
properties =
|
||||
mapOf(
|
||||
"name" to book.path.toString(),
|
||||
"source" to source.toString(),
|
||||
"upgrade" to if (upgrade) "Yes" else "No",
|
||||
|
|
@ -58,7 +62,8 @@ sealed class HistoricalEvent(
|
|||
type = "DuplicatePageDeleted",
|
||||
bookId = book.id,
|
||||
seriesId = book.seriesId,
|
||||
properties = mapOf(
|
||||
properties =
|
||||
mapOf(
|
||||
"name" to book.path.toString(),
|
||||
"page number" to page.pageNumber.toString(),
|
||||
"page file name" to page.fileName,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ data class KomgaUser(
|
|||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable {
|
||||
|
||||
@delegate:Transient
|
||||
val roles: Set<String> by lazy {
|
||||
buildSet {
|
||||
|
|
@ -65,20 +64,26 @@ data class KomgaUser(
|
|||
return sharedAllLibraries || sharedLibrariesIds.any { it == library.id }
|
||||
}
|
||||
|
||||
fun isContentAllowed(ageRating: Int? = null, sharingLabels: Set<String> = emptySet()): Boolean {
|
||||
fun isContentAllowed(
|
||||
ageRating: Int? = null,
|
||||
sharingLabels: Set<String> = emptySet(),
|
||||
): Boolean {
|
||||
val labels = sharingLabels.lowerNotBlank().toSet()
|
||||
|
||||
val ageAllowed =
|
||||
if (restrictions.ageRestriction?.restriction == AllowExclude.ALLOW_ONLY)
|
||||
ageRating != null && ageRating <= restrictions.ageRestriction.age
|
||||
else null
|
||||
else
|
||||
null
|
||||
|
||||
val labelAllowed =
|
||||
if (restrictions.labelsAllow.isNotEmpty())
|
||||
restrictions.labelsAllow.intersect(labels).isNotEmpty()
|
||||
else null
|
||||
else
|
||||
null
|
||||
|
||||
val allowed = when {
|
||||
val allowed =
|
||||
when {
|
||||
ageAllowed == null -> labelAllowed != false
|
||||
labelAllowed == null -> ageAllowed != false
|
||||
else -> ageAllowed != false || labelAllowed != false
|
||||
|
|
@ -88,12 +93,14 @@ data class KomgaUser(
|
|||
val ageDenied =
|
||||
if (restrictions.ageRestriction?.restriction == AllowExclude.EXCLUDE)
|
||||
ageRating != null && ageRating >= restrictions.ageRestriction.age
|
||||
else false
|
||||
else
|
||||
false
|
||||
|
||||
val labelDenied =
|
||||
if (restrictions.labelsExclude.isNotEmpty())
|
||||
restrictions.labelsExclude.intersect(labels).isNotEmpty()
|
||||
else false
|
||||
else
|
||||
false
|
||||
|
||||
return !ageDenied && !labelDenied
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,15 +34,11 @@ data class Library(
|
|||
val hashPages: Boolean = false,
|
||||
val analyzeDimensions: Boolean = true,
|
||||
val oneshotsDirectory: String? = null,
|
||||
|
||||
val unavailableDate: LocalDateTime? = null,
|
||||
|
||||
val id: String = TsidCreator.getTsid256().toString(),
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable {
|
||||
|
||||
enum class SeriesCover {
|
||||
FIRST,
|
||||
FIRST_UNREAD_OR_FIRST,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
enum class MarkSelectedPreference {
|
||||
NO, YES, IF_NONE_OR_GENERATED
|
||||
NO,
|
||||
YES,
|
||||
IF_NONE_OR_GENERATED,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,12 +15,15 @@ data class Media(
|
|||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable {
|
||||
|
||||
@delegate:Transient
|
||||
val profile: MediaProfile? by lazy { MediaType.fromMediaType(mediaType)?.profile }
|
||||
|
||||
enum class Status {
|
||||
UNKNOWN, ERROR, READY, UNSUPPORTED, OUTDATED
|
||||
UNKNOWN,
|
||||
ERROR,
|
||||
READY,
|
||||
UNSUPPORTED,
|
||||
OUTDATED,
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
|
|
|
|||
|
|
@ -8,17 +8,19 @@ interface MediaExtension
|
|||
class ProxyExtension private constructor(
|
||||
val extensionClassName: String,
|
||||
) : MediaExtension {
|
||||
|
||||
companion object {
|
||||
fun of(extensionClass: String?): ProxyExtension? =
|
||||
extensionClass?.let {
|
||||
val kClass = Class.forName(extensionClass).kotlin
|
||||
if (kClass.qualifiedName != MediaExtension::class.qualifiedName && kClass.isSubclassOf(MediaExtension::class)) ProxyExtension(extensionClass)
|
||||
else null
|
||||
if (kClass.qualifiedName != MediaExtension::class.qualifiedName && kClass.isSubclassOf(MediaExtension::class))
|
||||
ProxyExtension(extensionClass)
|
||||
else
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> proxyForType(): Boolean = T::class.qualifiedName == extensionClassName
|
||||
|
||||
fun proxyForType(clazz: KClass<out Any>): Boolean = clazz.qualifiedName == extensionClassName
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ data class MediaFile(
|
|||
val fileSize: Long? = null,
|
||||
) {
|
||||
enum class SubType {
|
||||
EPUB_PAGE, EPUB_ASSET
|
||||
EPUB_PAGE,
|
||||
EPUB_ASSET,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
enum class MetadataPatchTarget {
|
||||
BOOK, SERIES, READLIST, COLLECTION
|
||||
BOOK,
|
||||
SERIES,
|
||||
READLIST,
|
||||
COLLECTION,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ class PageHashKnown(
|
|||
val action: Action,
|
||||
val deleteCount: Int = 0,
|
||||
val matchCount: Int = 0,
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable, PageHash(hash, size) {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ data class R2Locator(
|
|||
*/
|
||||
val text: Text? = null,
|
||||
) {
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
data class Location(
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ data class R2Progression(
|
|||
val locator: R2Locator,
|
||||
)
|
||||
|
||||
fun ReadProgress.toR2Progression() = R2Progression(
|
||||
fun ReadProgress.toR2Progression() =
|
||||
R2Progression(
|
||||
modified = readDate.toZonedDateTime(),
|
||||
device = R2Device(deviceId, deviceName),
|
||||
locator = locator ?: R2Locator("", ""),
|
||||
|
|
|
|||
|
|
@ -11,14 +11,10 @@ data class ReadList(
|
|||
* Indicates whether the read list is ordered manually
|
||||
*/
|
||||
val ordered: Boolean = true,
|
||||
|
||||
val bookIds: SortedMap<Int, String> = sortedMapOf(),
|
||||
|
||||
val id: String = TsidCreator.getTsid256().toString(),
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
|
||||
/**
|
||||
* Indicates that the bookIds have been filtered and is not exhaustive.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ data class ReadProgress(
|
|||
val deviceId: String = "",
|
||||
val deviceName: String = "",
|
||||
val locator: R2Locator? = null,
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
enum class ReadStatus {
|
||||
UNREAD, READ, IN_PROGRESS
|
||||
UNREAD,
|
||||
READ,
|
||||
IN_PROGRESS,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,18 +10,14 @@ data class Series(
|
|||
val name: String,
|
||||
val url: URL,
|
||||
val fileLastModified: LocalDateTime,
|
||||
|
||||
val id: String = TsidCreator.getTsid256().toString(),
|
||||
val libraryId: String = "",
|
||||
val bookCount: Int = 0,
|
||||
|
||||
val deletedDate: LocalDateTime? = null,
|
||||
val oneshot: Boolean = false,
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable {
|
||||
|
||||
@delegate:Transient
|
||||
val path: Path by lazy { this.url.toURI().toPath() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,10 @@ data class SeriesCollection(
|
|||
* Indicates whether the collection is ordered manually
|
||||
*/
|
||||
val ordered: Boolean = false,
|
||||
|
||||
val seriesIds: List<String> = emptyList(),
|
||||
|
||||
val id: String = TsidCreator.getTsid256().toString(),
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
|
||||
/**
|
||||
* Indicates that the seriesIds have been filtered and is not exhaustive.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ class SeriesMetadata(
|
|||
sharingLabels: Set<String> = emptySet(),
|
||||
val links: List<WebLink> = emptyList(),
|
||||
val alternateTitles: List<AlternateTitle> = emptyList(),
|
||||
|
||||
val statusLock: Boolean = false,
|
||||
val titleLock: Boolean = false,
|
||||
val titleSortLock: Boolean = false,
|
||||
|
|
@ -33,9 +32,7 @@ class SeriesMetadata(
|
|||
val sharingLabelsLock: Boolean = false,
|
||||
val linksLock: Boolean = false,
|
||||
val alternateTitlesLock: Boolean = false,
|
||||
|
||||
val seriesId: String = "",
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable {
|
||||
|
|
@ -116,7 +113,10 @@ class SeriesMetadata(
|
|||
)
|
||||
|
||||
enum class Status {
|
||||
ENDED, ONGOING, ABANDONED, HIATUS
|
||||
ENDED,
|
||||
ONGOING,
|
||||
ABANDONED,
|
||||
HIATUS,
|
||||
}
|
||||
|
||||
enum class ReadingDirection {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,5 @@ data class SeriesMetadataPatch(
|
|||
val language: String?,
|
||||
val genres: Set<String>?,
|
||||
val totalBookCount: Int?,
|
||||
|
||||
val collections: Set<String>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ open class SeriesSearch(
|
|||
val oneshot: Boolean? = null,
|
||||
) {
|
||||
enum class SearchField {
|
||||
NAME, TITLE, TITLE_SORT
|
||||
NAME,
|
||||
TITLE,
|
||||
TITLE_SORT,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ data class Sidecar(
|
|||
val type: Type,
|
||||
val source: Source,
|
||||
) {
|
||||
|
||||
enum class Type {
|
||||
ARTWORK, METADATA
|
||||
ARTWORK,
|
||||
METADATA,
|
||||
}
|
||||
|
||||
enum class Source {
|
||||
SERIES, BOOK
|
||||
SERIES,
|
||||
BOOK,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,15 +14,15 @@ data class ThumbnailBook(
|
|||
val mediaType: String,
|
||||
val fileSize: Long,
|
||||
val dimension: Dimension,
|
||||
|
||||
val id: String = TsidCreator.getTsid256().toString(),
|
||||
val bookId: String = "",
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable {
|
||||
enum class Type {
|
||||
GENERATED, SIDECAR, USER_UPLOADED
|
||||
GENERATED,
|
||||
SIDECAR,
|
||||
USER_UPLOADED,
|
||||
}
|
||||
|
||||
fun exists(): Boolean {
|
||||
|
|
@ -39,7 +39,8 @@ data class ThumbnailBook(
|
|||
if (thumbnail != null) {
|
||||
if (other.thumbnail == null) return false
|
||||
if (!thumbnail.contentEquals(other.thumbnail)) return false
|
||||
} else if (other.thumbnail != null) return false
|
||||
} else if (other.thumbnail != null)
|
||||
return false
|
||||
if (url != other.url) return false
|
||||
if (selected != other.selected) return false
|
||||
if (type != other.type) return false
|
||||
|
|
|
|||
|
|
@ -10,10 +10,8 @@ data class ThumbnailReadList(
|
|||
val mediaType: String,
|
||||
val fileSize: Long,
|
||||
val dimension: Dimension,
|
||||
|
||||
val id: String = TsidCreator.getTsid256().toString(),
|
||||
val readListId: String = "",
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable {
|
||||
|
|
|
|||
|
|
@ -14,15 +14,14 @@ data class ThumbnailSeries(
|
|||
val mediaType: String,
|
||||
val fileSize: Long,
|
||||
val dimension: Dimension,
|
||||
|
||||
val id: String = TsidCreator.getTsid256().toString(),
|
||||
val seriesId: String = "",
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable {
|
||||
enum class Type {
|
||||
SIDECAR, USER_UPLOADED
|
||||
SIDECAR,
|
||||
USER_UPLOADED,
|
||||
}
|
||||
|
||||
fun exists(): Boolean {
|
||||
|
|
@ -39,7 +38,8 @@ data class ThumbnailSeries(
|
|||
if (thumbnail != null) {
|
||||
if (other.thumbnail == null) return false
|
||||
if (!thumbnail.contentEquals(other.thumbnail)) return false
|
||||
} else if (other.thumbnail != null) return false
|
||||
} else if (other.thumbnail != null)
|
||||
return false
|
||||
if (url != other.url) return false
|
||||
if (selected != other.selected) return false
|
||||
if (type != other.type) return false
|
||||
|
|
|
|||
|
|
@ -10,10 +10,8 @@ data class ThumbnailSeriesCollection(
|
|||
val mediaType: String,
|
||||
val fileSize: Long,
|
||||
val dimension: Dimension,
|
||||
|
||||
val id: String = TsidCreator.getTsid256().toString(),
|
||||
val collectionId: String = "",
|
||||
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
) : Auditable {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,17 @@ import java.time.LocalDateTime
|
|||
|
||||
interface AuthenticationActivityRepository {
|
||||
fun findAll(pageable: Pageable): Page<AuthenticationActivity>
|
||||
fun findAllByUser(user: KomgaUser, pageable: Pageable): Page<AuthenticationActivity>
|
||||
|
||||
fun findAllByUser(
|
||||
user: KomgaUser,
|
||||
pageable: Pageable,
|
||||
): Page<AuthenticationActivity>
|
||||
|
||||
fun findMostRecentByUser(user: KomgaUser): AuthenticationActivity?
|
||||
|
||||
fun insert(activity: AuthenticationActivity)
|
||||
|
||||
fun deleteByUser(user: KomgaUser)
|
||||
|
||||
fun deleteOlderThan(dateTime: LocalDateTime)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ import org.gotson.komga.domain.model.BookMetadataAggregation
|
|||
|
||||
interface BookMetadataAggregationRepository {
|
||||
fun findById(seriesId: String): BookMetadataAggregation
|
||||
|
||||
fun findByIdOrNull(seriesId: String): BookMetadataAggregation?
|
||||
|
||||
fun insert(metadata: BookMetadataAggregation)
|
||||
|
||||
fun update(metadata: BookMetadataAggregation)
|
||||
|
||||
fun delete(seriesId: String)
|
||||
|
||||
fun delete(seriesIds: Collection<String>)
|
||||
|
||||
fun count(): Long
|
||||
|
|
|
|||
|
|
@ -4,17 +4,21 @@ import org.gotson.komga.domain.model.BookMetadata
|
|||
|
||||
interface BookMetadataRepository {
|
||||
fun findById(bookId: String): BookMetadata
|
||||
|
||||
fun findByIdOrNull(bookId: String): BookMetadata?
|
||||
|
||||
fun findAllByIds(bookIds: Collection<String>): Collection<BookMetadata>
|
||||
|
||||
fun insert(metadata: BookMetadata)
|
||||
|
||||
fun insert(metadatas: Collection<BookMetadata>)
|
||||
|
||||
fun update(metadata: BookMetadata)
|
||||
|
||||
fun update(metadatas: Collection<BookMetadata>)
|
||||
|
||||
fun delete(bookId: String)
|
||||
|
||||
fun delete(bookIds: Collection<String>)
|
||||
|
||||
fun count(): Long
|
||||
|
|
|
|||
|
|
@ -10,40 +10,85 @@ import java.net.URL
|
|||
|
||||
interface BookRepository {
|
||||
fun findByIdOrNull(bookId: String): Book?
|
||||
fun findNotDeletedByLibraryIdAndUrlOrNull(libraryId: String, url: URL): Book?
|
||||
|
||||
fun findNotDeletedByLibraryIdAndUrlOrNull(
|
||||
libraryId: String,
|
||||
url: URL,
|
||||
): Book?
|
||||
|
||||
fun findAll(): Collection<Book>
|
||||
|
||||
fun findAllBySeriesId(seriesId: String): Collection<Book>
|
||||
|
||||
fun findAllBySeriesIds(seriesIds: Collection<String>): Collection<Book>
|
||||
fun findAllNotDeletedByLibraryIdAndUrlNotIn(libraryId: String, urls: Collection<URL>): Collection<Book>
|
||||
|
||||
fun findAllNotDeletedByLibraryIdAndUrlNotIn(
|
||||
libraryId: String,
|
||||
urls: Collection<URL>,
|
||||
): Collection<Book>
|
||||
|
||||
fun findAll(bookSearch: BookSearch): Collection<Book>
|
||||
fun findAll(bookSearch: BookSearch, pageable: Pageable): Page<Book>
|
||||
|
||||
fun findAll(
|
||||
bookSearch: BookSearch,
|
||||
pageable: Pageable,
|
||||
): Page<Book>
|
||||
|
||||
fun findAllDeletedByFileSize(fileSize: Long): Collection<Book>
|
||||
|
||||
fun findAllByLibraryIdAndWithEmptyHash(libraryId: String): Collection<Book>
|
||||
fun findAllByLibraryIdAndMediaTypes(libraryId: String, mediaTypes: Collection<String>): Collection<Book>
|
||||
fun findAllByLibraryIdAndMismatchedExtension(libraryId: String, mediaType: String, extension: String): Collection<Book>
|
||||
|
||||
fun findAllByLibraryIdAndMediaTypes(
|
||||
libraryId: String,
|
||||
mediaTypes: Collection<String>,
|
||||
): Collection<Book>
|
||||
|
||||
fun findAllByLibraryIdAndMismatchedExtension(
|
||||
libraryId: String,
|
||||
mediaType: String,
|
||||
extension: String,
|
||||
): Collection<Book>
|
||||
|
||||
fun getLibraryIdOrNull(bookId: String): String?
|
||||
|
||||
fun getSeriesIdOrNull(bookId: String): String?
|
||||
|
||||
fun findFirstIdInSeriesOrNull(seriesId: String): String?
|
||||
|
||||
fun findLastIdInSeriesOrNull(seriesId: String): String?
|
||||
fun findFirstUnreadIdInSeriesOrNull(seriesId: String, userId: String): String?
|
||||
|
||||
fun findFirstUnreadIdInSeriesOrNull(
|
||||
seriesId: String,
|
||||
userId: String,
|
||||
): String?
|
||||
|
||||
fun findAllIdsBySeriesId(seriesId: String): Collection<String>
|
||||
|
||||
fun findAllIdsBySeriesIds(seriesIds: Collection<String>): Collection<String>
|
||||
|
||||
fun findAllIdsByLibraryId(libraryId: String): Collection<String>
|
||||
fun findAllIds(bookSearch: BookSearch, sort: Sort): Collection<String>
|
||||
|
||||
fun findAllIds(
|
||||
bookSearch: BookSearch,
|
||||
sort: Sort,
|
||||
): Collection<String>
|
||||
|
||||
fun insert(book: Book)
|
||||
|
||||
fun insert(books: Collection<Book>)
|
||||
|
||||
fun update(book: Book)
|
||||
|
||||
fun update(books: Collection<Book>)
|
||||
|
||||
fun delete(bookId: String)
|
||||
|
||||
fun delete(bookIds: Collection<String>)
|
||||
|
||||
fun deleteAll()
|
||||
|
||||
fun count(): Long
|
||||
|
||||
fun countGroupedByLibraryId(): Map<String, Int>
|
||||
|
||||
fun getFilesizeGroupedByLibraryId(): Map<String, BigDecimal>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ interface KomgaUserRepository {
|
|||
fun count(): Long
|
||||
|
||||
fun findByIdOrNull(id: String): KomgaUser?
|
||||
|
||||
fun findByEmailIgnoreCaseOrNull(email: String): KomgaUser?
|
||||
|
||||
fun findAll(): Collection<KomgaUser>
|
||||
|
|
@ -13,11 +14,17 @@ interface KomgaUserRepository {
|
|||
fun existsByEmailIgnoreCase(email: String): Boolean
|
||||
|
||||
fun insert(user: KomgaUser)
|
||||
|
||||
fun update(user: KomgaUser)
|
||||
|
||||
fun delete(userId: String)
|
||||
|
||||
fun deleteAll()
|
||||
|
||||
fun findAnnouncementIdsReadByUserId(userId: String): Set<String>
|
||||
fun saveAnnouncementIdsRead(user: KomgaUser, announcementIds: Set<String>)
|
||||
|
||||
fun saveAnnouncementIdsRead(
|
||||
user: KomgaUser,
|
||||
announcementIds: Set<String>,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,19 @@ import org.gotson.komga.domain.model.Library
|
|||
|
||||
interface LibraryRepository {
|
||||
fun findById(libraryId: String): Library
|
||||
|
||||
fun findByIdOrNull(libraryId: String): Library?
|
||||
|
||||
fun findAll(): Collection<Library>
|
||||
|
||||
fun findAllByIds(libraryIds: Collection<String>): Collection<Library>
|
||||
|
||||
fun delete(libraryId: String)
|
||||
|
||||
fun deleteAll()
|
||||
|
||||
fun insert(library: Library)
|
||||
|
||||
fun update(library: Library)
|
||||
|
||||
fun count(): Long
|
||||
|
|
|
|||
|
|
@ -5,20 +5,29 @@ import org.gotson.komga.domain.model.MediaExtension
|
|||
|
||||
interface MediaRepository {
|
||||
fun findById(bookId: String): Media
|
||||
|
||||
fun findByIdOrNull(bookId: String): Media?
|
||||
|
||||
fun findAllBookIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(libraryId: String, mediaTypes: Collection<String>, pageHashing: Int): Collection<String>
|
||||
fun findAllBookIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(
|
||||
libraryId: String,
|
||||
mediaTypes: Collection<String>,
|
||||
pageHashing: Int,
|
||||
): Collection<String>
|
||||
|
||||
fun getPagesSize(bookId: String): Int
|
||||
|
||||
fun getPagesSizes(bookIds: Collection<String>): Collection<Pair<String, Int>>
|
||||
|
||||
fun findExtensionByIdOrNull(bookId: String): MediaExtension?
|
||||
|
||||
fun insert(media: Media)
|
||||
|
||||
fun insert(medias: Collection<Media>)
|
||||
|
||||
fun update(media: Media)
|
||||
|
||||
fun delete(bookId: String)
|
||||
|
||||
fun deleteByBookIds(bookIds: Collection<String>)
|
||||
|
||||
fun count(): Long
|
||||
|
|
|
|||
|
|
@ -9,14 +9,30 @@ import org.springframework.data.domain.Pageable
|
|||
|
||||
interface PageHashRepository {
|
||||
fun findKnown(pageHash: String): PageHashKnown?
|
||||
fun findAllKnown(actions: List<PageHashKnown.Action>?, pageable: Pageable): Page<PageHashKnown>
|
||||
|
||||
fun findAllKnown(
|
||||
actions: List<PageHashKnown.Action>?,
|
||||
pageable: Pageable,
|
||||
): Page<PageHashKnown>
|
||||
|
||||
fun findAllUnknown(pageable: Pageable): Page<PageHashUnknown>
|
||||
|
||||
fun findMatchesByHash(pageHash: String, pageable: Pageable): Page<PageHashMatch>
|
||||
fun findMatchesByKnownHashAction(actions: List<PageHashKnown.Action>?, libraryId: String?): Map<String, Collection<BookPageNumbered>>
|
||||
fun findMatchesByHash(
|
||||
pageHash: String,
|
||||
pageable: Pageable,
|
||||
): Page<PageHashMatch>
|
||||
|
||||
fun findMatchesByKnownHashAction(
|
||||
actions: List<PageHashKnown.Action>?,
|
||||
libraryId: String?,
|
||||
): Map<String, Collection<BookPageNumbered>>
|
||||
|
||||
fun getKnownThumbnail(pageHash: String): ByteArray?
|
||||
|
||||
fun insert(pageHash: PageHashKnown, thumbnail: ByteArray?)
|
||||
fun insert(
|
||||
pageHash: PageHashKnown,
|
||||
thumbnail: ByteArray?,
|
||||
)
|
||||
|
||||
fun update(pageHash: PageHashKnown)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,33 +10,51 @@ interface ReadListRepository {
|
|||
* Find one ReadList by [readListId],
|
||||
* optionally with only bookIds filtered by the provided [filterOnLibraryIds] it not null.
|
||||
*/
|
||||
fun findByIdOrNull(readListId: String, filterOnLibraryIds: Collection<String>? = null, restrictions: ContentRestrictions = ContentRestrictions()): ReadList?
|
||||
fun findByIdOrNull(
|
||||
readListId: String,
|
||||
filterOnLibraryIds: Collection<String>? = null,
|
||||
restrictions: ContentRestrictions = ContentRestrictions(),
|
||||
): ReadList?
|
||||
|
||||
/**
|
||||
* Find all ReadList
|
||||
* optionally with at least one Book belonging to the provided [belongsToLibraryIds] if not null,
|
||||
* optionally with only bookIds filtered by the provided [filterOnLibraryIds] if not null.
|
||||
*/
|
||||
fun findAll(belongsToLibraryIds: Collection<String>? = null, filterOnLibraryIds: Collection<String>? = null, search: String? = null, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page<ReadList>
|
||||
fun findAll(
|
||||
belongsToLibraryIds: Collection<String>? = null,
|
||||
filterOnLibraryIds: Collection<String>? = null,
|
||||
search: String? = null,
|
||||
pageable: Pageable,
|
||||
restrictions: ContentRestrictions = ContentRestrictions(),
|
||||
): Page<ReadList>
|
||||
|
||||
/**
|
||||
* Find all ReadList that contains the provided [containsBookId],
|
||||
* optionally with only bookIds filtered by the provided [filterOnLibraryIds] if not null.
|
||||
*/
|
||||
fun findAllContainingBookId(containsBookId: String, filterOnLibraryIds: Collection<String>?, restrictions: ContentRestrictions = ContentRestrictions()): Collection<ReadList>
|
||||
fun findAllContainingBookId(
|
||||
containsBookId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
restrictions: ContentRestrictions = ContentRestrictions(),
|
||||
): Collection<ReadList>
|
||||
|
||||
fun findAllEmpty(): Collection<ReadList>
|
||||
|
||||
fun findByNameOrNull(name: String): ReadList?
|
||||
|
||||
fun insert(readList: ReadList)
|
||||
|
||||
fun update(readList: ReadList)
|
||||
|
||||
fun removeBookFromAll(bookId: String)
|
||||
|
||||
fun removeBooksFromAll(bookIds: Collection<String>)
|
||||
|
||||
fun delete(readListId: String)
|
||||
|
||||
fun delete(readListIds: Collection<String>)
|
||||
|
||||
fun deleteAll()
|
||||
|
||||
fun existsByName(name: String): Boolean
|
||||
|
|
|
|||
|
|
@ -3,21 +3,43 @@ package org.gotson.komga.domain.persistence
|
|||
import org.gotson.komga.domain.model.ReadProgress
|
||||
|
||||
interface ReadProgressRepository {
|
||||
fun findByBookIdAndUserIdOrNull(bookId: String, userId: String): ReadProgress?
|
||||
fun findByBookIdAndUserIdOrNull(
|
||||
bookId: String,
|
||||
userId: String,
|
||||
): ReadProgress?
|
||||
|
||||
fun findAll(): Collection<ReadProgress>
|
||||
|
||||
fun findAllByUserId(userId: String): Collection<ReadProgress>
|
||||
|
||||
fun findAllByBookId(bookId: String): Collection<ReadProgress>
|
||||
fun findAllByBookIdsAndUserId(bookIds: Collection<String>, userId: String): Collection<ReadProgress>
|
||||
|
||||
fun findAllByBookIdsAndUserId(
|
||||
bookIds: Collection<String>,
|
||||
userId: String,
|
||||
): Collection<ReadProgress>
|
||||
|
||||
fun save(readProgress: ReadProgress)
|
||||
|
||||
fun save(readProgresses: Collection<ReadProgress>)
|
||||
|
||||
fun delete(bookId: String, userId: String)
|
||||
fun delete(
|
||||
bookId: String,
|
||||
userId: String,
|
||||
)
|
||||
|
||||
fun deleteByUserId(userId: String)
|
||||
|
||||
fun deleteByBookId(bookId: String)
|
||||
|
||||
fun deleteByBookIds(bookIds: Collection<String>)
|
||||
fun deleteByBookIdsAndUserId(bookIds: Collection<String>, userId: String)
|
||||
|
||||
fun deleteByBookIdsAndUserId(
|
||||
bookIds: Collection<String>,
|
||||
userId: String,
|
||||
)
|
||||
|
||||
fun deleteBySeriesIds(seriesIds: Collection<String>)
|
||||
|
||||
fun deleteAll()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,51 +6,185 @@ import org.springframework.data.domain.Pageable
|
|||
import java.time.LocalDate
|
||||
|
||||
interface ReferentialRepository {
|
||||
fun findAllAuthorsByName(search: String, filterOnLibraryIds: Collection<String>?): List<Author>
|
||||
fun findAllAuthorsByNameAndLibrary(search: String, libraryId: String, filterOnLibraryIds: Collection<String>?): List<Author>
|
||||
fun findAllAuthorsByNameAndCollection(search: String, collectionId: String, filterOnLibraryIds: Collection<String>?): List<Author>
|
||||
fun findAllAuthorsByNameAndSeries(search: String, seriesId: String, filterOnLibraryIds: Collection<String>?): List<Author>
|
||||
fun findAllAuthorsNamesByName(search: String, filterOnLibraryIds: Collection<String>?): List<String>
|
||||
fun findAllAuthorsByName(
|
||||
search: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): List<Author>
|
||||
|
||||
fun findAllAuthorsByNameAndLibrary(
|
||||
search: String,
|
||||
libraryId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): List<Author>
|
||||
|
||||
fun findAllAuthorsByNameAndCollection(
|
||||
search: String,
|
||||
collectionId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): List<Author>
|
||||
|
||||
fun findAllAuthorsByNameAndSeries(
|
||||
search: String,
|
||||
seriesId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): List<Author>
|
||||
|
||||
fun findAllAuthorsNamesByName(
|
||||
search: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): List<String>
|
||||
|
||||
fun findAllAuthorsRoles(filterOnLibraryIds: Collection<String>?): List<String>
|
||||
|
||||
fun findAllAuthorsByName(search: String?, role: String?, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author>
|
||||
fun findAllAuthorsByNameAndLibrary(search: String?, role: String?, libraryId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author>
|
||||
fun findAllAuthorsByNameAndCollection(search: String?, role: String?, collectionId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author>
|
||||
fun findAllAuthorsByNameAndSeries(search: String?, role: String?, seriesId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author>
|
||||
fun findAllAuthorsByNameAndReadList(search: String?, role: String?, readListId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<Author>
|
||||
fun findAllAuthorsByName(
|
||||
search: String?,
|
||||
role: String?,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
pageable: Pageable,
|
||||
): Page<Author>
|
||||
|
||||
fun findAllAuthorsByNameAndLibrary(
|
||||
search: String?,
|
||||
role: String?,
|
||||
libraryId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
pageable: Pageable,
|
||||
): Page<Author>
|
||||
|
||||
fun findAllAuthorsByNameAndCollection(
|
||||
search: String?,
|
||||
role: String?,
|
||||
collectionId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
pageable: Pageable,
|
||||
): Page<Author>
|
||||
|
||||
fun findAllAuthorsByNameAndSeries(
|
||||
search: String?,
|
||||
role: String?,
|
||||
seriesId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
pageable: Pageable,
|
||||
): Page<Author>
|
||||
|
||||
fun findAllAuthorsByNameAndReadList(
|
||||
search: String?,
|
||||
role: String?,
|
||||
readListId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
pageable: Pageable,
|
||||
): Page<Author>
|
||||
|
||||
fun findAllGenres(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllGenresByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllGenresByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
|
||||
fun findAllGenresByLibrary(
|
||||
libraryId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllGenresByCollection(
|
||||
collectionId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllSeriesAndBookTags(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllSeriesAndBookTagsByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllSeriesAndBookTagsByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
|
||||
fun findAllSeriesAndBookTagsByLibrary(
|
||||
libraryId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllSeriesAndBookTagsByCollection(
|
||||
collectionId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllSeriesTags(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllSeriesTagsByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllSeriesTagsByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
|
||||
fun findAllSeriesTagsByLibrary(
|
||||
libraryId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllSeriesTagsByCollection(
|
||||
collectionId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllBookTags(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllBookTagsBySeries(seriesId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllBookTagsByReadList(readListId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
|
||||
fun findAllBookTagsBySeries(
|
||||
seriesId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllBookTagsByReadList(
|
||||
readListId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllLanguages(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllLanguagesByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllLanguagesByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
|
||||
fun findAllLanguagesByLibrary(
|
||||
libraryId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllLanguagesByCollection(
|
||||
collectionId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllPublishers(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllPublishers(filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<String>
|
||||
fun findAllPublishersByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllPublishersByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
|
||||
fun findAllPublishers(
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
pageable: Pageable,
|
||||
): Page<String>
|
||||
|
||||
fun findAllPublishersByLibrary(
|
||||
libraryId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllPublishersByCollection(
|
||||
collectionId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllAgeRatings(filterOnLibraryIds: Collection<String>?): Set<Int?>
|
||||
fun findAllAgeRatingsByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<Int?>
|
||||
fun findAllAgeRatingsByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<Int?>
|
||||
|
||||
fun findAllAgeRatingsByLibrary(
|
||||
libraryId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<Int?>
|
||||
|
||||
fun findAllAgeRatingsByCollection(
|
||||
collectionId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<Int?>
|
||||
|
||||
fun findAllSeriesReleaseDates(filterOnLibraryIds: Collection<String>?): Set<LocalDate>
|
||||
fun findAllSeriesReleaseDatesByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<LocalDate>
|
||||
fun findAllSeriesReleaseDatesByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<LocalDate>
|
||||
|
||||
fun findAllSeriesReleaseDatesByLibrary(
|
||||
libraryId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<LocalDate>
|
||||
|
||||
fun findAllSeriesReleaseDatesByCollection(
|
||||
collectionId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<LocalDate>
|
||||
|
||||
fun findAllSharingLabels(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllSharingLabelsByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllSharingLabelsByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
|
||||
fun findAllSharingLabelsByLibrary(
|
||||
libraryId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
|
||||
fun findAllSharingLabelsByCollection(
|
||||
collectionId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<String>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,33 +10,51 @@ interface SeriesCollectionRepository {
|
|||
* Find one SeriesCollection by [collectionId],
|
||||
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null.
|
||||
*/
|
||||
fun findByIdOrNull(collectionId: String, filterOnLibraryIds: Collection<String>? = null, restrictions: ContentRestrictions = ContentRestrictions()): SeriesCollection?
|
||||
fun findByIdOrNull(
|
||||
collectionId: String,
|
||||
filterOnLibraryIds: Collection<String>? = null,
|
||||
restrictions: ContentRestrictions = ContentRestrictions(),
|
||||
): SeriesCollection?
|
||||
|
||||
/**
|
||||
* Find all SeriesCollection
|
||||
* optionally with at least one Series belonging to the provided [belongsToLibraryIds] if not null,
|
||||
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null.
|
||||
*/
|
||||
fun findAll(belongsToLibraryIds: Collection<String>? = null, filterOnLibraryIds: Collection<String>? = null, search: String? = null, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page<SeriesCollection>
|
||||
fun findAll(
|
||||
belongsToLibraryIds: Collection<String>? = null,
|
||||
filterOnLibraryIds: Collection<String>? = null,
|
||||
search: String? = null,
|
||||
pageable: Pageable,
|
||||
restrictions: ContentRestrictions = ContentRestrictions(),
|
||||
): Page<SeriesCollection>
|
||||
|
||||
/**
|
||||
* Find all SeriesCollection that contains the provided [containsSeriesId],
|
||||
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null.
|
||||
*/
|
||||
fun findAllContainingSeriesId(containsSeriesId: String, filterOnLibraryIds: Collection<String>?, restrictions: ContentRestrictions = ContentRestrictions()): Collection<SeriesCollection>
|
||||
fun findAllContainingSeriesId(
|
||||
containsSeriesId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
restrictions: ContentRestrictions = ContentRestrictions(),
|
||||
): Collection<SeriesCollection>
|
||||
|
||||
fun findAllEmpty(): Collection<SeriesCollection>
|
||||
|
||||
fun findByNameOrNull(name: String): SeriesCollection?
|
||||
|
||||
fun insert(collection: SeriesCollection)
|
||||
|
||||
fun update(collection: SeriesCollection)
|
||||
|
||||
fun removeSeriesFromAll(seriesId: String)
|
||||
|
||||
fun removeSeriesFromAll(seriesIds: Collection<String>)
|
||||
|
||||
fun delete(collectionId: String)
|
||||
|
||||
fun delete(collectionIds: Collection<String>)
|
||||
|
||||
fun deleteAll()
|
||||
|
||||
fun existsByName(name: String): Boolean
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ import org.gotson.komga.domain.model.SeriesMetadata
|
|||
|
||||
interface SeriesMetadataRepository {
|
||||
fun findById(seriesId: String): SeriesMetadata
|
||||
|
||||
fun findByIdOrNull(seriesId: String): SeriesMetadata?
|
||||
|
||||
fun insert(metadata: SeriesMetadata)
|
||||
|
||||
fun update(metadata: SeriesMetadata)
|
||||
|
||||
fun delete(seriesId: String)
|
||||
|
||||
fun delete(seriesIds: Collection<String>)
|
||||
|
||||
fun count(): Long
|
||||
|
|
|
|||
|
|
@ -6,12 +6,23 @@ import java.net.URL
|
|||
|
||||
interface SeriesRepository {
|
||||
fun findByIdOrNull(seriesId: String): Series?
|
||||
fun findNotDeletedByLibraryIdAndUrlOrNull(libraryId: String, url: URL): Series?
|
||||
|
||||
fun findNotDeletedByLibraryIdAndUrlOrNull(
|
||||
libraryId: String,
|
||||
url: URL,
|
||||
): Series?
|
||||
|
||||
fun findAll(): Collection<Series>
|
||||
|
||||
fun findAllByLibraryId(libraryId: String): Collection<Series>
|
||||
fun findAllNotDeletedByLibraryIdAndUrlNotIn(libraryId: String, urls: Collection<URL>): Collection<Series>
|
||||
|
||||
fun findAllNotDeletedByLibraryIdAndUrlNotIn(
|
||||
libraryId: String,
|
||||
urls: Collection<URL>,
|
||||
): Collection<Series>
|
||||
|
||||
fun findAllByTitleContaining(title: String): Collection<Series>
|
||||
|
||||
fun findAll(search: SeriesSearch): Collection<Series>
|
||||
|
||||
fun getLibraryId(seriesId: String): String?
|
||||
|
|
@ -19,12 +30,19 @@ interface SeriesRepository {
|
|||
fun findAllIdsByLibraryId(libraryId: String): Collection<String>
|
||||
|
||||
fun insert(series: Series)
|
||||
fun update(series: Series, updateModifiedTime: Boolean = true)
|
||||
|
||||
fun update(
|
||||
series: Series,
|
||||
updateModifiedTime: Boolean = true,
|
||||
)
|
||||
|
||||
fun delete(seriesId: String)
|
||||
|
||||
fun delete(seriesIds: Collection<String>)
|
||||
|
||||
fun deleteAll()
|
||||
|
||||
fun count(): Long
|
||||
|
||||
fun countGroupedByLibraryId(): Map<String, Int>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,16 @@ import java.net.URL
|
|||
interface SidecarRepository {
|
||||
fun findAll(): Collection<SidecarStored>
|
||||
|
||||
fun save(libraryId: String, sidecar: Sidecar)
|
||||
fun save(
|
||||
libraryId: String,
|
||||
sidecar: Sidecar,
|
||||
)
|
||||
|
||||
fun deleteByLibraryIdAndUrls(
|
||||
libraryId: String,
|
||||
urls: Collection<URL>,
|
||||
)
|
||||
|
||||
fun deleteByLibraryIdAndUrls(libraryId: String, urls: Collection<URL>)
|
||||
fun deleteByLibraryId(libraryId: String)
|
||||
|
||||
fun countGroupedByLibraryId(): Map<String, Int>
|
||||
|
|
|
|||
|
|
@ -6,20 +6,39 @@ import org.springframework.data.domain.Pageable
|
|||
|
||||
interface ThumbnailBookRepository {
|
||||
fun findByIdOrNull(thumbnailId: String): ThumbnailBook?
|
||||
|
||||
fun findSelectedByBookIdOrNull(bookId: String): ThumbnailBook?
|
||||
|
||||
fun findAllByBookId(bookId: String): Collection<ThumbnailBook>
|
||||
fun findAllByBookIdAndType(bookId: String, type: ThumbnailBook.Type): Collection<ThumbnailBook>
|
||||
|
||||
fun findAllByBookIdAndType(
|
||||
bookId: String,
|
||||
type: ThumbnailBook.Type,
|
||||
): Collection<ThumbnailBook>
|
||||
|
||||
fun findAllWithoutMetadata(pageable: Pageable): Page<ThumbnailBook>
|
||||
fun findAllBookIdsByThumbnailTypeAndDimensionSmallerThan(type: ThumbnailBook.Type, size: Int): Collection<String>
|
||||
|
||||
fun findAllBookIdsByThumbnailTypeAndDimensionSmallerThan(
|
||||
type: ThumbnailBook.Type,
|
||||
size: Int,
|
||||
): Collection<String>
|
||||
|
||||
fun insert(thumbnail: ThumbnailBook)
|
||||
|
||||
fun update(thumbnail: ThumbnailBook)
|
||||
|
||||
fun updateMetadata(thumbnails: Collection<ThumbnailBook>)
|
||||
|
||||
fun markSelected(thumbnail: ThumbnailBook)
|
||||
|
||||
fun delete(thumbnailBookId: String)
|
||||
|
||||
fun deleteByBookId(bookId: String)
|
||||
fun deleteByBookIdAndType(bookId: String, type: ThumbnailBook.Type)
|
||||
|
||||
fun deleteByBookIdAndType(
|
||||
bookId: String,
|
||||
type: ThumbnailBook.Type,
|
||||
)
|
||||
|
||||
fun deleteByBookIds(bookIds: Collection<String>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,17 +6,24 @@ import org.springframework.data.domain.Pageable
|
|||
|
||||
interface ThumbnailReadListRepository {
|
||||
fun findByIdOrNull(thumbnailId: String): ThumbnailReadList?
|
||||
|
||||
fun findSelectedByReadListIdOrNull(readListId: String): ThumbnailReadList?
|
||||
|
||||
fun findAllByReadListId(readListId: String): Collection<ThumbnailReadList>
|
||||
|
||||
fun findAllWithoutMetadata(pageable: Pageable): Page<ThumbnailReadList>
|
||||
|
||||
fun insert(thumbnail: ThumbnailReadList)
|
||||
|
||||
fun update(thumbnail: ThumbnailReadList)
|
||||
|
||||
fun updateMetadata(thumbnails: Collection<ThumbnailReadList>)
|
||||
|
||||
fun markSelected(thumbnail: ThumbnailReadList)
|
||||
|
||||
fun delete(thumbnailReadListId: String)
|
||||
|
||||
fun deleteByReadListId(readListId: String)
|
||||
|
||||
fun deleteByReadListIds(readListIds: Collection<String>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,17 +6,24 @@ import org.springframework.data.domain.Pageable
|
|||
|
||||
interface ThumbnailSeriesCollectionRepository {
|
||||
fun findByIdOrNull(thumbnailId: String): ThumbnailSeriesCollection?
|
||||
|
||||
fun findSelectedByCollectionIdOrNull(collectionId: String): ThumbnailSeriesCollection?
|
||||
|
||||
fun findAllByCollectionId(collectionId: String): Collection<ThumbnailSeriesCollection>
|
||||
|
||||
fun findAllWithoutMetadata(pageable: Pageable): Page<ThumbnailSeriesCollection>
|
||||
|
||||
fun insert(thumbnail: ThumbnailSeriesCollection)
|
||||
|
||||
fun update(thumbnail: ThumbnailSeriesCollection)
|
||||
|
||||
fun updateMetadata(thumbnails: Collection<ThumbnailSeriesCollection>)
|
||||
|
||||
fun markSelected(thumbnail: ThumbnailSeriesCollection)
|
||||
|
||||
fun delete(thumbnailCollectionId: String)
|
||||
|
||||
fun deleteByCollectionId(collectionId: String)
|
||||
|
||||
fun deleteByCollectionIds(collectionIds: Collection<String>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,17 +6,27 @@ import org.springframework.data.domain.Pageable
|
|||
|
||||
interface ThumbnailSeriesRepository {
|
||||
fun findByIdOrNull(thumbnailId: String): ThumbnailSeries?
|
||||
|
||||
fun findSelectedBySeriesIdOrNull(seriesId: String): ThumbnailSeries?
|
||||
|
||||
fun findAllBySeriesId(seriesId: String): Collection<ThumbnailSeries>
|
||||
fun findAllBySeriesIdIdAndType(seriesId: String, type: ThumbnailSeries.Type): Collection<ThumbnailSeries>
|
||||
|
||||
fun findAllBySeriesIdIdAndType(
|
||||
seriesId: String,
|
||||
type: ThumbnailSeries.Type,
|
||||
): Collection<ThumbnailSeries>
|
||||
|
||||
fun findAllWithoutMetadata(pageable: Pageable): Page<ThumbnailSeries>
|
||||
|
||||
fun insert(thumbnail: ThumbnailSeries)
|
||||
|
||||
fun updateMetadata(thumbnails: Collection<ThumbnailSeries>)
|
||||
|
||||
fun markSelected(thumbnail: ThumbnailSeries)
|
||||
|
||||
fun delete(thumbnailSeriesId: String)
|
||||
|
||||
fun deleteBySeriesId(seriesId: String)
|
||||
|
||||
fun deleteBySeriesIds(seriesIds: Collection<String>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import org.gotson.komga.domain.model.TransientBook
|
|||
|
||||
interface TransientBookRepository {
|
||||
fun findByIdOrNull(transientBookId: String): TransientBook?
|
||||
|
||||
fun save(transientBook: TransientBook)
|
||||
|
||||
fun save(transientBooks: Collection<TransientBook>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,22 +51,27 @@ class BookAnalyzer(
|
|||
@Qualifier("pdfImageType")
|
||||
private val pdfImageType: ImageType,
|
||||
) {
|
||||
|
||||
val divinaExtractors = extractors
|
||||
val divinaExtractors =
|
||||
extractors
|
||||
.flatMap { e -> e.mediaTypes().map { it to e } }
|
||||
.toMap()
|
||||
|
||||
fun analyze(book: Book, analyzeDimensions: Boolean): Media {
|
||||
fun analyze(
|
||||
book: Book,
|
||||
analyzeDimensions: Boolean,
|
||||
): Media {
|
||||
logger.info { "Trying to analyze book: $book" }
|
||||
return try {
|
||||
var mediaType = contentDetector.detectMediaType(book.path).let {
|
||||
var mediaType =
|
||||
contentDetector.detectMediaType(book.path).let {
|
||||
logger.info { "Detected media type: $it" }
|
||||
MediaType.fromMediaType(it) ?: return Media(mediaType = it, status = Media.Status.UNSUPPORTED, comment = "ERR_1001", bookId = book.id)
|
||||
}
|
||||
|
||||
if (book.path.extension.lowercase() == "epub" && mediaType != MediaType.EPUB) {
|
||||
if (epubExtractor.isEpub(book.path)) mediaType = MediaType.EPUB
|
||||
else {
|
||||
if (epubExtractor.isEpub(book.path)) {
|
||||
mediaType = MediaType.EPUB
|
||||
} else {
|
||||
logger.warn { "Epub file is malformed, file is probably broken: ${book.path}" }
|
||||
return Media(mediaType = mediaType.type, status = Media.Status.ERROR, comment = "ERR_1032", bookId = book.id)
|
||||
}
|
||||
|
|
@ -89,8 +94,13 @@ class BookAnalyzer(
|
|||
}.copy(bookId = book.id)
|
||||
}
|
||||
|
||||
private fun analyzeDivina(book: Book, mediaType: MediaType, analyzeDimensions: Boolean): Media {
|
||||
val entries = try {
|
||||
private fun analyzeDivina(
|
||||
book: Book,
|
||||
mediaType: MediaType,
|
||||
analyzeDimensions: Boolean,
|
||||
): Media {
|
||||
val entries =
|
||||
try {
|
||||
divinaExtractors[mediaType.type]?.getEntries(book.path, analyzeDimensions)
|
||||
?: return Media(status = Media.Status.UNSUPPORTED)
|
||||
} catch (ex: MediaUnsupportedException) {
|
||||
|
|
@ -100,7 +110,8 @@ class BookAnalyzer(
|
|||
return Media(status = Media.Status.ERROR, comment = "ERR_1008")
|
||||
}
|
||||
|
||||
val (pages, others) = entries
|
||||
val (pages, others) =
|
||||
entries
|
||||
.partition { entry ->
|
||||
entry.mediaType?.let { contentDetector.isImage(it) } ?: false
|
||||
}.let { (images, others) ->
|
||||
|
|
@ -110,7 +121,8 @@ class BookAnalyzer(
|
|||
)
|
||||
}
|
||||
|
||||
val entriesErrorSummary = others
|
||||
val entriesErrorSummary =
|
||||
others
|
||||
.filter { it.mediaType.isNullOrBlank() }
|
||||
.map { it.name }
|
||||
.ifEmpty { null }
|
||||
|
|
@ -127,9 +139,13 @@ class BookAnalyzer(
|
|||
return Media(status = Media.Status.READY, pages = pages, pageCount = pages.size, files = files, comment = entriesErrorSummary)
|
||||
}
|
||||
|
||||
private fun analyzeEpub(book: Book, analyzeDimensions: Boolean): Media {
|
||||
private fun analyzeEpub(
|
||||
book: Book,
|
||||
analyzeDimensions: Boolean,
|
||||
): Media {
|
||||
val manifest = epubExtractor.getManifest(book.path, analyzeDimensions)
|
||||
val entriesErrorSummary = manifest.missingResources
|
||||
val entriesErrorSummary =
|
||||
manifest.missingResources
|
||||
.map { it.fileName }
|
||||
.ifEmpty { null }
|
||||
?.joinToString(prefix = "ERR_1033 [", postfix = "]") { it }
|
||||
|
|
@ -139,7 +155,8 @@ class BookAnalyzer(
|
|||
files = manifest.resources,
|
||||
pageCount = manifest.pageCount,
|
||||
epubDivinaCompatible = manifest.divinaPages.isNotEmpty(),
|
||||
extension = MediaExtensionEpub(
|
||||
extension =
|
||||
MediaExtensionEpub(
|
||||
toc = manifest.toc,
|
||||
landmarks = manifest.landmarks,
|
||||
pageList = manifest.pageList,
|
||||
|
|
@ -150,7 +167,10 @@ class BookAnalyzer(
|
|||
)
|
||||
}
|
||||
|
||||
private fun analyzePdf(book: Book, analyzeDimensions: Boolean): Media {
|
||||
private fun analyzePdf(
|
||||
book: Book,
|
||||
analyzeDimensions: Boolean,
|
||||
): Media {
|
||||
val pages = pdfExtractor.getPages(book.path, analyzeDimensions).map { BookPage(it.name, "", it.dimension) }
|
||||
return Media(status = Media.Status.READY, pages = pages)
|
||||
}
|
||||
|
|
@ -167,7 +187,8 @@ class BookAnalyzer(
|
|||
throw MediaNotReadyException()
|
||||
}
|
||||
|
||||
val thumbnail = getPoster(book)?.let { cover ->
|
||||
val thumbnail =
|
||||
getPoster(book)?.let { cover ->
|
||||
imageConverter.resizeImageToByteArray(cover.bytes, thumbnailType, komgaSettingsProvider.thumbnailSize.maxEdge)
|
||||
} ?: throw NoThumbnailFoundException()
|
||||
|
||||
|
|
@ -181,8 +202,10 @@ class BookAnalyzer(
|
|||
)
|
||||
}
|
||||
|
||||
fun getPoster(book: BookWithMedia): TypedBytes? = when (book.media.profile) {
|
||||
MediaProfile.DIVINA -> divinaExtractors[book.media.mediaType]?.getEntryStream(book.book.path, book.media.pages.first().fileName)?.let {
|
||||
fun getPoster(book: BookWithMedia): TypedBytes? =
|
||||
when (book.media.profile) {
|
||||
MediaProfile.DIVINA ->
|
||||
divinaExtractors[book.media.mediaType]?.getEntryStream(book.book.path, book.media.pages.first().fileName)?.let {
|
||||
TypedBytes(
|
||||
it,
|
||||
book.media.pages.first().mediaType,
|
||||
|
|
@ -198,7 +221,10 @@ class BookAnalyzer(
|
|||
MediaNotReadyException::class,
|
||||
IndexOutOfBoundsException::class,
|
||||
)
|
||||
fun getPageContent(book: BookWithMedia, number: Int): ByteArray {
|
||||
fun getPageContent(
|
||||
book: BookWithMedia,
|
||||
number: Int,
|
||||
): ByteArray {
|
||||
logger.debug { "Get page #$number for book: $book" }
|
||||
|
||||
if (book.media.status != Media.Status.READY) {
|
||||
|
|
@ -215,8 +241,10 @@ class BookAnalyzer(
|
|||
MediaProfile.DIVINA -> divinaExtractors.getValue(book.media.mediaType!!).getEntryStream(book.book.path, book.media.pages[number - 1].fileName)
|
||||
MediaProfile.PDF -> pdfExtractor.getPageContentAsImage(book.book.path, number).bytes
|
||||
MediaProfile.EPUB ->
|
||||
if (book.media.epubDivinaCompatible) epubExtractor.getEntryStream(book.book.path, book.media.pages[number - 1].fileName)
|
||||
else throw MediaUnsupportedException("Epub profile does not support getting page content")
|
||||
if (book.media.epubDivinaCompatible)
|
||||
epubExtractor.getEntryStream(book.book.path, book.media.pages[number - 1].fileName)
|
||||
else
|
||||
throw MediaUnsupportedException("Epub profile does not support getting page content")
|
||||
|
||||
null -> throw MediaNotReadyException()
|
||||
}
|
||||
|
|
@ -226,7 +254,10 @@ class BookAnalyzer(
|
|||
MediaNotReadyException::class,
|
||||
IndexOutOfBoundsException::class,
|
||||
)
|
||||
fun getPageContentRaw(book: BookWithMedia, number: Int): TypedBytes {
|
||||
fun getPageContentRaw(
|
||||
book: BookWithMedia,
|
||||
number: Int,
|
||||
): TypedBytes {
|
||||
logger.debug { "Get raw page #$number for book: $book" }
|
||||
if (book.media.profile != MediaProfile.PDF) throw MediaUnsupportedException("Extractor does not support raw extraction of pages")
|
||||
|
||||
|
|
@ -246,7 +277,10 @@ class BookAnalyzer(
|
|||
@Throws(
|
||||
MediaNotReadyException::class,
|
||||
)
|
||||
fun getFileContent(book: BookWithMedia, fileName: String): ByteArray {
|
||||
fun getFileContent(
|
||||
book: BookWithMedia,
|
||||
fileName: String,
|
||||
): ByteArray {
|
||||
logger.debug { "Get file $fileName for book: $book" }
|
||||
|
||||
if (book.media.status != Media.Status.READY) {
|
||||
|
|
@ -268,12 +302,15 @@ class BookAnalyzer(
|
|||
* See [org.gotson.komga.infrastructure.configuration.KomgaProperties.pageHashing]
|
||||
*/
|
||||
fun hashPages(book: BookWithMedia): Media {
|
||||
val hashedPages = book.media.pages.mapIndexed { index, bookPage ->
|
||||
val hashedPages =
|
||||
book.media.pages.mapIndexed { index, bookPage ->
|
||||
if (bookPage.fileHash.isBlank() && (index < pageHashing || index >= (book.media.pageCount - pageHashing))) {
|
||||
val content = getPageContent(book, index + 1)
|
||||
val hash = hashPage(bookPage, content)
|
||||
bookPage.copy(fileHash = hash)
|
||||
} else bookPage
|
||||
} else {
|
||||
bookPage
|
||||
}
|
||||
}
|
||||
|
||||
return book.media.copy(pages = hashedPages)
|
||||
|
|
@ -284,7 +321,10 @@ class BookAnalyzer(
|
|||
*
|
||||
* For JPEG, the image is read/written to remove the metadata.
|
||||
*/
|
||||
fun hashPage(page: BookPage, content: ByteArray): String {
|
||||
fun hashPage(
|
||||
page: BookPage,
|
||||
content: ByteArray,
|
||||
): String {
|
||||
val bytes =
|
||||
if (page.mediaType == ImageType.JPEG.mediaType) {
|
||||
// JPEG could contain different EXIF data, reading and writing back the image will get rid of it
|
||||
|
|
@ -292,7 +332,9 @@ class BookAnalyzer(
|
|||
ImageIO.write(ImageIO.read(content.inputStream()), ImageType.JPEG.imageIOFormat, buffer)
|
||||
buffer.toByteArray()
|
||||
}
|
||||
} else content
|
||||
} else {
|
||||
content
|
||||
}
|
||||
|
||||
return hasher.computeHash(bytes.inputStream())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ class BookConverter(
|
|||
private val eventPublisher: ApplicationEventPublisher,
|
||||
private val historicalEventRepository: HistoricalEventRepository,
|
||||
) {
|
||||
|
||||
private val convertibleTypes = listOf(MediaType.RAR_4.type, MediaType.RAR_5.type)
|
||||
|
||||
private val mediaTypeToExtension =
|
||||
|
|
@ -60,10 +59,10 @@ class BookConverter(
|
|||
private val skippedRepairs = mutableListOf<String>()
|
||||
|
||||
fun getConvertibleBooks(library: Library): Collection<Book> =
|
||||
if (library.convertToCbz)
|
||||
if (library.convertToCbz) {
|
||||
bookRepository.findAllByLibraryIdAndMediaTypes(library.id, convertibleTypes)
|
||||
.also { logger.info { "Found ${it.size} books to convert" } }
|
||||
else {
|
||||
} else {
|
||||
logger.info { "CBZ conversion is not enabled, skipping" }
|
||||
emptyList()
|
||||
}
|
||||
|
|
@ -111,7 +110,8 @@ class BookConverter(
|
|||
}
|
||||
|
||||
// perform checks on new file
|
||||
val convertedBook = fileSystemScanner.scanFile(destinationPath)
|
||||
val convertedBook =
|
||||
fileSystemScanner.scanFile(destinationPath)
|
||||
?.copy(
|
||||
id = book.id,
|
||||
seriesId = book.seriesId,
|
||||
|
|
@ -200,7 +200,8 @@ class BookConverter(
|
|||
logger.info { "Renaming ${book.path} to $destinationPath" }
|
||||
book.path.moveTo(destinationPath)
|
||||
|
||||
val repairedBook = fileSystemScanner.scanFile(destinationPath)
|
||||
val repairedBook =
|
||||
fileSystemScanner.scanFile(destinationPath)
|
||||
?.copy(
|
||||
id = book.id,
|
||||
seriesId = book.seriesId,
|
||||
|
|
|
|||
|
|
@ -60,8 +60,13 @@ class BookImporter(
|
|||
private val taskEmitter: TaskEmitter,
|
||||
private val historicalEventRepository: HistoricalEventRepository,
|
||||
) {
|
||||
|
||||
fun importBook(sourceFile: Path, series: Series, copyMode: CopyMode, destinationName: String? = null, upgradeBookId: String? = null): Book {
|
||||
fun importBook(
|
||||
sourceFile: Path,
|
||||
series: Series,
|
||||
copyMode: CopyMode,
|
||||
destinationName: String? = null,
|
||||
upgradeBookId: String? = null,
|
||||
): Book {
|
||||
try {
|
||||
if (sourceFile.notExists()) throw FileNotFoundException("File not found: $sourceFile").withCode("ERR_1018")
|
||||
if (series.oneshot) throw IllegalArgumentException("Destination series is oneshot")
|
||||
|
|
@ -70,14 +75,20 @@ class BookImporter(
|
|||
if (sourceFile.startsWith(library.path)) throw PathContainedInPath("Cannot import file that is part of an existing library", "ERR_1019")
|
||||
}
|
||||
|
||||
val destFile = series.path.resolve(
|
||||
if (destinationName != null) Paths.get("$destinationName.${sourceFile.extension}").name
|
||||
else sourceFile.name,
|
||||
)
|
||||
val sidecars = fileSystemScanner.scanBookSidecars(sourceFile).associateWith {
|
||||
val destFile =
|
||||
series.path.resolve(
|
||||
if (destinationName != null) it.url.toURI().toPath().name.replace(sourceFile.nameWithoutExtension, destinationName, true)
|
||||
else it.url.toURI().toPath().name,
|
||||
if (destinationName != null)
|
||||
Paths.get("$destinationName.${sourceFile.extension}").name
|
||||
else
|
||||
sourceFile.name,
|
||||
)
|
||||
val sidecars =
|
||||
fileSystemScanner.scanBookSidecars(sourceFile).associateWith {
|
||||
series.path.resolve(
|
||||
if (destinationName != null)
|
||||
it.url.toURI().toPath().name.replace(sourceFile.nameWithoutExtension, destinationName, true)
|
||||
else
|
||||
it.url.toURI().toPath().name,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +97,9 @@ class BookImporter(
|
|||
bookRepository.findByIdOrNull(upgradeBookId)?.also {
|
||||
if (it.seriesId != series.id) throw IllegalArgumentException("Book to upgrade ($upgradeBookId) does not belong to series: $series").withCode("ERR_1020")
|
||||
}
|
||||
} else null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
var deletedUpgradedFile = false
|
||||
when {
|
||||
|
|
@ -135,7 +148,8 @@ class BookImporter(
|
|||
}
|
||||
}
|
||||
|
||||
CopyMode.HARDLINK -> try {
|
||||
CopyMode.HARDLINK ->
|
||||
try {
|
||||
logger.info { "Hardlink file $sourceFile to $destFile" }
|
||||
Files.createLink(destFile, sourceFile)
|
||||
sidecars.forEach {
|
||||
|
|
@ -154,7 +168,8 @@ class BookImporter(
|
|||
}
|
||||
}
|
||||
|
||||
val importedBook = fileSystemScanner.scanFile(destFile)
|
||||
val importedBook =
|
||||
fileSystemScanner.scanFile(destFile)
|
||||
?.copy(libraryId = series.libraryId)
|
||||
?: throw IllegalStateException("Newly imported book could not be scanned: $destFile").withCode("ERR_1022")
|
||||
|
||||
|
|
@ -208,7 +223,8 @@ class BookImporter(
|
|||
Sidecar.Type.ARTWORK -> taskEmitter.refreshBookLocalArtwork(importedBook)
|
||||
Sidecar.Type.METADATA -> taskEmitter.refreshBookMetadata(importedBook)
|
||||
}
|
||||
val destSidecar = sourceSidecar.copy(
|
||||
val destSidecar =
|
||||
sourceSidecar.copy(
|
||||
url = destPath.toUri().toURL(),
|
||||
parentUrl = destPath.parent.toUri().toURL(),
|
||||
lastModifiedTime = destPath.readAttributes<BasicFileAttributes>().getUpdatedTime(),
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ class BookLifecycle(
|
|||
@Qualifier("pdfImageType")
|
||||
private val pdfImageType: ImageType,
|
||||
) {
|
||||
|
||||
private val resizeTargetFormat = ImageType.JPEG
|
||||
|
||||
fun analyzeAndPersist(book: Book): Set<BookAction> {
|
||||
|
|
@ -80,7 +79,8 @@ class BookLifecycle(
|
|||
// if the number of pages has changed, delete all read progress for that book
|
||||
mediaRepository.findById(book.id).let { previous ->
|
||||
if (previous.status == Media.Status.OUTDATED && previous.pageCount != media.pageCount) {
|
||||
val adjustedProgress = readProgressRepository.findAllByBookId(book.id)
|
||||
val adjustedProgress =
|
||||
readProgressRepository.findAllByBookId(book.id)
|
||||
.map { it.copy(page = if (it.completed) media.pageCount else 1) }
|
||||
if (adjustedProgress.isNotEmpty()) {
|
||||
logger.info { "Number of pages differ, adjust read progress for book" }
|
||||
|
|
@ -130,7 +130,10 @@ class BookLifecycle(
|
|||
}
|
||||
}
|
||||
|
||||
fun addThumbnailForBook(thumbnail: ThumbnailBook, markSelected: MarkSelectedPreference): ThumbnailBook {
|
||||
fun addThumbnailForBook(
|
||||
thumbnail: ThumbnailBook,
|
||||
markSelected: MarkSelectedPreference,
|
||||
): ThumbnailBook {
|
||||
when (thumbnail.type) {
|
||||
ThumbnailBook.Type.GENERATED -> {
|
||||
// only one generated thumbnail is allowed
|
||||
|
|
@ -153,7 +156,8 @@ class BookLifecycle(
|
|||
}
|
||||
}
|
||||
|
||||
val selected = when (markSelected) {
|
||||
val selected =
|
||||
when (markSelected) {
|
||||
MarkSelectedPreference.YES -> true
|
||||
MarkSelectedPreference.IF_NONE_OR_GENERATED -> {
|
||||
val selectedThumbnail = thumbnailBookRepository.findSelectedByBookIdOrNull(thumbnail.bookId)
|
||||
|
|
@ -163,8 +167,10 @@ class BookLifecycle(
|
|||
MarkSelectedPreference.NO -> false
|
||||
}
|
||||
|
||||
if (selected) thumbnailBookRepository.markSelected(thumbnail)
|
||||
else thumbnailsHouseKeeping(thumbnail.bookId)
|
||||
if (selected)
|
||||
thumbnailBookRepository.markSelected(thumbnail)
|
||||
else
|
||||
thumbnailsHouseKeeping(thumbnail.bookId)
|
||||
|
||||
val newThumbnail = thumbnail.copy(selected = selected)
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(newThumbnail))
|
||||
|
|
@ -189,9 +195,13 @@ class BookLifecycle(
|
|||
return selected
|
||||
}
|
||||
|
||||
fun getThumbnailBytes(bookId: String, resizeTo: Int? = null): TypedBytes? {
|
||||
fun getThumbnailBytes(
|
||||
bookId: String,
|
||||
resizeTo: Int? = null,
|
||||
): TypedBytes? {
|
||||
getThumbnail(bookId)?.let {
|
||||
val thumbnailBytes = when {
|
||||
val thumbnailBytes =
|
||||
when {
|
||||
it.thumbnail != null -> it.thumbnail
|
||||
it.url != null -> File(it.url.toURI()).readBytes()
|
||||
else -> return null
|
||||
|
|
@ -238,13 +248,16 @@ class BookLifecycle(
|
|||
|
||||
private fun thumbnailsHouseKeeping(bookId: String) {
|
||||
logger.info { "House keeping thumbnails for book: $bookId" }
|
||||
val all = thumbnailBookRepository.findAllByBookId(bookId)
|
||||
val all =
|
||||
thumbnailBookRepository.findAllByBookId(bookId)
|
||||
.mapNotNull {
|
||||
if (!it.exists()) {
|
||||
logger.warn { "Thumbnail doesn't exist, removing entry" }
|
||||
thumbnailBookRepository.delete(it.id)
|
||||
null
|
||||
} else it
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
val selected = all.filter { it.selected }
|
||||
|
|
@ -274,15 +287,23 @@ class BookLifecycle(
|
|||
MediaNotReadyException::class,
|
||||
IndexOutOfBoundsException::class,
|
||||
)
|
||||
fun getBookPage(book: Book, number: Int, convertTo: ImageType? = null, resizeTo: Int? = null): TypedBytes {
|
||||
fun getBookPage(
|
||||
book: Book,
|
||||
number: Int,
|
||||
convertTo: ImageType? = null,
|
||||
resizeTo: Int? = null,
|
||||
): TypedBytes {
|
||||
val media = mediaRepository.findById(book.id)
|
||||
val pageContent = bookAnalyzer.getPageContent(BookWithMedia(book, media), number)
|
||||
val pageMediaType =
|
||||
if (media.profile == MediaProfile.PDF) pdfImageType.mediaType
|
||||
else media.pages[number - 1].mediaType
|
||||
if (media.profile == MediaProfile.PDF)
|
||||
pdfImageType.mediaType
|
||||
else
|
||||
media.pages[number - 1].mediaType
|
||||
|
||||
if (resizeTo != null) {
|
||||
val convertedPage = try {
|
||||
val convertedPage =
|
||||
try {
|
||||
imageConverter.resizeImageToByteArray(pageContent, resizeTargetFormat, resizeTo)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Resize page #$number of book $book to $resizeTo: failed" }
|
||||
|
|
@ -304,7 +325,8 @@ class BookLifecycle(
|
|||
}
|
||||
|
||||
logger.info { msg }
|
||||
val convertedPage = try {
|
||||
val convertedPage =
|
||||
try {
|
||||
imageConverter.convertImage(pageContent, it.imageIOFormat)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "$msg: conversion failed" }
|
||||
|
|
@ -360,7 +382,11 @@ class BookLifecycle(
|
|||
books.forEach { eventPublisher.publishEvent(DomainEvent.BookDeleted(it)) }
|
||||
}
|
||||
|
||||
fun markReadProgress(book: Book, user: KomgaUser, page: Int) {
|
||||
fun markReadProgress(
|
||||
book: Book,
|
||||
user: KomgaUser,
|
||||
page: Int,
|
||||
) {
|
||||
val pages = mediaRepository.getPagesSize(book.id)
|
||||
require(page in 1..pages) { "Page argument ($page) must be within 1 and book page count ($pages)" }
|
||||
|
||||
|
|
@ -369,7 +395,10 @@ class BookLifecycle(
|
|||
eventPublisher.publishEvent(DomainEvent.ReadProgressChanged(progress))
|
||||
}
|
||||
|
||||
fun markReadProgressCompleted(bookId: String, user: KomgaUser) {
|
||||
fun markReadProgressCompleted(
|
||||
bookId: String,
|
||||
user: KomgaUser,
|
||||
) {
|
||||
val media = mediaRepository.findById(bookId)
|
||||
|
||||
val progress = ReadProgress(bookId, user.id, media.pageCount, true)
|
||||
|
|
@ -377,21 +406,29 @@ class BookLifecycle(
|
|||
eventPublisher.publishEvent(DomainEvent.ReadProgressChanged(progress))
|
||||
}
|
||||
|
||||
fun deleteReadProgress(book: Book, user: KomgaUser) {
|
||||
fun deleteReadProgress(
|
||||
book: Book,
|
||||
user: KomgaUser,
|
||||
) {
|
||||
readProgressRepository.findByBookIdAndUserIdOrNull(book.id, user.id)?.let { progress ->
|
||||
readProgressRepository.delete(book.id, user.id)
|
||||
eventPublisher.publishEvent(DomainEvent.ReadProgressDeleted(progress))
|
||||
}
|
||||
}
|
||||
|
||||
fun markProgression(book: Book, user: KomgaUser, newProgression: R2Progression) {
|
||||
fun markProgression(
|
||||
book: Book,
|
||||
user: KomgaUser,
|
||||
newProgression: R2Progression,
|
||||
) {
|
||||
readProgressRepository.findByBookIdAndUserIdOrNull(book.id, user.id)?.let { savedProgress ->
|
||||
check(newProgression.modified.toLocalDateTime().toCurrentTimeZone().isAfter(savedProgress.readDate)) { "Progression is older than existing" }
|
||||
}
|
||||
|
||||
val media = mediaRepository.findById(book.id)
|
||||
requireNotNull(media.profile) { "Media has no profile" }
|
||||
val progress = when (media.profile!!) {
|
||||
val progress =
|
||||
when (media.profile!!) {
|
||||
MediaProfile.DIVINA,
|
||||
MediaProfile.PDF,
|
||||
-> {
|
||||
|
|
@ -409,14 +446,16 @@ class BookLifecycle(
|
|||
}
|
||||
|
||||
MediaProfile.EPUB -> {
|
||||
val href = newProgression.locator.href
|
||||
val href =
|
||||
newProgression.locator.href
|
||||
.replaceBefore("/resource/", "").removePrefix("/resource/")
|
||||
.replaceAfter("#", "").removeSuffix("#")
|
||||
.let { UriUtils.decode(it, Charsets.UTF_8) }
|
||||
require(href in media.files.map { it.fileName }) { "Resource does not exist in book: $href" }
|
||||
requireNotNull(newProgression.locator.locations?.progression) { "location.progression is required" }
|
||||
|
||||
val extension = mediaRepository.findExtensionByIdOrNull(book.id) as? MediaExtensionEpub
|
||||
val extension =
|
||||
mediaRepository.findExtensionByIdOrNull(book.id) as? MediaExtensionEpub
|
||||
?: throw IllegalArgumentException("Epub extension not found")
|
||||
// match progression with positions
|
||||
val matchingPositions = extension.positions.filter { it.href == href }
|
||||
|
|
@ -453,7 +492,8 @@ class BookLifecycle(
|
|||
if (book.path.notExists()) return logger.info { "Cannot delete book file, path does not exist: ${book.path}" }
|
||||
if (!book.path.isWritable()) return logger.info { "Cannot delete book file, path is not writable: ${book.path}" }
|
||||
|
||||
val thumbnails = thumbnailBookRepository.findAllByBookIdAndType(book.id, ThumbnailBook.Type.SIDECAR)
|
||||
val thumbnails =
|
||||
thumbnailBookRepository.findAllByBookIdAndType(book.id, ThumbnailBook.Type.SIDECAR)
|
||||
.mapNotNull { it.url?.toURI()?.toPath() }
|
||||
.filter { it.exists() && it.isWritable() }
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ class BookMetadataLifecycle(
|
|||
private val readListLifecycle: ReadListLifecycle,
|
||||
private val eventPublisher: ApplicationEventPublisher,
|
||||
) {
|
||||
|
||||
fun refreshMetadata(book: Book, capabilities: Set<BookMetadataPatchCapability>) {
|
||||
fun refreshMetadata(
|
||||
book: Book,
|
||||
capabilities: Set<BookMetadataPatchCapability>,
|
||||
) {
|
||||
logger.info { "Refresh metadata for book: $book with capabilities: $capabilities" }
|
||||
val media = mediaRepository.findById(book.id)
|
||||
|
||||
|
|
@ -44,7 +46,8 @@ class BookMetadataLifecycle(
|
|||
|
||||
else -> {
|
||||
logger.debug { "Provider: ${provider.javaClass.simpleName}" }
|
||||
val patch = try {
|
||||
val patch =
|
||||
try {
|
||||
provider.getBookMetadataFromBook(BookWithMedia(book, media))
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Error while getting metadata from ${provider.javaClass.simpleName} for book: $book" }
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ class BookPageEditor(
|
|||
|
||||
private val failedPageRemoval = mutableListOf<String>()
|
||||
|
||||
fun removeHashedPages(book: Book, pagesToDelete: Collection<BookPageNumbered>): BookAction? {
|
||||
fun removeHashedPages(
|
||||
book: Book,
|
||||
pagesToDelete: Collection<BookPageNumbered>,
|
||||
): BookAction? {
|
||||
// perform various checks
|
||||
if (failedPageRemoval.contains(book.id)) {
|
||||
logger.info { "Book page removal already failed before, skipping" }
|
||||
|
|
@ -75,7 +78,8 @@ class BookPageEditor(
|
|||
throw MediaNotReadyException()
|
||||
|
||||
// create a temp file with the pages removed
|
||||
val pagesToKeep = media.pages.filterIndexed { index, page ->
|
||||
val pagesToKeep =
|
||||
media.pages.filterIndexed { index, page ->
|
||||
pagesToDelete.find { candidate ->
|
||||
candidate.fileHash == page.fileHash &&
|
||||
candidate.mediaType == page.mediaType &&
|
||||
|
|
@ -109,7 +113,8 @@ class BookPageEditor(
|
|||
}
|
||||
|
||||
// perform checks on new file
|
||||
val createdBook = fileSystemScanner.scanFile(tempFile)
|
||||
val createdBook =
|
||||
fileSystemScanner.scanFile(tempFile)
|
||||
?.copy(
|
||||
id = book.id,
|
||||
seriesId = book.seriesId,
|
||||
|
|
@ -142,7 +147,8 @@ class BookPageEditor(
|
|||
}
|
||||
|
||||
tempFile.moveTo(book.path, true)
|
||||
val newBook = fileSystemScanner.scanFile(book.path)
|
||||
val newBook =
|
||||
fileSystemScanner.scanFile(book.path)
|
||||
?.copy(
|
||||
id = book.id,
|
||||
seriesId = book.seriesId,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ class FileSystemScanner(
|
|||
private val sidecarBookConsumers: List<SidecarBookConsumer>,
|
||||
private val sidecarSeriesConsumers: List<SidecarSeriesConsumer>,
|
||||
) {
|
||||
|
||||
private data class TempSidecar(
|
||||
val name: String,
|
||||
val url: URL,
|
||||
|
|
@ -55,7 +54,8 @@ class FileSystemScanner(
|
|||
scanEpub: Boolean = true,
|
||||
directoryExclusions: Set<String> = emptySet(),
|
||||
): ScanResult {
|
||||
val scanForExtensions = buildList {
|
||||
val scanForExtensions =
|
||||
buildList {
|
||||
if (scanCbx) addAll(listOf("cbz", "zip", "cbr", "rar"))
|
||||
if (scanPdf) add("pdf")
|
||||
if (scanEpub) add("epub")
|
||||
|
|
@ -84,15 +84,20 @@ class FileSystemScanner(
|
|||
setOf(FileVisitOption.FOLLOW_LINKS),
|
||||
Integer.MAX_VALUE,
|
||||
object : FileVisitor<Path> {
|
||||
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||
override fun preVisitDirectory(
|
||||
dir: Path,
|
||||
attrs: BasicFileAttributes,
|
||||
): FileVisitResult {
|
||||
logger.trace { "preVisit: $dir (regularFile:${attrs.isRegularFile}, directory:${attrs.isDirectory}, symbolicLink:${attrs.isSymbolicLink}, other:${attrs.isOther})" }
|
||||
if (dir.name.startsWith(".") ||
|
||||
directoryExclusions.any { exclude ->
|
||||
dir.pathString.contains(exclude, true)
|
||||
}
|
||||
) return FileVisitResult.SKIP_SUBTREE
|
||||
)
|
||||
return FileVisitResult.SKIP_SUBTREE
|
||||
|
||||
pathToSeries[dir] = Series(
|
||||
pathToSeries[dir] =
|
||||
Series(
|
||||
name = dir.name.ifBlank { dir.pathString },
|
||||
url = dir.toUri().toURL(),
|
||||
fileLastModified = attrs.getUpdatedTime(),
|
||||
|
|
@ -101,7 +106,10 @@ class FileSystemScanner(
|
|||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||
override fun visitFile(
|
||||
file: Path,
|
||||
attrs: BasicFileAttributes,
|
||||
): FileVisitResult {
|
||||
logger.trace { "visitFile: $file (regularFile:${attrs.isRegularFile}, directory:${attrs.isDirectory}, symbolicLink:${attrs.isSymbolicLink}, other:${attrs.isOther})" }
|
||||
if (!attrs.isSymbolicLink && !attrs.isDirectory) {
|
||||
if (scanForExtensions.contains(file.extension.lowercase()) &&
|
||||
|
|
@ -131,19 +139,26 @@ class FileSystemScanner(
|
|||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun visitFileFailed(file: Path?, exc: IOException?): FileVisitResult {
|
||||
override fun visitFileFailed(
|
||||
file: Path?,
|
||||
exc: IOException?,
|
||||
): FileVisitResult {
|
||||
logger.warn { "Could not access: $file" }
|
||||
return FileVisitResult.SKIP_SUBTREE
|
||||
}
|
||||
|
||||
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
|
||||
override fun postVisitDirectory(
|
||||
dir: Path,
|
||||
exc: IOException?,
|
||||
): FileVisitResult {
|
||||
logger.trace { "postVisit: $dir" }
|
||||
val books = pathToBooks[dir]
|
||||
val tempSeries = pathToSeries[dir]
|
||||
if (!books.isNullOrEmpty() && tempSeries !== null) {
|
||||
if (!oneshotsDir.isNullOrBlank() && dir.pathString.contains(oneshotsDir, true)) {
|
||||
books.forEach { book ->
|
||||
val series = Series(
|
||||
val series =
|
||||
Series(
|
||||
name = book.name,
|
||||
url = book.url,
|
||||
fileLastModified = book.fileLastModified,
|
||||
|
|
@ -166,7 +181,8 @@ class FileSystemScanner(
|
|||
|
||||
// book sidecars are matched here, with the actual list of books
|
||||
books.forEach { book ->
|
||||
val sidecars = pathToBookSidecars[dir]
|
||||
val sidecars =
|
||||
pathToBookSidecars[dir]
|
||||
?.mapNotNull { sidecar ->
|
||||
sidecarBookConsumers.firstOrNull { it.isSidecarBookMatch(book.name, sidecar.name) }?.let {
|
||||
sidecar to it.getSidecarBookType()
|
||||
|
|
@ -210,7 +226,10 @@ class FileSystemScanner(
|
|||
}
|
||||
}
|
||||
|
||||
private fun pathToBook(path: Path, attrs: BasicFileAttributes): Book =
|
||||
private fun pathToBook(
|
||||
path: Path,
|
||||
attrs: BasicFileAttributes,
|
||||
): Book =
|
||||
Book(
|
||||
name = path.nameWithoutExtension,
|
||||
url = path.toUri().toURL(),
|
||||
|
|
|
|||
|
|
@ -26,8 +26,11 @@ class KomgaUserLifecycle(
|
|||
private val transactionTemplate: TransactionTemplate,
|
||||
private val eventPublisher: ApplicationEventPublisher,
|
||||
) {
|
||||
|
||||
fun updatePassword(user: KomgaUser, newPassword: String, expireSessions: Boolean) {
|
||||
fun updatePassword(
|
||||
user: KomgaUser,
|
||||
newPassword: String,
|
||||
expireSessions: Boolean,
|
||||
) {
|
||||
logger.info { "Changing password for user ${user.email}" }
|
||||
val updatedUser = user.copy(password = passwordEncoder.encode(newPassword))
|
||||
userRepository.update(updatedUser)
|
||||
|
|
@ -45,7 +48,8 @@ class KomgaUserLifecycle(
|
|||
logger.info { "Update user: $toUpdate" }
|
||||
userRepository.update(toUpdate)
|
||||
|
||||
val expireSessions = existing.roles != user.roles ||
|
||||
val expireSessions =
|
||||
existing.roles != user.roles ||
|
||||
existing.restrictions != user.restrictions ||
|
||||
existing.sharedAllLibraries != user.sharedAllLibraries ||
|
||||
existing.sharedLibrariesIds != user.sharedLibrariesIds
|
||||
|
|
|
|||
|
|
@ -61,11 +61,14 @@ class LibraryContentLifecycle(
|
|||
private val thumbnailBookRepository: ThumbnailBookRepository,
|
||||
private val eventPublisher: ApplicationEventPublisher,
|
||||
) {
|
||||
|
||||
fun scanRootFolder(library: Library, scanDeep: Boolean = false) {
|
||||
fun scanRootFolder(
|
||||
library: Library,
|
||||
scanDeep: Boolean = false,
|
||||
) {
|
||||
logger.info { "Scan root folder for library: $library" }
|
||||
measureTime {
|
||||
val scanResult = try {
|
||||
val scanResult =
|
||||
try {
|
||||
fileSystemScanner.scanRootFolder(
|
||||
Paths.get(library.root.toURI()),
|
||||
library.scanForceModifiedTime,
|
||||
|
|
@ -113,13 +116,16 @@ class LibraryContentLifecycle(
|
|||
}
|
||||
|
||||
// delete books that don't exist anymore. We need to do this now, so trash bin can work
|
||||
val seriesToSortAndRefresh = scannedSeries.values.flatten().map { it.url }.let { urls ->
|
||||
val seriesToSortAndRefresh =
|
||||
scannedSeries.values.flatten().map { it.url }.let { urls ->
|
||||
val books = bookRepository.findAllNotDeletedByLibraryIdAndUrlNotIn(library.id, urls)
|
||||
if (books.isNotEmpty()) {
|
||||
logger.info { "Soft deleting books not on disk anymore: $books" }
|
||||
bookLifecycle.softDeleteMany(books)
|
||||
books.map { it.seriesId }.distinct().mapNotNull { seriesRepository.findByIdOrNull(it) }.toMutableList()
|
||||
} else mutableListOf()
|
||||
} else {
|
||||
mutableListOf()
|
||||
}
|
||||
}
|
||||
// we store the url of all the series that had deleted books
|
||||
// this can be used to detect changed series even if their file modified date did not change, for example because of NFS/SMB cache
|
||||
|
|
@ -155,12 +161,16 @@ class LibraryContentLifecycle(
|
|||
existingBooks.find { it.url == newBook.url && it.deletedDate == null }?.let { existingBook ->
|
||||
logger.debug { "Matched existing book: $existingBook" }
|
||||
if (newBook.fileLastModified.notEquals(existingBook.fileLastModified)) {
|
||||
val hash = if (existingBook.fileSize == newBook.fileSize && existingBook.fileHash.isNotBlank()) {
|
||||
val hash =
|
||||
if (existingBook.fileSize == newBook.fileSize && existingBook.fileHash.isNotBlank()) {
|
||||
hasher.computeHash(newBook.path)
|
||||
} else null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (hash == existingBook.fileHash) {
|
||||
logger.info { "Book changed on disk, but still has the same hash, no need to reset media status: $existingBook" }
|
||||
val updatedBook = existingBook.copy(
|
||||
val updatedBook =
|
||||
existingBook.copy(
|
||||
fileLastModified = newBook.fileLastModified,
|
||||
fileSize = newBook.fileSize,
|
||||
fileHash = hash,
|
||||
|
|
@ -168,7 +178,8 @@ class LibraryContentLifecycle(
|
|||
bookRepository.update(updatedBook)
|
||||
} else {
|
||||
logger.info { "Book changed on disk, update and reset media status: $existingBook" }
|
||||
val updatedBook = existingBook.copy(
|
||||
val updatedBook =
|
||||
existingBook.copy(
|
||||
fileLastModified = newBook.fileLastModified,
|
||||
fileSize = newBook.fileSize,
|
||||
fileHash = hash ?: "",
|
||||
|
|
@ -237,8 +248,10 @@ class LibraryContentLifecycle(
|
|||
}
|
||||
}
|
||||
|
||||
if (library.emptyTrashAfterScan) emptyTrash(library)
|
||||
else cleanupEmptySets()
|
||||
if (library.emptyTrashAfterScan)
|
||||
emptyTrash(library)
|
||||
else
|
||||
cleanupEmptySets()
|
||||
}.also { logger.info { "Library updated in $it" } }
|
||||
|
||||
eventPublisher.publishEvent(DomainEvent.LibraryScanned(library))
|
||||
|
|
@ -255,17 +268,23 @@ class LibraryContentLifecycle(
|
|||
* - Metadata. The metadata title will only be copied if locked. If not locked, the folder name is used.
|
||||
* - all books, via #tryRestoreBooks
|
||||
*/
|
||||
private fun tryRestoreSeries(newSeries: Series, newBooks: List<Book>) {
|
||||
private fun tryRestoreSeries(
|
||||
newSeries: Series,
|
||||
newBooks: List<Book>,
|
||||
) {
|
||||
logger.info { "Try to restore series: $newSeries" }
|
||||
val bookSizes = newBooks.map { it.fileSize }
|
||||
|
||||
val deletedCandidates = seriesRepository.findAll(SeriesSearch(deleted = true))
|
||||
val deletedCandidates =
|
||||
seriesRepository.findAll(SeriesSearch(deleted = true))
|
||||
.mapNotNull { deletedCandidate ->
|
||||
val deletedBooks = bookRepository.findAllBySeriesId(deletedCandidate.id)
|
||||
val deletedBooksSizes = deletedBooks.map { it.fileSize }
|
||||
if (newBooks.size == deletedBooks.size && bookSizes.containsAll(deletedBooksSizes) && deletedBooksSizes.containsAll(bookSizes) && deletedBooks.all { it.fileHash.isNotBlank() }) {
|
||||
deletedCandidate to deletedBooks
|
||||
} else null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
logger.debug { "Deleted series candidates: $deletedCandidates" }
|
||||
|
||||
|
|
@ -273,7 +292,8 @@ class LibraryContentLifecycle(
|
|||
val newBooksWithHash = newBooks.map { book -> bookRepository.findByIdOrNull(book.id)!!.copy(fileHash = hasher.computeHash(book.path)) }
|
||||
bookRepository.update(newBooksWithHash)
|
||||
|
||||
val match = deletedCandidates.find { (_, books) ->
|
||||
val match =
|
||||
deletedCandidates.find { (_, books) ->
|
||||
books.map { it.fileHash }.containsAll(newBooksWithHash.map { it.fileHash }) && newBooksWithHash.map { it.fileHash }.containsAll(books.map { it.fileHash })
|
||||
}
|
||||
|
||||
|
|
@ -332,8 +352,10 @@ class LibraryContentLifecycle(
|
|||
if (deletedCandidates.isNotEmpty()) {
|
||||
// if the book has no hash, compute the hash and store it
|
||||
val bookWithHash =
|
||||
if (bookToAdd.fileHash.isNotBlank()) bookToAdd
|
||||
else bookRepository.findByIdOrNull(bookToAdd.id)!!.copy(fileHash = hasher.computeHash(bookToAdd.path)).also { bookRepository.update(it) }
|
||||
if (bookToAdd.fileHash.isNotBlank())
|
||||
bookToAdd
|
||||
else
|
||||
bookRepository.findByIdOrNull(bookToAdd.id)!!.copy(fileHash = hasher.computeHash(bookToAdd.path)).also { bookRepository.update(it) }
|
||||
|
||||
val match = deletedCandidates.find { it.fileHash == bookWithHash.fileHash }
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ class LibraryLifecycle(
|
|||
private val transactionTemplate: TransactionTemplate,
|
||||
private val libraryScanScheduler: LibraryScanScheduler,
|
||||
) {
|
||||
|
||||
@Throws(
|
||||
FileNotFoundException::class,
|
||||
DirectoryNotFoundException::class,
|
||||
|
|
@ -70,7 +69,10 @@ class LibraryLifecycle(
|
|||
eventPublisher.publishEvent(DomainEvent.LibraryUpdated(toUpdate))
|
||||
}
|
||||
|
||||
private fun checkLibraryShouldRescan(existing: Library, updated: Library): Boolean {
|
||||
private fun checkLibraryShouldRescan(
|
||||
existing: Library,
|
||||
updated: Library,
|
||||
): Boolean {
|
||||
if (existing.root != updated.root) return true
|
||||
if (existing.oneshotsDirectory != updated.oneshotsDirectory) return true
|
||||
if (existing.scanCbx != updated.scanCbx) return true
|
||||
|
|
@ -81,7 +83,10 @@ class LibraryLifecycle(
|
|||
return false
|
||||
}
|
||||
|
||||
private fun checkLibraryValidity(library: Library, existing: Collection<Library>) {
|
||||
private fun checkLibraryValidity(
|
||||
library: Library,
|
||||
existing: Collection<Library>,
|
||||
) {
|
||||
if (!Files.exists(library.path))
|
||||
throw FileNotFoundException("Library root folder does not exist: ${library.root}")
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ class LocalArtworkLifecycle(
|
|||
private val seriesLifecycle: SeriesLifecycle,
|
||||
private val localArtworkProvider: LocalArtworkProvider,
|
||||
) {
|
||||
|
||||
fun refreshLocalArtwork(book: Book) {
|
||||
logger.info { "Refresh local artwork for book: $book" }
|
||||
val library = libraryRepository.findById(book.libraryId)
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import org.springframework.stereotype.Service
|
|||
|
||||
@Service
|
||||
class MetadataAggregator {
|
||||
|
||||
fun aggregate(metadatas: Collection<BookMetadata>): BookMetadataAggregation {
|
||||
val authors = metadatas.flatMap { it.authors }.distinctBy { "${it.role}__${it.name}" }
|
||||
val tags = metadatas.flatMap { it.tags }.toSet()
|
||||
val (summary, summaryNumber) = metadatas
|
||||
val (summary, summaryNumber) =
|
||||
metadatas
|
||||
.sortedBy { it.numberSort }
|
||||
.find { it.summary.isNotBlank() }
|
||||
?.let {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,20 @@ import org.springframework.stereotype.Service
|
|||
|
||||
@Service
|
||||
class MetadataApplier {
|
||||
private fun <T> getIfNotLocked(
|
||||
original: T,
|
||||
patched: T?,
|
||||
lock: Boolean,
|
||||
): T =
|
||||
if (patched != null && !lock)
|
||||
patched
|
||||
else
|
||||
original
|
||||
|
||||
private fun <T> getIfNotLocked(original: T, patched: T?, lock: Boolean): T =
|
||||
if (patched != null && !lock) patched
|
||||
else original
|
||||
|
||||
fun apply(patch: BookMetadataPatch, metadata: BookMetadata): BookMetadata =
|
||||
fun apply(
|
||||
patch: BookMetadataPatch,
|
||||
metadata: BookMetadata,
|
||||
): BookMetadata =
|
||||
with(metadata) {
|
||||
copy(
|
||||
title = getIfNotLocked(title, patch.title, titleLock),
|
||||
|
|
@ -28,7 +36,10 @@ class MetadataApplier {
|
|||
)
|
||||
}
|
||||
|
||||
fun apply(patch: SeriesMetadataPatch, metadata: SeriesMetadata): SeriesMetadata =
|
||||
fun apply(
|
||||
patch: SeriesMetadataPatch,
|
||||
metadata: SeriesMetadata,
|
||||
): SeriesMetadata =
|
||||
with(metadata) {
|
||||
copy(
|
||||
status = getIfNotLocked(status, patch.status, statusLock),
|
||||
|
|
|
|||
|
|
@ -23,19 +23,21 @@ class PageHashLifecycle(
|
|||
private val bookRepository: BookRepository,
|
||||
private val komgaProperties: KomgaProperties,
|
||||
) {
|
||||
|
||||
private val hashableMediaTypes = listOf(MediaType.ZIP.type)
|
||||
|
||||
fun getBookIdsWithMissingPageHash(library: Library): Collection<String> =
|
||||
if (library.hashPages)
|
||||
if (library.hashPages) {
|
||||
mediaRepository.findAllBookIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(library.id, hashableMediaTypes, komgaProperties.pageHashing)
|
||||
.also { logger.info { "Found ${it.size} books with missing page hash" } }
|
||||
else {
|
||||
} else {
|
||||
logger.info { "Page hashing is not enabled, skipping" }
|
||||
emptyList()
|
||||
}
|
||||
|
||||
fun getPage(pageHash: String, resizeTo: Int? = null): TypedBytes? {
|
||||
fun getPage(
|
||||
pageHash: String,
|
||||
resizeTo: Int? = null,
|
||||
): TypedBytes? {
|
||||
val match = pageHashRepository.findMatchesByHash(pageHash, Pageable.ofSize(1)).firstOrNull() ?: return null
|
||||
val book = bookRepository.findByIdOrNull(match.bookId) ?: return null
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ class ReadListLifecycle(
|
|||
private val eventPublisher: ApplicationEventPublisher,
|
||||
private val transactionTemplate: TransactionTemplate,
|
||||
) {
|
||||
|
||||
@Throws(
|
||||
DuplicateNameException::class,
|
||||
)
|
||||
|
|
@ -50,7 +49,8 @@ class ReadListLifecycle(
|
|||
@Transactional
|
||||
fun updateReadList(toUpdate: ReadList) {
|
||||
logger.info { "Update read list: $toUpdate" }
|
||||
val existing = readListRepository.findByIdOrNull(toUpdate.id)
|
||||
val existing =
|
||||
readListRepository.findByIdOrNull(toUpdate.id)
|
||||
?: throw IllegalArgumentException("Cannot update read list that does not exist")
|
||||
|
||||
if (!existing.name.equals(toUpdate.name, true) && readListRepository.existsByName(toUpdate.name))
|
||||
|
|
@ -75,14 +75,19 @@ class ReadListLifecycle(
|
|||
* Read list will be created if it doesn't exist.
|
||||
*/
|
||||
@Transactional
|
||||
fun addBookToReadList(readListName: String, book: Book, numberInList: Int?) {
|
||||
fun addBookToReadList(
|
||||
readListName: String,
|
||||
book: Book,
|
||||
numberInList: Int?,
|
||||
) {
|
||||
readListRepository.findByNameOrNull(readListName).let { existing ->
|
||||
if (existing != null) {
|
||||
if (existing.bookIds.containsValue(book.id))
|
||||
if (existing.bookIds.containsValue(book.id)) {
|
||||
logger.debug { "Book is already in existing read list '${existing.name}'" }
|
||||
else {
|
||||
} else {
|
||||
val map = existing.bookIds.toSortedMap()
|
||||
val key = if (numberInList != null && existing.bookIds.containsKey(numberInList)) {
|
||||
val key =
|
||||
if (numberInList != null && existing.bookIds.containsKey(numberInList)) {
|
||||
logger.debug { "Existing read list '${existing.name}' already contains a book at position $numberInList, adding book '${book.name}' at the end" }
|
||||
existing.bookIds.lastKey() + 1
|
||||
} else {
|
||||
|
|
@ -150,7 +155,8 @@ class ReadListLifecycle(
|
|||
return it.thumbnail
|
||||
}
|
||||
|
||||
val ids = with(mutableListOf<String>()) {
|
||||
val ids =
|
||||
with(mutableListOf<String>()) {
|
||||
while (size < 4) {
|
||||
this += readList.bookIds.values.take(4)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ class ReadListMatcher(
|
|||
logger.info { "Trying to match $request" }
|
||||
|
||||
val readListMatch =
|
||||
if (readListRepository.existsByName(request.name)) ReadListMatch(request.name, "ERR_1009")
|
||||
else ReadListMatch(request.name)
|
||||
if (readListRepository.existsByName(request.name))
|
||||
ReadListMatch(request.name, "ERR_1009")
|
||||
else
|
||||
ReadListMatch(request.name)
|
||||
|
||||
val matches = readListRequestRepository.matchBookRequests(request.books)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ class SeriesCollectionLifecycle(
|
|||
private val eventPublisher: ApplicationEventPublisher,
|
||||
private val transactionTemplate: TransactionTemplate,
|
||||
) {
|
||||
|
||||
@Throws(
|
||||
DuplicateNameException::class,
|
||||
)
|
||||
|
|
@ -47,7 +46,8 @@ class SeriesCollectionLifecycle(
|
|||
fun updateCollection(toUpdate: SeriesCollection) {
|
||||
logger.info { "Update collection: $toUpdate" }
|
||||
|
||||
val existing = collectionRepository.findByIdOrNull(toUpdate.id)
|
||||
val existing =
|
||||
collectionRepository.findByIdOrNull(toUpdate.id)
|
||||
?: throw IllegalArgumentException("Cannot update collection that does not exist")
|
||||
|
||||
if (!existing.name.equals(toUpdate.name, true) && collectionRepository.existsByName(toUpdate.name))
|
||||
|
|
@ -71,12 +71,15 @@ class SeriesCollectionLifecycle(
|
|||
* Collection will be created if it doesn't exist.
|
||||
*/
|
||||
@Transactional
|
||||
fun addSeriesToCollection(collectionName: String, series: Series) {
|
||||
fun addSeriesToCollection(
|
||||
collectionName: String,
|
||||
series: Series,
|
||||
) {
|
||||
collectionRepository.findByNameOrNull(collectionName).let { existing ->
|
||||
if (existing != null) {
|
||||
if (existing.seriesIds.contains(series.id))
|
||||
if (existing.seriesIds.contains(series.id)) {
|
||||
logger.debug { "Series is already in existing collection '${existing.name}'" }
|
||||
else {
|
||||
} else {
|
||||
logger.debug { "Adding series '${series.name}' to existing collection '${existing.name}'" }
|
||||
updateCollection(
|
||||
existing.copy(seriesIds = existing.seriesIds + series.id),
|
||||
|
|
@ -133,12 +136,16 @@ class SeriesCollectionLifecycle(
|
|||
fun getThumbnailBytes(thumbnailId: String): ByteArray? =
|
||||
thumbnailSeriesCollectionRepository.findByIdOrNull(thumbnailId)?.thumbnail
|
||||
|
||||
fun getThumbnailBytes(collection: SeriesCollection, userId: String): ByteArray {
|
||||
fun getThumbnailBytes(
|
||||
collection: SeriesCollection,
|
||||
userId: String,
|
||||
): ByteArray {
|
||||
thumbnailSeriesCollectionRepository.findSelectedByCollectionIdOrNull(collection.id)?.let {
|
||||
return it.thumbnail
|
||||
}
|
||||
|
||||
val ids = with(mutableListOf<String>()) {
|
||||
val ids =
|
||||
with(mutableListOf<String>()) {
|
||||
while (size < 4) {
|
||||
this += collection.seriesIds.take(4)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ class SeriesLifecycle(
|
|||
private val transactionTemplate: TransactionTemplate,
|
||||
private val historicalEventRepository: HistoricalEventRepository,
|
||||
) {
|
||||
|
||||
private val whitespacePattern = """\s+""".toRegex()
|
||||
|
||||
fun sortBooks(series: Series) {
|
||||
|
|
@ -73,7 +72,8 @@ class SeriesLifecycle(
|
|||
logger.debug { "Existing books: $books" }
|
||||
logger.debug { "Existing metadata: $metadatas" }
|
||||
|
||||
val sorted = books
|
||||
val sorted =
|
||||
books
|
||||
.sortedWith(
|
||||
compareBy(natSortComparator) {
|
||||
it.name
|
||||
|
|
@ -89,9 +89,12 @@ class SeriesLifecycle(
|
|||
sorted.mapIndexed { index, (book, _) -> book.copy(number = index + 1) },
|
||||
)
|
||||
|
||||
val oldToNew = sorted.mapIndexedNotNull { index, (book, metadata) ->
|
||||
if (metadata.numberLock && metadata.numberSortLock) null
|
||||
else Triple(
|
||||
val oldToNew =
|
||||
sorted.mapIndexedNotNull { index, (book, metadata) ->
|
||||
if (metadata.numberLock && metadata.numberSortLock)
|
||||
null
|
||||
else
|
||||
Triple(
|
||||
book,
|
||||
metadata,
|
||||
metadata.copy(
|
||||
|
|
@ -116,7 +119,10 @@ class SeriesLifecycle(
|
|||
}
|
||||
}
|
||||
|
||||
fun addBooks(series: Series, booksToAdd: Collection<Book>) {
|
||||
fun addBooks(
|
||||
series: Series,
|
||||
booksToAdd: Collection<Book>,
|
||||
) {
|
||||
booksToAdd.forEach {
|
||||
check(it.libraryId == series.libraryId) { "Cannot add book to series if they don't share the same libraryId" }
|
||||
}
|
||||
|
|
@ -197,13 +203,18 @@ class SeriesLifecycle(
|
|||
series.forEach { eventPublisher.publishEvent(DomainEvent.SeriesDeleted(it)) }
|
||||
}
|
||||
|
||||
fun markReadProgressCompleted(seriesId: String, user: KomgaUser) {
|
||||
val bookIds = bookRepository.findAllIdsBySeriesId(seriesId)
|
||||
fun markReadProgressCompleted(
|
||||
seriesId: String,
|
||||
user: KomgaUser,
|
||||
) {
|
||||
val bookIds =
|
||||
bookRepository.findAllIdsBySeriesId(seriesId)
|
||||
.filter { bookId ->
|
||||
val readProgress = readProgressRepository.findByBookIdAndUserIdOrNull(bookId, user.id)
|
||||
readProgress == null || !readProgress.completed
|
||||
}
|
||||
val progresses = mediaRepository.getPagesSizes(bookIds)
|
||||
val progresses =
|
||||
mediaRepository.getPagesSizes(bookIds)
|
||||
.map { (bookId, pageSize) -> ReadProgress(bookId, user.id, pageSize, true) }
|
||||
|
||||
readProgressRepository.save(progresses)
|
||||
|
|
@ -211,7 +222,10 @@ class SeriesLifecycle(
|
|||
eventPublisher.publishEvent(DomainEvent.ReadProgressSeriesChanged(seriesId, user.id))
|
||||
}
|
||||
|
||||
fun deleteReadProgress(seriesId: String, user: KomgaUser) {
|
||||
fun deleteReadProgress(
|
||||
seriesId: String,
|
||||
user: KomgaUser,
|
||||
) {
|
||||
val bookIds = bookRepository.findAllIdsBySeriesId(seriesId)
|
||||
val progresses = readProgressRepository.findAllByBookIdsAndUserId(bookIds, user.id)
|
||||
readProgressRepository.deleteByBookIdsAndUserId(bookIds, user.id)
|
||||
|
|
@ -243,18 +257,26 @@ class SeriesLifecycle(
|
|||
getBytesFromThumbnailSeries(it)
|
||||
}
|
||||
|
||||
fun getThumbnailBytes(seriesId: String, userId: String): ByteArray? {
|
||||
fun getThumbnailBytes(
|
||||
seriesId: String,
|
||||
userId: String,
|
||||
): ByteArray? {
|
||||
getSelectedThumbnail(seriesId)?.let {
|
||||
return getBytesFromThumbnailSeries(it)
|
||||
}
|
||||
|
||||
seriesRepository.findByIdOrNull(seriesId)?.let { series ->
|
||||
val bookId = when (libraryRepository.findById(series.libraryId).seriesCover) {
|
||||
val bookId =
|
||||
when (libraryRepository.findById(series.libraryId).seriesCover) {
|
||||
Library.SeriesCover.FIRST -> bookRepository.findFirstIdInSeriesOrNull(seriesId)
|
||||
Library.SeriesCover.FIRST_UNREAD_OR_FIRST -> bookRepository.findFirstUnreadIdInSeriesOrNull(seriesId, userId)
|
||||
Library.SeriesCover.FIRST_UNREAD_OR_FIRST ->
|
||||
bookRepository.findFirstUnreadIdInSeriesOrNull(seriesId, userId)
|
||||
?: bookRepository.findFirstIdInSeriesOrNull(seriesId)
|
||||
Library.SeriesCover.FIRST_UNREAD_OR_LAST -> bookRepository.findFirstUnreadIdInSeriesOrNull(seriesId, userId)
|
||||
|
||||
Library.SeriesCover.FIRST_UNREAD_OR_LAST ->
|
||||
bookRepository.findFirstUnreadIdInSeriesOrNull(seriesId, userId)
|
||||
?: bookRepository.findLastIdInSeriesOrNull(seriesId)
|
||||
|
||||
Library.SeriesCover.LAST -> bookRepository.findLastIdInSeriesOrNull(seriesId)
|
||||
}
|
||||
if (bookId != null) return bookLifecycle.getThumbnailBytes(bookId)?.bytes
|
||||
|
|
@ -263,7 +285,10 @@ class SeriesLifecycle(
|
|||
return null
|
||||
}
|
||||
|
||||
fun addThumbnailForSeries(thumbnail: ThumbnailSeries, markSelected: MarkSelectedPreference): ThumbnailSeries {
|
||||
fun addThumbnailForSeries(
|
||||
thumbnail: ThumbnailSeries,
|
||||
markSelected: MarkSelectedPreference,
|
||||
): ThumbnailSeries {
|
||||
// delete existing thumbnail with the same url
|
||||
if (thumbnail.url != null) {
|
||||
thumbnailsSeriesRepository.findAllBySeriesId(thumbnail.seriesId)
|
||||
|
|
@ -274,11 +299,13 @@ class SeriesLifecycle(
|
|||
}
|
||||
thumbnailsSeriesRepository.insert(thumbnail.copy(selected = false))
|
||||
|
||||
val selected = when (markSelected) {
|
||||
val selected =
|
||||
when (markSelected) {
|
||||
MarkSelectedPreference.YES -> true
|
||||
MarkSelectedPreference.IF_NONE_OR_GENERATED -> {
|
||||
thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(thumbnail.seriesId) == null
|
||||
}
|
||||
|
||||
MarkSelectedPreference.NO -> false
|
||||
}
|
||||
|
||||
|
|
@ -299,7 +326,8 @@ class SeriesLifecycle(
|
|||
if (series.path.notExists()) return logger.info { "Cannot delete series folder, path does not exist: ${series.path}" }
|
||||
if (!series.path.isWritable()) return logger.info { "Cannot delete series folder, path is not writable: ${series.path}" }
|
||||
|
||||
val thumbnails = thumbnailsSeriesRepository.findAllBySeriesIdIdAndType(series.id, ThumbnailSeries.Type.SIDECAR)
|
||||
val thumbnails =
|
||||
thumbnailsSeriesRepository.findAllBySeriesIdIdAndType(series.id, ThumbnailSeries.Type.SIDECAR)
|
||||
.mapNotNull { it.url?.toURI()?.toPath() }
|
||||
.filter { it.exists() && it.isWritable() }
|
||||
|
||||
|
|
@ -320,13 +348,16 @@ class SeriesLifecycle(
|
|||
|
||||
private fun thumbnailsHouseKeeping(seriesId: String) {
|
||||
logger.info { "House keeping thumbnails for series: $seriesId" }
|
||||
val all = thumbnailsSeriesRepository.findAllBySeriesId(seriesId)
|
||||
val all =
|
||||
thumbnailsSeriesRepository.findAllBySeriesId(seriesId)
|
||||
.mapNotNull {
|
||||
if (!it.exists()) {
|
||||
logger.warn { "Thumbnail doesn't exist, removing entry" }
|
||||
thumbnailsSeriesRepository.delete(it.id)
|
||||
null
|
||||
} else it
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
val selected = all.filter { it.selected }
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ class SeriesMetadataLifecycle(
|
|||
private val collectionLifecycle: SeriesCollectionLifecycle,
|
||||
private val eventPublisher: ApplicationEventPublisher,
|
||||
) {
|
||||
|
||||
fun refreshMetadata(series: Series) {
|
||||
logger.info { "Refresh metadata for series: $series" }
|
||||
|
||||
|
|
@ -49,7 +48,8 @@ class SeriesMetadataLifecycle(
|
|||
|
||||
else -> {
|
||||
logger.debug { "Provider: ${provider.javaClass.simpleName}" }
|
||||
val patches = bookRepository.findAllBySeriesId(series.id)
|
||||
val patches =
|
||||
bookRepository.findAllBySeriesId(series.id)
|
||||
.mapNotNull { book ->
|
||||
try {
|
||||
provider.getSeriesMetadataFromBook(BookWithMedia(book, mediaRepository.findById(book.id)), library.importComicInfoSeriesAppendVolume)
|
||||
|
|
@ -79,7 +79,8 @@ class SeriesMetadataLifecycle(
|
|||
logger.info { "Library is not set to import series metadata for this provider, skipping: ${provider.javaClass.simpleName}" }
|
||||
else -> {
|
||||
logger.debug { "Provider: ${provider.javaClass.simpleName}" }
|
||||
val patch = try {
|
||||
val patch =
|
||||
try {
|
||||
provider.getSeriesMetadata(series)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Error while getting metadata from ${provider::class.simpleName} for series: $series" }
|
||||
|
|
@ -101,7 +102,8 @@ class SeriesMetadataLifecycle(
|
|||
patches: List<SeriesMetadataPatch>,
|
||||
series: Series,
|
||||
) {
|
||||
val aggregatedPatch = SeriesMetadataPatch(
|
||||
val aggregatedPatch =
|
||||
SeriesMetadataPatch(
|
||||
title = patches.mostFrequent { it.title },
|
||||
titleSort = patches.mostFrequent { it.titleSort },
|
||||
status = patches.mostFrequent { it.status },
|
||||
|
|
|
|||
|
|
@ -122,15 +122,19 @@ class ThumbnailLifecycle(
|
|||
copier: (T, ThumbnailMetadata) -> T,
|
||||
updater: (Collection<T>) -> Unit,
|
||||
): Boolean {
|
||||
val (result, duration) = measureTimedValue {
|
||||
val (result, duration) =
|
||||
measureTimedValue {
|
||||
val thumbs = fetcher(Pageable.ofSize(1000))
|
||||
logger.info { "Fetched ${thumbs.numberOfElements} ${clazz.simpleName} to fix, total: ${thumbs.totalElements}" }
|
||||
|
||||
val fixedThumbs = thumbs.mapNotNull {
|
||||
val fixedThumbs =
|
||||
thumbs.mapNotNull {
|
||||
try {
|
||||
val meta = supplier(it)
|
||||
if (meta == null) null
|
||||
else copier(it, meta)
|
||||
if (meta == null)
|
||||
null
|
||||
else
|
||||
copier(it, meta)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Could not fix thumbnail: $it" }
|
||||
null
|
||||
|
|
@ -159,5 +163,6 @@ class ThumbnailLifecycle(
|
|||
)
|
||||
|
||||
private data class Result(val processed: Int, val hasMore: Boolean)
|
||||
|
||||
private data class ThumbnailMetadata(val mediaType: String, val fileSize: Long, val dimension: Dimension)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ class TransientBookLifecycle(
|
|||
private val seriesMetadataProviders: List<SeriesMetadataFromBookProvider>,
|
||||
bookMetadataProviders: List<BookMetadataProvider>,
|
||||
) {
|
||||
|
||||
val bookMetadataProviders = bookMetadataProviders.filter { it.capabilities.contains(BookMetadataPatchCapability.NUMBER_SORT) }
|
||||
|
||||
fun scanAndPersist(filePath: String): List<TransientBook> {
|
||||
|
|
@ -60,7 +59,8 @@ class TransientBookLifecycle(
|
|||
fun getMetadata(transientBook: TransientBook): Pair<String?, Float?> {
|
||||
val bookWithMedia = transientBook.toBookWithMedia()
|
||||
val number = bookMetadataProviders.firstNotNullOfOrNull { it.getBookMetadataFromBook(bookWithMedia)?.numberSort }
|
||||
val series = seriesMetadataProviders
|
||||
val series =
|
||||
seriesMetadataProviders
|
||||
.flatMap {
|
||||
buildList {
|
||||
if (it.supportsAppendVolume) add(it.getSeriesMetadataFromBook(bookWithMedia, true)?.title)
|
||||
|
|
@ -77,11 +77,16 @@ class TransientBookLifecycle(
|
|||
MediaNotReadyException::class,
|
||||
IndexOutOfBoundsException::class,
|
||||
)
|
||||
fun getBookPage(transientBook: TransientBook, number: Int): TypedBytes {
|
||||
fun getBookPage(
|
||||
transientBook: TransientBook,
|
||||
number: Int,
|
||||
): TypedBytes {
|
||||
val pageContent = bookAnalyzer.getPageContent(transientBook.toBookWithMedia(), number)
|
||||
val pageMediaType =
|
||||
if (transientBook.media.profile == MediaProfile.PDF) pdfImageType.mediaType
|
||||
else transientBook.media.pages[number - 1].mediaType
|
||||
if (transientBook.media.profile == MediaProfile.PDF)
|
||||
pdfImageType.mediaType
|
||||
else
|
||||
transientBook.media.pages[number - 1].mediaType
|
||||
|
||||
return TypedBytes(pageContent, pageMediaType)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ private val logger = KotlinLogging.logger {}
|
|||
|
||||
@Service
|
||||
class TransientBookCache : TransientBookRepository {
|
||||
private val cache = Caffeine.newBuilder()
|
||||
private val cache =
|
||||
Caffeine.newBuilder()
|
||||
.expireAfterAccess(1, TimeUnit.HOURS)
|
||||
.build<String, TransientBook>()
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import javax.sql.DataSource
|
|||
class DataSourcesConfiguration(
|
||||
private val komgaProperties: KomgaProperties,
|
||||
) {
|
||||
|
||||
@Bean("sqliteDataSource")
|
||||
@Primary
|
||||
fun sqliteDataSource(): DataSource =
|
||||
|
|
@ -28,13 +27,21 @@ class DataSourcesConfiguration(
|
|||
this.maximumPoolSize = 1
|
||||
}
|
||||
|
||||
private fun buildDataSource(poolName: String, dataSourceClass: Class<out SQLiteDataSource>, databaseProps: KomgaProperties.Database): HikariDataSource {
|
||||
val extraPragmas = databaseProps.pragmas.let {
|
||||
if (it.isEmpty()) ""
|
||||
else "?" + it.map { (key, value) -> "$key=$value" }.joinToString(separator = "&")
|
||||
private fun buildDataSource(
|
||||
poolName: String,
|
||||
dataSourceClass: Class<out SQLiteDataSource>,
|
||||
databaseProps: KomgaProperties.Database,
|
||||
): HikariDataSource {
|
||||
val extraPragmas =
|
||||
databaseProps.pragmas.let {
|
||||
if (it.isEmpty())
|
||||
""
|
||||
else
|
||||
"?" + it.map { (key, value) -> "$key=$value" }.joinToString(separator = "&")
|
||||
}
|
||||
|
||||
val dataSource = DataSourceBuilder.create()
|
||||
val dataSource =
|
||||
DataSourceBuilder.create()
|
||||
.driverClassName("org.sqlite.JDBC")
|
||||
.url("jdbc:sqlite:${databaseProps.file}$extraPragmas")
|
||||
.type(dataSourceClass)
|
||||
|
|
@ -47,9 +54,12 @@ class DataSourcesConfiguration(
|
|||
}
|
||||
|
||||
val poolSize =
|
||||
if (databaseProps.file.contains(":memory:") || databaseProps.file.contains("mode=memory")) 1
|
||||
else if (databaseProps.poolSize != null) databaseProps.poolSize!!
|
||||
else Runtime.getRuntime().availableProcessors().coerceAtMost(databaseProps.maxPoolSize)
|
||||
if (databaseProps.file.contains(":memory:") || databaseProps.file.contains("mode=memory"))
|
||||
1
|
||||
else if (databaseProps.poolSize != null)
|
||||
databaseProps.poolSize!!
|
||||
else
|
||||
Runtime.getRuntime().availableProcessors().coerceAtMost(databaseProps.maxPoolSize)
|
||||
|
||||
return HikariDataSource(
|
||||
HikariConfig().apply {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ class FlywaySecondaryMigrationInitializer(
|
|||
@Qualifier("tasksDataSource")
|
||||
private val tasksDataSource: DataSource,
|
||||
) : InitializingBean {
|
||||
|
||||
// by default Spring Boot will perform migration only on the @Primary datasource
|
||||
override fun afterPropertiesSet() {
|
||||
Flyway.configure()
|
||||
|
|
|
|||
|
|
@ -12,16 +12,18 @@ import java.sql.Connection
|
|||
private val log = KotlinLogging.logger {}
|
||||
|
||||
class SqliteUdfDataSource : SQLiteDataSource() {
|
||||
|
||||
companion object {
|
||||
const val udfStripAccents = "UDF_STRIP_ACCENTS"
|
||||
const val collationUnicode3 = "COLLATION_UNICODE_3"
|
||||
const val UDF_STRIP_ACCENTS = "UDF_STRIP_ACCENTS"
|
||||
const val COLLATION_UNICODE_3 = "COLLATION_UNICODE_3"
|
||||
}
|
||||
|
||||
override fun getConnection(): Connection =
|
||||
super.getConnection().also { addAllUdf(it as SQLiteConnection) }
|
||||
|
||||
override fun getConnection(username: String?, password: String?): SQLiteConnection =
|
||||
override fun getConnection(
|
||||
username: String?,
|
||||
password: String?,
|
||||
): SQLiteConnection =
|
||||
super.getConnection(username, password).also { addAllUdf(it) }
|
||||
|
||||
private fun addAllUdf(connection: SQLiteConnection) {
|
||||
|
|
@ -47,10 +49,10 @@ class SqliteUdfDataSource : SQLiteDataSource() {
|
|||
}
|
||||
|
||||
private fun createUdfStripAccents(connection: SQLiteConnection) {
|
||||
log.debug { "Adding custom $udfStripAccents function" }
|
||||
log.debug { "Adding custom $UDF_STRIP_ACCENTS function" }
|
||||
Function.create(
|
||||
connection,
|
||||
udfStripAccents,
|
||||
UDF_STRIP_ACCENTS,
|
||||
object : Function() {
|
||||
override fun xFunc() =
|
||||
when (val text = value_text(0)) {
|
||||
|
|
@ -62,17 +64,21 @@ class SqliteUdfDataSource : SQLiteDataSource() {
|
|||
}
|
||||
|
||||
private fun createUnicode3Collation(connection: SQLiteConnection) {
|
||||
log.debug { "Adding custom $collationUnicode3 collation" }
|
||||
log.debug { "Adding custom $COLLATION_UNICODE_3 collation" }
|
||||
Collation.create(
|
||||
connection,
|
||||
collationUnicode3,
|
||||
COLLATION_UNICODE_3,
|
||||
object : Collation() {
|
||||
val collator = Collator.getInstance().apply {
|
||||
val collator =
|
||||
Collator.getInstance().apply {
|
||||
strength = Collator.TERTIARY
|
||||
decomposition = Collator.CANONICAL_DECOMPOSITION
|
||||
}
|
||||
|
||||
override fun xCompare(str1: String, str2: String): Int = collator.compare(str1, str2)
|
||||
override fun xCompare(
|
||||
str1: String,
|
||||
str2: String,
|
||||
): Int = collator.compare(str1, str2)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ private const val SEED = 0
|
|||
|
||||
@Component
|
||||
class Hasher {
|
||||
|
||||
fun computeHash(path: Path): String {
|
||||
logger.debug { "Hashing: $path" }
|
||||
|
||||
|
|
@ -38,7 +37,8 @@ class Hasher {
|
|||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
private fun ByteArray.toHexString(): String = asUByteArray().joinToString("") {
|
||||
private fun ByteArray.toHexString(): String =
|
||||
asUByteArray().joinToString("") {
|
||||
it.toString(16).padStart(2, '0')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import org.springframework.context.annotation.Configuration
|
|||
|
||||
@Configuration
|
||||
class HttpExchangeConfiguration {
|
||||
|
||||
private val httpExchangeRepository = InMemoryHttpExchangeRepository()
|
||||
|
||||
@Bean
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ private val logger = KotlinLogging.logger {}
|
|||
|
||||
@Service
|
||||
class ImageAnalyzer {
|
||||
|
||||
/**
|
||||
* Returns the Dimension of the image contained in the stream.
|
||||
* The stream will not be closed, nor marked or reset.
|
||||
|
|
|
|||
|
|
@ -16,14 +16,13 @@ import kotlin.math.min
|
|||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private const val webpNightMonkeys = "com.github.gotson.nightmonkeys.webp.imageio.plugins.WebpImageReaderSpi"
|
||||
private const val WEBP_NIGHT_MONKEYS = "com.github.gotson.nightmonkeys.webp.imageio.plugins.WebpImageReaderSpi"
|
||||
|
||||
@Service
|
||||
class ImageConverter(
|
||||
private val imageAnalyzer: ImageAnalyzer,
|
||||
private val contentDetector: ContentDetector,
|
||||
) {
|
||||
|
||||
val supportedReadFormats by lazy { ImageIO.getReaderFormatNames().toList() }
|
||||
val supportedReadMediaTypes by lazy { ImageIO.getReaderMIMETypes().toList() }
|
||||
val supportedWriteFormats by lazy { ImageIO.getWriterFormatNames().toList() }
|
||||
|
|
@ -38,7 +37,8 @@ class ImageConverter(
|
|||
}
|
||||
|
||||
private fun chooseWebpReader() {
|
||||
val providers = IIORegistry.getDefaultInstance().getServiceProviders(
|
||||
val providers =
|
||||
IIORegistry.getDefaultInstance().getServiceProviders(
|
||||
ImageReaderSpi::class.java,
|
||||
{ it is ImageReaderSpi && it.mimeTypes.contains("image/webp") },
|
||||
false,
|
||||
|
|
@ -46,7 +46,7 @@ class ImageConverter(
|
|||
|
||||
if (providers.size > 1) {
|
||||
logger.debug { "WebP reader providers: ${providers.map { it.javaClass.canonicalName }}" }
|
||||
providers.firstOrNull { it.javaClass.canonicalName == webpNightMonkeys }?.let { nightMonkeys ->
|
||||
providers.firstOrNull { it.javaClass.canonicalName == WEBP_NIGHT_MONKEYS }?.let { nightMonkeys ->
|
||||
(providers - nightMonkeys).forEach {
|
||||
logger.debug { "Deregister provider: ${it.javaClass.canonicalName}" }
|
||||
IIORegistry.getDefaultInstance().deregisterServiceProvider(it)
|
||||
|
|
@ -57,16 +57,25 @@ class ImageConverter(
|
|||
|
||||
private val supportsTransparency = listOf("png")
|
||||
|
||||
fun canConvertMediaType(from: String, to: String) =
|
||||
fun canConvertMediaType(
|
||||
from: String,
|
||||
to: String,
|
||||
) =
|
||||
supportedReadMediaTypes.contains(from) && supportedWriteMediaTypes.contains(to)
|
||||
|
||||
fun convertImage(imageBytes: ByteArray, format: String): ByteArray =
|
||||
fun convertImage(
|
||||
imageBytes: ByteArray,
|
||||
format: String,
|
||||
): ByteArray =
|
||||
ByteArrayOutputStream().use { baos ->
|
||||
val image = ImageIO.read(imageBytes.inputStream())
|
||||
|
||||
val result = if (!supportsTransparency.contains(format) && containsAlphaChannel(image)) {
|
||||
if (containsTransparency(image)) logger.info { "Image contains alpha channel but is not opaque, visual artifacts may appear" }
|
||||
else logger.info { "Image contains alpha channel but is opaque, conversion should not generate any visual artifacts" }
|
||||
val result =
|
||||
if (!supportsTransparency.contains(format) && containsAlphaChannel(image)) {
|
||||
if (containsTransparency(image))
|
||||
logger.info { "Image contains alpha channel but is not opaque, visual artifacts may appear" }
|
||||
else
|
||||
logger.info { "Image contains alpha channel but is opaque, conversion should not generate any visual artifacts" }
|
||||
BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_RGB).also {
|
||||
it.createGraphics().drawImage(image, 0, 0, Color.WHITE, null)
|
||||
}
|
||||
|
|
@ -79,7 +88,11 @@ class ImageConverter(
|
|||
baos.toByteArray()
|
||||
}
|
||||
|
||||
fun resizeImageToByteArray(imageBytes: ByteArray, format: ImageType, size: Int): ByteArray {
|
||||
fun resizeImageToByteArray(
|
||||
imageBytes: ByteArray,
|
||||
format: ImageType,
|
||||
size: Int,
|
||||
): ByteArray {
|
||||
val builder = resizeImageBuilder(imageBytes, format, size) ?: return imageBytes
|
||||
|
||||
return ByteArrayOutputStream().use {
|
||||
|
|
@ -88,14 +101,23 @@ class ImageConverter(
|
|||
}
|
||||
}
|
||||
|
||||
fun resizeImageToBufferedImage(imageBytes: ByteArray, format: ImageType, size: Int): BufferedImage {
|
||||
fun resizeImageToBufferedImage(
|
||||
imageBytes: ByteArray,
|
||||
format: ImageType,
|
||||
size: Int,
|
||||
): BufferedImage {
|
||||
val builder = resizeImageBuilder(imageBytes, format, size) ?: return ImageIO.read(imageBytes.inputStream())
|
||||
|
||||
return builder.asBufferedImage()
|
||||
}
|
||||
|
||||
private fun resizeImageBuilder(imageBytes: ByteArray, format: ImageType, size: Int): Thumbnails.Builder<out InputStream>? {
|
||||
val longestEdge = imageAnalyzer.getDimension(imageBytes.inputStream())?.let {
|
||||
private fun resizeImageBuilder(
|
||||
imageBytes: ByteArray,
|
||||
format: ImageType,
|
||||
size: Int,
|
||||
): Thumbnails.Builder<out InputStream>? {
|
||||
val longestEdge =
|
||||
imageAnalyzer.getDimension(imageBytes.inputStream())?.let {
|
||||
val mediaType = contentDetector.detectMediaType(imageBytes.inputStream())
|
||||
val longestEdge = max(it.height, it.width)
|
||||
// don't resize if source and target format is the same, and source is smaller than desired
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import javax.sql.DataSource
|
|||
// as advised in https://docs.spring.io/spring-boot/docs/3.1.4/reference/htmlsingle/#howto.data-access.configure-jooq-with-multiple-datasources
|
||||
@Configuration
|
||||
class JooqConfiguration {
|
||||
|
||||
@Bean("dslContext")
|
||||
@Primary
|
||||
fun mainDslContext(
|
||||
|
|
@ -37,7 +36,12 @@ class JooqConfiguration {
|
|||
): DSLContext =
|
||||
createDslContext(dataSource, transactionProvider, executeListenerProviders)
|
||||
|
||||
private fun createDslContext(dataSource: DataSource, transactionProvider: ObjectProvider<TransactionProvider?>, executeListenerProviders: ObjectProvider<ExecuteListenerProvider?>) = DefaultDSLContext(
|
||||
private fun createDslContext(
|
||||
dataSource: DataSource,
|
||||
transactionProvider: ObjectProvider<TransactionProvider?>,
|
||||
executeListenerProviders: ObjectProvider<ExecuteListenerProvider?>,
|
||||
) =
|
||||
DefaultDSLContext(
|
||||
DefaultConfiguration().also { configuration ->
|
||||
configuration.set(SQLDialect.SQLITE)
|
||||
configuration.set(DataSourceConnectionProvider(TransactionAwareDataSourceProxy(dataSource)))
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import org.springframework.data.domain.Sort
|
|||
class UnpagedSorted(
|
||||
private val sort: Sort,
|
||||
) : Pageable {
|
||||
|
||||
override fun getPageNumber(): Int {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue