anki_record 0.4 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|