anki_record 0.3.2 → 0.4
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 +9 -1
- data/Gemfile +4 -0
- data/Gemfile.lock +9 -2
- data/README.md +79 -132
- data/anki_record.gemspec +2 -2
- data/lib/anki_record/anki21_database/anki21_database.rb +138 -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 +110 -176
- 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 +62 -86
- 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: 625337f0aac620a6662e846e1bf08a4ebe7a57224386b7154e24381ff275a507
|
4
|
+
data.tar.gz: cb8e0001a1b37ad480ef3b218a1e64ef1a7429da777bb7f7bd9b68b51d2cbd9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c0e0fbf06f373cf555bd233c6f42aff0123858f80e58b5a525ca72f53bec28572930c91988424b9c3a3756182f779177a92c4f6dc8d8eae86cb46a95d3216ae1
|
7
|
+
data.tar.gz: e89bc47239ad32052e9b51f1825073bc1dd40e8586b56360437fac0dbf7258fd8b6e7eb4d9f7f5a4b4dee8b00f66482fccf3b99aa03a024c83c636a54a4c5e0a
|
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,12 @@
|
|
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.
|
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)
|
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,26 @@ 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 also most of the features of the gem:
|
18
18
|
|
19
|
-
```ruby
|
20
|
-
require "anki_record"
|
21
|
-
|
22
|
-
AnkiRecord::AnkiPackage.new(name: "test") do |collection|
|
23
|
-
3.times do |number|
|
24
|
-
puts "#{3 - number}..."
|
25
|
-
end
|
26
|
-
puts "Countdown complete. Write any Ruby you want in here!"
|
27
|
-
end
|
28
|
-
# test.apkg now exists in the current working directory.
|
29
|
-
```
|
30
|
-
|
31
|
-
While execution is happening inside the block, temporary `collection.anki21` and `collection.anki2` SQLite databases and a `media` file exist inside of a temporary directory. These files are the normal zipped contents of an `*.apkg` file. `collection.anki21` is the database that the library is interacting with.
|
32
|
-
|
33
|
-
If an exception is raised inside the block, the files are deleted without creating a new `*.apkg` zip file, so this is the recommended way.
|
34
|
-
|
35
|
-
Alternatively, if `AnkiRecord::Package::new` is not passed a block, the `zip` method must be explicitly called on the Anki package object:
|
36
|
-
|
37
|
-
```ruby
|
38
|
-
require "anki_record"
|
39
|
-
|
40
|
-
apkg = AnkiRecord::AnkiPackage.new(name: "test")
|
41
|
-
collection = apkg.collection
|
42
|
-
# Add notes to the collection
|
43
|
-
apkg.zip # This zips the temporary files into test.apkg, and then deletes them.
|
44
19
|
```
|
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
20
|
require "anki_record"
|
52
21
|
|
53
|
-
AnkiRecord::AnkiPackage.
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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:, name: "New custom note type")
|
29
|
+
AnkiRecord::NoteField.new(note_type: custom_note_type, name: "custom front")
|
30
|
+
AnkiRecord::NoteField.new(note_type: custom_note_type, name: "custom back")
|
31
|
+
custom_card_template = AnkiRecord::CardTemplate.new(note_type: custom_note_type, name: "Custom template 1")
|
32
|
+
custom_card_template.question_format = "{{custom front}}"
|
33
|
+
custom_card_template.answer_format = "{{custom back}}"
|
34
|
+
second_custom_card_template = AnkiRecord::CardTemplate.new(note_type: custom_note_type, name: "Custom template 2")
|
35
|
+
second_custom_card_template.question_format = "{{custom back}}"
|
36
|
+
second_custom_card_template.answer_format = "{{custom front}}"
|
95
37
|
|
96
38
|
css = <<~CSS
|
97
39
|
.card {
|
@@ -101,83 +43,88 @@ AnkiRecord::AnkiPackage.new(name: "test_1") do |collection|
|
|
101
43
|
text-align: center;
|
102
44
|
}
|
103
45
|
CSS
|
46
|
+
custom_note_type.css = css
|
47
|
+
custom_note_type.save
|
104
48
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
note =
|
109
|
-
note.crazy_front = "Hello from test 1"
|
110
|
-
note.crazy_back = "World"
|
49
|
+
# Creating a note with the custom note type
|
50
|
+
note = AnkiRecord::Note.new(note_type: custom_note_type, deck: custom_deck)
|
51
|
+
note.custom_front = "Content of the 'custom front' field"
|
52
|
+
note.custom_back = "Content of the 'custom back' field"
|
111
53
|
note.save
|
112
54
|
|
113
|
-
|
55
|
+
# The default deck
|
56
|
+
default_deck = anki21_database.find_deck_by(name: "Default")
|
57
|
+
|
58
|
+
# All of the default Anki note types
|
59
|
+
basic_note_type = anki21_database.find_note_type_by(name: "Basic")
|
60
|
+
basic_and_reversed_card_note_type = anki21_database.find_note_type_by(name: "Basic (and reversed card)")
|
61
|
+
basic_and_optional_reversed_card_note_type = anki21_database.find_note_type_by(name: "Basic (optional reversed card)")
|
62
|
+
basic_type_in_the_answer_note_type = anki21_database.find_note_type_by(name: "Basic (type in the answer)")
|
63
|
+
cloze_note_type = anki21_database.find_note_type_by(name: "Cloze")
|
64
|
+
|
65
|
+
# Creating notes using the default note types
|
66
|
+
|
67
|
+
basic_note = AnkiRecord::Note.new(note_type: basic_note_type, deck: default_deck)
|
68
|
+
basic_note.front = "What molecule is most relevant to the name aerobic exercise?"
|
69
|
+
basic_note.back = "Oxygen"
|
70
|
+
basic_note.save
|
71
|
+
|
72
|
+
# Creating a nested deck
|
73
|
+
amino_acids_deck = AnkiRecord::Deck.new(anki21_database:, name: "Biochemistry::Amino acids")
|
74
|
+
amino_acids_deck.save
|
75
|
+
|
76
|
+
basic_and_reversed_note = AnkiRecord::Note.new(note_type: basic_and_reversed_card_note_type, deck: amino_acids_deck)
|
77
|
+
basic_and_reversed_note.front = "Tyrosine"
|
78
|
+
basic_and_reversed_note.back = "Y"
|
79
|
+
basic_and_reversed_note.save
|
80
|
+
|
81
|
+
basic_and_optional_reversed_note = AnkiRecord::Note.new(note_type: basic_and_optional_reversed_card_note_type, deck: default_deck)
|
82
|
+
basic_and_optional_reversed_note.front = "A technique where locations along a route are memorized and associated with ideas"
|
83
|
+
basic_and_optional_reversed_note.back = "The method of loci"
|
84
|
+
basic_and_optional_reversed_note.add_reverse = "Have a reverse card too"
|
85
|
+
basic_and_optional_reversed_note.save
|
86
|
+
|
87
|
+
basic_type_in_the_answer_note = AnkiRecord::Note.new(note_type: basic_type_in_the_answer_note_type, deck: default_deck)
|
88
|
+
basic_type_in_the_answer_note.front = "What Git command commits staged changes by changing the previous commit without editing the commit message?"
|
89
|
+
basic_type_in_the_answer_note.back = "git commit --amend --no-edit"
|
90
|
+
basic_type_in_the_answer_note.save
|
91
|
+
|
92
|
+
cloze_note = AnkiRecord::Note.new(note_type: cloze_note_type, deck: default_deck)
|
93
|
+
cloze_note.text = "Dysfunction of CN {{c1::VII}} occurs in Bell's palsy"
|
94
|
+
cloze_note.back_extra = "This condition involves one cranial nerve but can have myriad neurological symptoms"
|
95
|
+
cloze_note.save
|
114
96
|
end
|
97
|
+
# The example.apkg package now exists in the current working directory and contains 6 notes.
|
115
98
|
|
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
99
|
```
|
122
100
|
|
123
|
-
|
101
|
+
`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
102
|
|
125
|
-
|
103
|
+
The gem can also be used to update an existing Anki package:
|
126
104
|
|
127
|
-
|
105
|
+
```
|
106
|
+
require "anki_record"
|
128
107
|
|
129
|
-
|
108
|
+
AnkiRecord::AnkiPackage.update(path: "./example.apkg") do |anki21_database|
|
109
|
+
amino_acids_deck = anki21_database.find_deck_by(name: "Biochemistry::Amino acids")
|
110
|
+
custom_note_type = anki21_database.find_note_type_by(name: "New custom note type")
|
130
111
|
|
112
|
+
# Create more decks, note types, notes etc. There are not many methods that would be useful here for finding and updating notes yet.
|
113
|
+
end
|
131
114
|
```
|
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
115
|
|
140
|
-
|
116
|
+
If an error is thrown in the block here, the original Anki package will not be changed.
|
117
|
+
|
118
|
+
## Documentation
|
141
119
|
|
142
|
-
The
|
120
|
+
The [API Documentation](https://kylerego.github.io/anki_record_docs) is generated from comments in the source code could be useful if the above examples are not sufficient for your use case.
|
143
121
|
|
144
|
-
|
122
|
+
## Development
|
145
123
|
|
146
124
|
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
125
|
|
148
126
|
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
127
|
|
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
128
|
<!-- ## Contributing
|
182
129
|
|
183
130
|
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,138 @@
|
|
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
|
+
##
|
34
|
+
# Returns the note found by +id+, or nil if it is not found.
|
35
|
+
def find_note_by(id:)
|
36
|
+
note_cards_data = note_cards_data_for_note_id(id:)
|
37
|
+
return nil unless note_cards_data
|
38
|
+
|
39
|
+
AnkiRecord::Note.new(anki21_database: self, data: note_cards_data)
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Returns the note type found by either +name+ or +id+, or nil if it is not found.
|
44
|
+
def find_note_type_by(name: nil, id: nil)
|
45
|
+
if (id && name) || (id.nil? && name.nil?)
|
46
|
+
raise ArgumentError,
|
47
|
+
"You must pass either an id or name keyword argument."
|
48
|
+
end
|
49
|
+
|
50
|
+
name ? find_note_type_by_name(name:) : find_note_type_by_id(id:)
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Returns the deck found by either +name+ or +id+, or nil if it is not found.
|
55
|
+
def find_deck_by(name: nil, id: nil)
|
56
|
+
if (id && name) || (id.nil? && name.nil?)
|
57
|
+
raise ArgumentError,
|
58
|
+
"You must pass either an id or name keyword argument."
|
59
|
+
end
|
60
|
+
|
61
|
+
name ? find_deck_by_name(name:) : find_deck_by_id(id:)
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Returns the deck options group object found by +id+, or nil if it is not found.
|
66
|
+
def find_deck_options_group_by(id:)
|
67
|
+
deck_options_groups.find { |deck_options_group| deck_options_group.id == id }
|
68
|
+
end
|
69
|
+
|
70
|
+
def decks_json # :nodoc:
|
71
|
+
JSON.parse(prepare("select decks from col;").execute.first["decks"])
|
72
|
+
end
|
73
|
+
|
74
|
+
def models_json # :nodoc:
|
75
|
+
JSON.parse(prepare("select models from col;").execute.first["models"])
|
76
|
+
end
|
77
|
+
|
78
|
+
def col_record # :nodoc:
|
79
|
+
prepare("select * from col").execute.first
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_note_type(note_type) # :nodoc:
|
83
|
+
raise ArgumentError unless note_type.instance_of?(AnkiRecord::NoteType)
|
84
|
+
|
85
|
+
existing_note_type = nil
|
86
|
+
note_types.each do |nt|
|
87
|
+
existing_note_type = nt if nt.id == note_type.id
|
88
|
+
end
|
89
|
+
note_types.delete(existing_note_type) if existing_note_type
|
90
|
+
|
91
|
+
note_types << note_type
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_deck(deck) # :nodoc:
|
95
|
+
raise ArgumentError unless deck.instance_of?(AnkiRecord::Deck)
|
96
|
+
|
97
|
+
decks << deck
|
98
|
+
end
|
99
|
+
|
100
|
+
def add_deck_options_group(deck_options_group) # :nodoc:
|
101
|
+
raise ArgumentError unless deck_options_group.instance_of?(AnkiRecord::DeckOptionsGroup)
|
102
|
+
|
103
|
+
deck_options_groups << deck_options_group
|
104
|
+
end
|
105
|
+
|
106
|
+
# :nocov:
|
107
|
+
def inspect # :nodoc:
|
108
|
+
"[= Anki21Database of package with name #{package.name} =]"
|
109
|
+
end
|
110
|
+
# :nocov:
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def find_note_type_by_name(name:)
|
115
|
+
note_types.find { |note_type| note_type.name == name }
|
116
|
+
end
|
117
|
+
|
118
|
+
def find_note_type_by_id(id:)
|
119
|
+
note_types.find { |note_type| note_type.id == id }
|
120
|
+
end
|
121
|
+
|
122
|
+
def find_deck_by_name(name:)
|
123
|
+
decks.find { |deck| deck.name == name }
|
124
|
+
end
|
125
|
+
|
126
|
+
def find_deck_by_id(id:)
|
127
|
+
decks.find { |deck| deck.id == id }
|
128
|
+
end
|
129
|
+
|
130
|
+
def note_cards_data_for_note_id(id:)
|
131
|
+
note_data = prepare("select * from notes where id = ?").execute([id]).first
|
132
|
+
return nil unless note_data
|
133
|
+
|
134
|
+
cards_data = prepare("select * from cards where nid = ?").execute([id]).to_a
|
135
|
+
{ note_data:, cards_data: }
|
136
|
+
end
|
137
|
+
end
|
138
|
+
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
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
##
|
5
|
+
# Anki2Database represents the collection.anki2 Anki SQLite database in the Anki Package
|
6
|
+
#
|
7
|
+
# This is not the database targeted by the Anki Record gem but it is part of the Anki
|
8
|
+
# package zip file.
|
9
|
+
class Anki2Database
|
10
|
+
FILENAME = "collection.anki2"
|
11
|
+
|
12
|
+
# :nodoc:
|
13
|
+
|
14
|
+
attr_reader :anki_package, :database
|
15
|
+
|
16
|
+
def self.create_new(anki_package:)
|
17
|
+
anki2_database = new
|
18
|
+
anki2_database.create_initialize(anki_package:)
|
19
|
+
anki2_database
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_initialize(anki_package:)
|
23
|
+
@anki_package = anki_package
|
24
|
+
@database = SQLite3::Database.new("#{anki_package.tmpdir}/#{FILENAME}", options: {})
|
25
|
+
database.execute_batch(ANKI_SCHEMA_DEFINITION)
|
26
|
+
database.execute(INSERT_COLLECTION_ANKI_2_COL_RECORD)
|
27
|
+
database.close
|
28
|
+
database
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.update_new(anki_package:)
|
32
|
+
anki2_database = new
|
33
|
+
anki2_database.update_initialize(anki_package:)
|
34
|
+
anki2_database
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_initialize(anki_package:)
|
38
|
+
@anki_package = anki_package
|
39
|
+
@database = SQLite3::Database.new("#{anki_package.tmpdir}/#{FILENAME}", options: {})
|
40
|
+
database.close
|
41
|
+
database
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|