ebk 0.4.4__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.
- ebk/__init__.py +35 -0
- ebk/ai/__init__.py +23 -0
- ebk/ai/knowledge_graph.py +450 -0
- ebk/ai/llm_providers/__init__.py +26 -0
- ebk/ai/llm_providers/anthropic.py +209 -0
- ebk/ai/llm_providers/base.py +295 -0
- ebk/ai/llm_providers/gemini.py +285 -0
- ebk/ai/llm_providers/ollama.py +294 -0
- ebk/ai/metadata_enrichment.py +394 -0
- ebk/ai/question_generator.py +328 -0
- ebk/ai/reading_companion.py +224 -0
- ebk/ai/semantic_search.py +433 -0
- ebk/ai/text_extractor.py +393 -0
- ebk/calibre_import.py +66 -0
- ebk/cli.py +6433 -0
- ebk/config.py +230 -0
- ebk/db/__init__.py +37 -0
- ebk/db/migrations.py +507 -0
- ebk/db/models.py +725 -0
- ebk/db/session.py +144 -0
- ebk/decorators.py +1 -0
- ebk/exports/__init__.py +0 -0
- ebk/exports/base_exporter.py +218 -0
- ebk/exports/echo_export.py +279 -0
- ebk/exports/html_library.py +1743 -0
- ebk/exports/html_utils.py +87 -0
- ebk/exports/hugo.py +59 -0
- ebk/exports/jinja_export.py +286 -0
- ebk/exports/multi_facet_export.py +159 -0
- ebk/exports/opds_export.py +232 -0
- ebk/exports/symlink_dag.py +479 -0
- ebk/exports/zip.py +25 -0
- ebk/extract_metadata.py +341 -0
- ebk/ident.py +89 -0
- ebk/library_db.py +1440 -0
- ebk/opds.py +748 -0
- ebk/plugins/__init__.py +42 -0
- ebk/plugins/base.py +502 -0
- ebk/plugins/hooks.py +442 -0
- ebk/plugins/registry.py +499 -0
- ebk/repl/__init__.py +9 -0
- ebk/repl/find.py +126 -0
- ebk/repl/grep.py +173 -0
- ebk/repl/shell.py +1677 -0
- ebk/repl/text_utils.py +320 -0
- ebk/search_parser.py +413 -0
- ebk/server.py +3608 -0
- ebk/services/__init__.py +28 -0
- ebk/services/annotation_extraction.py +351 -0
- ebk/services/annotation_service.py +380 -0
- ebk/services/export_service.py +577 -0
- ebk/services/import_service.py +447 -0
- ebk/services/personal_metadata_service.py +347 -0
- ebk/services/queue_service.py +253 -0
- ebk/services/tag_service.py +281 -0
- ebk/services/text_extraction.py +317 -0
- ebk/services/view_service.py +12 -0
- ebk/similarity/__init__.py +77 -0
- ebk/similarity/base.py +154 -0
- ebk/similarity/core.py +471 -0
- ebk/similarity/extractors.py +168 -0
- ebk/similarity/metrics.py +376 -0
- ebk/skills/SKILL.md +182 -0
- ebk/skills/__init__.py +1 -0
- ebk/vfs/__init__.py +101 -0
- ebk/vfs/base.py +298 -0
- ebk/vfs/library_vfs.py +122 -0
- ebk/vfs/nodes/__init__.py +54 -0
- ebk/vfs/nodes/authors.py +196 -0
- ebk/vfs/nodes/books.py +480 -0
- ebk/vfs/nodes/files.py +155 -0
- ebk/vfs/nodes/metadata.py +385 -0
- ebk/vfs/nodes/root.py +100 -0
- ebk/vfs/nodes/similar.py +165 -0
- ebk/vfs/nodes/subjects.py +184 -0
- ebk/vfs/nodes/tags.py +371 -0
- ebk/vfs/resolver.py +228 -0
- ebk/vfs_router.py +275 -0
- ebk/views/__init__.py +32 -0
- ebk/views/dsl.py +668 -0
- ebk/views/service.py +619 -0
- ebk-0.4.4.dist-info/METADATA +755 -0
- ebk-0.4.4.dist-info/RECORD +87 -0
- ebk-0.4.4.dist-info/WHEEL +5 -0
- ebk-0.4.4.dist-info/entry_points.txt +2 -0
- ebk-0.4.4.dist-info/licenses/LICENSE +21 -0
- ebk-0.4.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OPDS Catalog Export.
|
|
3
|
+
|
|
4
|
+
Exports the library to a static OPDS (Atom) catalog file that can be
|
|
5
|
+
served from any static file host or used for backup/sharing.
|
|
6
|
+
|
|
7
|
+
OPDS Spec: https://specs.opds.io/opds-1.2
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, List
|
|
12
|
+
import shutil
|
|
13
|
+
|
|
14
|
+
from ..opds import (
|
|
15
|
+
FORMAT_MIMES,
|
|
16
|
+
OPDS_ACQUISITION_MIME,
|
|
17
|
+
escape_xml,
|
|
18
|
+
format_datetime,
|
|
19
|
+
get_mime_type,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_entry(book, base_url: str, files_dir: str = "files", covers_dir: str = "covers") -> str:
|
|
24
|
+
"""Build an OPDS entry for a book."""
|
|
25
|
+
book_id = book.id
|
|
26
|
+
title = escape_xml(book.title or "Untitled")
|
|
27
|
+
|
|
28
|
+
# Authors
|
|
29
|
+
authors_xml = ""
|
|
30
|
+
if book.authors:
|
|
31
|
+
for author in book.authors:
|
|
32
|
+
authors_xml += f"""
|
|
33
|
+
<author>
|
|
34
|
+
<name>{escape_xml(author.name)}</name>
|
|
35
|
+
</author>"""
|
|
36
|
+
|
|
37
|
+
# Summary/description
|
|
38
|
+
summary = ""
|
|
39
|
+
if book.description:
|
|
40
|
+
summary = f"<summary>{escape_xml(book.description[:500])}</summary>"
|
|
41
|
+
|
|
42
|
+
# Categories (subjects)
|
|
43
|
+
categories = ""
|
|
44
|
+
if book.subjects:
|
|
45
|
+
for subj in book.subjects:
|
|
46
|
+
categories += f'<category term="{escape_xml(subj.name)}" label="{escape_xml(subj.name)}"/>'
|
|
47
|
+
|
|
48
|
+
# Language
|
|
49
|
+
language = f"<dc:language>{escape_xml(book.language)}</dc:language>" if book.language else ""
|
|
50
|
+
|
|
51
|
+
# Publisher
|
|
52
|
+
publisher = f"<dc:publisher>{escape_xml(book.publisher)}</dc:publisher>" if book.publisher else ""
|
|
53
|
+
|
|
54
|
+
# Publication date
|
|
55
|
+
pub_date = ""
|
|
56
|
+
if book.publication_date:
|
|
57
|
+
pub_date = f"<dc:date>{escape_xml(str(book.publication_date))}</dc:date>"
|
|
58
|
+
|
|
59
|
+
# Cover image (using static file path)
|
|
60
|
+
cover_link = ""
|
|
61
|
+
if book.covers:
|
|
62
|
+
cover = book.covers[0]
|
|
63
|
+
if cover.path:
|
|
64
|
+
cover_filename = f"{book_id}.jpg"
|
|
65
|
+
cover_url = f"{base_url}/{covers_dir}/{cover_filename}" if base_url else f"{covers_dir}/{cover_filename}"
|
|
66
|
+
cover_link = f'<link rel="http://opds-spec.org/image/thumbnail" href="{cover_url}" type="image/jpeg"/>'
|
|
67
|
+
cover_link += f'\n <link rel="http://opds-spec.org/image" href="{cover_url}" type="image/jpeg"/>'
|
|
68
|
+
|
|
69
|
+
# Acquisition links (download links for each format)
|
|
70
|
+
acquisition_links = ""
|
|
71
|
+
if book.files:
|
|
72
|
+
for file in book.files:
|
|
73
|
+
mime = get_mime_type(file.format)
|
|
74
|
+
size_bytes = file.size_bytes or 0
|
|
75
|
+
size_kb = size_bytes // 1024 if size_bytes else 0
|
|
76
|
+
# Use hash-based filename for static export
|
|
77
|
+
file_ext = file.format.lower()
|
|
78
|
+
file_url = f"{base_url}/{files_dir}/{file.file_hash[:8]}_{book_id}.{file_ext}" if base_url else f"{files_dir}/{file.file_hash[:8]}_{book_id}.{file_ext}"
|
|
79
|
+
acquisition_links += f"""
|
|
80
|
+
<link rel="http://opds-spec.org/acquisition"
|
|
81
|
+
href="{file_url}"
|
|
82
|
+
type="{mime}"
|
|
83
|
+
length="{size_bytes}"
|
|
84
|
+
title="{file.format.upper()} ({size_kb} KB)"/>"""
|
|
85
|
+
|
|
86
|
+
# Updated timestamp
|
|
87
|
+
updated = format_datetime(book.updated_at if hasattr(book, 'updated_at') else None)
|
|
88
|
+
|
|
89
|
+
return f"""
|
|
90
|
+
<entry>
|
|
91
|
+
<id>urn:ebk:book:{book_id}</id>
|
|
92
|
+
<title>{title}</title>
|
|
93
|
+
<updated>{updated}</updated>{authors_xml}
|
|
94
|
+
{summary}
|
|
95
|
+
{categories}
|
|
96
|
+
{language}
|
|
97
|
+
{publisher}
|
|
98
|
+
{pub_date}
|
|
99
|
+
{cover_link}
|
|
100
|
+
{acquisition_links}
|
|
101
|
+
</entry>"""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_feed(
|
|
105
|
+
title: str,
|
|
106
|
+
entries: str,
|
|
107
|
+
base_url: str = "",
|
|
108
|
+
subtitle: str = "",
|
|
109
|
+
author_name: str = "ebk Library",
|
|
110
|
+
) -> str:
|
|
111
|
+
"""Build an OPDS Atom feed."""
|
|
112
|
+
updated = format_datetime()
|
|
113
|
+
feed_id = f"urn:ebk:catalog:{updated.replace(':', '-')}"
|
|
114
|
+
|
|
115
|
+
subtitle_xml = f"<subtitle>{escape_xml(subtitle)}</subtitle>" if subtitle else ""
|
|
116
|
+
base_url_escaped = escape_xml(base_url) if base_url else ""
|
|
117
|
+
|
|
118
|
+
# Self link
|
|
119
|
+
self_link = f'<link rel="self" href="{base_url_escaped}/catalog.xml" type="{OPDS_ACQUISITION_MIME}"/>' if base_url else ""
|
|
120
|
+
start_link = f'<link rel="start" href="{base_url_escaped}/catalog.xml" type="{OPDS_ACQUISITION_MIME}"/>' if base_url else ""
|
|
121
|
+
|
|
122
|
+
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
123
|
+
<feed xmlns="http://www.w3.org/2005/Atom"
|
|
124
|
+
xmlns:dc="http://purl.org/dc/terms/"
|
|
125
|
+
xmlns:opds="http://opds-spec.org/2010/catalog">
|
|
126
|
+
<id>{feed_id}</id>
|
|
127
|
+
<title>{escape_xml(title)}</title>
|
|
128
|
+
{subtitle_xml}
|
|
129
|
+
<updated>{updated}</updated>
|
|
130
|
+
<author>
|
|
131
|
+
<name>{escape_xml(author_name)}</name>
|
|
132
|
+
</author>
|
|
133
|
+
{self_link}
|
|
134
|
+
{start_link}
|
|
135
|
+
{entries}
|
|
136
|
+
</feed>"""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def export_to_opds(
|
|
140
|
+
books: List,
|
|
141
|
+
output_path: Path,
|
|
142
|
+
library_path: Path,
|
|
143
|
+
title: str = "ebk Library",
|
|
144
|
+
subtitle: str = "",
|
|
145
|
+
base_url: str = "",
|
|
146
|
+
copy_files: bool = False,
|
|
147
|
+
copy_covers: bool = False,
|
|
148
|
+
) -> dict:
|
|
149
|
+
"""
|
|
150
|
+
Export library to an OPDS catalog file.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
books: List of Book objects to export
|
|
154
|
+
output_path: Path to output XML file
|
|
155
|
+
library_path: Path to the library (for copying files)
|
|
156
|
+
title: Feed title
|
|
157
|
+
subtitle: Feed subtitle
|
|
158
|
+
base_url: Base URL for file links (e.g., "https://example.com/library")
|
|
159
|
+
copy_files: If True, copy ebook files to output directory
|
|
160
|
+
copy_covers: If True, copy cover images to output directory
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dict with export statistics
|
|
164
|
+
"""
|
|
165
|
+
output_path = Path(output_path)
|
|
166
|
+
output_dir = output_path.parent
|
|
167
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
|
|
169
|
+
files_dir = output_dir / "files"
|
|
170
|
+
covers_dir = output_dir / "covers"
|
|
171
|
+
|
|
172
|
+
stats = {
|
|
173
|
+
"books": len(books),
|
|
174
|
+
"files_copied": 0,
|
|
175
|
+
"covers_copied": 0,
|
|
176
|
+
"errors": []
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if copy_files:
|
|
180
|
+
files_dir.mkdir(exist_ok=True)
|
|
181
|
+
|
|
182
|
+
if copy_covers:
|
|
183
|
+
covers_dir.mkdir(exist_ok=True)
|
|
184
|
+
|
|
185
|
+
# Build entries
|
|
186
|
+
entries = []
|
|
187
|
+
for book in books:
|
|
188
|
+
# Copy files if requested
|
|
189
|
+
if copy_files and book.files:
|
|
190
|
+
for file in book.files:
|
|
191
|
+
try:
|
|
192
|
+
src_path = library_path / file.path
|
|
193
|
+
if src_path.exists():
|
|
194
|
+
file_ext = file.format.lower()
|
|
195
|
+
dst_filename = f"{file.file_hash[:8]}_{book.id}.{file_ext}"
|
|
196
|
+
dst_path = files_dir / dst_filename
|
|
197
|
+
if not dst_path.exists():
|
|
198
|
+
shutil.copy2(src_path, dst_path)
|
|
199
|
+
stats["files_copied"] += 1
|
|
200
|
+
except Exception as e:
|
|
201
|
+
stats["errors"].append(f"Failed to copy file for book {book.id}: {e}")
|
|
202
|
+
|
|
203
|
+
# Copy covers if requested
|
|
204
|
+
if copy_covers and book.covers:
|
|
205
|
+
try:
|
|
206
|
+
cover = book.covers[0]
|
|
207
|
+
if cover.path:
|
|
208
|
+
src_path = library_path / cover.path
|
|
209
|
+
if src_path.exists():
|
|
210
|
+
dst_filename = f"{book.id}.jpg"
|
|
211
|
+
dst_path = covers_dir / dst_filename
|
|
212
|
+
if not dst_path.exists():
|
|
213
|
+
shutil.copy2(src_path, dst_path)
|
|
214
|
+
stats["covers_copied"] += 1
|
|
215
|
+
except Exception as e:
|
|
216
|
+
stats["errors"].append(f"Failed to copy cover for book {book.id}: {e}")
|
|
217
|
+
|
|
218
|
+
# Build entry
|
|
219
|
+
entries.append(build_entry(book, base_url, "files", "covers"))
|
|
220
|
+
|
|
221
|
+
# Build feed
|
|
222
|
+
feed = build_feed(
|
|
223
|
+
title=title,
|
|
224
|
+
entries="".join(entries),
|
|
225
|
+
base_url=base_url,
|
|
226
|
+
subtitle=subtitle,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Write output
|
|
230
|
+
output_path.write_text(feed, encoding="utf-8")
|
|
231
|
+
|
|
232
|
+
return stats
|