diff --git a/README.md b/README.md index 50689be..2f25afc 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ The highlights are NOT saved in the PDF file unless you export the highlights in If you annotate your files outside the new Zotero PDF reader, this library will not work with your PDF annotations as those are not retrievable from Zotero API. In that case, you may want to use zotfile + mdnotes to extract the annotations and convert them into markdown files. - -**_This library is for you if you annotate (highlight + note) using the Zotero's PDF reader (including the beta version in iOs)_** +**_This library is for you if you annotate (highlight + note) using the Zotero's PDF reader (including the Zotero iOS)_** # Installation You can install the library by running @@ -19,13 +18,50 @@ pip install zotero2md Note: If you do not have pip installed on your system, you can follow the instructions [here](https://pip.pypa.io/en/stable/installation/). # Usage +Since we have to retrieve the notes from Zotero API, the minimum requirements are: +* **Zotero API key** [Required]: Create a new Zotero Key from [your Zotero settings](https://www.zotero.org/settings/key) +* **Zotero personal or group ID** [Required]: + * Your **personal library ID** (aka **userID**) can be found [here](https://www.zotero.org/settings/key) next to `Your userID for use in API calls is XXXXXX`. + * If you're using a **group library**, you can find the library ID by + 1. Go to `https://www.zotero.org/groups/` + 2. Click on the interested group. + 3. You can find the library ID from the URL link that has format like *https://www.zotero.org/groups//group_name*. The number between `/groups/` and `/group_name` is the libarry ID. +* **Zotero library type** [Optional]: *"user"* (default) if using personal library and *"group"* if using group library. + +Note that if you want to retrieve annotations and notes from a group, you should provide the group ID (`zotero_library_id=`) and set the library type to group (`zotero_library_type="group"`). + +## Approach 1 (Recommended) +After installing the library, open a Python terminal, and then execute the following: +```python +from zotero2md.zt2md import Zotero2Markdown + +zt = Zotero2Markdown( + zotero_key="your_zotero_key", + zotero_library_id="your_zotero_id", + zotero_library_type="user", # "user" (default) or "group" + params_filepath="", # [Default values provided bellow] # The path to JSON file containing the custom parameters (See Section Custom Output Parameters). + include_annotations=True, # Default: True + include_notes=True, # Default: True +) +zt.run_all() +``` +Just to make sure that all files are created, you can run `save_failed_items_to_txt()` to ensure that no file was +was failed to create. If a file or more failed to create, the filename (item title) and the corresponding Zotero +item key will be saved to a txt file. +```python +zt.save_failed_items_to_txt("failed_zotero_items.txt") +``` + +## Approach 2 +For this approach, you need to download `output_to_md.py` script. +Run `python output_to_md.py -h` to get more information about all options. ```shell -python zotero2md/generate.py +python zotero2md/output_to_md.py ``` For instance, assuming zotero_key=abcd and zotero_id=1234, you can simply run the following: ```shell -python zotero2md/generate.py abcd 1234 +python zotero2md/output_to_md.py abcd 1234 ``` diff --git a/zotero2md/__init__.py b/zotero2md/__init__.py index ecb34ea..474ae46 100644 --- a/zotero2md/__init__.py +++ b/zotero2md/__init__.py @@ -6,7 +6,7 @@ ROOT_DIR: Path = Path(__file__).parent TOP_DIR: Path = ROOT_DIR.parent -default_params = { +DEFAULT_PARAMS = { "convertTagsToInternalLinks": True, "doNotConvertFollowingTagsToLink": [], "includeHighlightDate": True, diff --git a/zotero2md/generate.py b/zotero2md/generate.py deleted file mode 100644 index f168f63..0000000 --- a/zotero2md/generate.py +++ /dev/null @@ -1,92 +0,0 @@ -from argparse import ArgumentParser -from typing import Union - -from pyzotero.zotero import Zotero - -from zotero2md.utils import group_by_parent_item -from zotero2md.zotero import ( - ItemAnnotationsAndNotes, - get_zotero_client, - retrieve_all_annotations, - retrieve_all_notes, -) - - -def generate_annotations_for_all_items( - zotero_client: Zotero, params_filepath: Union[str, None] = None -) -> None: - highlights = group_by_parent_item(retrieve_all_annotations(zotero_client)) - notes = group_by_parent_item(retrieve_all_notes(zotero_client)) - failed_files = [] - for i, item_key in enumerate(highlights.keys()): - item = zotero_client.item(item_key) - parent_item_key = item["data"].get("parentItem", None) - print(f"File {i + 1} of {len(highlights)} is under process ...") - item = ItemAnnotationsAndNotes( - zotero_client=zotero_client, - params_filepath=params_filepath, - item_annotations=highlights[item_key], - item_notes=notes.get(parent_item_key, None), - item_key=item_key, - ) - # del notes[parent_item_key] - - out = item.generate_output() - if out: - failed_files.append(out) - print("\n") - # - # for i, item_key in enumerate(notes.keys()): - # print(f"File {i + 1} of {len(highlights)} is under process ...") - # item = ItemAnnotationsAndNotes( - # zotero_client=zotero_client, - # params_filepath=params_filepath, - # item_annotations=None, - # item_notes=notes[item_key], - # item_key=item_key, - # ) - # out = item.generate_output() - # if out: - # failed_files.append(out) - # print("\n") - - if failed_files: - print("\nItems that failed to compile to a markdown file:") - for (file, item_key) in failed_files: - print(f"Item Key: {item_key} | File: {file}\n") - else: - print("\nAll items were successfully created!") - - -if __name__ == "__main__": - parser = ArgumentParser(description="Generate Markdown files") - parser.add_argument( - "zotero_key", help="Zotero API key (visit https://www.zotero.org/settings/keys)" - ) - parser.add_argument( - "zotero_user_id", - help="Zotero User ID (visit https://www.zotero.org/settings/keys)", - ) - parser.add_argument( - "--library_type", - default="user", - help="Zotero Library type ('user': for personal library (default value), 'group': for a shared library)", - ) - parser.add_argument( - "--config_filepath", - type=str, - help="Filepath to a .json file containing the path", - ) - - args = vars(parser.parse_args()) - - # ----- Create a Zotero client object - zot_client = get_zotero_client( - library_id=args["zotero_user_id"], - library_type=args["library_type"], - api_key=args["zotero_key"], - ) - - generate_annotations_for_all_items( - zot_client, params_filepath=args.get("config_filepath", None) - ) diff --git a/zotero2md/output_to_md.py b/zotero2md/output_to_md.py new file mode 100644 index 0000000..44a8fd9 --- /dev/null +++ b/zotero2md/output_to_md.py @@ -0,0 +1,37 @@ +from argparse import ArgumentParser + +from zotero2md.zt2md import Zotero2Markdown + +if __name__ == "__main__": + parser = ArgumentParser(description="Generate Markdown files") + parser.add_argument( + "zotero_key", help="Zotero API key (visit https://www.zotero.org/settings/keys)" + ) + parser.add_argument( + "zotero_user_id", + help="Zotero User ID (visit https://www.zotero.org/settings/keys)", + ) + parser.add_argument( + "--library_type", + default="user", + help="Zotero Library type ('user': for personal library (default value), 'group': for a shared library)", + ) + parser.add_argument( + "--config_filepath", + type=str, + help="Filepath to a .json file containing the path", + ) + + args = vars(parser.parse_args()) + + zt = Zotero2Markdown( + zotero_key=args["zotero_key"], + zotero_library_id=args["zotero_user_id"], + zotero_library_type=args["library_type"], + params_filepath=args.get("config_filepath", None), + include_annotations=True, + include_notes=True, + ) + + zt.run_all() + zt.save_failed_items_to_txt("failed_zotero_items.txt") diff --git a/zotero2md/zotero.py b/zotero2md/zotero.py index 0fe0761..b8a5468 100644 --- a/zotero2md/zotero.py +++ b/zotero2md/zotero.py @@ -8,7 +8,7 @@ from pyzotero.zotero_errors import ParamNotPassed, UnsupportedParams from snakemd import Document, MDList, Paragraph -from zotero2md import default_params +from zotero2md import DEFAULT_PARAMS from zotero2md.utils import sanitize_filename, sanitize_tag _OUTPUT_DIR = Path("zotero_output") @@ -81,7 +81,7 @@ def __init__( self.parent_item_key = self.item_details["data"].get("parentItem", None) # Load output configurations used for generating markdown files. - self.md_config = default_params + self.md_config = DEFAULT_PARAMS if params_filepath: with open(Path(params_filepath), "r") as f: @@ -170,6 +170,7 @@ def __init__( super().__init__(zotero_client, item_key, params_filepath) self.item_annotations = item_annotations self.item_notes = item_notes + self.failed_item: str = "" self.doc = Document( name="initial_filename" @@ -277,7 +278,7 @@ def create_annotations_section(self, annotations: List) -> None: self.doc.add_element(MDList(annots)) - def generate_output(self) -> Union[None, Tuple[str, str]]: + def generate_output(self) -> None: """Generate the markdown file for a Zotero Item combining metadata, annotations Returns @@ -301,15 +302,12 @@ def generate_output(self) -> Union[None, Tuple[str, str]]: with open(_OUTPUT_DIR.joinpath(output_filename), "w+") as f: f.write(self.doc.render()) print( - f'File "{output_filename}" (item_key="{self.item_key}") was successfully created.' + f'File "{output_filename}" (item_key="{self.item_key}") was successfully created.\n' ) - return None except: - print( - f'File "{output_filename}" (item_key="{self.item_key}") is failed to generate.\n' - f"SKIPPING..." - ) - return output_filename, self.item_key + msg = f'File "{output_filename}" (item_key="{self.item_key}") is failed to generate.\n' + print(msg + "SKIPPING...\n") + self.failed_item = msg def retrieve_all_annotations(zotero_client: Zotero) -> List[Dict]: diff --git a/zotero2md/zt2md.py b/zotero2md/zt2md.py new file mode 100644 index 0000000..990115d --- /dev/null +++ b/zotero2md/zt2md.py @@ -0,0 +1,88 @@ +from typing import Dict, List, Union + +from zotero2md.utils import group_by_parent_item +from zotero2md.zotero import ( + ItemAnnotationsAndNotes, + get_zotero_client, + retrieve_all_annotations, + retrieve_all_notes, +) + + +class Zotero2Markdown: + def __init__( + self, + zotero_key: str, + zotero_library_id: str, + zotero_library_type: str = "user", + params_filepath: str = None, + include_annotations: bool = True, + include_notes: bool = True, + ): + + self.zotero_client = get_zotero_client( + library_id=zotero_library_id, + library_type=zotero_library_type, + api_key=zotero_key, + ) + self.config_filepath = params_filepath + self.include_annots = include_annotations + self.include_notes = include_notes + self.failed_items: List[str] = [] + + def run_all(self, params_filepath: Union[str, None] = None) -> None: + annots_grouped: Dict = {} + notes_grouped: Dict = {} + if self.include_annots: + annots_grouped = group_by_parent_item( + retrieve_all_annotations(self.zotero_client) + ) + if self.include_notes: + notes_grouped = group_by_parent_item(retrieve_all_notes(self.zotero_client)) + + for i, item_key in enumerate(annots_grouped.keys()): + item = self.zotero_client.item(item_key) + parent_item_key = item["data"].get("parentItem", None) + print(f"File {i + 1} of {len(annots_grouped)} is under process ...") + zotero_item = ItemAnnotationsAndNotes( + zotero_client=self.zotero_client, + params_filepath=params_filepath, + item_annotations=annots_grouped[item_key], + item_notes=notes_grouped.get(parent_item_key, None), + item_key=item_key, + ) + # del notes[parent_item_key] + + if zotero_item.failed_item: + self.failed_items.append(zotero_item.failed_item) + else: + zotero_item.generate_output() + + # for i, item_key in enumerate(notes.keys()): + # print(f"File {i + 1} of {len(highlights)} is under process ...") + # item = ItemAnnotationsAndNotes( + # zotero_client=zotero_client, + # params_filepath=params_filepath, + # item_annotations=None, + # item_notes=notes[item_key], + # item_key=item_key, + # ) + # out = item.generate_output() + # if out: + # failed_files.append(out) + # print("\n") + + def save_failed_items_to_txt(self, txt_filepath: str = ""): + if txt_filepath == "": + txt_filepath = "failed_zotero_items.txt" + + if self.failed_items: + print( + f"\n {len(self.failed_items)} markdown files (with all their annotations and notes) failed to create." + ) + with open(txt_filepath, "w") as f: + file_content = "\n".join(self.failed_items) + f.write(file_content) + print(f"List of all failed items are saved into {txt_filepath}") + else: + print("No failed item")