anki_record 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -2
  3. data/CHANGELOG.md +37 -9
  4. data/Gemfile +3 -1
  5. data/Gemfile.lock +10 -2
  6. data/README.md +120 -35
  7. data/anki_record.gemspec +1 -5
  8. data/lib/anki_record/anki_package/anki_package.rb +237 -0
  9. data/lib/anki_record/card/card.rb +108 -0
  10. data/lib/anki_record/card/card_attributes.rb +39 -0
  11. data/lib/anki_record/card_template/card_template.rb +64 -0
  12. data/lib/anki_record/card_template/card_template_attributes.rb +69 -0
  13. data/lib/anki_record/collection/collection.rb +182 -0
  14. data/lib/anki_record/collection/collection_attributes.rb +35 -0
  15. data/lib/anki_record/database_setup_constants.rb +88 -0
  16. data/lib/anki_record/deck/deck.rb +99 -0
  17. data/lib/anki_record/deck/deck_attributes.rb +30 -0
  18. data/lib/anki_record/deck/deck_defaults.rb +19 -0
  19. data/lib/anki_record/{deck_options_group.rb → deck_options_group/deck_options_group.rb} +12 -31
  20. data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +23 -0
  21. data/lib/anki_record/helpers/checksum_helper.rb +10 -11
  22. data/lib/anki_record/helpers/data_query_helper.rb +15 -0
  23. data/lib/anki_record/helpers/shared_constants_helper.rb +6 -6
  24. data/lib/anki_record/helpers/time_helper.rb +18 -13
  25. data/lib/anki_record/note/note.rb +178 -0
  26. data/lib/anki_record/note/note_attributes.rb +56 -0
  27. data/lib/anki_record/note_field/note_field.rb +62 -0
  28. data/lib/anki_record/note_field/note_field_attributes.rb +39 -0
  29. data/lib/anki_record/note_field/note_field_defaults.rb +19 -0
  30. data/lib/anki_record/note_type/note_type.rb +161 -0
  31. data/lib/anki_record/note_type/note_type_attributes.rb +80 -0
  32. data/lib/anki_record/note_type/note_type_defaults.rb +38 -0
  33. data/lib/anki_record/version.rb +1 -1
  34. data/lib/anki_record.rb +1 -16
  35. metadata +26 -16
  36. data/lib/anki_record/anki_package.rb +0 -194
  37. data/lib/anki_record/card.rb +0 -75
  38. data/lib/anki_record/card_template.rb +0 -105
  39. data/lib/anki_record/collection.rb +0 -105
  40. data/lib/anki_record/db/anki_schema_definition.rb +0 -77
  41. data/lib/anki_record/db/clean_collection21_record.rb +0 -10
  42. data/lib/anki_record/db/clean_collection2_record.rb +0 -10
  43. data/lib/anki_record/deck.rb +0 -101
  44. data/lib/anki_record/note.rb +0 -135
  45. data/lib/anki_record/note_field.rb +0 -84
  46. data/lib/anki_record/note_type.rb +0 -233
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 060b1e641776c8bf67bc20fa50029c1801ca7b367e2e038b2aa9b3024cd7b22b
4
- data.tar.gz: 21a27f8b9d0726122bc2ba22eb6132573e161d30d73e4db866f081db87ea8409
3
+ metadata.gz: b89baf921abc0de4f35babe65cb40cf5fa5647d4d1fe9b3b81ecb80f8a47a15b
4
+ data.tar.gz: f766e3344683a73d46450c8bf3cf6ae8226f4cbefee4d5126a194c6dcd831cab
5
5
  SHA512:
6
- metadata.gz: 8fdf8b0330814f95a11dffba1fbf2d113aa97269a8eba51d3675d7a8a761e37148dc5561b5fd96eeb9c94fd8b416ae00ade70acf88b20a0e5f7a9a668c4a8a7b
7
- data.tar.gz: de6e49c1acbb6f56b952f682be94affaa8beecfcea79fb473fed9b53f0970e40394302b48a2b2cc167315589d8200669a13f5f4721ac184a4606b8a3ef4972f2
6
+ metadata.gz: 3533d5803a630df54a2c52605fefc8f4dc4ce87e0a649a34672a7a3e369a53722f7d2481e9362388519eb89bd59fbfce8f7f226e0e489433e1d1a7d12fb285fd
7
+ data.tar.gz: 7b2ee79fd7ba76b1f229e06831909d3e4b7949c6edc79e608fdc2aae6fe481caa9ccf14c0cd586212b78ef1deddf9e47a91d505c30bd645cf4dae8282e3ff6a8
data/.rubocop.yml CHANGED
@@ -1,3 +1,6 @@
1
+ require:
2
+ - rubocop-rspec
3
+
1
4
  AllCops:
2
5
  TargetRubyVersion: 3.2.1
3
6
  NewCops: enable
@@ -12,12 +15,15 @@ Style/StringLiteralsInInterpolation:
12
15
 
13
16
  Layout/LineLength:
14
17
  Max: 120
18
+ # I like the rspec --format doc output to be very readable.
15
19
  Exclude:
16
- - "spec/anki_record/*"
20
+ - "spec/anki_record/**/*"
17
21
 
18
22
  Metrics/BlockLength:
19
23
  Exclude:
24
+ - "spec/*"
20
25
  - "spec/anki_record/*"
26
+ - "bin/test_scripts/*"
21
27
 
22
28
  Layout/IndentationConsistency:
23
29
  EnforcedStyle: indented_internal_methods
@@ -26,4 +32,9 @@ Metrics/ClassLength:
26
32
  Max: 120
27
33
 
28
34
  Style/HashSyntax:
29
- EnforcedShorthandSyntax: either
35
+ EnforcedShorthandSyntax: either
36
+
37
+ # One expectation per test is a good practice. For this test suite,
38
+ # following this rule would have a very high performance cost.
39
+ RSpec/MultipleExpectations:
40
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,16 +1,44 @@
1
1
  ## [Development started] - 02-02-2023
2
2
 
3
- ## [Unreleased/0.1.0] - 02-22-2023
4
-
5
- - The gem can be used to create an *.apkg zip file that successfully imports into Anki.
6
- - Raw SQL statements can be executed against the temporary database before it is zipped.
7
-
8
3
  ## [0.1.1] - 02-24-2023
9
4
 
10
- - Updated documentation to release the first version
5
+ - The initial version released.
6
+ - The gem can create empty `.apkg` files that import into Anki.
7
+ - SQL statements can be executed against the `collection.anki21` database before the zip file is created.
11
8
 
12
9
  ## [0.2.0] - 03-05-2023
13
10
 
14
- - `AnkiPackage#zip_and_close` is changed to `AnkiPackage#zip`
15
- - Decks and note types can be accessed through the collection object
16
- - Notes can be created, updated, and saved to the database and this also populates the corresponding records in the `cards` table
11
+ - `AnkiPackage#zip_and_close` renamed to `AnkiPackage#zip`.
12
+ - Decks and note types can be accessed with `Collection#find_deck_by` and `Collection#find_note_type_by`.
13
+ - Note objects can be created and updated, and then saved to the `collection.anki21` database.
14
+ - This also populates corresponding records in the `cards` table.
15
+
16
+ ## [0.3.0] - 03-26-2023
17
+
18
+ - `AnkiPackage::new` yields the collection object to the block instead of the Anki package object.
19
+ - `AnkiPackage::open` has been developed to a point that it is useable.
20
+ - An "opened" Anki package now has its contents copied into the temporary `collection.anki21` database.
21
+ - `AnkiPackage#execute` was removed.
22
+ - `AnkiPackage#prepare` was added. Any SQL statements executed directly against `collection.anki21` must now be prepared statements.
23
+ - Custom decks (and nested decks/subdecks) and custom note types can be created and updated, and then saved to the `collection.anki21` database.
24
+ - Notes can be accessed with `Collection#find_note_by`.
25
+ - `Note#save` now updates a note (and its corresponding cards) if it was already in the `collection.anki21` database.
26
+ - `Note::new` does not accept a `cloze` argument anymore; this attribute can be changed after instantiation with the `cloze=` setter.
27
+ - `Deck` has a `deck_options_group` attribute instead of `deck_options_group_id`
28
+ - `#inspect` added to `Deck`
29
+ - Deck options groups can be accessed with `Collection#find_deck_options_group_by`
30
+ - Multiple classes with `last_modified_time` and `creation_timestamp` attributes had these renamed to `last_modified_timestamp` and `created_at_timestamp`.
31
+ - More helpful error messages in various places (e.g. "The package name must be a string without spaces.").
32
+ - Bug fixes that may have affected previous version:
33
+ - Instantiating a note type from an existing Anki package no longer duplicates the note type when it is saved.
34
+ - Note types are not instantiated/saved with an invalid `req` value.
35
+ - In fixing this bug, other issues with `tags` and `vers` were introduced and then fixed.
36
+ - It was also noticed that the default note types with "Basic" in the name should not have `tags` and `vers` so this was changed too.
37
+ - API documentation changed from using RDoc to SDoc with the Rails template.
38
+ - RSpec test suite was refactored to improve speed: 4 minutes -> 1.5 minutes.
39
+
40
+ ## [0.4.0] - Not released
41
+
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
+ - `Helper` modules moved into the `Helpers` module namespace.
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.
data/Gemfile CHANGED
@@ -17,4 +17,6 @@ gem "rubocop", "~> 1.21"
17
17
 
18
18
  gem "pry"
19
19
 
20
- gem "rdoc"
20
+ gem "sdoc", "~> 2.6"
21
+
22
+ gem "rubocop-rspec", "~> 2.20"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- anki_record (0.2.0)
4
+ anki_record (0.3.1)
5
5
  rubyzip (>= 2.3)
6
6
  sqlite3 (~> 1.3)
7
7
 
@@ -53,8 +53,15 @@ GEM
53
53
  unicode-display_width (>= 2.4.0, < 3.0)
54
54
  rubocop-ast (1.24.1)
55
55
  parser (>= 3.1.1.0)
56
+ rubocop-capybara (2.17.1)
57
+ rubocop (~> 1.41)
58
+ rubocop-rspec (2.20.0)
59
+ rubocop (~> 1.33)
60
+ rubocop-capybara (~> 2.17)
56
61
  ruby-progressbar (1.11.0)
57
62
  rubyzip (2.3.2)
63
+ sdoc (2.6.1)
64
+ rdoc (>= 5.0)
58
65
  simplecov (0.22.0)
59
66
  docile (~> 1.1)
60
67
  simplecov-html (~> 0.11)
@@ -72,9 +79,10 @@ DEPENDENCIES
72
79
  anki_record!
73
80
  pry
74
81
  rake (~> 13.0)
75
- rdoc
76
82
  rspec (~> 3.0)
77
83
  rubocop (~> 1.21)
84
+ rubocop-rspec (~> 2.20)
85
+ sdoc (~> 2.6)
78
86
  simplecov
79
87
 
80
88
  RUBY VERSION
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
- # AnkiRecord
1
+ # Anki Record
2
2
 
3
- AnkiRecord is a Ruby gem which provides a programmatic interface to creating and updating Anki flashcard deck files (`*.apkg` Anki SQLite databases). **This gem is in an early stage of development--I do not recommend you use it yet.**
4
-
5
- [API Documentation](https://kylerego.github.io/anki_record_docs)
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.
6
4
 
7
5
  ## Installation
8
6
 
@@ -16,20 +14,23 @@ If bundler is not being used to manage dependencies, install the gem by executin
16
14
 
17
15
  ## Usage
18
16
 
19
- The Anki package object is instantiated with `AnkiRecord::AnkiPackage.new` and if passed a block, will execute the block and zip the `*.apkg` file:
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:
20
18
 
21
19
  ```ruby
22
20
  require "anki_record"
23
21
 
24
- AnkiRecord::AnkiPackage.new(name: "test") do |apkg|
22
+ AnkiRecord::AnkiPackage.new(name: "test") do |collection|
25
23
  3.times do |number|
26
24
  puts "#{3 - number}..."
27
25
  end
28
26
  puts "Countdown complete. Write any Ruby you want in here!"
29
27
  end
28
+ # test.apkg now exists in the current working directory.
30
29
  ```
31
30
 
32
- If an exception is raised inside the block, the temporary `collection.anki2` and `collection.anki21` databases are deleted without creating a new `*.apkg` zip file, so this is the recommended way.
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.
33
34
 
34
35
  Alternatively, if `AnkiRecord::Package::new` is not passed a block, the `zip` method must be explicitly called on the Anki package object:
35
36
 
@@ -37,61 +38,145 @@ Alternatively, if `AnkiRecord::Package::new` is not passed a block, the `zip` me
37
38
  require "anki_record"
38
39
 
39
40
  apkg = AnkiRecord::AnkiPackage.new(name: "test")
40
- apkg.zip
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.
41
44
  ```
42
45
 
43
- 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 can be accessed through the `collection` attribute of the Anki package object through the `find_deck_by` and `find_note_type_by` methods by passing the `name` keyword argument:
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:
44
49
 
45
50
  ```ruby
46
51
  require "anki_record"
47
52
 
48
- apkg = AnkiRecord::AnkiPackage.new name: "test"
53
+ AnkiRecord::AnkiPackage.new(name: "test") do |collection|
54
+ deck = collection.find_deck_by name: "Default"
49
55
 
50
- deck = apkg.collection.find_deck_by name: "Default"
56
+ note_type = collection.find_note_type_by name: "Basic"
51
57
 
52
- note_type = apkg.collection.find_note_type_by name: "Basic"
58
+ note = AnkiRecord::Note.new note_type: note_type, deck: deck
59
+ note.front = "Hello"
60
+ note.back = "World"
61
+ note.save
53
62
 
54
- note = AnkiRecord::Note.new note_type: note_type, deck: deck
55
- note.front = "Hello"
56
- note.back = "World"
57
- note.save
63
+ note_type2 = collection.find_note_type_by name: "Cloze"
58
64
 
59
- note_type2 = apkg.collection.find_note_type_by name: "Cloze"
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
60
70
 
61
- note2 = AnkiRecord::Note.new note_type: note_type2, deck: deck
62
- note2.text = "Cloze {{c1::Hello}}"
63
- note2.back_extra = "World"
64
- note2.save
71
+ ```
65
72
 
66
- apkg.zip
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.
67
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}}"
95
+
96
+ css = <<~CSS
97
+ .card {
98
+ font-size: 16px;
99
+ font-style: Verdana;
100
+ background: transparent;
101
+ text-align: center;
102
+ }
103
+ CSS
104
+
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"
111
+ note.save
112
+
113
+ note_id = note.id
114
+ end
115
+
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
68
121
  ```
69
122
 
70
- 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.
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).
124
+
125
+ ## Documentation
126
+
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.
128
+
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:
130
+
131
+ ```
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
+
140
+ ```
71
141
 
72
- The RSpec examples are intended to provide executable documentation, and reading them may be helpful to understand the API (e.g. [anki_package_spec.rb](https://github.com/KyleRego/anki_record/blob/main/spec/anki_record/anki_package_spec.rb)).
142
+ The RSpec test suite files in `spec` are organized similarly to the the source code in `lib`.
73
143
 
74
- ## Development
144
+ <!-- ## Development
75
145
 
76
146
  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.
77
147
 
78
148
  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).
79
149
 
80
- ### Development road map:
81
- - Work on creating and updating notes and cards to the collection.anki21 database
82
- - Validation logic of what makes the note valid based on the note type's card templates and fields
83
- - Work on adding media support
84
- - Checksum for notes needs to be updated
85
- - Work on updating and saving decks
86
- - Work on updating and saving deck options groups
87
- - Work on updating and saving note types including the note fields and card templates
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.
88
172
 
89
173
  ### Release checklist
174
+ - Remove `require "pry"`
90
175
  - Update changelog
91
176
  - Update usage examples
92
177
  - Update and regenerate documentation
93
178
  - Bump version
94
- - Release gem
179
+ - Release gem -->
95
180
 
96
181
  <!-- ## Contributing
97
182
 
@@ -103,4 +188,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
103
188
 
104
189
  ## Code of Conduct
105
190
 
106
- Everyone interacting in the AnkiRecord project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/KyleRego/anki_record/blob/main/CODE_OF_CONDUCT.md).
191
+ Everyone interacting in the Anki Record project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/KyleRego/anki_record/blob/main/CODE_OF_CONDUCT.md).
data/anki_record.gemspec CHANGED
@@ -10,13 +10,12 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = "Automate Anki flashcard editing with the Ruby programming language."
12
12
  spec.description = <<-DESC
13
- This Ruby library, which is currently in development, will provide an interface to inspect, update, and create Anki SQLite3 databases (*.apkg files).
13
+ A Ruby library which provides a programmatic interface to Anki flashcard decks (.apkg files/zipped Anki SQLite databases).
14
14
  DESC
15
15
  spec.homepage = "https://github.com/KyleRego/anki_record"
16
16
  spec.license = "MIT"
17
17
  spec.required_ruby_version = ">= 3.2.1"
18
18
 
19
- # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
20
19
  spec.metadata["rubygems_mfa_required"] = "true"
21
20
  spec.metadata["homepage_uri"] = spec.homepage
22
21
  spec.metadata["source_code_uri"] = "https://github.com/KyleRego/anki_record"
@@ -35,7 +34,4 @@ Gem::Specification.new do |spec|
35
34
 
36
35
  spec.add_dependency "rubyzip", ">= 2.3"
37
36
  spec.add_dependency "sqlite3", "~> 1.3"
38
-
39
- # For more information and examples about making a new gem, check out our
40
- # guide at: https://bundler.io/guides/creating_gem.html
41
37
  end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ require_relative "../card/card"
6
+ require_relative "../collection/collection"
7
+ require_relative "../note/note"
8
+ require_relative "../database_setup_constants"
9
+
10
+ # rubocop:disable Metrics/ClassLength
11
+ module AnkiRecord
12
+ ##
13
+ # AnkiPackage represents an Anki package deck file.
14
+ class AnkiPackage
15
+ include AnkiRecord::Helpers::DataQueryHelper
16
+
17
+ ##
18
+ # The package's collection object.
19
+ attr_reader :collection
20
+
21
+ ##
22
+ # Instantiates a new Anki package object.
23
+ #
24
+ # See the README for usage details.
25
+ def initialize(name:, target_directory: Dir.pwd, data: nil, open_path: nil, &closure)
26
+ check_name_argument_is_valid(name:)
27
+ @name = name.end_with?(".apkg") ? name[0, name.length - 5] : name
28
+ @target_directory = target_directory
29
+ @open_path = open_path
30
+ check_directory_argument_is_valid
31
+ setup_other_package_instance_variables
32
+ insert_existing_data(data: data) if data
33
+
34
+ execute_closure_and_zip(collection, &closure) if block_given?
35
+ end
36
+
37
+ # Returns an SQLite3::Statement object representing the given SQL and coupled to the collection.anki21 database.
38
+ #
39
+ # The Statement is executed using Statement#execute (see sqlite3 gem).
40
+ def prepare(sql)
41
+ @anki21_database.prepare sql
42
+ end
43
+
44
+ private
45
+
46
+ def execute_closure_and_zip(collection, &closure)
47
+ closure.call(collection)
48
+ rescue StandardError => e
49
+ destroy_temporary_directory
50
+ puts_error_and_standard_message(error: e)
51
+ else
52
+ zip
53
+ end
54
+
55
+ def setup_other_package_instance_variables
56
+ @tmpdir = Dir.mktmpdir
57
+ @tmp_files = []
58
+ @anki21_database = setup_anki21_database_object
59
+ @anki2_database = setup_anki2_database_object
60
+ @media_file = setup_media
61
+ @collection = Collection.new(anki_package: self)
62
+ end
63
+
64
+ def check_name_argument_is_valid(name:)
65
+ return if name.instance_of?(String) && !name.empty? && !name.include?(" ")
66
+
67
+ raise ArgumentError, "The package name must be a string without spaces."
68
+ end
69
+
70
+ def check_directory_argument_is_valid
71
+ raise ArgumentError, "No directory was found at the given path." unless File.directory?(@target_directory)
72
+ end
73
+
74
+ def setup_anki21_database_object
75
+ anki21_file_name = "collection.anki21"
76
+ db = SQLite3::Database.new "#{@tmpdir}/#{anki21_file_name}", options: {}
77
+ @tmp_files << anki21_file_name
78
+ db.execute_batch ANKI_SCHEMA_DEFINITION
79
+ db.execute INSERT_COLLECTION_ANKI_21_COL_RECORD
80
+ db.results_as_hash = true
81
+ db
82
+ end
83
+
84
+ def setup_anki2_database_object
85
+ anki2_file_name = "collection.anki2"
86
+ db = SQLite3::Database.new "#{@tmpdir}/#{anki2_file_name}", options: {}
87
+ @tmp_files << anki2_file_name
88
+ db.execute_batch ANKI_SCHEMA_DEFINITION
89
+ db.execute INSERT_COLLECTION_ANKI_2_COL_RECORD
90
+ db.close
91
+ db
92
+ end
93
+
94
+ def setup_media
95
+ media_file_path = FileUtils.touch("#{@tmpdir}/media")[0]
96
+ media_file = File.open(media_file_path, mode: "w")
97
+ media_file.write("{}")
98
+ media_file.close
99
+ @tmp_files << "media"
100
+ media_file
101
+ end
102
+
103
+ def insert_existing_data(data:)
104
+ @collection.copy_over_existing(col_record: data[:col_record])
105
+ copy_over_notes_and_cards(note_ids: data[:note_ids])
106
+ end
107
+
108
+ def copy_over_notes_and_cards(note_ids:)
109
+ temporarily_unzip_source_apkg do |source_collection_anki21|
110
+ note_ids.each do |note_id|
111
+ note_cards_data = note_cards_data_for_note_id(sql_able: source_collection_anki21, id: note_id)
112
+ AnkiRecord::Note.new(collection: @collection, data: note_cards_data).save
113
+ end
114
+ end
115
+ end
116
+
117
+ def standard_error_thrown_in_block_message
118
+ "Any temporary files created have been deleted.\nNo new *.apkg zip file was saved."
119
+ end
120
+
121
+ def puts_error_and_standard_message(error:)
122
+ puts error.backtrace
123
+ puts "#{error}\n#{standard_error_thrown_in_block_message}"
124
+ end
125
+
126
+ public
127
+
128
+ ##
129
+ # Instantiates a new Anki package object seeded with data from the opened Anki package.
130
+ #
131
+ # See the README for details.
132
+ def self.open(path:, target_directory: nil, &closure)
133
+ pathname = Pathname.new(path)
134
+ raise "*No .apkg file was found at the given path." unless pathname.file? && pathname.extname == ".apkg"
135
+
136
+ new_apkg_name = "#{File.basename(pathname.to_s, ".apkg")}-#{seconds_since_epoch}"
137
+ data = col_record_and_note_ids_to_copy_over(pathname: pathname)
138
+
139
+ if target_directory
140
+ new(name: new_apkg_name, data: data, open_path: pathname,
141
+ target_directory: target_directory, &closure)
142
+ else
143
+ new(name: new_apkg_name, data: data, open_path: pathname, &closure)
144
+ end
145
+ end
146
+
147
+ def was_instantiated_from_existing_apkg? # :nodoc:
148
+ !@open_path.nil?
149
+ end
150
+
151
+ # rubocop:disable Metrics/MethodLength
152
+ # :nodoc:
153
+ def temporarily_unzip_source_apkg
154
+ raise ArgumentError unless @open_path && block_given?
155
+
156
+ Zip::File.open(@open_path) do |zip_file|
157
+ zip_file.each do |entry|
158
+ next unless entry.name == "collection.anki21"
159
+
160
+ entry.extract
161
+ source_collection_anki21 = SQLite3::Database.open "collection.anki21"
162
+ source_collection_anki21.results_as_hash = true
163
+
164
+ yield source_collection_anki21
165
+ end
166
+ end
167
+ File.delete("collection.anki21")
168
+ end
169
+ # rubocop:enable Metrics/MethodLength
170
+
171
+ class << self
172
+ include Helpers::TimeHelper
173
+
174
+ # rubocop:disable Metrics/MethodLength
175
+ # rubocop:disable Metrics/AbcSize
176
+ def col_record_and_note_ids_to_copy_over(pathname:) # :nodoc:
177
+ data = {}
178
+ Zip::File.open(pathname) do |zip_file|
179
+ zip_file.each do |entry|
180
+ next unless entry.name == "collection.anki21"
181
+
182
+ entry.extract
183
+ source_collection_anki21 = SQLite3::Database.open "collection.anki21"
184
+ source_collection_anki21.results_as_hash = true
185
+ col_record = source_collection_anki21.prepare("select * from col").execute.first
186
+ note_ids = source_collection_anki21.prepare("select id from notes").execute.map { |note| note["id"] }
187
+ data = { col_record: col_record, note_ids: note_ids }
188
+ end
189
+ end
190
+ File.delete("collection.anki21")
191
+ data
192
+ end
193
+ # rubocop:enable Metrics/AbcSize
194
+ # rubocop:enable Metrics/MethodLength
195
+ end
196
+
197
+ ##
198
+ # Zips the temporary files (collection.anki21, collection.anki2, and media) into a new *.apkg package file.
199
+ #
200
+ # The temporary files, and the temporary directory they were in, are deleted after zipping.
201
+ def zip
202
+ create_zip_file && destroy_temporary_directory
203
+ end
204
+
205
+ private
206
+
207
+ def create_zip_file
208
+ Zip::File.open(target_zip_file, create: true) do |zip_file|
209
+ @tmp_files.each do |file_name|
210
+ zip_file.add(file_name, File.join(@tmpdir, file_name))
211
+ end
212
+ end
213
+ true
214
+ end
215
+
216
+ def target_zip_file
217
+ "#{@target_directory}/#{@name}.apkg"
218
+ end
219
+
220
+ def destroy_temporary_directory
221
+ FileUtils.rm_rf(@tmpdir)
222
+ end
223
+
224
+ public
225
+
226
+ # :nodoc:
227
+ def open?
228
+ !closed?
229
+ end
230
+
231
+ # :nodoc:
232
+ def closed?
233
+ @anki21_database.closed?
234
+ end
235
+ end
236
+ end
237
+ # rubocop:enable Metrics/ClassLength