anki_record 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +31 -9
- data/Gemfile +1 -1
- data/Gemfile.lock +4 -2
- data/README.md +114 -31
- data/anki_record.gemspec +1 -5
- data/lib/anki_record/anki_package/anki_package.rb +245 -0
- data/lib/anki_record/anki_package/database_setup_constants.rb +91 -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 +180 -0
- data/lib/anki_record/collection/collection_attributes.rb +35 -0
- data/lib/anki_record/deck/deck.rb +101 -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} +10 -29
- data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +23 -0
- data/lib/anki_record/helpers/checksum_helper.rb +2 -5
- data/lib/anki_record/helpers/data_query_helper.rb +13 -0
- data/lib/anki_record/helpers/shared_constants_helper.rb +1 -3
- data/lib/anki_record/helpers/time_helper.rb +7 -5
- data/lib/anki_record/note/note.rb +181 -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: 4da2246d01a22eec129d18c242062ac1eb39b9b794129bf4666fc7eb2615087b
|
4
|
+
data.tar.gz: 5de4bdb4e80e078d8b5711466b2eba97b68563bfbb44f24805fc63b909a31d08
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac4bb975b12ca50c5d9bba91caeec3accd75a952f04ae88a17ebef9b6b15cc7af335316ee024f1f50322ec77d321aad9106c6fac3a99eb657f48a1f513353c35
|
7
|
+
data.tar.gz: 62b7cbd1d173f23eb2c1a3b816a092f7cbd7200945ce8bea5aafa2d73dc59714f571f2869aadb62016749dbc2446d0f43cb16fc5d670f89ea8bc3507e2c2a2f0
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,16 +1,38 @@
|
|
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.
|
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.0)
|
5
5
|
rubyzip (>= 2.3)
|
6
6
|
sqlite3 (~> 1.3)
|
7
7
|
|
@@ -55,6 +55,8 @@ GEM
|
|
55
55
|
parser (>= 3.1.1.0)
|
56
56
|
ruby-progressbar (1.11.0)
|
57
57
|
rubyzip (2.3.2)
|
58
|
+
sdoc (2.6.1)
|
59
|
+
rdoc (>= 5.0)
|
58
60
|
simplecov (0.22.0)
|
59
61
|
docile (~> 1.1)
|
60
62
|
simplecov-html (~> 0.11)
|
@@ -72,9 +74,9 @@ DEPENDENCIES
|
|
72
74
|
anki_record!
|
73
75
|
pry
|
74
76
|
rake (~> 13.0)
|
75
|
-
rdoc
|
76
77
|
rspec (~> 3.0)
|
77
78
|
rubocop (~> 1.21)
|
79
|
+
sdoc (~> 2.6)
|
78
80
|
simplecov
|
79
81
|
|
80
82
|
RUBY VERSION
|
data/README.md
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
#
|
1
|
+
# Anki Record
|
2
2
|
|
3
|
-
|
3
|
+
Anki Record is a Ruby library which provides a programmatic interface to Anki flashcard decks/deck packages (`.apkg` files or Anki SQLite databases).
|
4
4
|
|
5
|
-
|
5
|
+
**Development is ongoing and currently released versions may have some bugs. The library does not support media yet.**
|
6
|
+
|
7
|
+
**To import the deck packages into Anki, click the File menu and then "Import" from inside Anki. Do not double click the `.apkg` file to open Anki and import it at the same time.**
|
6
8
|
|
7
9
|
## Installation
|
8
10
|
|
@@ -16,7 +18,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
16
18
|
|
17
19
|
## Usage
|
18
20
|
|
19
|
-
The Anki package object is instantiated with `AnkiRecord::AnkiPackage.new
|
21
|
+
The Anki package object is instantiated with `AnkiRecord::AnkiPackage.new`. If this is passed a block, it will execute the block, and afterwards zip an `*.apkg` file where `*` is the name argument (this argument is not allowed to contain spaces):
|
20
22
|
|
21
23
|
```ruby
|
22
24
|
require "anki_record"
|
@@ -27,9 +29,12 @@ AnkiRecord::AnkiPackage.new(name: "test") do |apkg|
|
|
27
29
|
end
|
28
30
|
puts "Countdown complete. Write any Ruby you want in here!"
|
29
31
|
end
|
32
|
+
# test.apkg now exists in the current working directory.
|
30
33
|
```
|
31
34
|
|
32
|
-
|
35
|
+
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.
|
36
|
+
|
37
|
+
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
38
|
|
34
39
|
Alternatively, if `AnkiRecord::Package::new` is not passed a block, the `zip` method must be explicitly called on the Anki package object:
|
35
40
|
|
@@ -37,39 +42,102 @@ Alternatively, if `AnkiRecord::Package::new` is not passed a block, the `zip` me
|
|
37
42
|
require "anki_record"
|
38
43
|
|
39
44
|
apkg = AnkiRecord::AnkiPackage.new(name: "test")
|
40
|
-
apkg.zip
|
45
|
+
apkg.zip # This zips the temporary files into test.apkg, and then deletes them.
|
41
46
|
```
|
42
47
|
|
43
|
-
|
48
|
+
The second, optional argument to `AnkiRecord::Package.new` is `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.
|
49
|
+
|
50
|
+
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
51
|
|
45
52
|
```ruby
|
46
53
|
require "anki_record"
|
47
54
|
|
48
|
-
apkg = AnkiRecord::AnkiPackage.new
|
55
|
+
apkg = AnkiRecord::AnkiPackage.new(name: "test") do |apkg|
|
56
|
+
deck = apkg.collection.find_deck_by name: "Default"
|
57
|
+
|
58
|
+
note_type = apkg.collection.find_note_type_by name: "Basic"
|
59
|
+
|
60
|
+
note = AnkiRecord::Note.new note_type: note_type, deck: deck
|
61
|
+
note.front = "Hello"
|
62
|
+
note.back = "World"
|
63
|
+
note.save
|
49
64
|
|
50
|
-
|
65
|
+
note_type2 = apkg.collection.find_note_type_by name: "Cloze"
|
51
66
|
|
52
|
-
|
67
|
+
note2 = AnkiRecord::Note.new note_type: note_type2, deck: deck
|
68
|
+
note2.text = "Cloze {{c1::Hello}}"
|
69
|
+
note2.back_extra = "World"
|
70
|
+
note2.save
|
71
|
+
end
|
53
72
|
|
54
|
-
|
55
|
-
note.front = "Hello"
|
56
|
-
note.back = "World"
|
57
|
-
note.save
|
73
|
+
```
|
58
74
|
|
59
|
-
|
75
|
+
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.
|
60
76
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
77
|
+
The next example shows some other features of the library:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
require "anki_record"
|
65
81
|
|
66
|
-
|
82
|
+
note_id = nil
|
83
|
+
|
84
|
+
AnkiRecord::AnkiPackage.new(name: "test_1") do |collection|
|
85
|
+
crazy_deck = AnkiRecord::Deck.new collection: collection, name: "test_1_deck"
|
86
|
+
|
87
|
+
crazy_note_type = AnkiRecord::NoteType.new collection: collection, name: "test 1 note type"
|
88
|
+
AnkiRecord::NoteField.new note_type: crazy_note_type, name: "crazy front"
|
89
|
+
AnkiRecord::NoteField.new note_type: crazy_note_type, name: "crazy back"
|
90
|
+
crazy_card_template = AnkiRecord::CardTemplate.new note_type: crazy_note_type, name: "test 1 card 1"
|
91
|
+
crazy_card_template.question_format = "{{crazy front}}"
|
92
|
+
crazy_card_template.answer_format = "{{crazy back}}"
|
93
|
+
second_crazy_card_template = AnkiRecord::CardTemplate.new note_type: crazy_note_type, name: "test 1 card 2"
|
94
|
+
second_crazy_card_template.question_format = "{{crazy back}}"
|
95
|
+
second_crazy_card_template.answer_format = "{{crazy front}}"
|
96
|
+
|
97
|
+
css = <<~CSS
|
98
|
+
.card {
|
99
|
+
font-size: 16px;
|
100
|
+
font-style: Verdana;
|
101
|
+
background: transparent;
|
102
|
+
text-align: center;
|
103
|
+
}
|
104
|
+
CSS
|
105
|
+
|
106
|
+
crazy_note_type.css = css
|
107
|
+
crazy_note_type.save
|
108
|
+
|
109
|
+
note = AnkiRecord::Note.new note_type: crazy_note_type, deck: crazy_deck
|
110
|
+
note.crazy_front = "Hello from test 1"
|
111
|
+
note.crazy_back = "World"
|
112
|
+
note.save
|
113
|
+
|
114
|
+
note_id = note.id
|
115
|
+
end
|
67
116
|
|
117
|
+
AnkiRecord::AnkiPackage.open(path: "./test_1.apkg") do |collection|
|
118
|
+
note = collection.find_note_by id: note_id
|
119
|
+
note.crazy_back = "Ruby"
|
120
|
+
note.save
|
121
|
+
end
|
68
122
|
```
|
69
123
|
|
70
|
-
This
|
124
|
+
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).
|
125
|
+
|
126
|
+
## Documentation
|
127
|
+
|
128
|
+
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.
|
129
|
+
|
130
|
+
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. The following is an example of output from one example in `spec/anki_record/note_spec.rb`:
|
131
|
+
|
132
|
+
```
|
133
|
+
AnkiRecord::Note
|
134
|
+
#save
|
135
|
+
for a note that does not exist in the collection.anki21 database (custom note type, 2 card templates)
|
136
|
+
should save two card records to the collection.anki21 database
|
137
|
+
with nid values equal to the id of the cards' note object's id
|
138
|
+
```
|
71
139
|
|
72
|
-
The RSpec
|
140
|
+
The RSpec test suite files in `spec` have a mapping with the source code in `lib`.
|
73
141
|
|
74
142
|
## Development
|
75
143
|
|
@@ -77,16 +145,31 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
77
145
|
|
78
146
|
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
147
|
|
80
|
-
### Development road map:
|
81
|
-
-
|
82
|
-
-
|
83
|
-
-
|
84
|
-
|
85
|
-
-
|
86
|
-
- Work on updating and saving
|
87
|
-
-
|
148
|
+
### Development road map:
|
149
|
+
- Better messages when `ArgumentError` raised
|
150
|
+
- Add #inspect methods
|
151
|
+
- Refactor tests to improve speed
|
152
|
+
- Copying the contents of an existing package into the new package when it is opened
|
153
|
+
- Add more unit tests
|
154
|
+
- Work on creating, updating, and saving notes and cards to the collection.anki21 database
|
155
|
+
- Updating notes when they already exist in the database
|
156
|
+
- Add more unit tests
|
157
|
+
- Validation logic of what makes the note valid based on the note type's card templates and fields
|
158
|
+
- Work on adding media support
|
159
|
+
- The checksum calculation for notes will need to be updated to account for HTML in the content
|
160
|
+
- Saving note types, decks, and deck options groups to the collection.anki21 database
|
161
|
+
- Deck options groups cannot be saved yet.
|
162
|
+
- Add being able to handle subdecks
|
163
|
+
- Updating them when they already exist
|
164
|
+
- Setters for any relevant attributes with validation
|
165
|
+
- Refactoring
|
166
|
+
- Use more specific RSpec matchers than `eq` everywhere
|
167
|
+
- Investigate if note guid is determined in Anki in a non-random way
|
168
|
+
- Investigate if the database ever needs to be explicitly opened or closed
|
169
|
+
- Note type allowed fields: investigate if there are other special field names that should be allowed.
|
88
170
|
|
89
171
|
### Release checklist
|
172
|
+
- Remove `require "pry"`
|
90
173
|
- Update changelog
|
91
174
|
- Update usage examples
|
92
175
|
- Update and regenerate documentation
|
@@ -103,4 +186,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
103
186
|
|
104
187
|
## Code of Conduct
|
105
188
|
|
106
|
-
Everyone interacting in the
|
189
|
+
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,245 @@
|
|
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.
|
14
|
+
#
|
15
|
+
# Here, Anki package refers to the zip file that Anki can export and import.
|
16
|
+
class AnkiPackage
|
17
|
+
include AnkiRecord::DataQueryHelper
|
18
|
+
|
19
|
+
##
|
20
|
+
# The package's collection object.
|
21
|
+
attr_reader :collection
|
22
|
+
|
23
|
+
##
|
24
|
+
# Instantiates a new Anki package object.
|
25
|
+
#
|
26
|
+
# See the README for usage details.
|
27
|
+
def initialize(name:, target_directory: Dir.pwd, data: nil, open_path: nil, &closure)
|
28
|
+
check_name_argument_is_valid(name:)
|
29
|
+
@name = name.end_with?(".apkg") ? name[0, name.length - 5] : name
|
30
|
+
@target_directory = target_directory
|
31
|
+
@open_path = open_path
|
32
|
+
check_directory_argument_is_valid
|
33
|
+
setup_other_package_instance_variables
|
34
|
+
insert_existing_data(data: data) if data
|
35
|
+
|
36
|
+
execute_closure_and_zip(collection, &closure) if block_given?
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns an SQLite3::Statement object representing the given SQL and coupled to the collection.anki21 database.
|
40
|
+
#
|
41
|
+
# The Statement is executed using Statement#execute (see sqlite3 gem).
|
42
|
+
def prepare(sql)
|
43
|
+
@anki21_database.prepare sql
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def execute_closure_and_zip(object_to_yield, &closure)
|
49
|
+
closure.call(object_to_yield)
|
50
|
+
rescue StandardError => e
|
51
|
+
destroy_temporary_directory
|
52
|
+
puts_error_and_standard_message(error: e)
|
53
|
+
else
|
54
|
+
zip
|
55
|
+
end
|
56
|
+
|
57
|
+
def setup_other_package_instance_variables
|
58
|
+
@tmpdir = Dir.mktmpdir
|
59
|
+
@tmp_files = []
|
60
|
+
@anki21_database = setup_anki21_database_object
|
61
|
+
@anki2_database = setup_anki2_database_object
|
62
|
+
@media_file = setup_media
|
63
|
+
@collection = Collection.new(anki_package: self)
|
64
|
+
end
|
65
|
+
|
66
|
+
def check_name_argument_is_valid(name:)
|
67
|
+
return if name.instance_of?(String) && !name.empty? && !name.include?(" ")
|
68
|
+
|
69
|
+
raise ArgumentError, "The package name must be a string without spaces."
|
70
|
+
end
|
71
|
+
|
72
|
+
def check_directory_argument_is_valid
|
73
|
+
raise ArgumentError, "No directory was found at the given path." unless File.directory?(@target_directory)
|
74
|
+
end
|
75
|
+
|
76
|
+
def setup_anki21_database_object
|
77
|
+
anki21_file_name = "collection.anki21"
|
78
|
+
db = SQLite3::Database.new "#{@tmpdir}/#{anki21_file_name}", options: {}
|
79
|
+
@tmp_files << anki21_file_name
|
80
|
+
db.execute_batch ANKI_SCHEMA_DEFINITION
|
81
|
+
db.execute INSERT_COLLECTION_ANKI_21_COL_RECORD
|
82
|
+
db.results_as_hash = true
|
83
|
+
db
|
84
|
+
end
|
85
|
+
|
86
|
+
def setup_anki2_database_object
|
87
|
+
anki2_file_name = "collection.anki2"
|
88
|
+
db = SQLite3::Database.new "#{@tmpdir}/#{anki2_file_name}", options: {}
|
89
|
+
@tmp_files << anki2_file_name
|
90
|
+
db.execute_batch ANKI_SCHEMA_DEFINITION
|
91
|
+
db.execute INSERT_COLLECTION_ANKI_2_COL_RECORD
|
92
|
+
db.close
|
93
|
+
db
|
94
|
+
end
|
95
|
+
|
96
|
+
def setup_media
|
97
|
+
media_file_path = FileUtils.touch("#{@tmpdir}/media")[0]
|
98
|
+
media_file = File.open(media_file_path, mode: "w")
|
99
|
+
media_file.write("{}")
|
100
|
+
media_file.close
|
101
|
+
@tmp_files << "media"
|
102
|
+
media_file
|
103
|
+
end
|
104
|
+
|
105
|
+
def insert_existing_data(data:)
|
106
|
+
@collection.copy_over_existing(col_record: data[:col_record])
|
107
|
+
copy_over_notes_and_cards(note_ids: data[:note_ids])
|
108
|
+
end
|
109
|
+
|
110
|
+
def copy_over_notes_and_cards(note_ids:)
|
111
|
+
temporarily_unzip_source_apkg do |source_collection_anki21|
|
112
|
+
note_ids.each do |note_id|
|
113
|
+
note_cards_data = note_cards_data_for_note_id(sql_able: source_collection_anki21, id: note_id)
|
114
|
+
AnkiRecord::Note.new(collection: @collection, data: note_cards_data).save
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def standard_error_thrown_in_block_message
|
120
|
+
"Any temporary files created have been deleted.\nNo new *.apkg zip file was saved."
|
121
|
+
end
|
122
|
+
|
123
|
+
def puts_error_and_standard_message(error:)
|
124
|
+
puts error.backtrace
|
125
|
+
puts "#{error}\n#{standard_error_thrown_in_block_message}"
|
126
|
+
end
|
127
|
+
|
128
|
+
public
|
129
|
+
|
130
|
+
##
|
131
|
+
# Instantiates a new Anki package object seeded with data from the opened Anki package.
|
132
|
+
#
|
133
|
+
# See the README for details.
|
134
|
+
def self.open(path:, target_directory: nil, &closure)
|
135
|
+
pathname = Pathname.new(path)
|
136
|
+
raise "*No .apkg file was found at the given path." unless pathname.file? && pathname.extname == ".apkg"
|
137
|
+
|
138
|
+
new_apkg_name = "#{File.basename(pathname.to_s, ".apkg")}-#{seconds_since_epoch}"
|
139
|
+
data = col_record_and_note_ids_to_copy_over(pathname: pathname)
|
140
|
+
|
141
|
+
if target_directory
|
142
|
+
new(name: new_apkg_name, data: data, open_path: pathname,
|
143
|
+
target_directory: target_directory, &closure)
|
144
|
+
else
|
145
|
+
new(name: new_apkg_name, data: data, open_path: pathname, &closure)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def was_instantiated_from_existing_apkg? # :nodoc:
|
150
|
+
!@open_path.nil?
|
151
|
+
end
|
152
|
+
|
153
|
+
# rubocop:disable Metrics/MethodLength
|
154
|
+
|
155
|
+
##
|
156
|
+
# Unzips the *.apkg file that was opened and yields its collection.anki21 database
|
157
|
+
# as a SQLite3::Database object (see sqlite3 gem) to the block.
|
158
|
+
#
|
159
|
+
# After the block executes, the files created by unzipping are deleted.
|
160
|
+
#
|
161
|
+
# Throws an error if the Anki package was not instantiated using ::open.
|
162
|
+
#
|
163
|
+
def temporarily_unzip_source_apkg
|
164
|
+
raise ArgumentError unless @open_path && block_given?
|
165
|
+
|
166
|
+
Zip::File.open(@open_path) do |zip_file|
|
167
|
+
zip_file.each do |entry|
|
168
|
+
next unless entry.name == "collection.anki21"
|
169
|
+
|
170
|
+
entry.extract
|
171
|
+
source_collection_anki21 = SQLite3::Database.open "collection.anki21"
|
172
|
+
source_collection_anki21.results_as_hash = true
|
173
|
+
|
174
|
+
yield source_collection_anki21
|
175
|
+
end
|
176
|
+
end
|
177
|
+
File.delete("collection.anki21")
|
178
|
+
end
|
179
|
+
# rubocop:enable Metrics/MethodLength
|
180
|
+
|
181
|
+
class << self
|
182
|
+
include TimeHelper
|
183
|
+
|
184
|
+
# rubocop:disable Metrics/MethodLength
|
185
|
+
# rubocop:disable Metrics/AbcSize
|
186
|
+
def col_record_and_note_ids_to_copy_over(pathname:) # :nodoc:
|
187
|
+
data = {}
|
188
|
+
Zip::File.open(pathname) do |zip_file|
|
189
|
+
zip_file.each do |entry|
|
190
|
+
next unless entry.name == "collection.anki21"
|
191
|
+
|
192
|
+
entry.extract
|
193
|
+
source_collection_anki21 = SQLite3::Database.open "collection.anki21"
|
194
|
+
source_collection_anki21.results_as_hash = true
|
195
|
+
col_record = source_collection_anki21.prepare("select * from col").execute.first
|
196
|
+
note_ids = source_collection_anki21.prepare("select id from notes").execute.map { |note| note["id"] }
|
197
|
+
data = { col_record: col_record, note_ids: note_ids }
|
198
|
+
end
|
199
|
+
end
|
200
|
+
File.delete("collection.anki21")
|
201
|
+
data
|
202
|
+
end
|
203
|
+
# rubocop:enable Metrics/AbcSize
|
204
|
+
# rubocop:enable Metrics/MethodLength
|
205
|
+
end
|
206
|
+
|
207
|
+
##
|
208
|
+
# Zips the temporary files (collection.anki21, collection.anki2, and media) into a new *.apkg package file.
|
209
|
+
#
|
210
|
+
# The temporary files, and the temporary directory they were in, are deleted after zipping.
|
211
|
+
def zip
|
212
|
+
create_zip_file && destroy_temporary_directory
|
213
|
+
end
|
214
|
+
|
215
|
+
private
|
216
|
+
|
217
|
+
def create_zip_file
|
218
|
+
Zip::File.open(target_zip_file, create: true) do |zip_file|
|
219
|
+
@tmp_files.each do |file_name|
|
220
|
+
zip_file.add(file_name, File.join(@tmpdir, file_name))
|
221
|
+
end
|
222
|
+
end
|
223
|
+
true
|
224
|
+
end
|
225
|
+
|
226
|
+
def target_zip_file
|
227
|
+
"#{@target_directory}/#{@name}.apkg"
|
228
|
+
end
|
229
|
+
|
230
|
+
def destroy_temporary_directory
|
231
|
+
FileUtils.rm_rf(@tmpdir)
|
232
|
+
end
|
233
|
+
|
234
|
+
public
|
235
|
+
|
236
|
+
def open? # :nodoc:
|
237
|
+
!closed?
|
238
|
+
end
|
239
|
+
|
240
|
+
def closed? # :nodoc:
|
241
|
+
@anki21_database.closed?
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
# rubocop:enable Metrics/ClassLength
|