|
|
- import axios, { AxiosInstance } from 'axios';
-
- import { IAnkiCard, IAnkiDeckConfig, IAnkiQuery } from './anki';
- import { logger } from './logger';
-
- export interface IAnkiConnectActions
- extends Record<
- string,
- {
- params?: Record<string, unknown>;
- result: unknown;
- }
- > {
- // Card Actions
- cardsInfo: {
- params: {
- cards: number[];
- };
- result: {
- answer: string;
- question: string;
- deckName: string;
- modelName: string;
- fieldOrder: number;
- fields: {
- [fieldName: string]: {
- value: string;
- order: number;
- };
- };
- css: string;
- cardId: number;
- interval: number;
- note: number;
- ord: number;
- type: number;
- queue: number;
- due: number;
- reps: number;
- lapses: number;
- left: number;
- mod: number;
- }[];
- };
- findCards: {
- params: {
- query: IAnkiQuery;
- };
- result: number[];
- };
-
- // Deck Actions
- deckNames: {
- result: string[];
- };
- getDecks: {
- params: {
- cards: number[];
- };
- result: {
- [deckName: string]: number[];
- };
- };
- changeDeck: {
- params: {
- cards: number[];
- deck: string;
- };
- result: null;
- };
- getDeckConfig: {
- params: {
- deck: string;
- };
- result: IAnkiDeckConfig;
- };
- setDeckConfigId: {
- params: {
- decks: string[];
- configId: number;
- };
- result: boolean;
- };
-
- // Media Actions
- storeMediaFile: {
- params: IAnkiConnectMedia;
- result: string;
- };
- retrieveMediaFile: {
- /**
- * Retrieves the base64-encoded contents of the specified file, returning `false` if the file does not exist.
- */
- params: {
- filename: string;
- };
- result: string | false;
- };
- getMediaFilesNames: {
- /**
- * Gets the names of media files matched the pattern. Returning all names by default.
- */
- params: {
- pattern: string;
- };
- result: string[];
- };
- deleteMediaFile: {
- params: {
- filename: string;
- };
- result: null;
- };
-
- // Miscellaneous Actions
- exportPackage: {
- /**
- * Exports a given deck in .apkg format.
- * Returns true if successful or false otherwise.
- */
- params: {
- deck: string;
- path: string;
- /**
- * The optional property includeSched (default is false) can be specified to include the cards’ scheduling data.
- */
- includeSched?: boolean;
- };
- result: boolean;
- };
- importPackage: {
- /**
- * Imports a file in .apkg format into the collection.
- * Returns true if successful or false otherwise.
- */
- params: {
- /**
- * Note that the file path is relative to Anki’s collection.media folder, not to the client.
- */
- path: string;
- };
- result: boolean;
- };
- reloadCollection: {
- result: null;
- };
-
- // Model Actions
- modelNames: {
- result: string[];
- };
- modelNamesAndIds: {
- result: {
- [modelName: string]: number;
- };
- };
- modelFieldNames: {
- params: {
- modelName: string;
- };
- result: string[];
- };
- modelFieldsOnTemplates: {
- params: {
- modelName: string;
- };
- result: {
- [side: string]: [string[], string[]];
- };
- };
- createModel: {
- params: {
- modelName: string;
- inOrderFields: string[];
- /**
- * Default to built-in CSS
- */
- css?: string;
- isCloze?: boolean;
- cardTemplates: IAnkiConnectCardTemplate[];
- };
- result: IAnkiCard;
- };
- modelTemplates: {
- params: {
- modelName: string;
- };
- result: {
- [side: string]: {
- Front: string;
- Back: string;
- };
- };
- };
- modelStyling: {
- params: {
- modelName: string;
- };
- result: {
- css: string;
- };
- };
- updateModelTemplates: {
- /**
- * 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.
- */
- params: {
- model: {
- name: string;
- templates: {
- [side: string]: {
- Front?: string;
- Back?: string;
- };
- };
- };
- };
- result: null;
- };
- updateModelStyling: {
- params: {
- model: {
- name: string;
- css: string;
- };
- };
- result: null;
- };
- findAndReplaceInModels: {
- params: {
- model: {
- modelName: string;
- findText: string;
- replaceText: string;
- front?: boolean;
- back?: boolean;
- css?: boolean;
- };
- };
- result: number;
- };
-
- // Note Actions
- addNote: {
- params: {
- note: IAnkiConnectNote;
- };
- result: number | null;
- };
- addNotes: {
- params: {
- notes: IAnkiConnectNote[];
- };
- result: (number | null)[];
- };
- canAddNotes: {
- params: {
- notes: IAnkiConnectNote[];
- };
- result: boolean[];
- };
- updateNoteFields: {
- params: {
- note: {
- id: number;
- fields?: {
- [fieldName: string]: string;
- };
- audio?: IAnkiConnectMediaInNote[];
- video?: IAnkiConnectMediaInNote[];
- picture?: IAnkiConnectMediaInNote[];
- };
- };
- result: null;
- };
- addTags: {
- params: {
- notes: number[];
- /**
- * The docs don't make it clear if tags are space-separated.
- *
- * @link https://foosoft.net/projects/anki-connect/#miscellaneous-actions
- */
- tags: string;
- };
- result: null;
- };
- removeTags: {
- params: {
- notes: number[];
- /**
- * The docs don't make it clear if tags are space-separated.
- *
- * @link https://foosoft.net/projects/anki-connect/#miscellaneous-actions
- */
- tags: string;
- };
- result: null;
- };
- getTags: {
- result: string[];
- };
- cleanUnusedTags: {
- result: null;
- };
- replaceTags: {
- params: {
- notes: number[];
- tag_to_replace: string;
- replace_with_tag: string;
- };
- result: null;
- };
- replaceTagsInAllNotes: {
- params: {
- tag_to_replace: string;
- replace_with_tag: string;
- };
- result: null;
- };
- findNotes: {
- params: {
- query: IAnkiQuery;
- };
- result: number[];
- };
- notesInfo: {
- params: {
- notes: number[];
- };
- result: {
- noteId: number;
- modelName: string;
- tags: string[];
- fields: {
- [fieldName: string]: {
- value: string;
- ord: number;
- };
- };
- }[];
- };
- deleteNotes: {
- params: {
- notes: number[];
- };
- result: null;
- };
- removeEmptyNotes: {
- result: null;
- };
- }
-
- export interface IAnkiConnectCardTemplate {
- /**
- * By default the card names will be Card 1, Card 2, and so on.
- */
- Name?: string;
- Front: string;
- Back: string;
- }
-
- export interface IAnkiConnectNote {
- deckName: string;
- modelName: string;
- fields: {
- [fieldName: string]: string;
- };
- options?: {
- /**
- * 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.
- */
- allowDuplicate: boolean;
- /**
- * 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.
- */
- duplicateScope?: string;
- duplicateScopeOptions?: {
- /**
- * Specify which deck to use for checking duplicates in.
- * If undefined or null, the target deck will be used.
- */
- deckName?: string | null;
- /**
- * Change whether or not duplicate cards are checked in child decks.
- * The default value is false.
- */
- checkChildren?: boolean;
- /**
- * Specifies whether duplicate checks are performed across all note types.
- * The default value is false.
- */
- checkAllModel?: boolean;
- };
- };
- tags: string[];
- audio?: IAnkiConnectMediaInNote[];
- video?: IAnkiConnectMediaInNote[];
- picture?: IAnkiConnectMediaInNote[];
- }
-
- export type IAnkiConnectMedia = (
- | {
- /**
- * Specified base64-encoded contents
- */
- data: string;
- }
- | {
- path: string;
- }
- | {
- url: string;
- }
- ) & {
- /**
- * To prevent Anki from removing files not used by any cards (e.g. for configuration files),
- * prefix the filename with an underscore.
- */
- filename: string;
- /**
- * 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.
- */
- deleteExisting?: boolean;
- };
-
- export type IAnkiConnectMediaInNote = IAnkiConnectMedia & {
- /**
- * The skipHash field can be optionally provided to skip the inclusion of files with an MD5 hash that matches the provided value.
- */
- skipHash?: string;
- /**
- * The fields member is a list of fields that should play audio or video,
- * or show a picture when the card is displayed in Anki.
- */
- fields?: string[];
- };
-
- export class AnkiConnect {
- $axios: AxiosInstance;
-
- constructor(public url = 'http://localhost:8765', public version = 6) {
- this.$axios = axios.create({
- baseURL: url,
- });
- }
-
- async api<A extends keyof IAnkiConnectActions>(
- action: A,
- params: IAnkiConnectActions[A]['params'],
- version = this.version,
- ): Promise<IAnkiConnectActions[A]['result']> {
- logger(`AnkiConnect: calling ${action}`, params);
-
- const { data: response } = await this.$axios.post<{
- result: any;
- error: string | null;
- }>('/', { action, params, version });
-
- if (Object.getOwnPropertyNames(response).length != 2) {
- throw 'response has an unexpected number of fields';
- }
- if (!response.hasOwnProperty('error')) {
- throw 'response is missing required error field';
- }
- if (!response.hasOwnProperty('result')) {
- throw 'response is missing required result field';
- }
- if (response.error) {
- throw response.error;
- }
-
- logger(`AnkiConnect: finished ${action}`, params);
-
- return response.result;
- }
-
- /**
- * Strongly-typed usage is via `<[action1, action2, ...]>`:
- *
- * ```typescript
- * await new AnkiConnect().multi<['findNotes', 'getTags']>({
- * actions: [
- * {
- * action: 'findNotes',
- * params: {
- * query: 'added:1',
- * },
- * },
- * { action: 'getTags' }
- * ]
- * });
- * ```
- */
- async multi<Actions extends (keyof IAnkiConnectActions)[]>(
- params: {
- actions: {
- [K in keyof Actions]: Actions[K] extends keyof IAnkiConnectActions
- ? {
- action: Actions[K];
- } & ('params' extends keyof IAnkiConnectActions[Actions[K]]
- ? {
- params: IAnkiConnectActions[Actions[K]]['params'];
- }
- : {})
- : never;
- };
- },
- version = this.version,
- ): Promise<{
- [K in keyof Actions]: Actions[K] extends keyof IAnkiConnectActions
- ? // { result: IAnkiConnectActions[Actions[K]]['result']; error: string | null; }
- // On try-testing - error fields not returned, unlike in the docs.
- IAnkiConnectActions[Actions[K]]['result']
- : never;
- }> {
- return this.api('multi', params, version) as any;
- }
- }
|