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"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="" /> <option name="gradleJvm" value="21" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
+4 -3
View File
@@ -7,10 +7,11 @@
<option name="jvmTarget" value="21" /> <option name="jvmTarget" value="21" />
</component> </component>
<component name="KotlinCommonCompilerArguments"> <component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.1" /> <option name="apiVersion" value="2.3" />
<option name="languageVersion" value="2.1" /> <option name="languageVersion" value="2.3" />
</component> </component>
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.10" /> <option name="externalSystemId" value="Gradle" />
<option name="version" value="2.3.0" />
</component> </component>
</project> </project>
+1 -20
View File
@@ -1,28 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration"> <component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" /> <file type="web" url="file://$PROJECT_DIR$" />
</component> </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" /> <output url="file://$PROJECT_DIR$/out" />
</component> </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> </project>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="SqlDialectMappings"> <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" /> <file url="PROJECT" dialect="SQLite" />
</component> </component>
</project> </project>
+13 -3
View File
@@ -2,6 +2,7 @@ plugins {
application application
kotlin("jvm") version masterLibs.versions.kotlinVersion kotlin("jvm") version masterLibs.versions.kotlinVersion
kotlin("plugin.serialization") version masterLibs.versions.kotlinVersion kotlin("plugin.serialization") version masterLibs.versions.kotlinVersion
id("org.graalvm.buildtools.native") version "0.11.2"
alias(masterLibs.plugins.versionPlugin) alias(masterLibs.plugins.versionPlugin)
} }
@@ -31,9 +32,10 @@ application {
group = "net.keinekekse" group = "net.keinekekse"
version = libs.versions.ebookcmdVersion.get() version = libs.versions.ebookcmdVersion.get()
description = "E-Book Command Line Tool" description = "Serien-EPUBs in die Calibre-Bibliothek importieren oder\\n" +
extra["displayname"] = "ebook cmd" "Metadaten von EPUB-Dateien anzeigen bzw. verändern."
extra["copyright"] = "Copyright (c) 2025 Hartmut Bauer" extra["displayname"] = "eBook CMD"
extra["copyright"] = "Copyright (c) 2025-2026 Hartmut Bauer"
repositories { repositories {
maven("https://nexus.keinekekse.net/repository/maven-public/") maven("https://nexus.keinekekse.net/repository/maven-public/")
@@ -59,3 +61,11 @@ kotlin {
languageVersion.set(JavaLanguageVersion.of(masterLibs.versions.javaVersion.get())) 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 #auto generated
#Sun Mar 09 18:07:43 CET 2025 #Sun Jan 18 02:02:40 CET 2026
build.number=115 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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
Vendored
+3 -6
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # 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 # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -205,15 +203,14 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # 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. # 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 # * 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. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.
Vendored
+1 -2
View File
@@ -70,11 +70,10 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @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 :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
+5 -1
View File
@@ -16,8 +16,12 @@ dependencyResolutionManagement {
} }
versionCatalogs { versionCatalogs {
// create("libs") {
// from(files("../gradle/libs.versions.toml"))
// }
create("masterLibs") { 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 package net.keinekekse
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.keinekekse.epub.getSeriesFromDb import net.keinekekse.args.CommandLine
import net.keinekekse.version.Version 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 //TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter. // 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") val res = object {}::class.java.getResource("/META-INF/MANIFEST.MF")
@@ -30,15 +32,162 @@ fun main() {
println("Hello, world!") println("Hello, world!")
*/ */
runBlocking {
val verLib = Version(Version::class.java)
println(verLib.infoString)
val ver = Version(VersionCheck::class.java)
println(ver.infoString)
println(getSeriesFromDb()) runBlocking {
println("#########") // testEpub()
println(getSeriesFromDb()) /*
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() } // 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.async
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.keinekekse.epub.Ebook import net.keinekekse.epub.Ebook
import net.keinekekse.epub.EbookMeta import net.keinekekse.epub.EbookMetaTypes
import net.keinekekse.epub.convertToSeriesList import net.keinekekse.epub.convertToSeriesList
import net.keinekekse.epub.getSeriesFromCalibre import net.keinekekse.epub.getSeriesFromCalibre
import net.keinekekse.exec.CommandExecutionException import net.keinekekse.exec.CommandExecutionException
import java.io.File import java.io.File
suspend fun testEpub() { suspend fun testEpub() {
// testEpubList() testEpubList()
// testEpubWrite() // testEpubWrite()
testSeries() // testSeries()
} }
suspend fun testSeries() { suspend fun testSeries() {
@@ -34,12 +34,11 @@ fun testEpubWrite() {
// val epub = "div/Kotlin_in_Action_Second_Edition.epub" // val epub = "div/Kotlin_in_Action_Second_Edition.epub"
try { try {
val ebook = Ebook(epub.path) val ebook = Ebook(epub.path)
printMeta(ebook.meta) printMeta(ebook)
if (ebook.meta.authors.contains("&") || ebook.meta["authorSort"].isBlank()) { if (ebook.getChangesFor(EbookMetaTypes.AUTHORS).isNotBlank()) {
println("*** author sort anpassen") println("*** author sort anpassen")
ebook.changedAuthors = ebook.meta.authors
ebook.setMeta() ebook.setMeta()
printMeta(ebook.meta) printMeta(ebook)
} }
} catch (e: CommandExecutionException) { } catch (e: CommandExecutionException) {
println("ERROR reading ${epub.path}:\n${e.message}") println("ERROR reading ${epub.path}:\n${e.message}")
@@ -57,22 +56,28 @@ fun testEpubList() {
for (epub in epubs) { for (epub in epubs) {
// println("EPUB: ${epub.path} - ${epub.absolutePath}") // println("EPUB: ${epub.path} - ${epub.absolutePath}")
try { try {
printMeta(Ebook(epub.path).meta) printMeta(Ebook(epub.path))
} catch (e: CommandExecutionException) { } catch (e: CommandExecutionException) {
println("ERROR reading ${epub.path}:\n${e.message}") println("\nERROR reading ${epub.path}:\n${e.message}")
} }
} }
} }
fun printMeta(meta: EbookMeta?) { fun printMeta(ebook: Ebook) {
if (meta == null) return println("------------------------")
println(meta.fileName) println(ebook.fileName)
println("TITLE: _${meta.title}_ SORT: _${meta["titleSort"]}_") // println("TITLE: _${meta[TITLE]}_ SORT: _${meta[TITLESORT]}_")
println("AUTHORS: _${meta.authors}_ SORT: _${meta["authorSort"]}_ ") // println("AUTHORS: _${meta[AUTHORS]}_ SORT: _${meta[AUTHORSORT]}_ ")
println("SERIES: _${meta.series}_ INDEX: _${meta.seriesIndex}_") // println("SERIES: _${meta[SERIES]}_ INDEX: _${meta[SERIESINDEX]}_")
println("PUBLISHED: _${meta["pubDate"]}_") // println("PUBLISHED: _${meta[PUBDATE]}_")
println("LANG: _${meta["lang"]}_") // println("LANG: _${meta[LANG]}_")
println("ISBN: _${meta["isbn"]}_") // 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 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.CommandExecutionException
import net.keinekekse.exec.ExecutionResult import net.keinekekse.exec.ExecutionResult
import java.io.File
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -9,23 +12,19 @@ import java.time.format.DateTimeFormatter
* Represents an ebook file. * Represents an ebook file.
* *
* @property fileName the file name of the ebook * @property fileName the file name of the ebook
* @property meta the metadata of the ebook as [EbookMeta] * @property metadata the metadata of the ebook as [Map]
* @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
* *
* @throws CommandExecutionException if the ebook metadata cannot be read or written * @throws CommandExecutionException if the ebook metadata cannot be read or written
*/ */
class Ebook(fileName: String) { class Ebook(var fileName: String) {
var meta: EbookMeta = getEpubMeta(fileName)
var changedTitle: String = "" private val changedMetadata: MutableMap<EbookMetaTypes, String> =
var changedTitleSort: String = "" entries.filter { it.setCmd.isNotBlank() }
var changedAuthors: String = "" .associateWith { "" } as MutableMap<EbookMetaTypes, String>
var changedSeries: String = ""
var changedSeriesIndex: String = "" var metadata: Map<EbookMetaTypes, String> = getEbookMeta(fileName)
val name
get() = File(fileName).nameWithoutExtension
/** /**
* Writes the changed metadata to the ebook file. * 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 * @throws CommandExecutionException if the ebook metadata cannot be written or read
*/ */
fun setMeta() { fun setMeta() {
changedTitle.takeIf { it.isNotBlank() }?.let { val changes = changedMetadata.filterValues { it.isNotBlank() }
meta = setEpubMeta(meta.fileName, metaDataItems["title"]!!.setCmd!!, it) if (changes.isEmpty()) return
changedTitle = ""
val commandline = listOf(
"ebook-meta",
*changes.flatMap { listOf(it.key.setCmd, it.value) }.toTypedArray()
).toTypedArray()
metadata = setEbookMeta(fileName, *commandline)
} }
changedTitleSort.takeIf { it.isNotBlank() }?.let {
meta = setEpubMeta(meta.fileName, metaDataItems["titleSort"]!!.setCmd!!, it) /**
changedTitleSort = "" * 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
} }
changedAuthors.takeIf { it.isNotBlank() }?.let {
meta = setEpubMeta(meta.fileName, metaDataItems["authors"]!!.setCmd!!, it) override fun toString(): String {
changedAuthors = "" val HEADING = "\n=== %s\n"
} val CHANGED_SECTION = "*** Changed Metadata:\n"
changedSeries.takeIf { it.isNotBlank() }?.let {
meta = setEpubMeta(meta.fileName, metaDataItems["series"]!!.setCmd!!, it) fun formatMetadataItem(key: String, value: String, width: Int): String =
changedSeries = "" "${key.padEnd(width)}$value\n"
}
changedSeriesIndex.takeIf { it.isNotBlank() }?.let { fun getChangedMetadataItems(width: Int): List<String> = buildList {
meta = setEpubMeta(meta.fileName, metaDataItems["seriesIndex"]!!.setCmd!!, it) changedMetadata.forEach { (key, value) ->
changedSeriesIndex = "" 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")
}
} }
data class EbookMeta(val fileName: String) { /**
private val metadata = mutableMapOf<String, String>() * Enum class representing various metadata types for an eBook.
*
operator fun get(key: String): String = metadata[key] ?: "" * @property displayName The human-readable name of the metadata type.
* @property regex A string pattern used to identify the metadata type in raw data.
operator fun set(key: String, value: String) { * @property setCmd Optional command string used when setting this metadata type programmatically.
metadata[key] = value * @property extractData Function to extract and transform the metadata value from a raw string.
} */
enum class EbookMetaTypes(
var title by metadata
var authors by metadata
var series by metadata
var seriesIndex by metadata
}
class EbookMetaItem(
val displayName: String, val displayName: String,
val regex: String, val regex: String,
val setCmd: String? = null, val setCmd: String = "",
val extractData: (String) -> String = { it }, val extractData: (String) -> String = { it },
) ) {
TITLE("Title", """title""", "--title"),
val metaDataItems = mapOf( TITLE_SORT("Title Sort", """title sort""", "--title-sort"),
"title" to EbookMetaItem("Title", """title""", "--title"), AUTHORS("Author(s)", """author\(s\)""", "--authors", {
"titleSort" to EbookMetaItem("Title sort", """title sort""", "--title-sort"), """(.*?)\s*(\[|$)""".toRegex().find(it)?.groupValues?.get(1) ?: ""
"authors" to EbookMetaItem("Author(s)", """author\(s\)""", "--authors") { }),
"""(.*?)\s*(\[|$)""".toRegex().find(it)?.groupValues?.get(1)?.replace(",", " &") ?: "" AUTHORS_SORT("Author(s) Sort", """author\(s\)""", "", {
},
"authorSort" to EbookMetaItem("Author sort", """author\(s\)""", "--author-sort") {
""".*\[(.*)]""".toRegex().find(it)?.groupValues?.get(1) ?: "" """.*\[(.*)]""".toRegex().find(it)?.groupValues?.get(1) ?: ""
}, }),
"series" to EbookMetaItem("Series", """series""", "--series") { SERIES("Series", """series""", "--series", {
"""(.*?)\s*(#|$)""".toRegex().find(it)?.groupValues?.get(1) ?: "" """(.*?)\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) ?: "" val strSer = """#(\d*)\s*""".toRegex().find(it)?.groupValues?.get(1) ?: ""
strSer.toIntOrNull()?.toString() ?: "" strSer.toIntOrNull()?.toString() ?: ""
}, }),
"lang" to EbookMetaItem("Language(s)", """languages"""), LANG("Language(s)", """languages"""),
"pubDate" to EbookMetaItem("Publication date", """published""") { PUBLICATION_DATE("Publication Date", """published""", "", {
try { try {
LocalDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME) LocalDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME)
.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) .format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
} catch (_: Exception) { } catch (_: Exception) {
"" ""
} }
}, }),
"isbn" to EbookMetaItem("ISBN", """identifiers""") { ISBN("ISBN", """identifiers""", "", {
"""isbn:(\d*)""".toRegex().find(it)?.groupValues?.get(1) ?: "" """isbn:(\d*)""".toRegex().find(it)?.groupValues?.get(1) ?: ""
}) });
private val reParam = setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE) fun getEpubMetaFromDump(dump: String): String {
val reParam = setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)
private fun EbookMetaItem.getEpubMetaFromDump(dump: String) = return extractData(
extractData("""\s*$regex\s+:\s+(.*)""".toRegex(reParam).find(dump)?.groupValues?.get(1) ?: "") """\s*$regex\s+:\s+(.*)""".toRegex(reParam).find(dump)?.groupValues?.get(
1
) ?: ""
)
}
}
// Suchbegriffe: // Suchbegriffe:
// - Title // - Title
@@ -126,17 +183,3 @@ private fun EbookMetaItem.getEpubMetaFromDump(dump: String) =
// Identifiers : isbn:9780708899403, mobi-asin:B07K23HGTW // Identifiers : isbn:9780708899403, mobi-asin:B07K23HGTW
// Tags // Tags
// Comments // 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