diff --git a/README.md b/README.md
index a9d139e..585e9ff 100644
--- a/README.md
+++ b/README.md
@@ -3058,7 +3058,7 @@ corresponding to when the API was available for use.
#### `updateNoteFields`
-* Modify the fields of an exist note. You can also include audio, video, or picture files which will be added to the note with an
+* Modify the fields of an existing note. You can also include audio, video, or picture files which will be added to the note with an
optional `audio`, `video`, or `picture` property. Please see the documentation for `addNote` for an explanation of objects in the `audio`, `video`, or `picture` array.
> **Warning**:
@@ -3105,6 +3105,122 @@ corresponding to when the API was available for use.
```
+#### `updateNote`
+
+* Modify the fields and/or tags of an existing note.
+ In other words, combines `updateNoteFields` and `updateNoteTags`.
+ Please see their documentation for an explanation of all properties.
+
+ Either `fields` or `tags` property can be omitted without affecting the other.
+ Thus valid requests to `updateNoteFields` also work with `updateNote`.
+ The note must have the `fields` property in order to update the optional audio, video, or picture objects.
+
+ If neither `fields` nor `tags` are provided, the method will fail.
+ Fields are updated first and are not rolled back if updating tags fails.
+ Tags are not updated if updating fields fails.
+
+ > **Warning**
+ > You must not be viewing the note that you are updating on your Anki browser, otherwise
+ > the fields will not update. See [this issue](https://github.com/FooSoft/anki-connect/issues/82)
+ > for further details.
+
+
+ Sample request:
+
+ ```json
+ {
+ "action": "updateNote",
+ "version": 6,
+ "params": {
+ "note": {
+ "id": 1514547547030,
+ "fields": {
+ "Front": "new front content",
+ "Back": "new back content"
+ },
+ "tags": ["new", "tags"]
+ }
+ }
+ }
+ ```
+
+
+
+
+ Sample result:
+
+ ```json
+ {
+ "result": null,
+ "error": null
+ }
+ ```
+
+
+
+#### `updateNoteTags`
+
+* Set a note's tags by note ID. Old tags will be removed.
+
+
+ Sample request:
+
+ ```json
+ {
+ "action": "updateNoteTags",
+ "version": 6,
+ "params": {
+ "note": 1483959289817,
+ "tags": ["european-languages"]
+ }
+ }
+ ```
+
+
+
+
+ Sample result:
+
+ ```json
+ {
+ "result": null,
+ "error": null
+ }
+ ```
+
+
+
+#### `getNoteTags`
+
+* Get a note's tags by note ID.
+
+
+ Sample request:
+
+ ```json
+ {
+ "action": "getNoteTags",
+ "version": 6,
+ "params": {
+ "note": 1483959289817
+ }
+ }
+ ```
+
+
+
+
+ Sample result:
+
+ ```json
+ {
+ "result": ["european-languages"],
+ "error": null
+ }
+ ```
+
+
+
#### `addTags`
* Adds tags to notes by note ID.
diff --git a/plugin/__init__.py b/plugin/__init__.py
index c578ee4..eaefc32 100644
--- a/plugin/__init__.py
+++ b/plugin/__init__.py
@@ -819,6 +819,37 @@ class AnkiConnect:
self.stopEditing()
+ @util.api()
+ def updateNote(self, note):
+ updated = False
+ if 'fields' in note.keys():
+ self.updateNoteFields(note)
+ updated = True
+ if 'tags' in note.keys():
+ self.updateNoteTags(note['id'], note['tags'])
+ updated = True
+ if not updated:
+ raise Exception('Must provide a "fields" or "tags" property.')
+
+
+ @util.api()
+ def updateNoteTags(self, note, tags):
+ if type(tags) == str:
+ tags = [tags]
+ if type(tags) != list or not all([type(t) == str for t in tags]):
+ raise Exception('Must provide tags as a list of strings')
+
+ for old_tag in self.getNoteTags(note):
+ self.removeTags([note], old_tag)
+ for new_tag in tags:
+ self.addTags([note], new_tag)
+
+
+ @util.api()
+ def getNoteTags(self, note):
+ return self.getNote(note).tags
+
+
@util.api()
def addTags(self, notes, tags, add=True):
self.startEditing()
diff --git a/tests/test_notes.py b/tests/test_notes.py
index f4bc874..7558f99 100755
--- a/tests/test_notes.py
+++ b/tests/test_notes.py
@@ -98,6 +98,12 @@ class TestTags:
ac.clearUnusedTags()
assert ac.getTags() == ["tag1"]
+ def test_updateNoteTags_and_getNoteTags(self, setup):
+ ac.updateNoteTags(note=setup.note1_id, tags="footag")
+ assert ac.getNoteTags(note=setup.note1_id) == ["footag"]
+ ac.updateNoteTags(note=setup.note1_id, tags=["foo", "bar", "baz"])
+ assert len(ac.getNoteTags(note=setup.note1_id)) == 3
+
class TestUpdateNoteFields:
def test_updateNoteFields(self, setup):
@@ -107,12 +113,27 @@ class TestUpdateNoteFields:
notes_info = ac.notesInfo(notes=[setup.note1_id])
assert notes_info[0]["fields"]["field2"]["value"] == "bar"
- def test_updateNoteFields_will_note_update_invalid_notes(self, setup):
+ def test_updateNoteFields_will_not_update_invalid_notes(self, setup):
bad_note = {"id": 123, "fields": make_note()["fields"]}
with pytest.raises(NotFoundError):
ac.updateNoteFields(note=bad_note)
+class TestUpdateNote:
+ def test_updateNote(self, setup):
+ new_fields = {"field1": "frontbar", "field2": "backbar"}
+ new_tags = ["foobar"]
+ good_note = {"id": setup.note1_id, "fields": new_fields, "tags": new_tags}
+ ac.updateNote(note=good_note)
+ notes_info = ac.notesInfo(notes=[setup.note1_id])
+ assert notes_info[0]["fields"]["field2"]["value"] == "backbar"
+ assert notes_info[0]["tags"] == ["foobar"]
+
+ def test_updateNote_requires_either_fields_or_tags(self, setup):
+ with pytest.raises(Exception, match="ust provide"):
+ ac.updateNote(note={"id": setup.note1_id})
+
+
class TestCanAddNotes:
foo_bar_notes = [make_note(front="foo"), make_note(front="bar")]