div Test-Iterationen

This commit is contained in:
Hartmut Bauer committed 2026-01-18 02:19:19 +01:00
1 parent 9008074a17
commit c7d0e4170b
20 files changed
+754 -227

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="" />
<option name="gradleJvm" value="21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
+4 -3
View File
@@ -7,10 +7,11 @@
<option name="jvmTarget" value="21" />
</component>
<component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.1" />
<option name="languageVersion" value="2.1" />
<option name="apiVersion" value="2.3" />
<option name="languageVersion" value="2.3" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.10" />
<option name="externalSystemId" value="Gradle" />
<option name="version" value="2.3.0" />
</component>
</project>
+1 -20
View File
@@ -1,28 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="ms-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="ScriptDefinitionSettings">
<option name="settings">
<list>
<ScriptDefinitionSetting>
<option name="definitionId" value="org.jetbrains.kotlin.mainKts.MainKtsScript" />
</ScriptDefinitionSetting>
<ScriptDefinitionSetting>
<option name="definitionId" value="kotlin.Any" />
</ScriptDefinitionSetting>
<ScriptDefinitionSetting>
<option name="definitionId" value="kotlin.Any" />
</ScriptDefinitionSetting>
</list>
</option>
</component>
<component name="TaskProjectConfiguration">
<server type="Redmine" url="https://redmine.keinekekse.net" />
</component>
</project>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/src/main/kotlin/net/keinekekse/epub/calibre.kt" dialect="GenericSQL" />
<file url="file://$PROJECT_DIR$/src/main/kotlin/net/keinekekse/epub/calibredb.kt" dialect="GenericSQL" />
<file url="PROJECT" dialect="SQLite" />
</component>
</project>
+13 -3
View File
@@ -2,6 +2,7 @@ plugins {
application
kotlin("jvm") version masterLibs.versions.kotlinVersion
kotlin("plugin.serialization") version masterLibs.versions.kotlinVersion
id("org.graalvm.buildtools.native") version "0.11.2"
alias(masterLibs.plugins.versionPlugin)
}
@@ -31,9 +32,10 @@ application {
group = "net.keinekekse"
version = libs.versions.ebookcmdVersion.get()
description = "E-Book Command Line Tool"
extra["displayname"] = "ebook cmd"
extra["copyright"] = "Copyright (c) 2025 Hartmut Bauer"
description = "Serien-EPUBs in die Calibre-Bibliothek importieren oder\\n" +
"Metadaten von EPUB-Dateien anzeigen bzw. verändern."
extra["displayname"] = "eBook CMD"
extra["copyright"] = "Copyright (c) 2025-2026 Hartmut Bauer"
repositories {
maven("https://nexus.keinekekse.net/repository/maven-public/")
@@ -59,3 +61,11 @@ kotlin {
languageVersion.set(JavaLanguageVersion.of(masterLibs.versions.javaVersion.get()))
}
}
graalvmNative {
binaries {
named("main") {
buildArgs.add("--initialize-at-build-time")
}
}
}
+2 -2
View File
@@ -1,3 +1,3 @@
#auto generated
#Sun Mar 09 18:07:43 CET 2025
build.number=115
#Sun Jan 18 02:02:40 CET 2026
build.number=258
+1
View File
@@ -0,0 +1 @@
../../gradle/version-catalog/gradle/libs.versions.toml
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+3 -6
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -205,15 +203,14 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
Vendored
+1 -2
View File
@@ -70,11 +70,10 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
+5 -1
View File
@@ -16,8 +16,12 @@ dependencyResolutionManagement {
}
versionCatalogs {
// create("libs") {
// from(files("../gradle/libs.versions.toml"))
// }
create("masterLibs") {
from("net.keinekekse:version-catalog:56")
from(files("gradle/masterlibs.versions.toml"))
// from("net.keinekekse:version-catalog:98")
}
}
}
+171 -12
View File
@@ -1,12 +1,14 @@
package net.keinekekse
import kotlinx.coroutines.runBlocking
import net.keinekekse.epub.getSeriesFromDb
import net.keinekekse.version.Version
import net.keinekekse.args.CommandLine
import net.keinekekse.config.AbstractInitConfig
import net.keinekekse.ebookcmd.VERSION
import net.keinekekse.epub.EbookAutoImport
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
fun main() {
fun main(args: Array<String>) {
/*
val res = object {}::class.java.getResource("/META-INF/MANIFEST.MF")
@@ -30,15 +32,162 @@ fun main() {
println("Hello, world!")
*/
runBlocking {
val verLib = Version(Version::class.java)
println(verLib.infoString)
val ver = Version(VersionCheck::class.java)
println(ver.infoString)
println(getSeriesFromDb())
println("#########")
println(getSeriesFromDb())
runBlocking {
// testEpub()
/*
cmd line:
ohne -> auto mit Bestätigung
only file -> metadata
--auto -> auto ohne Bestätigung
--authors, --author_sort, --title, --series, --series_index + file-> chg
--sync -> nur calibre sync
*/
val commandLine = CommandLine(
VERSION,
remainingDescription = "E-Books"
)
commandLine.setExecution { additionalParameters ->
// noop
}
val cmdImport =
commandLine.addSubCommand("import", "Serien-EPUBs importieren.", listOf("imp"))
val auto by cmdImport.addBooleanParameter(
"auto",
'a',
"Ohne Rückfrage importieren.",
false
)
cmdImport.setExecution { additionalParameters ->
// TODO err if additionalParameters.isNotEmpty()
println("import auto=${auto}")
val ebookAutoImport = EbookAutoImport()
val newEbooks = ebookAutoImport.checkEbookFiles {
println("auto: ${it.autoUpdate} - ${it.ebook.fileName} / ${it.seriesPath}")
}
if (auto == true) {
for (newEbook in newEbooks) {
// if (ebookAutoImport.import(newEbook))
println("Imported ${newEbook.ebook.name}.")
}
} else {
newEbooks.filter { it.autoUpdate }.forEach {
println(it.ebook)
println("Importieren? (y/n)")
val yesno = readlnOrNull()
if (yesno?.lowercase() == "y") {
// if (ebookAutoImport.import(it))
println("Imported ${it.ebook.name}.")
}
}
}
}
val cmdSync =
commandLine.addSubCommand("sync", "Calibre-Bibliothek mit Calibre-Web synchronisieren.")
cmdSync.setExecution { additionalParameters ->
println("sync")
}
val cmdChange =
commandLine.addSubCommand("change", "Metadaten ändern.", listOf("chg", "meta"))
val authors by cmdChange.addStringParameter(
"authors",
null,
"Autor(en) eintragen oder ändern (Vorname Name [& Vorname Name]).",
false
)
val title by cmdChange.addStringParameter(
"title",
null,
"Titel eintragen oder ändern.",
false
)
val series by cmdChange.addStringParameter(
"series",
null,
"Serie eintragen oder ändern.",
false
)
val seriesIndex by cmdChange.addLongParameter(
"series-index",
null,
"Serien-Index eintragen oder ändern",
false
)
cmdChange.setExecution { additionalParameters ->
println("change meta data")
println("authors: ${authors} / title: ${title} / series: ${series} / nr: ${seriesIndex}")
// do chg
}
val cmdShow = commandLine.addSubCommand("show", "Metadaten anzeigen.")
cmdShow.setExecution { additionalParameters ->
if (additionalParameters.isNotEmpty()) {
println("show meta")
for (file in additionalParameters) {
println(" $file")
}
// show meta data
}
}
commandLine.parseAndRun(*args)
return@runBlocking
// TODO Bestätigung für Import
// ebookAutoImport.writeChanges(newEbooks)
// ebookAutoImport.importEbooksIntoCalibre(newEbooks)
// newEbooks.forEach {
// println("auto: ${it.autoUpdate} - ${it.changesWritten} - ${it.imported} - ${it.ebook.fileName}")
// }
/* --- cmd line ---
val arguments = CommandLineParser(
"""
${ver.infoString}
Metadaten von EPUB-Dateien anzeigen und bearbeiten.
usage: ebookcmd [options] <epub(s)>
""".trimIndent()
)
val name by arguments.stringOption(OptionConfig("name", 'n', "some name", true))
val bool by arguments.boolOption(OptionConfig("bool", 'b', "some boolean", false))
val test1 by arguments.stringOption(OptionConfig("test-one", 't', "test1", true))
val test2 by arguments.longOption(OptionConfig("int", 'i', "integer"))
val params by arguments.parametersList()
arguments.parse(args)
println("name: \"$name\"")
println("bool: $bool")
println("test-one: \"$test1\"")
println("int: $test2")
println("params: ${params?.joinToString(" ") { "\"$it\"" }}")
*/
// println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()))
// val series = getSeriesFromDb()
// series.forEach { serie ->
// println(serie.name)
// }
// println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()))
// println("#########")
// println(getSeriesFromDb())
// launch(Dispatchers.Default) { testEpub() }
//
@@ -51,4 +200,14 @@ fun main() {
}
data class VersionCheck(val dummy: String)
object Config : AbstractInitConfig(VERSION) {
// properties
const val CALIBRE_DB_CONNECT = "calibre.db.connect-string"
const val CALIBRE_DB_DRIVER = "calibre.db.driver"
const val CALIBRE_HOST = "calibre.host"
const val CALIBRE_PORT = "calibre.port"
const val CALIBRE_USER = "calibre.user"
const val CALIBRE_PWD = "calibre.pwd"
const val AUTO_IMPORT_CONFIG = "ebook.auto-import.config"
}
@@ -0,0 +1,18 @@
package net.keinekekse.ebookcmd
import net.keinekekse.version.Version
object VERSION: Version {
override val version = "0.2.0-beta+build.258"
override val fullVersion = "0.2.0-beta+build.258"
override val major = 0
override val minor = 2
override val patch = 0
override val preRelease = "beta"
override val buildMetadata = "build.258"
override val buildDate = "2026-01-18T01:02:40.808477Z"
override val displayName = "eBook CMD"
override val copyright = "Copyright (c) 2025-2026 Hartmut Bauer"
override val name = "ebookcmd"
override val description = "Serien-EPUBs in die Calibre-Bibliothek importieren oder\nMetadaten von EPUB-Dateien anzeigen bzw. verändern."
}
@@ -4,16 +4,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import net.keinekekse.epub.Ebook
import net.keinekekse.epub.EbookMeta
import net.keinekekse.epub.EbookMetaTypes
import net.keinekekse.epub.convertToSeriesList
import net.keinekekse.epub.getSeriesFromCalibre
import net.keinekekse.exec.CommandExecutionException
import java.io.File
suspend fun testEpub() {
// testEpubList()
testEpubList()
// testEpubWrite()
testSeries()
// testSeries()
}
suspend fun testSeries() {
@@ -34,12 +34,11 @@ fun testEpubWrite() {
// val epub = "div/Kotlin_in_Action_Second_Edition.epub"
try {
val ebook = Ebook(epub.path)
printMeta(ebook.meta)
if (ebook.meta.authors.contains("&") || ebook.meta["authorSort"].isBlank()) {
printMeta(ebook)
if (ebook.getChangesFor(EbookMetaTypes.AUTHORS).isNotBlank()) {
println("*** author sort anpassen")
ebook.changedAuthors = ebook.meta.authors
ebook.setMeta()
printMeta(ebook.meta)
printMeta(ebook)
}
} catch (e: CommandExecutionException) {
println("ERROR reading ${epub.path}:\n${e.message}")
@@ -57,22 +56,28 @@ fun testEpubList() {
for (epub in epubs) {
// println("EPUB: ${epub.path} - ${epub.absolutePath}")
try {
printMeta(Ebook(epub.path).meta)
printMeta(Ebook(epub.path))
} catch (e: CommandExecutionException) {
println("ERROR reading ${epub.path}:\n${e.message}")
println("\nERROR reading ${epub.path}:\n${e.message}")
}
}
}
fun printMeta(meta: EbookMeta?) {
if (meta == null) return
println(meta.fileName)
println("TITLE: _${meta.title}_ SORT: _${meta["titleSort"]}_")
println("AUTHORS: _${meta.authors}_ SORT: _${meta["authorSort"]}_ ")
println("SERIES: _${meta.series}_ INDEX: _${meta.seriesIndex}_")
println("PUBLISHED: _${meta["pubDate"]}_")
println("LANG: _${meta["lang"]}_")
println("ISBN: _${meta["isbn"]}_")
fun printMeta(ebook: Ebook) {
println("------------------------")
println(ebook.fileName)
// println("TITLE: _${meta[TITLE]}_ SORT: _${meta[TITLESORT]}_")
// println("AUTHORS: _${meta[AUTHORS]}_ SORT: _${meta[AUTHORSORT]}_ ")
// println("SERIES: _${meta[SERIES]}_ INDEX: _${meta[SERIESINDEX]}_")
// println("PUBLISHED: _${meta[PUBDATE]}_")
// println("LANG: _${meta[LANG]}_")
// println("ISBN: _${meta[ISBN]}_")
for ((key, value) in ebook.metadata) {
println("${key.displayName}: $value")
}
if (ebook.getChangesFor(EbookMetaTypes.AUTHORS).isNotBlank()) {
println("CHANGED AUTHORS: ${ebook.getChangesFor(EbookMetaTypes.AUTHORS)}")
}
}
+273
View File
@@ -0,0 +1,273 @@
package net.keinekekse.epub
import kotlinx.serialization.Serializable
import net.keinekekse.Config
import net.keinekekse.Config.AUTO_IMPORT_CONFIG
import net.keinekekse.config.JsonConfig
import java.io.File
// auto import
/*
def pr_zyklus(nr):
""" PR-Zyklus-Titel und Datei-Pfad für die Ablage.
"""
...
elif 3300 <= nr <= 3349:
return {"ser": "Perry Rhodan - 045_PHOENIX",
"path": "Serie/PerryRhodan/045_PHOENIX"}
""" Regeln für die Anpassung der Metadata von Ebooks der jeweiligen Serien.
identify_re + nr_identify_by beziehen sich auf den Dateinamen (identify*by = "name"),
sonst auf den Titel aus den Metadaten.
title_re bezieht sich immer auf den Titel im Ebook.
"""
series_config = [
{
"identify_by": "name", -> from file-name | meta-data.title
"identify_re": r"^(pr|perry[\s_]?rhodan)[\s_]?",
"nr_identify_by": "name", -> dto.
"nr_identify_re": r"^(pr|perry[\s_]?rhodan)[\s_]?(\d+)",
"title_re": r"(pr|perry.?rhodan)\s*(\d+)\s*\S\s+(.*)",
"title_re_nr": 3, -> nur eine Gruppe (Rest mit (?:...) )
"title_fmt": "PR{:04d} - {}", -> ???
"series_from_file": file name, -> pr-zyk
"ser" : series,
"path" : "Serie/...." -> einheitliche Pfade?
},
{
"identify_by": "name",
"identify_re": r"^kartanin \d",
"nr_identify_by": "name",
"nr_identify_re": r"^(kartanin)\s*(\d+)\s*",
"title_re": r"(pr kartanin)\s*(\d+)\s*\S\s*(.*)",
"title_re_nr": 3,
"title_fmt": "PR Kartanin {:02d} - {}",
"series": lambda nr: {"ser": "PRmini - 12 Kartanin", "path": "Serie/PRmini/12 Kartanin"}
},
*/
private enum class IdentifyBy {
FILE_NAME,
META_TITLE,
;
}
private enum class MainSeries {
NONE,
PR,
;
}
@Serializable
private data class AutoImport(
val newEbookPath: String, // einzulesende Ebooks
val ebookBackupPath: String, // ebooks backup root - originale Ebooks
val ebookLibraryPath: String, // ebooks root - bearbeitete Ebooks
val seriesConfig: List<SeriesConfig>,
val prZyklus: List<PRZyklus> = emptyList(),
)
@Serializable
private data class SeriesConfig(
val mainSeries: MainSeries = MainSeries.NONE,
val series: String,
val identifyBy: IdentifyBy = IdentifyBy.FILE_NAME,
val identifyRE: String,
val nrIdentifyBy: IdentifyBy = IdentifyBy.FILE_NAME,
val nrIdentifyRE: String,
val titleIdentifyBy: IdentifyBy = IdentifyBy.META_TITLE,
val titleIdentifyRE: String,
val titleFmt: String, // "%1$02d $2$Ss".format(nr, title)
val path: String,
)
@Serializable
private data class PRZyklus(
val begin: Int,
val end: Int,
val zyklus: String,
val path: String,
)
data class NewEbook(
val ebook: Ebook,
var seriesPath: String = "",
var autoUpdate: Boolean = false,
var changesWritten: Boolean = false,
var imported: Boolean = false,
)
//
// FIXME fürs Testen muss die .json jeweils nach ~/.config/ebookcmd kopiert werden!
//
class EbookAutoImport {
private val autoImport = getImportConfig()
// val newEbooks = checkEbookFiles()
private data class SeriesMetadata(
val title: String,
val seriesInfo: SeriesInfo,
val seriesNumber: Int,
)
private data class SeriesInfo(
val seriesName: String,
val seriesPath: String,
)
// TODO - Rückfrage: (y/n f. autoupdate) else (y/n if series) (else Roman/Sachbuch/no -> set series+nr)
// evtl. auto auch hier abhandeln
// --- sync Calibre library
// TODO
// --- import into Calibre
fun import(newEbook: NewEbook): Boolean {
if (newEbook.imported) return true
if (newEbook.autoUpdate) {
if (writeChanges(newEbook)) {
importEbookIntoCalibre(newEbook.ebook.fileName)
newEbook.imported = true
return true
}
}
return false
}
fun importIntoCalibre(newEbook: NewEbook): Boolean =
// TODO catch CommandExecutionException
if (newEbook.changesWritten) {
importEbookIntoCalibre(newEbook.ebook.fileName)
newEbook.imported = true
true
} else {
false
}
// --- move/copy epub & write new metadata
fun writeChanges(newEbook: NewEbook): Boolean =
// TODO catch exceptions
if (newEbook.autoUpdate) {
val backupPath = moveToBackup(newEbook)
copyToLibraryPath(backupPath, newEbook)
newEbook.ebook.setMeta()
newEbook.changesWritten = true
true
} else {
false
}
private fun moveToBackup(newEbook: NewEbook): File {
val backupPath = File(autoImport.ebookBackupPath + File.separator + newEbook.seriesPath)
if (!backupPath.exists()) backupPath.mkdirs()
val source = File(newEbook.ebook.fileName)
val target = File(backupPath, source.name)
return source.copyTo(target, overwrite = true).also { source.delete() }
}
private fun copyToLibraryPath(backupPath: File, newEbook: NewEbook) {
val libraryPath = File(autoImport.ebookLibraryPath + File.separator + newEbook.seriesPath)
if (!libraryPath.exists()) libraryPath.mkdirs()
val target = backupPath.copyTo(File(libraryPath, backupPath.name), overwrite = true)
newEbook.ebook.fileName = target.absolutePath
}
// --- check for new ebooks and fill the newEbooks array
fun checkEbookFiles(callback: (NewEbook) -> Unit = {}): List<NewEbook> {
val newEbooks: MutableList<NewEbook> = mutableListOf()
for (epub in findEbooks()) {
val ebook = Ebook(epub.path)
val newEbook = NewEbook(ebook).also { newEbooks.add(it) }
processEpubFile(ebook, newEbook)
callback(newEbook)
}
return newEbooks
}
private fun findEbooks() =
File(autoImport.newEbookPath).listFiles { it.extension == "epub" }?.toList() ?: emptyList()
private fun processEpubFile(ebook: Ebook, newEbook: NewEbook) {
autoImport.seriesConfig.forEach { seriesConfig ->
processSeriesConfig(seriesConfig, ebook)?.let {
applyMetadataChanges(ebook, it)
newEbook.autoUpdate = true
newEbook.seriesPath = it.seriesInfo.seriesPath
}
}
}
private fun processSeriesConfig(seriesConfig: SeriesConfig, ebook: Ebook): SeriesMetadata? {
// Check if the ebook matches the series configuration
val identifierValue = getValueToCheck(seriesConfig.identifyBy, ebook)
if (!matchRegex(seriesConfig.identifyRE, identifierValue)) return null
// Extract series number
val numberValue = getValueToCheck(seriesConfig.nrIdentifyBy, ebook)
val seriesNumber = findFirstGroup(seriesConfig.nrIdentifyRE, numberValue)?.toIntOrNull()
?: return null
// Extract and format title
val titleValue = getValueToCheck(seriesConfig.titleIdentifyBy, ebook)
val rawTitle = findFirstGroup(seriesConfig.titleIdentifyRE, titleValue) ?: return null
val formattedTitle = seriesConfig.titleFmt.format(seriesNumber, rawTitle)
// Get series information
val seriesInfo = getSeriesInfo(seriesConfig, seriesNumber) ?: return null
return SeriesMetadata(
title = formattedTitle,
seriesInfo = seriesInfo,
seriesNumber = seriesNumber
)
}
private fun getSeriesInfo(seriesConfig: SeriesConfig, number: Int): SeriesInfo? {
return when (seriesConfig.mainSeries) {
MainSeries.PR -> findPrZyklusInfo(number)
MainSeries.NONE -> SeriesInfo(seriesConfig.series, seriesConfig.path)
}
}
private fun findPrZyklusInfo(number: Int): SeriesInfo? {
return autoImport.prZyklus
.find { number in it.begin..it.end }
?.let { SeriesInfo(it.zyklus, it.path) }
}
private fun applyMetadataChanges(ebook: Ebook, metadata: SeriesMetadata) {
with(ebook) {
changeMetadata(EbookMetaTypes.SERIES, metadata.seriesInfo.seriesName)
changeMetadata(EbookMetaTypes.SERIES_INDEX, metadata.seriesNumber.toString())
changeMetadata(EbookMetaTypes.TITLE, metadata.title)
changeMetadata(EbookMetaTypes.TITLE_SORT, metadata.title)
}
}
private fun getValueToCheck(by: IdentifyBy, ebook: Ebook) = when (by) {
IdentifyBy.FILE_NAME -> ebook.name
IdentifyBy.META_TITLE -> ebook.metadata[EbookMetaTypes.TITLE] ?: ""
}
private fun matchRegex(re: String, check: String) =
re.toRegex(RegexOption.IGNORE_CASE).containsMatchIn(check)
private fun findFirstGroup(re: String, check: String) =
re.toRegex(RegexOption.IGNORE_CASE).find(check)?.let {
it.groups[1]?.value
}
private fun getImportConfig(): AutoImport =
(Config.configDirectory + File.separator + Config.userConfig[AUTO_IMPORT_CONFIG]).let {
JsonConfig.parseFromFile<AutoImport>(it)
}
}
@@ -1,72 +0,0 @@
package net.keinekekse.epub
import net.keinekekse.config.ConfigProperties
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.StdOutSqlLogger
import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.transactions.transaction
import java.sql.Connection
/*
DB
------
connect:
jdbc:sqlite:/Users/hb/Books.localized/Calibre Library/metadata.db
series, max_index, entry_count:
select name, max(series_index) as max, count(title) as cnt
from books, books_series_link, series
where books.id = books_series_link.book and books_series_link.series = series.id
group by series.name
*/
data class Series(val name: String, val max: Int, val count: Int)
object CalibreDb {
val db by lazy {
TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
Database.connect(
ConfigProperties.get("calibre.connect.string"),
driver = "org.sqlite.JDBC"
)
}
}
/*
Beispiel f. PostgeSQL
object PostgesqlDb {
val db by lazy {
Database.connect(
"jdbc:postgresql://postgres:5432/schlumpf",
driver = "org.postgresql.Driver",
user = "schlumpf",
password = "schlumpfpw"
)
}
}
*/
fun getSeriesFromDb(): MutableMap<Int, Series> {
return transaction(CalibreDb.db) {
var seriesList: MutableMap<Int, Series> = mutableMapOf()
addLogger(StdOutSqlLogger)
exec(
"select series.id, name, max(series_index) as max, count(title) as cnt " +
"from books, books_series_link, series " +
"where books.id = books_series_link.book and books_series_link.series = series.id " +
"group by series.name"
) { result ->
while (result.next()) {
seriesList.put(
result.getInt("id"),
Series(result.getString("name"), result.getInt("max"), result.getInt("cnt"))
)
}
}
seriesList
}
}
@@ -0,0 +1,107 @@
package net.keinekekse.epub
import net.keinekekse.Config
import net.keinekekse.Config.CALIBRE_DB_CONNECT
import net.keinekekse.Config.CALIBRE_DB_DRIVER
import net.keinekekse.Config.CALIBRE_HOST
import net.keinekekse.Config.CALIBRE_PORT
import net.keinekekse.Config.CALIBRE_PWD
import net.keinekekse.Config.CALIBRE_USER
import net.keinekekse.exec.CommandExecutionException
import net.keinekekse.exec.ExecutionResult
import net.keinekekse.util.isConnectionPossible
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.transactions.transaction
import java.sql.Connection
/*
DB
------
connect:
jdbc:sqlite:/Users/hb/Books.localized/Calibre Library/metadata.db
series, max_index, entry_count:
select name, max(series_index) as max, count(title) as cnt
from books, books_series_link, series
where books.id = books_series_link.book and books_series_link.series = series.id
group by series.name
*/
data class Series(val name: String, val max: Int, val count: Int)
object CalibreDb {
val db by lazy {
TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
// hier wird nur vorbereitet, Exception erst beim Zugriff
Database.connect(
url = Config.userConfig[CALIBRE_DB_CONNECT] ?: "",
driver = Config.userConfig[CALIBRE_DB_DRIVER] ?: "",
)
}
}
/*
Beispiel f. PostgeSQL
object PostgesqlDb {
val db by lazy {
Database.connect(
"jdbc:postgresql://postgres:5432/schlumpf",
driver = "org.postgresql.Driver",
user = "schlumpf",
password = "schlumpfpw"
)
}
}
*/
fun getSeriesFromDb(): List<Series> {
return transaction(CalibreDb.db) {
val seriesList: MutableList<Series> = mutableListOf()
// addLogger(StdOutSqlLogger)
exec(
"select series.id, name, max(series_index) as max, count(title) as cnt " +
"from books, books_series_link, series " +
"where books.id = books_series_link.book and books_series_link.series = series.id " +
"group by series.name"
) { result ->
while (result.next()) {
seriesList.add(
Series(result.getString("name"), result.getInt("max"), result.getInt("cnt"))
)
}
}
seriesList
}
}
fun importEbookIntoCalibre(fileName: String) {
val calibreHost = Config.userConfig[CALIBRE_HOST] ?: ""
val calibrePort = Config.userConfig[CALIBRE_PORT] ?: ""
if (!isConnectionPossible(calibreHost, calibrePort.toInt())) {
throw CommandExecutionException("Connection to Calibre DB failed.")
}
val commandline = listOf(
"calibredb",
"add",
fileName,
"--with-library=http://$calibreHost:$calibrePort/",
"--username",
Config.userConfig[CALIBRE_USER] ?: "",
"--password",
Config.userConfig[CALIBRE_PWD] ?: "",
)//.toTypedArray()
/*
(f"calibredb add \"{ebook}\" --with-library=\"http://{calibre_host}:{calibre_port}/\" "
f"--username={calibre_user} --password={calibre_pwd}")
*/
// ExecutionResult(listOf(*commandline)).apply {
ExecutionResult(commandline).apply {
if (stderr.isNotBlank()) throw CommandExecutionException("Error running calibredb:\n$stderr")
}
}
+127 -84
View File
@@ -1,7 +1,10 @@
package net.keinekekse.epub
import net.keinekekse.epub.EbookMetaTypes.AUTHORS
import net.keinekekse.epub.EbookMetaTypes.entries
import net.keinekekse.exec.CommandExecutionException
import net.keinekekse.exec.ExecutionResult
import java.io.File
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@@ -9,23 +12,19 @@ import java.time.format.DateTimeFormatter
* Represents an ebook file.
*
* @property fileName the file name of the ebook
* @property meta the metadata of the ebook as [EbookMeta]
* @property changedTitle the new title of the ebook
* @property changedTitleSort the new title sort of the ebook
* @property changedAuthors the new authors of the ebook
* @property changedSeries the new series of the ebook
* @property changedSeriesIndex the new series index of the ebook
* @property metadata the metadata of the ebook as [Map]
*
* @throws CommandExecutionException if the ebook metadata cannot be read or written
*/
class Ebook(fileName: String) {
var meta: EbookMeta = getEpubMeta(fileName)
class Ebook(var fileName: String) {
var changedTitle: String = ""
var changedTitleSort: String = ""
var changedAuthors: String = ""
var changedSeries: String = ""
var changedSeriesIndex: String = ""
private val changedMetadata: MutableMap<EbookMetaTypes, String> =
entries.filter { it.setCmd.isNotBlank() }
.associateWith { "" } as MutableMap<EbookMetaTypes, String>
var metadata: Map<EbookMetaTypes, String> = getEbookMeta(fileName)
val name
get() = File(fileName).nameWithoutExtension
/**
* Writes the changed metadata to the ebook file.
@@ -33,84 +32,142 @@ class Ebook(fileName: String) {
* @throws CommandExecutionException if the ebook metadata cannot be written or read
*/
fun setMeta() {
changedTitle.takeIf { it.isNotBlank() }?.let {
meta = setEpubMeta(meta.fileName, metaDataItems["title"]!!.setCmd!!, it)
changedTitle = ""
}
changedTitleSort.takeIf { it.isNotBlank() }?.let {
meta = setEpubMeta(meta.fileName, metaDataItems["titleSort"]!!.setCmd!!, it)
changedTitleSort = ""
}
changedAuthors.takeIf { it.isNotBlank() }?.let {
meta = setEpubMeta(meta.fileName, metaDataItems["authors"]!!.setCmd!!, it)
changedAuthors = ""
}
changedSeries.takeIf { it.isNotBlank() }?.let {
meta = setEpubMeta(meta.fileName, metaDataItems["series"]!!.setCmd!!, it)
changedSeries = ""
}
changedSeriesIndex.takeIf { it.isNotBlank() }?.let {
meta = setEpubMeta(meta.fileName, metaDataItems["seriesIndex"]!!.setCmd!!, it)
changedSeriesIndex = ""
}
}
}
val changes = changedMetadata.filterValues { it.isNotBlank() }
if (changes.isEmpty()) return
data class EbookMeta(val fileName: String) {
private val metadata = mutableMapOf<String, String>()
val commandline = listOf(
"ebook-meta",
*changes.flatMap { listOf(it.key.setCmd, it.value) }.toTypedArray()
).toTypedArray()
operator fun get(key: String): String = metadata[key] ?: ""
operator fun set(key: String, value: String) {
metadata[key] = value
metadata = setEbookMeta(fileName, *commandline)
}
var title by metadata
var authors by metadata
var series by metadata
var seriesIndex by metadata
/**
* Returns the changed entry for ```type``` or an empty string.
*/
fun getChangesFor(type: EbookMetaTypes): String = changedMetadata[type] ?: ""
/**
* Updates the metadata of the eBook with the specified type and value.
* Throws an exception if the specified metadata type cannot be changed.
*
* @param type The type of metadata to be changed, represented by the `EbookMetaTypes` enumeration.
* @param value The new value for the specified metadata type.
* @throws IllegalArgumentException if the specified metadata type cannot be changed.
*/
fun changeMetadata(type: EbookMetaTypes, value: String) {
if (type !in changedMetadata) throw IllegalArgumentException("Ebook $fileName: Metadata type $type cannot be changed.")
changedMetadata[type] = value
}
override fun toString(): String {
val HEADING = "\n=== %s\n"
val CHANGED_SECTION = "*** Changed Metadata:\n"
fun formatMetadataItem(key: String, value: String, width: Int): String =
"${key.padEnd(width)}$value\n"
fun getChangedMetadataItems(width: Int): List<String> = buildList {
changedMetadata.forEach { (key, value) ->
if (value.isNotBlank()) {
add(formatMetadataItem(key.displayName, value, width))
}
}
}
return buildString {
// Header section
append(HEADING.format(fileName))
// Metadata section
val labelWidth = metadata.keys.maxOf { it.displayName.length } + 2
metadata.forEach { (key, value) ->
append(formatMetadataItem(key.displayName + ":", value, labelWidth))
}
// Changed metadata section
val changedItems = getChangedMetadataItems(labelWidth)
if (changedItems.isNotEmpty()) {
append(CHANGED_SECTION)
changedItems.forEach { append(it) }
}
}
}
private fun getEbookMeta(ebookFile: String) = runEbookMeta(ebookFile).let { runResult ->
mutableMapOf<EbookMetaTypes, String>().apply {
EbookMetaTypes.entries.forEach {
this[it] = it.getEpubMetaFromDump(runResult.stdout)
}
this[AUTHORS]?.let {
if (it.contains(",")) {
changeMetadata(AUTHORS, it.replace(",", " &"))
}
}
}
}
private fun setEbookMeta(ebookFile: String, vararg cmd: String) =
runEbookMeta(ebookFile, *cmd).let { getEbookMeta(ebookFile) }
private fun runEbookMeta(vararg cmd: String) =
ExecutionResult(listOf("ebook-meta", *cmd)).apply {
if (stderr.isNotBlank()) throw CommandExecutionException("Error running ebook-meta:\n$stderr")
}
}
class EbookMetaItem(
/**
* Enum class representing various metadata types for an eBook.
*
* @property displayName The human-readable name of the metadata type.
* @property regex A string pattern used to identify the metadata type in raw data.
* @property setCmd Optional command string used when setting this metadata type programmatically.
* @property extractData Function to extract and transform the metadata value from a raw string.
*/
enum class EbookMetaTypes(
val displayName: String,
val regex: String,
val setCmd: String? = null,
val setCmd: String = "",
val extractData: (String) -> String = { it },
)
val metaDataItems = mapOf(
"title" to EbookMetaItem("Title", """title""", "--title"),
"titleSort" to EbookMetaItem("Title sort", """title sort""", "--title-sort"),
"authors" to EbookMetaItem("Author(s)", """author\(s\)""", "--authors") {
"""(.*?)\s*(\[|$)""".toRegex().find(it)?.groupValues?.get(1)?.replace(",", " &") ?: ""
},
"authorSort" to EbookMetaItem("Author sort", """author\(s\)""", "--author-sort") {
) {
TITLE("Title", """title""", "--title"),
TITLE_SORT("Title Sort", """title sort""", "--title-sort"),
AUTHORS("Author(s)", """author\(s\)""", "--authors", {
"""(.*?)\s*(\[|$)""".toRegex().find(it)?.groupValues?.get(1) ?: ""
}),
AUTHORS_SORT("Author(s) Sort", """author\(s\)""", "", {
""".*\[(.*)]""".toRegex().find(it)?.groupValues?.get(1) ?: ""
},
"series" to EbookMetaItem("Series", """series""", "--series") {
}),
SERIES("Series", """series""", "--series", {
"""(.*?)\s*(#|$)""".toRegex().find(it)?.groupValues?.get(1) ?: ""
},
"seriesIndex" to EbookMetaItem("Series index", """series""", "--index") {
}),
SERIES_INDEX("Series Index", """series""", "--index", {
val strSer = """#(\d*)\s*""".toRegex().find(it)?.groupValues?.get(1) ?: ""
strSer.toIntOrNull()?.toString() ?: ""
},
"lang" to EbookMetaItem("Language(s)", """languages"""),
"pubDate" to EbookMetaItem("Publication date", """published""") {
}),
LANG("Language(s)", """languages"""),
PUBLICATION_DATE("Publication Date", """published""", "", {
try {
LocalDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME)
.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
} catch (_: Exception) {
""
}
},
"isbn" to EbookMetaItem("ISBN", """identifiers""") {
}),
ISBN("ISBN", """identifiers""", "", {
"""isbn:(\d*)""".toRegex().find(it)?.groupValues?.get(1) ?: ""
})
});
private val reParam = setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)
private fun EbookMetaItem.getEpubMetaFromDump(dump: String) =
extractData("""\s*$regex\s+:\s+(.*)""".toRegex(reParam).find(dump)?.groupValues?.get(1) ?: "")
fun getEpubMetaFromDump(dump: String): String {
val reParam = setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)
return extractData(
"""\s*$regex\s+:\s+(.*)""".toRegex(reParam).find(dump)?.groupValues?.get(
1
) ?: ""
)
}
}
// Suchbegriffe:
// - Title
@@ -126,17 +183,3 @@ private fun EbookMetaItem.getEpubMetaFromDump(dump: String) =
// Identifiers : isbn:9780708899403, mobi-asin:B07K23HGTW
// Tags
// Comments
private fun getEpubMeta(epubFile: String) = runEbookMeta(epubFile).let { runResult ->
EbookMeta(epubFile).apply {
metaDataItems.forEach { (name, item) ->
this[name] = item.getEpubMetaFromDump(runResult.stdout)
}
}
}
private fun setEpubMeta(epubFile: String, cmd: String, text: String) =
runEbookMeta(epubFile, cmd, text).let { getEpubMeta(epubFile) }
private fun runEbookMeta(vararg cmd: String) = ExecutionResult(listOf("ebook-meta", *cmd)).apply {
if (stderr.isNotBlank()) throw CommandExecutionException("Error running ebook-meta:\n$stderr")
}
+2 -1
View File
@@ -1 +1,2 @@
calibre.connect.string=jdbc:sqlite:/Users/hb/Books.localized/Calibre Library/metadata.db
calibre.db.connect-string=jdbc:sqlite:/Users/hb/Books.localized/Calibre Library/metadata.db
calibre.db.driver=org.sqlite.JDBC