Refactor multi api call

This commit is contained in:
louie 2019-11-01 22:59:15 +08:00 committed by louie
parent 375d6d66d2
commit 0d219e4118

View file

@ -5,7 +5,7 @@
;; Description: Make Anki Cards in Org-mode ;; Description: Make Anki Cards in Org-mode
;; Author: Lei Tan ;; Author: Lei Tan
;; Version: 0.3.3 ;; Version: 0.3.3
;; Package-Requires: ((emacs "25") (request "0.3.0") (dash "2.12.0")) ;; Package-Requires: ((emacs "25.1") (request "0.3.0"))
;; URL: https://github.com/louietan/anki-editor ;; URL: https://github.com/louietan/anki-editor
;; ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -59,7 +59,6 @@
;;; Code: ;;; Code:
(require 'cl-lib) (require 'cl-lib)
(require 'dash)
(require 'json) (require 'json)
(require 'org-element) (require 'org-element)
(require 'ox) (require 'ox)
@ -88,10 +87,6 @@
"If non-nil, consecutive `}' will be automatically separated by spaces to prevent early-closing of cloze. "If non-nil, consecutive `}' will be automatically separated by spaces to prevent early-closing of cloze.
See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.") See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.")
(defcustom anki-editor-create-decks
nil
"If non-nil, creates deck before creating a note.")
(defcustom anki-editor-org-tags-as-anki-tags (defcustom anki-editor-org-tags-as-anki-tags
t t
"If nil, tags of entries won't be counted as Anki tags.") "If nil, tags of entries won't be counted as Anki tags.")
@ -120,16 +115,8 @@ form entries."
(defcustom anki-editor-use-math-jax nil (defcustom anki-editor-use-math-jax nil
"Use Anki's built in MathJax support instead of LaTeX.") "Use Anki's built in MathJax support instead of LaTeX.")
;;; AnkiConnect
(defun anki-editor--anki-connect-invoke-queue () ;;; AnkiConnect
(let (action-queue)
(lambda (&optional action params handler)
(if action
(push (cons (anki-editor--anki-connect-action action params) handler) action-queue)
(when action-queue
(apply #'anki-editor--anki-connect-invoke-multi (nreverse action-queue))
(setq action-queue nil))))))
(defun anki-editor-api-call (action &rest params) (defun anki-editor-api-call (action &rest params)
"Invoke AnkiConnect with ACTION and PARAMS." "Invoke AnkiConnect with ACTION and PARAMS."
@ -162,28 +149,48 @@ or raise an error."
(when .error (error .error)) (when .error (error .error))
.result)) .result))
(defun anki-editor--anki-connect-invoke-multi (&rest actions) (defmacro anki-editor-api-with-multi (&rest body)
"Invoke AnkiConnect with ACTIONS, a list of (action . result-handler) pairs." "Use in combination with `anki-editor-api-enqueue' to combine
(-zip-with (lambda (result handler) multiple api calls into a single 'multi' call, return the results
(when-let ((_ (listp result)) of these calls in the same order."
`(let (--anki-editor-var-multi-actions--
--anki-editor-var-multi-results--)
,@body
(setq --anki-editor-var-multi-results--
(anki-editor-api-call-result
'multi
:actions (nreverse
;; Here we make a vector from the action list,
;; or `json-encode' will consider it as an association list.
(vconcat
--anki-editor-var-multi-actions--))))
(cl-loop for result in --anki-editor-var-multi-results--
do (when-let ((_ (listp result))
(err (alist-get 'error result))) (err (alist-get 'error result)))
(error err)) (error err))
(and handler (funcall handler result))) collect result)))
(anki-editor--anki-connect-invoke-result
"multi" `((actions . ,(mapcar #'car actions))))
(mapcar #'cdr actions)))
(defun anki-editor--anki-connect-map-note (note) (defmacro anki-editor-api-enqueue (action &rest params)
"Like `anki-editor-api-call', but is only used in combination
with `anki-editor-api-with-multi'. Instead of sending the
request directly, it simply queues the request."
`(let ((action (list :action ,action))
(params (list ,@params)))
(when params
(plist-put action :params params))
(push action --anki-editor-var-multi-actions--)))
(defun anki-editor-api--note (note)
"Convert NOTE to the form that AnkiConnect accepts." "Convert NOTE to the form that AnkiConnect accepts."
(let-alist note (list
(list (cons "id" .note-id) :id (string-to-number (or (anki-editor-note-id note) "0"))
(cons "deckName" .deck) :deckName (anki-editor-note-deck note)
(cons "modelName" .note-type) :modelName (anki-editor-note-model note)
(cons "fields" .fields) :fields (anki-editor-note-fields note)
;; Convert tags to a vector since empty list is identical to nil ;; Convert tags to a vector since empty list is identical to nil
;; which will become None in Python, but AnkiConnect requires it ;; which will become None in Python, but AnkiConnect requires it
;; to be type of list. ;; to be type of list.
(cons "tags" (vconcat .tags))))) :tags (vconcat (anki-editor-note-tags note))))
(defun anki-editor-api--store-media-file (path) (defun anki-editor-api--store-media-file (path)
"Store media file for PATH, which is an absolute file name. "Store media file for PATH, which is an absolute file name.
@ -367,6 +374,9 @@ The implementation is borrowed and simplified from ox-html."
;;; Core Functions ;;; Core Functions
(cl-defstruct anki-editor-note
id model deck fields tags)
(defun anki-editor-map-note-entries (func &optional match scope &rest skip) (defun anki-editor-map-note-entries (func &optional match scope &rest skip)
"Simple wrapper that calls `org-map-entries' with "Simple wrapper that calls `org-map-entries' with
`&ANKI_NOTE_TYPE<>\"\"' appended to MATCH." `&ANKI_NOTE_TYPE<>\"\"' appended to MATCH."
@ -396,9 +406,11 @@ Where the subtree is created depends on PREFIX."
(defun anki-editor--push-note (note) (defun anki-editor--push-note (note)
"Request AnkiConnect for updating or creating NOTE." "Request AnkiConnect for updating or creating NOTE."
(if (= (alist-get 'note-id note) -1) (cond
(anki-editor--create-note note) ((null (anki-editor-note-id note))
(anki-editor--update-note note))) (anki-editor--create-note note))
(t
(anki-editor--update-note note))))
(defun anki-editor--set-note-id (id) (defun anki-editor--set-note-id (id)
(unless id (unless id
@ -407,46 +419,37 @@ Where the subtree is created depends on PREFIX."
(defun anki-editor--create-note (note) (defun anki-editor--create-note (note)
"Request AnkiConnect for creating NOTE." "Request AnkiConnect for creating NOTE."
(let ((queue (anki-editor--anki-connect-invoke-queue))) (thread-last
(when anki-editor-create-decks (anki-editor-api-with-multi
(funcall queue (anki-editor-api-enqueue 'createDeck
'createDeck :deck (anki-editor-note-deck note))
`((deck . ,(alist-get 'deck note))))) (anki-editor-api-enqueue 'addNote
(funcall queue :note (anki-editor-api--note note)))
'addNote (nth 1)
`((note . ,(anki-editor--anki-connect-map-note note))) (anki-editor--set-note-id)))
#'anki-editor--set-note-id)
(funcall queue)))
(defun anki-editor--update-note (note) (defun anki-editor--update-note (note)
"Request AnkiConnect for updating fields and tags of NOTE." "Request AnkiConnect for updating fields and tags of NOTE."
(let ((queue (anki-editor--anki-connect-invoke-queue))) (let* ((oldnote (caar (anki-editor-api-with-multi
(funcall queue (anki-editor-api-enqueue 'notesInfo
'updateNoteFields :notes (list (anki-editor-note-id note)))
`((note . ,(anki-editor--anki-connect-map-note note)))) (anki-editor-api-enqueue 'updateNoteFields
(funcall queue :note (anki-editor-api--note note)))))
'notesInfo (tagsadd (cl-set-difference (anki-editor-note-tags note)
`((notes . (,(alist-get 'note-id note)))) (alist-get 'tags oldnote)
(lambda (result) :test 'string=))
;; update tags (tagsdel (thread-first (alist-get 'tags oldnote)
(let* ((existing-note (car result)) (cl-set-difference (anki-editor-note-tags note) :test 'string=)
(tags-to-add (-difference (-difference (alist-get 'tags note) (cl-set-difference anki-editor-protected-tags :test 'string=))))
(alist-get 'tags existing-note)) (anki-editor-api-with-multi
anki-editor-ignored-org-tags)) (when tagsadd
(tags-to-remove (-difference (-difference (alist-get 'tags existing-note) (anki-editor-api-enqueue 'addTags
(alist-get 'tags note)) :notes (list (anki-editor-note-id note))
anki-editor-protected-tags)) :tags (mapconcat #'identity tagsadd " ")))
(tag-queue (anki-editor--anki-connect-invoke-queue))) (when tagsdel
(when tags-to-add (anki-editor-api-enqueue 'removeTags
(funcall tag-queue :notes (list (anki-editor-note-id note))
'addTags `((notes . (,(alist-get 'note-id note))) :tags (mapconcat #'identity tagsdel " "))))))
(tags . ,(mapconcat #'identity tags-to-add " ")))))
(when tags-to-remove
(funcall tag-queue
'removeTags `((notes . (,(alist-get 'note-id note)))
(tags . ,(mapconcat #'identity tags-to-remove " ")))))
(funcall tag-queue))))
(funcall queue)))
(defun anki-editor--set-failure-reason (reason) (defun anki-editor--set-failure-reason (reason)
"Set failure reason to REASON in property drawer at point." "Set failure reason to REASON in property drawer at point."
@ -485,7 +488,7 @@ Where the subtree is created depends on PREFIX."
(when (and (anki-editor--enable-tag-completion) (when (and (anki-editor--enable-tag-completion)
(not just-align)) (not just-align))
(setq anki-editor--anki-tags-cache (anki-editor-all-tags)) (setq anki-editor--anki-tags-cache (anki-editor-all-tags))
(unless (-all? #'anki-editor-is-valid-org-tag anki-editor--anki-tags-cache) (when (cl-notevery #'anki-editor-is-valid-org-tag anki-editor--anki-tags-cache)
(warn "Some tags from Anki contain characters that are not valid in Org tags.")))) (warn "Some tags from Anki contain characters that are not valid in Org tags."))))
(defun anki-editor--get-buffer-tags (oldfun) (defun anki-editor--get-buffer-tags (oldfun)
@ -499,30 +502,32 @@ Where the subtree is created depends on PREFIX."
(anki-editor-api-call-result 'modelNames)) (anki-editor-api-call-result 'modelNames))
(defun anki-editor-note-at-point () (defun anki-editor-note-at-point ()
"Construct an alist representing a note from current entry." "Make a note struct from current entry."
(let ((org-trust-scanner-tags t) (let ((org-trust-scanner-tags t)
(deck (org-entry-get-with-inheritance anki-editor-prop-deck)) (deck (org-entry-get-with-inheritance anki-editor-prop-deck))
(note-id (org-entry-get nil anki-editor-prop-note-id)) (note-id (org-entry-get nil anki-editor-prop-note-id))
(note-type (org-entry-get nil anki-editor-prop-note-type)) (note-type (org-entry-get nil anki-editor-prop-note-type))
(tags (anki-editor--get-tags)) (tags (cl-set-difference (anki-editor--get-tags)
anki-editor-ignored-org-tags
:test 'string=))
(fields (anki-editor--build-fields))) (fields (anki-editor--build-fields)))
(unless deck (error "No deck specified")) (unless deck (error "No deck specified"))
(unless note-type (error "Missing note type")) (unless note-type (error "Missing note type"))
(unless fields (error "Missing fields")) (unless fields (error "Missing fields"))
`((deck . ,deck) (make-anki-editor-note :id note-id
(note-id . ,(string-to-number (or note-id "-1"))) :model note-type
(note-type . ,note-type) :deck deck
(tags . ,tags) :tags tags
(fields . ,fields)))) :fields fields)))
(defun anki-editor--get-tags () (defun anki-editor--get-tags ()
(let ((tags (anki-editor--entry-get-multivalued-property-with-inheritance (let ((tags (anki-editor--entry-get-multivalued-property-with-inheritance
nil nil
anki-editor-prop-tags))) anki-editor-prop-tags)))
(if anki-editor-org-tags-as-anki-tags (if anki-editor-org-tags-as-anki-tags
(append tags (org-get-tags-at)) (append tags (org-get-tags))
tags))) tags)))
(defun anki-editor--entry-get-multivalued-property-with-inheritance (pom property) (defun anki-editor--entry-get-multivalued-property-with-inheritance (pom property)
@ -601,7 +606,10 @@ name and the cdr of which is field content."
(defun anki-editor--concat-multivalued-property-value (prop value) (defun anki-editor--concat-multivalued-property-value (prop value)
(let ((old-values (org-entry-get-multivalued-property nil prop))) (let ((old-values (org-entry-get-multivalued-property nil prop)))
(unless (string-suffix-p prop "+") (unless (string-suffix-p prop "+")
(setq old-values (-difference old-values (org-entry-get-multivalued-property nil (concat prop "+"))))) (setq old-values (cl-set-difference old-values
(org-entry-get-multivalued-property
nil (concat prop "+"))
:test 'string=)))
(mapconcat #'org-entry-protect-space (mapconcat #'org-entry-protect-space
(append old-values (list value)) (append old-values (list value))
" "))) " ")))
@ -827,15 +835,16 @@ note or deck."
entry." entry."
(interactive) (interactive)
(anki-editor-api-call-result 'guiAddCards (anki-editor-api-call-result 'guiAddCards
:note `((:options (:closeAfterAdding t)) :note (append
,(anki-editor--api-note (anki-editor-api--note
(anki-editor-note-at-point))))) (anki-editor-note-at-point))
(list :options '(:closeAfterAdding t)))))
(defun anki-editor-find-notes (&optional query) (defun anki-editor-find-notes (&optional query)
"Find notes with QUERY." "Find notes with QUERY."
(interactive "sQuery: ") (interactive "sQuery: ")
(let ((nids (anki-editor-api-call-result 'findNotes (let ((nids (anki-editor-api-call-result 'findNotes
:query query))) :query (or query ""))))
(if (called-interactively-p 'interactive) (if (called-interactively-p 'interactive)
(message "%S" nids) (message "%S" nids)
nids))) nids)))