zotlib 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zotlib/__init__.py +24 -0
- zotlib/backup.py +98 -0
- zotlib/cli.py +448 -0
- zotlib/config.py +184 -0
- zotlib/covers.py +113 -0
- zotlib/database.py +67 -0
- zotlib/exporters.py +601 -0
- zotlib/extractors.py +230 -0
- zotlib/formatters/__init__.py +5 -0
- zotlib/formatters/apa.py +86 -0
- zotlib/paths.py +43 -0
- zotlib/tables.py +828 -0
- zotlib-0.4.3.dist-info/METADATA +219 -0
- zotlib-0.4.3.dist-info/RECORD +16 -0
- zotlib-0.4.3.dist-info/WHEEL +4 -0
- zotlib-0.4.3.dist-info/entry_points.txt +2 -0
zotlib/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Zotlib - Extract and format bibliographic data from Zotero databases."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.4.3"
|
|
4
|
+
|
|
5
|
+
from zotlib.database import ZoteroDatabase
|
|
6
|
+
from zotlib.extractors import (
|
|
7
|
+
extract_collections,
|
|
8
|
+
extract_creators,
|
|
9
|
+
extract_cv_items,
|
|
10
|
+
extract_items,
|
|
11
|
+
extract_libraries,
|
|
12
|
+
)
|
|
13
|
+
from zotlib.formatters.apa import format_apa_reference, format_cv_as_apa
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ZoteroDatabase",
|
|
17
|
+
"extract_items",
|
|
18
|
+
"extract_creators",
|
|
19
|
+
"extract_collections",
|
|
20
|
+
"extract_libraries",
|
|
21
|
+
"extract_cv_items",
|
|
22
|
+
"format_apa_reference",
|
|
23
|
+
"format_cv_as_apa",
|
|
24
|
+
]
|
zotlib/backup.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Backup Zotero data directory as a compressed archive."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tarfile
|
|
5
|
+
from datetime import date
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.progress import (
|
|
10
|
+
BarColumn,
|
|
11
|
+
MofNCompleteColumn,
|
|
12
|
+
Progress,
|
|
13
|
+
SpinnerColumn,
|
|
14
|
+
TextColumn,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_directory_stats(source_dir: Path) -> tuple[int, int]:
|
|
19
|
+
"""Walk a directory and return (file_count, total_bytes)."""
|
|
20
|
+
file_count = 0
|
|
21
|
+
total_bytes = 0
|
|
22
|
+
for dirpath, _dirnames, filenames in os.walk(source_dir):
|
|
23
|
+
for filename in filenames:
|
|
24
|
+
filepath = Path(dirpath) / filename
|
|
25
|
+
try:
|
|
26
|
+
total_bytes += filepath.stat().st_size
|
|
27
|
+
file_count += 1
|
|
28
|
+
except OSError as e:
|
|
29
|
+
print(f"Warning: skipped {filepath}: {e}")
|
|
30
|
+
continue
|
|
31
|
+
return file_count, total_bytes
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def default_backup_path() -> Path:
|
|
35
|
+
"""Generate default backup path in data/backups/."""
|
|
36
|
+
today = date.today().isoformat()
|
|
37
|
+
return Path("data") / "backups" / f"zotero-{today}.tar.bz2"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_backup(
|
|
41
|
+
source_dir: Path,
|
|
42
|
+
output_path: Path,
|
|
43
|
+
console: Console,
|
|
44
|
+
) -> Path:
|
|
45
|
+
"""Archive a Zotero data directory as .tar.bz2.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
source_dir: The Zotero data directory to archive.
|
|
49
|
+
output_path: Path for the output .tar.bz2 file.
|
|
50
|
+
console: Rich console for progress output.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Path to the created archive.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
FileNotFoundError: If source_dir does not exist.
|
|
57
|
+
FileExistsError: If output_path already exists.
|
|
58
|
+
"""
|
|
59
|
+
if not source_dir.is_dir():
|
|
60
|
+
raise FileNotFoundError(f"Zotero directory not found: {source_dir}")
|
|
61
|
+
|
|
62
|
+
if output_path.exists():
|
|
63
|
+
raise FileExistsError(f"Output file already exists: {output_path}")
|
|
64
|
+
|
|
65
|
+
file_count, total_bytes = get_directory_stats(source_dir)
|
|
66
|
+
size_mb = total_bytes / (1024 * 1024)
|
|
67
|
+
console.print(f"Source: {source_dir}")
|
|
68
|
+
console.print(f"Files: {file_count:,} ({size_mb:,.1f} MB)")
|
|
69
|
+
|
|
70
|
+
archived = 0
|
|
71
|
+
with Progress(
|
|
72
|
+
SpinnerColumn(),
|
|
73
|
+
TextColumn("[progress.description]{task.description}"),
|
|
74
|
+
BarColumn(),
|
|
75
|
+
MofNCompleteColumn(),
|
|
76
|
+
console=console,
|
|
77
|
+
) as progress:
|
|
78
|
+
task = progress.add_task("Archiving", total=file_count)
|
|
79
|
+
|
|
80
|
+
with tarfile.open(output_path, "w:bz2") as tar:
|
|
81
|
+
for dirpath, _dirnames, filenames in os.walk(source_dir):
|
|
82
|
+
rel_dir = Path(dirpath).relative_to(source_dir.parent)
|
|
83
|
+
tar.add(dirpath, arcname=str(rel_dir), recursive=False)
|
|
84
|
+
|
|
85
|
+
for filename in filenames:
|
|
86
|
+
filepath = Path(dirpath) / filename
|
|
87
|
+
arcname = str(rel_dir / filename)
|
|
88
|
+
try:
|
|
89
|
+
tar.add(filepath, arcname=arcname)
|
|
90
|
+
archived += 1
|
|
91
|
+
progress.update(task, completed=archived)
|
|
92
|
+
except OSError as e:
|
|
93
|
+
progress.console.print(f"[yellow]Warning: skipped {filepath}: {e}[/yellow]")
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
archive_mb = output_path.stat().st_size / (1024 * 1024)
|
|
97
|
+
console.print(f"Saved: {output_path} ({archive_mb:,.1f} MB)")
|
|
98
|
+
return output_path
|
zotlib/cli.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""Command-line interface for zotlib."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import polars as pl
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from zotlib.backup import create_backup, default_backup_path
|
|
12
|
+
from zotlib.config import (
|
|
13
|
+
CONFIG_FILE,
|
|
14
|
+
discover_pdfs_dir,
|
|
15
|
+
discover_zotero_database,
|
|
16
|
+
get_database_path,
|
|
17
|
+
get_pdfs_dir,
|
|
18
|
+
load_config,
|
|
19
|
+
write_config,
|
|
20
|
+
)
|
|
21
|
+
from zotlib.covers import generate_covers, generate_thumbnails
|
|
22
|
+
from zotlib.database import ZoteroDatabase
|
|
23
|
+
from zotlib.exporters import export_collection
|
|
24
|
+
from zotlib.extractors import (
|
|
25
|
+
extract_collections,
|
|
26
|
+
extract_creators,
|
|
27
|
+
extract_cv_items,
|
|
28
|
+
extract_items,
|
|
29
|
+
extract_libraries,
|
|
30
|
+
)
|
|
31
|
+
from zotlib.formatters.apa import format_cv_as_apa
|
|
32
|
+
from zotlib.tables import ALL_SCHEMAS, CORE_SCHEMAS, SCHEMA_MAP
|
|
33
|
+
|
|
34
|
+
app = typer.Typer(
|
|
35
|
+
name="zotlib",
|
|
36
|
+
help="Extract and format bibliographic data from Zotero databases.",
|
|
37
|
+
)
|
|
38
|
+
console = Console()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# --- Init ---
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def init():
|
|
46
|
+
"""Discover Zotero paths and save to zotlib.toml.
|
|
47
|
+
|
|
48
|
+
Auto-discovers the Zotero database and linked PDFs directory,
|
|
49
|
+
then writes them to a config file for future use.
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
zotlib init
|
|
53
|
+
"""
|
|
54
|
+
# Check for existing config
|
|
55
|
+
existing = load_config()
|
|
56
|
+
if existing:
|
|
57
|
+
console.print(f"[yellow]Existing {CONFIG_FILE}:[/yellow]")
|
|
58
|
+
for key, value in existing.items():
|
|
59
|
+
console.print(f" {key} = {value}")
|
|
60
|
+
overwrite = typer.confirm("Overwrite?", default=False)
|
|
61
|
+
if not overwrite:
|
|
62
|
+
raise typer.Abort()
|
|
63
|
+
|
|
64
|
+
# Discover database
|
|
65
|
+
db_path = discover_zotero_database()
|
|
66
|
+
if db_path:
|
|
67
|
+
console.print(f"[green]database:[/green] {db_path}")
|
|
68
|
+
else:
|
|
69
|
+
console.print("[red]Could not find Zotero database[/red]")
|
|
70
|
+
raise typer.Exit(1)
|
|
71
|
+
|
|
72
|
+
# Discover PDFs directory
|
|
73
|
+
pdfs_dir = discover_pdfs_dir(check_exists=False)
|
|
74
|
+
if pdfs_dir:
|
|
75
|
+
console.print(f"[green]pdfs_dir:[/green] {pdfs_dir}")
|
|
76
|
+
if not pdfs_dir.exists():
|
|
77
|
+
console.print("[yellow] Path not currently accessible[/yellow]")
|
|
78
|
+
else:
|
|
79
|
+
console.print("[yellow]Could not find linked PDFs directory[/yellow]")
|
|
80
|
+
|
|
81
|
+
# Write config
|
|
82
|
+
config_path = write_config(db_path, pdfs_dir)
|
|
83
|
+
console.print(f"\nSaved to {config_path}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# --- Explore ---
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("show-tables")
|
|
90
|
+
def show_tables(
|
|
91
|
+
table_name: Annotated[
|
|
92
|
+
str | None,
|
|
93
|
+
typer.Argument(help="Table name to show schema for (optional)"),
|
|
94
|
+
] = None,
|
|
95
|
+
database: Annotated[
|
|
96
|
+
Path | None,
|
|
97
|
+
typer.Option("--database", "-d", help="Path to zotero.sqlite"),
|
|
98
|
+
] = None,
|
|
99
|
+
all: Annotated[
|
|
100
|
+
bool,
|
|
101
|
+
typer.Option("--all", "-a", help="List all tables in the database"),
|
|
102
|
+
] = False,
|
|
103
|
+
):
|
|
104
|
+
"""Show Zotero database schema for tables used by zotlib.
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
zotlib show-tables
|
|
108
|
+
zotlib show-tables items
|
|
109
|
+
zotlib show-tables itemAnnotations
|
|
110
|
+
zotlib show-tables --all
|
|
111
|
+
"""
|
|
112
|
+
wide = Console(width=250, force_terminal=True)
|
|
113
|
+
if all:
|
|
114
|
+
db_path = get_database_path(database)
|
|
115
|
+
db = ZoteroDatabase(db_path)
|
|
116
|
+
table_names = db.get_table_names()
|
|
117
|
+
table = Table(title=f"All Tables in {db_path.name}")
|
|
118
|
+
table.add_column("Table", style="cyan", no_wrap=True)
|
|
119
|
+
table.add_column("Description", no_wrap=True)
|
|
120
|
+
table.add_column("Columns")
|
|
121
|
+
for name in sorted(table_names):
|
|
122
|
+
schema = SCHEMA_MAP.get(name)
|
|
123
|
+
desc = schema.description if schema else ""
|
|
124
|
+
cols = ",".join(schema.columns.keys()) if schema else ""
|
|
125
|
+
if len(cols) > 132:
|
|
126
|
+
cols = cols[:132].rsplit(",", 1)[0] + ",..."
|
|
127
|
+
table.add_row(name, desc, cols)
|
|
128
|
+
wide.print(table)
|
|
129
|
+
elif table_name:
|
|
130
|
+
# Show specific table
|
|
131
|
+
for s in ALL_SCHEMAS:
|
|
132
|
+
if s.name == table_name:
|
|
133
|
+
table = Table(show_header=True)
|
|
134
|
+
table.add_column("Table", style="cyan bold")
|
|
135
|
+
table.add_column("Column", style="green")
|
|
136
|
+
table.add_column("Type")
|
|
137
|
+
table.add_column("Description")
|
|
138
|
+
for i, (col, col_def) in enumerate(s.columns.items()):
|
|
139
|
+
table.add_row(
|
|
140
|
+
s.name if i == 0 else "",
|
|
141
|
+
col,
|
|
142
|
+
col_def.type,
|
|
143
|
+
col_def.description,
|
|
144
|
+
)
|
|
145
|
+
wide.print(table)
|
|
146
|
+
return
|
|
147
|
+
wide.print(f"[red]Unknown table: {table_name}[/red]")
|
|
148
|
+
wide.print(f"Available: {', '.join(s.name for s in ALL_SCHEMAS)}")
|
|
149
|
+
else:
|
|
150
|
+
# Show core tables used by zotlib
|
|
151
|
+
table = Table(title="Zotero Database Schema (used by zotlib)")
|
|
152
|
+
table.add_column("Table", style="cyan", no_wrap=True)
|
|
153
|
+
table.add_column("Description", no_wrap=True)
|
|
154
|
+
table.add_column("Columns")
|
|
155
|
+
for s in CORE_SCHEMAS:
|
|
156
|
+
cols = ",".join(s.columns.keys())
|
|
157
|
+
if len(cols) > 132:
|
|
158
|
+
cols = cols[:132].rsplit(",", 1)[0] + ",..."
|
|
159
|
+
table.add_row(s.name, s.description, cols)
|
|
160
|
+
wide.print(table)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@app.command("show-collections")
|
|
164
|
+
def show_collections(
|
|
165
|
+
database: Annotated[
|
|
166
|
+
Path | None,
|
|
167
|
+
typer.Option("--database", "-d", help="Path to zotero.sqlite"),
|
|
168
|
+
] = None,
|
|
169
|
+
):
|
|
170
|
+
"""List all collections in the Zotero library."""
|
|
171
|
+
db_path = get_database_path(database)
|
|
172
|
+
db = ZoteroDatabase(db_path)
|
|
173
|
+
|
|
174
|
+
colls = extract_collections(db)
|
|
175
|
+
unique_collections = colls.select("collectionID", "collectionName").unique()
|
|
176
|
+
counts = colls.group_by("collectionName").agg(pl.len().alias("items"))
|
|
177
|
+
unique_collections = unique_collections.join(counts, on="collectionName")
|
|
178
|
+
|
|
179
|
+
table = Table(title="Zotero Collections")
|
|
180
|
+
table.add_column("ID", style="dim")
|
|
181
|
+
table.add_column("Name", style="bold")
|
|
182
|
+
table.add_column("Items", justify="right")
|
|
183
|
+
|
|
184
|
+
for row in unique_collections.iter_rows(named=True):
|
|
185
|
+
table.add_row(
|
|
186
|
+
str(row["collectionID"]),
|
|
187
|
+
row["collectionName"],
|
|
188
|
+
str(row["items"]),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
console.print(table)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# --- Export ---
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@app.command("export-annotations")
|
|
198
|
+
def export_annotations(
|
|
199
|
+
collection: Annotated[
|
|
200
|
+
str,
|
|
201
|
+
typer.Option("--collection", "-c", help="Collection name"),
|
|
202
|
+
],
|
|
203
|
+
database: Annotated[
|
|
204
|
+
Path | None,
|
|
205
|
+
typer.Option("--database", "-d", help="Path to zotero.sqlite"),
|
|
206
|
+
] = None,
|
|
207
|
+
output_dir: Annotated[
|
|
208
|
+
Path,
|
|
209
|
+
typer.Option("--output", "-o", help="Output directory"),
|
|
210
|
+
] = Path("output/export-annotations"),
|
|
211
|
+
pdfs_dir: Annotated[
|
|
212
|
+
Path | None,
|
|
213
|
+
typer.Option("--pdfs-dir", "-p", help="Directory for linked PDF attachments"),
|
|
214
|
+
] = None,
|
|
215
|
+
):
|
|
216
|
+
"""Export collection with baked annotations and markdown notes.
|
|
217
|
+
|
|
218
|
+
For each item in the collection, exports a subdirectory containing
|
|
219
|
+
the PDF (with annotations baked in) and a markdown file with YAML
|
|
220
|
+
frontmatter and annotation text.
|
|
221
|
+
|
|
222
|
+
Examples:
|
|
223
|
+
zotlib export-annotations -c mycollection
|
|
224
|
+
zotlib export-annotations -c mycollection -p "/mnt/i/My Drive/zotero-pdfs/"
|
|
225
|
+
zotlib export-annotations -c mycollection -o custom/output/path
|
|
226
|
+
"""
|
|
227
|
+
db_path = get_database_path(database)
|
|
228
|
+
console.print(f"Using database: {db_path}")
|
|
229
|
+
|
|
230
|
+
db = ZoteroDatabase(db_path)
|
|
231
|
+
if pdfs_dir is None:
|
|
232
|
+
pdfs_dir = get_pdfs_dir()
|
|
233
|
+
if pdfs_dir:
|
|
234
|
+
console.print(f"Using linked PDFs directory: {pdfs_dir}")
|
|
235
|
+
collection_dir = output_dir / collection
|
|
236
|
+
|
|
237
|
+
exported, skipped, warnings = export_collection(
|
|
238
|
+
db, collection, collection_dir, pdfs_dir=pdfs_dir, console=console
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
console.print(f"\nExported {exported} items to {collection_dir}")
|
|
242
|
+
if skipped:
|
|
243
|
+
console.print(f"[yellow]Skipped {skipped} items (no PDF or annotations)[/yellow]")
|
|
244
|
+
if warnings:
|
|
245
|
+
console.print("[yellow]Warnings:[/yellow]")
|
|
246
|
+
for w in warnings:
|
|
247
|
+
console.print(f" - {w}")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@app.command("export-apa")
|
|
251
|
+
def export_apa(
|
|
252
|
+
collection: Annotated[
|
|
253
|
+
str,
|
|
254
|
+
typer.Option("--collection", "-c", help="Collection name"),
|
|
255
|
+
],
|
|
256
|
+
database: Annotated[
|
|
257
|
+
Path | None,
|
|
258
|
+
typer.Option("--database", "-d", help="Path to zotero.sqlite"),
|
|
259
|
+
] = None,
|
|
260
|
+
output_dir: Annotated[
|
|
261
|
+
Path,
|
|
262
|
+
typer.Option("--output", "-o", help="Output directory"),
|
|
263
|
+
] = Path("output/export-apa"),
|
|
264
|
+
group_by: Annotated[
|
|
265
|
+
str,
|
|
266
|
+
typer.Option("--group-by", "-g", help="Column to group references by"),
|
|
267
|
+
] = "typeName",
|
|
268
|
+
):
|
|
269
|
+
"""Format collection items as APA references.
|
|
270
|
+
|
|
271
|
+
Examples:
|
|
272
|
+
zotlib export-apa -c publications
|
|
273
|
+
zotlib export-apa -c publications -g year
|
|
274
|
+
"""
|
|
275
|
+
db_path = get_database_path(database)
|
|
276
|
+
console.print(f"Using database: {db_path}")
|
|
277
|
+
|
|
278
|
+
db = ZoteroDatabase(db_path)
|
|
279
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
|
|
281
|
+
items = extract_cv_items(db, collection)
|
|
282
|
+
apa_path = output_dir / f"{collection}.md"
|
|
283
|
+
format_cv_as_apa(items, apa_path, group_by=group_by)
|
|
284
|
+
console.print(f"Saved: {apa_path} ({len(items)} items)")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@app.command("export-covers")
|
|
288
|
+
def export_covers(
|
|
289
|
+
collection: Annotated[
|
|
290
|
+
str,
|
|
291
|
+
typer.Option("--collection", "-c", help="Collection name"),
|
|
292
|
+
],
|
|
293
|
+
database: Annotated[
|
|
294
|
+
Path | None,
|
|
295
|
+
typer.Option("--database", "-d", help="Path to zotero.sqlite"),
|
|
296
|
+
] = None,
|
|
297
|
+
output_dir: Annotated[
|
|
298
|
+
Path,
|
|
299
|
+
typer.Option("--output", "-o", help="Output directory"),
|
|
300
|
+
] = Path("output/export-covers"),
|
|
301
|
+
pdfs_dir: Annotated[
|
|
302
|
+
Path | None,
|
|
303
|
+
typer.Option("--pdfs-dir", "-p", help="Directory for linked PDF attachments"),
|
|
304
|
+
] = None,
|
|
305
|
+
dpi: Annotated[
|
|
306
|
+
int,
|
|
307
|
+
typer.Option("--dpi", help="Image resolution in DPI"),
|
|
308
|
+
] = 300,
|
|
309
|
+
thumbnail_width: Annotated[
|
|
310
|
+
int,
|
|
311
|
+
typer.Option("--thumb-width", "-w", help="Thumbnail width in pixels"),
|
|
312
|
+
] = 400,
|
|
313
|
+
no_thumbnails: Annotated[
|
|
314
|
+
bool,
|
|
315
|
+
typer.Option("--no-thumbnails", help="Skip thumbnail generation"),
|
|
316
|
+
] = False,
|
|
317
|
+
):
|
|
318
|
+
"""Generate first-page cover images and thumbnails for PDFs in a collection.
|
|
319
|
+
|
|
320
|
+
Examples:
|
|
321
|
+
zotlib export-covers -c publications
|
|
322
|
+
zotlib export-covers -c publications -p "/mnt/i/My Drive/zotero-pdfs/"
|
|
323
|
+
zotlib export-covers -c publications --no-thumbnails
|
|
324
|
+
"""
|
|
325
|
+
db_path = get_database_path(database)
|
|
326
|
+
console.print(f"Using database: {db_path}")
|
|
327
|
+
|
|
328
|
+
db = ZoteroDatabase(db_path)
|
|
329
|
+
if pdfs_dir is None:
|
|
330
|
+
pdfs_dir = get_pdfs_dir()
|
|
331
|
+
if pdfs_dir:
|
|
332
|
+
console.print(f"Using linked PDFs directory: {pdfs_dir}")
|
|
333
|
+
fullsize_dir = output_dir / collection / "fullsize"
|
|
334
|
+
|
|
335
|
+
cover_paths, skipped = generate_covers(db, collection, fullsize_dir, dpi=dpi, pdfs_dir=pdfs_dir)
|
|
336
|
+
console.print(f"Generated {len(cover_paths)} cover images in {fullsize_dir}")
|
|
337
|
+
|
|
338
|
+
# Generate thumbnails
|
|
339
|
+
if not no_thumbnails:
|
|
340
|
+
thumbs_dir = output_dir / collection / "thumbnails"
|
|
341
|
+
count = generate_thumbnails(fullsize_dir, thumbs_dir, width=thumbnail_width)
|
|
342
|
+
console.print(f"Generated {count} thumbnails ({thumbnail_width}px wide) in {thumbs_dir}")
|
|
343
|
+
|
|
344
|
+
# Add cover paths to collection CSV (sibling export-csv directory)
|
|
345
|
+
csv_dir = output_dir.parent / "export-csv"
|
|
346
|
+
csv_dir.mkdir(parents=True, exist_ok=True)
|
|
347
|
+
csv_path = csv_dir / f"{collection}.csv"
|
|
348
|
+
if csv_path.exists():
|
|
349
|
+
items_df = pl.read_csv(csv_path)
|
|
350
|
+
else:
|
|
351
|
+
items_df = extract_cv_items(db, collection)
|
|
352
|
+
|
|
353
|
+
cover_map = {item_id: str(path) for item_id, path in cover_paths.items()}
|
|
354
|
+
items_df = items_df.with_columns(
|
|
355
|
+
pl.col("itemID").replace_strict(cover_map, default=None).alias("cover")
|
|
356
|
+
)
|
|
357
|
+
items_df.write_csv(csv_path)
|
|
358
|
+
console.print(f"Saved: {csv_path}")
|
|
359
|
+
|
|
360
|
+
if skipped:
|
|
361
|
+
console.print(f"[yellow]Skipped {len(skipped)} missing PDFs:[/yellow]")
|
|
362
|
+
for item in skipped:
|
|
363
|
+
console.print(f" - {item}")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@app.command("export-csv")
|
|
367
|
+
def export_csv(
|
|
368
|
+
database: Annotated[
|
|
369
|
+
Path | None,
|
|
370
|
+
typer.Option("--database", "-d", help="Path to zotero.sqlite"),
|
|
371
|
+
] = None,
|
|
372
|
+
output_dir: Annotated[
|
|
373
|
+
Path,
|
|
374
|
+
typer.Option("--output", "-o", help="Output directory"),
|
|
375
|
+
] = Path("output/export-csv"),
|
|
376
|
+
collection: Annotated[
|
|
377
|
+
str | None,
|
|
378
|
+
typer.Option("--collection", "-c", help="Filter to collection name"),
|
|
379
|
+
] = None,
|
|
380
|
+
):
|
|
381
|
+
"""Export bibliographic data from Zotero database as CSV.
|
|
382
|
+
|
|
383
|
+
Examples:
|
|
384
|
+
zotlib export-csv
|
|
385
|
+
zotlib export-csv -c publications
|
|
386
|
+
zotlib export-csv -d /path/to/zotero.sqlite -c rer
|
|
387
|
+
"""
|
|
388
|
+
db_path = get_database_path(database)
|
|
389
|
+
console.print(f"Using database: {db_path}")
|
|
390
|
+
|
|
391
|
+
db = ZoteroDatabase(db_path)
|
|
392
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
393
|
+
|
|
394
|
+
if collection:
|
|
395
|
+
items = extract_cv_items(db, collection)
|
|
396
|
+
csv_path = output_dir / f"{collection}.csv"
|
|
397
|
+
items.write_csv(csv_path)
|
|
398
|
+
console.print(f"Saved: {csv_path} ({len(items)} items)")
|
|
399
|
+
else:
|
|
400
|
+
items = extract_items(db)
|
|
401
|
+
creators = extract_creators(db)
|
|
402
|
+
collections = extract_collections(db)
|
|
403
|
+
libraries = extract_libraries(db)
|
|
404
|
+
|
|
405
|
+
for name, df in [
|
|
406
|
+
("items", items),
|
|
407
|
+
("creators", creators),
|
|
408
|
+
("collections", collections),
|
|
409
|
+
("libraries", libraries),
|
|
410
|
+
]:
|
|
411
|
+
path = output_dir / f"{name}.csv"
|
|
412
|
+
df.write_csv(path)
|
|
413
|
+
console.print(f"Saved: {path} ({len(df)} rows)")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# --- Manage ---
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@app.command()
|
|
420
|
+
def backup(
|
|
421
|
+
database: Annotated[
|
|
422
|
+
Path | None,
|
|
423
|
+
typer.Option("--database", "-d", help="Path to zotero.sqlite"),
|
|
424
|
+
] = None,
|
|
425
|
+
output: Annotated[
|
|
426
|
+
Path | None,
|
|
427
|
+
typer.Option("--output", "-o", help="Output archive path"),
|
|
428
|
+
] = None,
|
|
429
|
+
):
|
|
430
|
+
"""Back up the Zotero data directory as a .tar.bz2 archive.
|
|
431
|
+
|
|
432
|
+
Examples:
|
|
433
|
+
zotlib backup
|
|
434
|
+
zotlib backup -o ~/backups/zotero-2026-03-02.tar.bz2
|
|
435
|
+
zotlib backup -d /path/to/zotero.sqlite
|
|
436
|
+
"""
|
|
437
|
+
db_path = get_database_path(database)
|
|
438
|
+
source_dir = db_path.parent
|
|
439
|
+
|
|
440
|
+
if output is None:
|
|
441
|
+
output = default_backup_path()
|
|
442
|
+
|
|
443
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
444
|
+
create_backup(source_dir, output, console)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
if __name__ == "__main__":
|
|
448
|
+
app()
|