diff --git a/README.md b/README.md index 780adfa..5385408 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ -# Anki💗Notion addon +# Notion Toggles Sync -It's an [Anki](https://apps.ankiweb.net/) addon that loads toggle lists from [Notion](https://notion.so) as notes to -a specified deck. +It's an [Anki](https://apps.ankiweb.net/) addon that loads toggle lists from [Notion](https://notion.so) as notes to specified decks and keeps them syncronized. -[![Supported versions](https://img.shields.io/badge/python-3.8%20%7C%203.9-blue)](https://github.com/9dogs/notion-anki-sync) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Codestyle: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) @@ -48,15 +46,17 @@ Edit plugin config file from Anki: `Tools ➡ Add-ons ➡ Notion Toggles Loader "sync_every_minutes": 30, "anki_target_deck": "Notion Sync", "notion_token": "", - "notion_namespace": "", "notion_pages": [ { "page_id": "", - "recursive": false + "recursive": false, + "target_deck": "Math" }, { "page_id": "", - "recursive": true + "recursive": true, + "target_deck": "Biology" } ] } @@ -73,16 +73,24 @@ Notion API is used, the addon may break without a warning. - Some toggle blocks are empty on export which leads to empty Anki notes. The issue is on the Notion side (and they're aware of it). +## TODO: + +- Add option to use headers (H1, H2, H3) as hierarchy to build subdecks +- Allow users to define custom note types in Anki and map different Notion blocks to these custom note types. +- Implement a bi-directional sync that not only pulls data from Notion to Anki but also pushes updates from Anki back to Notion (for hot fixes furing learning). +- Enhance error handling and provide detailed error reports to the user, including suggestions for resolving common issues like when deck names or IDs dont match +- Add option to add tags to headers preceding the toggles that apply to all the toggles undernearth (to avoid having to add them manually) + ## Configuration parameters - `debug`: `bool [default: false]` — enable debug logging to file. - `sync_every_minutes`: `int [default: 30]` — auto sync interval in minutes. Set to 0 to disable auto sync. -- `anki_target_deck`: `str [default: "Notion Sync"]` — the deck loaded notes will be added to. +- `anki_target_deck`: `str [default: "Notion Sync"]` — the default deck loaded notes will be added to, if not specified in the notion pages. - `notion_token`: `str [default: None]` — Notion APIv2 token. - `notion_namespace`: `str [default: None]` — Notion namespace (your username) to form source URLs. - `notion_pages`: `array [default: [] ]` — List of Notion pages to export notes from. - -## Inspiration +Additional Information +This fork is based on the unmaintained [notion-toggles loader](https://github.com/9dogs/notion-anki-sync) plugin. The enhancements are intended to provide added functionality to the original plugin. This project is inspired by a great [Notion to Anki](https://github.com/alemayhu/Notion-to-Anki). diff --git a/notion_sync_addon/__init__.py b/notion_sync_addon/__init__.py index fcb17c7..0a6245a 100644 --- a/notion_sync_addon/__init__.py +++ b/notion_sync_addon/__init__.py @@ -12,9 +12,8 @@ from aqt.gui_hooks import main_window_did_init from aqt.utils import showCritical, showInfo from jsonschema import ValidationError, validate -from PyQt6.QtCore import QObject, QRunnable, QThreadPool, QTimer, pyqtSignal -from PyQt6.QtGui import QAction -from PyQt6.QtWidgets import QMenu, QMessageBox +from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, pyqtSignal +from PyQt5.QtWidgets import QAction, QMenu, QMessageBox from .helpers import ( BASE_DIR, @@ -197,9 +196,9 @@ def handle_worker_result(self, notes: List[AnkiNote]) -> None: is_updated = self.notes_manager.update_note(note_id, note) if is_updated: self._updated += 1 - # Create new note + # Create new note in the target deck else: - note_id = self.notes_manager.create_note(note) + note_id = self.notes_manager.create_note_in_deck(note, note.target_deck) self._created += 1 self.synced_note_ids.add(note_id) except Exception: @@ -332,12 +331,14 @@ def _sync(self) -> None: for page_spec in self.config.get('notion_pages', []): page_id = page_spec['page_id'] recursive = page_spec.get('recursive', False) + target_deck = page_spec.get('target_deck', self.config.get('anki_target_deck', self.DEFAULT_DECK_NAME)) page_id = normalize_block_id(page_id) worker = NotesExtractorWorker( notion_token=self.config['notion_token'], page_id=page_id, recursive=recursive, notion_namespace=self.config.get('notion_namespace', ''), + target_deck=target_deck, debug=self.debug, ) worker.signals.result.connect(self.handle_worker_result) @@ -360,14 +361,13 @@ class NoteExtractorSignals(QObject): class NotesExtractorWorker(QRunnable): - """Notes extractor worker thread.""" - def __init__( self, notion_token: str, page_id: str, recursive: bool, notion_namespace: str, + target_deck: str, debug: bool = False, ): """Init notes extractor. @@ -376,6 +376,7 @@ def __init__( :param page_id: Notion page id :param recursive: recursive export :param notion_namespace: Notion namespace to form source links + :param target_deck: target deck in Anki :param debug: debug log level """ super().__init__() @@ -386,6 +387,7 @@ def __init__( self.page_id = page_id self.recursive = recursive self.notion_namespace = notion_namespace + self.target_deck = target_deck def run(self) -> None: """Extract note data from given Notion page. @@ -419,6 +421,9 @@ def run(self) -> None: debug=self.debug, ) self.logger.info('Notes extracted: count=%s', len(notes)) + # Attach target deck to each note + for note in notes: + note.target_deck = self.target_deck except NotionClientError as exc: self.logger.error('Error extracting notes', exc_info=exc) error_msg = f'Cannot export {self.page_id}:\n{exc}' diff --git a/notion_sync_addon/config.json b/notion_sync_addon/config.json index 94b66e8..09b1bb3 100644 --- a/notion_sync_addon/config.json +++ b/notion_sync_addon/config.json @@ -1,8 +1,23 @@ { "debug": false, "sync_every_minutes": 30, - "anki_target_deck": "Notion Sync", + "anki_target_deck": "Default deck", "notion_token": "", "notion_namespace": "", - "notion_pages": [{"page_id": "ID", "recursive": true}] + "notion_pages": [ + { + "page_id": "page-id-1", + "recursive": false, + "target_deck": "deck01" + }, + { + "page_id": "page-id-2", + "recursive": true, + "target_deck": "deck02" + }, + { + "page_id": "page_id3", + "recursive": true + } + ] } diff --git a/notion_sync_addon/config.md b/notion_sync_addon/config.md index 3d28b8d..07ac6a0 100644 --- a/notion_sync_addon/config.md +++ b/notion_sync_addon/config.md @@ -20,11 +20,13 @@ The `notion_pages` section should look like that: "notion_pages": [ { "page_id": "", - "recursive": false + "recursive": false, + "target_deck": "Math" }, { "page_id": "", - "recursive": true + "recursive": true, + "target_deck": "Biology" } ] ``` diff --git a/notion_sync_addon/meta.json b/notion_sync_addon/meta.json new file mode 100644 index 0000000..244fe54 --- /dev/null +++ b/notion_sync_addon/meta.json @@ -0,0 +1 @@ +{"config": {"anki_target_deck": "Notion Sync", "debug": false, "notion_namespace": "elliotverstraelen", "notion_pages": [{"page_id": "5fbd73fb944e4929af70faede1c9b5c9", "recursive": true, "target_deck": "Machine-Learning-test"}, {"page_id": "a0e163f763154e048a97998f5c389ca7", "recursive": true, "target_deck": "TMIRI-test"}, {"page_id": "page_id3", "recursive": true}], "notion_token": "v02%3Auser_token_or_cookies%3AyNbL1xVS_Z5g_T5f76sn0nIn-z8eIqaj2lazRlnHS4LCoBuCPx7qaIg6DRVe4WhZMNN7u0qCjy6qf8lfdwF7g5XcKaowTWn3ol5Rzb0AUeYUaJLkC9AcH7Yiny8X_QE-hdBR", "sync_every_minutes": 30}} \ No newline at end of file diff --git a/notion_sync_addon/notes_manager.py b/notion_sync_addon/notes_manager.py index 44beb5f..ea3f46b 100644 --- a/notion_sync_addon/notes_manager.py +++ b/notion_sync_addon/notes_manager.py @@ -59,7 +59,7 @@ def __init__( self.logger = get_logger(self.__class__.__name__, debug) self.collection = collection self.deck_name = deck_name - self.deck = self.get_deck() + self.deck = self.get_deck(self.deck_name) # Pass deck_name to get_deck self.create_models() @property @@ -139,12 +139,13 @@ def create_models(self) -> None: cloze_model.pop('css') self.logger.info(f'Cloze model updated: {cloze_model}') - def get_deck(self) -> int: + def get_deck(self, deck_name: str) -> int: """Get or create target deck. - :returns: working deck + :param deck_name: name of the target deck + :returns: deck id """ - deck_id = self.collection.decks.id(self.deck_name, create=True) + deck_id = self.collection.decks.id(deck_name, create=True) assert deck_id # mypy return deck_id @@ -186,10 +187,11 @@ def _fill_fields( target[field_name] = new_value return updated_data - def create_note(self, note: AnkiNote) -> int: - """Create new note. + def create_note_in_deck(self, note: AnkiNote, deck_name: str) -> int: + """Create new note in specified deck. :param note: note + :param deck_name: target deck name :returns: id of the note created """ # Pick the model @@ -199,7 +201,7 @@ def create_note(self, note: AnkiNote) -> int: model = self.collection.models.by_name(self.MODEL_NAME) # Create a note and add it to the deck anki_note = Note(self.collection, model) - deck_id = self.get_deck() + deck_id = self.get_deck(deck_name) self.collection.add_note(anki_note, deck_id) # Upload note media media_manager = self.collection.media @@ -229,6 +231,14 @@ def create_note(self, note: AnkiNote) -> int: ) return note_id + def create_note(self, note: AnkiNote) -> int: + """Create new note in default deck. + + :param note: note + :returns: id of the note created + """ + return self.create_note_in_deck(note, self.deck_name) + def update_note(self, note_id: int, note: AnkiNote) -> bool: """Update existing note.