This commit is contained in:
ryan
2026-02-20 09:54:50 -05:00
parent a8c34d5874
commit 13c65dfdf6
5 changed files with 127 additions and 0 deletions

113
fixer.py Normal file
View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
Fix EPUB series metadata for Kavita compatibility.
Usage: python fix_series.py /path/to/books "Series Name"
"""
import os
import sys
import re
import zipfile
import shutil
from lxml import etree
def find_opf(epub_path):
"""Find the OPF file path inside the epub."""
with zipfile.ZipFile(epub_path, 'r') as z:
container = z.read('META-INF/container.xml')
root = etree.fromstring(container)
ns = {'c': 'urn:oasis:names:tc:opendocument:xmlns:container'}
opf_path = root.find('.//c:rootfile', ns).get('full-path')
return opf_path
def update_opf_metadata(opf_content, series_name, series_index):
"""Add/update Calibre series metadata in OPF XML."""
root = etree.fromstring(opf_content)
ns = {'opf': 'http://www.idpf.org/2007/opf',
'dc': 'http://purl.org/dc/elements/1.1/'}
metadata = root.find('.//opf:metadata', ns)
if metadata is None:
print(" WARNING: No metadata element found, skipping.")
return None
# Remove existing series meta tags
for meta in metadata.findall('opf:meta', ns):
name = meta.get('name', '')
if name in ('calibre:series', 'calibre:series_index'):
metadata.remove(meta)
# Add new ones
OPF = 'http://www.idpf.org/2007/opf'
series_meta = etree.SubElement(metadata, f'{{{OPF}}}meta')
series_meta.set('name', 'calibre:series')
series_meta.set('content', series_name)
index_meta = etree.SubElement(metadata, f'{{{OPF}}}meta')
index_meta.set('name', 'calibre:series_index')
index_meta.set('content', str(series_index))
return etree.tostring(root, xml_declaration=True, encoding='utf-8', pretty_print=True)
def extract_index_from_filename(filename):
"""Try to extract a volume/chapter number from the filename."""
match = re.search(r'[vVcC](?:ol|olume|h|hapter)?[.\s_-]*(\d+(?:\.\d+)?)', filename)
if match:
return float(match.group(1))
# fallback: any number in the filename
match = re.search(r'(\d+(?:\.\d+)?)', filename)
if match:
return float(match.group(1))
return None
def fix_epub(epub_path, series_name, series_index):
"""Update the OPF metadata inside an epub in-place."""
tmp_path = epub_path + '.tmp'
opf_path = find_opf(epub_path)
with zipfile.ZipFile(epub_path, 'r') as zin:
with zipfile.ZipFile(tmp_path, 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
data = zin.read(item.filename)
if item.filename == opf_path:
updated = update_opf_metadata(data, series_name, series_index)
if updated:
data = updated
zout.writestr(item, data)
shutil.move(tmp_path, epub_path)
print(f" ✓ Updated: index={series_index}")
def main():
if len(sys.argv) < 3:
print("Usage: python fix_series.py /path/to/folder \"Series Name\"")
sys.exit(1)
folder = sys.argv[1]
series_name = sys.argv[2]
epubs = sorted([f for f in os.listdir(folder) if f.lower().endswith('.epub')])
if not epubs:
print("No EPUBs found.")
sys.exit(0)
print(f"Found {len(epubs)} EPUBs. Series: '{series_name}'\n")
for i, filename in enumerate(epubs, start=1):
path = os.path.join(folder, filename)
index = extract_index_from_filename(filename)
if index is None:
index = float(i)
print(f"[{filename}] No index found, using position {i}")
else:
print(f"[{filename}] Detected index {index}")
try:
fix_epub(path, series_name, index)
except Exception as e:
print(f" ERROR: {e}")
print("\nDone! Rescan your Kavita library.")
if __name__ == '__main__':
main()