anki_record 0.3.2 → 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/.rspec +0 -1
- data/.rubocop.yml +2 -7
- data/CHANGELOG.md +15 -1
- data/Gemfile +4 -0
- data/Gemfile.lock +9 -2
- data/README.md +89 -132
- data/anki_record.gemspec +2 -2
- data/lib/anki_record/anki21_database/anki21_database.rb +172 -0
- data/lib/anki_record/anki21_database/anki21_database_attributes.rb +31 -0
- data/lib/anki_record/anki21_database/anki21_database_constructors.rb +52 -0
- data/lib/anki_record/anki2_database/anki2_database.rb +44 -0
- data/lib/anki_record/anki_package/anki_package.rb +115 -174
- data/lib/anki_record/card/card.rb +17 -33
- data/lib/anki_record/card/card_attributes.rb +3 -34
- data/lib/anki_record/card_template/card_template.rb +2 -2
- data/lib/anki_record/card_template/card_template_attributes.rb +11 -11
- data/lib/anki_record/collection/collection.rb +20 -154
- data/lib/anki_record/collection/collection_attributes.rb +2 -30
- data/lib/anki_record/deck/deck.rb +11 -10
- data/lib/anki_record/deck/deck_attributes.rb +6 -8
- data/lib/anki_record/deck/deck_defaults.rb +1 -1
- data/lib/anki_record/deck_options_group/deck_options_group.rb +8 -6
- data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +5 -7
- data/lib/anki_record/helpers/anki_guid_helper.rb +20 -0
- data/lib/anki_record/media/media.rb +36 -0
- data/lib/anki_record/note/note.rb +72 -93
- data/lib/anki_record/note/note_attributes.rb +18 -17
- data/lib/anki_record/note_field/note_field.rb +3 -3
- data/lib/anki_record/note_field/note_field_attributes.rb +9 -9
- data/lib/anki_record/note_type/note_type.rb +13 -14
- data/lib/anki_record/note_type/note_type_attributes.rb +17 -21
- data/lib/anki_record/version.rb +1 -1
- metadata +11 -7
- data/lib/anki_record/helpers/data_query_helper.rb +0 -15
- data/lib/anki_record/note/note_guid_helper.rb +0 -10
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/.rspec
CHANGED
data/.rubocop.yml
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require:
|
2
2
|
- rubocop-rspec
|
3
|
+
- rubocop-rake
|
4
|
+
- rubocop-performance
|
3
5
|
|
4
6
|
AllCops:
|
5
7
|
TargetRubyVersion: 3.2.1
|
@@ -9,10 +11,6 @@ Style/StringLiterals:
|
|
9
11
|
Enabled: true
|
10
12
|
EnforcedStyle: double_quotes
|
11
13
|
|
12
|
-
Style/StringLiteralsInInterpolation:
|
13
|
-
Enabled: true
|
14
|
-
EnforcedStyle: double_quotes
|
15
|
-
|
16
14
|
Layout/LineLength:
|
17
15
|
Max: 120
|
18
16
|
# I like the rspec --format doc output to be very readable.
|
@@ -31,9 +29,6 @@ Layout/IndentationConsistency:
|
|
31
29
|
Metrics/ClassLength:
|
32
30
|
Max: 120
|
33
31
|
|
34
|
-
Style/HashSyntax:
|
35
|
-
EnforcedShorthandSyntax: either
|
36
|
-
|
37
32
|
# One expectation per test is a good practice. For this test suite,
|
38
33
|
# following this rule would have a very high performance cost.
|
39
34
|
RSpec/MultipleExpectations:
|
data/CHANGELOG.md
CHANGED
@@ -45,4 +45,18 @@
|
|
45
45
|
|
46
46
|
## [0.3.2] - 05-20-2023
|
47
47
|
- The private `Note` method `globally_unique_id` has been moved to `NoteGuidHelper` and included into note.
|
48
|
-
- The `guid` attribute also now has a public setter.
|
48
|
+
- The `guid` attribute also now has a public setter.
|
49
|
+
|
50
|
+
## [0.4] - 07-08-2023
|
51
|
+
- `AnkiPackage.new` and `AnkiPackage.open` have been removed and replaced with `AnkiPackage.create` and `AnkiPackage.update`.
|
52
|
+
- `AnkiPackage.update` is different from `AnkiPackage.open` in that it does not create a new Anki package with a timestamp. It effectively updates the original Anki package file as long as no error is thrown.
|
53
|
+
- `Anki21Database` is yielded to the block of the above methods instead of `Collection`.
|
54
|
+
- Responsibilites of `Collection` have been reorganized to `Anki21Database`.
|
55
|
+
- The `guid` attribute of notes ic computed in a different way that allows a larger number of possible values.
|
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
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
anki_record (0.
|
4
|
+
anki_record (0.4.1)
|
5
5
|
rubyzip (>= 2.3)
|
6
6
|
sqlite3 (~> 1.3)
|
7
7
|
|
@@ -55,6 +55,11 @@ GEM
|
|
55
55
|
parser (>= 3.1.1.0)
|
56
56
|
rubocop-capybara (2.17.1)
|
57
57
|
rubocop (~> 1.41)
|
58
|
+
rubocop-performance (1.18.0)
|
59
|
+
rubocop (>= 1.7.0, < 2.0)
|
60
|
+
rubocop-ast (>= 0.4.0)
|
61
|
+
rubocop-rake (0.6.0)
|
62
|
+
rubocop (~> 1.0)
|
58
63
|
rubocop-rspec (2.20.0)
|
59
64
|
rubocop (~> 1.33)
|
60
65
|
rubocop-capybara (~> 2.17)
|
@@ -68,7 +73,7 @@ GEM
|
|
68
73
|
simplecov_json_formatter (~> 0.1)
|
69
74
|
simplecov-html (0.12.3)
|
70
75
|
simplecov_json_formatter (0.1.4)
|
71
|
-
sqlite3 (1.6.
|
76
|
+
sqlite3 (1.6.3-x86_64-linux)
|
72
77
|
stringio (3.0.5)
|
73
78
|
unicode-display_width (2.4.2)
|
74
79
|
|
@@ -81,6 +86,8 @@ DEPENDENCIES
|
|
81
86
|
rake (~> 13.0)
|
82
87
|
rspec (~> 3.0)
|
83
88
|
rubocop (~> 1.21)
|
89
|
+
rubocop-performance (~> 1.18)
|
90
|
+
rubocop-rake (~> 0.6.0)
|
84
91
|
rubocop-rspec (~> 2.20)
|
85
92
|
sdoc (~> 2.6)
|
86
93
|
simplecov
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Anki Record
|
2
2
|
|
3
|
-
Anki Record is a Ruby gem
|
3
|
+
Anki Record is a Ruby gem to create and update Anki flashcard deck packages (files with the .apkg extension). It does not support adding media yet.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -14,84 +14,31 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
14
14
|
|
15
15
|
## Usage
|
16
16
|
|
17
|
-
|
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
|
-
AnkiRecord::AnkiPackage.
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
# Add notes to the collection
|
43
|
-
apkg.zip # This zips the temporary files into test.apkg, and then deletes them.
|
44
|
-
```
|
45
|
-
|
46
|
-
The second, optional argument to `AnkiRecord::AnkiPackage.new` is `target_directory`. The default value is the current working directory, but if a relative file path argument is given, the new `*.apkg` file will be saved in that directory. An exception will be raised if the relative file path is not to a directory that exists.
|
47
|
-
|
48
|
-
A new Anki package object is initialized with the "Default" deck and the default note types of a new Anki collection (including "Basic" and "Cloze"). The deck and note type objects are accessed through the `collection` attribute of the Anki package object through the `find_deck_by` and `find_note_type_by` methods passed the `name` keyword argument:
|
49
|
-
|
50
|
-
```ruby
|
51
|
-
require "anki_record"
|
52
|
-
|
53
|
-
AnkiRecord::AnkiPackage.new(name: "test") do |collection|
|
54
|
-
deck = collection.find_deck_by name: "Default"
|
55
|
-
|
56
|
-
note_type = collection.find_note_type_by name: "Basic"
|
57
|
-
|
58
|
-
note = AnkiRecord::Note.new note_type: note_type, deck: deck
|
59
|
-
note.front = "Hello"
|
60
|
-
note.back = "World"
|
61
|
-
note.save
|
62
|
-
|
63
|
-
note_type2 = collection.find_note_type_by name: "Cloze"
|
64
|
-
|
65
|
-
note2 = AnkiRecord::Note.new note_type: note_type2, deck: deck
|
66
|
-
note2.text = "Cloze {{c1::Hello}}"
|
67
|
-
note2.back_extra = "World"
|
68
|
-
note2.save
|
69
|
-
end
|
70
|
-
|
71
|
-
```
|
72
|
-
|
73
|
-
This example creates a `test.apkg` zip file in the current working directory, which when imported into Anki, will add one Basic note and one Cloze note.
|
74
|
-
|
75
|
-
The next example shows some other features of the library:
|
76
|
-
|
77
|
-
```ruby
|
78
|
-
require "anki_record"
|
79
|
-
|
80
|
-
note_id = nil
|
81
|
-
|
82
|
-
AnkiRecord::AnkiPackage.new(name: "test_1") do |collection|
|
83
|
-
crazy_deck = AnkiRecord::Deck.new collection: collection, name: "test_1_deck"
|
84
|
-
crazy_deck.save
|
85
|
-
|
86
|
-
crazy_note_type = AnkiRecord::NoteType.new collection: collection, name: "test 1 note type"
|
87
|
-
AnkiRecord::NoteField.new note_type: crazy_note_type, name: "crazy front"
|
88
|
-
AnkiRecord::NoteField.new note_type: crazy_note_type, name: "crazy back"
|
89
|
-
crazy_card_template = AnkiRecord::CardTemplate.new note_type: crazy_note_type, name: "test 1 card 1"
|
90
|
-
crazy_card_template.question_format = "{{crazy front}}"
|
91
|
-
crazy_card_template.answer_format = "{{crazy back}}"
|
92
|
-
second_crazy_card_template = AnkiRecord::CardTemplate.new note_type: crazy_note_type, name: "test 1 card 2"
|
93
|
-
second_crazy_card_template.question_format = "{{crazy back}}"
|
94
|
-
second_crazy_card_template.answer_format = "{{crazy front}}"
|
22
|
+
AnkiRecord::AnkiPackage.create(name: "example") do |anki21_database|
|
23
|
+
# Creating a new deck
|
24
|
+
custom_deck = AnkiRecord::Deck.new(anki21_database:, name: "New custom deck")
|
25
|
+
custom_deck.save
|
26
|
+
|
27
|
+
# Creating a new note type
|
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")
|
36
|
+
custom_card_template.question_format = "{{custom front}}"
|
37
|
+
custom_card_template.answer_format = "{{custom back}}"
|
38
|
+
second_custom_card_template = AnkiRecord::CardTemplate.new(note_type: custom_note_type,
|
39
|
+
name: "Custom template 2")
|
40
|
+
second_custom_card_template.question_format = "{{custom back}}"
|
41
|
+
second_custom_card_template.answer_format = "{{custom front}}"
|
95
42
|
|
96
43
|
css = <<~CSS
|
97
44
|
.card {
|
@@ -101,83 +48,93 @@ AnkiRecord::AnkiPackage.new(name: "test_1") do |collection|
|
|
101
48
|
text-align: center;
|
102
49
|
}
|
103
50
|
CSS
|
51
|
+
custom_note_type.css = css
|
52
|
+
custom_note_type.save
|
104
53
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
note =
|
109
|
-
note.crazy_front = "Hello from test 1"
|
110
|
-
note.crazy_back = "World"
|
54
|
+
# Creating a new note with the custom note type
|
55
|
+
note = AnkiRecord::Note.new(note_type: custom_note_type, deck: custom_deck)
|
56
|
+
note.custom_front = "Content of the 'custom front' field"
|
57
|
+
note.custom_back = "Content of the 'custom back' field"
|
111
58
|
note.save
|
112
59
|
|
113
|
-
|
60
|
+
# Finding the default deck
|
61
|
+
default_deck = anki21_database.find_deck_by(name: "Default")
|
62
|
+
|
63
|
+
# Finding all of the default Anki note types
|
64
|
+
basic_note_type = anki21_database.find_note_type_by(name: "Basic")
|
65
|
+
basic_and_reversed_card_note_type = anki21_database.find_note_type_by(name: "Basic (and reversed card)")
|
66
|
+
basic_and_optional_reversed_card_note_type = anki21_database.find_note_type_by(name: "Basic (optional reversed card)")
|
67
|
+
basic_type_in_the_answer_note_type = anki21_database.find_note_type_by(name: "Basic (type in the answer)")
|
68
|
+
cloze_note_type = anki21_database.find_note_type_by(name: "Cloze")
|
69
|
+
|
70
|
+
# Creating new notes using the default note types
|
71
|
+
|
72
|
+
basic_note = AnkiRecord::Note.new(note_type: basic_note_type, deck: default_deck)
|
73
|
+
basic_note.front = "What molecule is most relevant to the name aerobic exercise?"
|
74
|
+
basic_note.back = "Oxygen"
|
75
|
+
basic_note.save
|
76
|
+
|
77
|
+
# Creating a new nested deck
|
78
|
+
amino_acids_deck = AnkiRecord::Deck.new(anki21_database:,
|
79
|
+
name: "Biochemistry::Amino acids")
|
80
|
+
amino_acids_deck.save
|
81
|
+
|
82
|
+
basic_and_reversed_note = AnkiRecord::Note.new(note_type: basic_and_reversed_card_note_type,
|
83
|
+
deck: amino_acids_deck)
|
84
|
+
basic_and_reversed_note.front = "Tyrosine"
|
85
|
+
basic_and_reversed_note.back = "Y"
|
86
|
+
basic_and_reversed_note.save
|
87
|
+
|
88
|
+
basic_and_optional_reversed_note = AnkiRecord::Note.new(note_type: basic_and_optional_reversed_card_note_type,
|
89
|
+
deck: default_deck)
|
90
|
+
basic_and_optional_reversed_note.front = "A technique where locations along a route are memorized and associated with ideas"
|
91
|
+
basic_and_optional_reversed_note.back = "The method of loci"
|
92
|
+
basic_and_optional_reversed_note.add_reverse = "Have a reverse card too"
|
93
|
+
basic_and_optional_reversed_note.save
|
94
|
+
|
95
|
+
basic_type_in_the_answer_note = AnkiRecord::Note.new(note_type: basic_type_in_the_answer_note_type,
|
96
|
+
deck: default_deck)
|
97
|
+
basic_type_in_the_answer_note.front = "What Git command commits staged changes by changing the previous commit without editing the commit message?"
|
98
|
+
basic_type_in_the_answer_note.back = "git commit --amend --no-edit"
|
99
|
+
basic_type_in_the_answer_note.save
|
100
|
+
|
101
|
+
cloze_note = AnkiRecord::Note.new(note_type: cloze_note_type, deck: default_deck)
|
102
|
+
cloze_note.text = "Dysfunction of CN {{c1::VII}} occurs in Bell's palsy"
|
103
|
+
cloze_note.back_extra = "This condition involves one cranial nerve but can have myriad neurological symptoms"
|
104
|
+
cloze_note.save
|
114
105
|
end
|
106
|
+
# An example.apkg file should be in the current
|
107
|
+
# working directory with 6 notes.
|
115
108
|
|
116
|
-
AnkiRecord::AnkiPackage.open(path: "./test_1.apkg") do |collection|
|
117
|
-
note = collection.find_note_by id: note_id
|
118
|
-
note.crazy_back = "Ruby"
|
119
|
-
note.save
|
120
|
-
end
|
121
109
|
```
|
122
110
|
|
123
|
-
|
111
|
+
`AnkiRecord::AnkiPackage.new` can also take a `target_directory` keyword argument to specify the directory to save the Anki package. If an error is thrown inside the block argument, temporary files that exist during execution of the block (Anki SQLite databases and the file called `media`) are deleted and no new Anki package is created.
|
124
112
|
|
125
|
-
|
113
|
+
The gem can also be used to update an existing Anki package:
|
126
114
|
|
127
|
-
|
115
|
+
```ruby
|
116
|
+
require "anki_record"
|
128
117
|
|
129
|
-
|
118
|
+
AnkiRecord::AnkiPackage.update(path: "./example.apkg") do |anki21_database|
|
119
|
+
amino_acids_deck = anki21_database.find_deck_by(name: "Biochemistry::Amino acids")
|
120
|
+
custom_note_type = anki21_database.find_note_type_by(name: "New custom note type")
|
130
121
|
|
122
|
+
# Create more decks, note types, notes etc. There are not many methods that would be useful here for finding and updating notes yet.
|
123
|
+
end
|
131
124
|
```
|
132
|
-
AnkiRecord::Deck#save
|
133
|
-
when the deck does not exist in the collection.anki21 database
|
134
|
-
saves the deck object's id as a key in the decks column's JSON object in the collection.anki21 database
|
135
|
-
saves the deck object as a hash, as the value of the deck object's id key, in the decks hash
|
136
|
-
saves the deck object as a hash with the following keys: 'id', 'mod', 'name', 'usn', 'lrnToday', 'revToday', 'newToday', 'timeToday', 'collapsed', 'browserCollapsed', 'desc', 'dyn', 'conf', 'extendNew', 'extendRev'
|
137
|
-
saves the deck object as a hash with the deck object's id attribute as the value for the id key in the deck hash
|
138
|
-
saves the deck object as a hash with the deck object's last_modified_timestamp attribute as the value for the mod key in the deck hash
|
139
125
|
|
140
|
-
|
126
|
+
If an error is thrown in the block here, the original Anki package will not be changed.
|
141
127
|
|
142
|
-
|
128
|
+
## Documentation
|
143
129
|
|
144
|
-
|
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.
|
131
|
+
|
132
|
+
## Development
|
145
133
|
|
146
134
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
147
135
|
|
148
136
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
149
137
|
|
150
|
-
### Development road map:
|
151
|
-
- Better messages when `ArgumentError` raised
|
152
|
-
- Add #inspect methods
|
153
|
-
- Refactor tests to improve speed
|
154
|
-
- Copying the contents of an existing package into the new package when it is opened
|
155
|
-
- Add more unit tests
|
156
|
-
- Work on creating, updating, and saving notes and cards to the collection.anki21 database
|
157
|
-
- Updating notes when they already exist in the database
|
158
|
-
- Add more unit tests
|
159
|
-
- Validation logic of what makes the note valid based on the note type's card templates and fields
|
160
|
-
- Work on adding media support
|
161
|
-
- The checksum calculation for notes will need to be updated to account for HTML in the content
|
162
|
-
- Saving note types, decks, and deck options groups to the collection.anki21 database
|
163
|
-
- Deck options groups cannot be saved yet.
|
164
|
-
- Add being able to handle subdecks
|
165
|
-
- Updating them when they already exist
|
166
|
-
- Setters for any relevant attributes with validation
|
167
|
-
- Refactoring
|
168
|
-
- Use more specific RSpec matchers than `eq` everywhere
|
169
|
-
- Investigate if note guid is determined in Anki in a non-random way
|
170
|
-
- Investigate if the database ever needs to be explicitly opened or closed
|
171
|
-
- Note type allowed fields: investigate if there are other special field names that should be allowed.
|
172
|
-
|
173
|
-
### Release checklist
|
174
|
-
- Remove `require "pry"`
|
175
|
-
- Update changelog
|
176
|
-
- Update usage examples
|
177
|
-
- Update and regenerate documentation
|
178
|
-
- Bump version
|
179
|
-
- Release gem -->
|
180
|
-
|
181
138
|
<!-- ## Contributing
|
182
139
|
|
183
140
|
Bug reports and pull requests are welcome on GitHub at https://github.com/KyleRego/anki_record. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/KyleRego/anki_record/blob/master/CODE_OF_CONDUCT.md). -->
|
data/anki_record.gemspec
CHANGED
@@ -8,9 +8,9 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.authors = ["Kyle Rego"]
|
9
9
|
spec.email = ["regoky@outlook.com"]
|
10
10
|
|
11
|
-
spec.summary = "
|
11
|
+
spec.summary = "Create and update Anki deck packages with Ruby."
|
12
12
|
spec.description = <<-DESC
|
13
|
-
|
13
|
+
Anki Record lets you create Anki deck packages or update existing ones with the Ruby programming language.
|
14
14
|
DESC
|
15
15
|
spec.homepage = "https://github.com/KyleRego/anki_record"
|
16
16
|
spec.license = "MIT"
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "anki21_database_attributes"
|
4
|
+
require_relative "anki21_database_constructors"
|
5
|
+
|
6
|
+
module AnkiRecord
|
7
|
+
##
|
8
|
+
# Anki21Database represents the collection.anki21 Anki SQLite database in the Anki Package
|
9
|
+
class Anki21Database
|
10
|
+
include Anki21DatabaseAttributes
|
11
|
+
include Anki21DatabaseConstructors
|
12
|
+
|
13
|
+
def self.create_new(anki_package:) # :nodoc:
|
14
|
+
anki21_database = new
|
15
|
+
anki21_database.create_initialize(anki_package:)
|
16
|
+
anki21_database
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.update_new(anki_package:) # :nodoc:
|
20
|
+
anki21_database = new
|
21
|
+
anki21_database.update_initialize(anki_package:)
|
22
|
+
anki21_database
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Returns an SQLite3::Statement object (sqlite3 gem) to be executed against the collection.anki21 database.
|
27
|
+
#
|
28
|
+
# Statement#execute executes the statement.
|
29
|
+
def prepare(sql)
|
30
|
+
database.prepare sql
|
31
|
+
end
|
32
|
+
|
33
|
+
# rubocop:disable Metrics/MethodLength
|
34
|
+
##
|
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
|
46
|
+
return nil unless note_cards_data
|
47
|
+
|
48
|
+
AnkiRecord::Note.new(anki21_database: self, data: note_cards_data)
|
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
|
69
|
+
|
70
|
+
##
|
71
|
+
# Returns the note type found by either +name+ or +id+, or nil if it is not found.
|
72
|
+
def find_note_type_by(name: nil, id: nil)
|
73
|
+
if (id && name) || (id.nil? && name.nil?)
|
74
|
+
raise ArgumentError,
|
75
|
+
"You must pass either an id or name keyword argument to find_note_type_by."
|
76
|
+
end
|
77
|
+
|
78
|
+
name ? find_note_type_by_name(name:) : find_note_type_by_id(id:)
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Returns the deck found by either +name+ or +id+, or nil if it is not found.
|
83
|
+
def find_deck_by(name: nil, id: nil)
|
84
|
+
if (id && name) || (id.nil? && name.nil?)
|
85
|
+
raise ArgumentError,
|
86
|
+
"You must pass either an id or name keyword argument to find_deck_by."
|
87
|
+
end
|
88
|
+
|
89
|
+
name ? find_deck_by_name(name:) : find_deck_by_id(id:)
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Returns the deck options group object found by +id+, or nil if it is not found.
|
94
|
+
def find_deck_options_group_by(id:)
|
95
|
+
deck_options_groups.find { |deck_options_group| deck_options_group.id == id }
|
96
|
+
end
|
97
|
+
|
98
|
+
def decks_json # :nodoc:
|
99
|
+
JSON.parse(prepare("select decks from col;").execute.first["decks"])
|
100
|
+
end
|
101
|
+
|
102
|
+
def models_json # :nodoc:
|
103
|
+
JSON.parse(prepare("select models from col;").execute.first["models"])
|
104
|
+
end
|
105
|
+
|
106
|
+
def col_record # :nodoc:
|
107
|
+
prepare("select * from col").execute.first
|
108
|
+
end
|
109
|
+
|
110
|
+
def add_note_type(note_type) # :nodoc:
|
111
|
+
raise ArgumentError unless note_type.instance_of?(AnkiRecord::NoteType)
|
112
|
+
|
113
|
+
existing_note_type = nil
|
114
|
+
note_types.each do |nt|
|
115
|
+
existing_note_type = nt if nt.id == note_type.id
|
116
|
+
end
|
117
|
+
note_types.delete(existing_note_type) if existing_note_type
|
118
|
+
|
119
|
+
note_types << note_type
|
120
|
+
end
|
121
|
+
|
122
|
+
def add_deck(deck) # :nodoc:
|
123
|
+
raise ArgumentError unless deck.instance_of?(AnkiRecord::Deck)
|
124
|
+
|
125
|
+
decks << deck
|
126
|
+
end
|
127
|
+
|
128
|
+
def add_deck_options_group(deck_options_group) # :nodoc:
|
129
|
+
raise ArgumentError unless deck_options_group.instance_of?(AnkiRecord::DeckOptionsGroup)
|
130
|
+
|
131
|
+
deck_options_groups << deck_options_group
|
132
|
+
end
|
133
|
+
|
134
|
+
# :nocov:
|
135
|
+
def inspect # :nodoc:
|
136
|
+
"[= Anki21Database of package with name #{package.name} =]"
|
137
|
+
end
|
138
|
+
# :nocov:
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def find_note_type_by_name(name:)
|
143
|
+
note_types.find { |note_type| note_type.name == name }
|
144
|
+
end
|
145
|
+
|
146
|
+
def find_note_type_by_id(id:)
|
147
|
+
note_types.find { |note_type| note_type.id == id }
|
148
|
+
end
|
149
|
+
|
150
|
+
def find_deck_by_name(name:)
|
151
|
+
decks.find { |deck| deck.name == name }
|
152
|
+
end
|
153
|
+
|
154
|
+
def find_deck_by_id(id:)
|
155
|
+
decks.find { |deck| deck.id == id }
|
156
|
+
end
|
157
|
+
|
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
|
164
|
+
return nil unless note_data
|
165
|
+
|
166
|
+
id ||= note_data["id"]
|
167
|
+
|
168
|
+
cards_data = prepare("select * from cards where nid = ?").execute([id]).to_a
|
169
|
+
{ note_data:, cards_data: }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
##
|
5
|
+
# Module with Anki21Database's attribute readers, writers, and accessors
|
6
|
+
module Anki21DatabaseAttributes
|
7
|
+
##
|
8
|
+
# The database's note types as an array
|
9
|
+
attr_reader :note_types
|
10
|
+
|
11
|
+
##
|
12
|
+
# The database's decks as an array
|
13
|
+
attr_reader :decks
|
14
|
+
|
15
|
+
##
|
16
|
+
# The database's deck option groups as an array
|
17
|
+
attr_reader :deck_options_groups
|
18
|
+
|
19
|
+
##
|
20
|
+
# The database's parent Anki package
|
21
|
+
attr_reader :anki_package
|
22
|
+
|
23
|
+
##
|
24
|
+
# The database's collection record
|
25
|
+
attr_reader :collection
|
26
|
+
|
27
|
+
##
|
28
|
+
# The database's collection.anki21 SQLite3::Database
|
29
|
+
attr_reader :database
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
module Anki21DatabaseConstructors # :nodoc:
|
5
|
+
FILENAME = "collection.anki21"
|
6
|
+
|
7
|
+
def create_initialize(anki_package:)
|
8
|
+
@anki_package = anki_package
|
9
|
+
@database = SQLite3::Database.new "#{anki_package.tmpdir}/#{FILENAME}", options: {}
|
10
|
+
database.execute_batch ANKI_SCHEMA_DEFINITION
|
11
|
+
database.execute INSERT_COLLECTION_ANKI_21_COL_RECORD
|
12
|
+
database.results_as_hash = true
|
13
|
+
@collection = Collection.new(anki21_database: self)
|
14
|
+
initialize_note_types
|
15
|
+
initialize_deck_options_groups
|
16
|
+
initialize_decks
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_initialize(anki_package:)
|
20
|
+
@anki_package = anki_package
|
21
|
+
@database = SQLite3::Database.new("#{anki_package.tmpdir}/#{FILENAME}", options: {})
|
22
|
+
database.results_as_hash = true
|
23
|
+
@collection = Collection.new(anki21_database: self)
|
24
|
+
initialize_note_types
|
25
|
+
initialize_deck_options_groups
|
26
|
+
initialize_decks
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def initialize_note_types
|
32
|
+
@note_types = []
|
33
|
+
JSON.parse(col_record["models"]).values.map do |model_hash|
|
34
|
+
NoteType.new(anki21_database: self, args: model_hash)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize_decks
|
39
|
+
@decks = []
|
40
|
+
JSON.parse(col_record["decks"]).values.map do |deck_hash|
|
41
|
+
Deck.new(anki21_database: self, args: deck_hash)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize_deck_options_groups
|
46
|
+
@deck_options_groups = []
|
47
|
+
JSON.parse(col_record["dconf"]).values.map do |dconf_hash|
|
48
|
+
DeckOptionsGroup.new(anki21_database: self, args: dconf_hash)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|