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

2 years ago
2 years ago
2 years ago
2 years ago
  1. import axios, { AxiosInstance } from 'axios';
  2. import { IAnkiCard, IAnkiDeckConfig, IAnkiQuery } from './anki';
  3. import { logger } from './logger';
  4. export interface IAnkiConnectActions
  5. extends Record<
  6. string,
  7. {
  8. params?: Record<string, unknown>;
  9. result: unknown;
  10. }
  11. > {
  12. // Card Actions
  13. cardsInfo: {
  14. params: {
  15. cards: number[];
  16. };
  17. result: {
  18. answer: string;
  19. question: string;
  20. deckName: string;
  21. modelName: string;
  22. fieldOrder: number;
  23. fields: {
  24. [fieldName: string]: {
  25. value: string;
  26. order: number;
  27. };
  28. };
  29. css: string;
  30. cardId: number;
  31. interval: number;
  32. note: number;
  33. ord: number;
  34. type: number;
  35. queue: number;
  36. due: number;
  37. reps: number;
  38. lapses: number;
  39. left: number;
  40. mod: number;
  41. }[];
  42. };
  43. findCards: {
  44. params: {
  45. query: IAnkiQuery;
  46. };
  47. result: number[];
  48. };
  49. // Deck Actions
  50. deckNames: {
  51. result: string[];
  52. };
  53. getDecks: {
  54. params: {
  55. cards: number[];
  56. };
  57. result: {
  58. [deckName: string]: number[];
  59. };
  60. };
  61. changeDeck: {
  62. params: {
  63. cards: number[];
  64. deck: string;
  65. };
  66. result: null;
  67. };
  68. getDeckConfig: {
  69. params: {
  70. deck: string;
  71. };
  72. result: IAnkiDeckConfig;
  73. };
  74. setDeckConfigId: {
  75. params: {
  76. decks: string[];
  77. configId: number;
  78. };
  79. result: boolean;
  80. };
  81. // Media Actions
  82. storeMediaFile: {
  83. params: IAnkiConnectMedia;
  84. result: string;
  85. };
  86. retrieveMediaFile: {
  87. /**
  88. * Retrieves the base64-encoded contents of the specified file, returning `false` if the file does not exist.
  89. */
  90. params: {
  91. filename: string;
  92. };
  93. result: string | false;
  94. };
  95. getMediaFilesNames: {
  96. /**
  97. * Gets the names of media files matched the pattern. Returning all names by default.
  98. */
  99. params: {
  100. pattern: string;
  101. };
  102. result: string[];
  103. };
  104. deleteMediaFile: {
  105. params: {
  106. filename: string;
  107. };
  108. result: null;
  109. };
  110. // Miscellaneous Actions
  111. exportPackage: {
  112. /**
  113. * Exports a given deck in .apkg format.
  114. * Returns true if successful or false otherwise.
  115. */
  116. params: {
  117. deck: string;
  118. path: string;
  119. /**
  120. * The optional property includeSched (default is false) can be specified to include the cards scheduling data.
  121. */
  122. includeSched?: boolean;
  123. };
  124. result: boolean;
  125. };
  126. importPackage: {
  127. /**
  128. * Imports a file in .apkg format into the collection.
  129. * Returns true if successful or false otherwise.
  130. */
  131. params: {
  132. /**
  133. * Note that the file path is relative to Ankis collection.media folder, not to the client.
  134. */
  135. path: string;
  136. };
  137. result: boolean;
  138. };
  139. reloadCollection: {
  140. result: null;
  141. };
  142. // Model Actions
  143. modelNames: {
  144. result: string[];
  145. };
  146. modelNamesAndIds: {
  147. result: {
  148. [modelName: string]: number;
  149. };
  150. };
  151. modelFieldNames: {
  152. params: {
  153. modelName: string;
  154. };
  155. result: string[];
  156. };
  157. modelFieldsOnTemplates: {
  158. params: {
  159. modelName: string;
  160. };
  161. result: {
  162. [side: string]: [string[], string[]];
  163. };
  164. };
  165. createModel: {
  166. params: {
  167. modelName: string;
  168. inOrderFields: string[];
  169. /**
  170. * Default to built-in CSS
  171. */
  172. css?: string;
  173. isCloze?: boolean;
  174. cardTemplates: IAnkiConnectCardTemplate[];
  175. };
  176. result: IAnkiCard;
  177. };
  178. modelTemplates: {
  179. params: {
  180. modelName: string;
  181. };
  182. result: {
  183. [side: string]: {
  184. Front: string;
  185. Back: string;
  186. };
  187. };
  188. };
  189. modelStyling: {
  190. params: {
  191. modelName: string;
  192. };
  193. result: {
  194. css: string;
  195. };
  196. };
  197. updateModelTemplates: {
  198. /**
  199. * Modify the templates of an existing model by name.
  200. * Only specifies cards and specified sides will be modified.
  201. *
  202. * If an existing card or side is not included in the request, it will be left unchanged.
  203. */
  204. params: {
  205. model: {
  206. name: string;
  207. templates: {
  208. [side: string]: {
  209. Front?: string;
  210. Back?: string;
  211. };
  212. };
  213. };
  214. };
  215. result: null;
  216. };
  217. updateModelStyling: {
  218. params: {
  219. model: {
  220. name: string;
  221. css: string;
  222. };
  223. };
  224. result: null;
  225. };
  226. findAndReplaceInModels: {
  227. params: {
  228. model: {
  229. modelName: string;
  230. findText: string;
  231. replaceText: string;
  232. front?: boolean;
  233. back?: boolean;
  234. css?: boolean;
  235. };
  236. };
  237. result: number;
  238. };
  239. // Note Actions
  240. addNote: {
  241. params: {
  242. note: IAnkiConnectNote;
  243. };
  244. result: number | null;
  245. };
  246. addNotes: {
  247. params: {
  248. notes: IAnkiConnectNote[];
  249. };
  250. result: (number | null)[];
  251. };
  252. canAddNotes: {
  253. params: {
  254. notes: IAnkiConnectNote[];
  255. };
  256. result: boolean[];
  257. };
  258. updateNoteFields: {
  259. params: {
  260. note: {
  261. id: number;
  262. fields?: {
  263. [fieldName: string]: string;
  264. };
  265. audio?: IAnkiConnectMediaInNote[];
  266. video?: IAnkiConnectMediaInNote[];
  267. picture?: IAnkiConnectMediaInNote[];
  268. };
  269. };
  270. result: null;
  271. };
  272. addTags: {
  273. params: {
  274. notes: number[];
  275. /**
  276. * The docs don't make it clear if tags are space-separated.
  277. *
  278. * @link https://foosoft.net/projects/anki-connect/#miscellaneous-actions
  279. */
  280. tags: string;
  281. };
  282. result: null;
  283. };
  284. removeTags: {
  285. params: {
  286. notes: number[];
  287. /**
  288. * The docs don't make it clear if tags are space-separated.
  289. *
  290. * @link https://foosoft.net/projects/anki-connect/#miscellaneous-actions
  291. */
  292. tags: string;
  293. };
  294. result: null;
  295. };
  296. getTags: {
  297. result: string[];
  298. };
  299. cleanUnusedTags: {
  300. result: null;
  301. };
  302. replaceTags: {
  303. params: {
  304. notes: number[];
  305. tag_to_replace: string;
  306. replace_with_tag: string;
  307. };
  308. result: null;
  309. };
  310. replaceTagsInAllNotes: {
  311. params: {
  312. tag_to_replace: string;
  313. replace_with_tag: string;
  314. };
  315. result: null;
  316. };
  317. findNotes: {
  318. params: {
  319. query: IAnkiQuery;
  320. };
  321. result: number[];
  322. };
  323. notesInfo: {
  324. params: {
  325. notes: number[];
  326. };
  327. result: {
  328. noteId: number;
  329. modelName: string;
  330. tags: string[];
  331. fields: {
  332. [fieldName: string]: {
  333. value: string;
  334. ord: number;
  335. };
  336. };
  337. }[];
  338. };
  339. deleteNotes: {
  340. params: {
  341. notes: number[];
  342. };
  343. result: null;
  344. };
  345. removeEmptyNotes: {
  346. result: null;
  347. };
  348. }
  349. export interface IAnkiConnectCardTemplate {
  350. /**
  351. * By default the card names will be Card 1, Card 2, and so on.
  352. */
  353. Name?: string;
  354. Front: string;
  355. Back: string;
  356. }
  357. export interface IAnkiConnectNote {
  358. deckName: string;
  359. modelName: string;
  360. fields: {
  361. [fieldName: string]: string;
  362. };
  363. options?: {
  364. /**
  365. * The allowDuplicate member inside options group can be set to true to enable adding duplicate cards.
  366. * Normally duplicate cards can not be added and trigger exception.
  367. */
  368. allowDuplicate: boolean;
  369. /**
  370. * The duplicateScope member inside options can be used to specify the scope for which duplicates are checked.
  371. * A value of "deck" will only check for duplicates in the target deck;
  372. * any other value will check the entire collection.
  373. */
  374. duplicateScope?: string;
  375. duplicateScopeOptions?: {
  376. /**
  377. * Specify which deck to use for checking duplicates in.
  378. * If undefined or null, the target deck will be used.
  379. */
  380. deckName?: string | null;
  381. /**
  382. * Change whether or not duplicate cards are checked in child decks.
  383. * The default value is false.
  384. */
  385. checkChildren?: boolean;
  386. /**
  387. * Specifies whether duplicate checks are performed across all note types.
  388. * The default value is false.
  389. */
  390. checkAllModel?: boolean;
  391. };
  392. };
  393. tags: string[];
  394. audio?: IAnkiConnectMediaInNote[];
  395. video?: IAnkiConnectMediaInNote[];
  396. picture?: IAnkiConnectMediaInNote[];
  397. }
  398. export type IAnkiConnectMedia = (
  399. | {
  400. /**
  401. * Specified base64-encoded contents
  402. */
  403. data: string;
  404. }
  405. | {
  406. path: string;
  407. }
  408. | {
  409. url: string;
  410. }
  411. ) & {
  412. /**
  413. * To prevent Anki from removing files not used by any cards (e.g. for configuration files),
  414. * prefix the filename with an underscore.
  415. */
  416. filename: string;
  417. /**
  418. * Any existing file with the same name is deleted by default.
  419. * Set `deleteExisting` to `false` to prevent that by letting Anki give the new file a non-conflicting name.
  420. */
  421. deleteExisting?: boolean;
  422. };
  423. export type IAnkiConnectMediaInNote = IAnkiConnectMedia & {
  424. /**
  425. * The skipHash field can be optionally provided to skip the inclusion of files with an MD5 hash that matches the provided value.
  426. */
  427. skipHash?: string;
  428. /**
  429. * The fields member is a list of fields that should play audio or video,
  430. * or show a picture when the card is displayed in Anki.
  431. */
  432. fields?: string[];
  433. };
  434. export class AnkiConnect {
  435. $axios: AxiosInstance;
  436. constructor(public url = 'http://localhost:8765', public version = 6) {
  437. this.$axios = axios.create({
  438. baseURL: url,
  439. });
  440. }
  441. async api<A extends keyof IAnkiConnectActions>(
  442. action: A,
  443. params: IAnkiConnectActions[A]['params'],
  444. version = this.version,
  445. ): Promise<IAnkiConnectActions[A]['result']> {
  446. logger(`AnkiConnect: calling ${action}`, params);
  447. const { data: response } = await this.$axios.post<{
  448. result: any;
  449. error: string | null;
  450. }>('/', { action, params, version });
  451. if (Object.getOwnPropertyNames(response).length != 2) {
  452. throw 'response has an unexpected number of fields';
  453. }
  454. if (!response.hasOwnProperty('error')) {
  455. throw 'response is missing required error field';
  456. }
  457. if (!response.hasOwnProperty('result')) {
  458. throw 'response is missing required result field';
  459. }
  460. if (response.error) {
  461. throw response.error;
  462. }
  463. logger(`AnkiConnect: finished ${action}`, params);
  464. return response.result;
  465. }
  466. /**
  467. * Strongly-typed usage is via `<[action1, action2, ...]>`:
  468. *
  469. * ```typescript
  470. * await new AnkiConnect().multi<['findNotes', 'getTags']>({
  471. * actions: [
  472. * {
  473. * action: 'findNotes',
  474. * params: {
  475. * query: 'added:1',
  476. * },
  477. * },
  478. * { action: 'getTags' }
  479. * ]
  480. * });
  481. * ```
  482. */
  483. async multi<Actions extends (keyof IAnkiConnectActions)[]>(
  484. params: {
  485. actions: {
  486. [K in keyof Actions]: Actions[K] extends keyof IAnkiConnectActions
  487. ? {
  488. action: Actions[K];
  489. } & ('params' extends keyof IAnkiConnectActions[Actions[K]]
  490. ? {
  491. params: IAnkiConnectActions[Actions[K]]['params'];
  492. }
  493. : {})
  494. : never;
  495. };
  496. },
  497. version = this.version,
  498. ): Promise<{
  499. [K in keyof Actions]: Actions[K] extends keyof IAnkiConnectActions
  500. ? // { result: IAnkiConnectActions[Actions[K]]['result']; error: string | null; }
  501. // On try-testing - error fields not returned, unlike in the docs.
  502. IAnkiConnectActions[Actions[K]]['result']
  503. : never;
  504. }> {
  505. return this.api('multi', params, version) as any;
  506. }
  507. }