josh-kindler 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Joshua Peek
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ = KindleR
2
+
3
+ KindleR is a library for packaging and formatting MOBI files.
data/bin/kindler ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mobi'
4
+
5
+ unless file = ARGV[0]
6
+ raise ArgumentError
7
+ end
8
+
9
+ mobi = Mobi.new
10
+ mobi.title = File.basename(file, '.html')
11
+ mobi.content = File.open(file).read
12
+ mobi.write_file "#{File.basename(file, '.html')}.mobi"
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ module Kindler
7
+ class Dropbox
8
+ SUPPORTED_FILE_FORMATS = {
9
+ :documents => ['mobi', 'prc', 'pdf', 'txt', 'azw', 'azw1'],
10
+ :music => ['mp3'],
11
+ :audible => ['aa', 'aax']
12
+ }
13
+
14
+ def self.run
15
+ new.run
16
+ end
17
+
18
+ def run
19
+ if kindle_is_connected? && dropbox.exist?
20
+ process_files
21
+ end
22
+
23
+ run_post_mount_hook
24
+ end
25
+
26
+ protected
27
+ def drive
28
+ @drive ||= Pathname.new('/Volumes/Kindle')
29
+ end
30
+
31
+ def dropbox
32
+ @dropbox ||= Pathname.new(ENV['KINDLE_DROPBOX'] || "/Users/#{ENV['USER']}/Documents/Kindle")
33
+ end
34
+
35
+ def kindle_is_connected?
36
+ drive.directory?
37
+ end
38
+
39
+ def process_files
40
+ files_to_be_processed.each do |file|
41
+ target_folder = map_folder_to File.extname(file)
42
+
43
+ if target_folder
44
+ FileUtils.mv(file, target_folder, :force => true)
45
+ end
46
+ end
47
+ end
48
+
49
+ def files_to_be_processed
50
+ @files_to_be_processed ||= dropbox.entries.reject { |f| f.to_s.match(/^\./) }.map { |file| dropbox.join(file) }
51
+ end
52
+
53
+ def map_folder_to(file_extension)
54
+ return nil unless file_extension
55
+ file_extension = file_extension.delete('.')
56
+ folder = SUPPORTED_FILE_FORMATS.find(proc {[]}) { |folder_map| folder_map[1].include?(file_extension) }[0]
57
+ folder ? drive.join(folder.to_s) : nil
58
+ end
59
+
60
+ def run_post_mount_hook
61
+ if post_mount_hook.exist?
62
+ system(post_mount_hook)
63
+ end
64
+ end
65
+
66
+ def post_mount_hook
67
+ drive.join(".post-mount")
68
+ end
69
+ end
70
+ end
71
+
72
+ Kindler::Dropbox.run
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mobi'
4
+ require 'optparse'
5
+
6
+ name = nil
7
+
8
+ opts = OptionParser.new("", 24, ' ') { |opts|
9
+ opts.banner = "Usage: kindler-rename book.mobi [options]"
10
+
11
+ opts.on("--title NAME", "New title") { |n| name = m }
12
+
13
+ opts.on_tail("-h", "--help", "Show this message") do
14
+ puts opts
15
+ exit
16
+ end
17
+
18
+ opts.parse!
19
+ }
20
+
21
+ unless file = ARGV[0]
22
+ puts opts
23
+ exit
24
+ end
25
+
26
+ mobi = Palm::PDB.new(file)
27
+ Mobi.rename(mobi, name || mobi.name)
28
+ mobi.write_file(file)
data/lib/mobi.rb ADDED
@@ -0,0 +1,89 @@
1
+ require 'palm'
2
+
3
+ class Mobi < Palm::PDB
4
+ autoload :ExtendedHeader, 'mobi/extended_header'
5
+ autoload :Header, 'mobi/header'
6
+
7
+ # Optimized version of Mobi#title= that does not parse the
8
+ # entire document.
9
+ def self.rename(pdb, name)
10
+ header = pdb.data[0].data
11
+
12
+ offset = header.unpack('@84N')[0]
13
+ length = header.unpack('@88N')[0]
14
+ header[offset...offset+length] = ("\000" * length)
15
+
16
+ header[88...88+4] = [name.length].pack('N*')
17
+ header[offset...offset+name.length] = name
18
+
19
+ pdb.name = name
20
+
21
+ pdb
22
+ end
23
+
24
+ def initialize(from = nil)
25
+ super
26
+
27
+ self.creator = 'MOBI'
28
+ self.type = 'BOOK'
29
+ self.data[0] ||= Header.new
30
+ end
31
+
32
+ def header
33
+ data[0]
34
+ end
35
+
36
+ def title
37
+ header.full_name
38
+ end
39
+
40
+ def title=(title)
41
+ header.full_name = title
42
+ self.name = title
43
+ end
44
+
45
+ def content
46
+ text = ''
47
+ header.record_count.times do |n|
48
+ text << data[n + 1].data
49
+ end
50
+ text
51
+ end
52
+
53
+ def content=(text)
54
+ header.text_length = text.length
55
+ record_size = header.record_size
56
+ io = StringIO.new(text)
57
+ index = 1
58
+
59
+ while str = io.read(record_size)
60
+ record = Palm::RawRecord.new(str)
61
+ record.record_id = index
62
+ index += 1
63
+ data << record
64
+ end
65
+
66
+ header.record_count = index
67
+
68
+ nil
69
+ end
70
+
71
+ protected
72
+ def unpack_entry(byte_string)
73
+ header = byte_string.unpack('@16a4') == ['MOBI'] rescue false
74
+ header ? Header.from_binary(byte_string) :
75
+ Palm::RawRecord.new(byte_string)
76
+ end
77
+
78
+ def unpack_header(header)
79
+ super
80
+ end
81
+
82
+ def pack_header(app_info_offset, sort_offset)
83
+ super
84
+ end
85
+
86
+ def pack_entry(entry)
87
+ entry.data
88
+ end
89
+ end
@@ -0,0 +1,77 @@
1
+ class Mobi
2
+ class ExtendedHeader
3
+ TYPES = {
4
+ 1 => "drm_server_id",
5
+ 2 => "drm_commerce_id",
6
+ 3 => "drm_ebookbase_book_id",
7
+ 100 => "author",
8
+ 101 => "publisher",
9
+ 102 => "imprint",
10
+ 103 => "description",
11
+ 104 => "isbn",
12
+ 105 => "subject",
13
+ 106 => "publishingdate",
14
+ 107 => "review",
15
+ 108 => "contributor",
16
+ 109 => "rights",
17
+ 110 => "subjectcode",
18
+ 111 => "type",
19
+ 112 => "source",
20
+ 113 => "asin",
21
+ 114 => "versionnumber",
22
+ 115 => "sample",
23
+ 116 => "startreading",
24
+ 118 => "price",
25
+ 119 => "currency",
26
+ 201 => "coveroffset",
27
+ 202 => "thumboffset",
28
+ 203 => "hasfakecover",
29
+ 401 => "clippinglimit",
30
+ 402 => "publisherlimit",
31
+ 404 => "ttsflag",
32
+ 501 => "cdecontenttype",
33
+ 502 => "lastupdatetime",
34
+ 503 => "updatedtitle",
35
+ 504 => "cdecontentkey"
36
+ }
37
+
38
+ def self.from_binary(data, offset, max = nil)
39
+ type = data.unpack("@#{offset + 4}N")[0]
40
+ length = data.unpack("@#{offset + 8}N")[0]
41
+ raise RangeError if max && (offset - 256 + length) > max
42
+ data = data.unpack("@#{offset + 12}C#{length - 8}")
43
+ new(type, data)
44
+ end
45
+
46
+ def initialize(type = 0, data = [])
47
+ @type, @data = type, data
48
+ end
49
+
50
+ def type
51
+ TYPES[@type] || @type
52
+ end
53
+ attr_writer :type
54
+
55
+ def length
56
+ @data.length + 8
57
+ end
58
+ alias_method :size, :length
59
+
60
+ def data
61
+ if type.is_a?(String)
62
+ @data.pack('C*').unpack('a*').join
63
+ else
64
+ @data.pack('C*').unpack('N*')
65
+ end
66
+ end
67
+ attr_writer :data
68
+
69
+ def to_a
70
+ [@type, length] + @data.pack('C*').unpack('N*')
71
+ end
72
+
73
+ def to_s
74
+ to_a.pack("N*")
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,263 @@
1
+ class Mobi
2
+ class Header < Palm::Record
3
+ TYPES = {
4
+ 2 => 'BOOK',
5
+ 3 => 'PALMDOC',
6
+ 4 => 'AUDIO',
7
+ 257 => 'NEWS',
8
+ 258 => 'NEWS_FEED',
9
+ 259 => 'NEWS_MAGAZINE',
10
+ 513 => 'PICS',
11
+ 514 => 'WORD',
12
+ 515 => 'XLS',
13
+ 516 => 'PPT',
14
+ 517 => 'TEXT',
15
+ 518 => 'HTML'
16
+ }
17
+
18
+ ENCODING = {
19
+ 1252 => 'ISO-8859-1',
20
+ 65001 => 'UTF-8'
21
+ }
22
+
23
+ LANGUAGES = {
24
+ 'es' => 0x000a,
25
+ 'sv' => 0x001d,
26
+ 'sv-se' => 0x041d,
27
+ 'sv-fi' => 0x081d,
28
+ 'fi' => 0x000b,
29
+ 'en' => 0x0009,
30
+ 'en-au' => 0x0C09,
31
+ 'en-bz' => 0x2809,
32
+ 'en-ca' => 0x1009,
33
+ 'en-cb' => 0x2409,
34
+ 'en-ie' => 0x1809,
35
+ 'en-jm' => 0x2009,
36
+ 'en-nz' => 0x1409,
37
+ 'en-ph' => 0x3409,
38
+ 'en-za' => 0x1c09,
39
+ 'en-tt' => 0x2c09,
40
+ 'en-us' => 0x0409,
41
+ 'en-gb' => 0x0809,
42
+ 'en-zw' => 0x3009,
43
+ 'da' => 0x0006,
44
+ 'da-dk' => 0x0406,
45
+ 'da' => 0x0006,
46
+ 'da' => 0x0006,
47
+ 'nl' => 0x0013,
48
+ 'nl-be' => 0x0813,
49
+ 'nl-nl' => 0x0413,
50
+ 'fi' => 0x000b,
51
+ 'fi-fi' => 0x040b,
52
+ 'fr' => 0x000c,
53
+ 'fr-fr' => 0x040c,
54
+ 'de' => 0x0007,
55
+ 'de-at' => 0x0c07,
56
+ 'de-de' => 0x0407,
57
+ 'de-lu' => 0x1007,
58
+ 'de-ch' => 0x0807,
59
+ 'no' => 0x0014,
60
+ 'nb-no' => 0x0414,
61
+ 'nn-no' => 0x0814
62
+ }
63
+
64
+ NO_DRM = 0xffffffff
65
+
66
+ DEFAULTS = {
67
+ :compression => 1,
68
+ :text_length => 0,
69
+ :record_count => 1,
70
+ :record_size => 4096,
71
+ :encryption => 0,
72
+
73
+ :identifier => 'MOBI',
74
+ :length => 232,
75
+ :type => 'BOOK',
76
+ :encoding => 'ISO-8859-1',
77
+ :generator_version => 4,
78
+
79
+ :first_non_book_index => 2,
80
+ :full_name_offset => 352,
81
+ :full_name => 'untitled',
82
+
83
+ :language => 'en-us',
84
+ :format_version => 4,
85
+ :image_record_index => 2,
86
+ :extended_flags => 0x50,
87
+
88
+ :drm_offset => NO_DRM,
89
+ :drm_count => 0,
90
+ :drm_size => 0,
91
+ :drm_flags => 0,
92
+
93
+ :exth_identifier => 'EXTH',
94
+ :exth_length => 0,
95
+ :exth_count => 0,
96
+ :extended_headers => []
97
+ }
98
+
99
+ def self.from_binary(data)
100
+ h = new
101
+
102
+ h.compression = data.unpack('n2')[0]
103
+ h.text_length = data.unpack('@4N')[0]
104
+ h.record_count = data.unpack('@8n2')[0]
105
+ h.record_size = data.unpack('@10n2')[0]
106
+ h.encryption = data.unpack('@12n2')[0]
107
+
108
+ h.identifier = data.unpack('@16a4')[0]
109
+ h.length = data.unpack('@20N')[0]
110
+ h.type = data.unpack('@24N')[0]
111
+ h.encoding = data.unpack('@28N')[0]
112
+ h.id = data.unpack('@32N')[0]
113
+ h.generator_version = data.unpack('@36N')[0]
114
+
115
+ h.first_non_book_index = data.unpack('@80N')[0]
116
+ h.full_name_offset = data.unpack('@84N')[0]
117
+ full_name_length = data.unpack('@88N')[0]
118
+ h.full_name = data.unpack("@#{h.full_name_offset}a#{full_name_length}")[0]
119
+
120
+ h.language = data.unpack('@92N')[0]
121
+ h.format_version = data.unpack('@104N')[0]
122
+ h.image_record_index = data.unpack('@108N')[0]
123
+ h.extended_flags = data.unpack('@128N')[0]
124
+
125
+ h.drm_offset = data.unpack('@168N')[0]
126
+ h.drm_count = data.unpack('@172N')[0]
127
+ h.drm_size = data.unpack('@174N')[0]
128
+ h.drm_flags = data.unpack('@176N')[0]
129
+
130
+ h.exth_identifier = data.unpack('@248a4')[0]
131
+ h.exth_length = data.unpack('@252N')[0]
132
+ h.exth_count = data.unpack('@256N')[0]
133
+
134
+ h.extended_headers = []
135
+ offset = 256
136
+ h.exth_count.times do
137
+ offset += h.extended_headers.last.length if h.extended_headers.last
138
+ begin
139
+ header = ExtendedHeader.from_binary(data, offset, h.exth_length)
140
+ rescue Exception => e
141
+ warn "Corrupted EXTH Header" if $DEBUG
142
+ header = ExtendedHeader.new
143
+ end
144
+ h.extended_headers << header
145
+ end
146
+
147
+ h
148
+ end
149
+
150
+ def initialize
151
+ super
152
+
153
+ @type, @language, @encoding = nil
154
+
155
+ @expunged = @dirty = @deleted = @private = false
156
+
157
+ DEFAULTS.each do |attr, value|
158
+ send("#{attr}=", value) unless send(attr)
159
+ end
160
+
161
+ @id ||= (rand() * 1_000_000_000).floor
162
+ end
163
+
164
+ attr_accessor :compression, :text_length, :record_count, :record_size, :encryption
165
+ attr_accessor :identifier, :length, :id, :generator_version
166
+ attr_accessor :first_non_book_index
167
+ attr_accessor :full_name_offset, :full_name
168
+ attr_accessor :format_version, :image_record_index, :extended_flags
169
+ attr_accessor :drm_offset, :drm_count, :drm_size, :drm_flags
170
+ attr_accessor :exth_identifier, :exth_length, :exth_count, :extended_headers
171
+ alias_method :size, :length
172
+
173
+ def compressed?
174
+ compression != 1
175
+ end
176
+
177
+ def encrypted?
178
+ encryption != 0
179
+ end
180
+
181
+ def type
182
+ TYPES[@type]
183
+ end
184
+
185
+ def type=(type)
186
+ @type = TYPES.index(type) || type
187
+ end
188
+
189
+ def encoding
190
+ ENCODING[@encoding]
191
+ end
192
+
193
+ def encoding=(encoding)
194
+ @encoding = ENCODING.index(encoding) || encoding
195
+ end
196
+
197
+ def language
198
+ LANGUAGES.index(@language)
199
+ end
200
+
201
+ def language=(language)
202
+ @language = LANGUAGES[language] || language
203
+ end
204
+
205
+ def full_name_length
206
+ full_name.length
207
+ end
208
+
209
+ def to_s
210
+ [
211
+ repack_as_int([compression, 0], 'n2'),
212
+ text_length,
213
+ repack_as_int([record_count, record_size], 'n2'),
214
+ repack_as_int([encryption, 0], 'n2'),
215
+ repack_as_int([identifier], 'a4'),
216
+ length,
217
+ TYPES.index(type),
218
+ ENCODING.index(encoding),
219
+ id,
220
+ generator_version,
221
+ 0xffffffff,
222
+ 0xffffffff,
223
+ 0xffffffff,
224
+ 0xffffffff,
225
+ 0xffffffff,
226
+ 0xffffffff,
227
+ 0xffffffff,
228
+ 0xffffffff,
229
+ 0xffffffff,
230
+ 0xffffffff,
231
+ first_non_book_index,
232
+ full_name_offset,
233
+ full_name_length,
234
+ LANGUAGES[language],
235
+ 0, 0,
236
+ format_version,
237
+ image_record_index,
238
+ 0, 0, 0, 0,
239
+ extended_flags,
240
+ 0, 0, 0, 0, 0, 0, 0, 0,
241
+ drm_offset,
242
+ drm_count,
243
+ drm_size,
244
+ drm_flags,
245
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
246
+ repack_as_int([exth_identifier], 'a4'),
247
+ exth_length,
248
+ exth_count,
249
+ extended_headers.map { |exth| exth.to_a }
250
+ ].flatten.pack('N*')
251
+ end
252
+ alias_method :data, :to_s
253
+
254
+ def to_a
255
+ to_s.unpack('N*')
256
+ end
257
+
258
+ private
259
+ def repack_as_int(ary, format)
260
+ ary.pack(format).unpack('N')[0]
261
+ end
262
+ end
263
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: josh-kindler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Joshua Peek
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-06 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: palm
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.4
24
+ version:
25
+ description: KindleR is a library for packaging and formatting MOBI files.
26
+ email: josh@joshpeek.com
27
+ executables:
28
+ - kindler
29
+ - kindler-dropbox
30
+ - kindler-rename
31
+ extensions: []
32
+
33
+ extra_rdoc_files:
34
+ - README.rdoc
35
+ - MIT-LICENSE
36
+ files:
37
+ - lib/mobi.rb
38
+ - lib/mobi/extended_header.rb
39
+ - lib/mobi/header.rb
40
+ - README.rdoc
41
+ - MIT-LICENSE
42
+ has_rdoc: false
43
+ homepage: http://github.com/josh/kindler
44
+ post_install_message:
45
+ rdoc_options: []
46
+
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project:
64
+ rubygems_version: 1.2.0
65
+ signing_key:
66
+ specification_version: 2
67
+ summary: Ruby Kindle library
68
+ test_files: []
69
+