anki_record 0.1.1 → 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 +33 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +5 -3
- data/README.md +147 -11
- data/anki_record.gemspec +2 -6
- 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.rb → card_template/card_template.rb} +20 -47
- 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 -23
- data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +23 -0
- data/lib/anki_record/helpers/checksum_helper.rb +17 -0
- 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 -15
- metadata +32 -20
- data/.rdoc_options +0 -27
- data/lib/anki_record/anki_package.rb +0 -183
- data/lib/anki_record/collection.rb +0 -65
- 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 -88
- data/lib/anki_record/note_field.rb +0 -63
- data/lib/anki_record/note_type.rb +0 -147
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../card_template/card_template"
|
4
|
+
require_relative "../helpers/shared_constants_helper"
|
5
|
+
require_relative "../helpers/time_helper"
|
6
|
+
require_relative "../note_field/note_field"
|
7
|
+
require_relative "note_type_attributes"
|
8
|
+
require_relative "note_type_defaults"
|
9
|
+
|
10
|
+
module AnkiRecord
|
11
|
+
##
|
12
|
+
# NoteType represents an Anki note type (also called a model).
|
13
|
+
#
|
14
|
+
# The attributes are documented in the NoteTypeAttributes module.
|
15
|
+
class NoteType
|
16
|
+
include AnkiRecord::SharedConstantsHelper
|
17
|
+
include AnkiRecord::TimeHelper
|
18
|
+
include AnkiRecord::NoteTypeAttributes
|
19
|
+
include AnkiRecord::NoteTypeDefaults
|
20
|
+
|
21
|
+
NOTE_TYPES_WITHOUT_TAGS_AND_VERS_VALUES = ["Basic", "Basic (and reversed card)",
|
22
|
+
"Basic (optional reversed card)", "Basic (type in the answer)"].freeze
|
23
|
+
|
24
|
+
def initialize(collection:, name: nil, args: nil)
|
25
|
+
raise ArgumentError unless (name && args.nil?) || (args && args["name"])
|
26
|
+
|
27
|
+
@collection = collection
|
28
|
+
|
29
|
+
if args
|
30
|
+
setup_note_type_instance_variables_from_existing(args: args)
|
31
|
+
else
|
32
|
+
setup_instance_variables_for_new_note_type(name: name)
|
33
|
+
end
|
34
|
+
|
35
|
+
@collection.add_note_type self
|
36
|
+
save
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Saves the note type to the collection.anki21 database
|
41
|
+
def save
|
42
|
+
collection_models_hash = collection.models_json
|
43
|
+
collection_models_hash[@id] = to_h
|
44
|
+
sql = "update col set models = ? where id = ?"
|
45
|
+
collection.anki_package.prepare(sql).execute([JSON.generate(collection_models_hash), collection.id])
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_h # :nodoc:
|
49
|
+
self_to_h = { id: @id, name: @name, type: @cloze ? 1 : 0,
|
50
|
+
mod: @last_modified_timestamp, usn: @usn, sortf: @sort_field, did: @deck_id,
|
51
|
+
tmpls: @card_templates.map(&:to_h), flds: @note_fields.map(&:to_h), css: @css,
|
52
|
+
latexPre: @latex_preamble, latexPost: @latex_postamble, latexsvg: @latex_svg,
|
53
|
+
req: @req }
|
54
|
+
self_to_h.merge({ tags: @tags, vers: @vers }) unless NOTE_TYPES_WITHOUT_TAGS_AND_VERS_VALUES.include?(@name)
|
55
|
+
self_to_h
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Returns the note type object's card template with name +name+ or nil if it is not found
|
60
|
+
def find_card_template_by(name:)
|
61
|
+
card_templates.find { |template| template.name == name }
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Returns an array of the note type's fields' names ordered by field ordinal values.
|
66
|
+
def field_names_in_order
|
67
|
+
@note_fields.sort_by(&:ordinal_number).map(&:name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def snake_case_field_names # :nodoc:
|
71
|
+
field_names_in_order.map { |field_name| field_name.downcase.gsub(" ", "_") }
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Returns the name of the note type's field used to sort notes in the browser.
|
76
|
+
def sort_field_name
|
77
|
+
@note_fields.find { |field| field.ordinal_number == @sort_field }&.name
|
78
|
+
end
|
79
|
+
|
80
|
+
def snake_case_sort_field_name # :nodoc:
|
81
|
+
sort_field_name.downcase.gsub(" ", "_")
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Returns allowed field_name values in {{field_name}} in the question format.
|
86
|
+
def allowed_card_template_question_format_field_names
|
87
|
+
allowed = field_names_in_order
|
88
|
+
cloze ? allowed + field_names_in_order.map { |field_name| "cloze:#{field_name}" } : allowed
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Returns allowed field_name values in {{field_name}} in the answer format.
|
93
|
+
def allowed_card_template_answer_format_field_names
|
94
|
+
allowed_card_template_question_format_field_names + ["FrontSide"]
|
95
|
+
end
|
96
|
+
|
97
|
+
def add_note_field(note_field) # :nodoc:
|
98
|
+
raise ArgumentError unless note_field.instance_of?(AnkiRecord::NoteField)
|
99
|
+
|
100
|
+
@note_fields << note_field
|
101
|
+
end
|
102
|
+
|
103
|
+
def add_card_template(card_template) # :nodoc:
|
104
|
+
raise ArgumentError unless card_template.instance_of?(AnkiRecord::CardTemplate)
|
105
|
+
|
106
|
+
@card_templates << card_template
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def setup_note_type_instance_variables_from_existing(args:)
|
112
|
+
setup_collaborator_object_instance_variables_from_existing(args: args)
|
113
|
+
setup_simple_instance_variables_from_existing(args: args)
|
114
|
+
end
|
115
|
+
|
116
|
+
def setup_collaborator_object_instance_variables_from_existing(args:)
|
117
|
+
@note_fields = []
|
118
|
+
args["flds"].each { |fld| NoteField.new(note_type: self, args: fld) }
|
119
|
+
@card_templates = []
|
120
|
+
args["tmpls"].each { |tmpl| CardTemplate.new(note_type: self, args: tmpl) }
|
121
|
+
end
|
122
|
+
|
123
|
+
def setup_simple_instance_variables_from_existing(args:)
|
124
|
+
%w[id name usn css req tags vers].each do |note_type_attribute|
|
125
|
+
instance_variable_set "@#{note_type_attribute}", args[note_type_attribute]
|
126
|
+
end
|
127
|
+
@cloze = args["type"] == 1
|
128
|
+
@last_modified_timestamp = args["mod"]
|
129
|
+
@sort_field = args["sortf"]
|
130
|
+
@deck_id = args["did"]
|
131
|
+
@latex_preamble = args["latexPre"]
|
132
|
+
@latex_postamble = args["latexPost"]
|
133
|
+
@latex_svg = args["latexsvg"]
|
134
|
+
end
|
135
|
+
|
136
|
+
def setup_instance_variables_for_new_note_type(name:)
|
137
|
+
@name = name
|
138
|
+
@cloze = false
|
139
|
+
setup_collaborator_object_instance_variables_for_new_note_type
|
140
|
+
setup_simple_instance_variables_for_new_note_type
|
141
|
+
end
|
142
|
+
|
143
|
+
def setup_collaborator_object_instance_variables_for_new_note_type
|
144
|
+
@note_fields = []
|
145
|
+
@card_templates = []
|
146
|
+
end
|
147
|
+
|
148
|
+
def setup_simple_instance_variables_for_new_note_type
|
149
|
+
@id = milliseconds_since_epoch
|
150
|
+
@last_modified_timestamp = seconds_since_epoch
|
151
|
+
@usn = NEW_OBJECT_USN
|
152
|
+
@sort_field = default_note_type_sort_field
|
153
|
+
@deck_id = @tags = @vers = nil
|
154
|
+
@css = default_css
|
155
|
+
@latex_preamble = default_latex_preamble
|
156
|
+
@latex_postamble = default_latex_postamble
|
157
|
+
@latex_svg = false
|
158
|
+
@req = []
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
##
|
5
|
+
# Module with the NoteType class's attribute readers, writers, and accessors.
|
6
|
+
module NoteTypeAttributes
|
7
|
+
##
|
8
|
+
# The note type's collection object.
|
9
|
+
attr_reader :collection
|
10
|
+
|
11
|
+
##
|
12
|
+
# The note type's id.
|
13
|
+
attr_reader :id
|
14
|
+
|
15
|
+
##
|
16
|
+
# The note type's name.
|
17
|
+
attr_accessor :name
|
18
|
+
|
19
|
+
##
|
20
|
+
# A boolean that indicates if this note type is a cloze-deletion note type.
|
21
|
+
attr_accessor :cloze
|
22
|
+
|
23
|
+
##
|
24
|
+
# The number of seconds since the 1970 epoch at which the note type was last modified.
|
25
|
+
attr_reader :last_modified_timestamp
|
26
|
+
|
27
|
+
##
|
28
|
+
# The note type's update sequence number.
|
29
|
+
attr_reader :usn
|
30
|
+
|
31
|
+
##
|
32
|
+
# The note type's sort field.
|
33
|
+
attr_reader :sort_field
|
34
|
+
|
35
|
+
##
|
36
|
+
# The note type's CSS.
|
37
|
+
attr_accessor :css
|
38
|
+
|
39
|
+
##
|
40
|
+
# The note type's LaTeX preamble.
|
41
|
+
attr_reader :latex_preamble
|
42
|
+
|
43
|
+
##
|
44
|
+
# The note type's LaTeX postamble.
|
45
|
+
attr_reader :latex_postamble
|
46
|
+
|
47
|
+
##
|
48
|
+
# The note type's card template objects, as an array.
|
49
|
+
attr_reader :card_templates
|
50
|
+
|
51
|
+
##
|
52
|
+
# The note type's field objects, as an array.
|
53
|
+
attr_reader :note_fields
|
54
|
+
|
55
|
+
##
|
56
|
+
# The note type's deck's id.
|
57
|
+
attr_reader :deck_id
|
58
|
+
|
59
|
+
##
|
60
|
+
# The note type's deck.
|
61
|
+
def deck
|
62
|
+
return nil unless @deck_id
|
63
|
+
|
64
|
+
@collection.find_deck_by id: @deck_id
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Sets the note type's deck object.
|
69
|
+
def deck=(deck)
|
70
|
+
unless deck.instance_of?(AnkiRecord::Deck)
|
71
|
+
raise ArgumentError,
|
72
|
+
"You can only set this attribute to an instance of AnkiRecord::Deck."
|
73
|
+
end
|
74
|
+
|
75
|
+
@deck_id = deck.id
|
76
|
+
end
|
77
|
+
|
78
|
+
attr_reader :latex_svg, :tags, :req, :vers
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
module NoteTypeDefaults # :nodoc:
|
5
|
+
private
|
6
|
+
|
7
|
+
def default_note_type_sort_field
|
8
|
+
0
|
9
|
+
end
|
10
|
+
|
11
|
+
def default_css
|
12
|
+
<<~CSS
|
13
|
+
.card {
|
14
|
+
color: black;
|
15
|
+
background-color: transparent;
|
16
|
+
text-align: center;
|
17
|
+
}
|
18
|
+
CSS
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_latex_preamble
|
22
|
+
<<~LATEX_PRE
|
23
|
+
\\documentclass[12pt]{article}
|
24
|
+
\\special{papersize=3in,5in}
|
25
|
+
\\usepackage{amssymb,amsmath}
|
26
|
+
\\pagestyle{empty}
|
27
|
+
\\setlength{\\parindent}{0in}
|
28
|
+
\\begin{document}
|
29
|
+
LATEX_PRE
|
30
|
+
end
|
31
|
+
|
32
|
+
def default_latex_postamble
|
33
|
+
<<~LATEX_POST
|
34
|
+
\\end{document}
|
35
|
+
LATEX_POST
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/anki_record/version.rb
CHANGED
data/lib/anki_record.rb
CHANGED
@@ -1,25 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "securerandom"
|
4
3
|
require "sqlite3"
|
5
4
|
require "zip"
|
6
5
|
|
7
|
-
require_relative "anki_record/anki_package"
|
6
|
+
require_relative "anki_record/anki_package/anki_package"
|
8
7
|
require_relative "anki_record/version"
|
9
8
|
|
10
|
-
##
|
11
|
-
# This module is the namespace for all AnkiRecord classes:
|
12
|
-
# - AnkiPackage
|
13
|
-
# - CardTemplate
|
14
|
-
# - Collection
|
15
|
-
# - DeckOptionsGroup
|
16
|
-
# - Deck
|
17
|
-
# - NoteField
|
18
|
-
# - NoteType
|
19
|
-
#
|
20
|
-
# And modules:
|
21
|
-
# - SharedConstantsHelper
|
22
|
-
# - TimeHelper
|
23
9
|
module AnkiRecord
|
24
10
|
class Error < StandardError; end # :nodoc:
|
25
11
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: anki_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kyle Rego
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-03-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubyzip
|
@@ -28,25 +28,24 @@ dependencies:
|
|
28
28
|
name: sqlite3
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '1.3'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
41
|
-
description: "
|
42
|
-
|
40
|
+
version: '1.3'
|
41
|
+
description: " A Ruby library which provides a programmatic interface to Anki flashcard
|
42
|
+
decks (.apkg files/zipped Anki SQLite databases).\n"
|
43
43
|
email:
|
44
44
|
- regoky@outlook.com
|
45
45
|
executables: []
|
46
46
|
extensions: []
|
47
47
|
extra_rdoc_files: []
|
48
48
|
files:
|
49
|
-
- ".rdoc_options"
|
50
49
|
- ".rspec"
|
51
50
|
- ".rubocop.yml"
|
52
51
|
- CHANGELOG.md
|
@@ -58,18 +57,31 @@ files:
|
|
58
57
|
- Rakefile
|
59
58
|
- anki_record.gemspec
|
60
59
|
- lib/anki_record.rb
|
61
|
-
- lib/anki_record/anki_package.rb
|
62
|
-
- lib/anki_record/
|
63
|
-
- lib/anki_record/
|
64
|
-
- lib/anki_record/
|
65
|
-
- lib/anki_record/
|
66
|
-
- lib/anki_record/
|
67
|
-
- lib/anki_record/
|
68
|
-
- lib/anki_record/
|
60
|
+
- lib/anki_record/anki_package/anki_package.rb
|
61
|
+
- lib/anki_record/anki_package/database_setup_constants.rb
|
62
|
+
- lib/anki_record/card/card.rb
|
63
|
+
- lib/anki_record/card/card_attributes.rb
|
64
|
+
- lib/anki_record/card_template/card_template.rb
|
65
|
+
- lib/anki_record/card_template/card_template_attributes.rb
|
66
|
+
- lib/anki_record/collection/collection.rb
|
67
|
+
- lib/anki_record/collection/collection_attributes.rb
|
68
|
+
- lib/anki_record/deck/deck.rb
|
69
|
+
- lib/anki_record/deck/deck_attributes.rb
|
70
|
+
- lib/anki_record/deck/deck_defaults.rb
|
71
|
+
- lib/anki_record/deck_options_group/deck_options_group.rb
|
72
|
+
- lib/anki_record/deck_options_group/deck_options_group_attributes.rb
|
73
|
+
- lib/anki_record/helpers/checksum_helper.rb
|
74
|
+
- lib/anki_record/helpers/data_query_helper.rb
|
69
75
|
- lib/anki_record/helpers/shared_constants_helper.rb
|
70
76
|
- lib/anki_record/helpers/time_helper.rb
|
71
|
-
- lib/anki_record/
|
72
|
-
- lib/anki_record/
|
77
|
+
- lib/anki_record/note/note.rb
|
78
|
+
- lib/anki_record/note/note_attributes.rb
|
79
|
+
- lib/anki_record/note_field/note_field.rb
|
80
|
+
- lib/anki_record/note_field/note_field_attributes.rb
|
81
|
+
- lib/anki_record/note_field/note_field_defaults.rb
|
82
|
+
- lib/anki_record/note_type/note_type.rb
|
83
|
+
- lib/anki_record/note_type/note_type_attributes.rb
|
84
|
+
- lib/anki_record/note_type/note_type_defaults.rb
|
73
85
|
- lib/anki_record/version.rb
|
74
86
|
homepage: https://github.com/KyleRego/anki_record
|
75
87
|
licenses:
|
@@ -94,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
106
|
- !ruby/object:Gem::Version
|
95
107
|
version: '0'
|
96
108
|
requirements: []
|
97
|
-
rubygems_version: 3.4.
|
109
|
+
rubygems_version: 3.4.6
|
98
110
|
signing_key:
|
99
111
|
specification_version: 4
|
100
112
|
summary: Automate Anki flashcard editing with the Ruby programming language.
|
data/.rdoc_options
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
---
|
2
|
-
encoding: UTF-8
|
3
|
-
static_path: []
|
4
|
-
rdoc_include: []
|
5
|
-
page_dir:
|
6
|
-
charset: UTF-8
|
7
|
-
exclude:
|
8
|
-
- "~\\z"
|
9
|
-
- "\\.orig\\z"
|
10
|
-
- "\\.rej\\z"
|
11
|
-
- "\\.bak\\z"
|
12
|
-
- "\\.gemspec\\z"
|
13
|
-
hyperlink_all: false
|
14
|
-
line_numbers: false
|
15
|
-
locale:
|
16
|
-
locale_dir: locale
|
17
|
-
locale_name:
|
18
|
-
main_page:
|
19
|
-
markup: rdoc
|
20
|
-
output_decoration: true
|
21
|
-
show_hash: false
|
22
|
-
skip_tests: true
|
23
|
-
tab_width: 8
|
24
|
-
template_stylesheets: []
|
25
|
-
title:
|
26
|
-
visibility: :protected
|
27
|
-
webcvs:
|
@@ -1,183 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "pry"
|
4
|
-
require "pathname"
|
5
|
-
|
6
|
-
require_relative "db/anki_schema_definition"
|
7
|
-
require_relative "db/clean_collection2_record"
|
8
|
-
require_relative "db/clean_collection21_record"
|
9
|
-
require_relative "collection"
|
10
|
-
|
11
|
-
module AnkiRecord
|
12
|
-
##
|
13
|
-
# Represents an Anki SQLite3 package/database
|
14
|
-
#
|
15
|
-
# Use ::new to create a new object or ::open to create an object from an existing one
|
16
|
-
class AnkiPackage
|
17
|
-
NAME_ERROR_MESSAGE = "The name argument must be a string without spaces."
|
18
|
-
PATH_ERROR_MESSAGE = "*No .apkg file was found at the given path."
|
19
|
-
STANDARD_ERROR_MESSAGE = <<-MSG
|
20
|
-
An error occurred.
|
21
|
-
The temporary *.anki21 database has been deleted.
|
22
|
-
No *.apkg zip file has been saved.
|
23
|
-
MSG
|
24
|
-
|
25
|
-
private_constant :NAME_ERROR_MESSAGE, :PATH_ERROR_MESSAGE, :STANDARD_ERROR_MESSAGE
|
26
|
-
|
27
|
-
##
|
28
|
-
# Creates a new object which represents an Anki SQLite3 database
|
29
|
-
#
|
30
|
-
# This method takes an optional block argument.
|
31
|
-
#
|
32
|
-
# When a block argument is used, execution is yielded to the block.
|
33
|
-
# After the block executes, the temporary files are zipped into the +name+.apkg file
|
34
|
-
# which is saved in +directory+. +directory+ is the current working directory by default.
|
35
|
-
# If the block throws a runtime error, the temporary files are deleted but the zip file is not created.
|
36
|
-
#
|
37
|
-
# When no block argument is used, #zip_and_close must be called explicitly at the end of your script.
|
38
|
-
def initialize(name:, directory: Dir.pwd)
|
39
|
-
setup_package_instance_variables(name: name, directory: directory)
|
40
|
-
|
41
|
-
return unless block_given?
|
42
|
-
|
43
|
-
begin
|
44
|
-
yield self
|
45
|
-
rescue StandardError => e
|
46
|
-
close
|
47
|
-
puts_error_and_standard_message(error: e)
|
48
|
-
else
|
49
|
-
zip_and_close
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
##
|
54
|
-
# Executes a raw SQL statement against the *.anki21 database
|
55
|
-
#
|
56
|
-
# Do not use this to execute data definition language SQL statements
|
57
|
-
# (i.e. do not create, alter, or drop tables or indexes)
|
58
|
-
# unless you have a good reason to change the database schema.
|
59
|
-
def execute(raw_sql_string)
|
60
|
-
@anki21_database.execute raw_sql_string
|
61
|
-
end
|
62
|
-
|
63
|
-
private
|
64
|
-
|
65
|
-
def setup_package_instance_variables(name:, directory:)
|
66
|
-
@name = check_name_is_valid(name: name)
|
67
|
-
@directory = directory # TODO: check directory is valid
|
68
|
-
@tmpdir = Dir.mktmpdir
|
69
|
-
@tmp_files = []
|
70
|
-
@anki21_database = setup_anki21_database_object
|
71
|
-
@anki2_database = setup_anki2_database_object
|
72
|
-
@media_file = setup_media
|
73
|
-
@collection = Collection.new(anki_package: self)
|
74
|
-
end
|
75
|
-
|
76
|
-
def check_name_is_valid(name:)
|
77
|
-
raise ArgumentError, NAME_ERROR_MESSAGE unless name.instance_of?(String) && !name.empty? && !name.include?(" ")
|
78
|
-
|
79
|
-
name.end_with?(".apkg") ? name[0, name.length - 5] : name
|
80
|
-
end
|
81
|
-
|
82
|
-
def setup_anki21_database_object
|
83
|
-
anki21_file_name = "collection.anki21"
|
84
|
-
db = SQLite3::Database.new "#{@tmpdir}/#{anki21_file_name}", options: {}
|
85
|
-
@tmp_files << anki21_file_name
|
86
|
-
db.execute_batch ANKI_SCHEMA_DEFINITION
|
87
|
-
db.execute CLEAN_COLLECTION_21_RECORD
|
88
|
-
db.results_as_hash = true
|
89
|
-
db
|
90
|
-
end
|
91
|
-
|
92
|
-
def setup_anki2_database_object
|
93
|
-
anki2_file_name = "collection.anki2"
|
94
|
-
db = SQLite3::Database.new "#{@tmpdir}/#{anki2_file_name}", options: {}
|
95
|
-
@tmp_files << anki2_file_name
|
96
|
-
db.execute_batch ANKI_SCHEMA_DEFINITION
|
97
|
-
db.execute CLEAN_COLLECTION_2_RECORD
|
98
|
-
db.close
|
99
|
-
db
|
100
|
-
end
|
101
|
-
|
102
|
-
def setup_media
|
103
|
-
media_file_path = FileUtils.touch("#{@tmpdir}/media")[0]
|
104
|
-
media_file = File.open(media_file_path, mode: "w")
|
105
|
-
media_file.write("{}")
|
106
|
-
media_file.close
|
107
|
-
@tmp_files << "media"
|
108
|
-
media_file
|
109
|
-
end
|
110
|
-
|
111
|
-
def puts_error_and_standard_message(error:)
|
112
|
-
puts "#{error}\n#{STANDARD_ERROR_MESSAGE}"
|
113
|
-
end
|
114
|
-
|
115
|
-
public
|
116
|
-
|
117
|
-
##
|
118
|
-
# Creates a new object which represents the Anki SQLite3 database file at +path+
|
119
|
-
#
|
120
|
-
# Development has focused on ::new so this method is not recommended at this time
|
121
|
-
def self.open(path:, create_backup: true)
|
122
|
-
pathname = check_file_at_path_is_valid(path: path)
|
123
|
-
copy_apkg_file(pathname: pathname) if create_backup
|
124
|
-
@anki_package = new(name: pathname.basename.to_s, directory: pathname.dirname)
|
125
|
-
end
|
126
|
-
|
127
|
-
class << self
|
128
|
-
private
|
129
|
-
|
130
|
-
def check_file_at_path_is_valid(path:)
|
131
|
-
pathname = Pathname.new(path)
|
132
|
-
raise PATH_ERROR_MESSAGE unless pathname.file? && pathname.extname == ".apkg"
|
133
|
-
|
134
|
-
pathname
|
135
|
-
end
|
136
|
-
|
137
|
-
def copy_apkg_file(pathname:)
|
138
|
-
path = pathname.to_s
|
139
|
-
FileUtils.cp path, "#{path}.copy-#{Time.now.to_i}"
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
##
|
144
|
-
# Zips the temporary files into the *.apkg package and deletes the temporary files.
|
145
|
-
def zip_and_close
|
146
|
-
zip && close
|
147
|
-
end
|
148
|
-
|
149
|
-
private
|
150
|
-
|
151
|
-
def zip
|
152
|
-
Zip::File.open(target_zip_file, create: true) do |zip_file|
|
153
|
-
@tmp_files.each do |file_name|
|
154
|
-
zip_file.add(file_name, File.join(@tmpdir, file_name))
|
155
|
-
end
|
156
|
-
end
|
157
|
-
true
|
158
|
-
end
|
159
|
-
|
160
|
-
def target_zip_file
|
161
|
-
"#{@directory}/#{@name}.apkg"
|
162
|
-
end
|
163
|
-
|
164
|
-
def close
|
165
|
-
@anki21_database.close
|
166
|
-
FileUtils.rm_rf(@tmpdir)
|
167
|
-
end
|
168
|
-
|
169
|
-
public
|
170
|
-
|
171
|
-
##
|
172
|
-
# Returns true if the database is open
|
173
|
-
def open?
|
174
|
-
!closed?
|
175
|
-
end
|
176
|
-
|
177
|
-
##
|
178
|
-
# Returns true if the database is closed
|
179
|
-
def closed?
|
180
|
-
@anki21_database.closed?
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
@@ -1,65 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "pry"
|
4
|
-
require "json"
|
5
|
-
|
6
|
-
require_relative "deck"
|
7
|
-
require_relative "deck_options_group"
|
8
|
-
require_relative "helpers/time_helper"
|
9
|
-
require_relative "note_type"
|
10
|
-
|
11
|
-
module AnkiRecord
|
12
|
-
##
|
13
|
-
# Collection represents the single record in the Anki database `col` table
|
14
|
-
class Collection
|
15
|
-
include AnkiRecord::TimeHelper
|
16
|
-
|
17
|
-
##
|
18
|
-
# An array of the collection's note type objects
|
19
|
-
attr_reader :note_types
|
20
|
-
|
21
|
-
##
|
22
|
-
# An array of the collection's deck objects
|
23
|
-
attr_reader :decks
|
24
|
-
|
25
|
-
##
|
26
|
-
# Instantiates the collection object for the +anki_package+
|
27
|
-
def initialize(anki_package:)
|
28
|
-
setup_collection_instance_variables(anki_package: anki_package)
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
# rubocop:disable Metrics/MethodLength
|
34
|
-
# rubocop:disable Metrics/AbcSize
|
35
|
-
def setup_collection_instance_variables(anki_package:)
|
36
|
-
@anki_package = anki_package
|
37
|
-
@id = col_record["id"]
|
38
|
-
@crt = col_record["crt"]
|
39
|
-
@last_modified_time = (mod = col_record["mod"]).zero? ? milliseconds_since_epoch : mod
|
40
|
-
@scm = col_record["scm"]
|
41
|
-
@ver = col_record["ver"]
|
42
|
-
@dty = col_record["dty"]
|
43
|
-
@usn = col_record["usn"]
|
44
|
-
@ls = col_record["ls"]
|
45
|
-
@configuration = JSON.parse(col_record["conf"])
|
46
|
-
@note_types = JSON.parse(col_record["models"]).values.map do |model_hash|
|
47
|
-
NoteType.new(collection: self, args: model_hash)
|
48
|
-
end
|
49
|
-
@decks = JSON.parse(col_record["decks"]).values.map do |deck_hash|
|
50
|
-
Deck.new(collection: self, args: deck_hash)
|
51
|
-
end
|
52
|
-
@deck_option_groups = JSON.parse(col_record["dconf"]).values.map do |dconf_hash|
|
53
|
-
DeckOptionsGroup.new(collection: self, args: dconf_hash)
|
54
|
-
end
|
55
|
-
@tags = JSON.parse(col_record["tags"])
|
56
|
-
remove_instance_variable(:@col_record)
|
57
|
-
end
|
58
|
-
# rubocop:enable Metrics/AbcSize
|
59
|
-
# rubocop:enable Metrics/MethodLength
|
60
|
-
|
61
|
-
def col_record
|
62
|
-
@col_record ||= @anki_package.execute("select * from col;").first
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|