strongly typed AnkiConnect
This commit is contained in:
1 parent
9516f287c8
commit
a144c7fd74
3 files changed
+265
-57
No files matched your search
@@ -0,0 +1,27 @@
|
|||||||
|
import { AnkiConnect } from '@/ankiconnect';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const anki = new AnkiConnect();
|
||||||
|
console.log(
|
||||||
|
await anki.multi<['findNotes', 'getTags']>({
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'findNotes',
|
||||||
|
params: {
|
||||||
|
query: 'added:1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ action: 'getTags' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
await anki.api('findNotes', {
|
||||||
|
query: 'deck:Takoboto',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Query syntax is documented here.
|
||||||
|
*
|
||||||
|
* @link https://docs.ankiweb.net/searching.html
|
||||||
|
*/
|
||||||
|
export type IAnkiQuery = string;
|
||||||
|
|
||||||
export interface IAnkiCard {
|
export interface IAnkiCard {
|
||||||
sortf: number;
|
sortf: number;
|
||||||
did: number;
|
did: number;
|
||||||
|
|||||||
+231
-57
@@ -1,11 +1,6 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
|
||||||
import { IAnkiCard } from './anki';
|
import { IAnkiCard, IAnkiQuery } from './anki';
|
||||||
|
|
||||||
export interface IAnkiConnectResponse<K extends keyof IAnkiConnectActions> {
|
|
||||||
result: IAnkiConnectActions[K];
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAnkiConnectActions
|
export interface IAnkiConnectActions
|
||||||
extends Record<
|
extends Record<
|
||||||
@@ -37,31 +32,7 @@ export interface IAnkiConnectActions
|
|||||||
|
|
||||||
// Media Actions
|
// Media Actions
|
||||||
storeMediaFile: {
|
storeMediaFile: {
|
||||||
params: (
|
params: 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;
|
|
||||||
};
|
|
||||||
result: string;
|
result: string;
|
||||||
};
|
};
|
||||||
retrieveMediaFile: {
|
retrieveMediaFile: {
|
||||||
@@ -222,8 +193,112 @@ export interface IAnkiConnectActions
|
|||||||
|
|
||||||
// Note Actions
|
// Note Actions
|
||||||
addNote: {
|
addNote: {
|
||||||
params: {};
|
params: {
|
||||||
result: number;
|
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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +311,86 @@ export interface IAnkiConnectCardTemplate {
|
|||||||
Back: 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 {
|
export class AnkiConnect {
|
||||||
$axios: AxiosInstance;
|
$axios: AxiosInstance;
|
||||||
|
|
||||||
@@ -248,31 +403,8 @@ export class AnkiConnect {
|
|||||||
async api<A extends keyof IAnkiConnectActions>(
|
async api<A extends keyof IAnkiConnectActions>(
|
||||||
action: A,
|
action: A,
|
||||||
params: IAnkiConnectActions[A]['params'],
|
params: IAnkiConnectActions[A]['params'],
|
||||||
version?: number,
|
|
||||||
): Promise<IAnkiConnectActions[A]['result']>;
|
|
||||||
|
|
||||||
async api<Results extends IAnkiConnectResponse<any>[]>(
|
|
||||||
action: 'multi',
|
|
||||||
params: {
|
|
||||||
actions: Array<
|
|
||||||
{
|
|
||||||
[A in keyof IAnkiConnectActions]: {
|
|
||||||
action: A;
|
|
||||||
params: IAnkiConnectActions[A]['params'];
|
|
||||||
};
|
|
||||||
}[keyof IAnkiConnectActions]
|
|
||||||
>;
|
|
||||||
},
|
|
||||||
version?: number,
|
|
||||||
): Promise<{
|
|
||||||
result: Results;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
async api<A extends keyof IAnkiConnectActions | 'multi'>(
|
|
||||||
action: A,
|
|
||||||
params: unknown,
|
|
||||||
version = this.version,
|
version = this.version,
|
||||||
) {
|
): Promise<IAnkiConnectActions[A]['result']> {
|
||||||
const { data: response } = await this.$axios.post<{
|
const { data: response } = await this.$axios.post<{
|
||||||
result: any;
|
result: any;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
@@ -293,4 +425,46 @@ export class AnkiConnect {
|
|||||||
|
|
||||||
return response.result;
|
return response.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strongly-typed usage is via `[string, tuple]`:
|
||||||
|
*
|
||||||
|
* ```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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user