anki_connect 0.1.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 12cd7e1f5f63083dfb30d873a1ba70985b90b2107dcb50de09f151b8414532aa
4
+ data.tar.gz: 03145b84fe4f8ef9bb7412d2bc65f79dafffcdc9a8758dc0ff84e3b1263f41f1
5
+ SHA512:
6
+ metadata.gz: 46dffccf988e0497c76c7129d40be5b2becd687615730d3b53ff515eceb2a0ddc93d991929c7de8b56e2fae8d57e7419c21d92059df636a125d5d49ab03ab2d9
7
+ data.tar.gz: 261dd1bcd832014c623ee8c87ade177c0084e0d578e6808f138e6170a7464110ee98c5e07e20244a6e2b85bef72ddeb1f7e97f71c25a2a481e3eb4830fcd89f2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vadik49b
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # AnkiConnect Ruby
2
+
3
+ [AnkiConnect](https://git.sr.ht/~foosoft/anki-connect) provides a simple HTTP API to communicate with Anki. This Ruby gem is a wrapper around that API.
4
+
5
+ ## Requirements
6
+
7
+ - Ruby 3.4+
8
+ - Anki with Anki-Connect plugin installed
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'anki_connect'
16
+ ```
17
+
18
+ Or install it yourself:
19
+
20
+ ```bash
21
+ gem install anki_connect
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```ruby
27
+ require 'anki_connect'
28
+
29
+ # Create a client (default: localhost:8765)
30
+ client = AnkiConnect::Client.new
31
+
32
+ # Get all decks
33
+ decks = client.deck_names
34
+
35
+ # Add a new note
36
+ note_id = client.add_note(
37
+ deck_name: "Default",
38
+ model_name: "Basic",
39
+ fields: { Front: "What is Ruby?", Back: "A programming language" },
40
+ tags: ["programming"]
41
+ )
42
+
43
+ # Get note details
44
+ notes = client.get_notes(query: "deck:Default")
45
+ # => [{ "noteId" => 1234567890,
46
+ # "modelName" => "Basic",
47
+ # "tags" => ["programming"],
48
+ # "fields" => { "Front" => { "value" => "What is Ruby?", "order" => 0 },
49
+ # "Back" => { "value" => "A programming language", "order" => 1 } } }]
50
+ ```
51
+
52
+ For more examples, see the [`examples/`](examples/) directory.
53
+
54
+ ## Development
55
+
56
+ ```bash
57
+ # Install dependencies
58
+ bundle install
59
+
60
+ # Open console with gem loaded
61
+ bundle exec rake console
62
+ ```
63
+
64
+ ## Acknowledgments
65
+
66
+ - [Anki-Connect](https://git.sr.ht/~foosoft/anki-connect) by FooSoft for the excellent Anki plugin
67
+ - [Anki](https://apps.ankiweb.net/) for the amazing spaced repetition software
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiConnect
4
+ class Client
5
+ # Methods to query, modify, suspend, and manage individual flashcards.
6
+ module Cards
7
+ # Gets ease factors for cards.
8
+ #
9
+ # @param card_ids [Array<Integer>] Array of card IDs
10
+ # @return [Array<Integer>] Array of ease factor values
11
+ def get_ease_factors(card_ids)
12
+ request(:getEaseFactors, cards: card_ids)
13
+ end
14
+
15
+ # Sets ease factors for cards.
16
+ #
17
+ # @param card_ids [Array<Integer>] Array of card IDs
18
+ # @param factors [Array<Integer>] Array of ease factor values
19
+ # @return [Array<Boolean>] Array indicating success for each card
20
+ def set_ease_factors(card_ids, factors)
21
+ request(:setEaseFactors, cards: card_ids, easeFactors: factors)
22
+ end
23
+
24
+ # Sets specific database values for a single card.
25
+ #
26
+ # @param card_id [Integer] Card ID
27
+ # @param fields [Hash] Database field names to new values
28
+ # @param warning_check [Boolean] Must be true for certain risky keys
29
+ # @return [Array<Boolean>] Array indicating success for each field
30
+ def update_card(card_id, fields, warning_check: false)
31
+ request(:setSpecificValueOfCard, card: card_id, keys: fields.keys, newValues: fields.values,
32
+ warning_check: warning_check)
33
+ end
34
+
35
+ # Suspends cards.
36
+ #
37
+ # @param card_ids [Array<Integer>] Array of card IDs
38
+ # @return [Boolean] true if at least one card wasn't already suspended
39
+ def suspend_cards(card_ids)
40
+ request(:suspend, cards: card_ids)
41
+ end
42
+
43
+ # Unsuspends cards.
44
+ #
45
+ # @param card_ids [Array<Integer>] Array of card IDs
46
+ # @return [Boolean] true if at least one card was previously suspended
47
+ def unsuspend_cards(card_ids)
48
+ request(:unsuspend, cards: card_ids)
49
+ end
50
+
51
+ # Checks suspension status for cards.
52
+ #
53
+ # @param card_ids [Integer, Array<Integer>] Single card ID or array
54
+ # @return [Boolean, Array<Boolean, nil>] Boolean for single, array for multiple
55
+ def suspended?(card_ids)
56
+ if card_ids.is_a?(Array)
57
+ request(:areSuspended, cards: card_ids)
58
+ else
59
+ request(:suspended, card: card_ids)
60
+ end
61
+ end
62
+
63
+ # Checks if cards are due for review.
64
+ #
65
+ # @param card_ids [Integer, Array<Integer>] Single card ID or array
66
+ # @return [Boolean, Array<Boolean>] Boolean for single, array for multiple
67
+ def due?(card_ids)
68
+ if card_ids.is_a?(Array)
69
+ request(:areDue, cards: card_ids)
70
+ else
71
+ request(:areDue, cards: [card_ids]).first
72
+ end
73
+ end
74
+
75
+ # Gets intervals for cards.
76
+ #
77
+ # @param card_ids [Array<Integer>] Array of card IDs
78
+ # @param complete [Boolean] If true, returns all intervals
79
+ # @return [Array<Integer>, Array<Array<Integer>>] Intervals
80
+ def get_intervals(card_ids, complete: false)
81
+ request(:getIntervals, cards: card_ids, complete: complete)
82
+ end
83
+
84
+ # Searches for cards matching a query.
85
+ #
86
+ # @param query [String] Anki search query string
87
+ # @return [Array<Integer>] Array of card IDs
88
+ def search_cards(query)
89
+ request(:findCards, query: query)
90
+ end
91
+
92
+ # Converts card IDs to their parent note IDs.
93
+ #
94
+ # @param card_ids [Array<Integer>] Array of card IDs
95
+ # @return [Array<Integer>] Array of note IDs
96
+ def get_note_ids(card_ids)
97
+ request(:cardsToNotes, cards: card_ids)
98
+ end
99
+
100
+ # Gets modification times for cards.
101
+ #
102
+ # @param card_ids [Array<Integer>] Array of card IDs
103
+ # @return [Array<Hash>] Array of objects with cardId and mod
104
+ def get_cards_mod_time(card_ids)
105
+ request(:cardsModTime, cards: card_ids)
106
+ end
107
+
108
+ # Gets detailed information about cards.
109
+ #
110
+ # @param card_ids [Array<Integer>] Array of card IDs
111
+ # @return [Array<Hash>] Array of card objects
112
+ def get_cards(card_ids)
113
+ request(:cardsInfo, cards: card_ids)
114
+ end
115
+
116
+ # Resets cards to "new" status.
117
+ #
118
+ # @param card_ids [Array<Integer>] Array of card IDs
119
+ # @return [nil]
120
+ def forget_cards(card_ids)
121
+ request(:forgetCards, cards: card_ids)
122
+ end
123
+
124
+ # Makes cards enter "relearning" state.
125
+ #
126
+ # @param card_ids [Array<Integer>] Array of card IDs
127
+ # @return [nil]
128
+ def relearn_cards(card_ids)
129
+ request(:relearnCards, cards: card_ids)
130
+ end
131
+
132
+ # Answers cards programmatically.
133
+ #
134
+ # @param answers [Array<Hash>] Array of { cardId:, ease: } (1=Again, 2=Hard, 3=Good, 4=Easy)
135
+ # @return [Array<Boolean>] Array indicating if each card exists
136
+ def answer_cards(answers)
137
+ request(:answerCards, answers: answers)
138
+ end
139
+
140
+ # Sets due date for cards.
141
+ #
142
+ # @param card_ids [Array<Integer>] Array of card IDs
143
+ # @param days [String, Integer] Due date (0=today, 1!=tomorrow, 3-7=random range)
144
+ # @return [Boolean] true on success
145
+ def set_due_date(card_ids, days)
146
+ request(:setDueDate, cards: card_ids, days: days)
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module AnkiConnect
8
+ # Main client class that includes all API modules and provides
9
+ # the core request mechanism.
10
+ class Client
11
+ include AnkiConnect::Client::Cards
12
+ include AnkiConnect::Client::Decks
13
+ include AnkiConnect::Client::Models
14
+ include AnkiConnect::Client::Notes
15
+ include AnkiConnect::Client::Media
16
+ include AnkiConnect::Client::Graphical
17
+ include AnkiConnect::Client::Statistics
18
+ include AnkiConnect::Client::Miscellaneous
19
+
20
+ # @return [String] AnkiConnect server host
21
+ attr_reader :host
22
+ # @return [Integer] AnkiConnect server port
23
+ attr_reader :port
24
+ # @return [String, nil] API key for authentication (if configured)
25
+ attr_reader :api_key
26
+
27
+ # Creates a new AnkiConnect client.
28
+ #
29
+ # @param host [String] AnkiConnect server host (default: "127.0.0.1")
30
+ # @param port [Integer] AnkiConnect server port (default: 8765)
31
+ # @param api_key [String, nil] Optional API key for authentication
32
+ def initialize(host: '127.0.0.1', port: 8765, api_key: nil)
33
+ @host = host
34
+ @port = port
35
+ @api_key = api_key
36
+ @uri = URI("http://#{host}:#{port}")
37
+ end
38
+
39
+ # Makes a request to the AnkiConnect API.
40
+ # This is the core method used by all API operations.
41
+ #
42
+ # @param action [Symbol] The API action to perform
43
+ # @param params [Hash] Parameters to send with the request
44
+ # @return [Object] The result from the API
45
+ # @raise [Error] If the API returns an error
46
+ def request(action, **params)
47
+ body = {
48
+ action: action,
49
+ version: API_VERSION,
50
+ params: params
51
+ }
52
+ body[:key] = @api_key if @api_key
53
+
54
+ response = Net::HTTP.post(
55
+ @uri,
56
+ body.to_json,
57
+ 'Content-Type' => 'application/json'
58
+ )
59
+
60
+ result = JSON.parse(response.body)
61
+
62
+ raise Error, result['error'] if result['error']
63
+
64
+ result['result']
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :uri
70
+ end
71
+
72
+ # Error raised when the AnkiConnect API returns an error response.
73
+ class Error < StandardError; end
74
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiConnect
4
+ class Client
5
+ # Methods to create, configure, and manage decks and their settings.
6
+ module Decks
7
+ # Gets complete list of deck names.
8
+ #
9
+ # @return [Array<String>] Array of deck name strings
10
+ def deck_names
11
+ request(:deckNames)
12
+ end
13
+
14
+ # Gets deck names with their IDs.
15
+ #
16
+ # @return [Hash] Deck names mapped to IDs
17
+ def deck_names_and_ids
18
+ request(:deckNamesAndIds)
19
+ end
20
+
21
+ # Gets deck membership for given cards.
22
+ #
23
+ # @param card_ids [Array<Integer>] Array of card IDs
24
+ # @return [Hash] Deck names mapped to arrays of card IDs
25
+ def get_decks_for_cards(card_ids)
26
+ request(:getDecks, cards: card_ids)
27
+ end
28
+
29
+ # Creates a new empty deck.
30
+ #
31
+ # @param name [String] Deck name (use :: for hierarchy)
32
+ # @return [Integer] Deck ID
33
+ def create_deck(name)
34
+ request(:createDeck, deck: name)
35
+ end
36
+
37
+ # Moves cards to a different deck.
38
+ #
39
+ # @param card_ids [Array<Integer>] Array of card IDs
40
+ # @param to [String] Target deck name
41
+ # @return [nil]
42
+ def move_cards(card_ids, to:)
43
+ request(:changeDeck, cards: card_ids, deck: to)
44
+ end
45
+
46
+ # Deletes decks by name.
47
+ #
48
+ # @param names [Array<String>] Array of deck names
49
+ # @param cards_too [Boolean] Must be true to confirm deletion
50
+ # @return [nil]
51
+ def delete_decks(names, cards_too: true)
52
+ request(:deleteDecks, decks: names, cardsToo: cards_too)
53
+ end
54
+
55
+ # Gets configuration for a deck.
56
+ #
57
+ # @param name [String] Deck name
58
+ # @return [Hash] Configuration object
59
+ def get_deck_config(name)
60
+ request(:getDeckConfig, deck: name)
61
+ end
62
+
63
+ # Saves a deck configuration.
64
+ #
65
+ # @param config [Hash] Complete configuration object
66
+ # @return [Boolean] true on success
67
+ def save_deck_config(config)
68
+ request(:saveDeckConfig, config: config)
69
+ end
70
+
71
+ # Sets configuration for decks.
72
+ #
73
+ # @param names [Array<String>] Array of deck names
74
+ # @param config_id [Integer] Configuration group ID
75
+ # @return [Boolean] true on success
76
+ def set_deck_config(names, config_id)
77
+ request(:setDeckConfigId, decks: names, configId: config_id)
78
+ end
79
+
80
+ # Clones a deck configuration.
81
+ #
82
+ # @param name [String] Name for new config group
83
+ # @param clone_from [Integer, nil] Config ID to clone from
84
+ # @return [Integer, Boolean] New config ID, or false if source doesn't exist
85
+ def clone_deck_config(name, clone_from: nil)
86
+ params = { name: name }
87
+ params[:cloneFrom] = clone_from if clone_from
88
+ request(:cloneDeckConfigId, **params)
89
+ end
90
+
91
+ # Removes a deck configuration.
92
+ #
93
+ # @param config_id [Integer] Configuration group ID
94
+ # @return [Boolean] true on success
95
+ def remove_deck_config(config_id)
96
+ request(:removeDeckConfigId, configId: config_id)
97
+ end
98
+
99
+ # Gets statistics for decks.
100
+ #
101
+ # @param names [Array<String>] Array of deck names
102
+ # @return [Hash] Deck IDs mapped to stats objects
103
+ def get_deck_stats(names)
104
+ request(:getDeckStats, decks: names)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiConnect
4
+ class Client
5
+ # Methods to interact with Anki's GUI windows and dialogs
6
+ # (card browser, review screen, editing interfaces).
7
+ module Graphical
8
+ # Opens Card Browser dialog and searches for query.
9
+ #
10
+ # @param query [String] Search query string
11
+ # @param reorder_cards [Hash, nil] (optional) Object with order (ascending/descending) and columnId
12
+ # @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
16
+ request(:guiBrowse, **params)
17
+ end
18
+
19
+ # Selects a card in the open Card Browser.
20
+ #
21
+ # @param card_id [Integer] Card ID
22
+ # @return [Boolean] true if browser is open, false otherwise
23
+ def gui_select_card(card_id)
24
+ request(:guiSelectCard, card: card_id)
25
+ end
26
+
27
+ # Gets selected notes from open Card Browser.
28
+ #
29
+ # @return [Array<Integer>] Array of note IDs (empty if browser not open)
30
+ def gui_selected_notes
31
+ request(:guiSelectedNotes)
32
+ end
33
+
34
+ # Opens Add Cards dialog with preset values.
35
+ # Multiple invocations close old window and reopen with new values.
36
+ #
37
+ # @param note [Hash] Note object with deckName, modelName, fields, tags, and optional audio/video/picture
38
+ # @return [Integer] Note ID that would be created if user confirms
39
+ def gui_add_cards(note)
40
+ request(:guiAddCards, note: note)
41
+ end
42
+
43
+ # Opens Edit dialog for a note.
44
+ # Opens edit dialog with Preview, Browse, and navigation buttons.
45
+ #
46
+ # @param note_id [Integer] Note ID
47
+ # @return [nil]
48
+ def gui_edit_note(note_id)
49
+ request(:guiEditNote, note: note_id)
50
+ end
51
+
52
+ # Sets fields/tags/deck/model in open Add Note dialog.
53
+ # Returns error if Add Note dialog not open. Deck/model always replace; fields/tags respect append flag.
54
+ #
55
+ # @param note [Hash] Note object with optional deckName, modelName, fields, tags
56
+ # @param append [Boolean] If true, appends to fields/tags; otherwise replaces (default: false)
57
+ # @return [Boolean] true on success
58
+ def gui_add_note_set_data(note, append: false)
59
+ request(:guiAddNoteSetData, note: note, append: append)
60
+ end
61
+
62
+ # Gets information about current card in review.
63
+ #
64
+ # @return [Hash, nil] Object with card info, or nil if not in review mode
65
+ def gui_current_card
66
+ request(:guiCurrentCard)
67
+ end
68
+
69
+ # Starts/resets timer for current card.
70
+ # Useful for accurate time tracking when displaying cards via API.
71
+ #
72
+ # @return [Boolean] true
73
+ def gui_start_card_timer
74
+ request(:guiStartCardTimer)
75
+ end
76
+
77
+ # Shows question side of current card.
78
+ #
79
+ # @return [Boolean] true if in review mode, false otherwise
80
+ def gui_show_question
81
+ request(:guiShowQuestion)
82
+ end
83
+
84
+ # Shows answer side of current card.
85
+ #
86
+ # @return [Boolean] true if in review mode, false otherwise
87
+ def gui_show_answer
88
+ request(:guiShowAnswer)
89
+ end
90
+
91
+ # Answers the current card.
92
+ # Answer must be displayed before answering.
93
+ #
94
+ # @param ease [Integer] Answer button (1-4)
95
+ # @return [Boolean] true on success, false otherwise
96
+ def gui_answer_card(ease)
97
+ request(:guiAnswerCard, ease: ease)
98
+ end
99
+
100
+ # Undoes last action/card.
101
+ #
102
+ # @return [Boolean] true on success, false otherwise
103
+ def gui_undo
104
+ request(:guiUndo)
105
+ end
106
+
107
+ # Opens Deck Overview dialog for a deck.
108
+ #
109
+ # @param name [String] Deck name
110
+ # @return [Boolean] true on success, false otherwise
111
+ def gui_deck_overview(name)
112
+ request(:guiDeckOverview, name: name)
113
+ end
114
+
115
+ # Opens Deck Browser dialog.
116
+ #
117
+ # @return [nil]
118
+ def gui_deck_browser
119
+ request(:guiDeckBrowser)
120
+ end
121
+
122
+ # Starts review for a deck.
123
+ #
124
+ # @param name [String] Deck name
125
+ # @return [Boolean] true on success, false otherwise
126
+ def gui_deck_review(name)
127
+ request(:guiDeckReview, name: name)
128
+ end
129
+
130
+ # Opens Import dialog with optional file path.
131
+ # Opens file dialog if no path provided. Forward slashes required on Windows. Anki 2.1.52+ only.
132
+ #
133
+ # @param path [String, nil] File path to import (optional)
134
+ # @return [nil]
135
+ def gui_import_file(path: nil)
136
+ params = {}
137
+ params[:path] = path if path
138
+ request(:guiImportFile, **params)
139
+ end
140
+
141
+ # Schedules graceful Anki shutdown.
142
+ # Asynchronous - returns immediately without waiting for termination.
143
+ #
144
+ # @return [nil]
145
+ def gui_exit_anki
146
+ request(:guiExitAnki)
147
+ end
148
+
149
+ # Requests database check.
150
+ # Returns immediately without waiting for check to complete.
151
+ #
152
+ # @return [Boolean] true (always)
153
+ def gui_check_database
154
+ request(:guiCheckDatabase)
155
+ end
156
+
157
+ # Plays audio for current card side.
158
+ #
159
+ # @return [Boolean] true on success, false otherwise
160
+ def gui_play_audio
161
+ request(:guiPlayAudio)
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiConnect
4
+ class Client
5
+ # Methods to store, retrieve, and manage media files.
6
+ module Media
7
+ # Stores a file in the media folder.
8
+ #
9
+ # @param filename [String] File name (prefix with _ to prevent auto-deletion)
10
+ # @param data [String, nil] Base64-encoded contents
11
+ # @param path [String, nil] Absolute file path
12
+ # @param url [String, nil] URL to download from
13
+ # @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)
16
+ params = { filename: filename, deleteExisting: overwrite }
17
+ params[:data] = data if data
18
+ params[:path] = path if path
19
+ params[:url] = url if url
20
+ request(:storeMediaFile, **params)
21
+ end
22
+
23
+ # Retrieves a media file's contents.
24
+ #
25
+ # @param filename [String] File name
26
+ # @return [String, Boolean] Base64-encoded contents, or false if not found
27
+ def retrieve_media(filename)
28
+ request(:retrieveMediaFile, filename: filename)
29
+ end
30
+
31
+ # Lists media files matching a pattern.
32
+ #
33
+ # @param pattern [String] Glob pattern
34
+ # @return [Array<String>] Array of filenames
35
+ def list_media(pattern: '*')
36
+ request(:getMediaFilesNames, pattern: pattern)
37
+ end
38
+
39
+ # Gets the media folder path.
40
+ #
41
+ # @return [String] Absolute path
42
+ def media_dir_path
43
+ request(:getMediaDirPath)
44
+ end
45
+
46
+ # Deletes a media file.
47
+ #
48
+ # @param filename [String] File name
49
+ # @return [nil]
50
+ def delete_media(filename)
51
+ request(:deleteMediaFile, filename: filename)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiConnect
4
+ class Client
5
+ # Methods for API permissions, version checking, profile management,
6
+ # synchronization, and import/export.
7
+ module Miscellaneous
8
+ # Requests API permission (first call to establish trust).
9
+ # Only method accepting requests from any origin. Shows popup for untrusted origins.
10
+ #
11
+ # @return [Hash] Object with permission (granted/denied), and optionally requireApiKey and version
12
+ def request_permission
13
+ request(:requestPermission)
14
+ end
15
+
16
+ # Gets AnkiConnect API version.
17
+ #
18
+ # @return [Integer] Version number (currently 6)
19
+ def version
20
+ request(:version)
21
+ end
22
+
23
+ # Gets information about available APIs.
24
+ #
25
+ # @param scopes [Array<String>] Array of scope names (currently only "actions" supported)
26
+ # @param actions [Array<String>, nil] null for all actions, or array of action names to check (optional)
27
+ # @return [Hash] Object with scopes used and available actions
28
+ def api_reflect(scopes, actions: nil)
29
+ params = { scopes: scopes }
30
+ params[:actions] = actions if actions
31
+ request(:apiReflect, **params)
32
+ end
33
+
34
+ # Synchronizes local collection with AnkiWeb.
35
+ #
36
+ # @return [nil]
37
+ def sync
38
+ request(:sync)
39
+ end
40
+
41
+ # Retrieves list of profiles.
42
+ #
43
+ # @return [Array<String>] Array of profile names
44
+ def profiles
45
+ request(:getProfiles)
46
+ end
47
+
48
+ # Gets the active profile.
49
+ #
50
+ # @return [String] Profile name string
51
+ def active_profile
52
+ request(:getActiveProfile)
53
+ end
54
+
55
+ # Switches to specified profile.
56
+ #
57
+ # @param name [String] Profile name
58
+ # @return [Boolean] true on success
59
+ def load_profile(name)
60
+ request(:loadProfile, name: name)
61
+ end
62
+
63
+ # Performs multiple actions in one request.
64
+ #
65
+ # @param actions [Array<Hash>] Array of action objects (each with action, version, params)
66
+ # @return [Array] Array of responses in same order
67
+ def multi(actions)
68
+ request(:multi, actions: actions)
69
+ end
70
+
71
+ # Exports deck to .apkg format.
72
+ #
73
+ # @param deck_name [String] Deck name
74
+ # @param path [String] Output file path
75
+ # @param include_scheduling [Boolean] Include scheduling data (default: false)
76
+ # @return [Boolean] true on success, false otherwise
77
+ def export_deck(deck_name, path, include_scheduling: false)
78
+ request(:exportPackage, deck: deck_name, path: path, includeSched: include_scheduling)
79
+ end
80
+
81
+ # Imports .apkg file into collection.
82
+ #
83
+ # @param path [String] File path (relative to collection.media folder)
84
+ # @return [Boolean] true on success, false otherwise
85
+ def import_deck(path)
86
+ request(:importPackage, path: path)
87
+ end
88
+
89
+ # Reloads all data from database.
90
+ #
91
+ # @return [nil]
92
+ def reload_collection
93
+ request(:reloadCollection)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiConnect
4
+ class Client
5
+ # Methods to create and modify note types (models).
6
+ module Models
7
+ # Gets complete list of model names.
8
+ #
9
+ # @return [Array<String>] Array of model name strings
10
+ def model_names
11
+ request(:modelNames)
12
+ end
13
+
14
+ # Gets model names with their IDs.
15
+ #
16
+ # @return [Hash] Model names mapped to IDs
17
+ def model_names_and_ids
18
+ request(:modelNamesAndIds)
19
+ end
20
+
21
+ # Gets models by ID.
22
+ #
23
+ # @param ids [Array<Integer>] Array of model IDs
24
+ # @return [Array<Hash>] Array of model objects
25
+ def get_models_by_id(ids)
26
+ request(:findModelsById, modelIds: ids)
27
+ end
28
+
29
+ # Gets models by name.
30
+ #
31
+ # @param names [Array<String>] Array of model names
32
+ # @return [Array<Hash>] Array of model objects
33
+ def get_models_by_name(names)
34
+ request(:findModelsByName, modelNames: names)
35
+ end
36
+
37
+ # Gets field names for a model.
38
+ #
39
+ # @param model_name [String] Model name
40
+ # @return [Array<String>] Array of field names in order
41
+ def get_field_names(model_name)
42
+ request(:modelFieldNames, modelName: model_name)
43
+ end
44
+
45
+ # Gets field descriptions for a model.
46
+ #
47
+ # @param model_name [String] Model name
48
+ # @return [Array<String>] Array of description strings
49
+ def get_field_descriptions(model_name)
50
+ request(:modelFieldDescriptions, modelName: model_name)
51
+ end
52
+
53
+ # Gets field fonts and sizes for a model.
54
+ #
55
+ # @param model_name [String] Model name
56
+ # @return [Hash] Field names mapped to { font:, size: }
57
+ def get_field_fonts(model_name)
58
+ request(:modelFieldFonts, modelName: model_name)
59
+ end
60
+
61
+ # Gets fields used on templates.
62
+ #
63
+ # @param model_name [String] Model name
64
+ # @return [Hash] Template names mapped to [questionFields, answerFields]
65
+ def get_fields_on_templates(model_name)
66
+ request(:modelFieldsOnTemplates, modelName: model_name)
67
+ end
68
+
69
+ # Creates a new model.
70
+ #
71
+ # @param name [String] Model name
72
+ # @param fields [Array<String>] Field names in order
73
+ # @param templates [Array<Hash>] Template objects with Name, Front, Back
74
+ # @param css [String, nil] CSS styling
75
+ # @param is_cloze [Boolean] true for cloze type
76
+ # @return [Hash] Complete model object
77
+ def create_model(name:, fields:, templates:, css: nil, is_cloze: false)
78
+ params = { modelName: name, inOrderFields: fields, cardTemplates: templates, isCloze: is_cloze }
79
+ params[:css] = css if css
80
+ request(:createModel, **params)
81
+ end
82
+
83
+ # Gets templates for a model.
84
+ #
85
+ # @param model_name [String] Model name
86
+ # @return [Hash] Template names mapped to { Front:, Back: }
87
+ def get_templates(model_name)
88
+ request(:modelTemplates, modelName: model_name)
89
+ end
90
+
91
+ # Gets CSS styling for a model.
92
+ #
93
+ # @param model_name [String] Model name
94
+ # @return [Hash] Object with css property
95
+ def get_styling(model_name)
96
+ request(:modelStyling, modelName: model_name)
97
+ end
98
+
99
+ # Updates a model's templates and/or CSS.
100
+ #
101
+ # @param name [String] Model name
102
+ # @param templates [Hash, nil] Template names mapped to Front/Back
103
+ # @param css [String, nil] CSS styling
104
+ # @return [nil]
105
+ def update_model(name, templates: nil, css: nil)
106
+ request(:updateModelTemplates, model: { name: name, templates: templates }) if templates
107
+ return unless css
108
+
109
+ request(:updateModelStyling, model: { name: name, css: css })
110
+ end
111
+
112
+ # Find and replace in model templates/CSS.
113
+ #
114
+ # @param model_name [String] Model name
115
+ # @param find [String] Text to find
116
+ # @param replace [String] Replacement text
117
+ # @param front [Boolean] Search front templates
118
+ # @param back [Boolean] Search back templates
119
+ # @param css [Boolean] Search CSS
120
+ # @return [Integer] Number of replacements made
121
+ def find_and_replace_in_model(model_name:, find:, replace:, front: true, back: true, css: true)
122
+ request(:findAndReplaceInModels, model: {
123
+ modelName: model_name, findText: find, replaceText: replace,
124
+ front: front, back: back, css: css
125
+ })
126
+ end
127
+
128
+ # Renames a template.
129
+ #
130
+ # @param model_name [String] Model name
131
+ # @param from [String] Current template name
132
+ # @param to [String] New template name
133
+ # @return [nil]
134
+ def rename_template(model_name, from:, to:)
135
+ request(:modelTemplateRename, modelName: model_name, oldTemplateName: from, newTemplateName: to)
136
+ end
137
+
138
+ # Moves a template to a new position.
139
+ #
140
+ # @param model_name [String] Model name
141
+ # @param template_name [String] Template name
142
+ # @param index [Integer] New position (0-based)
143
+ # @return [nil]
144
+ def reposition_template(model_name, template_name, index)
145
+ request(:modelTemplateReposition, modelName: model_name, templateName: template_name, index: index)
146
+ end
147
+
148
+ # Adds a template to a model.
149
+ #
150
+ # @param model_name [String] Model name
151
+ # @param template [Hash] Template with Name, Front, Back
152
+ # @return [nil]
153
+ def add_template(model_name, template)
154
+ request(:modelTemplateAdd, modelName: model_name, template: template)
155
+ end
156
+
157
+ # Removes a template from a model.
158
+ #
159
+ # @param model_name [String] Model name
160
+ # @param template_name [String] Template name
161
+ # @return [nil]
162
+ def remove_template(model_name, template_name)
163
+ request(:modelTemplateRemove, modelName: model_name, templateName: template_name)
164
+ end
165
+
166
+ # Renames a field.
167
+ #
168
+ # @param model_name [String] Model name
169
+ # @param from [String] Current field name
170
+ # @param to [String] New field name
171
+ # @return [nil]
172
+ def rename_field(model_name, from:, to:)
173
+ request(:modelFieldRename, modelName: model_name, oldFieldName: from, newFieldName: to)
174
+ end
175
+
176
+ # Moves a field to a new position.
177
+ #
178
+ # @param model_name [String] Model name
179
+ # @param field_name [String] Field name
180
+ # @param index [Integer] New position (0-based)
181
+ # @return [nil]
182
+ def reposition_field(model_name, field_name, index)
183
+ request(:modelFieldReposition, modelName: model_name, fieldName: field_name, index: index)
184
+ end
185
+
186
+ # Adds a field to a model.
187
+ #
188
+ # @param model_name [String] Model name
189
+ # @param field_name [String] Field name
190
+ # @param index [Integer, nil] Position (defaults to end)
191
+ # @return [nil]
192
+ def add_field(model_name, field_name, index: nil)
193
+ params = { modelName: model_name, fieldName: field_name }
194
+ params[:index] = index if index
195
+ request(:modelFieldAdd, **params)
196
+ end
197
+
198
+ # Removes a field from a model.
199
+ #
200
+ # @param model_name [String] Model name
201
+ # @param field_name [String] Field name
202
+ # @return [nil]
203
+ def remove_field(model_name, field_name)
204
+ request(:modelFieldRemove, modelName: model_name, fieldName: field_name)
205
+ end
206
+
207
+ # Sets font for a field.
208
+ #
209
+ # @param model_name [String] Model name
210
+ # @param field_name [String] Field name
211
+ # @param font [String] Font name
212
+ # @return [nil]
213
+ def set_field_font(model_name, field_name, font)
214
+ request(:modelFieldSetFont, modelName: model_name, fieldName: field_name, font: font)
215
+ end
216
+
217
+ # Sets font size for a field.
218
+ #
219
+ # @param model_name [String] Model name
220
+ # @param field_name [String] Field name
221
+ # @param size [Integer] Font size
222
+ # @return [nil]
223
+ def set_field_font_size(model_name, field_name, size)
224
+ request(:modelFieldSetFontSize, modelName: model_name, fieldName: field_name, fontSize: size)
225
+ end
226
+
227
+ # Sets description for a field.
228
+ #
229
+ # @param model_name [String] Model name
230
+ # @param field_name [String] Field name
231
+ # @param description [String] Description text
232
+ # @return [Boolean] true on success
233
+ def set_field_description(model_name, field_name, description)
234
+ request(:modelFieldSetDescription, modelName: model_name, fieldName: field_name, description: description)
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiConnect
4
+ class Client
5
+ # Methods to create, update, query, and manage notes (which generate cards).
6
+ module Notes
7
+ # Creates a new note.
8
+ #
9
+ # @param deck_name [String] Target deck
10
+ # @param model_name [String] Note type
11
+ # @param fields [Hash] Field names to values
12
+ # @param tags [Array<String>] Tags (optional)
13
+ # @param media [Hash, nil] Media to add (audio:, video:, picture: arrays)
14
+ # @param options [Hash, nil] Options (allowDuplicate, duplicateScope, etc.)
15
+ # @return [Integer, nil] Note ID on success, nil on failure
16
+ def add_note(deck_name:, model_name:, fields:, tags: [], media: nil, options: nil)
17
+ note = { deckName: deck_name, modelName: model_name, fields: fields, tags: tags }
18
+ note.merge!(media) if media
19
+ note[:options] = options if options
20
+ request(:addNote, note: note)
21
+ end
22
+
23
+ # Creates multiple notes.
24
+ #
25
+ # @param notes [Array<Hash>] Array of note hashes (same keys as add_note)
26
+ # @return [Array<Integer, nil>] Array of note IDs (nil for failed notes)
27
+ def add_notes(notes)
28
+ request(:addNotes, notes: notes)
29
+ end
30
+
31
+ # Checks if notes can be added.
32
+ #
33
+ # @param notes [Array<Hash>] Array of candidate note objects
34
+ # @param details [Boolean] If true, returns error details (default: false)
35
+ # @return [Array<Boolean>, Array<Hash>] Array of booleans, or hashes with canAdd and error if details=true
36
+ def can_add_notes(notes, details: false)
37
+ if details
38
+ request(:canAddNotesWithErrorDetail, notes: notes)
39
+ else
40
+ request(:canAddNotes, notes: notes)
41
+ end
42
+ end
43
+
44
+ # Updates a note's fields, tags, or media.
45
+ #
46
+ # @param id [Integer] Note ID
47
+ # @param fields [Hash, nil] Field names to new values
48
+ # @param tags [Array<String>, nil] New tags (replaces existing)
49
+ # @param media [Hash, nil] Media to add (audio:, video:, picture: arrays)
50
+ # @return [nil]
51
+ def update_note(id, fields: nil, tags: nil, media: nil)
52
+ note = { id: id }
53
+ note[:fields] = fields if fields
54
+ note[:tags] = tags if tags
55
+ note.merge!(media) if media
56
+ request(:updateNote, note: note)
57
+ end
58
+
59
+ # Changes a note's model type.
60
+ #
61
+ # @param id [Integer] Note ID
62
+ # @param model_name [String] New model name
63
+ # @param fields [Hash] New field values
64
+ # @param tags [Array<String>] New tags
65
+ # @return [nil]
66
+ def change_note_model(id, model_name:, fields:, tags:)
67
+ request(:updateNoteModel, note: { id: id, modelName: model_name, fields: fields, tags: tags })
68
+ end
69
+
70
+ # Gets tags for a note.
71
+ #
72
+ # @param note_id [Integer] Note ID
73
+ # @return [Array<String>] Array of tag strings
74
+ def get_note_tags(note_id)
75
+ request(:getNoteTags, note: note_id)
76
+ end
77
+
78
+ # Adds tags to notes.
79
+ #
80
+ # @param note_ids [Array<Integer>] Array of note IDs
81
+ # @param tags [String, Array<String>] Tag(s) to add
82
+ # @return [nil]
83
+ def add_tags(note_ids, tags)
84
+ request(:addTags, notes: note_ids, tags: tags)
85
+ end
86
+
87
+ # Removes tags from notes.
88
+ #
89
+ # @param note_ids [Array<Integer>] Array of note IDs
90
+ # @param tags [String, Array<String>] Tag(s) to remove
91
+ # @return [nil]
92
+ def remove_tags(note_ids, tags)
93
+ request(:removeTags, notes: note_ids, tags: tags)
94
+ end
95
+
96
+ # Gets all tags in collection.
97
+ #
98
+ # @return [Array<String>] Array of all tag strings
99
+ def all_tags
100
+ request(:getTags)
101
+ end
102
+
103
+ # Removes unused tags from collection.
104
+ #
105
+ # @return [nil]
106
+ def clear_unused_tags
107
+ request(:clearUnusedTags)
108
+ end
109
+
110
+ # Replaces a tag with another.
111
+ #
112
+ # @param from [String] Old tag
113
+ # @param to [String] New tag
114
+ # @param note_ids [Array<Integer>, nil] Specific notes, or nil for all notes
115
+ # @return [nil]
116
+ def replace_tag(from:, to:, note_ids: nil)
117
+ if note_ids
118
+ request(:replaceTags, notes: note_ids, tag_to_replace: from, replace_with_tag: to)
119
+ else
120
+ request(:replaceTagsInAllNotes, tag_to_replace: from, replace_with_tag: to)
121
+ end
122
+ end
123
+
124
+ # Searches for notes matching a query.
125
+ #
126
+ # @param query [String] Search query string
127
+ # @return [Array<Integer>] Array of note IDs
128
+ def search_notes(query)
129
+ request(:findNotes, query: query)
130
+ end
131
+
132
+ # Gets detailed information about notes.
133
+ #
134
+ # @param note_ids [Array<Integer>, nil] Array of note IDs
135
+ # @param query [String, nil] Search query string
136
+ # @return [Array<Hash>] Array of note objects
137
+ def get_notes(note_ids: nil, query: nil)
138
+ params = {}
139
+ params[:notes] = note_ids if note_ids
140
+ params[:query] = query if query
141
+ request(:notesInfo, **params)
142
+ end
143
+
144
+ # Gets modification times for notes.
145
+ #
146
+ # @param note_ids [Array<Integer>] Array of note IDs
147
+ # @return [Array<Hash>] Array of objects with noteId and mod
148
+ def get_notes_mod_time(note_ids)
149
+ request(:notesModTime, notes: note_ids)
150
+ end
151
+
152
+ # Deletes notes and all associated cards.
153
+ #
154
+ # @param note_ids [Array<Integer>] Array of note IDs
155
+ # @return [nil]
156
+ def delete_notes(note_ids)
157
+ request(:deleteNotes, notes: note_ids)
158
+ end
159
+
160
+ # Removes all empty notes.
161
+ #
162
+ # @return [nil]
163
+ def remove_empty_notes
164
+ request(:removeEmptyNotes)
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiConnect
4
+ class Client
5
+ # Methods to query review counts, retrieve review history,
6
+ # and access collection statistics.
7
+ module Statistics
8
+ # Gets count of cards reviewed today.
9
+ # "Today" uses day start time as configured in Anki.
10
+ #
11
+ # @return [Integer] Count of cards reviewed today
12
+ def cards_reviewed_today
13
+ request(:getNumCardsReviewedToday)
14
+ end
15
+
16
+ # Gets review counts by day.
17
+ #
18
+ # @return [Array<Array>] Array of [dateString, count] pairs
19
+ def cards_reviewed_by_day
20
+ request(:getNumCardsReviewedByDay)
21
+ end
22
+
23
+ # Gets collection statistics report as HTML.
24
+ #
25
+ # @param whole_collection [Boolean] Whether to get stats for whole collection (default: true)
26
+ # @return [String] HTML string
27
+ def collection_stats_html(whole_collection: true)
28
+ request(:getCollectionStatsHTML, wholeCollection: whole_collection)
29
+ end
30
+
31
+ # Gets all card reviews for a deck after a certain time.
32
+ #
33
+ # @param deck_name [String] Deck name
34
+ # @param after [Integer] Unix timestamp (reviews after this time, exclusive)
35
+ # @return [Array<Array>] Array of 9-tuples: (reviewTime, cardID, usn, buttonPressed, newInterval, previousInterval, newFactor, reviewDuration, reviewType)
36
+ def get_reviews(deck_name, after:)
37
+ request(:cardReviews, deck: deck_name, startID: after)
38
+ end
39
+
40
+ # Gets all reviews for specific cards.
41
+ #
42
+ # @param card_ids [Array<Integer>] Array of card IDs
43
+ # @return [Hash] Dictionary mapping card IDs to arrays of review objects with id, usn, ease, ivl, lastIvl, factor, time, type
44
+ def get_reviews_for_cards(card_ids)
45
+ request(:getReviewsOfCards, cards: card_ids)
46
+ end
47
+
48
+ # Gets unix time of latest review for a deck.
49
+ #
50
+ # @param deck_name [String] Deck name
51
+ # @return [Integer] Unix timestamp, or 0 if no reviews
52
+ def latest_review_time(deck_name)
53
+ request(:getLatestReviewID, deck: deck_name)
54
+ end
55
+
56
+ # Inserts review records into database.
57
+ #
58
+ # @param reviews [Array<Array>] Array of 9-tuples (same format as get_reviews output)
59
+ # @return [nil]
60
+ def insert_reviews(reviews)
61
+ request(:insertReviews, reviews: reviews)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiConnect
4
+ # The version of the anki_connect gem
5
+ VERSION = '0.1.1'
6
+
7
+ # The AnkiConnect API version this gem is compatible with
8
+ API_VERSION = 6
9
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'anki_connect/version'
4
+ require_relative 'anki_connect/cards'
5
+ require_relative 'anki_connect/decks'
6
+ require_relative 'anki_connect/models'
7
+ require_relative 'anki_connect/notes'
8
+ require_relative 'anki_connect/media'
9
+ require_relative 'anki_connect/graphical'
10
+ require_relative 'anki_connect/statistics'
11
+ require_relative 'anki_connect/miscellaneous'
12
+ require_relative 'anki_connect/client'
13
+
14
+ # Ruby client for AnkiConnect, enabling external applications to interact
15
+ # with Anki through HTTP.
16
+ #
17
+ # @example Basic usage
18
+ # client = AnkiConnect::Client.new
19
+ # decks = client.deck_names
20
+ # cards = client.search_cards("deck:Default")
21
+ #
22
+ # @see https://foosoft.net/projects/anki-connect/ AnkiConnect Documentation
23
+ module AnkiConnect
24
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: anki_connect
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - vadik49b
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: AnkiConnect provides a simple HTTP API to communicate with Anki. This
13
+ Ruby gem is a wrapper around that API.
14
+ email:
15
+ - vadim@boltach.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - lib/anki_connect.rb
23
+ - lib/anki_connect/cards.rb
24
+ - lib/anki_connect/client.rb
25
+ - lib/anki_connect/decks.rb
26
+ - lib/anki_connect/graphical.rb
27
+ - lib/anki_connect/media.rb
28
+ - lib/anki_connect/miscellaneous.rb
29
+ - lib/anki_connect/models.rb
30
+ - lib/anki_connect/notes.rb
31
+ - lib/anki_connect/statistics.rb
32
+ - lib/anki_connect/version.rb
33
+ homepage: https://github.com/vadik49b/anki-connect.rb
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ homepage_uri: https://github.com/vadik49b/anki-connect.rb
38
+ source_code_uri: https://github.com/vadik49b/anki-connect.rb
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.4.0
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.6.9
54
+ specification_version: 4
55
+ summary: Ruby wrapper for the Anki-Connect HTTP API
56
+ test_files: []