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.
- checksums.yaml +4 -4
- data/.rubocop.yml +13 -2
- data/CHANGELOG.md +37 -9
- data/Gemfile +3 -1
- data/Gemfile.lock +10 -2
- data/README.md +120 -35
- data/anki_record.gemspec +1 -5
- data/lib/anki_record/anki_package/anki_package.rb +237 -0
- data/lib/anki_record/card/card.rb +108 -0
- data/lib/anki_record/card/card_attributes.rb +39 -0
- data/lib/anki_record/card_template/card_template.rb +64 -0
- data/lib/anki_record/card_template/card_template_attributes.rb +69 -0
- data/lib/anki_record/collection/collection.rb +182 -0
- data/lib/anki_record/collection/collection_attributes.rb +35 -0
- data/lib/anki_record/database_setup_constants.rb +88 -0
- data/lib/anki_record/deck/deck.rb +99 -0
- data/lib/anki_record/deck/deck_attributes.rb +30 -0
- data/lib/anki_record/deck/deck_defaults.rb +19 -0
- data/lib/anki_record/{deck_options_group.rb → deck_options_group/deck_options_group.rb} +12 -31
- data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +23 -0
- data/lib/anki_record/helpers/checksum_helper.rb +10 -11
- data/lib/anki_record/helpers/data_query_helper.rb +15 -0
- data/lib/anki_record/helpers/shared_constants_helper.rb +6 -6
- data/lib/anki_record/helpers/time_helper.rb +18 -13
- data/lib/anki_record/note/note.rb +178 -0
- data/lib/anki_record/note/note_attributes.rb +56 -0
- data/lib/anki_record/note_field/note_field.rb +62 -0
- data/lib/anki_record/note_field/note_field_attributes.rb +39 -0
- data/lib/anki_record/note_field/note_field_defaults.rb +19 -0
- data/lib/anki_record/note_type/note_type.rb +161 -0
- data/lib/anki_record/note_type/note_type_attributes.rb +80 -0
- data/lib/anki_record/note_type/note_type_defaults.rb +38 -0
- data/lib/anki_record/version.rb +1 -1
- data/lib/anki_record.rb +1 -16
- metadata +26 -16
- data/lib/anki_record/anki_package.rb +0 -194
- data/lib/anki_record/card.rb +0 -75
- data/lib/anki_record/card_template.rb +0 -105
- data/lib/anki_record/collection.rb +0 -105
- data/lib/anki_record/db/anki_schema_definition.rb +0 -77
- data/lib/anki_record/db/clean_collection21_record.rb +0 -10
- data/lib/anki_record/db/clean_collection2_record.rb +0 -10
- data/lib/anki_record/deck.rb +0 -101
- data/lib/anki_record/note.rb +0 -135
- data/lib/anki_record/note_field.rb +0 -84
- data/lib/anki_record/note_type.rb +0 -233
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b89baf921abc0de4f35babe65cb40cf5fa5647d4d1fe9b3b81ecb80f8a47a15b
|
4
|
+
data.tar.gz: f766e3344683a73d46450c8bf3cf6ae8226f4cbefee4d5126a194c6dcd831cab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
-
|
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`
|
15
|
-
- Decks and note types can be accessed
|
16
|
-
-
|
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
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.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
|
-
#
|
1
|
+
# Anki Record
|
2
2
|
|
3
|
-
|
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
|
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 |
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
53
|
+
AnkiRecord::AnkiPackage.new(name: "test") do |collection|
|
54
|
+
deck = collection.find_deck_by name: "Default"
|
49
55
|
|
50
|
-
|
56
|
+
note_type = collection.find_note_type_by name: "Basic"
|
51
57
|
|
52
|
-
|
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
|
-
|
55
|
-
note.front = "Hello"
|
56
|
-
note.back = "World"
|
57
|
-
note.save
|
63
|
+
note_type2 = collection.find_note_type_by name: "Cloze"
|
58
64
|
|
59
|
-
|
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
|
-
|
62
|
-
note2.text = "Cloze {{c1::Hello}}"
|
63
|
-
note2.back_extra = "World"
|
64
|
-
note2.save
|
71
|
+
```
|
65
72
|
|
66
|
-
apkg.
|
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
|
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
|
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
|
-
-
|
82
|
-
-
|
83
|
-
-
|
84
|
-
|
85
|
-
-
|
86
|
-
- Work on updating and saving
|
87
|
-
-
|
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
|
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
|
-
|
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
|