anki_record 0.3.1 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.rubocop.yml +2 -7
  4. data/CHANGELOG.md +13 -1
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +9 -2
  7. data/README.md +79 -132
  8. data/anki_record.gemspec +2 -2
  9. data/lib/anki_record/anki21_database/anki21_database.rb +138 -0
  10. data/lib/anki_record/anki21_database/anki21_database_attributes.rb +31 -0
  11. data/lib/anki_record/anki21_database/anki21_database_constructors.rb +52 -0
  12. data/lib/anki_record/anki2_database/anki2_database.rb +44 -0
  13. data/lib/anki_record/anki_package/anki_package.rb +110 -176
  14. data/lib/anki_record/card/card.rb +17 -33
  15. data/lib/anki_record/card/card_attributes.rb +3 -34
  16. data/lib/anki_record/card_template/card_template.rb +2 -2
  17. data/lib/anki_record/card_template/card_template_attributes.rb +11 -11
  18. data/lib/anki_record/collection/collection.rb +20 -154
  19. data/lib/anki_record/collection/collection_attributes.rb +2 -30
  20. data/lib/anki_record/deck/deck.rb +11 -10
  21. data/lib/anki_record/deck/deck_attributes.rb +6 -8
  22. data/lib/anki_record/deck/deck_defaults.rb +1 -1
  23. data/lib/anki_record/deck_options_group/deck_options_group.rb +8 -6
  24. data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +5 -7
  25. data/lib/anki_record/helpers/anki_guid_helper.rb +20 -0
  26. data/lib/anki_record/media/media.rb +36 -0
  27. data/lib/anki_record/note/note.rb +62 -88
  28. data/lib/anki_record/note/note_attributes.rb +19 -18
  29. data/lib/anki_record/note_field/note_field.rb +3 -3
  30. data/lib/anki_record/note_field/note_field_attributes.rb +9 -9
  31. data/lib/anki_record/note_type/note_type.rb +13 -14
  32. data/lib/anki_record/note_type/note_type_attributes.rb +17 -21
  33. data/lib/anki_record/version.rb +1 -1
  34. metadata +11 -6
  35. data/lib/anki_record/helpers/data_query_helper.rb +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b89baf921abc0de4f35babe65cb40cf5fa5647d4d1fe9b3b81ecb80f8a47a15b
4
- data.tar.gz: f766e3344683a73d46450c8bf3cf6ae8226f4cbefee4d5126a194c6dcd831cab
3
+ metadata.gz: 625337f0aac620a6662e846e1bf08a4ebe7a57224386b7154e24381ff275a507
4
+ data.tar.gz: cb8e0001a1b37ad480ef3b218a1e64ef1a7429da777bb7f7bd9b68b51d2cbd9a
5
5
  SHA512:
6
- metadata.gz: 3533d5803a630df54a2c52605fefc8f4dc4ce87e0a649a34672a7a3e369a53722f7d2481e9362388519eb89bd59fbfce8f7f226e0e489433e1d1a7d12fb285fd
7
- data.tar.gz: 7b2ee79fd7ba76b1f229e06831909d3e4b7949c6edc79e608fdc2aae6fe481caa9ccf14c0cd586212b78ef1deddf9e47a91d505c30bd645cf4dae8282e3ff6a8
6
+ metadata.gz: c0e0fbf06f373cf555bd233c6f42aff0123858f80e58b5a525ca72f53bec28572930c91988424b9c3a3756182f779177a92c4f6dc8d8eae86cb46a95d3216ae1
7
+ data.tar.gz: e89bc47239ad32052e9b51f1825073bc1dd40e8586b56360437fac0dbf7258fd8b6e7eb4d9f7f5a4b4dee8b00f66482fccf3b99aa03a024c83c636a54a4c5e0a
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
- --format documentation
2
1
  --color
3
2
  --require spec_helper
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
@@ -37,8 +37,20 @@
37
37
  - API documentation changed from using RDoc to SDoc with the Rails template.
38
38
  - RSpec test suite was refactored to improve speed: 4 minutes -> 1.5 minutes.
39
39
 
40
- ## [0.4.0] - Not released
40
+ ## [0.3.1] - 04-29-2023
41
41
 
42
42
  - `Deck.new` was saving the deck to the `collection.anki21` database. Now it will only instantiate it and `#save` must be called to save it.
43
43
  - `Helper` modules moved into the `Helpers` module namespace.
44
44
  - Bug fix addressing using the approximate milliseconds since the epoch as the primary key id causing the uniqueness constraint to fail when creating a lot of notes.
45
+
46
+ ## [0.3.2] - 05-20-2023
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.
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
@@ -20,3 +20,7 @@ gem "pry"
20
20
  gem "sdoc", "~> 2.6"
21
21
 
22
22
  gem "rubocop-rspec", "~> 2.20"
23
+
24
+ gem "rubocop-rake", "~> 0.6.0"
25
+
26
+ gem "rubocop-performance", "~> 1.18"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- anki_record (0.3.1)
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.0-x86_64-linux)
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 providing an API to Anki flashcard deck packages (zipped SQLite databases). The main thing it does not support yet is adding media to the notes.
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
- The Anki package object is instantiated with `AnkiRecord::AnkiPackage.new`. If this is passed a block, the collection object is yielded to the block, and an Anki deck package file is created after execution of the block:
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.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:, 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
- crazy_note_type.css = css
106
- crazy_note_type.save
107
-
108
- note = AnkiRecord::Note.new note_type: crazy_note_type, deck: crazy_deck
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
- note_id = note.id
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
- This script creates an Anki package `test_1.apkg` with a new deck and new note type, and one note in that deck using that type. It then opens that Anki package, and edits the note. Note that the `test_1.apkg` file is not changed by this. Instead, a new package with a name similar to `test_1-1679835468.apkg` is created (the number is a timestamp).
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
- ## Documentation
103
+ The gem can also be used to update an existing Anki package:
126
104
 
127
- The [API Documentation](https://kylerego.github.io/anki_record_docs) is generated using SDoc from comments in the source code. You might notice that some public methods are intentionally omitted from this documentation. Although public, these methods are not intended to be used outside of the gem's implementation and should be treated as private.
105
+ ```
106
+ require "anki_record"
128
107
 
129
- The RSpec examples are intended to provide executable documentation and may also be helpful to understand the API. Running the test suite with the `rspec` command will output these in a more readable way that also reflects the nesting of the RSpec examples and example groups. This is an example of part of the output:
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 RSpec test suite files in `spec` are organized similarly to the the source code in `lib`.
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
- <!-- ## Development
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 = "Automate Anki flashcard editing with the Ruby programming language."
11
+ spec.summary = "Create and update Anki deck packages with Ruby."
12
12
  spec.description = <<-DESC
13
- A Ruby library which provides a programmatic interface to Anki flashcard decks (.apkg files/zipped Anki SQLite databases).
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