Skip to content

Commit 35ad418

Browse files
Merge branch 'main' into main
2 parents 061de11 + eeb936e commit 35ad418

5 files changed

Lines changed: 238 additions & 8 deletions

File tree

CONTRIBUTORS

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,6 @@ Copyright (C) 2024-2026 Calibre-Web Automated contributors
313313
- zelazna (1 commits)
314314
- zhiyue (1 commits)
315315
# Fork Contributors (crocodilestick/calibre-web-automated)
316-
317316
- crocodilestick (931 commits)
318317
- jmarmstrong1207 (73 commits)
319318
- demitrix (30 commits)

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,14 @@ Calibre-Web Automated aims to be an all-in-one solution, combining the modern li
4646

4747
## _Affiliated Projects_ 👬
4848

49-
### Calibre-Web Automated Book Downloader
49+
### Shelfmark: Book Downloader
5050

5151
- An intuitive web interface for searching and requesting book downloads, designed to work seamlessly with Calibre-Web-Automated. This project streamlines the process of downloading books and preparing them for integration into your Calibre library
5252

53-
[<img src="https://raw.githubusercontent.com/vadret/android/master/assets/get-github.png" alt="Get it on GitHub" height="80">](https://github.com/calibrain/calibre-web-automated-book-downloader)
53+
> [!IMPORTANT]
54+
> CWA does not approve of or support piracy of copyrighted materials and is not responsible for user behaviour
55+
56+
[<img src="https://raw.githubusercontent.com/vadret/android/master/assets/get-github.png" alt="Get it on GitHub" height="80">](https://github.com/calibrain/shelfmark)
5457

5558
___
5659

16.9 KB
Binary file not shown.

scripts/generate_book_checksums.py

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
for newly added books.
1616
1717
Usage:
18-
python generate_book_checksums.py [--library-path /path/to/calibre/library] [--force]
18+
python generate_book_checksums.py [--library-path /path/to/calibre/library] [--books-path /path/to/books] [--force]
1919
2020
Options:
2121
--library-path Path to Calibre library directory (defaults to /calibre-library)
22+
--books-path Path to books directory (defaults to config_calibre_split_dir setting with --library-path fallback)
2223
--force Regenerate checksums even if they already exist
2324
--batch-size Number of books to process before committing (default: 100)
2425
"""
@@ -32,11 +33,12 @@
3233
# Import the centralized partial MD5 calculation function
3334
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
3435
from cps.progress_syncing.checksums import calculate_koreader_partial_md5, store_checksum, CHECKSUM_VERSION
35-
def generate_checksums(library_path: str, force: bool = False, batch_size: int = 100):
36+
def generate_checksums(library_path: str, books_path: str = None, force: bool = False, batch_size: int = 100):
3637
"""Generate checksums for all books in the library
3738
3839
Args:
39-
library_path: Path to Calibre library directory
40+
library_path: Path to Calibre library directory (contains metadata.db)
41+
books_path: Path to books directory (if different from library_path in split mode)
4042
force: If True, regenerate checksums even if they exist
4143
batch_size: Number of books to process before committing
4244
"""
@@ -46,7 +48,14 @@ def generate_checksums(library_path: str, force: bool = False, batch_size: int =
4648
print(f"ERROR: Calibre database not found at {metadata_db}")
4749
sys.exit(1)
4850

51+
# Use books_path if provided and valid, otherwise fall back to library_path
52+
base_path = books_path if (books_path and os.path.exists(books_path)) else library_path
53+
4954
print(f"Connecting to Calibre library at: {library_path}")
55+
if base_path != library_path:
56+
print(f"Books path (split library mode): {base_path}")
57+
else:
58+
print(f"Books path: {base_path}")
5059
print(f"Force regenerate: {force}")
5160
print(f"Batch size: {batch_size}")
5261
print(f"Checksum version: {CHECKSUM_VERSION}")
@@ -98,7 +107,7 @@ def generate_checksums(library_path: str, force: bool = False, batch_size: int =
98107
processed += 1
99108

100109
# Construct full file path
101-
file_path = os.path.join(library_path, book_path, f"{format_name}.{format_ext.lower()}")
110+
file_path = os.path.join(base_path, book_path, f"{format_name}.{format_ext.lower()}")
102111

103112
if not os.path.exists(file_path):
104113
print(f"[{processed}/{total}] SKIP: File not found - {title} ({format_ext})")
@@ -152,6 +161,42 @@ def generate_checksums(library_path: str, force: bool = False, batch_size: int =
152161
conn.close()
153162

154163

164+
def get_books_path():
165+
"""
166+
Get the split library books path from app.db if split mode is enabled.
167+
168+
Returns:
169+
The books path from config_calibre_split_dir if it exists and is valid,
170+
otherwise None to indicate the library path should be used.
171+
"""
172+
try:
173+
conn = sqlite3.connect("/config/app.db", timeout=30)
174+
cur = conn.cursor()
175+
176+
# Check if split mode is enabled and get split path
177+
result = cur.execute('SELECT config_calibre_split, config_calibre_split_dir FROM settings LIMIT 1;').fetchone()
178+
179+
if not result:
180+
return None
181+
182+
split_enabled, split_path = result
183+
184+
# Only return split path if split mode is enabled, path is not NULL, and path exists
185+
if split_enabled and split_path and os.path.exists(split_path):
186+
return split_path
187+
188+
return None
189+
190+
except sqlite3.Error as e:
191+
# Log warning but don't crash - fall back to library path
192+
print(f"WARNING: Could not read split library setting from app.db: {e}")
193+
print(f"WARNING: Falling back to --library-path for books location")
194+
return None
195+
finally:
196+
if 'conn' in locals():
197+
conn.close()
198+
199+
155200
def main():
156201
parser = argparse.ArgumentParser(
157202
description='Generate KOReader sync checksums for books in Calibre library',
@@ -164,6 +209,12 @@ def main():
164209
help='Path to Calibre library directory (default: /calibre-library)'
165210
)
166211

212+
parser.add_argument(
213+
'--books-path',
214+
default=get_books_path(),
215+
help='Path to books directory (default: config_calibre_split_dir setting or --library-path)'
216+
)
217+
167218
parser.add_argument(
168219
'--force',
169220
action='store_true',
@@ -185,7 +236,7 @@ def main():
185236
sys.exit(1)
186237

187238
try:
188-
generate_checksums(args.library_path, args.force, args.batch_size)
239+
generate_checksums(args.library_path, args.books_path, args.force, args.batch_size)
189240
except KeyboardInterrupt:
190241
print("\n\nInterrupted by user. Exiting...")
191242
sys.exit(130)

tests/unit/test_generate_checksums.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,183 @@ def test_handles_missing_files_gracefully(self, tmp_path):
316316
assert result.returncode == 0
317317
assert "SKIP" in result.stdout or "not found" in result.stdout.lower()
318318

319+
def test_split_library_with_separate_paths(self, tmp_path):
320+
"""Test checksum generation with split library (separate metadata and books)."""
321+
# Create separate directories for metadata and books
322+
metadata_dir = tmp_path / "metadata"
323+
books_dir = tmp_path / "books"
324+
325+
metadata_dir.mkdir()
326+
books_dir.mkdir()
327+
328+
# Create metadata.db in metadata_dir
329+
create_minimal_calibre_library(metadata_dir)
330+
331+
# Add book to metadata.db
332+
db_path = metadata_dir / "metadata.db"
333+
conn = sqlite3.connect(db_path)
334+
cur = conn.cursor()
335+
336+
cur.execute("""
337+
INSERT INTO books (title, sort, path, has_cover)
338+
VALUES ('Split Library Book', 'Split Library Book', 'split_book', 0)
339+
""")
340+
book_id = cur.lastrowid
341+
342+
cur.execute("""
343+
INSERT INTO data (book, format, name)
344+
VALUES (?, 'EPUB', 'split_book')
345+
""", (book_id,))
346+
347+
conn.commit()
348+
conn.close()
349+
350+
# Create actual book file in books_dir (NOT metadata_dir)
351+
book_folder = books_dir / "split_book"
352+
book_folder.mkdir()
353+
(book_folder / "split_book.epub").write_bytes(b"Test EPUB content for split library testing")
354+
355+
# Run script with separate books-path
356+
script_path = scripts_dir / "generate_book_checksums.py"
357+
result = subprocess.run(
358+
[sys.executable, str(script_path),
359+
"--library-path", str(metadata_dir),
360+
"--books-path", str(books_dir)],
361+
capture_output=True,
362+
text=True,
363+
timeout=30
364+
)
365+
366+
assert result.returncode == 0
367+
assert "Split Library Book" in result.stdout
368+
assert "✓" in result.stdout
369+
assert "split library mode" in result.stdout.lower()
370+
371+
# Verify checksum was stored in metadata.db
372+
conn = sqlite3.connect(db_path)
373+
cur = conn.cursor()
374+
checksum = cur.execute("""
375+
SELECT checksum FROM book_format_checksums
376+
WHERE book = ? AND format = 'EPUB'
377+
""", (book_id,)).fetchone()
378+
conn.close()
379+
380+
assert checksum is not None
381+
assert len(checksum[0]) == 32 # Valid MD5
382+
383+
def test_books_path_falls_back_to_library_path(self, tmp_path):
384+
"""Test that invalid books-path falls back to library-path."""
385+
library_path = tmp_path / "library"
386+
create_minimal_calibre_library(library_path)
387+
add_book_to_library(library_path, "Fallback Test Book", ["EPUB"])
388+
389+
# Pass nonexistent books-path
390+
script_path = scripts_dir / "generate_book_checksums.py"
391+
result = subprocess.run(
392+
[sys.executable, str(script_path),
393+
"--library-path", str(library_path),
394+
"--books-path", "/nonexistent/path"],
395+
capture_output=True,
396+
text=True,
397+
timeout=30
398+
)
399+
400+
# Should succeed by falling back to library_path
401+
assert result.returncode == 0
402+
assert "Fallback Test Book" in result.stdout
403+
assert "✓" in result.stdout
404+
405+
def test_books_path_with_none_value(self, tmp_path):
406+
"""Test that None books-path uses library-path."""
407+
library_path = tmp_path / "library"
408+
create_minimal_calibre_library(library_path)
409+
add_book_to_library(library_path, "Normal Mode Book", ["EPUB"])
410+
411+
# Run without --books-path argument (default behavior)
412+
script_path = scripts_dir / "generate_book_checksums.py"
413+
result = subprocess.run(
414+
[sys.executable, str(script_path),
415+
"--library-path", str(library_path)],
416+
capture_output=True,
417+
text=True,
418+
timeout=30
419+
)
420+
421+
# Should succeed using library_path for books
422+
assert result.returncode == 0
423+
assert "Normal Mode Book" in result.stdout
424+
assert "✓" in result.stdout
425+
# Should NOT show split library mode message
426+
assert "Books path: " in result.stdout
427+
428+
def test_split_library_with_multiple_formats(self, tmp_path):
429+
"""Test split library with book having multiple formats."""
430+
metadata_dir = tmp_path / "metadata"
431+
books_dir = tmp_path / "books"
432+
433+
metadata_dir.mkdir()
434+
books_dir.mkdir()
435+
436+
create_minimal_calibre_library(metadata_dir)
437+
438+
# Add book with multiple formats
439+
db_path = metadata_dir / "metadata.db"
440+
conn = sqlite3.connect(db_path)
441+
cur = conn.cursor()
442+
443+
cur.execute("""
444+
INSERT INTO books (title, sort, path, has_cover)
445+
VALUES ('Multi Format Book', 'Multi Format Book', 'multi_format', 0)
446+
""")
447+
book_id = cur.lastrowid
448+
449+
# Add multiple formats
450+
for fmt in ['EPUB', 'MOBI', 'PDF']:
451+
cur.execute("""
452+
INSERT INTO data (book, format, name)
453+
VALUES (?, ?, 'multi_format')
454+
""", (book_id, fmt))
455+
456+
conn.commit()
457+
conn.close()
458+
459+
# Create actual book files in books_dir
460+
book_folder = books_dir / "multi_format"
461+
book_folder.mkdir()
462+
(book_folder / "multi_format.epub").write_bytes(b"EPUB content")
463+
(book_folder / "multi_format.mobi").write_bytes(b"MOBI content")
464+
(book_folder / "multi_format.pdf").write_bytes(b"PDF content")
465+
466+
# Run script
467+
script_path = scripts_dir / "generate_book_checksums.py"
468+
result = subprocess.run(
469+
[sys.executable, str(script_path),
470+
"--library-path", str(metadata_dir),
471+
"--books-path", str(books_dir)],
472+
capture_output=True,
473+
text=True,
474+
timeout=30
475+
)
476+
477+
assert result.returncode == 0
478+
479+
# Verify all three formats got checksums
480+
conn = sqlite3.connect(db_path)
481+
cur = conn.cursor()
482+
checksums = cur.execute("""
483+
SELECT format, checksum FROM book_format_checksums
484+
WHERE book = ?
485+
ORDER BY format
486+
""", (book_id,)).fetchall()
487+
conn.close()
488+
489+
assert len(checksums) == 3
490+
assert all(len(checksum[1]) == 32 for checksum in checksums)
491+
formats = [c[0] for c in checksums]
492+
assert 'EPUB' in formats
493+
assert 'MOBI' in formats
494+
assert 'PDF' in formats
495+
319496
def test_batch_size_parameter(self, tmp_path):
320497
"""Test that batch-size parameter is respected."""
321498
library_path = tmp_path / "test_library"

0 commit comments

Comments
 (0)