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