Extra contents beyond WaniKani
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

526 lines
11 KiB

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;
}
}