div Test-Iterationen
This commit is contained in:
1 parent
9008074a17
commit
c7d0e4170b
20 files changed
+748
-221
No files matched your search
Generated
+1
-1
@@ -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$" />
|
||||||
|
|||||||
Generated
+4
-3
@@ -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>
|
||||||
Generated
+1
-20
@@ -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>
|
||||||
Generated
+1
-1
@@ -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
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../gradle/version-catalog/gradle/libs.versions.toml
|
||||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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)}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
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)
|
return buildString {
|
||||||
changedSeriesIndex = ""
|
// 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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class EbookMeta(val fileName: String) {
|
private fun getEbookMeta(ebookFile: String) = runEbookMeta(ebookFile).let { runResult ->
|
||||||
private val metadata = mutableMapOf<String, String>()
|
mutableMapOf<EbookMetaTypes, String>().apply {
|
||||||
|
EbookMetaTypes.entries.forEach {
|
||||||
operator fun get(key: String): String = metadata[key] ?: ""
|
this[it] = it.getEpubMetaFromDump(runResult.stdout)
|
||||||
|
}
|
||||||
operator fun set(key: String, value: String) {
|
this[AUTHORS]?.let {
|
||||||
metadata[key] = value
|
if (it.contains(",")) {
|
||||||
|
changeMetadata(AUTHORS, it.replace(",", " &"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var title by metadata
|
private fun setEbookMeta(ebookFile: String, vararg cmd: String) =
|
||||||
var authors by metadata
|
runEbookMeta(ebookFile, *cmd).let { getEbookMeta(ebookFile) }
|
||||||
var series by metadata
|
|
||||||
var seriesIndex by metadata
|
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 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")
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user