|
1 | 1 | import json |
| 2 | +import os |
2 | 3 | import re |
3 | 4 | import tempfile |
4 | 5 | from loguru import logger |
@@ -574,6 +575,132 @@ def _write_text(name: str, text: str) -> str: |
574 | 575 | return doclist |
575 | 576 |
|
576 | 577 |
|
| 578 | +def parse_karakeep( |
| 579 | + cli_kwargs: dict, |
| 580 | + path: Union[str, Path], |
| 581 | + karakeep_api_endpoint: Optional[str] = None, |
| 582 | + karakeep_api_key: Optional[str] = None, |
| 583 | + karakeep_verify_ssl: bool = True, |
| 584 | + karakeep_content_source: Literal["auto", "native", "wdoc"] = "auto", |
| 585 | + **extra_args, |
| 586 | +) -> List[DocDict]: |
| 587 | + """ |
| 588 | + Turn a DocDict that has `filetype==karakeep` into one DocDict per loadable |
| 589 | + bookmark of the selected Karakeep source. |
| 590 | +
|
| 591 | + The `path` carries the selector (see `parse_selector`): a list name by |
| 592 | + default, or one of `tag:...`, `search:...`, `ids:...`, `library`/`*`, |
| 593 | + `favourites`, `archived`. Each bookmark resolves to a single sub-document: |
| 594 | + - a `local_html` doc for a link bookmark's stored crawled html; |
| 595 | + - a `txt` doc for a text bookmark or an asset's pre-extracted text; |
| 596 | + - a `pdf` doc for a downloaded stored pdf/archive asset. |
| 597 | + A compact metadata header (title / url / author / tags / note / summary) is |
| 598 | + prepended to text and html docs. The live bookmarked url is never re-fetched. |
| 599 | +
|
| 600 | + Args: |
| 601 | + cli_kwargs: Base CLI arguments to inherit |
| 602 | + path: The Karakeep selector (see above) |
| 603 | + karakeep_api_endpoint: Karakeep API endpoint, else KARAKEEP_PYTHON_API_ENDPOINT |
| 604 | + karakeep_api_key: Karakeep api key, else KARAKEEP_PYTHON_API_KEY |
| 605 | + karakeep_verify_ssl: verify the instance's TLS certificate |
| 606 | + karakeep_content_source: 'auto' (stored text/html, else stored pdf), 'native' |
| 607 | + (stored extracted text/html only), or 'wdoc' (prefer the stored |
| 608 | + pdf/archive asset, parsed by wdoc's loaders). None re-fetch the live url. |
| 609 | + **extra_args: Additional arguments to pass to each document |
| 610 | +
|
| 611 | + Returns: |
| 612 | + List of DocDict objects, each a loadable sub-document |
| 613 | + """ |
| 614 | + from wdoc.utils.loaders.karakeep import ( |
| 615 | + bookmark_web_url, |
| 616 | + cached_karakeep_content, |
| 617 | + get_karakeep_client, |
| 618 | + parse_selector, |
| 619 | + resolve_bookmarks, |
| 620 | + ) |
| 621 | + |
| 622 | + logger.info(f"Loading karakeep selector: '{path}'") |
| 623 | + selector = parse_selector(path) |
| 624 | + resolved_endpoint = karakeep_api_endpoint or os.environ.get( |
| 625 | + "KARAKEEP_PYTHON_API_ENDPOINT" |
| 626 | + ) |
| 627 | + client = get_karakeep_client( |
| 628 | + api_endpoint=karakeep_api_endpoint, |
| 629 | + api_key=karakeep_api_key, |
| 630 | + verify_ssl=karakeep_verify_ssl, |
| 631 | + ) |
| 632 | + bookmarks = resolve_bookmarks(client, selector) |
| 633 | + assert bookmarks, f"No Karakeep bookmarks found for selector '{path}'" |
| 634 | + |
| 635 | + temp_dir = Path(tempfile.mkdtemp(prefix="wdoc_karakeep_")) |
| 636 | + recur_parent_id = str(uuid.uuid4()) |
| 637 | + doclist: List[DocDict] = [] |
| 638 | + |
| 639 | + def _emit(filetype: str, location: str, title: str, subitem: str): |
| 640 | + doc_kwargs = cli_kwargs.copy() |
| 641 | + for k in list(doc_kwargs.keys()): |
| 642 | + if k.startswith("karakeep_"): |
| 643 | + del doc_kwargs[k] |
| 644 | + doc_kwargs.pop("filetype", None) |
| 645 | + doc_kwargs["path"] = location |
| 646 | + doc_kwargs["filetype"] = filetype |
| 647 | + doc_kwargs["title"] = title |
| 648 | + doc_kwargs["subitem_link"] = subitem or "" |
| 649 | + doc_kwargs.update(extra_args) |
| 650 | + doc_kwargs["recur_parent_id"] = recur_parent_id |
| 651 | + # A fan-out over a whole library will inevitably hit individual |
| 652 | + # bookmarks that fail to load (an empty crawl, a corrupt asset, ...). |
| 653 | + # Degrade gracefully: warn and skip that sub-document instead of |
| 654 | + # crashing the entire selection. The batch still raises if every single |
| 655 | + # sub-document fails. |
| 656 | + doc_kwargs["loading_failure"] = "warn" |
| 657 | + doclist.append(DocDict(doc_kwargs)) |
| 658 | + |
| 659 | + for bookmark in bookmarks: |
| 660 | + bid = bookmark.get("id") |
| 661 | + title = ( |
| 662 | + bookmark.get("title") |
| 663 | + or (bookmark.get("content") or {}).get("title") |
| 664 | + or "Untitled" |
| 665 | + ) |
| 666 | + try: |
| 667 | + descriptor = cached_karakeep_content( |
| 668 | + bid, |
| 669 | + bookmark.get("modifiedAt"), |
| 670 | + karakeep_content_source, |
| 671 | + resolved_endpoint, |
| 672 | + client=client, |
| 673 | + bookmark=bookmark, |
| 674 | + ) |
| 675 | + except Exception as err: |
| 676 | + logger.warning(f"Could not resolve Karakeep bookmark {bid}: {err}") |
| 677 | + continue |
| 678 | + if not descriptor: |
| 679 | + logger.warning( |
| 680 | + f"Karakeep bookmark {bid} ('{title}') has no usable stored " |
| 681 | + f"content for source '{karakeep_content_source}', skipping" |
| 682 | + ) |
| 683 | + continue |
| 684 | + |
| 685 | + out = temp_dir / f"{bid}{descriptor['suffix']}" |
| 686 | + if descriptor["data"] is not None: |
| 687 | + out.write_bytes(descriptor["data"]) |
| 688 | + else: |
| 689 | + out.write_text(descriptor["text"]) |
| 690 | + _emit( |
| 691 | + descriptor["filetype"], |
| 692 | + str(out), |
| 693 | + title, |
| 694 | + bookmark_web_url(bookmark, resolved_endpoint), |
| 695 | + ) |
| 696 | + |
| 697 | + assert doclist, ( |
| 698 | + f"Karakeep selector '{path}' produced no loadable documents " |
| 699 | + f"(bookmarks found: {len(bookmarks)})" |
| 700 | + ) |
| 701 | + return doclist |
| 702 | + |
| 703 | + |
577 | 704 | @memoizer |
578 | 705 | def parse_load_functions(load_functions: Tuple[str, ...]) -> bytes: |
579 | 706 | load_functions = list(load_functions) |
@@ -601,5 +728,6 @@ def parse_load_functions(load_functions: Tuple[str, ...]) -> bytes: |
601 | 728 | "youtube_playlist": parse_youtube_playlist, |
602 | 729 | "ddg": parse_ddg_search, |
603 | 730 | "zotero": parse_zotero, |
| 731 | + "karakeep": parse_karakeep, |
604 | 732 | "auto": None, |
605 | 733 | } |
0 commit comments