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
ebk/vfs/nodes/books.py ADDED
@@ -0,0 +1,480 @@
1
+ """Book-related VFS nodes."""
2
+
3
+ from typing import List, Optional, Dict, Any
4
+
5
+ from ebk.vfs.base import VirtualNode, DirectoryNode, FileNode, Node
6
+ from ebk.library_db import Library
7
+ from ebk.db.models import Book
8
+
9
+
10
+ class BooksDirectoryNode(VirtualNode):
11
+ """/books/ - Virtual directory listing all books.
12
+
13
+ Children are BookNode instances (one per book in library).
14
+ Books are accessed by ID: /books/42/
15
+ """
16
+
17
+ def __init__(self, library: Library, parent: Optional[DirectoryNode] = None):
18
+ """Initialize books directory.
19
+
20
+ Args:
21
+ library: Library instance
22
+ parent: Parent node (usually root)
23
+ """
24
+ super().__init__(name="books", parent=parent)
25
+ self.library = library
26
+
27
+ def list_children(self) -> List[Node]:
28
+ """List all books in the library.
29
+
30
+ Returns:
31
+ List of BookNode instances
32
+ """
33
+ # Query all books from database
34
+ books = self.library.query().all()
35
+
36
+ # Create BookNode for each
37
+ book_nodes = []
38
+ for book in books:
39
+ node = BookNode(book, self.library, parent=self)
40
+ book_nodes.append(node)
41
+
42
+ return book_nodes
43
+
44
+ def get_child(self, name: str) -> Optional[Node]:
45
+ """Get a book by ID.
46
+
47
+ Args:
48
+ name: Book ID as string
49
+
50
+ Returns:
51
+ BookNode or None if not found
52
+ """
53
+ # Parse book ID
54
+ try:
55
+ book_id = int(name)
56
+ except ValueError:
57
+ return None
58
+
59
+ # Get book from database
60
+ book = self.library.get_book(book_id)
61
+ if book is None:
62
+ return None
63
+
64
+ return BookNode(book, self.library, parent=self)
65
+
66
+ def get_info(self) -> Dict[str, Any]:
67
+ """Get books directory info.
68
+
69
+ Returns:
70
+ Dict with directory information
71
+ """
72
+ total = self.library.query().count()
73
+ return {
74
+ "type": "virtual",
75
+ "name": "books",
76
+ "total_books": total,
77
+ "path": self.get_path(),
78
+ }
79
+
80
+
81
+ class BookNode(DirectoryNode):
82
+ """/books/42/ - A specific book with metadata and files.
83
+
84
+ Contains:
85
+ - title - Book title (file)
86
+ - authors - Authors list (file)
87
+ - subjects - Subjects/tags (file)
88
+ - description - Book description (file)
89
+ - text - Extracted full text (file)
90
+ - year - Publication year (file)
91
+ - language - Language code (file)
92
+ - publisher - Publisher name (file)
93
+ - metadata - All metadata formatted (file)
94
+ - files/ - Physical files (directory)
95
+ - similar/ - Similar books (virtual directory)
96
+ - annotations/ - User annotations (directory)
97
+ - covers/ - Cover images (directory)
98
+ """
99
+
100
+ def __init__(
101
+ self,
102
+ book: Book,
103
+ library: Library,
104
+ parent: Optional[DirectoryNode] = None,
105
+ ):
106
+ """Initialize book node.
107
+
108
+ Args:
109
+ book: Book database model
110
+ library: Library instance
111
+ parent: Parent node (usually /books/)
112
+ """
113
+ super().__init__(name=str(book.id), parent=parent)
114
+ self.book = book
115
+ self.library = library
116
+ self._children_cache: Optional[Dict[str, Node]] = None
117
+
118
+ def list_children(self) -> List[Node]:
119
+ """List all files and subdirectories for this book.
120
+
121
+ Returns:
122
+ List of child nodes
123
+ """
124
+ if self._children_cache is None:
125
+ self._build_children()
126
+
127
+ return list(self._children_cache.values())
128
+
129
+ def get_child(self, name: str) -> Optional[Node]:
130
+ """Get a child by name.
131
+
132
+ Args:
133
+ name: Child name
134
+
135
+ Returns:
136
+ Child node or None
137
+ """
138
+ if self._children_cache is None:
139
+ self._build_children()
140
+
141
+ return self._children_cache.get(name)
142
+
143
+ def _build_children(self) -> None:
144
+ """Build child nodes."""
145
+ from ebk.vfs.nodes.metadata import (
146
+ TitleFileNode,
147
+ AuthorsFileNode,
148
+ SubjectsFileNode,
149
+ DescriptionFileNode,
150
+ TextFileNode,
151
+ YearFileNode,
152
+ LanguageFileNode,
153
+ PublisherFileNode,
154
+ MetadataFileNode,
155
+ BookColorFile,
156
+ )
157
+ from ebk.vfs.nodes.files import FilesDirectoryNode
158
+ from ebk.vfs.nodes.similar import SimilarDirectoryNode
159
+
160
+ self._children_cache = {
161
+ "title": TitleFileNode(self.book, parent=self),
162
+ "authors": AuthorsFileNode(self.book, parent=self),
163
+ "subjects": SubjectsFileNode(self.book, parent=self),
164
+ "description": DescriptionFileNode(self.book, parent=self),
165
+ "text": TextFileNode(self.book, parent=self),
166
+ "year": YearFileNode(self.book, parent=self),
167
+ "language": LanguageFileNode(self.book, parent=self),
168
+ "publisher": PublisherFileNode(self.book, parent=self),
169
+ "metadata": MetadataFileNode(self.book, parent=self),
170
+ "color": BookColorFile(self.book, self.library, parent=self),
171
+ "files": FilesDirectoryNode(self.book, parent=self),
172
+ "similar": SimilarDirectoryNode(self.book, self.library, parent=self),
173
+ "tags": BookTagsDirectoryNode(self.book, self.library, parent=self),
174
+ # TODO: annotations/, covers/
175
+ }
176
+
177
+ def get_info(self) -> Dict[str, Any]:
178
+ """Get book info.
179
+
180
+ Returns:
181
+ Dict with book information
182
+ """
183
+ authors_str = ", ".join(a.name for a in self.book.authors) if self.book.authors else ""
184
+ info = {
185
+ "type": "directory",
186
+ "name": str(self.book.id),
187
+ "title": self.book.title,
188
+ "authors": authors_str,
189
+ "language": self.book.language,
190
+ "files_count": len(self.book.files),
191
+ "path": self.get_path(),
192
+ }
193
+
194
+ # Include color if set
195
+ if self.book.color:
196
+ info["color"] = self.book.color
197
+
198
+ return info
199
+
200
+
201
+ class BookTagsDirectoryNode(VirtualNode):
202
+ """/books/42/tags/ - Tags associated with this book.
203
+
204
+ Shows symlinks to tag paths where this book appears.
205
+ Allows easy navigation to see all tags for a book.
206
+ """
207
+
208
+ def __init__(
209
+ self,
210
+ book: Book,
211
+ library: Library,
212
+ parent: Optional[DirectoryNode] = None,
213
+ ):
214
+ """Initialize book tags directory.
215
+
216
+ Args:
217
+ book: Book database model
218
+ library: Library instance
219
+ parent: Parent BookNode
220
+ """
221
+ super().__init__(name="tags", parent=parent)
222
+ self.book = book
223
+ self.library = library
224
+
225
+ def list_children(self) -> List[Node]:
226
+ """List all tags for this book, organized hierarchically.
227
+
228
+ Returns:
229
+ List of BookTagHierarchyNode (directories) and SymlinkNode instances
230
+ """
231
+ from ebk.vfs.base import SymlinkNode
232
+
233
+ # Collect all tag paths for this book
234
+ tag_paths = {tag.path for tag in self.book.tags}
235
+
236
+ # Find root-level entries (first component of each path)
237
+ root_entries = {}
238
+ for tag in self.book.tags:
239
+ parts = tag.path.split('/')
240
+ root_name = parts[0]
241
+
242
+ if root_name not in root_entries:
243
+ root_entries[root_name] = []
244
+ root_entries[root_name].append(tag.path)
245
+
246
+ # Create nodes for root-level entries
247
+ children = []
248
+ for root_name, paths in root_entries.items():
249
+ # Check if this root name is a complete tag itself
250
+ if root_name in tag_paths:
251
+ # It's a leaf tag - create symlink
252
+ tag = next(t for t in self.book.tags if t.path == root_name)
253
+ target_path = f"/tags/{tag.path}"
254
+ metadata = {
255
+ "path": tag.path,
256
+ "depth": tag.depth,
257
+ }
258
+ if tag.description:
259
+ metadata["description"] = tag.description[:50] + "..." if len(tag.description) > 50 else tag.description
260
+
261
+ symlink = SymlinkNode(root_name, target_path, parent=self, metadata=metadata)
262
+ children.append(symlink)
263
+ else:
264
+ # It's an intermediate directory - create hierarchy node
265
+ hierarchy_node = BookTagHierarchyNode(
266
+ root_name,
267
+ self.book,
268
+ self.library,
269
+ parent=self
270
+ )
271
+ children.append(hierarchy_node)
272
+
273
+ return children
274
+
275
+ def get_child(self, name: str) -> Optional[Node]:
276
+ """Get a tag by root name.
277
+
278
+ Args:
279
+ name: Root tag name (first component of path)
280
+
281
+ Returns:
282
+ SymlinkNode or BookTagHierarchyNode or None
283
+ """
284
+ from ebk.vfs.base import SymlinkNode
285
+
286
+ # Collect tag paths to check if name is a complete tag
287
+ tag_paths = {tag.path for tag in self.book.tags}
288
+
289
+ # Check if any tag starts with this name
290
+ matching_tags = [tag for tag in self.book.tags if tag.path.split('/')[0] == name]
291
+ if not matching_tags:
292
+ return None
293
+
294
+ # If name is a complete tag path, return symlink
295
+ if name in tag_paths:
296
+ tag = next(t for t in self.book.tags if t.path == name)
297
+ target_path = f"/tags/{tag.path}"
298
+ metadata = {
299
+ "path": tag.path,
300
+ "depth": tag.depth,
301
+ }
302
+ if tag.description:
303
+ metadata["description"] = tag.description[:50] + "..." if len(tag.description) > 50 else tag.description
304
+
305
+ return SymlinkNode(name, target_path, parent=self, metadata=metadata)
306
+ else:
307
+ # It's an intermediate directory
308
+ return BookTagHierarchyNode(name, self.book, self.library, parent=self)
309
+
310
+ def get_info(self) -> Dict[str, Any]:
311
+ """Get tags directory info.
312
+
313
+ Returns:
314
+ Dict with directory information
315
+ """
316
+ return {
317
+ "type": "virtual",
318
+ "name": "tags",
319
+ "tag_count": len(self.book.tags) if hasattr(self.book, 'tags') else 0,
320
+ "path": self.get_path(),
321
+ }
322
+
323
+
324
+ class BookTagHierarchyNode(VirtualNode):
325
+ """/books/42/tags/a/ - Intermediate directory in book's tag hierarchy.
326
+
327
+ Represents a level in the tag hierarchy for tags assigned to a book.
328
+ For example, if book has tag 'Work/Project-2024', then:
329
+ - /books/42/tags/Work/ is a BookTagHierarchyNode
330
+ - /books/42/tags/Work/Project-2024 is a SymlinkNode
331
+ """
332
+
333
+ def __init__(
334
+ self,
335
+ name: str,
336
+ book: Book,
337
+ library: Library,
338
+ parent: Optional[DirectoryNode] = None,
339
+ ):
340
+ """Initialize book tag hierarchy node.
341
+
342
+ Args:
343
+ name: Directory name (e.g., "Work")
344
+ book: Book database model
345
+ library: Library instance
346
+ parent: Parent node
347
+ """
348
+ super().__init__(name=name, parent=parent)
349
+ self.book = book
350
+ self.library = library
351
+
352
+ def _get_current_prefix(self) -> str:
353
+ """Get the current path prefix for this node.
354
+
355
+ Returns:
356
+ Path prefix like "a" or "a/b"
357
+ """
358
+ # Build path from root tags directory
359
+ parts = []
360
+ node = self
361
+ while node is not None and not isinstance(node, BookTagsDirectoryNode):
362
+ parts.insert(0, node.name)
363
+ node = node.parent
364
+
365
+ return '/'.join(parts)
366
+
367
+ def list_children(self) -> List[Node]:
368
+ """List tags at this level of hierarchy.
369
+
370
+ Returns:
371
+ List of BookTagHierarchyNode (subdirs) and SymlinkNode (tags)
372
+ """
373
+ from ebk.vfs.base import SymlinkNode
374
+
375
+ prefix = self._get_current_prefix()
376
+ prefix_depth = len(prefix.split('/'))
377
+
378
+ # Find all tags under this prefix
379
+ matching_tags = [
380
+ tag for tag in self.book.tags
381
+ if tag.path.startswith(prefix + '/')
382
+ ]
383
+
384
+ # Also check if prefix itself is a tag
385
+ tag_paths = {tag.path for tag in self.book.tags}
386
+
387
+ # Find next-level entries
388
+ next_level_entries = {}
389
+ for tag in matching_tags:
390
+ # Get the part after the prefix
391
+ relative_path = tag.path[len(prefix) + 1:] # Remove prefix and /
392
+ parts = relative_path.split('/')
393
+ next_name = parts[0]
394
+
395
+ if next_name not in next_level_entries:
396
+ next_level_entries[next_name] = []
397
+ next_level_entries[next_name].append(tag.path)
398
+
399
+ # Create nodes
400
+ children = []
401
+ for next_name, paths in next_level_entries.items():
402
+ full_path = f"{prefix}/{next_name}"
403
+
404
+ if full_path in tag_paths:
405
+ # It's a complete tag - create symlink
406
+ tag = next(t for t in self.book.tags if t.path == full_path)
407
+ target_path = f"/tags/{tag.path}"
408
+ metadata = {
409
+ "path": tag.path,
410
+ "depth": tag.depth,
411
+ }
412
+ if tag.description:
413
+ metadata["description"] = tag.description[:50] + "..." if len(tag.description) > 50 else tag.description
414
+
415
+ symlink = SymlinkNode(next_name, target_path, parent=self, metadata=metadata)
416
+ children.append(symlink)
417
+ else:
418
+ # It's an intermediate directory
419
+ hierarchy_node = BookTagHierarchyNode(
420
+ next_name,
421
+ self.book,
422
+ self.library,
423
+ parent=self
424
+ )
425
+ children.append(hierarchy_node)
426
+
427
+ return children
428
+
429
+ def get_child(self, name: str) -> Optional[Node]:
430
+ """Get a child tag or subdirectory.
431
+
432
+ Args:
433
+ name: Child name
434
+
435
+ Returns:
436
+ SymlinkNode or BookTagHierarchyNode or None
437
+ """
438
+ from ebk.vfs.base import SymlinkNode
439
+
440
+ prefix = self._get_current_prefix()
441
+ full_path = f"{prefix}/{name}"
442
+
443
+ # Check if this is a complete tag
444
+ tag_paths = {tag.path for tag in self.book.tags}
445
+
446
+ if full_path in tag_paths:
447
+ # It's a complete tag - return symlink
448
+ tag = next(t for t in self.book.tags if t.path == full_path)
449
+ target_path = f"/tags/{tag.path}"
450
+ metadata = {
451
+ "path": tag.path,
452
+ "depth": tag.depth,
453
+ }
454
+ if tag.description:
455
+ metadata["description"] = tag.description[:50] + "..." if len(tag.description) > 50 else tag.description
456
+
457
+ return SymlinkNode(name, target_path, parent=self, metadata=metadata)
458
+ else:
459
+ # Check if it's an intermediate directory
460
+ has_children = any(
461
+ tag.path.startswith(full_path + '/')
462
+ for tag in self.book.tags
463
+ )
464
+ if has_children:
465
+ return BookTagHierarchyNode(name, self.book, self.library, parent=self)
466
+
467
+ return None
468
+
469
+ def get_info(self) -> Dict[str, Any]:
470
+ """Get node info.
471
+
472
+ Returns:
473
+ Dict with node information
474
+ """
475
+ return {
476
+ "type": "virtual",
477
+ "name": self.name,
478
+ "path": self.get_path(),
479
+ "tag_prefix": self._get_current_prefix(),
480
+ }
ebk/vfs/nodes/files.py ADDED
@@ -0,0 +1,155 @@
1
+ """File-related VFS nodes."""
2
+
3
+ from typing import List, Optional, Dict, Any
4
+
5
+ from ebk.vfs.base import DirectoryNode, FileNode, Node
6
+ from ebk.db.models import Book, File as DBFile
7
+
8
+
9
+ class FilesDirectoryNode(DirectoryNode):
10
+ """/books/42/files/ - Directory listing physical file formats.
11
+
12
+ Each child is a PhysicalFileNode representing an actual ebook file
13
+ (PDF, EPUB, etc.).
14
+ """
15
+
16
+ def __init__(self, book: Book, parent: Optional[DirectoryNode] = None):
17
+ """Initialize files directory.
18
+
19
+ Args:
20
+ book: Book database model
21
+ parent: Parent node (usually BookNode)
22
+ """
23
+ super().__init__(name="files", parent=parent)
24
+ self.book = book
25
+
26
+ def list_children(self) -> List[Node]:
27
+ """List all physical files for this book.
28
+
29
+ Returns:
30
+ List of PhysicalFileNode instances
31
+ """
32
+ if not self.book.files:
33
+ return []
34
+
35
+ file_nodes = []
36
+ for db_file in self.book.files:
37
+ # Use original filename if available, otherwise use format
38
+ filename = f"{self.book.title or 'book'}.{db_file.format.lower()}"
39
+ node = PhysicalFileNode(db_file, filename, parent=self)
40
+ file_nodes.append(node)
41
+
42
+ return file_nodes
43
+
44
+ def get_child(self, name: str) -> Optional[Node]:
45
+ """Get a physical file by name.
46
+
47
+ Args:
48
+ name: Filename (e.g., "book.pdf")
49
+
50
+ Returns:
51
+ PhysicalFileNode or None
52
+ """
53
+ if not self.book.files:
54
+ return None
55
+
56
+ # Try to match by format extension
57
+ for db_file in self.book.files:
58
+ filename = f"{self.book.title or 'book'}.{db_file.format.lower()}"
59
+ if filename == name or name.endswith(f".{db_file.format.lower()}"):
60
+ return PhysicalFileNode(db_file, filename, parent=self)
61
+
62
+ return None
63
+
64
+ def get_info(self) -> Dict[str, Any]:
65
+ """Get files directory info.
66
+
67
+ Returns:
68
+ Dict with directory information
69
+ """
70
+ total_size = sum(f.size_bytes for f in self.book.files if f.size_bytes is not None) if self.book.files else 0
71
+ return {
72
+ "type": "directory",
73
+ "name": "files",
74
+ "file_count": len(self.book.files) if self.book.files else 0,
75
+ "total_size": total_size,
76
+ "path": self.get_path(),
77
+ }
78
+
79
+
80
+ class PhysicalFileNode(FileNode):
81
+ """A physical ebook file (PDF, EPUB, etc.).
82
+
83
+ When cat'd, shows file metadata.
84
+ Use 'open' command to actually open the file.
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ db_file: DBFile,
90
+ filename: str,
91
+ parent: Optional[DirectoryNode] = None,
92
+ ):
93
+ """Initialize physical file node.
94
+
95
+ Args:
96
+ db_file: File database model
97
+ filename: Display filename
98
+ parent: Parent node (usually FilesDirectoryNode)
99
+ """
100
+ super().__init__(name=filename, parent=parent, size=db_file.size_bytes)
101
+ self.db_file = db_file
102
+
103
+ def read_content(self) -> str:
104
+ """Read file metadata (not the actual file content).
105
+
106
+ Returns:
107
+ Formatted file metadata
108
+ """
109
+ lines = []
110
+ lines.append(f"Format: {self.db_file.format.upper()}")
111
+
112
+ if self.db_file.size_bytes:
113
+ # Format size nicely
114
+ size_mb = self.db_file.size_bytes / (1024 * 1024)
115
+ if size_mb < 1:
116
+ size_kb = self.db_file.size_bytes / 1024
117
+ lines.append(f"Size: {size_kb:.1f} KB")
118
+ else:
119
+ lines.append(f"Size: {size_mb:.1f} MB")
120
+
121
+ lines.append(f"Hash: {self.db_file.file_hash[:16]}...")
122
+ lines.append(f"Path: {self.db_file.path}")
123
+
124
+ # Check if text was extracted
125
+ if self.db_file.extracted_text:
126
+ lines.append("Text: Extracted")
127
+ else:
128
+ lines.append("Text: Not extracted")
129
+
130
+ return "\n".join(lines)
131
+
132
+ def get_info(self) -> Dict[str, Any]:
133
+ """Get file metadata.
134
+
135
+ Returns:
136
+ Dict with file information
137
+ """
138
+ return {
139
+ "type": "file",
140
+ "name": self.name,
141
+ "format": self.db_file.format,
142
+ "size": self.db_file.size_bytes,
143
+ "hash": self.db_file.file_hash,
144
+ "path": self.get_path(),
145
+ }
146
+
147
+ def get_physical_path(self) -> str:
148
+ """Get the actual filesystem path to this file.
149
+
150
+ Returns:
151
+ Absolute path to physical file
152
+ """
153
+ # This will be used by the 'open' command
154
+ # Reconstruct from library path + relative path
155
+ return str(self.db_file.path)