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.
Files changed (87) hide show
  1. ebk/__init__.py +35 -0
  2. ebk/ai/__init__.py +23 -0
  3. ebk/ai/knowledge_graph.py +450 -0
  4. ebk/ai/llm_providers/__init__.py +26 -0
  5. ebk/ai/llm_providers/anthropic.py +209 -0
  6. ebk/ai/llm_providers/base.py +295 -0
  7. ebk/ai/llm_providers/gemini.py +285 -0
  8. ebk/ai/llm_providers/ollama.py +294 -0
  9. ebk/ai/metadata_enrichment.py +394 -0
  10. ebk/ai/question_generator.py +328 -0
  11. ebk/ai/reading_companion.py +224 -0
  12. ebk/ai/semantic_search.py +433 -0
  13. ebk/ai/text_extractor.py +393 -0
  14. ebk/calibre_import.py +66 -0
  15. ebk/cli.py +6433 -0
  16. ebk/config.py +230 -0
  17. ebk/db/__init__.py +37 -0
  18. ebk/db/migrations.py +507 -0
  19. ebk/db/models.py +725 -0
  20. ebk/db/session.py +144 -0
  21. ebk/decorators.py +1 -0
  22. ebk/exports/__init__.py +0 -0
  23. ebk/exports/base_exporter.py +218 -0
  24. ebk/exports/echo_export.py +279 -0
  25. ebk/exports/html_library.py +1743 -0
  26. ebk/exports/html_utils.py +87 -0
  27. ebk/exports/hugo.py +59 -0
  28. ebk/exports/jinja_export.py +286 -0
  29. ebk/exports/multi_facet_export.py +159 -0
  30. ebk/exports/opds_export.py +232 -0
  31. ebk/exports/symlink_dag.py +479 -0
  32. ebk/exports/zip.py +25 -0
  33. ebk/extract_metadata.py +341 -0
  34. ebk/ident.py +89 -0
  35. ebk/library_db.py +1440 -0
  36. ebk/opds.py +748 -0
  37. ebk/plugins/__init__.py +42 -0
  38. ebk/plugins/base.py +502 -0
  39. ebk/plugins/hooks.py +442 -0
  40. ebk/plugins/registry.py +499 -0
  41. ebk/repl/__init__.py +9 -0
  42. ebk/repl/find.py +126 -0
  43. ebk/repl/grep.py +173 -0
  44. ebk/repl/shell.py +1677 -0
  45. ebk/repl/text_utils.py +320 -0
  46. ebk/search_parser.py +413 -0
  47. ebk/server.py +3608 -0
  48. ebk/services/__init__.py +28 -0
  49. ebk/services/annotation_extraction.py +351 -0
  50. ebk/services/annotation_service.py +380 -0
  51. ebk/services/export_service.py +577 -0
  52. ebk/services/import_service.py +447 -0
  53. ebk/services/personal_metadata_service.py +347 -0
  54. ebk/services/queue_service.py +253 -0
  55. ebk/services/tag_service.py +281 -0
  56. ebk/services/text_extraction.py +317 -0
  57. ebk/services/view_service.py +12 -0
  58. ebk/similarity/__init__.py +77 -0
  59. ebk/similarity/base.py +154 -0
  60. ebk/similarity/core.py +471 -0
  61. ebk/similarity/extractors.py +168 -0
  62. ebk/similarity/metrics.py +376 -0
  63. ebk/skills/SKILL.md +182 -0
  64. ebk/skills/__init__.py +1 -0
  65. ebk/vfs/__init__.py +101 -0
  66. ebk/vfs/base.py +298 -0
  67. ebk/vfs/library_vfs.py +122 -0
  68. ebk/vfs/nodes/__init__.py +54 -0
  69. ebk/vfs/nodes/authors.py +196 -0
  70. ebk/vfs/nodes/books.py +480 -0
  71. ebk/vfs/nodes/files.py +155 -0
  72. ebk/vfs/nodes/metadata.py +385 -0
  73. ebk/vfs/nodes/root.py +100 -0
  74. ebk/vfs/nodes/similar.py +165 -0
  75. ebk/vfs/nodes/subjects.py +184 -0
  76. ebk/vfs/nodes/tags.py +371 -0
  77. ebk/vfs/resolver.py +228 -0
  78. ebk/vfs_router.py +275 -0
  79. ebk/views/__init__.py +32 -0
  80. ebk/views/dsl.py +668 -0
  81. ebk/views/service.py +619 -0
  82. ebk-0.4.4.dist-info/METADATA +755 -0
  83. ebk-0.4.4.dist-info/RECORD +87 -0
  84. ebk-0.4.4.dist-info/WHEEL +5 -0
  85. ebk-0.4.4.dist-info/entry_points.txt +2 -0
  86. ebk-0.4.4.dist-info/licenses/LICENSE +21 -0
  87. 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