yanki-connect
Version:
A fully-typed AnkiConnect API client.
935 lines (932 loc) • 35.7 kB
JavaScript
//#region src/utilities/platform.ts
const ENVIRONMENT = typeof window === "undefined" ? typeof process === "undefined" ? "other" : "node" : "browser";
const PLATFORM = ENVIRONMENT === "browser" ? /windows/i.test(navigator.userAgent) ? "windows" : /mac/i.test(navigator.userAgent) ? "mac" : "other" : ENVIRONMENT === "node" ? process.platform === "win32" ? "windows" : process.platform === "darwin" ? "mac" : "other" : "other";
//#endregion
//#region src/utilities/launcher.ts
let lastLaunchAttemptTime = 0;
const launchAttemptInterval = 5e3;
let launchAttemptCount = 0;
const matchLaunchAttemptsPerSession = 100;
/**
* Launches the Anki desktop app if it is not already running.
*
* Mac only for now, and works on a best-effort basis. Retries as necessary, but
* times out eventually.
*/
async function launchAnkiApp() {
if (PLATFORM === "mac" && ENVIRONMENT === "node") {
const { openApp } = await import("open");
if (launchAttemptCount >= matchLaunchAttemptsPerSession) {
console.warn("Too many Anki App launch attempts this session, ignoring");
return;
}
if (lastLaunchAttemptTime === 0 || Date.now() - lastLaunchAttemptTime > launchAttemptInterval) {
console.warn("Attempting to launch Anki app");
lastLaunchAttemptTime = Date.now();
launchAttemptCount++;
await openApp("/Applications/Anki.app", {
background: true,
newInstance: false,
wait: false
});
}
} else console.warn("Automatic Anki App launch is only supported on macOS in Node.js environment");
}
//#endregion
//#region src/client.ts
const defaultYankiConnectOptions = {
autoLaunch: false,
fetchAdapter: fetch.bind(globalThis),
host: "http://127.0.0.1",
key: void 0,
port: 8765,
version: 6
};
/**
* **YankiConnect is a client for the [AnkiConnect
* API](https://git.sr.ht/~foosoft/anki-connect/tree/25.11.9.0/item/README.md)**.
*
* It implements every endpoint from AnkiConnect version 25.11.9.0, released
* 2025-11-09.
*
* Inline documentation is by the AnkiConnect authors, generated from [the
* readme.md](https://git.sr.ht/~foosoft/anki-connect/tree/25.11.9.0/item/README.md)
*/
var YankiConnect = class {
/**
* **Card Actions**
*
* [Documentation](https://git.sr.ht/~foosoft/anki-connect/tree/25.11.9.0/item/README.md#card-actions)
*/
card = {
/**
* Answer cards. Ease is between 1 (Again) and 4 (Easy). Will start the
* timer immediately before answering. Returns true if card exists, `false`
* otherwise.
*/
answerCards: this.build("answerCards"),
/**
* 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.
*/
areDue: this.build("areDue"),
/**
* Returns an array indicating whether each of the given cards is suspended
* (in the same order). If card doesn’t exist returns `null`.
*/
areSuspended: this.build("areSuspended"),
/**
* 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, last modification timestamp as well as ease
* and interval.
*/
cardsInfo: this.build("cardsInfo"),
/**
* Returns a list of objects containing for each card ID the modification
* time. This function is about 15 times faster than executing `cardsInfo`.
*/
cardsModTime: this.build("cardsModTime"),
/**
* 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.
*/
cardsToNotes: this.build("cardsToNotes"),
/**
* Returns an array of card IDs for a given query. Functionally identical to
* `guiBrowse` but doesn’t use the GUI for better performance.
*/
findCards: this.build("findCards"),
/**
* Forget cards, making the cards new again.
*/
forgetCards: this.build("forgetCards"),
/**
* Returns an array with the ease factor for each of the given cards (in the
* same order).
*/
getEaseFactors: this.build("getEaseFactors"),
/**
* 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.
*/
getIntervals: this.build("getIntervals"),
/**
* Make cards be “relearning”.
*/
relearnCards: this.build("relearnCards"),
/**
* Set Due Date. Turns cards into review cards if they are new, and makes
* them due on a certain date.
*
* - 0 = today
* - 1! = tomorrow + change interval to 1
* - 3-7 = random choice of 3-7 days
*/
setDueDate: this.build("setDueDate"),
/**
* Sets ease factor of cards by card ID; returns `true` if successful (all
* cards existed) or `false` otherwise.
*/
setEaseFactors: this.build("setEaseFactors"),
/**
* Sets specific value of a single card. Given the risk of wreaking havoc in
* the database when changing some of the values of a card, some of the keys
* require the argument “warning_check” set to True. This can be used to set
* a card’s flag, change it’s ease factor, change the review order in a
* filtered deck and change the column “data” (not currently used by anki
* apparently), and many other values. A list of values and explanation of
* their respective utility can be found at [AnkiDroid’s
* wiki](https://github.com/ankidroid/Anki-Android/wiki/Database-Structure).
*/
setSpecificValueOfCard: this.build("setSpecificValueOfCard"),
/**
* Suspend cards by card ID; returns `true` if successful (at least one card
* wasn’t already suspended) or `false` otherwise.
*/
suspend: this.build("suspend"),
/**
* Check if card is suspended by its ID. Returns `true` if suspended,
* `false` otherwise.
*/
suspended: this.build("suspended"),
/**
* Unsuspend cards by card ID; returns `true` if successful (at least one
* card was previously suspended) or `false` otherwise.
*/
unsuspend: this.build("unsuspend")
};
/**
* **Deck Actions**
*
* [Documentation](https://git.sr.ht/~foosoft/anki-connect/tree/25.11.9.0/item/README.md#deck-actions)
*/
deck = {
/**
* Moves cards with the given IDs to a different deck, creating the deck if
* it doesn’t exist yet.
*/
changeDeck: this.build("changeDeck"),
/**
* 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.
*/
cloneDeckConfigId: this.build("cloneDeckConfigId"),
/**
* Create a new empty deck. Will not overwrite a deck that exists with the
* same name.
*/
createDeck: this.build("createDeck"),
/**
* Gets the complete list of deck names for the current user.
*/
deckNames: this.build("deckNames"),
/**
* Gets the complete list of deck names and their respective IDs for the
* current user.
*/
deckNamesAndIds: this.build("deckNamesAndIds"),
/**
* Deletes decks with the given names. The argument `cardsToo` must be
* specified and set to `true`.
*/
deleteDecks: this.build("deleteDecks"),
/**
* Gets the configuration group object for the given deck.
*/
getDeckConfig: this.build("getDeckConfig"),
/**
* 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.
*/
getDecks: this.build("getDecks"),
/**
* ```
* Gets statistics such as total cards and cards due for the given decks.
* ```
*/
getDeckStats: this.build("getDeckStats"),
/**
* 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.
*/
removeDeckConfigId: this.build("removeDeckConfigId"),
/**
* 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).
*/
saveDeckConfig: this.build("saveDeckConfig"),
/**
* 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.
*/
setDeckConfigId: this.build("setDeckConfigId")
};
/**
* **Graphical Actions**
*
* [Documentation](https://git.sr.ht/~foosoft/anki-connect/tree/25.11.9.0/item/README.md#graphical-actions)
*/
graphical = {
/**
* Invokes the _Add Cards_ dialog, presets the note using the given deck and
* model, with the provided field values and tags. Invoking it multiple
* times closes the old window and _reopen the window_ with the new provided
* values.
*
* Audio, video, and picture files can be embedded into the fields via the
* `audio`, `video`, and `picture` keys, respectively. Refer to the
* documentation of `addNote` and `storeMediaFile` for an explanation of
* these fields.
*
* The result is the ID of the note which would be added, if the user chose
* to confirm the _Add Cards_ dialogue.
*/
guiAddCards: this.build("guiAddCards"),
/**
* Answers the current card; returns `true` if succeeded or `false`
* otherwise. Note that the answer for the current card must be displayed
* before before any answer can be accepted by Anki.
*/
guiAnswerCard: this.build("guiAnswerCard"),
/**
* Invokes the _Card Browser_ dialog and searches for a given query. Returns
* an array of identifiers of the cards that were found. Query syntax is
* [documented here](https://docs.ankiweb.net/searching.html).
*
* Optionally, the `reorderCards` property can be provided to reorder the
* cards shown in the _Card Browser_. This is an array including the `order`
* and `columnId` objects. `order` can be either `ascending` or `descending`
* while `columnId` can be one of several column identifiers (as documented
* in the [Anki source
* code](https://github.com/ankitects/anki/blob/main/rslib/src/browser_table.rs)).
* The specified column needs to be visible in the _Card Browser_.
*/
guiBrowse: this.build("guiBrowse"),
/**
* Requests a database check, but returns immediately without waiting for
* the check to complete. Therefore, the action will always return `true`
* even if errors are detected during the database check.
*/
guiCheckDatabase: this.build("guiCheckDatabase"),
/**
* Returns information about the current card or `null` if not in review
* mode.
*/
guiCurrentCard: this.build("guiCurrentCard"),
/**
* Opens the _Deck Browser_ dialog.
*/
guiDeckBrowser: this.build("guiDeckBrowser"),
/**
* Opens the _Deck Overview_ dialog for the deck with the given name;
* returns `true` if succeeded or `false` otherwise.
*/
guiDeckOverview: this.build("guiDeckOverview"),
/**
* Starts review for the deck with the given name; returns `true` if
* succeeded or `false` otherwise.
*/
guiDeckReview: this.build("guiDeckReview"),
/**
* Opens the _Edit_ dialog with a note corresponding to given note ID. The
* dialog is similar to the _Edit Current_ dialog, but:
*
* - Has a Preview button to preview the cards for the note
* - Has a Browse button to open the browser with these cards
* - Has Previous/Back buttons to navigate the history of the dialog
* - Has no bar with the Close button
*/
guiEditNote: this.build("guiEditNote"),
/**
* 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.
*/
guiExitAnki: this.build("guiExitAnki"),
/**
* Invokes the _Import… (Ctrl+Shift+I)_ dialog with an optional file path.
* Brings up the dialog for user to review the import. Supports all file
* types that Anki supports. Brings open file dialog if no path is provided.
* Forward slashes must be used in the path on Windows. Only supported for
* Anki 2.1.52+.
*/
guiImportFile: this.build("guiImportFile"),
/**
* Plays any Audio for the current side of the current card; returns true if
* succeeded or false otherwise.
*/
guiPlayAudio: this.build("guiPlayAudio"),
/**
* Finds the open instance of the Card Browser dialog and selects a card
* given a card identifier. Returns true if the Card Browser is open, false
* otherwise.
*/
guiSelectCard: this.build("guiSelectCard"),
/**
* Finds the open instance of the _Card Browser_ dialog and returns an array
* of identifiers of the notes that are selected. Returns an empty list if
* the browser is not open.
*/
guiSelectedNotes: this.build("guiSelectedNotes"),
/**
* @deprecated Actually selects card IDs. Use `guiSelectCard` instead.'
*/
guiSelectNote: this.build("guiSelectNote"),
/**
* Shows answer text for the current card; returns `true` if in review mode
* or `false` otherwise.
*/
guiShowAnswer: this.build("guiShowAnswer"),
/**
* Shows question text for the current card; returns `true` if in review
* mode or `false` otherwise.
*/
guiShowQuestion: this.build("guiShowQuestion"),
/**
* 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`.
*/
guiStartCardTimer: this.build("guiStartCardTimer"),
/**
* Undo the last action / card; returns `true` if succeeded or `false`
* otherwise.
*/
guiUndo: this.build("guiUndo")
};
/**
* **Media Actions**
*
* [Documentation](https://git.sr.ht/~foosoft/anki-connect/tree/25.11.9.0/item/README.md#media-actions)
*/
media = {
/**
* Deletes the specified file inside the media folder.
*/
deleteMediaFile: this.build("deleteMediaFile"),
/**
* Gets the full path to the `collection.media` folder of the currently
* opened profile.
*/
getMediaDirPath: this.build("getMediaDirPath"),
/**
* Gets the names of media files matched the pattern. Returning all names by
* default.
*/
getMediaFilesNames: this.build("getMediaFilesNames"),
/**
* Retrieves the base64-encoded contents of the specified file, returning
* `false` if the file does not exist.
*/
retrieveMediaFile: this.build("retrieveMediaFile"),
/**
* Stores a file with the specified base64-encoded contents inside the media
* folder. Alternatively you can specify a absolute file path, or a url from
* where the file shell be downloaded. If more than one of `data`, `path`
* and `url` are provided, the `data` field will be used first, then `path`,
* and finally `url`. 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. Any existing
* file with the same name is deleted by default. Set `deleteExisting` to
* `false` to prevent that by [letting Anki give the new file a
* non-conflicting
* name](https://github.com/ankitects/anki/blob/aeba725d3ea9628c73300648f748140db3fdd5ed/rslib/src/media/files.rs#L194).
*/
storeMediaFile: this.build("storeMediaFile")
};
/**
* **Miscellaneous Actions**
*
* [Documentation](https://git.sr.ht/~foosoft/anki-connect/tree/25.11.9.0/item/README.md#miscellaneous-actions)
*/
miscellaneous = {
/**
* Gets information about the AnkiConnect APIs available. The request
* supports the following params:
*
* - `scopes` - An array of scopes to get reflection information about. The
* only currently supported value is `"actions"`.
* - `actions` - Either `null` or an array of API method names to check for.
* If the value is `null`, the result will list all of the available API
* actions. If the value is an array of strings, the result will only
* contain actions which were in this array.
*/
apiReflect: this.build("apiReflect"),
/**
* Exports a given deck in `.apkg` format. Returns `true` if successful or
* `false` otherwise. The optional property `includeSched` (default is
* `false`) can be specified to include the cards’ scheduling data.
*/
exportPackage: this.build("exportPackage"),
/**
* Retrieve the active profile.
*/
getActiveProfile: this.build("getActiveProfile"),
/**
* Retrieve the list of profiles.
*/
getProfiles: this.build("getProfiles"),
/**
* Imports a file in `.apkg` format into the collection. Returns `true` if
* successful or `false` otherwise. Note that the file path is relative to
* Anki’s collection.media folder, not to the client.
*/
importPackage: this.build("importPackage"),
/**
* Selects the profile specified in request.
*/
loadProfile: this.build("loadProfile"),
/**
* Performs multiple actions in one request, returning an array with the
* response of each action (in the given order).
*/
multi: this.build("multi"),
/**
* Tells `anki` to reload all data from the database.
*/
reloadCollection: this.build("reloadCollection"),
/**
* Requests permission to use the API exposed by this plugin. This method
* does not require the API key, and is the only one that accepts requests
* from any origin; the other methods only accept requests from trusted
* origins, which are listed under `webCorsOriginList` in the add-on config.
* `localhost` is trusted by default.
*
* Calling this method from an untrusted origin will display a popup in Anki
* asking the user whether they want to allow your origin to use the API;
* calls from trusted origins will return the result without displaying the
* popup. When denying permission, the user may also choose to ignore
* further permission requests from that origin. These origins end up in the
* `ignoreOriginList`, editable via the add-on config.
*
* The result always contains the `permission` field, which in turn contains
* either the string `granted` or `denied`, corresponding to whether your
* origin is trusted. If your origin is trusted, the fields `requireApiKey`
* (true if required) and `version` will also be returned.
*
* 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 which are available in the reported AnkiConnect version or
* earlier, everything should work fine.
*/
requestPermission: this.build("requestPermission"),
/**
* Synchronizes the local Anki collections with AnkiWeb.
*/
sync: this.build("sync"),
/**
* Gets the version of the API exposed by this plugin. Currently versions
* `1` through `6` are defined.
*/
version: this.build("version")
};
/**
* **Model Actions**
*
* [Documentation](https://git.sr.ht/~foosoft/anki-connect/tree/25.11.9.0/item/README.md#model-actions)
*/
model = {
/**
* Creates a new model to be used in Anki. User must provide the
* `modelName`, `inOrderFields` and `cardTemplates` to be used in the model.
* There are optional fields `css` and `isCloze`. If not specified, `css`
* will use the default Anki css and `isCloze` will be equal to `false`. If
* `isCloze` is `true` then model will be created as Cloze.
*
* Optionally the `Name` field can be provided for each entry of
* `cardTemplates`. By default the card names will be `Card 1`, `Card 2`,
* and so on.
*/
createModel: this.build("createModel"),
/**
* Find and replace string in existing model by model name. Customize to
* replace in front, back or css by setting to `true`/`false`.
*/
findAndReplaceInModels: this.build("findAndReplaceInModels"),
/**
* Gets a list of models for the provided model IDs from the current user.
*/
findModelsById: this.build("findModelsById"),
/**
* Gets a list of models for the provided model names from the current user.
*/
findModelsByName: this.build("findModelsByName"),
/**
* Creates a new field within a given model.
*
* Optionally, the `index` value can be provided, which works exactly the
* same as the index in `modelFieldReposition`. By default, the field is
* added to the end of the field list.
*/
modelFieldAdd: this.build("modelFieldAdd"),
/**
* Gets the complete list of field descriptions (the text seen in the gui
* editor when a field is empty) for the provided model name.
*/
modelFieldDescriptions: this.build("modelFieldDescriptions"),
/**
* Gets the complete list of fonts along with their font sizes.
*/
modelFieldFonts: this.build("modelFieldFonts"),
/**
* Gets the complete list of field names for the provided model name.
*/
modelFieldNames: this.build("modelFieldNames"),
/**
* Deletes a field within a given model.
*/
modelFieldRemove: this.build("modelFieldRemove"),
/**
* Rename the field name of a given model.
*/
modelFieldRename: this.build("modelFieldRename"),
/**
* Reposition the field within the field list of a given model.
*
* The value of `index` starts at 0. For example, an `index` of `0` puts the
* field in the first position, and an `index` of `2` puts the field in the
* third position.
*/
modelFieldReposition: this.build("modelFieldReposition"),
/**
* Sets the description (the text seen in the gui editor when a field is
* empty) for a field within a given model.
*
* Older versions of Anki (2.1.49 and below) do not have field descriptions.
* In that case, this will return with `false`.
*/
modelFieldSetDescription: this.build("modelFieldSetDescription"),
/**
* Sets the font for a field within a given model.
*/
modelFieldSetFont: this.build("modelFieldSetFont"),
/**
* Sets the font size for a field within a given model.
*/
modelFieldSetFontSize: this.build("modelFieldSetFontSize"),
/**
* 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.
*/
modelFieldsOnTemplates: this.build("modelFieldsOnTemplates"),
/**
* Gets the complete list of model names for the current user.
*/
modelNames: this.build("modelNames"),
/**
* Gets the complete list of model names and their corresponding IDs for the
* current user.
*/
modelNamesAndIds: this.build("modelNamesAndIds"),
/**
* Gets the CSS styling for the provided model by name.
*/
modelStyling: this.build("modelStyling"),
/**
* Adds a template to an existing model by name. If you want to update an
* existing template, use `updateModelTemplates`.
*/
modelTemplateAdd: this.build("modelTemplateAdd"),
/**
* Removes a template from an existing model.
*/
modelTemplateRemove: this.build("modelTemplateRemove"),
/**
* Renames a template in an existing model.
*/
modelTemplateRename: this.build("modelTemplateRename"),
/**
* Repositions a template in an existing model.
*/
modelTemplateReposition: this.build("modelTemplateReposition"),
/**
* Returns an object indicating the template content for each card connected
* to the provided model by name.
*
* The value of `index` starts at 0. For example, an `index` of `0` puts the
* template in the first position, and an `index` of `2` puts the template
* in the third position.
*/
modelTemplates: this.build("modelTemplates"),
/**
* Modify the CSS styling of an existing model by name.
*/
updateModelStyling: this.build("updateModelStyling"),
/**
* Modify the templates of an existing model by name. Only specifies cards
* and specified sides will be modified. If an existing card or side is not
* included in the request, it will be left unchanged.
*/
updateModelTemplates: this.build("updateModelTemplates")
};
/**
* **Note Actions**
*
* [Documentation](https://git.sr.ht/~foosoft/anki-connect/tree/25.11.9.0/item/README.md#note-actions)
*/
note = {
/**
* 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, video, and picture files and embed them
* in newly created notes. The corresponding `audio`, `video`, and `picture`
* note members are optional and can be omitted. If you choose to include
* any of them, they should contain a single object or an array of objects
* with the mandatory `filename` field and one of `data`, `path` or `url`.
* Refer to the documentation of `storeMediaFile` for an explanation of
* these fields. The `skipHash` field can be optionally provided to skip the
* inclusion of 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 field names to which the inserted media
* should be appended to. It can be omitted if this isn't required. The
* `allowDuplicate` member inside `options` group can be set to true to
* enable adding duplicate cards. Normally duplicate cards can not be added
* and trigger exception.
*
* The `duplicateScope` member inside `options` can be used to specify the
* scope for which duplicates are checked. A value of `"deck"` will only
* check for duplicates in the target deck; any other value will check the
* entire collection.
*
* The `duplicateScopeOptions` object can be used to specify some additional
* settings:
*
* - `duplicateScopeOptions.deckName` will specify which deck to use for
* checking duplicates in. If undefined or `null`, the target deck will be
* used.
* - `duplicateScopeOptions.checkChildren` will change whether or not
* duplicate cards are checked in child decks. The default value is
* `false`.
* - `duplicateScopeOptions.checkAllModels` specifies whether duplicate checks
* are performed across all note types. The default value is `false`.
*/
addNote: this.build("addNote"),
/**
* Creates multiple notes using the given deck and model, with the provided
* field values and tags. Returns an array of identifiers of the created
* notes. In the event of any errors, all errors are gathered and returned.
*
* Please see the documentation for `addNote` for an explanation of objects
* in the `notes` array.
*/
addNotes: this.build("addNotes"),
/**
* Adds tags to notes by note ID.
*/
addTags: this.build("addTags"),
/**
* Accepts an array of objects which define parameters for candidate notes
* (see `addNote`) and returns an array of booleans indicating whether or
* not the parameters at the corresponding index could be used to create a
* new note.
*/
canAddNotes: this.build("canAddNotes"),
/**
* Accepts an array of objects which define parameters for candidate notes
* (see `addNote`) and returns an array of objects with fields `canAdd` and
* `error`.
*
* - `canAdd` indicates whether or not the parameters at the corresponding
* index could be used to create a new note.
* - `error` contains an explanation of why a note cannot be added.
*/
canAddNotesWithErrorDetail: this.build("canAddNotesWithErrorDetail"),
/**
* Clears all the unused tags in the notes for the current user.
*/
clearUnusedTags: this.build("clearUnusedTags"),
/**
* Deletes notes with the given ids. If a note has several cards associated
* with it, all associated cards will be deleted.
*/
deleteNotes: this.build("deleteNotes"),
/**
* Returns an array of note IDs for a given query. Query syntax is
* [documented here](https://docs.ankiweb.net/searching.html).
*/
findNotes: this.build("findNotes"),
/**
* Get a note's tags by note ID.
*/
getNoteTags: this.build("getNoteTags"),
/**
* Gets the complete list of tags for the current user.
*/
getTags: this.build("getTags"),
/**
* Returns a list of objects containing for each note ID the note fields,
* tags, note type, modification time, the cards belonging to the note and
* the profile where the note was created.
*/
notesInfo: this.build("notesInfo"),
/**
* Returns a list of objects containing for each note ID the modification
* time.
*/
notesModTime: this.build("notesInfo"),
/**
* Removes all the empty notes for the current user.
*/
removeEmptyNotes: this.build("removeEmptyNotes"),
/**
* Remove tags from notes by note ID.
*/
removeTags: this.build("removeTags"),
/**
* Replace tags in notes by note ID.
*/
replaceTags: this.build("replaceTags"),
/**
* Replace tags in all the notes for the current user.
*/
replaceTagsInAllNotes: this.build("replaceTagsInAllNotes"),
/**
* Modify the fields and/or tags of an existing note. In other words,
* combines `updateNoteFields` and `updateNoteTags`. Please see their
* documentation for an explanation of all properties.
*
* Either `fields` or `tags` property can be omitted without affecting the
* other. Thus valid requests to `updateNoteFields` also work with
* `updateNote`. The note must have the `fields` property in order to update
* the optional audio, video, or picture objects.
*
* If neither `fields` nor `tags` are provided, the method will fail. Fields
* are updated first and are not rolled back if updating tags fails. Tags
* are not updated if updating fields fails.
*
* > [!WARNING] You must not be viewing the note that you are updating on your
* > Anki browser, otherwise the fields will not update. See [this
* > issue](https://github.com/FooSoft/anki-connect/issues/82) for further
* > details.
*/
updateNote: this.build("updateNote"),
/**
* Modify the fields of an existing note. You can also include audio, video,
* or picture files which will be added to the note with an optional
* `audio`, `video`, or `picture` property. Please see the documentation for
* `addNote` for an explanation of objects in the `audio`, `video`, or
* `picture` array.
*
* > [!WARNING] You must not be viewing the note that you are updating on your
* > Anki browser, otherwise the fields will not update. See [this
* > issue](https://github.com/FooSoft/anki-connect/issues/82) for further
* > details.
*/
updateNoteFields: this.build("updateNoteFields"),
/**
* Update the model, fields, and tags of an existing note. This allows you
* to change the note's model, update its fields with new content, and set
* new tags.
*/
updateNoteModel: this.build("updateNoteModel"),
/**
* Set a note's tags by note ID. Old tags will be removed.
*/
updateNoteTags: this.build("updateNoteTags")
};
/**
* **Statistic Actions**
*
* [Documentation](https://git.sr.ht/~foosoft/anki-connect/tree/25.11.9.0/item/README.md#statistic-actions)
*/
statistic = {
/**
* Requests all card reviews for a specified deck after a certain time.
* `startID` is the latest unix time not included in the result. Returns a
* list of 9-tuples `(reviewTime, cardID, usn, buttonPressed, newInterval,
* previousInterval, newFactor, reviewDuration, reviewType)`.
*/
cardReviews: this.build("cardReviews"),
/**
* Gets the collection statistics report.
*/
getCollectionStatsHTML: this.build("getCollectionStatsHTML"),
/**
* Returns the unix time of the latest review for the given deck. 0 if no
* review has ever been made for the deck.
*/
getLatestReviewID: this.build("getLatestReviewID"),
/**
* Gets the number of cards reviewed as a list of pairs of (`dateString`,
* `number`).
*/
getNumCardsReviewedByDay: this.build("getNumCardsReviewedByDay"),
/**
* Gets the count of cards that have been reviewed in the current day. (With
* day start time as configured by user in Anki).
*/
getNumCardsReviewedToday: this.build("getNumCardsReviewedToday"),
/**
* Requests all card reviews for each card ID. Returns a dictionary mapping
* each card ID to a list of dictionaries of the format:
*
* ```ts
* {
* "id": reviewTime,
* "usn": usn,
* "ease": buttonPressed,
* "ivl": newInterval,
* "lastIvl": previousInterval,
* "factor": newFactor,
* "time": reviewDuration,
* "type": reviewType,
* }
* ```
*
* The reason why these key values are used instead of the more descriptive
* counterparts is because these are the exact key values used in Anki's
* database.
*/
getReviewsOfCards: this.build("getReviewsOfCards"),
/**
* Inserts the given reviews into the database. Required format: list of
* 9-tuples `(reviewTime, cardID, usn, buttonPressed, newInterval,
* previousInterval, newFactor, reviewDuration, reviewType)`.
*/
insertReviews: this.build("insertReviews")
};
autoLaunch;
fetchAdapter;
host;
key;
port;
version;
constructor(options) {
this.host = options?.host ?? defaultYankiConnectOptions.host;
this.port = options?.port ?? defaultYankiConnectOptions.port;
this.version = options?.version ?? defaultYankiConnectOptions.version;
this.key = options?.key ?? defaultYankiConnectOptions.key;
this.autoLaunch = options?.autoLaunch ?? defaultYankiConnectOptions.autoLaunch;
if (defaultYankiConnectOptions.fetchAdapter === void 0) throw new Error("A fetch implementation is required");
this.fetchAdapter = options?.fetchAdapter ?? defaultYankiConnectOptions.fetchAdapter;
if ((PLATFORM !== "mac" || ENVIRONMENT !== "node") && this.autoLaunch !== false) {
console.warn("The autoLaunch option is only supported in a Node environment on macOS");
this.autoLaunch = false;
}
if (this.version !== 6) throw new Error("YankiConnect currently only supports AnkiConnect API version 6");
if (this.autoLaunch === "immediately") launchAnkiApp();
}
async invoke(action, params) {
let response;
let responseJson;
try {
response = await this.fetchAdapter(`${this.host}:${this.port}`, {
body: JSON.stringify({
action,
...this.key === void 0 ? {} : { key: this.key },
params,
version: this.version
}),
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/json"
},
method: "POST",
mode: "cors"
});
if (response === void 0) throw new Error("AnkiConnect response is undefined");
if (response.status !== 200) throw new Error(`AnkiConnect response status is ${response.status}`);
responseJson = await response.json();
if (this.autoLaunch !== false && responseJson.error === "collection is not available") throw new Error(responseJson.error);
} catch (error) {
if (this.autoLaunch !== false) {
console.warn(`Can't connect to Anki app, retrying with auto-launch...`);
await launchAnkiApp();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
if (params === void 0) return this.invoke(action);
return this.invoke(action, params);
}
throw error;
}
if (!("error" in responseJson)) throw new Error("response is missing required error field");
if (!("result" in responseJson)) throw new Error("response is missing required result field");
return responseJson;
}
build(action) {
return async (params) => {
const response = await this.invoke(action, params);
if (response.error !== null) throw new Error(response.error);
return response.result;
};
}
};
//#endregion
export { YankiConnect, defaultYankiConnectOptions };