anki_connect 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,11 +18,19 @@ module AnkiConnect
18
18
  request(:deckNamesAndIds)
19
19
  end
20
20
 
21
+ # Gets a deck name by ID.
22
+ #
23
+ # @param deck_id [Integer] Deck ID
24
+ # @return [String] Deck name
25
+ def deck_name_from_id(deck_id)
26
+ request(:deckNameFromId, deckId: deck_id)
27
+ end
28
+
21
29
  # Gets deck membership for given cards.
22
30
  #
23
31
  # @param card_ids [Array<Integer>] Array of card IDs
24
32
  # @return [Hash] Deck names mapped to arrays of card IDs
25
- def get_decks_for_cards(card_ids)
33
+ def decks_for_cards(card_ids)
26
34
  request(:getDecks, cards: card_ids)
27
35
  end
28
36
 
@@ -48,15 +56,17 @@ module AnkiConnect
48
56
  # @param names [Array<String>] Array of deck names
49
57
  # @param cards_too [Boolean] Must be true to confirm deletion
50
58
  # @return [nil]
51
- def delete_decks(names, cards_too: true)
59
+ def delete_decks(names, cards_too:)
60
+ raise ArgumentError, 'cards_too must be true to delete decks' unless cards_too == true
61
+
52
62
  request(:deleteDecks, decks: names, cardsToo: cards_too)
53
63
  end
54
64
 
55
65
  # Gets configuration for a deck.
56
66
  #
57
67
  # @param name [String] Deck name
58
- # @return [Hash] Configuration object
59
- def get_deck_config(name)
68
+ # @return [Hash, false] Configuration object, or false when the deck does not exist
69
+ def deck_config(name)
60
70
  request(:getDeckConfig, deck: name)
61
71
  end
62
72
 
@@ -100,7 +110,7 @@ module AnkiConnect
100
110
  #
101
111
  # @param names [Array<String>] Array of deck names
102
112
  # @return [Hash] Deck IDs mapped to stats objects
103
- def get_deck_stats(names)
113
+ def deck_stats(names)
104
114
  request(:getDeckStats, decks: names)
105
115
  end
106
116
  end
@@ -5,14 +5,27 @@ module AnkiConnect
5
5
  # Methods to interact with Anki's GUI windows and dialogs
6
6
  # (card browser, review screen, editing interfaces).
7
7
  module Graphical
8
+ REORDER_CARD_KEYS = {
9
+ 'order' => :order,
10
+ 'column_id' => :columnId
11
+ }.freeze
12
+ private_constant :REORDER_CARD_KEYS
13
+
8
14
  # Opens Card Browser dialog and searches for query.
9
15
  #
10
- # @param query [String] Search query string
11
- # @param reorder_cards [Hash, nil] (optional) Object with order (ascending/descending) and columnId
16
+ # @param query [String, nil] Search query string; nil preserves the current browser search
17
+ # @param reorder_cards [Hash, nil] Object with order and column_id
12
18
  # @return [Array<Integer>] Array of card IDs found
13
- def gui_browse(query, reorder_cards: nil)
14
- params = { query: query }
15
- params[:reorderCards] = reorder_cards if reorder_cards
19
+ def gui_browse(query = nil, reorder_cards: nil)
20
+ params = {}
21
+ params[:query] = query unless query.nil?
22
+ if reorder_cards
23
+ normalized = normalize_keys(reorder_cards, REORDER_CARD_KEYS, name: 'reorder_cards')
24
+ missing_keys = REORDER_CARD_KEYS.values.reject { |key| normalized.key?(key) }
25
+ raise ArgumentError, "missing reorder_cards keys: #{missing_keys.join(', ')}" unless missing_keys.empty?
26
+
27
+ params[:reorderCards] = normalized
28
+ end
16
29
  request(:guiBrowse, **params)
17
30
  end
18
31
 
@@ -34,10 +47,12 @@ module AnkiConnect
34
47
  # Opens Add Cards dialog with preset values.
35
48
  # Multiple invocations close old window and reopen with new values.
36
49
  #
37
- # @param note [Hash] Note object with deckName, modelName, fields, tags, and optional audio/video/picture
50
+ # @param note [Hash, nil] Optional note preset
38
51
  # @return [Integer] Note ID that would be created if user confirms
39
- def gui_add_cards(note)
40
- request(:guiAddCards, note: note)
52
+ def gui_add_cards(note = nil)
53
+ params = {}
54
+ params[:note] = normalize_gui_note(note) unless note.nil?
55
+ request(:guiAddCards, **params)
41
56
  end
42
57
 
43
58
  # Opens Edit dialog for a note.
@@ -52,16 +67,24 @@ module AnkiConnect
52
67
  # Sets fields/tags/deck/model in open Add Note dialog.
53
68
  # Returns error if Add Note dialog not open. Deck/model always replace; fields/tags respect append flag.
54
69
  #
55
- # @param note [Hash] Note object with optional deckName, modelName, fields, tags
70
+ # @param note [Hash] Note with optional deck_name, note_type_name, fields, tags, and media
56
71
  # @param append [Boolean] If true, appends to fields/tags; otherwise replaces (default: false)
57
- # @return [Boolean] true on success
72
+ # @return [Boolean, Hash] true on success, or an error hash when the dialog is closed
58
73
  def gui_add_note_set_data(note, append: false)
59
- request(:guiAddNoteSetData, note: note, append: append)
74
+ request(:guiAddNoteSetData, note: normalize_gui_note(note), append: append)
75
+ end
76
+
77
+ # Checks whether the review screen has an active card.
78
+ #
79
+ # @return [Boolean] true when review is active
80
+ def gui_review_active?
81
+ request(:guiReviewActive)
60
82
  end
61
83
 
62
84
  # Gets information about current card in review.
63
85
  #
64
- # @return [Hash, nil] Object with card info, or nil if not in review mode
86
+ # @return [Hash] Current card information
87
+ # @raise [APIError] If review is not active
65
88
  def gui_current_card
66
89
  request(:guiCurrentCard)
67
90
  end
@@ -69,7 +92,7 @@ module AnkiConnect
69
92
  # Starts/resets timer for current card.
70
93
  # Useful for accurate time tracking when displaying cards via API.
71
94
  #
72
- # @return [Boolean] true
95
+ # @return [Boolean] false when review is not active or no card is available
73
96
  def gui_start_card_timer
74
97
  request(:guiStartCardTimer)
75
98
  end
@@ -10,13 +10,16 @@ module AnkiConnect
10
10
  # @param data [String, nil] Base64-encoded contents
11
11
  # @param path [String, nil] Absolute file path
12
12
  # @param url [String, nil] URL to download from
13
+ # @param skip_hash [String, nil] MD5 hash that causes an identical upload to be skipped
13
14
  # @param overwrite [Boolean] If true, overwrites existing file
14
- # @return [String] Filename (possibly modified if overwrite=false)
15
- def store_media(filename, data: nil, path: nil, url: nil, overwrite: true)
15
+ # @return [String, nil] Filename, or nil when skipped by hash
16
+ def store_media(filename, data: nil, path: nil, url: nil, skip_hash: nil, overwrite: true)
17
+ sources = { data: data, path: path, url: url }.reject { |_key, value| value.nil? }
18
+ raise ArgumentError, 'provide exactly one of data, path, or url' unless sources.one?
19
+
16
20
  params = { filename: filename, deleteExisting: overwrite }
17
- params[:data] = data if data
18
- params[:path] = path if path
19
- params[:url] = url if url
21
+ params.merge!(sources)
22
+ params[:skipHash] = skip_hash unless skip_hash.nil?
20
23
  request(:storeMediaFile, **params)
21
24
  end
22
25
 
@@ -32,14 +35,14 @@ module AnkiConnect
32
35
  #
33
36
  # @param pattern [String] Glob pattern
34
37
  # @return [Array<String>] Array of filenames
35
- def list_media(pattern: '*')
38
+ def media_files(pattern: '*')
36
39
  request(:getMediaFilesNames, pattern: pattern)
37
40
  end
38
41
 
39
42
  # Gets the media folder path.
40
43
  #
41
44
  # @return [String] Absolute path
42
- def media_dir_path
45
+ def media_directory
43
46
  request(:getMediaDirPath)
44
47
  end
45
48
 
@@ -8,7 +8,7 @@ module AnkiConnect
8
8
  # Requests API permission (first call to establish trust).
9
9
  # Only method accepting requests from any origin. Shows popup for untrusted origins.
10
10
  #
11
- # @return [Hash] Object with permission (granted/denied), and optionally requireApiKey and version
11
+ # @return [Hash] Object with permission and optionally requireApikey and version
12
12
  def request_permission
13
13
  request(:requestPermission)
14
14
  end
@@ -16,7 +16,7 @@ module AnkiConnect
16
16
  # Gets AnkiConnect API version.
17
17
  #
18
18
  # @return [Integer] Version number (currently 6)
19
- def version
19
+ def api_version
20
20
  request(:version)
21
21
  end
22
22
 
@@ -25,7 +25,7 @@ module AnkiConnect
25
25
  # @param scopes [Array<String>] Array of scope names (currently only "actions" supported)
26
26
  # @param actions [Array<String>, nil] null for all actions, or array of action names to check (optional)
27
27
  # @return [Hash] Object with scopes used and available actions
28
- def api_reflect(scopes, actions: nil)
28
+ def api_capabilities(scopes, actions: nil)
29
29
  params = { scopes: scopes }
30
30
  params[:actions] = actions if actions
31
31
  request(:apiReflect, **params)
@@ -55,16 +55,18 @@ module AnkiConnect
55
55
  # Switches to specified profile.
56
56
  #
57
57
  # @param name [String] Profile name
58
- # @return [Boolean] true on success
58
+ # @return [Boolean] true on success, false when the profile does not exist
59
59
  def load_profile(name)
60
60
  request(:loadProfile, name: name)
61
61
  end
62
62
 
63
63
  # Performs multiple actions in one request.
64
+ # Unlike convenience wrappers, each action uses AnkiConnect's raw wire keys.
64
65
  #
65
- # @param actions [Array<Hash>] Array of action objects (each with action, version, params)
66
+ # @param actions [Array<Hash>] Raw wire action objects with action, version, and params
66
67
  # @return [Array] Array of responses in same order
67
- def multi(actions)
68
+ # @see #request Use request for individual actions
69
+ def batch(actions)
68
70
  request(:multi, actions: actions)
69
71
  end
70
72
 
@@ -80,9 +82,9 @@ module AnkiConnect
80
82
 
81
83
  # Imports .apkg file into collection.
82
84
  #
83
- # @param path [String] File path (relative to collection.media folder)
85
+ # @param path [String] Package file path on the Anki host
84
86
  # @return [Boolean] true on success, false otherwise
85
- def import_deck(path)
87
+ def import_package(path)
86
88
  request(:importPackage, path: path)
87
89
  end
88
90
 
@@ -0,0 +1,293 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiConnect
4
+ class Client
5
+ # Methods to create and modify note types.
6
+ module NoteTypes
7
+ # Gets complete list of note type names.
8
+ #
9
+ # @return [Array<String>] Array of note type names
10
+ def note_type_names
11
+ request(:modelNames)
12
+ end
13
+
14
+ # Gets note type names with their IDs.
15
+ #
16
+ # @return [Hash] Note type names mapped to IDs
17
+ def note_type_names_and_ids
18
+ request(:modelNamesAndIds)
19
+ end
20
+
21
+ # Gets a note type name by ID.
22
+ #
23
+ # @param note_type_id [Integer] Note type ID
24
+ # @return [String] Note type name
25
+ def note_type_name_from_id(note_type_id)
26
+ request(:modelNameFromId, modelId: note_type_id)
27
+ end
28
+
29
+ # Gets note types by ID.
30
+ #
31
+ # @param ids [Array<Integer>] Array of note type IDs
32
+ # @return [Array<Hash>] Array of note type objects
33
+ def note_types_by_id(ids)
34
+ request(:findModelsById, modelIds: ids)
35
+ end
36
+
37
+ # Gets note types by name.
38
+ #
39
+ # @param names [Array<String>] Array of note type names
40
+ # @return [Array<Hash>] Array of note type objects
41
+ def note_types_by_name(names)
42
+ request(:findModelsByName, modelNames: names)
43
+ end
44
+
45
+ # Gets field names for a note type.
46
+ #
47
+ # @param note_type_name [String] Note type name
48
+ # @return [Array<String>] Array of field names in order
49
+ def note_type_field_names(note_type_name)
50
+ request(:modelFieldNames, modelName: note_type_name)
51
+ end
52
+
53
+ # Gets field descriptions for a note type.
54
+ #
55
+ # @param note_type_name [String] Note type name
56
+ # @return [Array<String>] Array of description strings
57
+ def note_type_field_descriptions(note_type_name)
58
+ request(:modelFieldDescriptions, modelName: note_type_name)
59
+ end
60
+
61
+ # Gets field fonts and sizes for a note type.
62
+ #
63
+ # @param note_type_name [String] Note type name
64
+ # @return [Hash] Field names mapped to { font:, size: }
65
+ def note_type_field_fonts(note_type_name)
66
+ request(:modelFieldFonts, modelName: note_type_name)
67
+ end
68
+
69
+ # Gets fields used on templates.
70
+ #
71
+ # @param note_type_name [String] Note type name
72
+ # @return [Hash] Template names mapped to [questionFields, answerFields]
73
+ def note_type_fields_on_templates(note_type_name)
74
+ request(:modelFieldsOnTemplates, modelName: note_type_name)
75
+ end
76
+
77
+ # Creates a new note type.
78
+ #
79
+ # @param name [String] Note type name
80
+ # @param fields [Array<String>] Field names in order
81
+ # @param templates [Array<Hash>] Templates with name, front, and back
82
+ # @param css [String, nil] CSS styling
83
+ # @param cloze [Boolean] true for a cloze note type
84
+ # @return [Hash] Complete note type object
85
+ def create_note_type(name:, fields:, templates:, css: nil, cloze: false)
86
+ params = {
87
+ modelName: name,
88
+ inOrderFields: fields,
89
+ cardTemplates: templates.map { |template| normalize_template(template) },
90
+ isCloze: cloze
91
+ }
92
+ params[:css] = css if css
93
+ request(:createModel, **params)
94
+ end
95
+
96
+ # Gets templates for a note type.
97
+ #
98
+ # @param note_type_name [String] Note type name
99
+ # @return [Hash] Template names mapped to { Front:, Back: }
100
+ def note_type_templates(note_type_name)
101
+ request(:modelTemplates, modelName: note_type_name)
102
+ end
103
+
104
+ # Gets CSS styling for a note type.
105
+ #
106
+ # @param note_type_name [String] Note type name
107
+ # @return [Hash] Object with css property
108
+ def note_type_styling(note_type_name)
109
+ request(:modelStyling, modelName: note_type_name)
110
+ end
111
+
112
+ # Updates templates for a note type.
113
+ #
114
+ # @param name [String] Note type name
115
+ # @param templates [Hash] Template names mapped to optional front/back values
116
+ # @return [nil]
117
+ def update_note_type_templates(name, templates)
118
+ request(:updateModelTemplates, model: { name: name, templates: normalize_template_updates(templates) })
119
+ end
120
+
121
+ # Updates CSS styling for a note type.
122
+ #
123
+ # @param name [String] Note type name
124
+ # @param css [String] CSS styling
125
+ # @return [nil]
126
+ def update_note_type_styling(name, css:)
127
+ request(:updateModelStyling, model: { name: name, css: css })
128
+ end
129
+
130
+ # Find and replace in note type templates/CSS.
131
+ #
132
+ # @param note_type_name [String, nil] Note type name, or nil for all note types
133
+ # @param find [String] Text to find
134
+ # @param replace [String] Replacement text
135
+ # @param front [Boolean] Search front templates
136
+ # @param back [Boolean] Search back templates
137
+ # @param css [Boolean] Search CSS
138
+ # @return [Integer] Number of note types changed
139
+ def find_and_replace_in_note_type(note_type_name: nil, find:, replace:, front: true, back: true, css: true)
140
+ request(
141
+ :findAndReplaceInModels,
142
+ modelName: note_type_name,
143
+ findText: find,
144
+ replaceText: replace,
145
+ front: front,
146
+ back: back,
147
+ css: css
148
+ )
149
+ end
150
+
151
+ # Renames a template.
152
+ #
153
+ # @param note_type_name [String] Note type name
154
+ # @param from [String] Current template name
155
+ # @param to [String] New template name
156
+ # @return [nil]
157
+ def rename_note_type_template(note_type_name, from:, to:)
158
+ request(:modelTemplateRename, modelName: note_type_name, oldTemplateName: from, newTemplateName: to)
159
+ end
160
+
161
+ # Moves a template to a new position.
162
+ #
163
+ # @param note_type_name [String] Note type name
164
+ # @param template_name [String] Template name
165
+ # @param index [Integer] New position (0-based)
166
+ # @return [nil]
167
+ def reposition_note_type_template(note_type_name, template_name, index)
168
+ request(:modelTemplateReposition, modelName: note_type_name, templateName: template_name, index: index)
169
+ end
170
+
171
+ # Adds a template to a note type.
172
+ #
173
+ # @param note_type_name [String] Note type name
174
+ # @param template [Hash] Template with Name, Front, Back
175
+ # @return [nil]
176
+ def add_note_type_template(note_type_name, template)
177
+ request(:modelTemplateAdd, modelName: note_type_name, template: normalize_template(template))
178
+ end
179
+
180
+ # Removes a template from a note type.
181
+ #
182
+ # @param note_type_name [String] Note type name
183
+ # @param template_name [String] Template name
184
+ # @return [nil]
185
+ def remove_note_type_template(note_type_name, template_name)
186
+ request(:modelTemplateRemove, modelName: note_type_name, templateName: template_name)
187
+ end
188
+
189
+ # Renames a field.
190
+ #
191
+ # @param note_type_name [String] Note type name
192
+ # @param from [String] Current field name
193
+ # @param to [String] New field name
194
+ # @return [nil]
195
+ def rename_note_type_field(note_type_name, from:, to:)
196
+ request(:modelFieldRename, modelName: note_type_name, oldFieldName: from, newFieldName: to)
197
+ end
198
+
199
+ # Moves a field to a new position.
200
+ #
201
+ # @param note_type_name [String] Note type name
202
+ # @param field_name [String] Field name
203
+ # @param index [Integer] New position (0-based)
204
+ # @return [nil]
205
+ def reposition_note_type_field(note_type_name, field_name, index)
206
+ request(:modelFieldReposition, modelName: note_type_name, fieldName: field_name, index: index)
207
+ end
208
+
209
+ # Adds a field to a note type.
210
+ #
211
+ # @param note_type_name [String] Note type name
212
+ # @param field_name [String] Field name
213
+ # @param index [Integer, nil] Position (defaults to end)
214
+ # @return [nil]
215
+ def add_note_type_field(note_type_name, field_name, index: nil)
216
+ params = { modelName: note_type_name, fieldName: field_name }
217
+ params[:index] = index if index
218
+ request(:modelFieldAdd, **params)
219
+ end
220
+
221
+ # Removes a field from a note type.
222
+ #
223
+ # @param note_type_name [String] Note type name
224
+ # @param field_name [String] Field name
225
+ # @return [nil]
226
+ def remove_note_type_field(note_type_name, field_name)
227
+ request(:modelFieldRemove, modelName: note_type_name, fieldName: field_name)
228
+ end
229
+
230
+ # Sets font for a field.
231
+ #
232
+ # @param note_type_name [String] Note type name
233
+ # @param field_name [String] Field name
234
+ # @param font [String] Font name
235
+ # @return [nil]
236
+ def set_note_type_field_font(note_type_name, field_name, font)
237
+ request(:modelFieldSetFont, modelName: note_type_name, fieldName: field_name, font: font)
238
+ end
239
+
240
+ # Sets font size for a field.
241
+ #
242
+ # @param note_type_name [String] Note type name
243
+ # @param field_name [String] Field name
244
+ # @param size [Integer] Font size
245
+ # @return [nil]
246
+ def set_note_type_field_font_size(note_type_name, field_name, size)
247
+ request(:modelFieldSetFontSize, modelName: note_type_name, fieldName: field_name, fontSize: size)
248
+ end
249
+
250
+ # Sets description for a field.
251
+ #
252
+ # @param note_type_name [String] Note type name
253
+ # @param field_name [String] Field name
254
+ # @param description [String] Description text
255
+ # @return [Boolean] true on success
256
+ def set_note_type_field_description(note_type_name, field_name, description)
257
+ request(:modelFieldSetDescription, modelName: note_type_name, fieldName: field_name, description: description)
258
+ end
259
+
260
+ private
261
+
262
+ TEMPLATE_KEYS = {
263
+ 'name' => :Name,
264
+ 'front' => :Front,
265
+ 'back' => :Back
266
+ }.freeze
267
+ TEMPLATE_UPDATE_KEYS = {
268
+ 'front' => :Front,
269
+ 'back' => :Back
270
+ }.freeze
271
+ private_constant :TEMPLATE_KEYS, :TEMPLATE_UPDATE_KEYS
272
+
273
+ def normalize_template(template)
274
+ normalized = normalize_keys(template, TEMPLATE_KEYS, name: 'template')
275
+ missing_keys = TEMPLATE_KEYS.values.reject { |key| normalized.key?(key) }
276
+ raise ArgumentError, "missing template keys: #{missing_keys.join(', ')}" unless missing_keys.empty?
277
+
278
+ normalized
279
+ end
280
+
281
+ def normalize_template_updates(templates)
282
+ raise ArgumentError, 'templates must be a Hash' unless templates.is_a?(Hash)
283
+
284
+ templates.to_h do |name, template|
285
+ normalized = normalize_keys(template, TEMPLATE_UPDATE_KEYS, name: 'template update')
286
+ raise ArgumentError, 'template update must include front or back' if normalized.empty?
287
+
288
+ [name, normalized]
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end