josh-kindler 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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
+