div Test-Iterationen
This commit is contained in:
1 parent
9008074a17
commit
c7d0e4170b
20 files changed
+754
-227
No files matched your search
Generated
+1
-1
@@ -5,7 +5,7 @@
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="" />
|
||||
<option name="gradleJvm" value="21" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
||||
Generated
+4
-3
@@ -7,10 +7,11 @@
|
||||
<option name="jvmTarget" value="21" />
|
||||
</component>
|
||||
<component name="KotlinCommonCompilerArguments">
|
||||
<option name="apiVersion" value="2.1" />
|
||||
<option name="languageVersion" value="2.1" />
|
||||
<option name="apiVersion" value="2.3" />
|
||||
<option name="languageVersion" value="2.3" />
|
||||
</component>
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.1.10" />
|
||||
<option name="externalSystemId" value="Gradle" />
|
||||
<option name="version" value="2.3.0" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+1
-20
@@ -1,28 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="ms-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
<component name="ScriptDefinitionSettings">
|
||||
<option name="settings">
|
||||
<list>
|
||||
<ScriptDefinitionSetting>
|
||||
<option name="definitionId" value="org.jetbrains.kotlin.mainKts.MainKtsScript" />
|
||||
</ScriptDefinitionSetting>
|
||||
<ScriptDefinitionSetting>
|
||||
<option name="definitionId" value="kotlin.Any" />
|
||||
</ScriptDefinitionSetting>
|
||||
<ScriptDefinitionSetting>
|
||||
<option name="definitionId" value="kotlin.Any" />
|
||||
</ScriptDefinitionSetting>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="TaskProjectConfiguration">
|
||||
<server type="Redmine" url="https://redmine.keinekekse.net" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/src/main/kotlin/net/keinekekse/epub/calibre.kt" dialect="GenericSQL" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/kotlin/net/keinekekse/epub/calibredb.kt" dialect="GenericSQL" />
|
||||
<file url="PROJECT" dialect="SQLite" />
|
||||
</component>
|
||||
</project>
|
||||
+13
-3
@@ -2,6 +2,7 @@ plugins {
|
||||
application
|
||||
kotlin("jvm") version masterLibs.versions.kotlinVersion
|
||||
kotlin("plugin.serialization") version masterLibs.versions.kotlinVersion
|
||||
id("org.graalvm.buildtools.native") version "0.11.2"
|
||||
alias(masterLibs.plugins.versionPlugin)
|
||||
}
|
||||
|
||||
@@ -31,9 +32,10 @@ application {
|
||||
|
||||
group = "net.keinekekse"
|
||||
version = libs.versions.ebookcmdVersion.get()
|
||||
description = "E-Book Command Line Tool"
|
||||
extra["displayname"] = "ebook cmd"
|
||||
extra["copyright"] = "Copyright (c) 2025 Hartmut Bauer"
|
||||
description = "Serien-EPUBs in die Calibre-Bibliothek importieren oder\\n" +
|
||||
"Metadaten von EPUB-Dateien anzeigen bzw. verändern."
|
||||
extra["displayname"] = "eBook CMD"
|
||||
extra["copyright"] = "Copyright (c) 2025-2026 Hartmut Bauer"
|
||||
|
||||
repositories {
|
||||
maven("https://nexus.keinekekse.net/repository/maven-public/")
|
||||
@@ -59,3 +61,11 @@ kotlin {
|
||||
languageVersion.set(JavaLanguageVersion.of(masterLibs.versions.javaVersion.get()))
|
||||
}
|
||||
}
|
||||
|
||||
graalvmNative {
|
||||
binaries {
|
||||
named("main") {
|
||||
buildArgs.add("--initialize-at-build-time")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
#auto generated
|
||||
#Sun Mar 09 18:07:43 CET 2025
|
||||
build.number=115
|
||||
#Sun Jan 18 02:02:40 CET 2026
|
||||
build.number=258
|
||||
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
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -114,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -172,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -205,15 +203,14 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
Vendored
+1
-2
@@ -70,11 +70,10 @@ goto fail
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
+5
-1
@@ -16,8 +16,12 @@ dependencyResolutionManagement {
|
||||
}
|
||||
|
||||
versionCatalogs {
|
||||
// create("libs") {
|
||||
// from(files("../gradle/libs.versions.toml"))
|
||||
// }
|
||||
create("masterLibs") {
|
||||
from("net.keinekekse:version-catalog:56")
|
||||
from(files("gradle/masterlibs.versions.toml"))
|
||||
// from("net.keinekekse:version-catalog:98")
|
||||
}
|
||||
}
|
||||
}
|
||||
+171
-12
@@ -1,12 +1,14 @@
|
||||
package net.keinekekse
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.keinekekse.epub.getSeriesFromDb
|
||||
import net.keinekekse.version.Version
|
||||
import net.keinekekse.args.CommandLine
|
||||
import net.keinekekse.config.AbstractInitConfig
|
||||
import net.keinekekse.ebookcmd.VERSION
|
||||
import net.keinekekse.epub.EbookAutoImport
|
||||
|
||||
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
|
||||
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
|
||||
fun main() {
|
||||
fun main(args: Array<String>) {
|
||||
|
||||
/*
|
||||
val res = object {}::class.java.getResource("/META-INF/MANIFEST.MF")
|
||||
@@ -30,15 +32,162 @@ fun main() {
|
||||
println("Hello, world!")
|
||||
*/
|
||||
|
||||
runBlocking {
|
||||
val verLib = Version(Version::class.java)
|
||||
println(verLib.infoString)
|
||||
val ver = Version(VersionCheck::class.java)
|
||||
println(ver.infoString)
|
||||
|
||||
println(getSeriesFromDb())
|
||||
println("#########")
|
||||
println(getSeriesFromDb())
|
||||
runBlocking {
|
||||
// testEpub()
|
||||
/*
|
||||
cmd line:
|
||||
ohne -> auto mit Bestätigung
|
||||
only file -> metadata
|
||||
--auto -> auto ohne Bestätigung
|
||||
--authors, --author_sort, --title, --series, --series_index + file-> chg
|
||||
--sync -> nur calibre sync
|
||||
|
||||
*/
|
||||
|
||||
val commandLine = CommandLine(
|
||||
VERSION,
|
||||
remainingDescription = "E-Books"
|
||||
)
|
||||
commandLine.setExecution { additionalParameters ->
|
||||
// noop
|
||||
}
|
||||
|
||||
val cmdImport =
|
||||
commandLine.addSubCommand("import", "Serien-EPUBs importieren.", listOf("imp"))
|
||||
val auto by cmdImport.addBooleanParameter(
|
||||
"auto",
|
||||
'a',
|
||||
"Ohne Rückfrage importieren.",
|
||||
false
|
||||
)
|
||||
cmdImport.setExecution { additionalParameters ->
|
||||
// TODO err if additionalParameters.isNotEmpty()
|
||||
println("import auto=${auto}")
|
||||
|
||||
val ebookAutoImport = EbookAutoImport()
|
||||
val newEbooks = ebookAutoImport.checkEbookFiles {
|
||||
println("auto: ${it.autoUpdate} - ${it.ebook.fileName} / ${it.seriesPath}")
|
||||
}
|
||||
if (auto == true) {
|
||||
for (newEbook in newEbooks) {
|
||||
// if (ebookAutoImport.import(newEbook))
|
||||
println("Imported ${newEbook.ebook.name}.")
|
||||
}
|
||||
} else {
|
||||
newEbooks.filter { it.autoUpdate }.forEach {
|
||||
println(it.ebook)
|
||||
println("Importieren? (y/n)")
|
||||
val yesno = readlnOrNull()
|
||||
if (yesno?.lowercase() == "y") {
|
||||
// if (ebookAutoImport.import(it))
|
||||
println("Imported ${it.ebook.name}.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val cmdSync =
|
||||
commandLine.addSubCommand("sync", "Calibre-Bibliothek mit Calibre-Web synchronisieren.")
|
||||
cmdSync.setExecution { additionalParameters ->
|
||||
println("sync")
|
||||
}
|
||||
|
||||
val cmdChange =
|
||||
commandLine.addSubCommand("change", "Metadaten ändern.", listOf("chg", "meta"))
|
||||
val authors by cmdChange.addStringParameter(
|
||||
"authors",
|
||||
null,
|
||||
"Autor(en) eintragen oder ändern (Vorname Name [& Vorname Name]).",
|
||||
false
|
||||
)
|
||||
val title by cmdChange.addStringParameter(
|
||||
"title",
|
||||
null,
|
||||
"Titel eintragen oder ändern.",
|
||||
false
|
||||
)
|
||||
val series by cmdChange.addStringParameter(
|
||||
"series",
|
||||
null,
|
||||
"Serie eintragen oder ändern.",
|
||||
false
|
||||
)
|
||||
val seriesIndex by cmdChange.addLongParameter(
|
||||
"series-index",
|
||||
null,
|
||||
"Serien-Index eintragen oder ändern",
|
||||
false
|
||||
)
|
||||
cmdChange.setExecution { additionalParameters ->
|
||||
println("change meta data")
|
||||
println("authors: ${authors} / title: ${title} / series: ${series} / nr: ${seriesIndex}")
|
||||
|
||||
// do chg
|
||||
}
|
||||
|
||||
val cmdShow = commandLine.addSubCommand("show", "Metadaten anzeigen.")
|
||||
cmdShow.setExecution { additionalParameters ->
|
||||
if (additionalParameters.isNotEmpty()) {
|
||||
println("show meta")
|
||||
for (file in additionalParameters) {
|
||||
println(" $file")
|
||||
}
|
||||
// show meta data
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
commandLine.parseAndRun(*args)
|
||||
|
||||
return@runBlocking
|
||||
|
||||
// TODO Bestätigung für Import
|
||||
|
||||
// ebookAutoImport.writeChanges(newEbooks)
|
||||
// ebookAutoImport.importEbooksIntoCalibre(newEbooks)
|
||||
// newEbooks.forEach {
|
||||
// println("auto: ${it.autoUpdate} - ${it.changesWritten} - ${it.imported} - ${it.ebook.fileName}")
|
||||
// }
|
||||
|
||||
/* --- cmd line ---
|
||||
|
||||
val arguments = CommandLineParser(
|
||||
"""
|
||||
${ver.infoString}
|
||||
|
||||
Metadaten von EPUB-Dateien anzeigen und bearbeiten.
|
||||
|
||||
usage: ebookcmd [options] <epub(s)>
|
||||
|
||||
""".trimIndent()
|
||||
)
|
||||
val name by arguments.stringOption(OptionConfig("name", 'n', "some name", true))
|
||||
val bool by arguments.boolOption(OptionConfig("bool", 'b', "some boolean", false))
|
||||
val test1 by arguments.stringOption(OptionConfig("test-one", 't', "test1", true))
|
||||
val test2 by arguments.longOption(OptionConfig("int", 'i', "integer"))
|
||||
val params by arguments.parametersList()
|
||||
|
||||
arguments.parse(args)
|
||||
|
||||
println("name: \"$name\"")
|
||||
println("bool: $bool")
|
||||
println("test-one: \"$test1\"")
|
||||
println("int: $test2")
|
||||
|
||||
println("params: ${params?.joinToString(" ") { "\"$it\"" }}")
|
||||
*/
|
||||
|
||||
// println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()))
|
||||
// val series = getSeriesFromDb()
|
||||
// series.forEach { serie ->
|
||||
// println(serie.name)
|
||||
// }
|
||||
// println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()))
|
||||
|
||||
// println("#########")
|
||||
// println(getSeriesFromDb())
|
||||
|
||||
// launch(Dispatchers.Default) { testEpub() }
|
||||
//
|
||||
@@ -51,4 +200,14 @@ fun main() {
|
||||
|
||||
}
|
||||
|
||||
data class VersionCheck(val dummy: String)
|
||||
object Config : AbstractInitConfig(VERSION) {
|
||||
// properties
|
||||
const val CALIBRE_DB_CONNECT = "calibre.db.connect-string"
|
||||
const val CALIBRE_DB_DRIVER = "calibre.db.driver"
|
||||
const val CALIBRE_HOST = "calibre.host"
|
||||
const val CALIBRE_PORT = "calibre.port"
|
||||
const val CALIBRE_USER = "calibre.user"
|
||||
const val CALIBRE_PWD = "calibre.pwd"
|
||||
const val AUTO_IMPORT_CONFIG = "ebook.auto-import.config"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.keinekekse.ebookcmd
|
||||
|
||||
import net.keinekekse.version.Version
|
||||
|
||||
object VERSION: Version {
|
||||
override val version = "0.2.0-beta+build.258"
|
||||
override val fullVersion = "0.2.0-beta+build.258"
|
||||
override val major = 0
|
||||
override val minor = 2
|
||||
override val patch = 0
|
||||
override val preRelease = "beta"
|
||||
override val buildMetadata = "build.258"
|
||||
override val buildDate = "2026-01-18T01:02:40.808477Z"
|
||||
override val displayName = "eBook CMD"
|
||||
override val copyright = "Copyright (c) 2025-2026 Hartmut Bauer"
|
||||
override val name = "ebookcmd"
|
||||
override val description = "Serien-EPUBs in die Calibre-Bibliothek importieren oder\nMetadaten von EPUB-Dateien anzeigen bzw. verändern."
|
||||
}
|
||||
@@ -4,16 +4,16 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.keinekekse.epub.Ebook
|
||||
import net.keinekekse.epub.EbookMeta
|
||||
import net.keinekekse.epub.EbookMetaTypes
|
||||
import net.keinekekse.epub.convertToSeriesList
|
||||
import net.keinekekse.epub.getSeriesFromCalibre
|
||||
import net.keinekekse.exec.CommandExecutionException
|
||||
import java.io.File
|
||||
|
||||
suspend fun testEpub() {
|
||||
// testEpubList()
|
||||
testEpubList()
|
||||
// testEpubWrite()
|
||||
testSeries()
|
||||
// testSeries()
|
||||
}
|
||||
|
||||
suspend fun testSeries() {
|
||||
@@ -34,12 +34,11 @@ fun testEpubWrite() {
|
||||
// val epub = "div/Kotlin_in_Action_Second_Edition.epub"
|
||||
try {
|
||||
val ebook = Ebook(epub.path)
|
||||
printMeta(ebook.meta)
|
||||
if (ebook.meta.authors.contains("&") || ebook.meta["authorSort"].isBlank()) {
|
||||
printMeta(ebook)
|
||||
if (ebook.getChangesFor(EbookMetaTypes.AUTHORS).isNotBlank()) {
|
||||
println("*** author sort anpassen")
|
||||
ebook.changedAuthors = ebook.meta.authors
|
||||
ebook.setMeta()
|
||||
printMeta(ebook.meta)
|
||||
printMeta(ebook)
|
||||
}
|
||||
} catch (e: CommandExecutionException) {
|
||||
println("ERROR reading ${epub.path}:\n${e.message}")
|
||||
@@ -57,22 +56,28 @@ fun testEpubList() {
|
||||
for (epub in epubs) {
|
||||
// println("EPUB: ${epub.path} - ${epub.absolutePath}")
|
||||
try {
|
||||
printMeta(Ebook(epub.path).meta)
|
||||
printMeta(Ebook(epub.path))
|
||||
} catch (e: CommandExecutionException) {
|
||||
println("ERROR reading ${epub.path}:\n${e.message}")
|
||||
println("\nERROR reading ${epub.path}:\n${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun printMeta(meta: EbookMeta?) {
|
||||
if (meta == null) return
|
||||
println(meta.fileName)
|
||||
println("TITLE: _${meta.title}_ SORT: _${meta["titleSort"]}_")
|
||||
println("AUTHORS: _${meta.authors}_ SORT: _${meta["authorSort"]}_ ")
|
||||
println("SERIES: _${meta.series}_ INDEX: _${meta.seriesIndex}_")
|
||||
println("PUBLISHED: _${meta["pubDate"]}_")
|
||||
println("LANG: _${meta["lang"]}_")
|
||||
println("ISBN: _${meta["isbn"]}_")
|
||||
fun printMeta(ebook: Ebook) {
|
||||
println("------------------------")
|
||||
println(ebook.fileName)
|
||||
// println("TITLE: _${meta[TITLE]}_ SORT: _${meta[TITLESORT]}_")
|
||||
// println("AUTHORS: _${meta[AUTHORS]}_ SORT: _${meta[AUTHORSORT]}_ ")
|
||||
// println("SERIES: _${meta[SERIES]}_ INDEX: _${meta[SERIESINDEX]}_")
|
||||
// println("PUBLISHED: _${meta[PUBDATE]}_")
|
||||
// println("LANG: _${meta[LANG]}_")
|
||||
// println("ISBN: _${meta[ISBN]}_")
|
||||
|
||||
for ((key, value) in ebook.metadata) {
|
||||
println("${key.displayName}: $value")
|
||||
}
|
||||
if (ebook.getChangesFor(EbookMetaTypes.AUTHORS).isNotBlank()) {
|
||||
println("CHANGED AUTHORS: ${ebook.getChangesFor(EbookMetaTypes.AUTHORS)}")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
import net.keinekekse.epub.EbookMetaTypes.AUTHORS
|
||||
import net.keinekekse.epub.EbookMetaTypes.entries
|
||||
import net.keinekekse.exec.CommandExecutionException
|
||||
import net.keinekekse.exec.ExecutionResult
|
||||
import java.io.File
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@@ -9,23 +12,19 @@ import java.time.format.DateTimeFormatter
|
||||
* Represents an ebook file.
|
||||
*
|
||||
* @property fileName the file name of the ebook
|
||||
* @property meta the metadata of the ebook as [EbookMeta]
|
||||
* @property changedTitle the new title of the ebook
|
||||
* @property changedTitleSort the new title sort of the ebook
|
||||
* @property changedAuthors the new authors of the ebook
|
||||
* @property changedSeries the new series of the ebook
|
||||
* @property changedSeriesIndex the new series index of the ebook
|
||||
* @property metadata the metadata of the ebook as [Map]
|
||||
*
|
||||
* @throws CommandExecutionException if the ebook metadata cannot be read or written
|
||||
*/
|
||||
class Ebook(fileName: String) {
|
||||
var meta: EbookMeta = getEpubMeta(fileName)
|
||||
class Ebook(var fileName: String) {
|
||||
|
||||
var changedTitle: String = ""
|
||||
var changedTitleSort: String = ""
|
||||
var changedAuthors: String = ""
|
||||
var changedSeries: String = ""
|
||||
var changedSeriesIndex: String = ""
|
||||
private val changedMetadata: MutableMap<EbookMetaTypes, String> =
|
||||
entries.filter { it.setCmd.isNotBlank() }
|
||||
.associateWith { "" } as MutableMap<EbookMetaTypes, String>
|
||||
|
||||
var metadata: Map<EbookMetaTypes, String> = getEbookMeta(fileName)
|
||||
val name
|
||||
get() = File(fileName).nameWithoutExtension
|
||||
|
||||
/**
|
||||
* Writes the changed metadata to the ebook file.
|
||||
@@ -33,84 +32,142 @@ class Ebook(fileName: String) {
|
||||
* @throws CommandExecutionException if the ebook metadata cannot be written or read
|
||||
*/
|
||||
fun setMeta() {
|
||||
changedTitle.takeIf { it.isNotBlank() }?.let {
|
||||
meta = setEpubMeta(meta.fileName, metaDataItems["title"]!!.setCmd!!, it)
|
||||
changedTitle = ""
|
||||
}
|
||||
changedTitleSort.takeIf { it.isNotBlank() }?.let {
|
||||
meta = setEpubMeta(meta.fileName, metaDataItems["titleSort"]!!.setCmd!!, it)
|
||||
changedTitleSort = ""
|
||||
}
|
||||
changedAuthors.takeIf { it.isNotBlank() }?.let {
|
||||
meta = setEpubMeta(meta.fileName, metaDataItems["authors"]!!.setCmd!!, it)
|
||||
changedAuthors = ""
|
||||
}
|
||||
changedSeries.takeIf { it.isNotBlank() }?.let {
|
||||
meta = setEpubMeta(meta.fileName, metaDataItems["series"]!!.setCmd!!, it)
|
||||
changedSeries = ""
|
||||
}
|
||||
changedSeriesIndex.takeIf { it.isNotBlank() }?.let {
|
||||
meta = setEpubMeta(meta.fileName, metaDataItems["seriesIndex"]!!.setCmd!!, it)
|
||||
changedSeriesIndex = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
val changes = changedMetadata.filterValues { it.isNotBlank() }
|
||||
if (changes.isEmpty()) return
|
||||
|
||||
data class EbookMeta(val fileName: String) {
|
||||
private val metadata = mutableMapOf<String, String>()
|
||||
val commandline = listOf(
|
||||
"ebook-meta",
|
||||
*changes.flatMap { listOf(it.key.setCmd, it.value) }.toTypedArray()
|
||||
).toTypedArray()
|
||||
|
||||
operator fun get(key: String): String = metadata[key] ?: ""
|
||||
|
||||
operator fun set(key: String, value: String) {
|
||||
metadata[key] = value
|
||||
metadata = setEbookMeta(fileName, *commandline)
|
||||
}
|
||||
|
||||
var title by metadata
|
||||
var authors by metadata
|
||||
var series by metadata
|
||||
var seriesIndex by metadata
|
||||
/**
|
||||
* Returns the changed entry for ```type``` or an empty string.
|
||||
*/
|
||||
fun getChangesFor(type: EbookMetaTypes): String = changedMetadata[type] ?: ""
|
||||
|
||||
/**
|
||||
* Updates the metadata of the eBook with the specified type and value.
|
||||
* Throws an exception if the specified metadata type cannot be changed.
|
||||
*
|
||||
* @param type The type of metadata to be changed, represented by the `EbookMetaTypes` enumeration.
|
||||
* @param value The new value for the specified metadata type.
|
||||
* @throws IllegalArgumentException if the specified metadata type cannot be changed.
|
||||
*/
|
||||
fun changeMetadata(type: EbookMetaTypes, value: String) {
|
||||
if (type !in changedMetadata) throw IllegalArgumentException("Ebook $fileName: Metadata type $type cannot be changed.")
|
||||
changedMetadata[type] = value
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val HEADING = "\n=== %s\n"
|
||||
val CHANGED_SECTION = "*** Changed Metadata:\n"
|
||||
|
||||
fun formatMetadataItem(key: String, value: String, width: Int): String =
|
||||
"${key.padEnd(width)}$value\n"
|
||||
|
||||
fun getChangedMetadataItems(width: Int): List<String> = buildList {
|
||||
changedMetadata.forEach { (key, value) ->
|
||||
if (value.isNotBlank()) {
|
||||
add(formatMetadataItem(key.displayName, value, width))
|
||||
}
|
||||
}
|
||||
}
|
||||
return buildString {
|
||||
// Header section
|
||||
append(HEADING.format(fileName))
|
||||
|
||||
// Metadata section
|
||||
val labelWidth = metadata.keys.maxOf { it.displayName.length } + 2
|
||||
metadata.forEach { (key, value) ->
|
||||
append(formatMetadataItem(key.displayName + ":", value, labelWidth))
|
||||
}
|
||||
|
||||
// Changed metadata section
|
||||
val changedItems = getChangedMetadataItems(labelWidth)
|
||||
if (changedItems.isNotEmpty()) {
|
||||
append(CHANGED_SECTION)
|
||||
changedItems.forEach { append(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEbookMeta(ebookFile: String) = runEbookMeta(ebookFile).let { runResult ->
|
||||
mutableMapOf<EbookMetaTypes, String>().apply {
|
||||
EbookMetaTypes.entries.forEach {
|
||||
this[it] = it.getEpubMetaFromDump(runResult.stdout)
|
||||
}
|
||||
this[AUTHORS]?.let {
|
||||
if (it.contains(",")) {
|
||||
changeMetadata(AUTHORS, it.replace(",", " &"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEbookMeta(ebookFile: String, vararg cmd: String) =
|
||||
runEbookMeta(ebookFile, *cmd).let { getEbookMeta(ebookFile) }
|
||||
|
||||
private fun runEbookMeta(vararg cmd: String) =
|
||||
ExecutionResult(listOf("ebook-meta", *cmd)).apply {
|
||||
if (stderr.isNotBlank()) throw CommandExecutionException("Error running ebook-meta:\n$stderr")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EbookMetaItem(
|
||||
/**
|
||||
* Enum class representing various metadata types for an eBook.
|
||||
*
|
||||
* @property displayName The human-readable name of the metadata type.
|
||||
* @property regex A string pattern used to identify the metadata type in raw data.
|
||||
* @property setCmd Optional command string used when setting this metadata type programmatically.
|
||||
* @property extractData Function to extract and transform the metadata value from a raw string.
|
||||
*/
|
||||
enum class EbookMetaTypes(
|
||||
val displayName: String,
|
||||
val regex: String,
|
||||
val setCmd: String? = null,
|
||||
val setCmd: String = "",
|
||||
val extractData: (String) -> String = { it },
|
||||
)
|
||||
|
||||
val metaDataItems = mapOf(
|
||||
"title" to EbookMetaItem("Title", """title""", "--title"),
|
||||
"titleSort" to EbookMetaItem("Title sort", """title sort""", "--title-sort"),
|
||||
"authors" to EbookMetaItem("Author(s)", """author\(s\)""", "--authors") {
|
||||
"""(.*?)\s*(\[|$)""".toRegex().find(it)?.groupValues?.get(1)?.replace(",", " &") ?: ""
|
||||
},
|
||||
"authorSort" to EbookMetaItem("Author sort", """author\(s\)""", "--author-sort") {
|
||||
) {
|
||||
TITLE("Title", """title""", "--title"),
|
||||
TITLE_SORT("Title Sort", """title sort""", "--title-sort"),
|
||||
AUTHORS("Author(s)", """author\(s\)""", "--authors", {
|
||||
"""(.*?)\s*(\[|$)""".toRegex().find(it)?.groupValues?.get(1) ?: ""
|
||||
}),
|
||||
AUTHORS_SORT("Author(s) Sort", """author\(s\)""", "", {
|
||||
""".*\[(.*)]""".toRegex().find(it)?.groupValues?.get(1) ?: ""
|
||||
},
|
||||
"series" to EbookMetaItem("Series", """series""", "--series") {
|
||||
}),
|
||||
SERIES("Series", """series""", "--series", {
|
||||
"""(.*?)\s*(#|$)""".toRegex().find(it)?.groupValues?.get(1) ?: ""
|
||||
},
|
||||
"seriesIndex" to EbookMetaItem("Series index", """series""", "--index") {
|
||||
}),
|
||||
SERIES_INDEX("Series Index", """series""", "--index", {
|
||||
val strSer = """#(\d*)\s*""".toRegex().find(it)?.groupValues?.get(1) ?: ""
|
||||
strSer.toIntOrNull()?.toString() ?: ""
|
||||
},
|
||||
"lang" to EbookMetaItem("Language(s)", """languages"""),
|
||||
"pubDate" to EbookMetaItem("Publication date", """published""") {
|
||||
}),
|
||||
LANG("Language(s)", """languages"""),
|
||||
PUBLICATION_DATE("Publication Date", """published""", "", {
|
||||
try {
|
||||
LocalDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME)
|
||||
.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
},
|
||||
"isbn" to EbookMetaItem("ISBN", """identifiers""") {
|
||||
}),
|
||||
ISBN("ISBN", """identifiers""", "", {
|
||||
"""isbn:(\d*)""".toRegex().find(it)?.groupValues?.get(1) ?: ""
|
||||
})
|
||||
});
|
||||
|
||||
private val reParam = setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)
|
||||
|
||||
private fun EbookMetaItem.getEpubMetaFromDump(dump: String) =
|
||||
extractData("""\s*$regex\s+:\s+(.*)""".toRegex(reParam).find(dump)?.groupValues?.get(1) ?: "")
|
||||
fun getEpubMetaFromDump(dump: String): String {
|
||||
val reParam = setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)
|
||||
return extractData(
|
||||
"""\s*$regex\s+:\s+(.*)""".toRegex(reParam).find(dump)?.groupValues?.get(
|
||||
1
|
||||
) ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Suchbegriffe:
|
||||
// - Title
|
||||
@@ -126,17 +183,3 @@ private fun EbookMetaItem.getEpubMetaFromDump(dump: String) =
|
||||
// Identifiers : isbn:9780708899403, mobi-asin:B07K23HGTW
|
||||
// Tags
|
||||
// Comments
|
||||
private fun getEpubMeta(epubFile: String) = runEbookMeta(epubFile).let { runResult ->
|
||||
EbookMeta(epubFile).apply {
|
||||
metaDataItems.forEach { (name, item) ->
|
||||
this[name] = item.getEpubMetaFromDump(runResult.stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEpubMeta(epubFile: String, cmd: String, text: String) =
|
||||
runEbookMeta(epubFile, cmd, text).let { getEpubMeta(epubFile) }
|
||||
|
||||
private fun runEbookMeta(vararg cmd: String) = ExecutionResult(listOf("ebook-meta", *cmd)).apply {
|
||||
if (stderr.isNotBlank()) throw CommandExecutionException("Error running ebook-meta:\n$stderr")
|
||||
}
|
||||
@@ -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