From 6b81ef2f335184b26e603ef6441993c4757763cd Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Thu, 6 Jul 2017 00:14:04 -0300 Subject: [PATCH 01/85] Including travis support to execute tests. --- .travis.yml | 10 ++++++++++ tests/Dockerfile | 12 ++++++++++++ tests/prefs.db | Bin 0 -> 5120 bytes 3 files changed, 22 insertions(+) create mode 100644 .travis.yml create mode 100644 tests/Dockerfile create mode 100644 tests/prefs.db diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cae1ba7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +sudo: required +services: + - docker +language: python +python: + - "2.7" +install: + - docker build -f tests/Dockerfile -t txgio/anki-connect . +script: + - curl localhost:8765 -X POST -s -d '{"action": "version"}' \ No newline at end of file diff --git a/tests/Dockerfile b/tests/Dockerfile new file mode 100644 index 0000000..2932c33 --- /dev/null +++ b/tests/Dockerfile @@ -0,0 +1,12 @@ +FROM txgio/anki + +RUN mkdir -p /home/anki-user/Documents/Anki/addons + +COPY AnkiConnect.py /home/anki-user/Documents/Anki/addons + +# Required if you want to access it from another IP address. +# RUN sed -i 's/127.0.0.1/0.0.0.0/' /home/anki-user/Documents/Anki/addons/AnkiConnect.py + +COPY tests/prefs.db /home/anki-user/Documents/Anki/prefs.db + +CMD /bin/bash -c "(/usr/bin/ibus-daemon -xd; /usr/bin/anki;)" \ No newline at end of file diff --git a/tests/prefs.db b/tests/prefs.db new file mode 100644 index 0000000000000000000000000000000000000000..eee5d703bf9e8dd3327d05e289a7717d1e875c6d GIT binary patch literal 5120 zcmeHK&u`N(6i%9s4NDU@rfHfKfl!CUMx3^5dZU}FZK50Ny1}KQavP^aO_Sg_Tgz=i z;>O>>AH)B_VRue&;JtL&!X&f<7c^qUvfuao{Nwxls=Cu|xsvNnAjVAUWu>U7s`5hD z6-8NqItMi^Ds+me0WI3Ml?7$}+x(*TQz>X8MH^{fZwtu%IRkf;fw$`X((0;u{y?(6 z$3qb~uE(RaEo>h(8^=xkxUto0>M2Urd^YB~k3H|{ z^TNHQ?NwEAeVb3B_a5YRkHs=b>|Q!!kET5hcrIdstJ)8wd~ViYH!nE@IRkf}fuc&_ zkOl2R(Jr*_cmKTk33CSiR|bf-te!+%=(IqzmHWM+7xbC82z030Lj}IP=4Fno*&!vJ(kT}u5}IVsAW^5}xFV8Au@6ugpadHT&Lp=1s*pWesn^R!&9qD$ zZgYplo@_CH2<|FO$1!vheQ^l?=6M8W4Q8SUMR6Dk9z`dK3p`f3hV4f9e6Sm~9rzei zCu?3vll46o$^8fypp@*bCo{RY9SXZPDYZ6hWz(t})UayRnguZ%N&YKe6#V|-^67P< zxcvFa?Dy+Zk7()-Uh*j%&m;%HnL7>xA@>8D0|c7Ph=f7=+;$mYAj*tgS@1d4ff)fqL1rs(CI;_z-AD!kHfLF;{r*t@N%gRO@PA8BBx(SDCZ%rj{2*F@?Fr+&%Xm?`Qp&H%Epbs{*h$Sud z(`r4+7yvr_%J#bxTD`I+hS{B1cE&N(;wv~xTKiJOT24>7iH7k(+zW`S}jD!FH literal 0 HcmV?d00001 From d7e32d4081954a9c5ed65e4561191d702e391992 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Thu, 6 Jul 2017 00:15:42 -0300 Subject: [PATCH 02/85] Forcing build. --- tests/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Dockerfile b/tests/Dockerfile index 2932c33..c4d2953 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -5,7 +5,7 @@ RUN mkdir -p /home/anki-user/Documents/Anki/addons COPY AnkiConnect.py /home/anki-user/Documents/Anki/addons # Required if you want to access it from another IP address. -# RUN sed -i 's/127.0.0.1/0.0.0.0/' /home/anki-user/Documents/Anki/addons/AnkiConnect.py +# RUN sed -i 's/127.0.0.1/0.0.0.0/' /home/anki-user/Documents/Anki/addons/AnkiConnect.py COPY tests/prefs.db /home/anki-user/Documents/Anki/prefs.db From 989c750a733d8c8c5206403f2eec05b00d02e517 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Thu, 6 Jul 2017 00:19:44 -0300 Subject: [PATCH 03/85] Fixing .yml. --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index cae1ba7..dc83438 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ sudo: required +language: python services: - docker -language: python python: - "2.7" install: - docker build -f tests/Dockerfile -t txgio/anki-connect . -script: - - curl localhost:8765 -X POST -s -d '{"action": "version"}' \ No newline at end of file +script: > + - `curl localhost:8765 -X POST -s -d '{"action": "version"}'` \ No newline at end of file From 8734bacac2486a3dbe6ef24b1320a4ac16bf498d Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Thu, 6 Jul 2017 00:22:51 -0300 Subject: [PATCH 04/85] Fixing another syntax error. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dc83438..b8035cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,4 @@ python: install: - docker build -f tests/Dockerfile -t txgio/anki-connect . script: > - - `curl localhost:8765 -X POST -s -d '{"action": "version"}'` \ No newline at end of file + - curl localhost:8765 -X POST -s -d '{"action": "version"}' \ No newline at end of file From 4e94a358ac6e1b3ef980ccc150ebea7c63e467e8 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Thu, 6 Jul 2017 00:25:39 -0300 Subject: [PATCH 05/85] Trying to fix another error. --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b8035cf..d76ea34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,5 +6,4 @@ python: - "2.7" install: - docker build -f tests/Dockerfile -t txgio/anki-connect . -script: > - - curl localhost:8765 -X POST -s -d '{"action": "version"}' \ No newline at end of file +script: > curl localhost:8765 -X POST -s -d '{"action": "version"}' \ No newline at end of file From d4e521b17d33684ee87b3bfeb5e1933f03b1f0ea Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Thu, 6 Jul 2017 00:28:47 -0300 Subject: [PATCH 06/85] Another try. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d76ea34..fe00e3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,5 @@ python: - "2.7" install: - docker build -f tests/Dockerfile -t txgio/anki-connect . -script: > curl localhost:8765 -X POST -s -d '{"action": "version"}' \ No newline at end of file +script: > + curl localhost:8765 -X POST -s -d '{"action": "version"}' \ No newline at end of file From 6bec4bb5b6ae9224171d14d25197ca4ff570f16c Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Thu, 6 Jul 2017 00:53:54 -0300 Subject: [PATCH 07/85] Including entrypoint.sh to run xvfb to simulate an X Server. --- tests/Dockerfile | 15 ++++++++++++++- tests/entrypoint.sh | 8 ++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/entrypoint.sh diff --git a/tests/Dockerfile b/tests/Dockerfile index c4d2953..46f6001 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -1,12 +1,25 @@ FROM txgio/anki +USER root +RUN apt-get update +RUN apt-get install -y xvfb +USER anki-user + RUN mkdir -p /home/anki-user/Documents/Anki/addons COPY AnkiConnect.py /home/anki-user/Documents/Anki/addons # Required if you want to access it from another IP address. -# RUN sed -i 's/127.0.0.1/0.0.0.0/' /home/anki-user/Documents/Anki/addons/AnkiConnect.py +RUN sed -i 's/127.0.0.1/0.0.0.0/' /home/anki-user/Documents/Anki/addons/AnkiConnect.py COPY tests/prefs.db /home/anki-user/Documents/Anki/prefs.db +ADD tests/entrypoint.sh /entrypoint.sh + +USER root +RUN chmod +x /entrypoint.sh +USER anki-user + +ENTRYPOINT ["/entrypoint.sh"] + CMD /bin/bash -c "(/usr/bin/ibus-daemon -xd; /usr/bin/anki;)" \ No newline at end of file diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh new file mode 100644 index 0000000..8285fb9 --- /dev/null +++ b/tests/entrypoint.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -e + +# Start Xvfb +Xvfb -ac -screen scrn 1280x2000x24 :99.0 & +export DISPLAY=:99.0 + +exec "$@" \ No newline at end of file From b64c6e34ee10996c4d0aa77ee2f6969168ad5ade Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Thu, 6 Jul 2017 00:55:10 -0300 Subject: [PATCH 08/85] Running docker. --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index fe00e3a..cbf008e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,5 +6,7 @@ python: - "2.7" install: - docker build -f tests/Dockerfile -t txgio/anki-connect . -script: > - curl localhost:8765 -X POST -s -d '{"action": "version"}' \ No newline at end of file +script: + - docker run -ti -d --rm -p 8765:8765 txgio/anki-connect + - > + curl localhost:8765 -X POST -s -d '{"action": "version"}' \ No newline at end of file From fdabb37aaffe863f8f1b4a5465eef1bc975b5222 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Thu, 6 Jul 2017 00:59:42 -0300 Subject: [PATCH 09/85] Including a delay to start the test. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index cbf008e..30b971b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,5 +8,6 @@ install: - docker build -f tests/Dockerfile -t txgio/anki-connect . script: - docker run -ti -d --rm -p 8765:8765 txgio/anki-connect + - sleep 5 - > curl localhost:8765 -X POST -s -d '{"action": "version"}' \ No newline at end of file From b8c5b7c980af8bb17de116844e7d0ad74775b3e4 Mon Sep 17 00:00:00 2001 From: David Bailey Date: Wed, 16 Aug 2017 22:26:53 +0100 Subject: [PATCH 10/85] Add deckNamesAndIds action --- AnkiConnect.py | 16 ++++++++++++++++ README.md | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/AnkiConnect.py b/AnkiConnect.py index c7f478f..d31859c 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -515,6 +515,17 @@ class AnkiBridge: return collection.decks.allNames() + def deckNamesAndIds(self): + decks = {} + + deckNames = self.deckNames() + for deck in deckNames: + id = self.collection().decks.id(deck) + decks[deck] = id + + return decks + + def deckNameFromId(self, deckId): collection = self.collection() if collection is not None: @@ -753,6 +764,11 @@ class AnkiConnect: return self.anki.deckNames() + @webApi + def deckNamesAndIds(self): + return self.anki.deckNamesAndIds() + + @webApi def modelNames(self): return self.anki.modelNames() diff --git a/README.md b/README.md index e16faf1..d2493c3 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,24 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` +* **deckNamesAndIds** + + Gets the complete list of deck names and their respective IDs for the current user. + + *Sample request*: + ``` + { + "action": "deckNamesAndIds" + } + ``` + + *Sample response*: + ``` + { + "Default": 1 + } + ``` + * **modelNames** Gets the complete list of model names for the current user. From 5ec2d7e33e5a29a73ce2aafcf84f2065b059d86b Mon Sep 17 00:00:00 2001 From: David Bailey Date: Thu, 17 Aug 2017 13:25:06 +0100 Subject: [PATCH 11/85] Add config group actions + Move the multi action to a more sensible position --- AnkiConnect.py | 94 +++++++++++++++++++++++++--- README.md | 166 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 239 insertions(+), 21 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index d31859c..d16ddd4 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -474,6 +474,13 @@ class AnkiBridge: return self.collection().sched + def multi(self, actions): + response = [] + for item in actions: + response.append(AnkiConnect.handler(ac, item)) + return response + + def media(self): collection = self.collection() if collection is not None: @@ -502,11 +509,59 @@ class AnkiBridge: return [field['name'] for field in model['flds']] - def multi(self, actions): - response = [] - for item in actions: - response.append(AnkiConnect.handler(ac, item)) - return response + def confForDeck(self, deck): + if not deck in self.deckNames(): + return False + + id = self.collection().decks.id(deck) + return self.collection().decks.confForDid(id) + + + def saveConf(self, conf): + id = str(conf['id']) + if not id in self.collection().decks.dconf: + return False + + mod = anki.utils.intTime() + usn = self.collection().usn() + + conf['mod'] = mod + conf['usn'] = usn + + self.collection().decks.dconf[id] = conf + self.collection().decks.changed = True + return True + + + def changeConf(self, decks, confId): + for deck in decks: + if not deck in self.deckNames(): + return False + + if not str(confId) in self.collection().decks.dconf: + return False + + for deck in decks: + did = str(self.collection().decks.id(deck)) + aqt.mw.col.decks.decks[did]['conf'] = confId + + return True + + + def addConf(self, name, cloneFrom=1): + if not str(cloneFrom) in self.collection().decks.dconf: + return False + + cloneFrom = self.collection().decks.getConf(cloneFrom) + return self.collection().decks.confId(name, cloneFrom) + + + def remConf(self, id): + if id == 1 or not str(id) in self.collection().decks.dconf: + return False + + self.collection().decks.remConf(id) + return True def deckNames(self): @@ -759,6 +814,11 @@ class AnkiConnect: return handler(**params) + @webApi + def multi(self, actions): + return self.anki.multi(actions) + + @webApi def deckNames(self): return self.anki.deckNames() @@ -780,8 +840,28 @@ class AnkiConnect: @webApi - def multi(self, actions): - return self.anki.multi(actions) + def confForDeck(self, deck): + return self.anki.confForDeck(deck) + + + @webApi + def saveConf(self, conf): + return self.anki.saveConf(conf) + + + @webApi + def changeConf(self, decks, confId): + return self.anki.changeConf(decks, confId) + + + @webApi + def addConf(self, name, cloneFrom=1): + return self.anki.addConf(name, cloneFrom) + + + @webApi + def remConf(self, id): + return self.anki.remConf(id) @webApi diff --git a/README.md b/README.md index d2493c3..f36c6ed 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,35 @@ Below is a list of currently supported actions. Requests with invalid actions or ``` 4 ``` + +* **multi** + + Performs multiple actions in one request, returning an array with the response of each action (in the given order). + + *Sample request*: + ``` + { + "action": "multi", + "params": { + "actions": [ + {"action": "deckNames"}, + { + "action": "browse", + "params": {"query": "deck:current"} + } + ] + } + } + ``` + + *Sample response*: + ``` + [ + ["Default"], + [1494723142483, 1494703460437, 1494703479525] + ] + ``` + * **deckNames** Gets the complete list of deck names for the current user. @@ -180,32 +209,141 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` -* **multi** +* **confForDeck** - Performs multiple actions in one request, returning an array with the response of each action (in the given order). + Gets the config group object for the given deck. *Sample request*: ``` { - "action": "multi", + "action": "confForDeck", "params": { - "actions": [ - {"action": "deckNames"}, - { - "action": "browse", - "params": {"query": "deck:current"} - } - ] + "deck": "Default" } } ``` *Sample response*: ``` - [ - ["Default"], - [1494723142483, 1494703460437, 1494703479525] - ] + { + "lapse": { + "leechFails": 8, + "delays": [10], + "minInt": 1, + "leechAction": 0, + "mult": 0 + }, + "dyn": false, + "autoplay": true, + "mod": 1502970872, + "id": 1, + "maxTaken": 60, + "new": { + "bury": true, + "order": 1, + "initialFactor": 2500, + "perDay": 20, + "delays": [1, 10], + "separate": true, + "ints": [1, 4, 7] + }, + "name": "Default", + "rev": { + "bury": true, + "ivlFct": 1, + "ease4": 1.3, + "maxIvl": 36500, + "perDay": 100, + "minSpace": 1, + "fuzz": 0.05 + }, + "timer": 0, + "replayq": true, + "usn": -1 + } + ``` + +* **saveConf** + + Saves the given config group, returning `true` on success or `false` if the ID of the config group is invalid (i.e. + it does not exist). + + *Sample request*: + ``` + { + "action": "saveConf", + "params": { + "conf": (config group object) + } + } + ``` + + *Sample response*: + ``` + true + ``` + +* **changeConf** + + Changes the configuration group for the given decks to the one with the given ID. Returns `true` on success or + `false` if the given configuration group or any of the given decks do not exist. + + *Sample request*: + ``` + { + "action": "changeConf", + "params": { + "decks": ["Default"], + "confId": 1 + } + } + ``` + + *Sample response*: + ``` + true + ``` + +* **addConf** + + Creates a new config group with the given name, cloning from the group with the given ID, or from the default group + if this is unspecified. Returns the ID of the new config group, or `false` if the specified group to clone from does + not exist. + + *Sample request*: + ``` + { + "action": "addConf", + "params": { + "name": "Copy of Default", + "cloneFrom": 1 + } + } + ``` + + *Sample response*: + ``` + 1502972374573 + ``` + +* **remConf** + + Removes the config group with the given ID, returning `true` if successful, or `false` if attempting to remove + either the default config group (ID = 1) or a config group that does not exist. + + *Sample request*: + ``` + { + "action": "remConf", + "params": { + "id": 1502972374573 + } + } + ``` + + *Sample response*: + ``` + true ``` * **addNote** From a9f9acf85f490fa6b021410d2f59a69b665fa7f8 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Thu, 17 Aug 2017 21:22:15 -0700 Subject: [PATCH 12/85] Updating README.md --- README.md | 216 ++---------------------------------------------------- 1 file changed, 6 insertions(+), 210 deletions(-) diff --git a/README.md b/README.md index d2493c3..980f4fe 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Below is a list of currently supported actions. Requests with invalid actions or Gets the version of the API exposed by this plugin. Currently versions `1` through `4` are defined. This should be the first call you make to make sure that your application and AnkiConnect are able to communicate - properly with each other. New versions of AnkiConnect are backwards compatible; as long as you are using actions + properly with each other. New versions of AnkiConnect will backwards compatible; as long as you are using actions which are available in the reported AnkiConnect version or earlier, everything should work fine. *Sample request*: @@ -121,24 +121,6 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` -* **deckNamesAndIds** - - Gets the complete list of deck names and their respective IDs for the current user. - - *Sample request*: - ``` - { - "action": "deckNamesAndIds" - } - ``` - - *Sample response*: - ``` - { - "Default": 1 - } - ``` - * **modelNames** Gets the complete list of model names for the current user. @@ -365,8 +347,7 @@ Below is a list of currently supported actions. Requests with invalid actions or * **suspend** - Suspend cards by card ID; returns `true` if successful (at least one card wasn't already suspended) or `false` - otherwise. + Suspend cards by card ID. *Sample request*: ``` @@ -380,13 +361,12 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample response*: ``` - true + null ``` * **unsuspend** - Unsuspend cards by card ID; returns `true` if successful (at least one card was previously suspended) or `false` - otherwise. + Unsuspend cards by card ID. *Sample request*: ``` @@ -400,89 +380,9 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample response*: ``` - true + null ``` -* **areSuspended** - - Returns an array indicating whether each of the given cards is suspended (in the same order). - - *Sample request*: - ``` - { - "action": "areSuspended", - "params": { - "cards": [1483959291685, 1483959293217] - } - } - ``` - - *Sample response*: - ``` - [false, true] - ``` - -* **areDue** - - Returns an array indicating whether each of the given cards is due (in the same order). Note: cards in the learning - queue with a large interval (over 20 minutes) are treated as not due until the time of their interval has passed, to - match the way Anki treats them when reviewing. - - *Sample request*: - ``` - { - "action": "areDue", - "params": { - "cards": [1483959291685, 1483959293217] - } - } - ``` - - *Sample response*: - ``` - [false, true] - ``` - -* **getIntervals** - - Returns an array of the most recent intervals for each given card ID, or a 2-dimensional array of all the intervals - for each given card ID when `complete` is `true`. (Negative intervals are in seconds and positive intervals in days.) - - *Sample request 1*: - ``` - { - "action": "getIntervals", - "params": { - "cards": [1502298033753, 1502298036657] - } - } - ``` - - *Sample response 1*: - ``` - [-14400, 3] - ``` - - *Sample request 2*: - ``` - { - "action": "getIntervals", - "params": { - "cards": [1502298033753, 1502298036657], - "complete": true - } - } - ``` - - *Sample response 2*: - ``` - [ - [-120, -180, -240, -300, -360, -14400], - [-120, -180, -240, -300, -360, -14400, 1, 3] - ] - ``` - - * **findNotes** Returns an array of note IDs for a given query (same query syntax as **guiBrowse**). @@ -490,7 +390,7 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample request*: ``` { - "action": "findNotes", + "action": "findCards", "params": { "query": "deck:current" } @@ -529,94 +429,6 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` -* **getDecks** - - Accepts an array of card IDs and returns an object with each deck name as a key, and its value an array of the given - cards which belong to it. - - *Sample request*: - ``` - { - "action": "getDecks", - "params": { - "cards": [1502298036657, 1502298033753, 1502032366472] - } - } - ``` - - *Sample response*: - ``` - { - "Default": [1502032366472], - "Japanese::JLPT N3": [1502298036657, 1502298033753] - } - ``` - - -* **changeDeck** - - Moves cards with the given IDs to a different deck, creating the deck if it doesn't exist yet. - - *Sample request*: - ``` - { - "action": "changeDeck", - "params": { - "cards": [1502098034045, 1502098034048, 1502298033753], - "deck": "Japanese::JLPT N3" - } - } - ``` - - *Sample response*: - ``` - null - ``` - -* **deleteDecks** - - Deletes decks with the given names. If `cardsToo` is `true` (defaults to `false` if unspecified), the cards within - the deleted decks will also be deleted; otherwise they will be moved to the default deck. - - *Sample request*: - ``` - { - "action": "deleteDecks", - "params": { - "decks": ["Japanese::JLPT N5", "Easy Spanish"], - "cardsToo": true - } - } - ``` - - *Sample response*: - ``` - null - ``` - -* **cardsToNotes** - - Returns an (unordered) array of note IDs for the given card IDs. For cards with the same note, the ID is only - given once in the array. - - *Sample request*: - ``` - { - "action": "cardsToNotes", - "params": { - "cards": [1502098034045, 1502098034048, 1502298033753] - } - } - ``` - - *Sample response*: - ``` - [ - 1502098029797, - 1502298025183 - ] - ``` - * **guiBrowse** Invokes the card browser and searches for a given query. Returns an array of identifiers of the cards that were found. @@ -690,22 +502,6 @@ Below is a list of currently supported actions. Requests with invalid actions or } ``` -* **guiStartCardTimer** - - Starts or resets the 'timerStarted' value for the current card. This is useful for deferring the start time to when it is displayed via the API, allowing the recorded time taken to answer the card to be more accurate when calling guiAnswerCard. - - *Sample request*: - ``` - { - "action": "guiStartCardTimer" - } - ``` - - *Sample response*: - ``` - true - ``` - * **guiShowQuestion** Shows question text for the current card; returns `true` if in review mode or `false` otherwise. From 880ff722bb578ec6d0fa97aa48bc9853141f9035 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Thu, 17 Aug 2017 21:23:56 -0700 Subject: [PATCH 13/85] Revert "Updating README.md" This reverts commit a9f9acf85f490fa6b021410d2f59a69b665fa7f8. --- README.md | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 210 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 980f4fe..d2493c3 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Below is a list of currently supported actions. Requests with invalid actions or Gets the version of the API exposed by this plugin. Currently versions `1` through `4` are defined. This should be the first call you make to make sure that your application and AnkiConnect are able to communicate - properly with each other. New versions of AnkiConnect will backwards compatible; as long as you are using actions + properly with each other. New versions of AnkiConnect are backwards compatible; as long as you are using actions which are available in the reported AnkiConnect version or earlier, everything should work fine. *Sample request*: @@ -121,6 +121,24 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` +* **deckNamesAndIds** + + Gets the complete list of deck names and their respective IDs for the current user. + + *Sample request*: + ``` + { + "action": "deckNamesAndIds" + } + ``` + + *Sample response*: + ``` + { + "Default": 1 + } + ``` + * **modelNames** Gets the complete list of model names for the current user. @@ -347,7 +365,8 @@ Below is a list of currently supported actions. Requests with invalid actions or * **suspend** - Suspend cards by card ID. + Suspend cards by card ID; returns `true` if successful (at least one card wasn't already suspended) or `false` + otherwise. *Sample request*: ``` @@ -361,12 +380,13 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample response*: ``` - null + true ``` * **unsuspend** - Unsuspend cards by card ID. + Unsuspend cards by card ID; returns `true` if successful (at least one card was previously suspended) or `false` + otherwise. *Sample request*: ``` @@ -380,9 +400,89 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample response*: ``` - null + true ``` +* **areSuspended** + + Returns an array indicating whether each of the given cards is suspended (in the same order). + + *Sample request*: + ``` + { + "action": "areSuspended", + "params": { + "cards": [1483959291685, 1483959293217] + } + } + ``` + + *Sample response*: + ``` + [false, true] + ``` + +* **areDue** + + Returns an array indicating whether each of the given cards is due (in the same order). Note: cards in the learning + queue with a large interval (over 20 minutes) are treated as not due until the time of their interval has passed, to + match the way Anki treats them when reviewing. + + *Sample request*: + ``` + { + "action": "areDue", + "params": { + "cards": [1483959291685, 1483959293217] + } + } + ``` + + *Sample response*: + ``` + [false, true] + ``` + +* **getIntervals** + + Returns an array of the most recent intervals for each given card ID, or a 2-dimensional array of all the intervals + for each given card ID when `complete` is `true`. (Negative intervals are in seconds and positive intervals in days.) + + *Sample request 1*: + ``` + { + "action": "getIntervals", + "params": { + "cards": [1502298033753, 1502298036657] + } + } + ``` + + *Sample response 1*: + ``` + [-14400, 3] + ``` + + *Sample request 2*: + ``` + { + "action": "getIntervals", + "params": { + "cards": [1502298033753, 1502298036657], + "complete": true + } + } + ``` + + *Sample response 2*: + ``` + [ + [-120, -180, -240, -300, -360, -14400], + [-120, -180, -240, -300, -360, -14400, 1, 3] + ] + ``` + + * **findNotes** Returns an array of note IDs for a given query (same query syntax as **guiBrowse**). @@ -390,7 +490,7 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample request*: ``` { - "action": "findCards", + "action": "findNotes", "params": { "query": "deck:current" } @@ -429,6 +529,94 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` +* **getDecks** + + Accepts an array of card IDs and returns an object with each deck name as a key, and its value an array of the given + cards which belong to it. + + *Sample request*: + ``` + { + "action": "getDecks", + "params": { + "cards": [1502298036657, 1502298033753, 1502032366472] + } + } + ``` + + *Sample response*: + ``` + { + "Default": [1502032366472], + "Japanese::JLPT N3": [1502298036657, 1502298033753] + } + ``` + + +* **changeDeck** + + Moves cards with the given IDs to a different deck, creating the deck if it doesn't exist yet. + + *Sample request*: + ``` + { + "action": "changeDeck", + "params": { + "cards": [1502098034045, 1502098034048, 1502298033753], + "deck": "Japanese::JLPT N3" + } + } + ``` + + *Sample response*: + ``` + null + ``` + +* **deleteDecks** + + Deletes decks with the given names. If `cardsToo` is `true` (defaults to `false` if unspecified), the cards within + the deleted decks will also be deleted; otherwise they will be moved to the default deck. + + *Sample request*: + ``` + { + "action": "deleteDecks", + "params": { + "decks": ["Japanese::JLPT N5", "Easy Spanish"], + "cardsToo": true + } + } + ``` + + *Sample response*: + ``` + null + ``` + +* **cardsToNotes** + + Returns an (unordered) array of note IDs for the given card IDs. For cards with the same note, the ID is only + given once in the array. + + *Sample request*: + ``` + { + "action": "cardsToNotes", + "params": { + "cards": [1502098034045, 1502098034048, 1502298033753] + } + } + ``` + + *Sample response*: + ``` + [ + 1502098029797, + 1502298025183 + ] + ``` + * **guiBrowse** Invokes the card browser and searches for a given query. Returns an array of identifiers of the cards that were found. @@ -502,6 +690,22 @@ Below is a list of currently supported actions. Requests with invalid actions or } ``` +* **guiStartCardTimer** + + Starts or resets the 'timerStarted' value for the current card. This is useful for deferring the start time to when it is displayed via the API, allowing the recorded time taken to answer the card to be more accurate when calling guiAnswerCard. + + *Sample request*: + ``` + { + "action": "guiStartCardTimer" + } + ``` + + *Sample response*: + ``` + true + ``` + * **guiShowQuestion** Shows question text for the current card; returns `true` if in review mode or `false` otherwise. From 81d49c3cd886fc296b42939f9e03d2d2cae756ff Mon Sep 17 00:00:00 2001 From: David Bailey Date: Fri, 18 Aug 2017 16:15:08 +0100 Subject: [PATCH 14/85] Fix broken guiCurrentCard A map can't be converted to JSON --- AnkiConnect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index d31859c..f9061e7 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -636,7 +636,7 @@ class AnkiBridge: 'fieldOrder': card.ord, 'question': card._getQA()['q'], 'answer': card._getQA()['a'], - 'buttons': map(lambda b: b[0], reviewer._answerButtonList()), + 'buttons': [b[0] for b in reviewer._answerButtonList()], 'modelName': model['name'], 'deckName': self.deckNameFromId(card.did) } From ca6d513470458c2130a9182f266629298454c9c4 Mon Sep 17 00:00:00 2001 From: David Bailey Date: Fri, 18 Aug 2017 20:37:46 +0100 Subject: [PATCH 15/85] Change id variables to did / configId --- AnkiConnect.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index d16ddd4..b2f13bb 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -513,13 +513,13 @@ class AnkiBridge: if not deck in self.deckNames(): return False - id = self.collection().decks.id(deck) - return self.collection().decks.confForDid(id) + did = self.collection().decks.id(deck) + return self.collection().decks.confForDid(did) def saveConf(self, conf): - id = str(conf['id']) - if not id in self.collection().decks.dconf: + confId = str(conf['id']) + if not confId in self.collection().decks.dconf: return False mod = anki.utils.intTime() @@ -528,7 +528,7 @@ class AnkiBridge: conf['mod'] = mod conf['usn'] = usn - self.collection().decks.dconf[id] = conf + self.collection().decks.dconf[confId] = conf self.collection().decks.changed = True return True @@ -556,11 +556,11 @@ class AnkiBridge: return self.collection().decks.confId(name, cloneFrom) - def remConf(self, id): - if id == 1 or not str(id) in self.collection().decks.dconf: + def remConf(self, configId): + if configId == 1 or not str(configId) in self.collection().decks.dconf: return False - self.collection().decks.remConf(id) + self.collection().decks.remConf(configId) return True @@ -575,8 +575,8 @@ class AnkiBridge: deckNames = self.deckNames() for deck in deckNames: - id = self.collection().decks.id(deck) - decks[deck] = id + did = self.collection().decks.id(deck) + decks[deck] = did return decks @@ -637,8 +637,8 @@ class AnkiBridge: def deleteDecks(self, decks, cardsToo=False): self.startEditing() for deck in decks: - id = self.collection().decks.id(deck) - self.collection().decks.rem(id, cardsToo) + did = self.collection().decks.id(deck) + self.collection().decks.rem(did, cardsToo) self.stopEditing() @@ -860,8 +860,8 @@ class AnkiConnect: @webApi - def remConf(self, id): - return self.anki.remConf(id) + def remConf(self, configId): + return self.anki.remConf(configId) @webApi From e2c9eaaa3bd80c7c5df5a8ee5d071b8f0c9ddba0 Mon Sep 17 00:00:00 2001 From: David Bailey Date: Fri, 18 Aug 2017 20:54:32 +0100 Subject: [PATCH 16/85] Rename actions --- AnkiConnect.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index b2f13bb..d00fa23 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -509,7 +509,7 @@ class AnkiBridge: return [field['name'] for field in model['flds']] - def confForDeck(self, deck): + def getDeckConfig(self, deck): if not deck in self.deckNames(): return False @@ -517,7 +517,7 @@ class AnkiBridge: return self.collection().decks.confForDid(did) - def saveConf(self, conf): + def saveDeckConfig(self, conf): confId = str(conf['id']) if not confId in self.collection().decks.dconf: return False @@ -533,7 +533,7 @@ class AnkiBridge: return True - def changeConf(self, decks, confId): + def setDeckConfigId(self, decks, confId): for deck in decks: if not deck in self.deckNames(): return False @@ -548,7 +548,7 @@ class AnkiBridge: return True - def addConf(self, name, cloneFrom=1): + def cloneDeckConfigId(self, name, cloneFrom=1): if not str(cloneFrom) in self.collection().decks.dconf: return False @@ -556,7 +556,7 @@ class AnkiBridge: return self.collection().decks.confId(name, cloneFrom) - def remConf(self, configId): + def removeDeckConfigId(self, configId): if configId == 1 or not str(configId) in self.collection().decks.dconf: return False @@ -840,28 +840,28 @@ class AnkiConnect: @webApi - def confForDeck(self, deck): - return self.anki.confForDeck(deck) + def getDeckConfig(self, deck): + return self.anki.getDeckConfig(deck) @webApi - def saveConf(self, conf): - return self.anki.saveConf(conf) + def saveDeckConfig(self, conf): + return self.anki.saveDeckConfig(conf) @webApi - def changeConf(self, decks, confId): - return self.anki.changeConf(decks, confId) + def setDeckConfigId(self, decks, confId): + return self.anki.setDeckConfigId(decks, confId) @webApi - def addConf(self, name, cloneFrom=1): - return self.anki.addConf(name, cloneFrom) + def cloneDeckConfigId(self, name, cloneFrom=1): + return self.anki.cloneDeckConfigId(name, cloneFrom) @webApi - def remConf(self, configId): - return self.anki.remConf(configId) + def removeDeckConfigId(self, configId): + return self.anki.removeDeckConfigId(configId) @webApi From f980758207aed6bbf614fdc9518a0aabf65b9a65 Mon Sep 17 00:00:00 2001 From: David Bailey Date: Fri, 18 Aug 2017 20:57:19 +0100 Subject: [PATCH 17/85] Rename actions in README --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f36c6ed..9447453 100644 --- a/README.md +++ b/README.md @@ -209,14 +209,14 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` -* **confForDeck** +* **getDeckConfig** Gets the config group object for the given deck. *Sample request*: ``` { - "action": "confForDeck", + "action": "getDeckConfig", "params": { "deck": "Default" } @@ -263,7 +263,7 @@ Below is a list of currently supported actions. Requests with invalid actions or } ``` -* **saveConf** +* **saveDeckConfig** Saves the given config group, returning `true` on success or `false` if the ID of the config group is invalid (i.e. it does not exist). @@ -271,7 +271,7 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample request*: ``` { - "action": "saveConf", + "action": "saveDeckConfig", "params": { "conf": (config group object) } @@ -283,7 +283,7 @@ Below is a list of currently supported actions. Requests with invalid actions or true ``` -* **changeConf** +* **setDeckConfigId** Changes the configuration group for the given decks to the one with the given ID. Returns `true` on success or `false` if the given configuration group or any of the given decks do not exist. @@ -291,7 +291,7 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample request*: ``` { - "action": "changeConf", + "action": "setDeckConfigId", "params": { "decks": ["Default"], "confId": 1 @@ -304,7 +304,7 @@ Below is a list of currently supported actions. Requests with invalid actions or true ``` -* **addConf** +* **cloneDeckConfigId** Creates a new config group with the given name, cloning from the group with the given ID, or from the default group if this is unspecified. Returns the ID of the new config group, or `false` if the specified group to clone from does @@ -313,7 +313,7 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample request*: ``` { - "action": "addConf", + "action": "cloneDeckConfigId", "params": { "name": "Copy of Default", "cloneFrom": 1 @@ -326,7 +326,7 @@ Below is a list of currently supported actions. Requests with invalid actions or 1502972374573 ``` -* **remConf** +* **removeDeckConfigId** Removes the config group with the given ID, returning `true` if successful, or `false` if attempting to remove either the default config group (ID = 1) or a config group that does not exist. @@ -334,7 +334,7 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample request*: ``` { - "action": "remConf", + "action": "removeDeckConfigId", "params": { "id": 1502972374573 } From 933c7de6b977db6434a303c707895b64a3b601a2 Mon Sep 17 00:00:00 2001 From: David Bailey Date: Fri, 18 Aug 2017 21:09:28 +0100 Subject: [PATCH 18/85] confId => configId --- AnkiConnect.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index d00fa23..ef01195 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -518,8 +518,8 @@ class AnkiBridge: def saveDeckConfig(self, conf): - confId = str(conf['id']) - if not confId in self.collection().decks.dconf: + configId = str(conf['id']) + if not configId in self.collection().decks.dconf: return False mod = anki.utils.intTime() @@ -528,22 +528,22 @@ class AnkiBridge: conf['mod'] = mod conf['usn'] = usn - self.collection().decks.dconf[confId] = conf + self.collection().decks.dconf[configId] = conf self.collection().decks.changed = True return True - def setDeckConfigId(self, decks, confId): + def setDeckConfigId(self, decks, configId): for deck in decks: if not deck in self.deckNames(): return False - if not str(confId) in self.collection().decks.dconf: + if not str(configId) in self.collection().decks.dconf: return False for deck in decks: did = str(self.collection().decks.id(deck)) - aqt.mw.col.decks.decks[did]['conf'] = confId + aqt.mw.col.decks.decks[did]['conf'] = configId return True @@ -850,8 +850,8 @@ class AnkiConnect: @webApi - def setDeckConfigId(self, decks, confId): - return self.anki.setDeckConfigId(decks, confId) + def setDeckConfigId(self, decks, configId): + return self.anki.setDeckConfigId(decks, configId) @webApi From 33908a62973839ffadeae0ec3f472929770ff339 Mon Sep 17 00:00:00 2001 From: David Bailey Date: Fri, 18 Aug 2017 21:10:51 +0100 Subject: [PATCH 19/85] Update README (id => configId) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9447453..02ef413 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ Below is a list of currently supported actions. Requests with invalid actions or "action": "setDeckConfigId", "params": { "decks": ["Default"], - "confId": 1 + "configId": 1 } } ``` @@ -336,7 +336,7 @@ Below is a list of currently supported actions. Requests with invalid actions or { "action": "removeDeckConfigId", "params": { - "id": 1502972374573 + "configId": 1502972374573 } } ``` From b6ab724dd718cda0d6e7122090cda30d2233ddec Mon Sep 17 00:00:00 2001 From: David Bailey Date: Fri, 18 Aug 2017 21:16:50 +0100 Subject: [PATCH 20/85] conf => config in saveDeckConfig --- AnkiConnect.py | 14 +++++++------- README.md | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index ef01195..1d6d93e 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -517,18 +517,18 @@ class AnkiBridge: return self.collection().decks.confForDid(did) - def saveDeckConfig(self, conf): - configId = str(conf['id']) + def saveDeckConfig(self, config): + configId = str(config['id']) if not configId in self.collection().decks.dconf: return False mod = anki.utils.intTime() usn = self.collection().usn() - conf['mod'] = mod - conf['usn'] = usn + config['mod'] = mod + config['usn'] = usn - self.collection().decks.dconf[configId] = conf + self.collection().decks.dconf[configId] = config self.collection().decks.changed = True return True @@ -845,8 +845,8 @@ class AnkiConnect: @webApi - def saveDeckConfig(self, conf): - return self.anki.saveDeckConfig(conf) + def saveDeckConfig(self, config): + return self.anki.saveDeckConfig(config) @webApi diff --git a/README.md b/README.md index 02ef413..1f35bcc 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ Below is a list of currently supported actions. Requests with invalid actions or { "action": "saveDeckConfig", "params": { - "conf": (config group object) + "config": (config group object) } } ``` From bca361d79ada38c29af15313462c631b3824d317 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Sat, 19 Aug 2017 11:38:55 -0700 Subject: [PATCH 21/85] Updating README.md --- README.md | 216 ++---------------------------------------------------- 1 file changed, 6 insertions(+), 210 deletions(-) diff --git a/README.md b/README.md index d2493c3..980f4fe 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Below is a list of currently supported actions. Requests with invalid actions or Gets the version of the API exposed by this plugin. Currently versions `1` through `4` are defined. This should be the first call you make to make sure that your application and AnkiConnect are able to communicate - properly with each other. New versions of AnkiConnect are backwards compatible; as long as you are using actions + properly with each other. New versions of AnkiConnect will backwards compatible; as long as you are using actions which are available in the reported AnkiConnect version or earlier, everything should work fine. *Sample request*: @@ -121,24 +121,6 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` -* **deckNamesAndIds** - - Gets the complete list of deck names and their respective IDs for the current user. - - *Sample request*: - ``` - { - "action": "deckNamesAndIds" - } - ``` - - *Sample response*: - ``` - { - "Default": 1 - } - ``` - * **modelNames** Gets the complete list of model names for the current user. @@ -365,8 +347,7 @@ Below is a list of currently supported actions. Requests with invalid actions or * **suspend** - Suspend cards by card ID; returns `true` if successful (at least one card wasn't already suspended) or `false` - otherwise. + Suspend cards by card ID. *Sample request*: ``` @@ -380,13 +361,12 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample response*: ``` - true + null ``` * **unsuspend** - Unsuspend cards by card ID; returns `true` if successful (at least one card was previously suspended) or `false` - otherwise. + Unsuspend cards by card ID. *Sample request*: ``` @@ -400,89 +380,9 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample response*: ``` - true + null ``` -* **areSuspended** - - Returns an array indicating whether each of the given cards is suspended (in the same order). - - *Sample request*: - ``` - { - "action": "areSuspended", - "params": { - "cards": [1483959291685, 1483959293217] - } - } - ``` - - *Sample response*: - ``` - [false, true] - ``` - -* **areDue** - - Returns an array indicating whether each of the given cards is due (in the same order). Note: cards in the learning - queue with a large interval (over 20 minutes) are treated as not due until the time of their interval has passed, to - match the way Anki treats them when reviewing. - - *Sample request*: - ``` - { - "action": "areDue", - "params": { - "cards": [1483959291685, 1483959293217] - } - } - ``` - - *Sample response*: - ``` - [false, true] - ``` - -* **getIntervals** - - Returns an array of the most recent intervals for each given card ID, or a 2-dimensional array of all the intervals - for each given card ID when `complete` is `true`. (Negative intervals are in seconds and positive intervals in days.) - - *Sample request 1*: - ``` - { - "action": "getIntervals", - "params": { - "cards": [1502298033753, 1502298036657] - } - } - ``` - - *Sample response 1*: - ``` - [-14400, 3] - ``` - - *Sample request 2*: - ``` - { - "action": "getIntervals", - "params": { - "cards": [1502298033753, 1502298036657], - "complete": true - } - } - ``` - - *Sample response 2*: - ``` - [ - [-120, -180, -240, -300, -360, -14400], - [-120, -180, -240, -300, -360, -14400, 1, 3] - ] - ``` - - * **findNotes** Returns an array of note IDs for a given query (same query syntax as **guiBrowse**). @@ -490,7 +390,7 @@ Below is a list of currently supported actions. Requests with invalid actions or *Sample request*: ``` { - "action": "findNotes", + "action": "findCards", "params": { "query": "deck:current" } @@ -529,94 +429,6 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` -* **getDecks** - - Accepts an array of card IDs and returns an object with each deck name as a key, and its value an array of the given - cards which belong to it. - - *Sample request*: - ``` - { - "action": "getDecks", - "params": { - "cards": [1502298036657, 1502298033753, 1502032366472] - } - } - ``` - - *Sample response*: - ``` - { - "Default": [1502032366472], - "Japanese::JLPT N3": [1502298036657, 1502298033753] - } - ``` - - -* **changeDeck** - - Moves cards with the given IDs to a different deck, creating the deck if it doesn't exist yet. - - *Sample request*: - ``` - { - "action": "changeDeck", - "params": { - "cards": [1502098034045, 1502098034048, 1502298033753], - "deck": "Japanese::JLPT N3" - } - } - ``` - - *Sample response*: - ``` - null - ``` - -* **deleteDecks** - - Deletes decks with the given names. If `cardsToo` is `true` (defaults to `false` if unspecified), the cards within - the deleted decks will also be deleted; otherwise they will be moved to the default deck. - - *Sample request*: - ``` - { - "action": "deleteDecks", - "params": { - "decks": ["Japanese::JLPT N5", "Easy Spanish"], - "cardsToo": true - } - } - ``` - - *Sample response*: - ``` - null - ``` - -* **cardsToNotes** - - Returns an (unordered) array of note IDs for the given card IDs. For cards with the same note, the ID is only - given once in the array. - - *Sample request*: - ``` - { - "action": "cardsToNotes", - "params": { - "cards": [1502098034045, 1502098034048, 1502298033753] - } - } - ``` - - *Sample response*: - ``` - [ - 1502098029797, - 1502298025183 - ] - ``` - * **guiBrowse** Invokes the card browser and searches for a given query. Returns an array of identifiers of the cards that were found. @@ -690,22 +502,6 @@ Below is a list of currently supported actions. Requests with invalid actions or } ``` -* **guiStartCardTimer** - - Starts or resets the 'timerStarted' value for the current card. This is useful for deferring the start time to when it is displayed via the API, allowing the recorded time taken to answer the card to be more accurate when calling guiAnswerCard. - - *Sample request*: - ``` - { - "action": "guiStartCardTimer" - } - ``` - - *Sample response*: - ``` - true - ``` - * **guiShowQuestion** Shows question text for the current card; returns `true` if in review mode or `false` otherwise. From 658763dd5baf5088fa6635083a7377e70d4b6ab2 Mon Sep 17 00:00:00 2001 From: David Bailey Date: Sat, 19 Aug 2017 23:24:53 +0100 Subject: [PATCH 22/85] Allow other add-ons to add headers --- AnkiConnect.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index 9fbe5a0..a4acd20 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -199,6 +199,19 @@ class AjaxServer: self.handler = handler self.clients = [] self.sock = None + self.resetHeaders() + + + def addHeader(self, name, value): + self.headers.append([name, value]) + + + def resetHeaders(self): + self.headers = [ + ['HTTP/1.1 200 OK', None], + ['Content-Type', 'text/json'], + ['Content-Length', ''] + ] def advance(self): @@ -243,11 +256,9 @@ class AjaxServer: body = json.dumps(None); resp = bytes() - headers = [ - ['HTTP/1.1 200 OK', None], - ['Content-Type', 'text/json'], - ['Content-Length', str(len(body))] - ] + + headers = self.headers + headers[2][1] = str(len(body)) for key, value in headers: if value is None: From 8179643067745763563f819cb615bffb73cdcf82 Mon Sep 17 00:00:00 2001 From: David Bailey Date: Sun, 20 Aug 2017 20:32:16 +0100 Subject: [PATCH 23/85] Improve handling of HTTP headers --- AnkiConnect.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index a4acd20..ac1cfff 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -202,16 +202,22 @@ class AjaxServer: self.resetHeaders() - def addHeader(self, name, value): - self.headers.append([name, value]) - - + def setHeader(self, name, value): + self.headers[name] = value + + def resetHeaders(self): - self.headers = [ - ['HTTP/1.1 200 OK', None], - ['Content-Type', 'text/json'], - ['Content-Length', ''] - ] + self.headers = { + 'HTTP/1.1 200 OK': None, + 'Content-Type': 'text/json' + } + + + def getHeaders(self): + headers = [] + for name in self.headers: + headers.append([name, self.headers[name]]) + return headers def advance(self): @@ -256,9 +262,9 @@ class AjaxServer: body = json.dumps(None); resp = bytes() - - headers = self.headers - headers[2][1] = str(len(body)) + + self.setHeader('Content-Length', str(len(body))) + headers = self.getHeaders() for key, value in headers: if value is None: From d86722c3cb0ce1599785d4ab19a33443dac4d0ee Mon Sep 17 00:00:00 2001 From: David Bailey Date: Sun, 20 Aug 2017 22:29:52 +0100 Subject: [PATCH 24/85] Separate standard headers from extra headers --- AnkiConnect.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index ac1cfff..c8b3c2b 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -203,19 +203,20 @@ class AjaxServer: def setHeader(self, name, value): - self.headers[name] = value + self.extraHeaders[name] = value def resetHeaders(self): - self.headers = { - 'HTTP/1.1 200 OK': None, - 'Content-Type': 'text/json' - } + self.headers = [ + ['HTTP/1.1 200 OK', None], + ['Content-Type', 'text/json'] + ] + self.extraHeaders = {} def getHeaders(self): - headers = [] - for name in self.headers: + headers = self.headers + for name in self.extraHeaders: headers.append([name, self.headers[name]]) return headers From 0cc83ab161cf243507b46ee0b55dee800dceddcd Mon Sep 17 00:00:00 2001 From: David Bailey Date: Sun, 20 Aug 2017 22:39:50 +0100 Subject: [PATCH 25/85] Bugfix --- AnkiConnect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index c8b3c2b..8dececb 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -215,9 +215,9 @@ class AjaxServer: def getHeaders(self): - headers = self.headers + headers = self.headers[:] for name in self.extraHeaders: - headers.append([name, self.headers[name]]) + headers.append([name, self.extraHeaders[name]]) return headers From 75a3a25e91df16cfb3408304adbae43aad59b619 Mon Sep 17 00:00:00 2001 From: David Bailey Date: Mon, 21 Aug 2017 15:10:57 +0100 Subject: [PATCH 26/85] Add modelFieldsOnTemplates action --- AnkiConnect.py | 37 +++++++++++++++++++++++++++++++++++++ README.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/AnkiConnect.py b/AnkiConnect.py index 8dececb..391e6ce 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -21,6 +21,7 @@ import hashlib import inspect import json import os.path +import re import select import socket import sys @@ -527,6 +528,37 @@ class AnkiBridge: return [field['name'] for field in model['flds']] + def modelFieldsOnTemplates(self, modelName): + model = self.collection().models.byName(modelName) + + if model is not None: + templates = {} + for template in model['tmpls']: + fields = [] + + for side in ['qfmt', 'afmt']: + fieldsForSide = [] + + # based on _fieldsOnTemplate from aqt/clayout.py + matches = re.findall('{{[^#/}]+?}}', template[side]) + for match in matches: + # remove braces and modifiers + match = re.sub(r'[{}]', '', match) + match = match.split(":")[-1] + + # for the answer side, ignore fields present on the question side + the FrontSide field + if match == 'FrontSide' or side == 'afmt' and match in fields[0]: + continue + fieldsForSide.append(match) + + + fields.append(fieldsForSide) + + templates[template['name']] = fields + + return templates + + def getDeckConfig(self, deck): if not deck in self.deckNames(): return False @@ -857,6 +889,11 @@ class AnkiConnect: return self.anki.modelFieldNames(modelName) + @webApi + def modelFieldsOnTemplates(self, modelName): + return self.anki.modelFieldsOnTemplates(modelName) + + @webApi def getDeckConfig(self, deck): return self.anki.getDeckConfig(deck) diff --git a/README.md b/README.md index 1f35bcc..8d2c1fc 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,35 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` +* **modelFieldsOnTemplates** + + Returns an object indicating the fields on the question and answer side of each card template for the given model + name. The question side is given first in each array. + + *Sample request*: + ``` + { + "action": "modelFieldsOnTemplates", + "params": { + "modelName": "Basic (and reversed card)" + } + } + ``` + + *Sample response*: + ``` + { + "Card 1": [ + ["Front"], + ["Back"] + ], + "Card 2": [ + ["Back"], + ["Front"] + ] + } + ``` + * **getDeckConfig** Gets the config group object for the given deck. From 2e09db9dab2242f69c1979fdd5f19e9c651ba869 Mon Sep 17 00:00:00 2001 From: David Bailey Date: Mon, 21 Aug 2017 17:15:11 +0100 Subject: [PATCH 27/85] Add modelNamesAndIds action --- AnkiConnect.py | 17 +++++++++++++++++ README.md | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/AnkiConnect.py b/AnkiConnect.py index 391e6ce..036e935 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -512,6 +512,18 @@ class AnkiBridge: return collection.models.allNames() + def modelNamesAndIds(self): + models = {} + + modelNames = self.modelNames() + for model in modelNames: + mid = self.collection().models.byName(model)['id'] + mid = int(mid) # sometimes Anki stores the ID as a string + models[model] = mid + + return models + + def modelNameFromId(self, modelId): collection = self.collection() if collection is not None: @@ -884,6 +896,11 @@ class AnkiConnect: return self.anki.modelNames() + @webApi + def modelNamesAndIds(self): + return self.anki.modelNamesAndIds() + + @webApi def modelFieldNames(self, modelName): return self.anki.modelFieldNames(modelName) diff --git a/README.md b/README.md index 8d2c1fc..05320b5 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,27 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` +* **modelNamesAndIds** + + Gets the complete list of model names and their corresponding IDs for the current user. + + *Sample request*: + ``` + { + "action": "modelNamesAndIds" + } + ``` + + *Sample response*: + ``` + { + "Basic": 1483883011648 + "Basic (and reversed card)": 1483883011644 + "Basic (optional reversed card)": 1483883011631 + "Cloze": 1483883011630 + } + ``` + * **modelFieldNames** Gets the complete list of field names for the provided model name. From d64643baa6b00783275765a0789b8e60480090af Mon Sep 17 00:00:00 2001 From: David Bailey Date: Tue, 22 Aug 2017 08:53:35 +0100 Subject: [PATCH 28/85] Add media file actions --- AnkiConnect.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/AnkiConnect.py b/AnkiConnect.py index 8dececb..13c6a97 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -17,6 +17,7 @@ import anki import aqt +import base64 import hashlib import inspect import json @@ -334,6 +335,45 @@ class AnkiNoteParams: # class AnkiBridge: + def getFilePath(self, filename): + mediaFolder = self.collection().media.dir() + filePath = os.path.normpath(os.path.join(mediaFolder, filename)) + # catch attempts to write outside the media folder + if os.path.commonprefix([mediaFolder, filePath]) != mediaFolder: + return False + + return filePath + + + def storeFile(self, filename, data): + filePath = self.getFilePath(filename) + if filePath: + with open(filePath, 'wb') as file: + file.write(base64.b64decode(data)) + return True + + return False + + + def retrieveFile(self, filename): + filePath = self.getFilePath(filename) + if filePath and os.path.isfile(filePath): + with open(filePath, 'rb') as file: + data = base64.b64encode(file.read()) + return data.decode('ascii') + + return False + + + def deleteFile(self, filename): + filePath = self.getFilePath(filename) + if filePath and os.path.isfile(filePath): + os.remove(filePath) + return True + + return False + + def addNote(self, params): collection = self.collection() if collection is None: @@ -837,6 +877,21 @@ class AnkiConnect: return self.anki.multi(actions) + @webApi + def storeFile(self, filename, data): + return self.anki.storeFile(filename, data) + + + @webApi + def retrieveFile(self, filename): + return self.anki.retrieveFile(filename) + + + @webApi + def deleteFile(self, filename): + return self.anki.deleteFile(filename) + + @webApi def deckNames(self): return self.anki.deckNames() From c689e8276bca3de1dbacfc9fce880cc391152c2e Mon Sep 17 00:00:00 2001 From: David Bailey Date: Tue, 22 Aug 2017 09:12:49 +0100 Subject: [PATCH 29/85] Document media actions --- README.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/README.md b/README.md index 1f35bcc..8c3d94a 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,75 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` +* **storeFile** + + Stores a file with the specified base64-encoded contents inside the media folder. Returns `true` upon success or + `false` if attempting to write a file outside the media folder. + + Note: to prevent Anki from removing files not used by any cards (e.g. for configuration files), prefix the filename + with an underscore. These files are still synchronized to AnkiWeb. + + *Sample request*: + ``` + { + "action": "storeFile", + "params": { + "filename": "_hello.txt", + "data": "SGVsbG8sIHdvcmxkIQ==" + } + } + ``` + + *Sample response*: + ``` + true + ``` + + *Content of `_hello.txt`*: + ``` + Hello world! + ``` + +* **retrieveFile** + + Retrieves the base64-encoded contents of the specified file, returning `false` if the file does not exist or if + attempting to read a file outside the media folder. + + *Sample request*: + ``` + { + "action": "retrieveFile", + "params": { + "filename": "_hello.txt" + } + } + ``` + + *Sample response*: + ``` + "SGVsbG8sIHdvcmxkIQ==" + ``` + +* **deleteFile** + + Deletes the specified file inside the media folder, returning `true` if successful, or `false` if the file does not + exist or if attempting to delete a file outside the media folder. + + *Sample request*: + ``` + { + "action": "deleteFile", + "params": { + "filename": "_hello.txt" + } + } + ``` + + *Sample response*: + ``` + true + ``` + * **deckNames** Gets the complete list of deck names for the current user. From a95c56a0615298e02da5d1c55350fb8eba67c7fc Mon Sep 17 00:00:00 2001 From: David Bailey Date: Tue, 22 Aug 2017 09:43:37 +0100 Subject: [PATCH 30/85] Organize README into categories --- README.md | 390 +++++++++++++++++++++++++++++------------------------- 1 file changed, 212 insertions(+), 178 deletions(-) diff --git a/README.md b/README.md index 8c3d94a..0836655 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,22 @@ curl localhost:8765 -X POST -d '{"action": "version"}' Below is a list of currently supported actions. Requests with invalid actions or parameters will a return `null` result. +Categories: + +* [Miscellaneous](#miscellaneous) +* [Decks](#decks) +* [Deck Configurations](#deck-configurations) +* [Models](#models) +* [Note Creation](#note-creation) +* [Note Tags](#note-tags) +* [Card Suspension](#card-suspension) +* [Card Intervals](#card-intervals) +* [Finding Notes and Cards](#finding-notes-and-cards) +* [File Storage](#file-storage) +* [Graphical](#graphical) + +### Miscellaneous ### + * **version** Gets the version of the API exposed by this plugin. Currently versions `1` through `4` are defined. @@ -104,6 +120,24 @@ Below is a list of currently supported actions. Requests with invalid actions or 4 ``` +* **upgrade** + + Displays a confirmation dialog box in Anki asking the user if they wish to upgrade AnkiConnect to the latest version + from the project's [master branch](https://raw.githubusercontent.com/FooSoft/anki-connect/master/AnkiConnect.py) on + GitHub. Returns a boolean value indicating if the plugin was upgraded or not. + + *Sample request*: + ``` + { + "action": "upgrade" + } + ``` + + *Sample response*: + ``` + true + ``` + * **multi** Performs multiple actions in one request, returning an array with the response of each action (in the given order). @@ -132,74 +166,7 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` -* **storeFile** - - Stores a file with the specified base64-encoded contents inside the media folder. Returns `true` upon success or - `false` if attempting to write a file outside the media folder. - - Note: to prevent Anki from removing files not used by any cards (e.g. for configuration files), prefix the filename - with an underscore. These files are still synchronized to AnkiWeb. - - *Sample request*: - ``` - { - "action": "storeFile", - "params": { - "filename": "_hello.txt", - "data": "SGVsbG8sIHdvcmxkIQ==" - } - } - ``` - - *Sample response*: - ``` - true - ``` - - *Content of `_hello.txt`*: - ``` - Hello world! - ``` - -* **retrieveFile** - - Retrieves the base64-encoded contents of the specified file, returning `false` if the file does not exist or if - attempting to read a file outside the media folder. - - *Sample request*: - ``` - { - "action": "retrieveFile", - "params": { - "filename": "_hello.txt" - } - } - ``` - - *Sample response*: - ``` - "SGVsbG8sIHdvcmxkIQ==" - ``` - -* **deleteFile** - - Deletes the specified file inside the media folder, returning `true` if successful, or `false` if the file does not - exist or if attempting to delete a file outside the media folder. - - *Sample request*: - ``` - { - "action": "deleteFile", - "params": { - "filename": "_hello.txt" - } - } - ``` - - *Sample response*: - ``` - true - ``` +### Decks ### * **deckNames** @@ -237,47 +204,72 @@ Below is a list of currently supported actions. Requests with invalid actions or } ``` -* **modelNames** +* **getDecks** - Gets the complete list of model names for the current user. + Accepts an array of card IDs and returns an object with each deck name as a key, and its value an array of the given + cards which belong to it. *Sample request*: ``` { - "action": "modelNames" - } - ``` - - *Sample response*: - ``` - [ - "Basic", - "Basic (and reversed card)" - ] - ``` - -* **modelFieldNames** - - Gets the complete list of field names for the provided model name. - - *Sample request*: - ``` - { - "action": "modelFieldNames", + "action": "getDecks", "params": { - "modelName": "Basic" + "cards": [1502298036657, 1502298033753, 1502032366472] } } ``` *Sample response*: ``` - [ - "Front", - "Back" - ] + { + "Default": [1502032366472], + "Japanese::JLPT N3": [1502298036657, 1502298033753] + } ``` +* **changeDeck** + + Moves cards with the given IDs to a different deck, creating the deck if it doesn't exist yet. + + *Sample request*: + ``` + { + "action": "changeDeck", + "params": { + "cards": [1502098034045, 1502098034048, 1502298033753], + "deck": "Japanese::JLPT N3" + } + } + ``` + + *Sample response*: + ``` + null + ``` + +* **deleteDecks** + + Deletes decks with the given names. If `cardsToo` is `true` (defaults to `false` if unspecified), the cards within + the deleted decks will also be deleted; otherwise they will be moved to the default deck. + + *Sample request*: + ``` + { + "action": "deleteDecks", + "params": { + "decks": ["Japanese::JLPT N5", "Easy Spanish"], + "cardsToo": true + } + } + ``` + + *Sample response*: + ``` + null + ``` + +### Deck Configurations ### + * **getDeckConfig** Gets the config group object for the given deck. @@ -415,6 +407,51 @@ Below is a list of currently supported actions. Requests with invalid actions or true ``` +### Models ### + +* **modelNames** + + Gets the complete list of model names for the current user. + + *Sample request*: + ``` + { + "action": "modelNames" + } + ``` + + *Sample response*: + ``` + [ + "Basic", + "Basic (and reversed card)" + ] + ``` + +* **modelFieldNames** + + Gets the complete list of field names for the provided model name. + + *Sample request*: + ``` + { + "action": "modelFieldNames", + "params": { + "modelName": "Basic" + } + } + ``` + + *Sample response*: + ``` + [ + "Front", + "Back" + ] + ``` + +### Note Creation ### + * **addNote** Creates a note using the given deck and model, with the provided field values and tags. Returns the identifier of @@ -530,6 +567,8 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` +### Note Tags ### + * **addTags** Adds tags to notes by note ID. @@ -570,6 +609,8 @@ Below is a list of currently supported actions. Requests with invalid actions or null ``` +### Card Suspension ### + * **suspend** Suspend cards by card ID; returns `true` if successful (at least one card wasn't already suspended) or `false` @@ -629,6 +670,8 @@ Below is a list of currently supported actions. Requests with invalid actions or [false, true] ``` +### Card Intervals ### + * **areDue** Returns an array indicating whether each of the given cards is due (in the same order). Note: cards in the learning @@ -689,6 +732,7 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` +### Finding Notes and Cards ### * **findNotes** @@ -736,71 +780,6 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` -* **getDecks** - - Accepts an array of card IDs and returns an object with each deck name as a key, and its value an array of the given - cards which belong to it. - - *Sample request*: - ``` - { - "action": "getDecks", - "params": { - "cards": [1502298036657, 1502298033753, 1502032366472] - } - } - ``` - - *Sample response*: - ``` - { - "Default": [1502032366472], - "Japanese::JLPT N3": [1502298036657, 1502298033753] - } - ``` - - -* **changeDeck** - - Moves cards with the given IDs to a different deck, creating the deck if it doesn't exist yet. - - *Sample request*: - ``` - { - "action": "changeDeck", - "params": { - "cards": [1502098034045, 1502098034048, 1502298033753], - "deck": "Japanese::JLPT N3" - } - } - ``` - - *Sample response*: - ``` - null - ``` - -* **deleteDecks** - - Deletes decks with the given names. If `cardsToo` is `true` (defaults to `false` if unspecified), the cards within - the deleted decks will also be deleted; otherwise they will be moved to the default deck. - - *Sample request*: - ``` - { - "action": "deleteDecks", - "params": { - "decks": ["Japanese::JLPT N5", "Easy Spanish"], - "cardsToo": true - } - } - ``` - - *Sample response*: - ``` - null - ``` - * **cardsToNotes** Returns an (unordered) array of note IDs for the given card IDs. For cards with the same note, the ID is only @@ -824,6 +803,79 @@ Below is a list of currently supported actions. Requests with invalid actions or ] ``` +### File Storage ### + +* **storeFile** + + Stores a file with the specified base64-encoded contents inside the media folder. Returns `true` upon success or + `false` if attempting to write a file outside the media folder. + + Note: to prevent Anki from removing files not used by any cards (e.g. for configuration files), prefix the filename + with an underscore. These files are still synchronized to AnkiWeb. + + *Sample request*: + ``` + { + "action": "storeFile", + "params": { + "filename": "_hello.txt", + "data": "SGVsbG8sIHdvcmxkIQ==" + } + } + ``` + + *Sample response*: + ``` + true + ``` + + *Content of `_hello.txt`*: + ``` + Hello world! + ``` + +* **retrieveFile** + + Retrieves the base64-encoded contents of the specified file, returning `false` if the file does not exist or if + attempting to read a file outside the media folder. + + *Sample request*: + ``` + { + "action": "retrieveFile", + "params": { + "filename": "_hello.txt" + } + } + ``` + + *Sample response*: + ``` + "SGVsbG8sIHdvcmxkIQ==" + ``` + +* **deleteFile** + + Deletes the specified file inside the media folder, returning `true` if successful, or `false` if the file does not + exist or if attempting to delete a file outside the media folder. + + *Sample request*: + ``` + { + "action": "deleteFile", + "params": { + "filename": "_hello.txt" + } + } + ``` + + *Sample response*: + ``` + true + ``` + +### Graphical ### + * **guiBrowse** Invokes the card browser and searches for a given query. Returns an array of identifiers of the cards that were found. @@ -1019,24 +1071,6 @@ Below is a list of currently supported actions. Requests with invalid actions or true ``` -* **upgrade** - - Displays a confirmation dialog box in Anki asking the user if they wish to upgrade AnkiConnect to the latest version - from the project's [master branch](https://raw.githubusercontent.com/FooSoft/anki-connect/master/AnkiConnect.py) on - GitHub. Returns a boolean value indicating if the plugin was upgraded or not. - - *Sample request*: - ``` - { - "action": "upgrade" - } - ``` - - *Sample response*: - ``` - true - ``` - ## License ## This program is free software: you can redistribute it and/or modify From 56e8023d5f443063c469b54142ba5dfebe663963 Mon Sep 17 00:00:00 2001 From: David Bailey Date: Tue, 22 Aug 2017 09:51:01 +0100 Subject: [PATCH 31/85] Update with model actions --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/README.md b/README.md index 0836655..754c32a 100644 --- a/README.md +++ b/README.md @@ -428,6 +428,27 @@ Categories: ] ``` +* **modelNamesAndIds** + + Gets the complete list of model names and their corresponding IDs for the current user. + + *Sample request*: + ``` + { + "action": "modelNamesAndIds" + } + ``` + + *Sample response*: + ``` + { + "Basic": 1483883011648 + "Basic (and reversed card)": 1483883011644 + "Basic (optional reversed card)": 1483883011631 + "Cloze": 1483883011630 + } + ``` + * **modelFieldNames** Gets the complete list of field names for the provided model name. @@ -450,6 +471,35 @@ Categories: ] ``` +* **modelFieldsOnTemplates** + + Returns an object indicating the fields on the question and answer side of each card template for the given model + name. The question side is given first in each array. + + *Sample request*: + ``` + { + "action": "modelFieldsOnTemplates", + "params": { + "modelName": "Basic (and reversed card)" + } + } + ``` + + *Sample response*: + ``` + { + "Card 1": [ + ["Front"], + ["Back"] + ], + "Card 2": [ + ["Back"], + ["Front"] + ] + } + ``` + ### Note Creation ### * **addNote** From 04cf33a1d0a76891c9c7691251360b5c7cd0dfeb Mon Sep 17 00:00:00 2001 From: David Bailey Date: Tue, 22 Aug 2017 20:05:12 +0100 Subject: [PATCH 32/85] Update with requested changes --- AnkiConnect.py | 59 +++++++++++++++++++------------------------------- README.md | 29 +++++++++++-------------- 2 files changed, 35 insertions(+), 53 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index 40f29b3..b5f2774 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -27,6 +27,7 @@ import select import socket import sys from time import time +from unicodedata import normalize # @@ -336,43 +337,27 @@ class AnkiNoteParams: # class AnkiBridge: - def getFilePath(self, filename): - mediaFolder = self.collection().media.dir() - filePath = os.path.normpath(os.path.join(mediaFolder, filename)) - # catch attempts to write outside the media folder - if os.path.commonprefix([mediaFolder, filePath]) != mediaFolder: - return False - - return filePath + def storeMediaFile(self, filename, data): + self.deleteMediaFile(filename) + self.media().writeData(filename, base64.b64decode(data)) - def storeFile(self, filename, data): - filePath = self.getFilePath(filename) - if filePath: - with open(filePath, 'wb') as file: - file.write(base64.b64decode(data)) - return True + def retrieveMediaFile(self, filename): + # based on writeData from anki/media.py + filename = os.path.basename(filename) + filename = normalize("NFC", filename) + filename = self.media().stripIllegal(filename) + + path = os.path.join(self.media().dir(), filename) + if os.path.exists(path): + with open(path, 'rb') as file: + return base64.b64encode(file.read()).decode('ascii') return False - def retrieveFile(self, filename): - filePath = self.getFilePath(filename) - if filePath and os.path.isfile(filePath): - with open(filePath, 'rb') as file: - data = base64.b64encode(file.read()) - return data.decode('ascii') - - return False - - - def deleteFile(self, filename): - filePath = self.getFilePath(filename) - if filePath and os.path.isfile(filePath): - os.remove(filePath) - return True - - return False + def deleteMediaFile(self, filename): + self.media().syncDelete(filename) def addNote(self, params): @@ -922,18 +907,18 @@ class AnkiConnect: @webApi - def storeFile(self, filename, data): - return self.anki.storeFile(filename, data) + def storeMediaFile(self, filename, data): + return self.anki.storeMediaFile(filename, data) @webApi - def retrieveFile(self, filename): - return self.anki.retrieveFile(filename) + def retrieveMediaFile(self, filename): + return self.anki.retrieveMediaFile(filename) @webApi - def deleteFile(self, filename): - return self.anki.deleteFile(filename) + def deleteMediaFile(self, filename): + return self.anki.deleteMediaFile(filename) @webApi diff --git a/README.md b/README.md index 754c32a..219a110 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Categories: * [Card Suspension](#card-suspension) * [Card Intervals](#card-intervals) * [Finding Notes and Cards](#finding-notes-and-cards) -* [File Storage](#file-storage) +* [Media File Storage](#media-file-storage) * [Graphical](#graphical) ### Miscellaneous ### @@ -853,12 +853,11 @@ Categories: ] ``` -### File Storage ### +### Media File Storage ### -* **storeFile** +* **storeMediaFile** - Stores a file with the specified base64-encoded contents inside the media folder. Returns `true` upon success or - `false` if attempting to write a file outside the media folder. + Stores a file with the specified base64-encoded contents inside the media folder. Note: to prevent Anki from removing files not used by any cards (e.g. for configuration files), prefix the filename with an underscore. These files are still synchronized to AnkiWeb. @@ -866,7 +865,7 @@ Categories: *Sample request*: ``` { - "action": "storeFile", + "action": "storeMediaFile", "params": { "filename": "_hello.txt", "data": "SGVsbG8sIHdvcmxkIQ==" @@ -876,7 +875,7 @@ Categories: *Sample response*: ``` - true + null ``` *Content of `_hello.txt`*: @@ -884,15 +883,14 @@ Categories: Hello world! ``` -* **retrieveFile** +* **retrieveMediaFile** - Retrieves the base64-encoded contents of the specified file, returning `false` if the file does not exist or if - attempting to read a file outside the media folder. + Retrieves the base64-encoded contents of the specified file, returning `false` if the file does not exist. *Sample request*: ``` { - "action": "retrieveFile", + "action": "retrieveMediaFile", "params": { "filename": "_hello.txt" } @@ -904,15 +902,14 @@ Categories: "SGVsbG8sIHdvcmxkIQ==" ``` -* **deleteFile** +* **deleteMediaFile** - Deletes the specified file inside the media folder, returning `true` if successful, or `false` if the file does not - exist or if attempting to delete a file outside the media folder. + Deletes the specified file inside the media folder. *Sample request*: ``` { - "action": "deleteFile", + "action": "deleteMediaFile", "params": { "filename": "_hello.txt" } @@ -921,7 +918,7 @@ Categories: *Sample response*: ``` - true + null ``` ### Graphical ### From 074177e90be5ad674924fdfe8a558199577d89db Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Sun, 27 Aug 2017 15:32:49 -0700 Subject: [PATCH 33/85] add decorator for versioning --- AnkiConnect.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/AnkiConnect.py b/AnkiConnect.py index b5f2774..81b7475 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -67,6 +67,16 @@ else: # Helpers # +def webapi(*versions): + def decorator(func): + def method(*args, **kwargs): + return func(*args, **kwargs) + setattr(method, 'versions', versions) + setattr(method, 'api', True) + return method + return decorator + + def webApi(func): func.webApi = True return func @@ -901,6 +911,24 @@ class AnkiConnect: return handler(**params) + def invoke(self, version, name, *args, **kwargs): + for method_name, method_body in inspect.getmembers(self, predicate=inspect.ismethod): + api_version_last = 0 + api_name_last = None + + if getattr(method_body, 'api', False): + for api_version, api_name in getattr(method_body, 'versions', []): + if api_version_last < api_version <= version: + api_version_last = api_version + api_name_last = api_name + + if api_name_last is None and api_version_last == 0: + api_name_last = method_name + + if api_name_last is not None and api_name_last == name: + method_body(*args, **kwargs) + + @webApi def multi(self, actions): return self.anki.multi(actions) From b2f4d54ac2afba3ba1519c85421850819154ae29 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Mon, 28 Aug 2017 17:15:42 -0300 Subject: [PATCH 34/85] Allowing to bind on other IP Addresses through an environment variable. Writing the corresponding documentation. --- AnkiConnect.py | 3 ++- README.md | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index b5f2774..3a05908 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -21,6 +21,7 @@ import base64 import hashlib import inspect import json +import os import os.path import re import select @@ -38,7 +39,7 @@ API_VERSION = 4 TICK_INTERVAL = 25 URL_TIMEOUT = 10 URL_UPGRADE = 'https://raw.githubusercontent.com/FooSoft/anki-connect/master/AnkiConnect.py' -NET_ADDRESS = '127.0.0.1' +NET_ADDRESS = os.getenv('ANKICONNECT_BIND_ADDRESS', '127.0.0.1') NET_BACKLOG = 5 NET_PORT = 8765 diff --git a/README.md b/README.md index 219a110..b5c0f8e 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ AnkiConnect exposes Anki features to external applications via an easy to use initialize a minimal HTTP sever running on port 8765 every time Anki executes. Other applications (including browser extensions) can then communicate with it via HTTP POST requests. +By default, AnkiConnect will only bind the HTTP server to the `127.0.0.1.` IP Address, so you will only be able to access it from the same host on which it is running. If you need to access it over a network, you can set the environment variable `ANKICONNECT_BIND_ADDRESS` to change the binding address. For example, you can set it to `0.0.0.0` to bind it to all network interfaces on your host. + ### Sample Invocation ### Every request consists of a JSON-encoded object containing an *action*, and a set of contextual *parameters*. A simple From 4787641c7bd0dc4340012621d2fff38b3ba5d50b Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Mon, 28 Aug 2017 17:49:58 -0300 Subject: [PATCH 35/85] Making the incoming headers case insensitive. Any reference to incoming headers should now be made with lowercase. --- AnkiConnect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index b5f2774..4d151c3 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -180,10 +180,10 @@ class AjaxClient: headers = {} for line in parts[0].split(makeBytes('\r\n')): pair = line.split(makeBytes(': ')) - headers[pair[0]] = pair[1] if len(pair) > 1 else None + headers[pair[0].lower()] = pair[1] if len(pair) > 1 else None headerLength = len(parts[0]) + 4 - bodyLength = int(headers.get(makeBytes('Content-Length'), 0)) + bodyLength = int(headers.get(makeBytes('content-length'), 0)) totalLength = headerLength + bodyLength if totalLength > len(data): From a35313922d347794c5111fc7dd7b874c4c0f836b Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Mon, 28 Aug 2017 23:24:08 -0300 Subject: [PATCH 36/85] Creating the guiExitAnki endpoint to allow closing Anki programmatically. --- AnkiConnect.py | 11 +++++++++++ README.md | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/AnkiConnect.py b/AnkiConnect.py index ace378d..7659bde 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -850,6 +850,13 @@ class AnkiBridge: else: return False + def guiExitAnki(self): + timer = QTimer() + def exitAnki(): + timer.stop() + self.window().close() + timer.timeout.connect(exitAnki) + timer.start(1000) # 1s should be enough to allow the response to be sent. # # AnkiConnect @@ -1149,6 +1156,10 @@ class AnkiConnect: def guiDeckReview(self, name): return self.anki.guiDeckReview(name) + @webApi + def guiExitAnki(self): + return self.anki.guiExitAnki() + # # Entry diff --git a/README.md b/README.md index b5c0f8e..4a009f7 100644 --- a/README.md +++ b/README.md @@ -1119,6 +1119,21 @@ Categories: ``` true ``` +* **guiExitAnki** + + Schedules a request to close Anki after 1s. This operation is asynchronous, so it will return immediately. + + *Sample request*: + ``` + { + "action": "guiExitAnki" + } + ``` + + *Sample response*: + ``` + null + ``` ## License ## From 0adc2e784452bcc39df2a0c201f82f411459fe59 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Mon, 28 Aug 2017 23:27:28 -0300 Subject: [PATCH 37/85] Updating readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a009f7..1571167 100644 --- a/README.md +++ b/README.md @@ -1121,7 +1121,7 @@ Categories: ``` * **guiExitAnki** - Schedules a request to close Anki after 1s. This operation is asynchronous, so it will return immediately. + Schedules a request to close Anki after 1s. This operation is asynchronous, so it will return immediately and won't wait until Anki actually exits. *Sample request*: ``` From 0c9cd00b9ca75fc9131945c6dcdc3c288d2baf57 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Tue, 29 Aug 2017 00:18:10 -0300 Subject: [PATCH 38/85] Moving the docker stuff to another folder. --- .travis.yml | 2 +- tests/{ => docker}/Dockerfile | 7 +++---- tests/{ => docker}/entrypoint.sh | 0 tests/{ => docker}/prefs.db | Bin 4 files changed, 4 insertions(+), 5 deletions(-) rename tests/{ => docker}/Dockerfile (58%) rename tests/{ => docker}/entrypoint.sh (100%) rename tests/{ => docker}/prefs.db (100%) diff --git a/.travis.yml b/.travis.yml index 30b971b..44c576b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ services: python: - "2.7" install: - - docker build -f tests/Dockerfile -t txgio/anki-connect . + - docker build -f tests/docker/Dockerfile -t txgio/anki-connect . script: - docker run -ti -d --rm -p 8765:8765 txgio/anki-connect - sleep 5 diff --git a/tests/Dockerfile b/tests/docker/Dockerfile similarity index 58% rename from tests/Dockerfile rename to tests/docker/Dockerfile index 46f6001..e4c0e9e 100644 --- a/tests/Dockerfile +++ b/tests/docker/Dockerfile @@ -9,12 +9,11 @@ RUN mkdir -p /home/anki-user/Documents/Anki/addons COPY AnkiConnect.py /home/anki-user/Documents/Anki/addons -# Required if you want to access it from another IP address. -RUN sed -i 's/127.0.0.1/0.0.0.0/' /home/anki-user/Documents/Anki/addons/AnkiConnect.py +ENV ANKICONNECT_BIND_ADDRESS 0.0.0.0 -COPY tests/prefs.db /home/anki-user/Documents/Anki/prefs.db +COPY tests/docker/prefs.db /home/anki-user/Documents/Anki/prefs.db -ADD tests/entrypoint.sh /entrypoint.sh +ADD tests/docker/entrypoint.sh /entrypoint.sh USER root RUN chmod +x /entrypoint.sh diff --git a/tests/entrypoint.sh b/tests/docker/entrypoint.sh similarity index 100% rename from tests/entrypoint.sh rename to tests/docker/entrypoint.sh diff --git a/tests/prefs.db b/tests/docker/prefs.db similarity index 100% rename from tests/prefs.db rename to tests/docker/prefs.db From 86d6ff8b63935155059402f32e6c55fb43f8ed67 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Tue, 29 Aug 2017 01:21:32 -0300 Subject: [PATCH 39/85] Including travis tests. --- .travis.yml | 5 ++--- tests/docker/Dockerfile | 22 ++++++---------------- tests/test_decks.py | 16 ++++++++++++++++ tests/test_misc.py | 10 ++++++++++ tests/util.py | 11 +++++++++++ 5 files changed, 45 insertions(+), 19 deletions(-) create mode 100644 tests/test_decks.py create mode 100644 tests/test_misc.py create mode 100644 tests/util.py diff --git a/.travis.yml b/.travis.yml index 44c576b..2a1a2fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ python: install: - docker build -f tests/docker/Dockerfile -t txgio/anki-connect . script: - - docker run -ti -d --rm -p 8765:8765 txgio/anki-connect + - docker run -ti -d --rm -p 8888:8765 -e ANKICONNECT_BIND_ADDRESS=0.0.0.0 txgio/anki-connect - sleep 5 - - > - curl localhost:8765 -X POST -s -d '{"action": "version"}' \ No newline at end of file + - python -m unittest discover -v \ No newline at end of file diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile index e4c0e9e..6d5a8fe 100644 --- a/tests/docker/Dockerfile +++ b/tests/docker/Dockerfile @@ -1,24 +1,14 @@ -FROM txgio/anki +FROM txgio/anki:2.0.45 -USER root -RUN apt-get update -RUN apt-get install -y xvfb -USER anki-user +RUN apt-get update && \ + apt-get install -y xvfb -RUN mkdir -p /home/anki-user/Documents/Anki/addons +COPY AnkiConnect.py /data/addons/AnkiConnect.py -COPY AnkiConnect.py /home/anki-user/Documents/Anki/addons - -ENV ANKICONNECT_BIND_ADDRESS 0.0.0.0 - -COPY tests/docker/prefs.db /home/anki-user/Documents/Anki/prefs.db +COPY tests/docker/prefs.db /data/prefs.db ADD tests/docker/entrypoint.sh /entrypoint.sh -USER root -RUN chmod +x /entrypoint.sh -USER anki-user - ENTRYPOINT ["/entrypoint.sh"] -CMD /bin/bash -c "(/usr/bin/ibus-daemon -xd; /usr/bin/anki;)" \ No newline at end of file +CMD ["anki", "-b", "/data"] \ No newline at end of file diff --git a/tests/test_decks.py b/tests/test_decks.py new file mode 100644 index 0000000..f4a79e8 --- /dev/null +++ b/tests/test_decks.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +import unittest +from unittest import TestCase +from util import callAnkiConnectEndpoint + +class TestDeckNames(TestCase): + + def test_deckNames(self): + response = callAnkiConnectEndpoint({'action': 'deckNames'}) + self.assertEqual(['Default'], response) + +class TestGetDeckConfig(TestCase): + + def test_getDeckConfig(self): + response = callAnkiConnectEndpoint({'action': 'getDeckConfig', 'params': {'deck': 'Default'}}) + self.assertDictContainsSubset({'name': 'Default', 'replayq': True}, response) \ No newline at end of file diff --git a/tests/test_misc.py b/tests/test_misc.py new file mode 100644 index 0000000..99d9448 --- /dev/null +++ b/tests/test_misc.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +import unittest +from unittest import TestCase +from util import callAnkiConnectEndpoint + +class TestVersion(TestCase): + + def test_version(self): + response = callAnkiConnectEndpoint({'action': 'version'}) + self.assertEqual(4, response) \ No newline at end of file diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 0000000..bf121a0 --- /dev/null +++ b/tests/util.py @@ -0,0 +1,11 @@ +import json +import urllib +import urllib2 + +def callAnkiConnectEndpoint(data): + url = 'http://docker:8888' + dumpedData = json.dumps(data) + req = urllib2.Request(url, dumpedData) + response = urllib2.urlopen(req).read() + responseData = json.loads(response) + return responseData \ No newline at end of file From dad6ef54dd2f20a2d26b8ce038c98fb5c88e514c Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Tue, 29 Aug 2017 01:24:55 -0300 Subject: [PATCH 40/85] Changing file permission. --- tests/docker/entrypoint.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tests/docker/entrypoint.sh diff --git a/tests/docker/entrypoint.sh b/tests/docker/entrypoint.sh old mode 100644 new mode 100755 From a78630f5e42fceddd2298332f8d8ba1e7a81e313 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Tue, 29 Aug 2017 01:29:58 -0300 Subject: [PATCH 41/85] Changing start directory. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2a1a2fb..2404873 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,4 +9,4 @@ install: script: - docker run -ti -d --rm -p 8888:8765 -e ANKICONNECT_BIND_ADDRESS=0.0.0.0 txgio/anki-connect - sleep 5 - - python -m unittest discover -v \ No newline at end of file + - python -m unittest discover -s tests \ No newline at end of file From 1642c48f0cec3b2d5dddd9478c08fc2c119f8070 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Tue, 29 Aug 2017 01:34:08 -0300 Subject: [PATCH 42/85] Including the docker host. --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2404873..5a6f3a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,8 @@ sudo: required language: python +addons: + hosts: + - docker services: - docker python: From a9dcea9674b77b4d3263820f8d71b747db81cb46 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Tue, 29 Aug 2017 01:49:13 -0300 Subject: [PATCH 43/85] Wating the AnkiConnect to be up to run the tests. --- .travis.yml | 4 ++-- tests/scripts/wait-up.sh | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100755 tests/scripts/wait-up.sh diff --git a/.travis.yml b/.travis.yml index 5a6f3a0..de930d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,5 +11,5 @@ install: - docker build -f tests/docker/Dockerfile -t txgio/anki-connect . script: - docker run -ti -d --rm -p 8888:8765 -e ANKICONNECT_BIND_ADDRESS=0.0.0.0 txgio/anki-connect - - sleep 5 - - python -m unittest discover -s tests \ No newline at end of file + - ./tests/scripts/wait-up.sh http://docker:8888 + - python -m unittest discover -s tests -v \ No newline at end of file diff --git a/tests/scripts/wait-up.sh b/tests/scripts/wait-up.sh new file mode 100755 index 0000000..6491b19 --- /dev/null +++ b/tests/scripts/wait-up.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -e + +if [ $# -lt 1 ]; then + printf "First parameter URL required.\n" + exit 1 +fi + +COUNTER=0 +STEP_SIZE=1 +MAX_SECONDS=${2:-10} # Wait 10 seconds if parameter not provided +MAX_RETRIES=$(( $MAX_SECONDS / $STEP_SIZE)) + +URL=$1 + +printf "Waiting URL: "$URL"\n" + +until $(curl --insecure --output /dev/null --silent --fail $URL) || [ $COUNTER -eq $MAX_RETRIES ]; do + printf '.' + sleep $STEP_SIZE + COUNTER=$(($COUNTER + 1)) +done +if [ $COUNTER -eq $MAX_RETRIES ]; then + printf "\nTimeout after "$(( $COUNTER * $STEP_SIZE))" second(s).\n" + exit 2 +else + printf "\nUp successfully after "$(( $COUNTER * $STEP_SIZE))" second(s).\n" +fi \ No newline at end of file From 4ad5bd71b53cbdad0c76419c99e44b068a9c32f2 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Tue, 29 Aug 2017 01:51:51 -0300 Subject: [PATCH 44/85] Testing failure case. --- tests/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile index 6d5a8fe..af214b9 100644 --- a/tests/docker/Dockerfile +++ b/tests/docker/Dockerfile @@ -3,7 +3,7 @@ FROM txgio/anki:2.0.45 RUN apt-get update && \ apt-get install -y xvfb -COPY AnkiConnect.py /data/addons/AnkiConnect.py +#COPY AnkiConnect.py /data/addons/AnkiConnect.py COPY tests/docker/prefs.db /data/prefs.db From e59347a07124b66022721bffb0057cc236d6423a Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Tue, 29 Aug 2017 01:54:41 -0300 Subject: [PATCH 45/85] Returning the addon include. --- tests/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile index af214b9..6d5a8fe 100644 --- a/tests/docker/Dockerfile +++ b/tests/docker/Dockerfile @@ -3,7 +3,7 @@ FROM txgio/anki:2.0.45 RUN apt-get update && \ apt-get install -y xvfb -#COPY AnkiConnect.py /data/addons/AnkiConnect.py +COPY AnkiConnect.py /data/addons/AnkiConnect.py COPY tests/docker/prefs.db /data/prefs.db From 350b12d5804c38aa894babed7df9a13533ff3de7 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Sun, 27 Aug 2017 15:32:49 -0700 Subject: [PATCH 46/85] add decorator for versioning --- AnkiConnect.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/AnkiConnect.py b/AnkiConnect.py index 7659bde..36f5c9b 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -68,6 +68,16 @@ else: # Helpers # +def webapi(*versions): + def decorator(func): + def method(*args, **kwargs): + return func(*args, **kwargs) + setattr(method, 'versions', versions) + setattr(method, 'api', True) + return method + return decorator + + def webApi(func): func.webApi = True return func @@ -909,6 +919,24 @@ class AnkiConnect: return handler(**params) + def invoke(self, version, name, *args, **kwargs): + for method_name, method_body in inspect.getmembers(self, predicate=inspect.ismethod): + api_version_last = 0 + api_name_last = None + + if getattr(method_body, 'api', False): + for api_version, api_name in getattr(method_body, 'versions', []): + if api_version_last < api_version <= version: + api_version_last = api_version + api_name_last = api_name + + if api_name_last is None and api_version_last == 0: + api_name_last = method_name + + if api_name_last is not None and api_name_last == name: + method_body(*args, **kwargs) + + @webApi def multi(self, actions): return self.anki.multi(actions) From 0b4948823b3aa1bf6017b38b1735bb2ed1099fe1 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Tue, 29 Aug 2017 17:16:06 -0300 Subject: [PATCH 47/85] Including tests for the Anki 2.1.x version. --- .travis.yml | 10 +++++++--- tests/docker/{ => 2.0.x}/Dockerfile | 0 tests/docker/{ => 2.0.x}/entrypoint.sh | 0 tests/docker/{ => 2.0.x}/prefs.db | Bin tests/docker/2.1.x/Dockerfile | 14 ++++++++++++++ tests/docker/2.1.x/entrypoint.sh | 8 ++++++++ tests/docker/2.1.x/prefs21.db | Bin 0 -> 5120 bytes 7 files changed, 29 insertions(+), 3 deletions(-) rename tests/docker/{ => 2.0.x}/Dockerfile (100%) rename tests/docker/{ => 2.0.x}/entrypoint.sh (100%) rename tests/docker/{ => 2.0.x}/prefs.db (100%) create mode 100644 tests/docker/2.1.x/Dockerfile create mode 100755 tests/docker/2.1.x/entrypoint.sh create mode 100644 tests/docker/2.1.x/prefs21.db diff --git a/.travis.yml b/.travis.yml index de930d6..4cc34ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,12 @@ services: python: - "2.7" install: - - docker build -f tests/docker/Dockerfile -t txgio/anki-connect . + - docker build -f tests/docker/$ANKI_VERSION/Dockerfile -t txgio/anki-connect:$ANKI_VERSION . script: - - docker run -ti -d --rm -p 8888:8765 -e ANKICONNECT_BIND_ADDRESS=0.0.0.0 txgio/anki-connect + - docker run -ti -d --rm -p 8888:8765 -e ANKICONNECT_BIND_ADDRESS=0.0.0.0 txgio/anki-connect:$ANKI_VERSION - ./tests/scripts/wait-up.sh http://docker:8888 - - python -m unittest discover -s tests -v \ No newline at end of file + - python -m unittest discover -s tests -v + +env: + - ANKI_VERSION=2.0.x + - ANKI_VERSION=2.1.x \ No newline at end of file diff --git a/tests/docker/Dockerfile b/tests/docker/2.0.x/Dockerfile similarity index 100% rename from tests/docker/Dockerfile rename to tests/docker/2.0.x/Dockerfile diff --git a/tests/docker/entrypoint.sh b/tests/docker/2.0.x/entrypoint.sh similarity index 100% rename from tests/docker/entrypoint.sh rename to tests/docker/2.0.x/entrypoint.sh diff --git a/tests/docker/prefs.db b/tests/docker/2.0.x/prefs.db similarity index 100% rename from tests/docker/prefs.db rename to tests/docker/2.0.x/prefs.db diff --git a/tests/docker/2.1.x/Dockerfile b/tests/docker/2.1.x/Dockerfile new file mode 100644 index 0000000..58e9572 --- /dev/null +++ b/tests/docker/2.1.x/Dockerfile @@ -0,0 +1,14 @@ +FROM txgio/anki:2.1.0beta14 + +RUN apt-get update && \ + apt-get install -y xvfb + +COPY AnkiConnect.py /data/addons21/AnkiConnect.py + +COPY tests/docker/prefs21.db /data/prefs21.db + +ADD tests/docker/entrypoint.sh /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] + +CMD ["anki", "-b", "/data"] \ No newline at end of file diff --git a/tests/docker/2.1.x/entrypoint.sh b/tests/docker/2.1.x/entrypoint.sh new file mode 100755 index 0000000..8285fb9 --- /dev/null +++ b/tests/docker/2.1.x/entrypoint.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -e + +# Start Xvfb +Xvfb -ac -screen scrn 1280x2000x24 :99.0 & +export DISPLAY=:99.0 + +exec "$@" \ No newline at end of file diff --git a/tests/docker/2.1.x/prefs21.db b/tests/docker/2.1.x/prefs21.db new file mode 100644 index 0000000000000000000000000000000000000000..dc4d505fe83bc0586426afba324103d21bddc15e GIT binary patch literal 5120 zcmeHKO>f*p7`E42QVps&a41og%7Vjg5efd-&l^b;r4gIZWK-vYx^mXDsadb>+T*m# zEkg8w#4msoNB&BM#DxPtp?7Yyg6AE7Y!WyZ5Nr8k=9!sy=KUDY^9~OOzTo;KN@lLm zZKbZLs`81hD~hs$=LVkgQc<9%ye^kZ?D}^_S%00}(q1VwZK`Nf?Zx#D%lgs+{}T&5 zQde%Tt*IC92=_GLaS~1ZfT!iRwtcj-dAy?^Z*C2C^b%#w(4BEz@Mi*;KXa40KIQX| zbk7xT4u_G@!z>8yeO!C%_V${p_@T$2rB4Fv>yev@NY+QC#F1H+G}ygP8(h_XCl}nv zKU7>^Z^8oVcdO6uJxqC`n@uk^*|3q$!||tl-e9rC4$@)6pT$uk_9Kr2ve}-=08*!Y z?)fgT7TYr<7`Um}j)EvlfVLs(1H+i0*r5?|!ZFJ^hv?JDO+4V;av(*>5E6m)N-X7W zGJbsDPeqi_V*0EZ#HK-_!fdwXj;C3i0yj;zXG_=(g6Qe}{lv34$_uy33;(4QNSaW2nF%9PAR(E=71e;DVRxnw=8ICZK*@IdX#VAIdcoMrz3E z$tIu0!k_tHlKsp+4A|=TZKG#fJyK?|lA&+~^|1^H+_-qG*6)0^J30%ZQ#UAlbI!@M zq#F&L_(>{`vJem{Cb4nCA%(09w_Kgc;}bUv#J~;D&`kc}G*o&gi?NwG*|Ty=n#JU^ z^r37*OQn>?eRbN=_yydn3$`h|k~Uwl13 zsnu7%ZTzRX^4IS|hi~pyIv|1yx=&Gb08dq9pMq#0y;pJloMwX3NgwA-Uj`5H{5*WF zvwT*?QOWTuhUr{k_?H4`LGSYzvSn~r>|TbFWV=qq?ST;~rll?TG%Z~(caD08HOHedGM{nEct{0+U5rzI z{rQDbqhPInh_mYiiR&-mus@=1p?;3~PpJP;hV$o{VHmE0(LDy^2|qMZ?lVDT|0uRh zDp~ZhsoH`bWhMeY6p0A-Y?X%qEz%GNSqN&BggA>F1V};GrLW2i1aKc-Z5u7a?%Hi> z*oDDl0s@qDF|bAdXbENjJMi Nr8d1fRPAE_{|SGCI$!_* literal 0 HcmV?d00001 From eabfb97e811deb1236374d18b8c1fa9e48741f72 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Tue, 29 Aug 2017 17:19:21 -0300 Subject: [PATCH 48/85] Fixing some paths. --- tests/docker/2.0.x/Dockerfile | 4 ++-- tests/docker/2.1.x/Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/docker/2.0.x/Dockerfile b/tests/docker/2.0.x/Dockerfile index 6d5a8fe..650c050 100644 --- a/tests/docker/2.0.x/Dockerfile +++ b/tests/docker/2.0.x/Dockerfile @@ -5,9 +5,9 @@ RUN apt-get update && \ COPY AnkiConnect.py /data/addons/AnkiConnect.py -COPY tests/docker/prefs.db /data/prefs.db +COPY tests/docker/2.0.x/prefs.db /data/prefs.db -ADD tests/docker/entrypoint.sh /entrypoint.sh +ADD tests/docker/2.0.x/entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] diff --git a/tests/docker/2.1.x/Dockerfile b/tests/docker/2.1.x/Dockerfile index 58e9572..9dbeda6 100644 --- a/tests/docker/2.1.x/Dockerfile +++ b/tests/docker/2.1.x/Dockerfile @@ -5,9 +5,9 @@ RUN apt-get update && \ COPY AnkiConnect.py /data/addons21/AnkiConnect.py -COPY tests/docker/prefs21.db /data/prefs21.db +COPY tests/docker/2.1.x/prefs21.db /data/prefs21.db -ADD tests/docker/entrypoint.sh /entrypoint.sh +ADD tests/docker/2.1.x/entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] From adcb09bf1e724893321ac2ac71f6763ddd5d3ec3 Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Tue, 29 Aug 2017 18:16:47 -0300 Subject: [PATCH 49/85] Fixing the addon for the Anki 2.1.x by using it as a module. --- tests/docker/2.1.x/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/docker/2.1.x/Dockerfile b/tests/docker/2.1.x/Dockerfile index 9dbeda6..3438c74 100644 --- a/tests/docker/2.1.x/Dockerfile +++ b/tests/docker/2.1.x/Dockerfile @@ -3,7 +3,7 @@ FROM txgio/anki:2.1.0beta14 RUN apt-get update && \ apt-get install -y xvfb -COPY AnkiConnect.py /data/addons21/AnkiConnect.py +COPY AnkiConnect.py /data/addons21/AnkiConnect/__init__.py COPY tests/docker/2.1.x/prefs21.db /data/prefs21.db From 26ce24152beed6aa58b1aeb8e0d6deaf08be362a Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Tue, 29 Aug 2017 21:17:01 -0700 Subject: [PATCH 50/85] add zip script for anki 2.1 --- build_zip.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100755 build_zip.sh diff --git a/build_zip.sh b/build_zip.sh new file mode 100755 index 0000000..2f6d2ab --- /dev/null +++ b/build_zip.sh @@ -0,0 +1,5 @@ +#!/usr/bin/bash +rm AnkiConnect.zip +cp AnkiConnect.py __init__.py +7za a AnkiConnect.zip __init__.py +rm __init__.py From 337261245bc2e88dc7497e71deb5c0154a7a62b1 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Wed, 30 Aug 2017 20:47:08 -0700 Subject: [PATCH 51/85] bump version, improvements to api --- AnkiConnect.py | 170 +++++++++++++++++++++++-------------------------- 1 file changed, 81 insertions(+), 89 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index 36f5c9b..8306588 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -35,7 +35,7 @@ from unicodedata import normalize # Constants # -API_VERSION = 4 +API_VERSION = 5 TICK_INTERVAL = 25 URL_TIMEOUT = 10 URL_UPGRADE = 'https://raw.githubusercontent.com/FooSoft/anki-connect/master/AnkiConnect.py' @@ -68,21 +68,15 @@ else: # Helpers # -def webapi(*versions): +def webApi(*versions): def decorator(func): - def method(*args, **kwargs): - return func(*args, **kwargs) + method = lambda *args, **kwargs: func(*args, **kwargs) setattr(method, 'versions', versions) setattr(method, 'api', True) return method return decorator -def webApi(func): - func.webApi = True - return func - - def makeBytes(data): return data.encode('utf-8') @@ -896,130 +890,127 @@ class AnkiConnect: def handler(self, request): - action = request.get('action', '') - if hasattr(self, action): - handler = getattr(self, action) - if callable(handler) and hasattr(handler, 'webApi') and getattr(handler, 'webApi'): - spec = inspect.getargspec(handler) - argsAll = spec.args[1:] - argsReq = argsAll + name = request.get('action', '') + version = request.get('version', 4) + params = request.get('params', {}) + reply = {'result': None, 'error': None} - argsDef = spec.defaults - if argsDef is not None: - argsReq = argsAll[:-len(argsDef)] + try: + method = None - params = request.get('params', {}) - for argReq in argsReq: - if argReq not in params: - return - for param in params: - if param not in argsAll: - return + for methodName, methodInst in inspect.getmembers(self, predicate=inspect.ismethod): + apiVersionLast = 0 + apiNameLast = None - return handler(**params) + if getattr(methodInst, 'api', False): + for apiVersion, apiName in getattr(methodInst, 'versions', []): + if apiVersionLast < apiVersion <= version: + apiVersionLast = apiVersion + apiNameLast = apiName + + if apiNameLast is None and apiVersionLast == 0: + apiNameLast = methodName + + if apiNameLast is not None and apiNameLast == name: + method = methodInst + break + + if method is None: + raise Exception('unsupported action') + else: + reply['result'] = methodInst(**params) + except Exception as e: + reply['error'] = str(e) + + if version > 4: + return reply + else: + return reply['result'] - def invoke(self, version, name, *args, **kwargs): - for method_name, method_body in inspect.getmembers(self, predicate=inspect.ismethod): - api_version_last = 0 - api_name_last = None - - if getattr(method_body, 'api', False): - for api_version, api_name in getattr(method_body, 'versions', []): - if api_version_last < api_version <= version: - api_version_last = api_version - api_name_last = api_name - - if api_name_last is None and api_version_last == 0: - api_name_last = method_name - - if api_name_last is not None and api_name_last == name: - method_body(*args, **kwargs) - - - @webApi + @webApi() def multi(self, actions): return self.anki.multi(actions) - @webApi + @webApi() def storeMediaFile(self, filename, data): return self.anki.storeMediaFile(filename, data) - @webApi + @webApi() def retrieveMediaFile(self, filename): return self.anki.retrieveMediaFile(filename) - @webApi + @webApi() def deleteMediaFile(self, filename): return self.anki.deleteMediaFile(filename) - @webApi + @webApi() def deckNames(self): return self.anki.deckNames() - @webApi + @webApi() def deckNamesAndIds(self): return self.anki.deckNamesAndIds() - @webApi + @webApi() def modelNames(self): return self.anki.modelNames() - @webApi + @webApi() def modelNamesAndIds(self): return self.anki.modelNamesAndIds() - @webApi + @webApi() def modelFieldNames(self, modelName): return self.anki.modelFieldNames(modelName) - @webApi + @webApi() def modelFieldsOnTemplates(self, modelName): return self.anki.modelFieldsOnTemplates(modelName) - @webApi + @webApi() def getDeckConfig(self, deck): return self.anki.getDeckConfig(deck) - @webApi + @webApi() def saveDeckConfig(self, config): return self.anki.saveDeckConfig(config) - @webApi + @webApi() def setDeckConfigId(self, decks, configId): return self.anki.setDeckConfigId(decks, configId) - @webApi + @webApi() def cloneDeckConfigId(self, name, cloneFrom=1): return self.anki.cloneDeckConfigId(name, cloneFrom) - @webApi + @webApi() def removeDeckConfigId(self, configId): return self.anki.removeDeckConfigId(configId) - @webApi + @webApi() def addNote(self, note): params = AnkiNoteParams(note) if params.validate(): return self.anki.addNote(params) - @webApi + @webApi() def addNotes(self, notes): results = [] for note in notes: @@ -1032,7 +1023,7 @@ class AnkiConnect: return results - @webApi + @webApi() def canAddNotes(self, notes): results = [] for note in notes: @@ -1042,42 +1033,42 @@ class AnkiConnect: return results - @webApi + @webApi() def addTags(self, notes, tags, add=True): return self.anki.addTags(notes, tags, add) - @webApi + @webApi() def removeTags(self, notes, tags): return self.anki.addTags(notes, tags, False) - @webApi + @webApi() def suspend(self, cards, suspend=True): return self.anki.suspend(cards, suspend) - @webApi + @webApi() def unsuspend(self, cards): return self.anki.suspend(cards, False) - @webApi + @webApi() def areSuspended(self, cards): return self.anki.areSuspended(cards) - @webApi + @webApi() def areDue(self, cards): return self.anki.areDue(cards) - @webApi + @webApi() def getIntervals(self, cards, complete=False): return self.anki.getIntervals(cards, complete) - @webApi + @webApi() def upgrade(self): response = QMessageBox.question( self.anki.window(), @@ -1100,91 +1091,92 @@ class AnkiConnect: return False - @webApi + @webApi() def version(self): return API_VERSION - @webApi + @webApi() def findNotes(self, query=None): return self.anki.findNotes(query) - @webApi + @webApi() def findCards(self, query=None): return self.anki.findCards(query) - @webApi + @webApi() def getDecks(self, cards): return self.anki.getDecks(cards) - @webApi + @webApi() def changeDeck(self, cards, deck): return self.anki.changeDeck(cards, deck) - @webApi + @webApi() def deleteDecks(self, decks, cardsToo=False): return self.anki.deleteDecks(decks, cardsToo) - @webApi + @webApi() def cardsToNotes(self, cards): return self.anki.cardsToNotes(cards) - @webApi + @webApi() def guiBrowse(self, query=None): return self.anki.guiBrowse(query) - @webApi + @webApi() def guiAddCards(self): return self.anki.guiAddCards() - @webApi + @webApi() def guiCurrentCard(self): return self.anki.guiCurrentCard() - @webApi + @webApi() def guiStartCardTimer(self): return self.anki.guiStartCardTimer() - @webApi + @webApi() def guiAnswerCard(self, ease): return self.anki.guiAnswerCard(ease) - @webApi + @webApi() def guiShowQuestion(self): return self.anki.guiShowQuestion() - @webApi + @webApi() def guiShowAnswer(self): return self.anki.guiShowAnswer() - @webApi + @webApi() def guiDeckOverview(self, name): return self.anki.guiDeckOverview(name) - @webApi + @webApi() def guiDeckBrowser(self): return self.anki.guiDeckBrowser() - @webApi + @webApi() def guiDeckReview(self, name): return self.anki.guiDeckReview(name) - @webApi + + @webApi() def guiExitAnki(self): return self.anki.guiExitAnki() From b46dd623f6615576acd1eecbb2ff13daf4918c8f Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Wed, 30 Aug 2017 21:26:28 -0700 Subject: [PATCH 52/85] Updating README.md --- README.md | 182 +++++++++++++++++++++++++++++------------------------- 1 file changed, 98 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 1571167..b830c33 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The installation process is similar to that of other Anki plugins and can be acc Anki must be kept running in the background in order for other applications to be able to use AnkiConnect. You can verify that AnkiConnect is running at any time by accessing [localhost:8765](http://localhost:8765) in your browser. If -the server is running, you should see the message *AnkiConnect v.3* displayed in your browser window. +the server is running, you should see the message *AnkiConnect v.5* displayed in your browser window. ### Notes for Windows Users ### @@ -45,15 +45,18 @@ AnkiConnect exposes Anki features to external applications via an easy to use initialize a minimal HTTP sever running on port 8765 every time Anki executes. Other applications (including browser extensions) can then communicate with it via HTTP POST requests. -By default, AnkiConnect will only bind the HTTP server to the `127.0.0.1.` IP Address, so you will only be able to access it from the same host on which it is running. If you need to access it over a network, you can set the environment variable `ANKICONNECT_BIND_ADDRESS` to change the binding address. For example, you can set it to `0.0.0.0` to bind it to all network interfaces on your host. +By default, AnkiConnect will only bind the HTTP server to the `127.0.0.1` IP address, so that you will only be able to +access it from the same host on which it is running. If you need to access it over a network, you can set the +environment variable `ANKICONNECT_BIND_ADDRESS` to change the binding address. For example, you can set it to `0.0.0.0` +in order to bind it to all network interfaces on your host. ### Sample Invocation ### -Every request consists of a JSON-encoded object containing an *action*, and a set of contextual *parameters*. A simple -example of a JavaScript application communicating with the extension is illustrated below: +Every request consists of a JSON-encoded object containing an `action`, a `version`, and a set of contextual `params`. A +simple example of a modern JavaScript application communicating with the extension is illustrated below: ```JavaScript -function ankiInvoke(action, params={}) { +function ankiInvoke(action, version, params={}) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.addEventListener('loadend', () => { @@ -65,42 +68,52 @@ function ankiInvoke(action, params={}) { }); xhr.open('POST', 'http://127.0.0.1:8765'); - xhr.send(JSON.stringify({action, params})); + xhr.send(JSON.stringify({action, version, params})); }); } -ankiInvoke('version').then(response => { - window.alert(`detected API version: ${response}`); -}).catch(error => { - window.alert(`could not get API version: ${error}`); -}); +try { + const response = await ankiInvoke('deckNames', 5); + window.alert(`detected decks: ${response.result}`); +} catch (e) { + window.alert(`error getting decks: ${response.error}`); +} ``` -Or using [`curl`](https://curl.haxx.se): +Or using [`curl`](https://curl.haxx.se) from the command line: ``` -curl localhost:8765 -X POST -d '{"action": "version"}' +curl localhost:8765 -X POST -d '{"action": "deckNames", "version": 5}' ``` +AnkiConnect will respond with an object containing two fields: `result` and `error`. The `result` field contains the +return value of the executed API, and the `error` field is a description of any exception thrown during API execution +(the value `null` is used if execution completed successfully). + +*Sample successful response*: +``` +{"result": ["Default", "Filtered Deck 1"], "error": null} +``` + +*Samples of failed responses*: +``` +{"result": null, "error": "unsupported action"} +``` +``` +{"result": null, "error": "guiBrowse() got an unexpected keyword argument 'foobar'"} +``` + +For compatibility with clients designed to work with older versions of AnkiConnect, failing to provide a `version` field +in the request will make the version default to 4. Furthermore, when the provided version is level 4 or below, the API +response will only contain the value of the `result`; no `error` field is available for error handling. + ### Supported Actions ### -Below is a list of currently supported actions. Requests with invalid actions or parameters will a return `null` result. +Below is a comprehensive list of currently supported actions. Note that deprecated APIs will continue to function +despite not being listed on this page as long as your request is labeled with a version number corresponding to when the +API was available for use. -Categories: - -* [Miscellaneous](#miscellaneous) -* [Decks](#decks) -* [Deck Configurations](#deck-configurations) -* [Models](#models) -* [Note Creation](#note-creation) -* [Note Tags](#note-tags) -* [Card Suspension](#card-suspension) -* [Card Intervals](#card-intervals) -* [Finding Notes and Cards](#finding-notes-and-cards) -* [Media File Storage](#media-file-storage) -* [Graphical](#graphical) - -### Miscellaneous ### +#### Miscellaneous #### * **version** @@ -117,7 +130,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` 4 ``` @@ -135,7 +148,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` true ``` @@ -160,7 +173,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` [ ["Default"], @@ -168,7 +181,7 @@ Categories: ] ``` -### Decks ### +#### Decks #### * **deckNames** @@ -181,7 +194,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` [ "Default" @@ -199,7 +212,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` { "Default": 1 @@ -221,7 +234,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` { "Default": [1502032366472], @@ -244,7 +257,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` null ``` @@ -265,12 +278,12 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` null ``` -### Deck Configurations ### +#### Deck Configurations #### * **getDeckConfig** @@ -286,7 +299,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` { "lapse": { @@ -341,7 +354,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` true ``` @@ -362,7 +375,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` true ``` @@ -384,7 +397,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` 1502972374573 ``` @@ -404,12 +417,12 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` true ``` -### Models ### +#### Models #### * **modelNames** @@ -422,7 +435,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` [ "Basic", @@ -441,7 +454,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` { "Basic": 1483883011648 @@ -465,7 +478,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` [ "Front", @@ -488,7 +501,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` { "Card 1": [ @@ -502,7 +515,7 @@ Categories: } ``` -### Note Creation ### +#### Note Creation #### * **addNote** @@ -539,7 +552,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` 1496198395707 ``` @@ -577,7 +590,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` [ 1496198395707, @@ -612,14 +625,14 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` [ true ] ``` -### Note Tags ### +#### Note Tags #### * **addTags** @@ -636,7 +649,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` null ``` @@ -656,12 +669,12 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` null ``` -### Card Suspension ### +#### Card Suspension #### * **suspend** @@ -678,7 +691,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` true ``` @@ -698,7 +711,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` true ``` @@ -717,12 +730,12 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` [false, true] ``` -### Card Intervals ### +#### Card Intervals #### * **areDue** @@ -740,7 +753,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` [false, true] ``` @@ -760,7 +773,7 @@ Categories: } ``` - *Sample response 1*: + *Sample result 1*: ``` [-14400, 3] ``` @@ -776,7 +789,7 @@ Categories: } ``` - *Sample response 2*: + *Sample result 2*: ``` [ [-120, -180, -240, -300, -360, -14400], @@ -784,7 +797,7 @@ Categories: ] ``` -### Finding Notes and Cards ### +#### Finding Notes and Cards #### * **findNotes** @@ -800,7 +813,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` [ 1483959289817, @@ -823,7 +836,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` [ 1494723142483, @@ -847,7 +860,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` [ 1502098029797, @@ -855,7 +868,7 @@ Categories: ] ``` -### Media File Storage ### +#### Media File Storage #### * **storeMediaFile** @@ -875,7 +888,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` null ``` @@ -899,7 +912,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` "SGVsbG8sIHdvcmxkIQ==" ``` @@ -918,12 +931,12 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` null ``` -### Graphical ### +#### Graphical #### * **guiBrowse** @@ -939,7 +952,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` [ 1494723142483, @@ -959,7 +972,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` null ``` @@ -975,7 +988,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` { "answer": "back content", @@ -1009,7 +1022,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` true ``` @@ -1025,7 +1038,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` true ``` @@ -1041,7 +1054,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` true ``` @@ -1061,7 +1074,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` true ``` @@ -1080,7 +1093,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` true ``` @@ -1096,7 +1109,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` null ``` @@ -1115,10 +1128,11 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` true ``` + * **guiExitAnki** Schedules a request to close Anki after 1s. This operation is asynchronous, so it will return immediately and won't wait until Anki actually exits. @@ -1130,7 +1144,7 @@ Categories: } ``` - *Sample response*: + *Sample result*: ``` null ``` From b45d66a07496a03568986dab42630ed40e31019e Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Wed, 30 Aug 2017 21:43:15 -0700 Subject: [PATCH 53/85] Updating README.md --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b830c33..70d7807 100644 --- a/README.md +++ b/README.md @@ -113,11 +113,14 @@ Below is a comprehensive list of currently supported actions. Note that deprecat despite not being listed on this page as long as your request is labeled with a version number corresponding to when the API was available for use. +This page currently documents **version 5** of the API. Make sure to include this version number in your requests to +guarantee that your application continues to function properly in the future. + #### Miscellaneous #### * **version** - Gets the version of the API exposed by this plugin. Currently versions `1` through `4` are defined. + Gets the version of the API exposed by this plugin. Currently versions `1` through `5` are defined. This should be the first call you make to make sure that your application and AnkiConnect are able to communicate properly with each other. New versions of AnkiConnect are backwards compatible; as long as you are using actions @@ -132,7 +135,7 @@ API was available for use. *Sample result*: ``` - 4 + 5 ``` * **upgrade** From d05ba3c4edb19ef7bfc0f446d63bf91c13b5c781 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Sat, 9 Sep 2017 22:28:09 -0700 Subject: [PATCH 54/85] Updating README.md --- README.md | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 70d7807..eb3fbaa 100644 --- a/README.md +++ b/README.md @@ -56,14 +56,24 @@ Every request consists of a JSON-encoded object containing an `action`, a `versi simple example of a modern JavaScript application communicating with the extension is illustrated below: ```JavaScript -function ankiInvoke(action, version, params={}) { +function ankiConnectInvoke(action, version, params={}) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); - xhr.addEventListener('loadend', () => { - if (xhr.responseText) { - resolve(JSON.parse(xhr.responseText)); - } else { - reject('unable to connect to plugin'); + xhr.addEventListener('error', () => reject('failed to connect to AnkiConnect')); + xhr.addEventListener('load', () => { + try { + const response = JSON.parse(xhr.responseText); + if (response.error) { + throw response.error; + } else { + if (response.hasOwnProperty('result')) { + resolve(response.result); + } else { + reject('failed to get results from AnkiConnect'); + } + } + } catch (e) { + reject(e); } }); @@ -73,10 +83,10 @@ function ankiInvoke(action, version, params={}) { } try { - const response = await ankiInvoke('deckNames', 5); - window.alert(`detected decks: ${response.result}`); + const result = await ankiConnectInvoke('deckNames', 5); + console.log(`got list of decks: ${result}`); } catch (e) { - window.alert(`error getting decks: ${response.error}`); + console.log(`error getting decks: ${e}`); } ``` @@ -528,7 +538,8 @@ guarantee that your application continues to function properly in the future. AnkiConnect can download audio files and embed them in newly created notes. The corresponding *audio* note member is optional and can be omitted. If you choose to include it, the *url* and *filename* fields must be also defined. The *skipHash* field can be optionally provided to skip the inclusion of downloaded files with an MD5 hash that matches - the provided value. This is useful for avoiding the saving of error pages and stub files. + the provided value. This is useful for avoiding the saving of error pages and stub files. The *fields* member is a + list of fields that should play audio when the card is displayed in Anki. *Sample request*: ``` @@ -540,15 +551,16 @@ guarantee that your application continues to function properly in the future. "modelName": "Basic", "fields": { "Front": "front content", - "Back": "back content", + "Back": "back content" }, "tags": [ - "yomichan", + "yomichan" ], "audio": { "url": "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=猫&kana=ねこ", "filename": "yomichan_ねこ_猫.mp3", - "skipHash": "7e2c2f954ef6051373ba916f000168dc" + "skipHash": "7e2c2f954ef6051373ba916f000168dc", + "fields": "Front" } } } @@ -585,7 +597,8 @@ guarantee that your application continues to function properly in the future. "audio": { "url": "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=猫&kana=ねこ", "filename": "yomichan_ねこ_猫.mp3", - "skipHash": "7e2c2f954ef6051373ba916f000168dc" + "skipHash": "7e2c2f954ef6051373ba916f000168dc", + "fields": "Front" } } ] From 065c1991056a3e0a003b5edd4fbf6d91a751ccf8 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Fri, 15 Sep 2017 22:17:52 -0700 Subject: [PATCH 55/85] fix broken error handling for invalid json --- AnkiConnect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index 8306588..94ec384 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -268,7 +268,7 @@ class AjaxServer: params = json.loads(makeStr(req.body)) body = makeBytes(json.dumps(self.handler(params))) except ValueError: - body = json.dumps(None); + body = makeBytes(json.dumps(None)) resp = bytes() From bd071a3dfebc6573af776b98608c5ee1fd0125e0 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Fri, 29 Sep 2017 17:03:48 -0700 Subject: [PATCH 56/85] Updating README.md --- README.md | 196 +++++++++++++++++++++++++++--------------------------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index eb3fbaa..0725701 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ in order to bind it to all network interfaces on your host. Every request consists of a JSON-encoded object containing an `action`, a `version`, and a set of contextual `params`. A simple example of a modern JavaScript application communicating with the extension is illustrated below: -```JavaScript +```javascript function ankiConnectInvoke(action, version, params={}) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); @@ -92,7 +92,7 @@ try { Or using [`curl`](https://curl.haxx.se) from the command line: -``` +```bash curl localhost:8765 -X POST -d '{"action": "deckNames", "version": 5}' ``` @@ -101,15 +101,15 @@ return value of the executed API, and the `error` field is a description of any (the value `null` is used if execution completed successfully). *Sample successful response*: -``` +```json {"result": ["Default", "Filtered Deck 1"], "error": null} ``` *Samples of failed responses*: -``` +```json {"result": null, "error": "unsupported action"} ``` -``` +```json {"result": null, "error": "guiBrowse() got an unexpected keyword argument 'foobar'"} ``` @@ -137,14 +137,14 @@ guarantee that your application continues to function properly in the future. which are available in the reported AnkiConnect version or earlier, everything should work fine. *Sample request*: - ``` + ```json { "action": "version" } ``` *Sample result*: - ``` + ```json 5 ``` @@ -155,14 +155,14 @@ guarantee that your application continues to function properly in the future. GitHub. Returns a boolean value indicating if the plugin was upgraded or not. *Sample request*: - ``` + ```json { "action": "upgrade" } ``` *Sample result*: - ``` + ```json true ``` @@ -171,7 +171,7 @@ guarantee that your application continues to function properly in the future. Performs multiple actions in one request, returning an array with the response of each action (in the given order). *Sample request*: - ``` + ```json { "action": "multi", "params": { @@ -187,7 +187,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json [ ["Default"], [1494723142483, 1494703460437, 1494703479525] @@ -201,14 +201,14 @@ guarantee that your application continues to function properly in the future. Gets the complete list of deck names for the current user. *Sample request*: - ``` + ```json { "action": "deckNames" } ``` *Sample result*: - ``` + ```json [ "Default" ] @@ -219,14 +219,14 @@ guarantee that your application continues to function properly in the future. Gets the complete list of deck names and their respective IDs for the current user. *Sample request*: - ``` + ```json { "action": "deckNamesAndIds" } ``` *Sample result*: - ``` + ```json { "Default": 1 } @@ -238,7 +238,7 @@ guarantee that your application continues to function properly in the future. cards which belong to it. *Sample request*: - ``` + ```json { "action": "getDecks", "params": { @@ -248,7 +248,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json { "Default": [1502032366472], "Japanese::JLPT N3": [1502298036657, 1502298033753] @@ -260,7 +260,7 @@ guarantee that your application continues to function properly in the future. Moves cards with the given IDs to a different deck, creating the deck if it doesn't exist yet. *Sample request*: - ``` + ```json { "action": "changeDeck", "params": { @@ -271,7 +271,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json null ``` @@ -281,7 +281,7 @@ guarantee that your application continues to function properly in the future. the deleted decks will also be deleted; otherwise they will be moved to the default deck. *Sample request*: - ``` + ```json { "action": "deleteDecks", "params": { @@ -292,7 +292,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json null ``` @@ -303,7 +303,7 @@ guarantee that your application continues to function properly in the future. Gets the config group object for the given deck. *Sample request*: - ``` + ```json { "action": "getDeckConfig", "params": { @@ -313,7 +313,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json { "lapse": { "leechFails": 8, @@ -358,7 +358,7 @@ guarantee that your application continues to function properly in the future. it does not exist). *Sample request*: - ``` + ```json { "action": "saveDeckConfig", "params": { @@ -368,7 +368,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json true ``` @@ -378,7 +378,7 @@ guarantee that your application continues to function properly in the future. `false` if the given configuration group or any of the given decks do not exist. *Sample request*: - ``` + ```json { "action": "setDeckConfigId", "params": { @@ -389,7 +389,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json true ``` @@ -400,7 +400,7 @@ guarantee that your application continues to function properly in the future. not exist. *Sample request*: - ``` + ```json { "action": "cloneDeckConfigId", "params": { @@ -411,7 +411,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json 1502972374573 ``` @@ -421,7 +421,7 @@ guarantee that your application continues to function properly in the future. either the default config group (ID = 1) or a config group that does not exist. *Sample request*: - ``` + ```json { "action": "removeDeckConfigId", "params": { @@ -431,7 +431,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json true ``` @@ -442,14 +442,14 @@ guarantee that your application continues to function properly in the future. Gets the complete list of model names for the current user. *Sample request*: - ``` + ```json { "action": "modelNames" } ``` *Sample result*: - ``` + ```json [ "Basic", "Basic (and reversed card)" @@ -461,18 +461,18 @@ guarantee that your application continues to function properly in the future. Gets the complete list of model names and their corresponding IDs for the current user. *Sample request*: - ``` + ```json { "action": "modelNamesAndIds" } ``` *Sample result*: - ``` + ```json { - "Basic": 1483883011648 - "Basic (and reversed card)": 1483883011644 - "Basic (optional reversed card)": 1483883011631 + "Basic": 1483883011648, + "Basic (and reversed card)": 1483883011644, + "Basic (optional reversed card)": 1483883011631, "Cloze": 1483883011630 } ``` @@ -482,7 +482,7 @@ guarantee that your application continues to function properly in the future. Gets the complete list of field names for the provided model name. *Sample request*: - ``` + ```json { "action": "modelFieldNames", "params": { @@ -492,7 +492,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json [ "Front", "Back" @@ -505,7 +505,7 @@ guarantee that your application continues to function properly in the future. name. The question side is given first in each array. *Sample request*: - ``` + ```json { "action": "modelFieldsOnTemplates", "params": { @@ -515,7 +515,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json { "Card 1": [ ["Front"], @@ -542,7 +542,7 @@ guarantee that your application continues to function properly in the future. list of fields that should play audio when the card is displayed in Anki. *Sample request*: - ``` + ```json { "action": "addNote", "params": { @@ -568,7 +568,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json 1496198395707 ``` @@ -579,7 +579,7 @@ guarantee that your application continues to function properly in the future. documentation for `addNote` for an explanation of objects in the `notes` array. *Sample request*: - ``` + ```json { "action": "addNotes", "params": { @@ -607,7 +607,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json [ 1496198395707, null @@ -620,7 +620,7 @@ guarantee that your application continues to function properly in the future. booleans indicating whether or not the parameters at the corresponding index could be used to create a new note. *Sample request*: - ``` + ```json { "action": "canAddNotes", "params": { @@ -642,7 +642,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json [ true ] @@ -655,7 +655,7 @@ guarantee that your application continues to function properly in the future. Adds tags to notes by note ID. *Sample request*: - ``` + ```json { "action": "addTags", "params": { @@ -666,7 +666,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json null ``` @@ -675,7 +675,7 @@ guarantee that your application continues to function properly in the future. Remove tags from notes by note ID. *Sample request*: - ``` + ```json { "action": "removeTags", "params": { @@ -686,7 +686,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json null ``` @@ -698,7 +698,7 @@ guarantee that your application continues to function properly in the future. otherwise. *Sample request*: - ``` + ```json { "action": "suspend", "params": { @@ -708,7 +708,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json true ``` @@ -718,7 +718,7 @@ guarantee that your application continues to function properly in the future. otherwise. *Sample request*: - ``` + ```json { "action": "unsuspend", "params": { @@ -728,7 +728,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json true ``` @@ -737,7 +737,7 @@ guarantee that your application continues to function properly in the future. Returns an array indicating whether each of the given cards is suspended (in the same order). *Sample request*: - ``` + ```json { "action": "areSuspended", "params": { @@ -747,7 +747,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json [false, true] ``` @@ -760,7 +760,7 @@ guarantee that your application continues to function properly in the future. match the way Anki treats them when reviewing. *Sample request*: - ``` + ```json { "action": "areDue", "params": { @@ -770,7 +770,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json [false, true] ``` @@ -780,7 +780,7 @@ guarantee that your application continues to function properly in the future. for each given card ID when `complete` is `true`. (Negative intervals are in seconds and positive intervals in days.) *Sample request 1*: - ``` + ```json { "action": "getIntervals", "params": { @@ -790,12 +790,12 @@ guarantee that your application continues to function properly in the future. ``` *Sample result 1*: - ``` + ```json [-14400, 3] ``` *Sample request 2*: - ``` + ```json { "action": "getIntervals", "params": { @@ -806,7 +806,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result 2*: - ``` + ```json [ [-120, -180, -240, -300, -360, -14400], [-120, -180, -240, -300, -360, -14400, 1, 3] @@ -820,7 +820,7 @@ guarantee that your application continues to function properly in the future. Returns an array of note IDs for a given query (same query syntax as **guiBrowse**). *Sample request*: - ``` + ```json { "action": "findNotes", "params": { @@ -830,7 +830,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json [ 1483959289817, 1483959291695 @@ -843,7 +843,7 @@ guarantee that your application continues to function properly in the future. for better performance). *Sample request*: - ``` + ```json { "action": "findCards", "params": { @@ -853,7 +853,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json [ 1494723142483, 1494703460437, @@ -867,7 +867,7 @@ guarantee that your application continues to function properly in the future. given once in the array. *Sample request*: - ``` + ```json { "action": "cardsToNotes", "params": { @@ -877,7 +877,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json [ 1502098029797, 1502298025183 @@ -894,7 +894,7 @@ guarantee that your application continues to function properly in the future. with an underscore. These files are still synchronized to AnkiWeb. *Sample request*: - ``` + ```json { "action": "storeMediaFile", "params": { @@ -905,7 +905,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json null ``` @@ -919,7 +919,7 @@ guarantee that your application continues to function properly in the future. Retrieves the base64-encoded contents of the specified file, returning `false` if the file does not exist. *Sample request*: - ``` + ```json { "action": "retrieveMediaFile", "params": { @@ -929,7 +929,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json "SGVsbG8sIHdvcmxkIQ==" ``` @@ -938,7 +938,7 @@ guarantee that your application continues to function properly in the future. Deletes the specified file inside the media folder. *Sample request*: - ``` + ```json { "action": "deleteMediaFile", "params": { @@ -948,7 +948,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json null ``` @@ -959,7 +959,7 @@ guarantee that your application continues to function properly in the future. Invokes the card browser and searches for a given query. Returns an array of identifiers of the cards that were found. *Sample request*: - ``` + ```json { "action": "guiBrowse", "params": { @@ -969,7 +969,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json [ 1494723142483, 1494703460437, @@ -982,14 +982,14 @@ guarantee that your application continues to function properly in the future. Invokes the AddCards dialog. *Sample request*: - ``` + ```json { "action": "guiAddCards" } ``` *Sample result*: - ``` + ```json null ``` @@ -998,14 +998,14 @@ guarantee that your application continues to function properly in the future. Returns information about the current card or `null` if not in review mode. *Sample request*: - ``` + ```json { "action": "guiCurrentCard" } ``` *Sample result*: - ``` + ```json { "answer": "back content", "question": "front content", @@ -1032,14 +1032,14 @@ guarantee that your application continues to function properly in the future. Starts or resets the 'timerStarted' value for the current card. This is useful for deferring the start time to when it is displayed via the API, allowing the recorded time taken to answer the card to be more accurate when calling guiAnswerCard. *Sample request*: - ``` + ```json { "action": "guiStartCardTimer" } ``` *Sample result*: - ``` + ```json true ``` @@ -1048,14 +1048,14 @@ guarantee that your application continues to function properly in the future. Shows question text for the current card; returns `true` if in review mode or `false` otherwise. *Sample request*: - ``` + ```json { "action": "guiShowQuestion" } ``` *Sample result*: - ``` + ```json true ``` @@ -1064,14 +1064,14 @@ guarantee that your application continues to function properly in the future. Shows answer text for the current card; returns `true` if in review mode or `false` otherwise. *Sample request*: - ``` + ```json { "action": "guiShowAnswer" } ``` *Sample result*: - ``` + ```json true ``` @@ -1081,7 +1081,7 @@ guarantee that your application continues to function properly in the future. card must be displayed before before any answer can be accepted by Anki. *Sample request*: - ``` + ```json { "action": "guiAnswerCard", "params": { @@ -1091,7 +1091,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json true ``` @@ -1100,7 +1100,7 @@ guarantee that your application continues to function properly in the future. Opens the Deck Overview screen for the deck with the given name; returns `true` if succeeded or `false` otherwise. *Sample request*: - ``` + ```json { "action": "guiDeckOverview", "params": { @@ -1110,7 +1110,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json true ``` @@ -1119,14 +1119,14 @@ guarantee that your application continues to function properly in the future. Opens the Deck Browser screen. *Sample request*: - ``` + ```json { "action": "guiDeckBrowser" } ``` *Sample result*: - ``` + ```json null ``` @@ -1135,7 +1135,7 @@ guarantee that your application continues to function properly in the future. Starts review for the deck with the given name; returns `true` if succeeded or `false` otherwise. *Sample request*: - ``` + ```json { "action": "guiDeckReview", "params": { @@ -1145,7 +1145,7 @@ guarantee that your application continues to function properly in the future. ``` *Sample result*: - ``` + ```json true ``` @@ -1154,14 +1154,14 @@ guarantee that your application continues to function properly in the future. Schedules a request to close Anki after 1s. This operation is asynchronous, so it will return immediately and won't wait until Anki actually exits. *Sample request*: - ``` + ```json { "action": "guiExitAnki" } ``` *Sample result*: - ``` + ```json null ``` From 01676f7b2c6fbb355f8bde1513ecfb2d2b9560de Mon Sep 17 00:00:00 2001 From: tomasgodoi Date: Sun, 1 Oct 2017 12:22:04 -0300 Subject: [PATCH 57/85] Including the css field on the guiCurrentCard endpoint. --- AnkiConnect.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index 94ec384..436c771 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -785,7 +785,8 @@ class AnkiBridge: 'answer': card._getQA()['a'], 'buttons': [b[0] for b in reviewer._answerButtonList()], 'modelName': model['name'], - 'deckName': self.deckNameFromId(card.did) + 'deckName': self.deckNameFromId(card.did), + 'css': model['css'] } From 1c1cf2ec613c32da3478160fd369d08a8c69e101 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Sun, 1 Oct 2017 18:17:39 -0700 Subject: [PATCH 58/85] Updating README.md --- README.md | 534 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 356 insertions(+), 178 deletions(-) diff --git a/README.md b/README.md index 0725701..ff46222 100644 --- a/README.md +++ b/README.md @@ -139,13 +139,17 @@ guarantee that your application continues to function properly in the future. *Sample request*: ```json { - "action": "version" + "action": "version", + "version": 5 } ``` *Sample result*: ```json - 5 + { + "result": 5, + "error": null + } ``` * **upgrade** @@ -157,13 +161,17 @@ guarantee that your application continues to function properly in the future. *Sample request*: ```json { - "action": "upgrade" + "action": "upgrade", + "version": 5 } ``` *Sample result*: ```json - true + { + "result": true, + "error": null + } ``` * **multi** @@ -174,6 +182,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "multi", + "version": 5, "params": { "actions": [ {"action": "deckNames"}, @@ -188,10 +197,13 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - [ - ["Default"], - [1494723142483, 1494703460437, 1494703479525] - ] + { + "result": [ + ["Default"], + [1494723142483, 1494703460437, 1494703479525] + ], + "error": null + } ``` #### Decks #### @@ -203,15 +215,17 @@ guarantee that your application continues to function properly in the future. *Sample request*: ```json { - "action": "deckNames" + "action": "deckNames", + "version": 5 } ``` *Sample result*: ```json - [ - "Default" - ] + { + "result": ["Default"], + "error": null + } ``` * **deckNamesAndIds** @@ -221,14 +235,16 @@ guarantee that your application continues to function properly in the future. *Sample request*: ```json { - "action": "deckNamesAndIds" + "action": "deckNamesAndIds", + "version": 5 } ``` *Sample result*: ```json { - "Default": 1 + "result": {"Default": 1}, + "error": null } ``` @@ -241,6 +257,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "getDecks", + "version": 5, "params": { "cards": [1502298036657, 1502298033753, 1502032366472] } @@ -250,8 +267,11 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json { - "Default": [1502032366472], - "Japanese::JLPT N3": [1502298036657, 1502298033753] + "result": { + "Default": [1502032366472], + "Japanese::JLPT N3": [1502298036657, 1502298033753] + }, + "error": null } ``` @@ -263,6 +283,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "changeDeck", + "version": 5, "params": { "cards": [1502098034045, 1502098034048, 1502298033753], "deck": "Japanese::JLPT N3" @@ -272,7 +293,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - null + { + "result": null, + "error": null + } ``` * **deleteDecks** @@ -284,6 +308,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "deleteDecks", + "version": 5, "params": { "decks": ["Japanese::JLPT N5", "Easy Spanish"], "cardsToo": true @@ -293,19 +318,23 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - null + { + "result": null, + "error": null + } ``` #### Deck Configurations #### * **getDeckConfig** - Gets the config group object for the given deck. + Gets the configuration group object for the given deck. *Sample request*: ```json { "action": "getDeckConfig", + "version": 5, "params": { "deck": "Default" } @@ -315,61 +344,103 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json { - "lapse": { - "leechFails": 8, - "delays": [10], - "minInt": 1, - "leechAction": 0, - "mult": 0 + "result": { + "lapse": { + "leechFails": 8, + "delays": [10], + "minInt": 1, + "leechAction": 0, + "mult": 0 + }, + "dyn": false, + "autoplay": true, + "mod": 1502970872, + "id": 1, + "maxTaken": 60, + "new": { + "bury": true, + "order": 1, + "initialFactor": 2500, + "perDay": 20, + "delays": [1, 10], + "separate": true, + "ints": [1, 4, 7] + }, + "name": "Default", + "rev": { + "bury": true, + "ivlFct": 1, + "ease4": 1.3, + "maxIvl": 36500, + "perDay": 100, + "minSpace": 1, + "fuzz": 0.05 + }, + "timer": 0, + "replayq": true, + "usn": -1 }, - "dyn": false, - "autoplay": true, - "mod": 1502970872, - "id": 1, - "maxTaken": 60, - "new": { - "bury": true, - "order": 1, - "initialFactor": 2500, - "perDay": 20, - "delays": [1, 10], - "separate": true, - "ints": [1, 4, 7] - }, - "name": "Default", - "rev": { - "bury": true, - "ivlFct": 1, - "ease4": 1.3, - "maxIvl": 36500, - "perDay": 100, - "minSpace": 1, - "fuzz": 0.05 - }, - "timer": 0, - "replayq": true, - "usn": -1 + "error": null } ``` * **saveDeckConfig** - Saves the given config group, returning `true` on success or `false` if the ID of the config group is invalid (i.e. - it does not exist). + Saves the given configuration group, returning `true` on success or `false` if the ID of the configuration group is + invalid (such as when it does not exist). *Sample request*: ```json { "action": "saveDeckConfig", + "version": 5, "params": { - "config": (config group object) + "config": { + "lapse": { + "leechFails": 8, + "delays": [10], + "minInt": 1, + "leechAction": 0, + "mult": 0 + }, + "dyn": false, + "autoplay": true, + "mod": 1502970872, + "id": 1, + "maxTaken": 60, + "new": { + "bury": true, + "order": 1, + "initialFactor": 2500, + "perDay": 20, + "delays": [1, 10], + "separate": true, + "ints": [1, 4, 7] + }, + "name": "Default", + "rev": { + "bury": true, + "ivlFct": 1, + "ease4": 1.3, + "maxIvl": 36500, + "perDay": 100, + "minSpace": 1, + "fuzz": 0.05 + }, + "timer": 0, + "replayq": true, + "usn": -1 + } } } ``` *Sample result*: ```json - true + { + "result": true, + "error": null + } ``` * **setDeckConfigId** @@ -381,6 +452,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "setDeckConfigId", + "version": 5, "params": { "decks": ["Default"], "configId": 1 @@ -390,19 +462,23 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - true + { + "result": true, + "error": null + } ``` * **cloneDeckConfigId** - Creates a new config group with the given name, cloning from the group with the given ID, or from the default group - if this is unspecified. Returns the ID of the new config group, or `false` if the specified group to clone from does - not exist. + Creates a new configuration group with the given name, cloning from the group with the given ID, or from the default + group if this is unspecified. Returns the ID of the new configuration group, or `false` if the specified group to + clone from does not exist. *Sample request*: ```json { "action": "cloneDeckConfigId", + "version": 5, "params": { "name": "Copy of Default", "cloneFrom": 1 @@ -412,18 +488,22 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - 1502972374573 + { + "result": 1502972374573, + "error": null + } ``` * **removeDeckConfigId** - Removes the config group with the given ID, returning `true` if successful, or `false` if attempting to remove - either the default config group (ID = 1) or a config group that does not exist. + Removes the configuration group with the given ID, returning `true` if successful, or `false` if attempting to + remove either the default configuration group (ID = 1) or a configuration group that does not exist. *Sample request*: ```json { "action": "removeDeckConfigId", + "version": 5, "params": { "configId": 1502972374573 } @@ -432,7 +512,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - true + { + "result": true, + "error": null + } ``` #### Models #### @@ -444,16 +527,17 @@ guarantee that your application continues to function properly in the future. *Sample request*: ```json { - "action": "modelNames" + "action": "modelNames", + "version": 5 } ``` *Sample result*: ```json - [ - "Basic", - "Basic (and reversed card)" - ] + { + "result": ["Basic", "Basic (and reversed card)"], + "error": null + } ``` * **modelNamesAndIds** @@ -463,17 +547,21 @@ guarantee that your application continues to function properly in the future. *Sample request*: ```json { - "action": "modelNamesAndIds" + "action": "modelNamesAndIds", + "version": 5 } ``` *Sample result*: ```json { - "Basic": 1483883011648, - "Basic (and reversed card)": 1483883011644, - "Basic (optional reversed card)": 1483883011631, - "Cloze": 1483883011630 + "result": { + "Basic": 1483883011648, + "Basic (and reversed card)": 1483883011644, + "Basic (optional reversed card)": 1483883011631, + "Cloze": 1483883011630 + }, + "error": null } ``` @@ -485,6 +573,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "modelFieldNames", + "version": 5, "params": { "modelName": "Basic" } @@ -493,10 +582,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - [ - "Front", - "Back" - ] + { + "result": ["Front", "Back"], + "error": null + } ``` * **modelFieldsOnTemplates** @@ -508,6 +597,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "modelFieldsOnTemplates", + "version": 5, "params": { "modelName": "Basic (and reversed card)" } @@ -517,14 +607,11 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json { - "Card 1": [ - ["Front"], - ["Back"] - ], - "Card 2": [ - ["Back"], - ["Front"] - ] + "result": { + "Card 1": [["Front"], ["Back"]], + "Card 2": [["Back"], ["Front"]] + }, + "error": null } ``` @@ -535,16 +622,17 @@ guarantee that your application continues to function properly in the future. Creates a note using the given deck and model, with the provided field values and tags. Returns the identifier of the created note created on success, and `null` on failure. - AnkiConnect can download audio files and embed them in newly created notes. The corresponding *audio* note member is - optional and can be omitted. If you choose to include it, the *url* and *filename* fields must be also defined. The - *skipHash* field can be optionally provided to skip the inclusion of downloaded files with an MD5 hash that matches - the provided value. This is useful for avoiding the saving of error pages and stub files. The *fields* member is a + AnkiConnect can download audio files and embed them in newly created notes. The corresponding `audio` note member is + optional and can be omitted. If you choose to include it, the `url` and `filename` fields must be also defined. The + `skipHash` field can be optionally provided to skip the inclusion of downloaded files with an MD5 hash that matches + the provided value. This is useful for avoiding the saving of error pages and stub files. The `fields` member is a list of fields that should play audio when the card is displayed in Anki. *Sample request*: ```json { "action": "addNote", + "version": 5, "params": { "note": { "deckName": "Default", @@ -569,7 +657,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - 1496198395707 + { + "result": 1496198395707, + "error": null + } ``` * **addNotes** @@ -582,6 +673,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "addNotes", + "version": 5, "params": { "notes": [ { @@ -608,10 +700,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - [ - 1496198395707, - null - ] + { + "result": [1496198395707, null], + "error": null + } ``` * **canAddNotes** @@ -623,6 +715,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "canAddNotes", + "version": 5, "params": { "notes": [ { @@ -643,9 +736,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - [ - true - ] + { + "result": [true], + "error": null + } ``` #### Note Tags #### @@ -658,6 +752,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "addTags", + "version": 5, "params": { "notes": [1483959289817, 1483959291695], "tags": "european-languages" @@ -667,7 +762,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - null + { + "result": null, + "error": null + } ``` * **removeTags** @@ -678,6 +776,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "removeTags", + "version": 5, "params": { "notes": [1483959289817, 1483959291695], "tags": "european-languages" @@ -687,7 +786,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - null + { + "result": null, + "error": null + } ``` #### Card Suspension #### @@ -701,6 +803,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "suspend", + "version": 5, "params": { "cards": [1483959291685, 1483959293217] } @@ -709,7 +812,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - true + { + "result": true, + "error": null + } ``` * **unsuspend** @@ -721,6 +827,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "unsuspend", + "version": 5, "params": { "cards": [1483959291685, 1483959293217] } @@ -729,7 +836,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - true + { + "result": true, + "error": null + } ``` * **areSuspended** @@ -740,6 +850,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "areSuspended", + "version": 5, "params": { "cards": [1483959291685, 1483959293217] } @@ -748,21 +859,25 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - [false, true] + { + "result": [false, true], + "error": null + } ``` #### Card Intervals #### * **areDue** - Returns an array indicating whether each of the given cards is due (in the same order). Note: cards in the learning - queue with a large interval (over 20 minutes) are treated as not due until the time of their interval has passed, to - match the way Anki treats them when reviewing. + Returns an array indicating whether each of the given cards is due (in the same order). *Note*: cards in the + learning queue with a large interval (over 20 minutes) are treated as not due until the time of their interval has + passed, to match the way Anki treats them when reviewing. *Sample request*: ```json { "action": "areDue", + "version": 5, "params": { "cards": [1483959291685, 1483959293217] } @@ -771,18 +886,22 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - [false, true] + { + "result": [false, true], + "error": null + } ``` * **getIntervals** Returns an array of the most recent intervals for each given card ID, or a 2-dimensional array of all the intervals - for each given card ID when `complete` is `true`. (Negative intervals are in seconds and positive intervals in days.) + for each given card ID when `complete` is `true`. Negative intervals are in seconds and positive intervals in days. *Sample request 1*: ```json { "action": "getIntervals", + "version": 5, "params": { "cards": [1502298033753, 1502298036657] } @@ -791,13 +910,17 @@ guarantee that your application continues to function properly in the future. *Sample result 1*: ```json - [-14400, 3] + { + "result": [-14400, 3], + "error": null + } ``` *Sample request 2*: ```json { "action": "getIntervals", + "version": 5, "params": { "cards": [1502298033753, 1502298036657], "complete": true @@ -807,22 +930,26 @@ guarantee that your application continues to function properly in the future. *Sample result 2*: ```json - [ - [-120, -180, -240, -300, -360, -14400], - [-120, -180, -240, -300, -360, -14400, 1, 3] - ] + { + "result": [ + [-120, -180, -240, -300, -360, -14400], + [-120, -180, -240, -300, -360, -14400, 1, 3] + ], + "error": null + } ``` #### Finding Notes and Cards #### * **findNotes** - Returns an array of note IDs for a given query (same query syntax as **guiBrowse**). + Returns an array of note IDs for a given query. Same query syntax as `guiBrowse`. *Sample request*: ```json { "action": "findNotes", + "version": 5, "params": { "query": "deck:current" } @@ -831,21 +958,22 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - [ - 1483959289817, - 1483959291695 - ] + { + "result": [1483959289817, 1483959291695], + "error": null + } ``` * **findCards** - Returns an array of card IDs for a given query (functionally identical to **guiBrowse** but doesn't use the GUI - for better performance). + Returns an array of card IDs for a given query. Functionally identical to `guiBrowse` but doesn't use the GUI for + better performance. *Sample request*: ```json { "action": "findCards", + "version": 5, "params": { "query": "deck:current" } @@ -854,22 +982,22 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - [ - 1494723142483, - 1494703460437, - 1494703479525 - ] + { + "result": [1494723142483, 1494703460437, 1494703479525], + "error": null + } ``` * **cardsToNotes** - Returns an (unordered) array of note IDs for the given card IDs. For cards with the same note, the ID is only - given once in the array. + Returns an unordered array of note IDs for the given card IDs. For cards with the same note, the ID is only given + once in the array. *Sample request*: ```json { "action": "cardsToNotes", + "version": 5, "params": { "cards": [1502098034045, 1502098034048, 1502298033753] } @@ -878,10 +1006,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - [ - 1502098029797, - 1502298025183 - ] + { + "result": [1502098029797, 1502298025183], + "error": null + } ``` #### Media File Storage #### @@ -890,13 +1018,14 @@ guarantee that your application continues to function properly in the future. Stores a file with the specified base64-encoded contents inside the media folder. - Note: to prevent Anki from removing files not used by any cards (e.g. for configuration files), prefix the filename - with an underscore. These files are still synchronized to AnkiWeb. + *Note*: to prevent Anki from removing files not used by any cards (e.g. for configuration files), prefix the + filename with an underscore. These files are still synchronized to AnkiWeb. *Sample request*: ```json { "action": "storeMediaFile", + "version": 5, "params": { "filename": "_hello.txt", "data": "SGVsbG8sIHdvcmxkIQ==" @@ -906,7 +1035,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - null + { + "result": null, + "error": null + } ``` *Content of `_hello.txt`*: @@ -922,6 +1054,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "retrieveMediaFile", + "version": 5, "params": { "filename": "_hello.txt" } @@ -930,7 +1063,10 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - "SGVsbG8sIHdvcmxkIQ==" + { + "result": "SGVsbG8sIHdvcmxkIQ==", + "error": null + } ``` * **deleteMediaFile** @@ -941,6 +1077,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "deleteMediaFile", + "version": 5, "params": { "filename": "_hello.txt" } @@ -949,19 +1086,24 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - null + { + "result": null, + "error": null + } ``` #### Graphical #### * **guiBrowse** - Invokes the card browser and searches for a given query. Returns an array of identifiers of the cards that were found. + Invokes the *Card Browser* dialog and searches for a given query. Returns an array of identifiers of the cards that + were found. *Sample request*: ```json { "action": "guiBrowse", + "version": 5, "params": { "query": "deck:current" } @@ -970,27 +1112,30 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - [ - 1494723142483, - 1494703460437, - 1494703479525 - ] + { + "result": [1494723142483, 1494703460437, 1494703479525], + "error": null + } ``` * **guiAddCards** - Invokes the AddCards dialog. + Invokes the *Add Cards* dialog. *Sample request*: ```json { - "action": "guiAddCards" + "action": "guiAddCards", + "version": 5 } ``` *Sample result*: ```json - null + { + "result": null, + "error": null + } ``` * **guiCurrentCard** @@ -1000,47 +1145,51 @@ guarantee that your application continues to function properly in the future. *Sample request*: ```json { - "action": "guiCurrentCard" + "action": "guiCurrentCard", + "version": 5 } ``` *Sample result*: ```json { - "answer": "back content", - "question": "front content", - "deckName": "Default", - "modelName": "Basic", - "fieldOrder": 0, - "fields": { - "Front": { - "value": "front content", - "order": 0 + "result": { + "answer": "back content", + "question": "front content", + "deckName": "Default", + "modelName": "Basic", + "fieldOrder": 0, + "fields": { + "Front": {"value": "front content", "order": 0}, + "Back": {"value": "back content", "order": 1} }, - "Back": { - "value": "back content", - "order": 1 - } + "cardId": 1498938915662, + "buttons": [1, 2, 3] }, - "cardId": 1498938915662, - "buttons": [1, 2, 3] + "error": null } ``` * **guiStartCardTimer** - Starts or resets the 'timerStarted' value for the current card. This is useful for deferring the start time to when it is displayed via the API, allowing the recorded time taken to answer the card to be more accurate when calling guiAnswerCard. + Starts or resets the `timerStarted` value for the current card. This is useful for deferring the start time to when + it is displayed via the API, allowing the recorded time taken to answer the card to be more accurate when calling + `guiAnswerCard`. *Sample request*: ```json { - "action": "guiStartCardTimer" + "action": "guiStartCardTimer", + "version": 5 } ``` *Sample result*: ```json - true + { + "result": true, + "error": null + } ``` * **guiShowQuestion** @@ -1050,13 +1199,17 @@ guarantee that your application continues to function properly in the future. *Sample request*: ```json { - "action": "guiShowQuestion" + "action": "guiShowQuestion", + "version": 5 } ``` *Sample result*: ```json - true + { + "result": true, + "error": null + } ``` * **guiShowAnswer** @@ -1066,13 +1219,17 @@ guarantee that your application continues to function properly in the future. *Sample request*: ```json { - "action": "guiShowAnswer" + "action": "guiShowAnswer", + "version": 5 } ``` *Sample result*: ```json - true + { + "result": true, + "error": null + } ``` * **guiAnswerCard** @@ -1084,6 +1241,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "guiAnswerCard", + "version": 5, "params": { "ease": 1 } @@ -1092,17 +1250,21 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - true + { + "result": true, + "error": null + } ``` * **guiDeckOverview** - Opens the Deck Overview screen for the deck with the given name; returns `true` if succeeded or `false` otherwise. + Opens the *Deck Overview* dialog for the deck with the given name; returns `true` if succeeded or `false` otherwise. *Sample request*: ```json { "action": "guiDeckOverview", + "version": 5, "params": { "name": "Default" } @@ -1111,23 +1273,30 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - true + { + "result": true, + "error": null + } ``` * **guiDeckBrowser** - Opens the Deck Browser screen. + Opens the *Deck Browser* dialog. *Sample request*: ```json { - "action": "guiDeckBrowser" + "action": "guiDeckBrowser", + "version": 5 } ``` *Sample result*: ```json - null + { + "result": null, + "error": null + } ``` * **guiDeckReview** @@ -1138,6 +1307,7 @@ guarantee that your application continues to function properly in the future. ```json { "action": "guiDeckReview", + "version": 5, "params": { "name": "Default" } @@ -1146,23 +1316,31 @@ guarantee that your application continues to function properly in the future. *Sample result*: ```json - true + { + "result": true, + "error": null + } ``` * **guiExitAnki** - Schedules a request to close Anki after 1s. This operation is asynchronous, so it will return immediately and won't wait until Anki actually exits. + Schedules a request to gracefully close Anki. This operation is asynchronous, so it will return immediately and + won't wait until the Anki process actually terminates. *Sample request*: ```json { - "action": "guiExitAnki" + "action": "guiExitAnki", + "version": 5 } ``` *Sample result*: ```json - null + { + "result": null, + "error": null + } ``` ## License ## From 6aa40a698f04367e522f12778e6f05cad1336fba Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Mon, 2 Oct 2017 09:31:31 -0700 Subject: [PATCH 59/85] Updating README.md --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index ff46222..ee65981 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,27 @@ The AnkiConnect plugin enables external applications such as [Yomichan](https:// the user's card deck, automatically create new vocabulary and Kanji flash cards, and more. AnkiConnect is compatible with the latest stable (2.0.x) and alpha (2.1.x) releases of Anki and works on Linux, Windows, and Mac OS X. +## Table of Contents ## + +* [Installation](https://foosoft.net/projects/anki-connect/#installation) + * [Notes for Windows Users](https://foosoft.net/projects/anki-connect/#notes-for-windows-users) + * [Notes for Mac OS X Users](https://foosoft.net/projects/anki-connect/#notes-for-mac-os-x-users) +* [Application Interface for Developers](https://foosoft.net/projects/anki-connect/#application-interface-for-developers) + * [Sample Invocation](https://foosoft.net/projects/anki-connect/#sample-invocation) + * [Supported Actions](https://foosoft.net/projects/anki-connect/#supported-actions) + * [Miscellaneous](https://foosoft.net/projects/anki-connect/#miscellaneous) + * [Decks](https://foosoft.net/projects/anki-connect/#decks) + * [Deck Configurations](https://foosoft.net/projects/anki-connect/#deck-configurations) + * [Models](https://foosoft.net/projects/anki-connect/#models) + * [Note Creation](https://foosoft.net/projects/anki-connect/#note-creation) + * [Note Tags](https://foosoft.net/projects/anki-connect/#note-tags) + * [Card Suspension](https://foosoft.net/projects/anki-connect/#card-suspension) + * [Card Intervals](https://foosoft.net/projects/anki-connect/#card-intervals) + * [Finding Notes and Cards](https://foosoft.net/projects/anki-connect/#finding-notes-and-cards) + * [Media File Storage](https://foosoft.net/projects/anki-connect/#media-file-storage) + * [Graphical](https://foosoft.net/projects/anki-connect/#graphical) +* [License](https://foosoft.net/projects/anki-connect/#license) + ## Installation ## The installation process is similar to that of other Anki plugins and can be accomplished in three steps: From 9ddb7eb19a559e50aceae319023bf82d177ba74d Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Sun, 8 Oct 2017 14:34:17 -0700 Subject: [PATCH 60/85] Updating README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ee65981..900b6b0 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ try { Or using [`curl`](https://curl.haxx.se) from the command line: ```bash -curl localhost:8765 -X POST -d '{"action": "deckNames", "version": 5}' +curl localhost:8765 -X POST -d "{\"action\": \"deckNames\", \"version\": 5}" ``` AnkiConnect will respond with an object containing two fields: `result` and `error`. The `result` field contains the From 612afa75a1f475acb0cf67aa2c9593fa75d7a664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C5=BEe=20Bertoncelj?= Date: Fri, 22 Dec 2017 22:42:49 +0100 Subject: [PATCH 61/85] Update API version check --- tests/test_misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_misc.py b/tests/test_misc.py index 99d9448..11c8651 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -7,4 +7,4 @@ class TestVersion(TestCase): def test_version(self): response = callAnkiConnectEndpoint({'action': 'version'}) - self.assertEqual(4, response) \ No newline at end of file + self.assertEqual(5, response) From 20f1920e062247abfcf08b3c6d936132cfe78d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C5=BEe=20Bertoncelj?= Date: Fri, 22 Dec 2017 23:53:41 +0100 Subject: [PATCH 62/85] Add a header for CORS support --- AnkiConnect.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index 436c771..ecee2fe 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -217,7 +217,8 @@ class AjaxServer: def resetHeaders(self): self.headers = [ ['HTTP/1.1 200 OK', None], - ['Content-Type', 'text/json'] + ['Content-Type', 'text/json'], + ['Access-Control-Allow-Origin', '*'] ] self.extraHeaders = {} From 7de12f7d4b14acba6d989360eaef9b75efc9d571 Mon Sep 17 00:00:00 2001 From: Jingtao Xu Date: Mon, 1 Jan 2018 10:34:38 +0800 Subject: [PATCH 63/85] add support to modify fields of exist note. --- AnkiConnect.py | 15 +++++++++++++++ README.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/AnkiConnect.py b/AnkiConnect.py index 436c771..56465d4 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -424,6 +424,18 @@ class AnkiBridge: if not note.dupeOrEmpty(): return note + def updateNoteFields(self, params): + collection = self.collection() + if collection is None: + return + + note = collection.getNote(params['id']) + if note is None: + raise Exception("Failed to get note:{}".format(params['id'])) + for name, value in params['fields'].items(): + if name in note: + note[name] = value + note.flush() def addTags(self, notes, tags, add=True): self.startEditing() @@ -1023,6 +1035,9 @@ class AnkiConnect: return results + @webApi() + def updateNoteFields(self, note): + return self.anki.updateNoteFields(note) @webApi() def canAddNotes(self, notes): diff --git a/README.md b/README.md index 900b6b0..6e1c676 100644 --- a/README.md +++ b/README.md @@ -763,6 +763,38 @@ guarantee that your application continues to function properly in the future. } ``` +#### Note Modification #### + +* **updateNoteFields** + + Modify the fields of an exist note. + + *Sample request*: + ```json + { + "action": "updateNoteFields", + "version": 5, + "params": { + "note": { + "id": 1514547547030, + "fields": { + "Front": "new front content", + "Back": "new back content" + }, + } + } + } + ``` + + *Sample result*: + ```json + { + "result": null, + "error": null + } + ``` + + #### Note Tags #### * **addTags** From 63c2eb4887b42a18da6a2e68844c8e92d4a83bbf Mon Sep 17 00:00:00 2001 From: ReneBrals Date: Sat, 6 Jan 2018 20:48:25 +0100 Subject: [PATCH 64/85] Added API endpoint to retrieve card/note information. --- AnkiConnect.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/AnkiConnect.py b/AnkiConnect.py index 56465d4..8c752fe 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -29,6 +29,7 @@ import socket import sys from time import time from unicodedata import normalize +from operator import itemgetter # @@ -706,6 +707,80 @@ class AnkiBridge: else: return [] + def cardsInfo(self,cards): + result = [] + for cid in cards: + try: + card = self.collection().getCard(cid) + model = card.model() + note = card.note() + fields = {} + for info in model['flds']: + order = info['ord'] + name = info['name'] + fields[name] = note.fields[order] + + result.append({ + 'cardId': card.id, + 'fields': fields, + 'fieldOrder': card.ord, + 'question': card._getQA()['q'], + 'answer': card._getQA()['a'], + 'modelName': model['name'], + 'deckName': self.deckNameFromId(card.did), + 'css': model['css'], + 'factor': card.factor, + #This factor is 10 times the ease percentage, + # so an ease of 310% would be reported as 3100 + 'interval': card.ivl, + 'type': card.type, + #new = 0, learning = 1, review = 2 + 'queue': card.queue, + # same as type, plus: + # suspended = -1, user buried = -2, sched buried = -3 + 'due': card.due, + # does different things depending on queue, + # new: position in queue + # learning: integer timestamp + # review: integer days (since first review) + 'note': card.nid + }) + except TypeError as e: + # Anki will give a TypeError if the card ID does not exist. + # Best behavior is probably to add an "empty card" to the + # returned result, so that the items of the input and return + # lists correspond. + result.append({}) + + return result + + def notesInfo(self,notes): + result = [] + for nid in notes: + try: + note = self.collection().getNote(nid) + model = note.model() + + fields = {} + for info in model['flds']: + order = info['ord'] + name = info['name'] + fields[name] = note.fields[order] + + result.append({ + 'noteId': note.id, + 'tags' : note.tags, + 'fields': fields, + 'modelName': model['name'], + }) + except TypeError as e: + # Anki will give a TypeError if the note ID does not exist. + # Best behavior is probably to add an "empty card" to the + # returned result, so that the items of the input and return + # lists correspond. + result.append({}) + return result + def getDecks(self, cards): decks = {} @@ -1196,6 +1271,13 @@ class AnkiConnect: def guiExitAnki(self): return self.anki.guiExitAnki() + @webApi() + def cardsInfo(self, cards): + return self.anki.cardsInfo(cards) + + @webApi() + def notesInfo(self, notes): + return self.anki.notesInfo(notes) # # Entry From cd6dd8479c381d15ef18fe6f55d81360c01fe98a Mon Sep 17 00:00:00 2001 From: ReneBrals Date: Tue, 9 Jan 2018 14:09:38 +0100 Subject: [PATCH 65/85] Make note/card endpoints more like existing endpoints, removing some Anki-internal fields from it card.type, card.queue and card.due seem difficult to use to me, maybe card due date should be introduced when it's represented in a more meaningful way (like an actual date instead of an integer day). --- AnkiConnect.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index 8c752fe..8997f9a 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -718,7 +718,7 @@ class AnkiBridge: for info in model['flds']: order = info['ord'] name = info['name'] - fields[name] = note.fields[order] + fields[name] = {'value': note.fields[order], 'order': order} result.append({ 'cardId': card.id, @@ -733,16 +733,6 @@ class AnkiBridge: #This factor is 10 times the ease percentage, # so an ease of 310% would be reported as 3100 'interval': card.ivl, - 'type': card.type, - #new = 0, learning = 1, review = 2 - 'queue': card.queue, - # same as type, plus: - # suspended = -1, user buried = -2, sched buried = -3 - 'due': card.due, - # does different things depending on queue, - # new: position in queue - # learning: integer timestamp - # review: integer days (since first review) 'note': card.nid }) except TypeError as e: @@ -765,13 +755,14 @@ class AnkiBridge: for info in model['flds']: order = info['ord'] name = info['name'] - fields[name] = note.fields[order] + fields[name] = {'value': note.fields[order], 'order': order} result.append({ 'noteId': note.id, 'tags' : note.tags, 'fields': fields, 'modelName': model['name'], + 'cards': note.cards() }) except TypeError as e: # Anki will give a TypeError if the note ID does not exist. From 2d051c67c1196cae31e797f452022bf13534f6c1 Mon Sep 17 00:00:00 2001 From: ReneBrals Date: Tue, 9 Jan 2018 14:24:56 +0100 Subject: [PATCH 66/85] Fixed notesInfo fetching card IDs belonging to note --- AnkiConnect.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index 8997f9a..e051c16 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -762,7 +762,8 @@ class AnkiBridge: 'tags' : note.tags, 'fields': fields, 'modelName': model['name'], - 'cards': note.cards() + 'cards': self.collection().db.list( + "select id from cards where nid = ? order by ord", self.id) }) except TypeError as e: # Anki will give a TypeError if the note ID does not exist. From 150c49d273889554782b4ac17e8fff09a757b1da Mon Sep 17 00:00:00 2001 From: ReneBrals Date: Tue, 9 Jan 2018 18:40:07 +0100 Subject: [PATCH 67/85] Updated README to include notesInfo and cardsInfo --- README.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/README.md b/README.md index 6e1c676..3b45ecf 100644 --- a/README.md +++ b/README.md @@ -1065,6 +1065,92 @@ guarantee that your application continues to function properly in the future. } ``` +* **cardsInfo** + + Returns a list of objects containing for each card ID the card fields, front and back sides including CSS, note type, the note that the card belongs to, and deck name, as well as ease and interval. + + *Sample request*: + ```json + { + "action": "cardsInfo", + "version": 5, + "params": { + "cards": [1498938915662, 1502098034048] + } + } + ``` + + *Sample result*: + ```json + { + "result": [ + { + "answer": "back content", + "question": "front content", + "deckName": "Default", + "modelName": "Basic", + "fieldOrder": 1, + "fields": { + "Front": {"value": "front content", "order": 0}, + "Back": {"value": "back content", "order": 1} + }, + "css":"p {font-family:Arial;}", + "cardId": 1498938915662, + "interval": 16 + "note":1502298033753 + }, + { + "answer": "back content", + "question": "front content", + "deckName": "Default", + "modelName": "Basic", + "fieldOrder": 0, + "fields": { + "Front": {"value": "front content", "order": 0}, + "Back": {"value": "back content", "order": 1} + }, + "css":"p {font-family:Arial;}", + "cardId": 1502098034048, + "interval": 23 + "note":1502298033753 + } + ], + "error": null + } + ``` + +* **notesInfo** + + Returns a list of objects containing for each note ID the note fields, tags, note type and the cards belonging to the note. + + *Sample request*: + ```json + { + "action": "notesInfo", + "version": 5, + "params": { + } + } + ``` + + *Sample result*: + ```json + { + "result": [ + { + "noteId":1502298033753, + "modelName": "Basic", + "tags":['tag','another_tag'] + "fields": { + "Front": {"value": "front content", "order": 0}, + "Back": {"value": "back content", "order": 1} + }, + } + ], + "error": null + } + ``` + #### Media File Storage #### * **storeMediaFile** From 4411c4969f92cd54dc16398764146a6182d55981 Mon Sep 17 00:00:00 2001 From: ReneBrals Date: Tue, 9 Jan 2018 18:40:44 +0100 Subject: [PATCH 68/85] Fixed Typo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3b45ecf..42f87f4 100644 --- a/README.md +++ b/README.md @@ -1129,6 +1129,7 @@ guarantee that your application continues to function properly in the future. "action": "notesInfo", "version": 5, "params": { + "notes": [1502298033753] } } ``` From 6d7b525bad2ffac70adec219ec60b3761e102924 Mon Sep 17 00:00:00 2001 From: ReneBrals Date: Tue, 9 Jan 2018 18:44:50 +0100 Subject: [PATCH 69/85] Fixed another typo --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 42f87f4..489dd82 100644 --- a/README.md +++ b/README.md @@ -1096,7 +1096,7 @@ guarantee that your application continues to function properly in the future. }, "css":"p {font-family:Arial;}", "cardId": 1498938915662, - "interval": 16 + "interval": 16, "note":1502298033753 }, { @@ -1111,7 +1111,7 @@ guarantee that your application continues to function properly in the future. }, "css":"p {font-family:Arial;}", "cardId": 1502098034048, - "interval": 23 + "interval": 23, "note":1502298033753 } ], @@ -1141,7 +1141,6 @@ guarantee that your application continues to function properly in the future. { "noteId":1502298033753, "modelName": "Basic", - "tags":['tag','another_tag'] "fields": { "Front": {"value": "front content", "order": 0}, "Back": {"value": "back content", "order": 1} From 05b4e9ab8ddb184a4e5542d00ce6df1e9f6dfc4c Mon Sep 17 00:00:00 2001 From: ReneBrals Date: Tue, 9 Jan 2018 18:46:23 +0100 Subject: [PATCH 70/85] Missed a typo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 489dd82..1634948 100644 --- a/README.md +++ b/README.md @@ -1141,6 +1141,7 @@ guarantee that your application continues to function properly in the future. { "noteId":1502298033753, "modelName": "Basic", + "tags":["tag","another_tag"], "fields": { "Front": {"value": "front content", "order": 0}, "Back": {"value": "back content", "order": 1} From 4c96cc49374a2fbb56439c3d3078279000178238 Mon Sep 17 00:00:00 2001 From: louietan Date: Sun, 14 Jan 2018 17:59:17 +0800 Subject: [PATCH 71/85] Fix issue #64 --- AnkiConnect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index 556e10c..82a69f2 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -764,7 +764,7 @@ class AnkiBridge: 'fields': fields, 'modelName': model['name'], 'cards': self.collection().db.list( - "select id from cards where nid = ? order by ord", self.id) + "select id from cards where nid = ? order by ord", note.id) }) except TypeError as e: # Anki will give a TypeError if the note ID does not exist. From 88b375d4a891565390cc66d2a044cc89f87d7f05 Mon Sep 17 00:00:00 2001 From: louie Date: Sun, 14 Jan 2018 19:26:37 +0800 Subject: [PATCH 72/85] Add getTags action --- AnkiConnect.py | 9 +++++++++ README.md | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/AnkiConnect.py b/AnkiConnect.py index 556e10c..02959a3 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -445,6 +445,10 @@ class AnkiBridge: self.stopEditing() + def getTags(self): + return self.collection().tags.all() + + def suspend(self, cards, suspend=True): for card in cards: isSuspended = self.isSuspended(card) @@ -1127,6 +1131,11 @@ class AnkiConnect: return self.anki.addTags(notes, tags, False) + @webApi() + def getTags(self): + return self.anki.getTags() + + @webApi() def suspend(self, cards, suspend=True): return self.anki.suspend(cards, suspend) diff --git a/README.md b/README.md index 1634948..3fd4945 100644 --- a/README.md +++ b/README.md @@ -845,6 +845,26 @@ guarantee that your application continues to function properly in the future. } ``` +* **getTags** + + Gets the complete list of tags for the current user. + + *Sample request*: + ```json + { + "action": "getTags", + "version": 5 + } + ``` + + *Sample result*: + ```json + { + "result": ["european-languages", "idioms"], + "error": null + } + ``` + #### Card Suspension #### * **suspend** From c2f7acf9280340e969cbc0057ba41af3a176975c Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Fri, 26 Jan 2018 15:18:44 -0800 Subject: [PATCH 73/85] Updating README.md --- README.md | 153 +++++++++++++++++++++++++----------------------------- 1 file changed, 70 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 3fd4945..5b51e71 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,10 @@ with the latest stable (2.0.x) and alpha (2.1.x) releases of Anki and works on L * [Supported Actions](https://foosoft.net/projects/anki-connect/#supported-actions) * [Miscellaneous](https://foosoft.net/projects/anki-connect/#miscellaneous) * [Decks](https://foosoft.net/projects/anki-connect/#decks) - * [Deck Configurations](https://foosoft.net/projects/anki-connect/#deck-configurations) * [Models](https://foosoft.net/projects/anki-connect/#models) - * [Note Creation](https://foosoft.net/projects/anki-connect/#note-creation) - * [Note Tags](https://foosoft.net/projects/anki-connect/#note-tags) - * [Card Suspension](https://foosoft.net/projects/anki-connect/#card-suspension) - * [Card Intervals](https://foosoft.net/projects/anki-connect/#card-intervals) - * [Finding Notes and Cards](https://foosoft.net/projects/anki-connect/#finding-notes-and-cards) - * [Media File Storage](https://foosoft.net/projects/anki-connect/#media-file-storage) + * [Notes](https://foosoft.net/projects/anki-connect/#notes) + * [Cards](https://foosoft.net/projects/anki-connect/#cards) + * [Media](https://foosoft.net/projects/anki-connect/#media) * [Graphical](https://foosoft.net/projects/anki-connect/#graphical) * [License](https://foosoft.net/projects/anki-connect/#license) @@ -345,8 +341,6 @@ guarantee that your application continues to function properly in the future. } ``` -#### Deck Configurations #### - * **getDeckConfig** Gets the configuration group object for the given deck. @@ -636,7 +630,7 @@ guarantee that your application continues to function properly in the future. } ``` -#### Note Creation #### +#### Notes #### * **addNote** @@ -763,8 +757,6 @@ guarantee that your application continues to function properly in the future. } ``` -#### Note Modification #### - * **updateNoteFields** Modify the fields of an exist note. @@ -780,7 +772,7 @@ guarantee that your application continues to function properly in the future. "fields": { "Front": "new front content", "Back": "new back content" - }, + } } } } @@ -794,9 +786,6 @@ guarantee that your application continues to function properly in the future. } ``` - -#### Note Tags #### - * **addTags** Adds tags to notes by note ID. @@ -865,7 +854,65 @@ guarantee that your application continues to function properly in the future. } ``` -#### Card Suspension #### +* **findNotes** + + Returns an array of note IDs for a given query. Same query syntax as `guiBrowse`. + + *Sample request*: + ```json + { + "action": "findNotes", + "version": 5, + "params": { + "query": "deck:current" + } + } + ``` + + *Sample result*: + ```json + { + "result": [1483959289817, 1483959291695], + "error": null + } + ``` + +* **notesInfo** + + Returns a list of objects containing for each note ID the note fields, tags, note type and the cards belonging to + the note. + + *Sample request*: + ```json + { + "action": "notesInfo", + "version": 5, + "params": { + "notes": [1502298033753] + } + } + ``` + + *Sample result*: + ```json + { + "result": [ + { + "noteId":1502298033753, + "modelName": "Basic", + "tags":["tag","another_tag"], + "fields": { + "Front": {"value": "front content", "order": 0}, + "Back": {"value": "back content", "order": 1} + } + } + ], + "error": null + } + ``` + + +#### Cards #### * **suspend** @@ -938,8 +985,6 @@ guarantee that your application continues to function properly in the future. } ``` -#### Card Intervals #### - * **areDue** Returns an array indicating whether each of the given cards is due (in the same order). *Note*: cards in the @@ -1012,31 +1057,6 @@ guarantee that your application continues to function properly in the future. } ``` -#### Finding Notes and Cards #### - -* **findNotes** - - Returns an array of note IDs for a given query. Same query syntax as `guiBrowse`. - - *Sample request*: - ```json - { - "action": "findNotes", - "version": 5, - "params": { - "query": "deck:current" - } - } - ``` - - *Sample result*: - ```json - { - "result": [1483959289817, 1483959291695], - "error": null - } - ``` - * **findCards** Returns an array of card IDs for a given query. Functionally identical to `guiBrowse` but doesn't use the GUI for @@ -1087,7 +1107,8 @@ guarantee that your application continues to function properly in the future. * **cardsInfo** - Returns a list of objects containing for each card ID the card fields, front and back sides including CSS, note type, the note that the card belongs to, and deck name, as well as ease and interval. + Returns a list of objects containing for each card ID the card fields, front and back sides including CSS, note + type, the note that the card belongs to, and deck name, as well as ease and interval. *Sample request*: ```json @@ -1139,47 +1160,13 @@ guarantee that your application continues to function properly in the future. } ``` -* **notesInfo** - - Returns a list of objects containing for each note ID the note fields, tags, note type and the cards belonging to the note. - - *Sample request*: - ```json - { - "action": "notesInfo", - "version": 5, - "params": { - "notes": [1502298033753] - } - } - ``` - - *Sample result*: - ```json - { - "result": [ - { - "noteId":1502298033753, - "modelName": "Basic", - "tags":["tag","another_tag"], - "fields": { - "Front": {"value": "front content", "order": 0}, - "Back": {"value": "back content", "order": 1} - }, - } - ], - "error": null - } - ``` - -#### Media File Storage #### +#### Media #### * **storeMediaFile** - Stores a file with the specified base64-encoded contents inside the media folder. - - *Note*: to prevent Anki from removing files not used by any cards (e.g. for configuration files), prefix the - filename with an underscore. These files are still synchronized to AnkiWeb. + Stores a file with the specified base64-encoded contents inside the media folder. To prevent Anki from removing + files not used by any cards (e.g. for configuration files), prefix the filename with an underscore. These files are + still synchronized to AnkiWeb. *Sample request*: ```json From a16bd9434122e13c6ff0ade866eb450d2bb0f5fb Mon Sep 17 00:00:00 2001 From: Sudarshan Gaikaiwari Date: Wed, 21 Feb 2018 07:16:52 -0600 Subject: [PATCH 74/85] Expose Anki sync via AnkiConnect --- AnkiConnect.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/AnkiConnect.py b/AnkiConnect.py index e9cabcd..5280dd4 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -947,10 +947,14 @@ class AnkiBridge: timer.timeout.connect(exitAnki) timer.start(1000) # 1s should be enough to allow the response to be sent. + def sync(self): + self.window()._sync() + # # AnkiConnect # + class AnkiConnect: def __init__(self): self.anki = AnkiBridge() @@ -1281,6 +1285,11 @@ class AnkiConnect: def notesInfo(self, notes): return self.anki.notesInfo(notes) + @webApi() + def sync(self): + return self.anki.sync() + + # # Entry # From 834b5ace7b70454444114f6c9f1c5bae7a506000 Mon Sep 17 00:00:00 2001 From: Sudarshan Gaikaiwari Date: Wed, 21 Feb 2018 07:28:28 -0600 Subject: [PATCH 75/85] Calls onSync to prevent the corruption of collection message --- AnkiConnect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index 5280dd4..5a96d47 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -948,7 +948,7 @@ class AnkiBridge: timer.start(1000) # 1s should be enough to allow the response to be sent. def sync(self): - self.window()._sync() + self.window().onSync() # # AnkiConnect From 8d1fb1989bd9c01cb173a15f7ffa413c8853f55e Mon Sep 17 00:00:00 2001 From: Sudarshan Gaikaiwari Date: Wed, 21 Feb 2018 11:01:37 -0600 Subject: [PATCH 76/85] updates README.md --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 5b51e71..8c8cad4 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,27 @@ guarantee that your application continues to function properly in the future. } ``` +* **sync** + + Synchronizes the local anki collections with ankiweb. + + *Sample request*: + ```json + { + "action": "sync", + "version": 5 + + } + ``` + + *Sample result*: + ```json + { + "result": null, + "error": null + } + + * **multi** Performs multiple actions in one request, returning an array with the response of each action (in the given order). From c4fef2d0b26c8668809616d0e599a31dc5207b16 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Wed, 21 Feb 2018 19:14:50 -0800 Subject: [PATCH 77/85] Updating README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8c8cad4..1a196e4 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ guarantee that your application continues to function properly in the future. * **sync** Synchronizes the local anki collections with ankiweb. - + *Sample request*: ```json { @@ -210,7 +210,7 @@ guarantee that your application continues to function properly in the future. "result": null, "error": null } - + ``` * **multi** From 64a0a9d6a62fa5e3cb3d27a6bae223e3189c3431 Mon Sep 17 00:00:00 2001 From: c-okelly Date: Thu, 22 Feb 2018 18:34:12 +0000 Subject: [PATCH 78/85] Add createDeck action --- AnkiConnect.py | 10 ++++++++++ README.md | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/AnkiConnect.py b/AnkiConnect.py index 5a96d47..caa7fdd 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -793,6 +793,13 @@ class AnkiBridge: return decks + def createDeck(self, deck): + self.startEditing() + deckId = self.collection().decks.id(deck) + self.stopEditing() + + return deckId + def changeDeck(self, cards, deck): self.startEditing() @@ -1207,6 +1214,9 @@ class AnkiConnect: def getDecks(self, cards): return self.anki.getDecks(cards) + @webApi() + def createDeck(self, deck): + return self.anki.createDeck(deck) @webApi() def changeDeck(self, cards, deck): diff --git a/README.md b/README.md index 1a196e4..cb7b8f6 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,28 @@ guarantee that your application continues to function properly in the future. } ``` +* **createDeck** + + Create a new empty deck. Will not overwrite a deck that exists with the same name. + + *Sample request*: + ```json + { + "action": "createDeck", + "version": 5, + "params": { + "deck": "Japanese::Tokyo" + } + } + ``` + + *Sample result*: + ```json + { + "result": 1519323742721, + "error": null + } + ``` * **changeDeck** Moves cards with the given IDs to a different deck, creating the deck if it doesn't exist yet. From 5d23ef138590efb5dc155e023e2e5d9c6bcb6e82 Mon Sep 17 00:00:00 2001 From: c-okelly Date: Sat, 10 Mar 2018 01:04:08 +0000 Subject: [PATCH 79/85] Raise exception with relevant error messages when an error occurred during action. --- AnkiConnect.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index caa7fdd..2710ab7 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -89,11 +89,11 @@ def makeStr(data): def download(url): try: resp = web.urlopen(url, timeout=URL_TIMEOUT) - except web.URLError: - return None + except web.URLError as e: + raise Exception("A urlError has occoured for url " + url + ". Error messages was: " + e.message) if resp.code != 200: - return None + raise Exception("Return conde for url request" + url + "was not 200. Error code: " + resp.code) return resp.read() @@ -370,11 +370,11 @@ class AnkiBridge: def addNote(self, params): collection = self.collection() if collection is None: - return + raise Exception("Collection was not found.") note = self.createNote(params) if note is None: - return + raise Exception("Failed to create note from params" + str(params)) if params.audio is not None and len(params.audio.fields) > 0: data = download(params.audio.url) @@ -405,15 +405,15 @@ class AnkiBridge: def createNote(self, params): collection = self.collection() if collection is None: - return + raise Exception("Collection was not found.") model = collection.models.byName(params.modelName) if model is None: - return + raise Exception("Model was not found for model: " + str(params.modelName)) deck = collection.decks.byName(params.deckName) if deck is None: - return + raise Exception("Deck was not found for deck: " + str(params.deckName)) note = anki.notes.Note(collection, model) note.model()['did'] = deck['id'] @@ -429,7 +429,7 @@ class AnkiBridge: def updateNoteFields(self, params): collection = self.collection() if collection is None: - return + raise Exception("Collection was not found.") note = collection.getNote(params['id']) if note is None: @@ -854,7 +854,7 @@ class AnkiBridge: def guiCurrentCard(self): if not self.guiReviewActive(): - return + raise Exception("Gui review is not currently active.") reviewer = self.reviewer() card = reviewer.card From d79dd1408b4f81517fcc54e0d0383d426077a538 Mon Sep 17 00:00:00 2001 From: c-okelly Date: Sun, 11 Mar 2018 11:55:47 +0000 Subject: [PATCH 80/85] Adding string method for AnkiNoteParams class. Expanded error catching for addNote. addNotes can now handle when one card of an array throws and error and continue adding cards. --- AnkiConnect.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index 2710ab7..cc4d9be 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -337,7 +337,9 @@ class AnkiNoteParams: type(self.fields) == dict and verifyStringList(list(self.fields.keys())) and verifyStringList(list(self.fields.values())) and type(self.tags) == list and verifyStringList(self.tags) ) - + + def __str__(self): + return "DeckName: " + self.deckName + ". ModelName: " + self.modelName + ". Fields: " + str(self.fields) + ". Tags: " + str(self.tags) + "." # # AnkiBridge @@ -374,7 +376,7 @@ class AnkiBridge: note = self.createNote(params) if note is None: - raise Exception("Failed to create note from params" + str(params)) + raise Exception("Failed to create note from params: " + str(params)) if params.audio is not None and len(params.audio.fields) > 0: data = download(params.audio.url) @@ -409,11 +411,11 @@ class AnkiBridge: model = collection.models.byName(params.modelName) if model is None: - raise Exception("Model was not found for model: " + str(params.modelName)) + raise Exception("Model was not found for model: " + params.modelName) deck = collection.decks.byName(params.deckName) if deck is None: - raise Exception("Deck was not found for deck: " + str(params.deckName)) + raise Exception("Deck was not found for deck: " + params.deckName) note = anki.notes.Note(collection, model) note.model()['did'] = deck['id'] @@ -423,7 +425,13 @@ class AnkiBridge: if name in note: note[name] = value - if not note.dupeOrEmpty(): + # Returns 1 if empty. 2 if duplicate. Otherwise returns False + duplicateOrEmpty = note.dupeOrEmpty() + if duplicateOrEmpty == 1: + raise Exception("Note was empty. Param were: " + str(params)) + elif duplicateOrEmpty == 2: + raise Exception("Note is duplicate of existing note. Params were: " + str(params)) + elif duplicateOrEmpty == False: return note def updateNoteFields(self, params): @@ -1110,11 +1118,14 @@ class AnkiConnect: def addNotes(self, notes): results = [] for note in notes: - params = AnkiNoteParams(note) - if params.validate(): - results.append(self.anki.addNote(params)) - else: - results.append(None) + try: + params = AnkiNoteParams(note) + if params.validate(): + results.append(self.anki.addNote(params)) + else: + results.append(None) + except Exception as e: + results.append(str(e)) return results From 30b70dca68180138a264de2db69c3125c2570068 Mon Sep 17 00:00:00 2001 From: c-okelly Date: Sun, 11 Mar 2018 12:27:19 +0000 Subject: [PATCH 81/85] Add back isSuspended method --- AnkiConnect.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index caa7fdd..6976928 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -468,15 +468,17 @@ class AnkiBridge: return False + def isSuspended(self, card): + card = self.collection().getCard(card) + if card.queue == -1: + return True + else: + return False def areSuspended(self, cards): suspended = [] for card in cards: - card = self.collection().getCard(card) - if card.queue == -1: - suspended.append(True) - else: - suspended.append(False) + suspended.append(self.isSuspended(card)) return suspended From f68b8e06efb9fc76b23ee2e16b2f5b9efb5bbd36 Mon Sep 17 00:00:00 2001 From: c-okelly Date: Sun, 11 Mar 2018 22:10:07 +0000 Subject: [PATCH 82/85] Pull requests corrections. --- AnkiConnect.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index cc4d9be..63aeb76 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -90,10 +90,10 @@ def download(url): try: resp = web.urlopen(url, timeout=URL_TIMEOUT) except web.URLError as e: - raise Exception("A urlError has occoured for url " + url + ". Error messages was: " + e.message) + raise Exception('A urlError has occurred for url ' + url + '. Error messages was: ' + e.message) if resp.code != 200: - raise Exception("Return conde for url request" + url + "was not 200. Error code: " + resp.code) + raise Exception('Return code for url request' + url + 'was not 200. Error code: ' + resp.code) return resp.read() @@ -339,7 +339,7 @@ class AnkiNoteParams: ) def __str__(self): - return "DeckName: " + self.deckName + ". ModelName: " + self.modelName + ". Fields: " + str(self.fields) + ". Tags: " + str(self.tags) + "." + return 'DeckName: ' + self.deckName + '. ModelName: ' + self.modelName + '. Fields: ' + str(self.fields) + '. Tags: ' + str(self.tags) + '.' # # AnkiBridge @@ -354,7 +354,7 @@ class AnkiBridge: def retrieveMediaFile(self, filename): # based on writeData from anki/media.py filename = os.path.basename(filename) - filename = normalize("NFC", filename) + filename = normalize('NFC', filename) filename = self.media().stripIllegal(filename) path = os.path.join(self.media().dir(), filename) @@ -372,11 +372,11 @@ class AnkiBridge: def addNote(self, params): collection = self.collection() if collection is None: - raise Exception("Collection was not found.") + raise Exception('Collection was not found.') note = self.createNote(params) if note is None: - raise Exception("Failed to create note from params: " + str(params)) + raise Exception('Failed to create note from params: ' + str(params)) if params.audio is not None and len(params.audio.fields) > 0: data = download(params.audio.url) @@ -407,15 +407,15 @@ class AnkiBridge: def createNote(self, params): collection = self.collection() if collection is None: - raise Exception("Collection was not found.") + raise Exception('Collection was not found.') model = collection.models.byName(params.modelName) if model is None: - raise Exception("Model was not found for model: " + params.modelName) + raise Exception('Model was not found for model: ' + params.modelName) deck = collection.decks.byName(params.deckName) if deck is None: - raise Exception("Deck was not found for deck: " + params.deckName) + raise Exception('Deck was not found for deck: ' + params.deckName) note = anki.notes.Note(collection, model) note.model()['did'] = deck['id'] @@ -428,20 +428,20 @@ class AnkiBridge: # Returns 1 if empty. 2 if duplicate. Otherwise returns False duplicateOrEmpty = note.dupeOrEmpty() if duplicateOrEmpty == 1: - raise Exception("Note was empty. Param were: " + str(params)) + raise Exception('Note was empty. Param were: ' + str(params)) elif duplicateOrEmpty == 2: - raise Exception("Note is duplicate of existing note. Params were: " + str(params)) + raise Exception('Note is duplicate of existing note. Params were: ' + str(params)) elif duplicateOrEmpty == False: return note def updateNoteFields(self, params): collection = self.collection() if collection is None: - raise Exception("Collection was not found.") + raise Exception('Collection was not found.') note = collection.getNote(params['id']) if note is None: - raise Exception("Failed to get note:{}".format(params['id'])) + raise Exception('Failed to get note:{}'.format(params['id'])) for name, value in params['fields'].items(): if name in note: note[name] = value @@ -612,7 +612,7 @@ class AnkiBridge: for match in matches: # remove braces and modifiers match = re.sub(r'[{}]', '', match) - match = match.split(":")[-1] + match = match.split(':')[-1] # for the answer side, ignore fields present on the question side + the FrontSide field if match == 'FrontSide' or side == 'afmt' and match in fields[0]: @@ -750,7 +750,7 @@ class AnkiBridge: }) except TypeError as e: # Anki will give a TypeError if the card ID does not exist. - # Best behavior is probably to add an "empty card" to the + # Best behavior is probably to add an 'empty card' to the # returned result, so that the items of the input and return # lists correspond. result.append({}) @@ -776,11 +776,11 @@ class AnkiBridge: 'fields': fields, 'modelName': model['name'], 'cards': self.collection().db.list( - "select id from cards where nid = ? order by ord", note.id) + 'select id from cards where nid = ? order by ord', note.id) }) except TypeError as e: # Anki will give a TypeError if the note ID does not exist. - # Best behavior is probably to add an "empty card" to the + # Best behavior is probably to add an 'empty card' to the # returned result, so that the items of the input and return # lists correspond. result.append({}) @@ -862,7 +862,7 @@ class AnkiBridge: def guiCurrentCard(self): if not self.guiReviewActive(): - raise Exception("Gui review is not currently active.") + raise Exception('Gui review is not currently active.') reviewer = self.reviewer() card = reviewer.card @@ -1124,8 +1124,8 @@ class AnkiConnect: results.append(self.anki.addNote(params)) else: results.append(None) - except Exception as e: - results.append(str(e)) + except Exception: + results.append(None) return results From 13abf38ffbc2d18de5d4e44b4685b56cfd46e281 Mon Sep 17 00:00:00 2001 From: Alvaro Calace Date: Fri, 16 Mar 2018 09:51:48 -0300 Subject: [PATCH 83/85] Catch exception when checking if a note can be created --- AnkiConnect.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index 7dca7c1..f0ac191 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -401,7 +401,10 @@ class AnkiBridge: def canAddNote(self, note): - return bool(self.createNote(note)) + try: + return bool(self.createNote(note)) + except: + return False def createNote(self, params): From d3627e306018ed199b6a824ff19d6512b3816c86 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Sat, 31 Mar 2018 14:30:58 -0700 Subject: [PATCH 84/85] ignore zip file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0d20b64..b9b667b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.pyc +AnkiConnect.zip From 0efa20309550935cd2c1f669e57fa56e045d5a06 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Sat, 31 Mar 2018 14:40:01 -0700 Subject: [PATCH 85/85] cleanup --- AnkiConnect.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/AnkiConnect.py b/AnkiConnect.py index f0ac191..5699e22 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -117,7 +117,6 @@ def verifyStringList(strings): return True - # # AjaxRequest # @@ -337,7 +336,8 @@ class AnkiNoteParams: type(self.fields) == dict and verifyStringList(list(self.fields.keys())) and verifyStringList(list(self.fields.values())) and type(self.tags) == list and verifyStringList(self.tags) ) - + + def __str__(self): return 'DeckName: ' + self.deckName + '. ModelName: ' + self.modelName + '. Fields: ' + str(self.fields) + '. Tags: ' + str(self.tags) + '.' @@ -437,6 +437,7 @@ class AnkiBridge: elif duplicateOrEmpty == False: return note + def updateNoteFields(self, params): collection = self.collection() if collection is None: @@ -450,6 +451,7 @@ class AnkiBridge: note[name] = value note.flush() + def addTags(self, notes, tags, add=True): self.startEditing() self.collection().tags.bulkAdd(notes, tags, add) @@ -479,6 +481,7 @@ class AnkiBridge: return False + def isSuspended(self, card): card = self.collection().getCard(card) if card.queue == -1: @@ -486,6 +489,7 @@ class AnkiBridge: else: return False + def areSuspended(self, cards): suspended = [] for card in cards: @@ -725,6 +729,7 @@ class AnkiBridge: else: return [] + def cardsInfo(self,cards): result = [] for cid in cards: @@ -737,7 +742,7 @@ class AnkiBridge: order = info['ord'] name = info['name'] fields[name] = {'value': note.fields[order], 'order': order} - + result.append({ 'cardId': card.id, 'fields': fields, @@ -747,8 +752,8 @@ class AnkiBridge: 'modelName': model['name'], 'deckName': self.deckNameFromId(card.did), 'css': model['css'], - 'factor': card.factor, - #This factor is 10 times the ease percentage, + 'factor': card.factor, + #This factor is 10 times the ease percentage, # so an ease of 310% would be reported as 3100 'interval': card.ivl, 'note': card.nid @@ -762,6 +767,7 @@ class AnkiBridge: return result + def notesInfo(self,notes): result = [] for nid in notes: @@ -774,7 +780,7 @@ class AnkiBridge: order = info['ord'] name = info['name'] fields[name] = {'value': note.fields[order], 'order': order} - + result.append({ 'noteId': note.id, 'tags' : note.tags, @@ -813,6 +819,7 @@ class AnkiBridge: return deckId + def changeDeck(self, cards, deck): self.startEditing() @@ -906,6 +913,7 @@ class AnkiBridge: else: return False + def guiShowQuestion(self): if self.guiReviewActive(): self.reviewer()._showQuestion() @@ -959,6 +967,7 @@ class AnkiBridge: else: return False + def guiExitAnki(self): timer = QTimer() def exitAnki(): @@ -967,9 +976,11 @@ class AnkiBridge: timer.timeout.connect(exitAnki) timer.start(1000) # 1s should be enough to allow the response to be sent. + def sync(self): self.window().onSync() + # # AnkiConnect # @@ -1134,10 +1145,12 @@ class AnkiConnect: return results + @webApi() def updateNoteFields(self, note): return self.anki.updateNoteFields(note) + @webApi() def canAddNotes(self, notes): results = [] @@ -1230,10 +1243,12 @@ class AnkiConnect: def getDecks(self, cards): return self.anki.getDecks(cards) + @webApi() def createDeck(self, deck): return self.anki.createDeck(deck) + @webApi() def changeDeck(self, cards, deck): return self.anki.changeDeck(cards, deck) @@ -1303,14 +1318,17 @@ class AnkiConnect: def guiExitAnki(self): return self.anki.guiExitAnki() + @webApi() def cardsInfo(self, cards): return self.anki.cardsInfo(cards) + @webApi() def notesInfo(self, notes): return self.anki.notesInfo(notes) + @webApi() def sync(self): return self.anki.sync()