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,54 @@
|
|
|
1
|
+
"""VFS node implementations."""
|
|
2
|
+
|
|
3
|
+
from ebk.vfs.nodes.root import RootNode
|
|
4
|
+
from ebk.vfs.nodes.books import BooksDirectoryNode, BookNode
|
|
5
|
+
from ebk.vfs.nodes.metadata import (
|
|
6
|
+
TitleFileNode,
|
|
7
|
+
AuthorsFileNode,
|
|
8
|
+
SubjectsFileNode,
|
|
9
|
+
DescriptionFileNode,
|
|
10
|
+
TextFileNode,
|
|
11
|
+
YearFileNode,
|
|
12
|
+
LanguageFileNode,
|
|
13
|
+
PublisherFileNode,
|
|
14
|
+
MetadataFileNode,
|
|
15
|
+
)
|
|
16
|
+
from ebk.vfs.nodes.files import FilesDirectoryNode, PhysicalFileNode
|
|
17
|
+
from ebk.vfs.nodes.similar import SimilarDirectoryNode, SimilarBookSymlink
|
|
18
|
+
from ebk.vfs.nodes.authors import AuthorsDirectoryNode, AuthorNode
|
|
19
|
+
from ebk.vfs.nodes.subjects import SubjectsDirectoryNode, SubjectNode
|
|
20
|
+
from ebk.vfs.nodes.tags import (
|
|
21
|
+
TagsDirectoryNode,
|
|
22
|
+
TagNode,
|
|
23
|
+
TagDescriptionFile,
|
|
24
|
+
TagColorFile,
|
|
25
|
+
TagStatsFile,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"RootNode",
|
|
30
|
+
"BooksDirectoryNode",
|
|
31
|
+
"BookNode",
|
|
32
|
+
"TitleFileNode",
|
|
33
|
+
"AuthorsFileNode",
|
|
34
|
+
"SubjectsFileNode",
|
|
35
|
+
"DescriptionFileNode",
|
|
36
|
+
"TextFileNode",
|
|
37
|
+
"YearFileNode",
|
|
38
|
+
"LanguageFileNode",
|
|
39
|
+
"PublisherFileNode",
|
|
40
|
+
"MetadataFileNode",
|
|
41
|
+
"FilesDirectoryNode",
|
|
42
|
+
"PhysicalFileNode",
|
|
43
|
+
"SimilarDirectoryNode",
|
|
44
|
+
"SimilarBookSymlink",
|
|
45
|
+
"AuthorsDirectoryNode",
|
|
46
|
+
"AuthorNode",
|
|
47
|
+
"SubjectsDirectoryNode",
|
|
48
|
+
"SubjectNode",
|
|
49
|
+
"TagsDirectoryNode",
|
|
50
|
+
"TagNode",
|
|
51
|
+
"TagDescriptionFile",
|
|
52
|
+
"TagColorFile",
|
|
53
|
+
"TagStatsFile",
|
|
54
|
+
]
|
ebk/vfs/nodes/authors.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Author-related VFS nodes."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Dict, Any
|
|
4
|
+
|
|
5
|
+
from ebk.vfs.base import VirtualNode, DirectoryNode, SymlinkNode, Node
|
|
6
|
+
from ebk.library_db import Library
|
|
7
|
+
from ebk.db.models import Author
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthorsDirectoryNode(VirtualNode):
|
|
11
|
+
"""/authors/ - Virtual directory listing all authors.
|
|
12
|
+
|
|
13
|
+
Each child is an AuthorNode representing books by that author.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, library: Library, parent: Optional[DirectoryNode] = None):
|
|
17
|
+
"""Initialize authors directory.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
library: Library instance
|
|
21
|
+
parent: Parent node (usually root)
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(name="authors", parent=parent)
|
|
24
|
+
self.library = library
|
|
25
|
+
|
|
26
|
+
def list_children(self) -> List[Node]:
|
|
27
|
+
"""List all authors.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of AuthorNode instances
|
|
31
|
+
"""
|
|
32
|
+
# Query all authors from database
|
|
33
|
+
# For now, we'll get unique authors from books
|
|
34
|
+
authors_query = self.library.session.query(Author).all()
|
|
35
|
+
|
|
36
|
+
author_nodes = []
|
|
37
|
+
for author in authors_query:
|
|
38
|
+
# Create a slug from author name
|
|
39
|
+
slug = self._make_slug(author.name)
|
|
40
|
+
node = AuthorNode(author, slug, self.library, parent=self)
|
|
41
|
+
author_nodes.append(node)
|
|
42
|
+
|
|
43
|
+
return author_nodes
|
|
44
|
+
|
|
45
|
+
def get_child(self, name: str) -> Optional[Node]:
|
|
46
|
+
"""Get an author by slug.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
name: Author slug (e.g., "knuth-donald")
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
AuthorNode or None
|
|
53
|
+
"""
|
|
54
|
+
# Try to find author by matching slug
|
|
55
|
+
authors = self.library.session.query(Author).all()
|
|
56
|
+
|
|
57
|
+
for author in authors:
|
|
58
|
+
slug = self._make_slug(author.name)
|
|
59
|
+
if slug == name:
|
|
60
|
+
return AuthorNode(author, slug, self.library, parent=self)
|
|
61
|
+
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def _make_slug(self, name: str) -> str:
|
|
65
|
+
"""Convert author name to filesystem-safe slug.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
name: Author name
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Slugified name (e.g., "Donald Knuth" -> "knuth-donald")
|
|
72
|
+
"""
|
|
73
|
+
# Simple slugification: lowercase, replace spaces with hyphens
|
|
74
|
+
# Reverse name order (Last, First -> first-last)
|
|
75
|
+
parts = name.lower().split()
|
|
76
|
+
if len(parts) >= 2:
|
|
77
|
+
# Assume "First Last" or "Last, First"
|
|
78
|
+
if "," in name:
|
|
79
|
+
# "Last, First" format
|
|
80
|
+
slug = "-".join(reversed([p.strip(",") for p in parts]))
|
|
81
|
+
else:
|
|
82
|
+
# "First Last" format - reverse to "last-first"
|
|
83
|
+
slug = "-".join(reversed(parts))
|
|
84
|
+
else:
|
|
85
|
+
slug = "-".join(parts)
|
|
86
|
+
|
|
87
|
+
# Remove special characters
|
|
88
|
+
slug = "".join(c for c in slug if c.isalnum() or c == "-")
|
|
89
|
+
return slug
|
|
90
|
+
|
|
91
|
+
def get_info(self) -> Dict[str, Any]:
|
|
92
|
+
"""Get authors directory info.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Dict with directory information
|
|
96
|
+
"""
|
|
97
|
+
total = self.library.session.query(Author).count()
|
|
98
|
+
return {
|
|
99
|
+
"type": "virtual",
|
|
100
|
+
"name": "authors",
|
|
101
|
+
"total_authors": total,
|
|
102
|
+
"path": self.get_path(),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class AuthorNode(VirtualNode):
|
|
107
|
+
"""/authors/knuth-donald/ - Books by a specific author.
|
|
108
|
+
|
|
109
|
+
Contains symlinks to books by this author.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
author: Author,
|
|
115
|
+
slug: str,
|
|
116
|
+
library: Library,
|
|
117
|
+
parent: Optional[DirectoryNode] = None,
|
|
118
|
+
):
|
|
119
|
+
"""Initialize author node.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
author: Author database model
|
|
123
|
+
slug: Author slug for URL
|
|
124
|
+
library: Library instance
|
|
125
|
+
parent: Parent node (usually AuthorsDirectoryNode)
|
|
126
|
+
"""
|
|
127
|
+
super().__init__(name=slug, parent=parent)
|
|
128
|
+
self.author = author
|
|
129
|
+
self.library = library
|
|
130
|
+
|
|
131
|
+
def list_children(self) -> List[Node]:
|
|
132
|
+
"""List books by this author as symlinks.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of SymlinkNode instances
|
|
136
|
+
"""
|
|
137
|
+
symlinks = []
|
|
138
|
+
for book in self.author.books:
|
|
139
|
+
target_path = f"/books/{book.id}"
|
|
140
|
+
name = str(book.id)
|
|
141
|
+
|
|
142
|
+
# Include book metadata for display
|
|
143
|
+
metadata = {
|
|
144
|
+
"title": book.title or "Untitled",
|
|
145
|
+
}
|
|
146
|
+
if book.authors:
|
|
147
|
+
metadata["author"] = ", ".join([a.name for a in book.authors])
|
|
148
|
+
|
|
149
|
+
symlink = SymlinkNode(name, target_path, parent=self, metadata=metadata)
|
|
150
|
+
symlinks.append(symlink)
|
|
151
|
+
|
|
152
|
+
return symlinks
|
|
153
|
+
|
|
154
|
+
def get_child(self, name: str) -> Optional[Node]:
|
|
155
|
+
"""Get a book symlink by ID.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
name: Book ID as string
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
SymlinkNode or None
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
book_id = int(name)
|
|
165
|
+
except ValueError:
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
# Check if this book is by this author
|
|
169
|
+
for book in self.author.books:
|
|
170
|
+
if book.id == book_id:
|
|
171
|
+
target_path = f"/books/{book.id}"
|
|
172
|
+
|
|
173
|
+
# Include book metadata for display
|
|
174
|
+
metadata = {
|
|
175
|
+
"title": book.title or "Untitled",
|
|
176
|
+
}
|
|
177
|
+
if book.authors:
|
|
178
|
+
metadata["author"] = ", ".join([a.name for a in book.authors])
|
|
179
|
+
|
|
180
|
+
return SymlinkNode(name, target_path, parent=self, metadata=metadata)
|
|
181
|
+
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def get_info(self) -> Dict[str, Any]:
|
|
185
|
+
"""Get author node info.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Dict with author information
|
|
189
|
+
"""
|
|
190
|
+
return {
|
|
191
|
+
"type": "virtual",
|
|
192
|
+
"name": self.name,
|
|
193
|
+
"author": self.author.name,
|
|
194
|
+
"book_count": len(self.author.books),
|
|
195
|
+
"path": self.get_path(),
|
|
196
|
+
}
|