anki_record 0.4 → 0.4.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 +4 -4
- data/CHANGELOG.md +6 -0
- data/Gemfile.lock +1 -1
- data/README.md +29 -19
- data/lib/anki_record/anki21_database/anki21_database.rb +41 -7
- data/lib/anki_record/anki_package/anki_package.rb +7 -0
- data/lib/anki_record/note/note.rb +10 -7
- data/lib/anki_record/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1a86e3c0c6ba10ebfe3867e0bdc62d5a3e8e1218fc4783c1ade86c34d790318
|
4
|
+
data.tar.gz: 662ec96ab3e29c26b0de4405b1c699ba99a6b180af5dbb2a24eb6ad4e7919cef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d6de87dfadcc19fe0777202f67a97d632ddaec65426d171eda9ac74d87e7d99acc2afeb964e02529a695c1409a6c0fe26a9ee026d7b6fdbfb8c2d3b284230de
|
7
|
+
data.tar.gz: c35379873392e86fe7d6a402212d84c7c8d557543b84909d24c07e1ac34a927819628f13eff335f1b2a2e0d8bc7eda6218b86dca742660838d6b2148c16e4eee
|
data/CHANGELOG.md
CHANGED
@@ -54,3 +54,9 @@
|
|
54
54
|
- Responsibilites of `Collection` have been reorganized to `Anki21Database`.
|
55
55
|
- The `guid` attribute of notes ic computed in a different way that allows a larger number of possible values.
|
56
56
|
- `globally_unique_id` is now a module method rather than an included instance method.
|
57
|
+
|
58
|
+
## [0.4.1] - 08-21-2023
|
59
|
+
- `Anki21Database#find_note_by` now can take the sort field value of a note as argument.
|
60
|
+
- `Anki21Database#find_notes_by_exact_text_match` is a new method that returns an array of notes that have in any field text matching the argument.
|
61
|
+
- Trying to create an Anki package file with a path/name that already exists raises a more helpful error message.
|
62
|
+
- Saving a note to the database saves the `mod` column (last modified time) to make it easier to import an updated package into Anki.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -14,9 +14,9 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
14
14
|
|
15
15
|
## Usage
|
16
16
|
|
17
|
-
This example shows how to create a new Anki package and
|
17
|
+
This example shows how to create a new Anki package and most of the API:
|
18
18
|
|
19
|
-
```
|
19
|
+
```ruby
|
20
20
|
require "anki_record"
|
21
21
|
|
22
22
|
AnkiRecord::AnkiPackage.create(name: "example") do |anki21_database|
|
@@ -25,13 +25,18 @@ AnkiRecord::AnkiPackage.create(name: "example") do |anki21_database|
|
|
25
25
|
custom_deck.save
|
26
26
|
|
27
27
|
# Creating a new note type
|
28
|
-
custom_note_type = AnkiRecord::NoteType.new(anki21_database:,
|
29
|
-
|
30
|
-
AnkiRecord::NoteField.new(note_type: custom_note_type,
|
31
|
-
|
28
|
+
custom_note_type = AnkiRecord::NoteType.new(anki21_database:,
|
29
|
+
name: "New custom note type")
|
30
|
+
AnkiRecord::NoteField.new(note_type: custom_note_type,
|
31
|
+
name: "custom front")
|
32
|
+
AnkiRecord::NoteField.new(note_type: custom_note_type,
|
33
|
+
name: "custom back")
|
34
|
+
custom_card_template = AnkiRecord::CardTemplate.new(note_type: custom_note_type,
|
35
|
+
name: "Custom template 1")
|
32
36
|
custom_card_template.question_format = "{{custom front}}"
|
33
37
|
custom_card_template.answer_format = "{{custom back}}"
|
34
|
-
second_custom_card_template = AnkiRecord::CardTemplate.new(note_type: custom_note_type,
|
38
|
+
second_custom_card_template = AnkiRecord::CardTemplate.new(note_type: custom_note_type,
|
39
|
+
name: "Custom template 2")
|
35
40
|
second_custom_card_template.question_format = "{{custom back}}"
|
36
41
|
second_custom_card_template.answer_format = "{{custom front}}"
|
37
42
|
|
@@ -46,45 +51,49 @@ AnkiRecord::AnkiPackage.create(name: "example") do |anki21_database|
|
|
46
51
|
custom_note_type.css = css
|
47
52
|
custom_note_type.save
|
48
53
|
|
49
|
-
# Creating a note with the custom note type
|
54
|
+
# Creating a new note with the custom note type
|
50
55
|
note = AnkiRecord::Note.new(note_type: custom_note_type, deck: custom_deck)
|
51
56
|
note.custom_front = "Content of the 'custom front' field"
|
52
57
|
note.custom_back = "Content of the 'custom back' field"
|
53
58
|
note.save
|
54
59
|
|
55
|
-
#
|
60
|
+
# Finding the default deck
|
56
61
|
default_deck = anki21_database.find_deck_by(name: "Default")
|
57
62
|
|
58
|
-
#
|
63
|
+
# Finding all of the default Anki note types
|
59
64
|
basic_note_type = anki21_database.find_note_type_by(name: "Basic")
|
60
65
|
basic_and_reversed_card_note_type = anki21_database.find_note_type_by(name: "Basic (and reversed card)")
|
61
66
|
basic_and_optional_reversed_card_note_type = anki21_database.find_note_type_by(name: "Basic (optional reversed card)")
|
62
67
|
basic_type_in_the_answer_note_type = anki21_database.find_note_type_by(name: "Basic (type in the answer)")
|
63
68
|
cloze_note_type = anki21_database.find_note_type_by(name: "Cloze")
|
64
69
|
|
65
|
-
# Creating notes using the default note types
|
70
|
+
# Creating new notes using the default note types
|
66
71
|
|
67
72
|
basic_note = AnkiRecord::Note.new(note_type: basic_note_type, deck: default_deck)
|
68
73
|
basic_note.front = "What molecule is most relevant to the name aerobic exercise?"
|
69
74
|
basic_note.back = "Oxygen"
|
70
75
|
basic_note.save
|
71
76
|
|
72
|
-
# Creating a nested deck
|
73
|
-
amino_acids_deck = AnkiRecord::Deck.new(anki21_database:,
|
77
|
+
# Creating a new nested deck
|
78
|
+
amino_acids_deck = AnkiRecord::Deck.new(anki21_database:,
|
79
|
+
name: "Biochemistry::Amino acids")
|
74
80
|
amino_acids_deck.save
|
75
81
|
|
76
|
-
basic_and_reversed_note = AnkiRecord::Note.new(note_type: basic_and_reversed_card_note_type,
|
82
|
+
basic_and_reversed_note = AnkiRecord::Note.new(note_type: basic_and_reversed_card_note_type,
|
83
|
+
deck: amino_acids_deck)
|
77
84
|
basic_and_reversed_note.front = "Tyrosine"
|
78
85
|
basic_and_reversed_note.back = "Y"
|
79
86
|
basic_and_reversed_note.save
|
80
87
|
|
81
|
-
basic_and_optional_reversed_note = AnkiRecord::Note.new(note_type: basic_and_optional_reversed_card_note_type,
|
88
|
+
basic_and_optional_reversed_note = AnkiRecord::Note.new(note_type: basic_and_optional_reversed_card_note_type,
|
89
|
+
deck: default_deck)
|
82
90
|
basic_and_optional_reversed_note.front = "A technique where locations along a route are memorized and associated with ideas"
|
83
91
|
basic_and_optional_reversed_note.back = "The method of loci"
|
84
92
|
basic_and_optional_reversed_note.add_reverse = "Have a reverse card too"
|
85
93
|
basic_and_optional_reversed_note.save
|
86
94
|
|
87
|
-
basic_type_in_the_answer_note = AnkiRecord::Note.new(note_type: basic_type_in_the_answer_note_type,
|
95
|
+
basic_type_in_the_answer_note = AnkiRecord::Note.new(note_type: basic_type_in_the_answer_note_type,
|
96
|
+
deck: default_deck)
|
88
97
|
basic_type_in_the_answer_note.front = "What Git command commits staged changes by changing the previous commit without editing the commit message?"
|
89
98
|
basic_type_in_the_answer_note.back = "git commit --amend --no-edit"
|
90
99
|
basic_type_in_the_answer_note.save
|
@@ -94,7 +103,8 @@ AnkiRecord::AnkiPackage.create(name: "example") do |anki21_database|
|
|
94
103
|
cloze_note.back_extra = "This condition involves one cranial nerve but can have myriad neurological symptoms"
|
95
104
|
cloze_note.save
|
96
105
|
end
|
97
|
-
#
|
106
|
+
# An example.apkg file should be in the current
|
107
|
+
# working directory with 6 notes.
|
98
108
|
|
99
109
|
```
|
100
110
|
|
@@ -102,7 +112,7 @@ end
|
|
102
112
|
|
103
113
|
The gem can also be used to update an existing Anki package:
|
104
114
|
|
105
|
-
```
|
115
|
+
```ruby
|
106
116
|
require "anki_record"
|
107
117
|
|
108
118
|
AnkiRecord::AnkiPackage.update(path: "./example.apkg") do |anki21_database|
|
@@ -117,7 +127,7 @@ If an error is thrown in the block here, the original Anki package will not be c
|
|
117
127
|
|
118
128
|
## Documentation
|
119
129
|
|
120
|
-
The [API Documentation](https://kylerego.github.io/anki_record_docs)
|
130
|
+
The [API Documentation](https://kylerego.github.io/anki_record_docs) generated from source code comments might be useful but I think the examples above show everything you can do that you would want to do.
|
121
131
|
|
122
132
|
## Development
|
123
133
|
|
@@ -30,21 +30,49 @@ module AnkiRecord
|
|
30
30
|
database.prepare sql
|
31
31
|
end
|
32
32
|
|
33
|
+
# rubocop:disable Metrics/MethodLength
|
33
34
|
##
|
34
|
-
# Returns the note found by +id+, or nil if it is not found.
|
35
|
-
def find_note_by(id:)
|
36
|
-
|
35
|
+
# Returns the note found by either +sfld" or +id+, or nil if it is not found.
|
36
|
+
def find_note_by(sfld: nil, id: nil)
|
37
|
+
if (id && sfld) || (id.nil? && sfld.nil?)
|
38
|
+
raise ArgumentError,
|
39
|
+
"You must pass either an id or sfld keyword argument to find_note_by."
|
40
|
+
end
|
41
|
+
note_cards_data = if id
|
42
|
+
note_cards_data_for_note(id:)
|
43
|
+
else
|
44
|
+
note_cards_data_for_note(sfld:)
|
45
|
+
end
|
37
46
|
return nil unless note_cards_data
|
38
47
|
|
39
48
|
AnkiRecord::Note.new(anki21_database: self, data: note_cards_data)
|
40
49
|
end
|
50
|
+
# rubocop:enable Metrics/MethodLength
|
51
|
+
|
52
|
+
##
|
53
|
+
# Returns an array of notes that have any field value matching +text+
|
54
|
+
def find_notes_by_exact_text_match(text:)
|
55
|
+
return [] if text == ""
|
56
|
+
|
57
|
+
like_matcher = "%#{text}%"
|
58
|
+
note_datas = prepare("select * from notes where flds LIKE ?").execute([like_matcher])
|
59
|
+
|
60
|
+
note_datas.map do |note_data|
|
61
|
+
id = note_data["id"]
|
62
|
+
|
63
|
+
cards_data = prepare("select * from cards where nid = ?").execute([id]).to_a
|
64
|
+
note_cards_data = { note_data:, cards_data: }
|
65
|
+
|
66
|
+
AnkiRecord::Note.new(anki21_database: self, data: note_cards_data)
|
67
|
+
end
|
68
|
+
end
|
41
69
|
|
42
70
|
##
|
43
71
|
# Returns the note type found by either +name+ or +id+, or nil if it is not found.
|
44
72
|
def find_note_type_by(name: nil, id: nil)
|
45
73
|
if (id && name) || (id.nil? && name.nil?)
|
46
74
|
raise ArgumentError,
|
47
|
-
"You must pass either an id or name keyword argument."
|
75
|
+
"You must pass either an id or name keyword argument to find_note_type_by."
|
48
76
|
end
|
49
77
|
|
50
78
|
name ? find_note_type_by_name(name:) : find_note_type_by_id(id:)
|
@@ -55,7 +83,7 @@ module AnkiRecord
|
|
55
83
|
def find_deck_by(name: nil, id: nil)
|
56
84
|
if (id && name) || (id.nil? && name.nil?)
|
57
85
|
raise ArgumentError,
|
58
|
-
"You must pass either an id or name keyword argument."
|
86
|
+
"You must pass either an id or name keyword argument to find_deck_by."
|
59
87
|
end
|
60
88
|
|
61
89
|
name ? find_deck_by_name(name:) : find_deck_by_id(id:)
|
@@ -127,10 +155,16 @@ module AnkiRecord
|
|
127
155
|
decks.find { |deck| deck.id == id }
|
128
156
|
end
|
129
157
|
|
130
|
-
def
|
131
|
-
note_data =
|
158
|
+
def note_cards_data_for_note(id: nil, sfld: nil)
|
159
|
+
note_data = if id
|
160
|
+
prepare("select * from notes where id = ?").execute([id]).first
|
161
|
+
elsif sfld
|
162
|
+
prepare("select * from notes where sfld = ?").execute([sfld]).first
|
163
|
+
end
|
132
164
|
return nil unless note_data
|
133
165
|
|
166
|
+
id ||= note_data["id"]
|
167
|
+
|
134
168
|
cards_data = prepare("select * from cards where nid = ?").execute([id]).to_a
|
135
169
|
{ note_data:, cards_data: }
|
136
170
|
end
|
@@ -96,6 +96,7 @@ module AnkiRecord
|
|
96
96
|
def validate_arguments(name:, target_directory:)
|
97
97
|
check_name_argument_is_valid(name:)
|
98
98
|
check_target_directory_argument_is_valid(target_directory:)
|
99
|
+
check_anki_package_does_not_already_exist(name:, target_directory:)
|
99
100
|
end
|
100
101
|
|
101
102
|
def check_name_argument_is_valid(name:)
|
@@ -110,6 +111,12 @@ module AnkiRecord
|
|
110
111
|
raise ArgumentError, "No directory was found at the given path."
|
111
112
|
end
|
112
113
|
|
114
|
+
def check_anki_package_does_not_already_exist(name:, target_directory:)
|
115
|
+
return unless Pathname.new("#{target_directory}/#{new_apkg_name(name:)}.apkg").exist?
|
116
|
+
|
117
|
+
raise ArgumentError, "An Anki package with that name already exists."
|
118
|
+
end
|
119
|
+
|
113
120
|
def new_apkg_name(name:)
|
114
121
|
name.end_with?(".apkg") ? name[0, name.length - 5] : name
|
115
122
|
end
|
@@ -62,6 +62,13 @@ module AnkiRecord
|
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
+
##
|
66
|
+
# Returns the text value of the note's sfld (sort field) which is determined by
|
67
|
+
# the note's note type
|
68
|
+
def sort_field_value
|
69
|
+
@field_contents[note_type.snake_case_sort_field_name]
|
70
|
+
end
|
71
|
+
|
65
72
|
private
|
66
73
|
|
67
74
|
# rubocop:disable Metrics/AbcSize
|
@@ -78,7 +85,7 @@ module AnkiRecord
|
|
78
85
|
end
|
79
86
|
@id = milliseconds_since_epoch
|
80
87
|
@guid = Helpers::AnkiGuidHelper.globally_unique_id
|
81
|
-
@last_modified_timestamp =
|
88
|
+
@last_modified_timestamp = nil
|
82
89
|
@usn = NEW_OBJECT_USN
|
83
90
|
@tags = []
|
84
91
|
@flags = 0
|
@@ -123,7 +130,7 @@ module AnkiRecord
|
|
123
130
|
update notes set guid = ?, mid = ?, mod = ?, usn = ?, tags = ?,
|
124
131
|
flds = ?, sfld = ?, csum = ?, flags = ?, data = ? where id = ?
|
125
132
|
SQL
|
126
|
-
statement.execute([@guid, note_type.id,
|
133
|
+
statement.execute([@guid, note_type.id, seconds_since_epoch,
|
127
134
|
@usn, @tags.join(" "), field_values_separated_by_us, sort_field_value,
|
128
135
|
checksum(sort_field_value), @flags, @data, @id])
|
129
136
|
cards.each { |card| card.save(note_exists_already: true) }
|
@@ -134,7 +141,7 @@ module AnkiRecord
|
|
134
141
|
insert into notes (id, guid, mid, mod, usn, tags, flds, sfld, csum, flags, data)
|
135
142
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
136
143
|
SQL
|
137
|
-
statement.execute([@id, @guid, note_type.id,
|
144
|
+
statement.execute([@id, @guid, note_type.id, seconds_since_epoch,
|
138
145
|
@usn, @tags.join(" "), field_values_separated_by_us, sort_field_value,
|
139
146
|
checksum(sort_field_value), @flags, @data])
|
140
147
|
cards.each(&:save)
|
@@ -144,9 +151,5 @@ module AnkiRecord
|
|
144
151
|
# The ASCII control code represented by hexadecimal 1F is the Unit Separator (US)
|
145
152
|
note_type.snake_case_field_names.map { |field_name| @field_contents[field_name] }.join("\x1F")
|
146
153
|
end
|
147
|
-
|
148
|
-
def sort_field_value
|
149
|
-
@field_contents[note_type.snake_case_sort_field_name]
|
150
|
-
end
|
151
154
|
end
|
152
155
|
end
|
data/lib/anki_record/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: anki_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kyle Rego
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-08-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubyzip
|