mobi 0.1.2 → 0.2.0
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/.rspec +2 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +4 -4
- data/Gemfile.lock +21 -9
- data/README.rdoc +41 -0
- data/Rakefile +3 -17
- data/VERSION +1 -1
- data/lib/mobi.rb +7 -3
- data/lib/mobi/header/exth_header.rb +46 -0
- data/lib/mobi/header/mobi_header.rb +153 -0
- data/lib/mobi/header/palm_doc_header.rb +73 -0
- data/lib/mobi/metadata.rb +45 -68
- data/lib/mobi/metadata_streams.rb +46 -0
- data/lib/mobi/stream_slicer.rb +14 -37
- data/mobi.gemspec +34 -26
- data/spec/fixtures/sherlock.mobi +0 -0
- data/spec/lib/mobi/header/exth_header_spec.rb +36 -0
- data/spec/lib/mobi/header/mobi_header_spec.rb +79 -0
- data/spec/lib/mobi/header/palm_doc_header_spec.rb +45 -0
- data/spec/lib/mobi/metadata_spec.rb +62 -0
- data/spec/lib/mobi/stream_slicer_spec.rb +64 -0
- data/spec/lib/mobi_spec.rb +10 -0
- data/spec/spec_helper.rb +4 -0
- metadata +90 -67
- data/test/helper.rb +0 -18
- data/test/test_mobi.rb +0 -7
data/lib/mobi/metadata.rb
CHANGED
@@ -1,79 +1,56 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
1
3
|
module Mobi
|
2
4
|
class Metadata
|
3
|
-
|
4
|
-
|
5
|
-
EXTH_RECORDS =
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
EXTH_RECORDS = %w(author publisher imprint description isbn subject
|
8
|
+
published_at review contributor rights subject_code type
|
9
|
+
source asin version)
|
10
|
+
|
11
|
+
# Raw data stream
|
12
|
+
attr_reader :data
|
13
|
+
# Individual header classes for your reading pleasure.
|
14
|
+
attr_reader :palm_doc_header, :mobi_header, :exth_header
|
15
|
+
|
16
|
+
def initialize(file)
|
17
|
+
@file = file
|
18
|
+
@data = StreamSlicer.new(file)
|
19
|
+
|
20
|
+
raise InvalidMobi, "The supplied file is not in a valid mobi format" unless bookmobi?
|
21
|
+
|
22
|
+
@record_zero_stream = MetadataStreams.record_zero_stream(file)
|
23
|
+
@palm_doc_header = Header::PalmDocHeader.new @record_zero_stream
|
24
|
+
@mobi_header = Header::MobiHeader.new @record_zero_stream
|
25
|
+
|
26
|
+
@exth_stream = MetadataStreams.exth_stream(file, @mobi_header.header_length)
|
27
|
+
@exth_header = Header::ExthHeader.new @exth_stream
|
28
|
+
end
|
29
|
+
|
30
|
+
# Gets the title of the book.
|
31
|
+
#
|
32
|
+
# Returns a String.
|
33
|
+
def title
|
34
|
+
return @title if @title
|
10
35
|
|
11
|
-
|
12
|
-
|
36
|
+
offset = @mobi_header.full_name_offset
|
37
|
+
length = @mobi_header.full_name_length
|
13
38
|
|
14
|
-
|
15
|
-
@stream = stream
|
16
|
-
@data = StreamSlicer.new(stream)
|
17
|
-
@exth_records = []
|
18
|
-
return unless bookmobi?
|
19
|
-
@mobi = mobi_stream
|
20
|
-
@title = read_title
|
21
|
-
@exth = exth_stream
|
22
|
-
|
23
|
-
store_mobi_data
|
39
|
+
@title = @record_zero_stream[offset, length]
|
24
40
|
end
|
25
|
-
|
41
|
+
|
42
|
+
# Determines if the file is a valid mobi file.
|
43
|
+
#
|
44
|
+
# Returns true if the file is a valid MOBI.
|
26
45
|
def bookmobi?
|
27
46
|
@data[60, 8] == "BOOKMOBI"
|
28
47
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
stop, = @data[offset + 8, offset + 12].unpack('N*')
|
34
|
-
StreamSlicer.new(self.stream, start, stop)
|
35
|
-
end
|
36
|
-
|
37
|
-
def exth_stream
|
38
|
-
exth_off = @mobi[20, 4].unpack('N*').first + 16 + @mobi.start
|
39
|
-
StreamSlicer.new(stream, exth_off, @mobi.stop)
|
40
|
-
end
|
41
|
-
|
42
|
-
def read_title
|
43
|
-
offset, = @mobi[84, 4].unpack('N*')
|
44
|
-
length, = @mobi[88, 4].unpack('N*')
|
45
|
-
@mobi[offset.to_i, length.to_i]
|
46
|
-
end
|
47
|
-
|
48
|
-
def store_mobi_data
|
49
|
-
record_count, = @exth[8, 4].unpack('N*')
|
50
|
-
start = 12
|
51
|
-
record_count.times do
|
52
|
-
code, = @exth[start, 4].unpack('N*')
|
53
|
-
code = code.to_i
|
54
|
-
|
55
|
-
length, = @exth[start + 4, 4].unpack('N*')
|
56
|
-
value = @exth[start + 8, length - 8]
|
57
|
-
|
58
|
-
if EXTH_RECORDS[code]
|
59
|
-
instance_variable_set "@#{EXTH_RECORDS[code].to_s}", value
|
60
|
-
@exth_records << EXTH_RECORDS[code]
|
61
|
-
end
|
62
|
-
|
63
|
-
start += length
|
64
|
-
end
|
48
|
+
|
49
|
+
# Delegate EXTH records types to the EXTH header.
|
50
|
+
EXTH_RECORDS.each do |type|
|
51
|
+
def_delegators :@exth_header, type.to_sym, type.to_sym
|
65
52
|
end
|
66
53
|
|
54
|
+
class InvalidMobi < ArgumentError;end;
|
67
55
|
end
|
68
|
-
|
69
|
-
end
|
70
|
-
|
71
|
-
# def record(self, n):
|
72
|
-
# if n >= self.nrecs:
|
73
|
-
# raise ValueError('non-existent record %r' % n)
|
74
|
-
# offoff = 78 + (8 * n)
|
75
|
-
# start, = unpack('>I', self.data[offoff + 0:offoff + 4])
|
76
|
-
# stop = None
|
77
|
-
# if n < (self.nrecs - 1):
|
78
|
-
# stop, = unpack('>I', self.data[offoff + 8:offoff + 12])
|
79
|
-
# return StreamSlicer(self.stream, start, stop)
|
56
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Mobi
|
2
|
+
module MetadataStreams
|
3
|
+
|
4
|
+
# Creates a stream starting at the Record 0 in the PalmDOC.
|
5
|
+
#
|
6
|
+
# Returns a StreamSlicer.
|
7
|
+
def self.record_zero_stream(file)
|
8
|
+
data = StreamSlicer.new(file)
|
9
|
+
|
10
|
+
start, stop = record_zero_endpoints(data)
|
11
|
+
|
12
|
+
StreamSlicer.new(file, start, stop)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Creates a stream starting at the EXTH header in Record 0.
|
16
|
+
#
|
17
|
+
# Returns a StreamSlicer.
|
18
|
+
def self.exth_stream(file, header_length)
|
19
|
+
record_zero_stream = record_zero_stream(file)
|
20
|
+
|
21
|
+
record_zero_offset = record_zero_stream.start
|
22
|
+
palm_doc_header_length = 16
|
23
|
+
|
24
|
+
exth_off = record_zero_offset +
|
25
|
+
palm_doc_header_length +
|
26
|
+
header_length
|
27
|
+
|
28
|
+
StreamSlicer.new(file, exth_off, record_zero_stream.stop)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Determines the start and end points of Record 0 in the PalmDOC. The start point
|
34
|
+
# is returned as the first value in the array, and the end point as the
|
35
|
+
# second value.
|
36
|
+
#
|
37
|
+
# Returns an Array.
|
38
|
+
def self.record_zero_endpoints(data)
|
39
|
+
offset = 78
|
40
|
+
start, = data[offset, 4].unpack('N*')
|
41
|
+
stop, = data[offset + 8, offset + 12].unpack('N*');
|
42
|
+
[start, stop]
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
data/lib/mobi/stream_slicer.rb
CHANGED
@@ -1,67 +1,44 @@
|
|
1
1
|
module Mobi
|
2
2
|
class StreamSlicer
|
3
3
|
|
4
|
-
attr_reader
|
4
|
+
attr_reader :stream, :length
|
5
5
|
attr_accessor :start, :stop
|
6
6
|
|
7
|
-
def initialize(stream, start=0, stop=nil)
|
7
|
+
def initialize(stream, start = 0, stop = nil)
|
8
8
|
@stream = stream
|
9
9
|
@start = start
|
10
|
+
|
10
11
|
if stop.nil?
|
11
12
|
stream.seek(0, 2)
|
12
13
|
stop = stream.tell
|
13
14
|
end
|
14
|
-
|
15
|
+
|
16
|
+
@stop = stop
|
15
17
|
@length = stop - start
|
16
18
|
end
|
17
19
|
|
18
20
|
def [](offset, bytes=1)
|
19
21
|
stream = @stream
|
20
|
-
base
|
21
|
-
|
22
|
+
base = @start
|
23
|
+
|
22
24
|
if bytes == 1
|
23
25
|
stream.seek(base + offset)
|
24
26
|
return stream.read(1)
|
25
27
|
end
|
26
|
-
|
28
|
+
|
27
29
|
start = offset
|
28
30
|
stop = offset + bytes
|
29
|
-
|
31
|
+
|
30
32
|
# Reverse if you want to pass in negative bytes
|
31
33
|
start, stop = stop, start if bytes < 0
|
32
|
-
|
34
|
+
|
35
|
+
# I can't find a use case where it will ever get here
|
33
36
|
size = stop - start
|
34
|
-
return
|
35
|
-
|
37
|
+
return nil if size <= 0
|
38
|
+
|
36
39
|
stream.seek(base + start)
|
37
40
|
data = stream.read(size)
|
38
41
|
return data
|
39
42
|
end
|
40
|
-
|
41
|
-
|
42
|
-
# def [](key)
|
43
|
-
# stream = self.stream
|
44
|
-
# base = self.start
|
45
|
-
# if key.is_a? Integer
|
46
|
-
# stream.seek(base + key)
|
47
|
-
# return stream.read(1)
|
48
|
-
# end
|
49
|
-
# if key.is_a? Range
|
50
|
-
# start = key.first
|
51
|
-
# stop = key.last
|
52
|
-
#
|
53
|
-
# if key.max.nil?
|
54
|
-
# start, stop = stop, start
|
55
|
-
# end
|
56
|
-
# size = stop - start
|
57
|
-
#
|
58
|
-
# return "" if size <= 0
|
59
|
-
#
|
60
|
-
# stream.seek(base + start)
|
61
|
-
# data = stream.read(size)
|
62
|
-
# return data
|
63
|
-
# end
|
64
|
-
# end
|
65
|
-
|
66
43
|
end
|
67
|
-
end
|
44
|
+
end
|
data/mobi.gemspec
CHANGED
@@ -4,20 +4,22 @@
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
|
-
s.name =
|
8
|
-
s.version = "0.
|
7
|
+
s.name = "mobi"
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["jkongie"]
|
12
|
-
s.date =
|
13
|
-
s.description =
|
14
|
-
s.email =
|
12
|
+
s.date = "2012-08-06"
|
13
|
+
s.description = "Mobi is a Rubygem that allows you to easily read MOBI e-book format metadata."
|
14
|
+
s.email = "jkongie@gmail.com"
|
15
15
|
s.extra_rdoc_files = [
|
16
16
|
"LICENSE.txt",
|
17
17
|
"README.rdoc"
|
18
18
|
]
|
19
19
|
s.files = [
|
20
20
|
".document",
|
21
|
+
".rspec",
|
22
|
+
"CHANGELOG.md",
|
21
23
|
"Gemfile",
|
22
24
|
"Gemfile.lock",
|
23
25
|
"LICENSE.txt",
|
@@ -25,41 +27,47 @@ Gem::Specification.new do |s|
|
|
25
27
|
"Rakefile",
|
26
28
|
"VERSION",
|
27
29
|
"lib/mobi.rb",
|
30
|
+
"lib/mobi/header/exth_header.rb",
|
31
|
+
"lib/mobi/header/mobi_header.rb",
|
32
|
+
"lib/mobi/header/palm_doc_header.rb",
|
28
33
|
"lib/mobi/metadata.rb",
|
34
|
+
"lib/mobi/metadata_streams.rb",
|
29
35
|
"lib/mobi/stream_slicer.rb",
|
30
36
|
"mobi.gemspec",
|
31
|
-
"
|
32
|
-
"
|
37
|
+
"spec/fixtures/sherlock.mobi",
|
38
|
+
"spec/lib/mobi/header/exth_header_spec.rb",
|
39
|
+
"spec/lib/mobi/header/mobi_header_spec.rb",
|
40
|
+
"spec/lib/mobi/header/palm_doc_header_spec.rb",
|
41
|
+
"spec/lib/mobi/metadata_spec.rb",
|
42
|
+
"spec/lib/mobi/stream_slicer_spec.rb",
|
43
|
+
"spec/lib/mobi_spec.rb",
|
44
|
+
"spec/spec_helper.rb"
|
33
45
|
]
|
34
|
-
s.homepage =
|
46
|
+
s.homepage = "http://github.com/jkongie/mobi"
|
35
47
|
s.licenses = ["MIT"]
|
36
48
|
s.require_paths = ["lib"]
|
37
|
-
s.rubygems_version =
|
38
|
-
s.summary =
|
39
|
-
s.test_files = [
|
40
|
-
"test/helper.rb",
|
41
|
-
"test/test_mobi.rb"
|
42
|
-
]
|
49
|
+
s.rubygems_version = "1.8.24"
|
50
|
+
s.summary = "A Rubygem that inspects MOBI metadata."
|
43
51
|
|
44
52
|
if s.respond_to? :specification_version then
|
45
53
|
s.specification_version = 3
|
46
54
|
|
47
55
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
48
|
-
s.add_development_dependency(%q<
|
49
|
-
s.add_development_dependency(%q<
|
50
|
-
s.add_development_dependency(%q<
|
51
|
-
s.add_development_dependency(%q<
|
56
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.11.0"])
|
57
|
+
s.add_development_dependency(%q<rr>, ["~> 1.0.4"])
|
58
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.1.0"])
|
59
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.8.0"])
|
52
60
|
else
|
53
|
-
s.add_dependency(%q<
|
54
|
-
s.add_dependency(%q<
|
55
|
-
s.add_dependency(%q<
|
56
|
-
s.add_dependency(%q<
|
61
|
+
s.add_dependency(%q<rspec>, ["~> 2.11.0"])
|
62
|
+
s.add_dependency(%q<rr>, ["~> 1.0.4"])
|
63
|
+
s.add_dependency(%q<bundler>, ["~> 1.1.0"])
|
64
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.0"])
|
57
65
|
end
|
58
66
|
else
|
59
|
-
s.add_dependency(%q<
|
60
|
-
s.add_dependency(%q<
|
61
|
-
s.add_dependency(%q<
|
62
|
-
s.add_dependency(%q<
|
67
|
+
s.add_dependency(%q<rspec>, ["~> 2.11.0"])
|
68
|
+
s.add_dependency(%q<rr>, ["~> 1.0.4"])
|
69
|
+
s.add_dependency(%q<bundler>, ["~> 1.1.0"])
|
70
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.0"])
|
63
71
|
end
|
64
72
|
end
|
65
73
|
|
Binary file
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'mobi/stream_slicer'
|
4
|
+
require 'mobi/metadata_streams'
|
5
|
+
require 'mobi/header/mobi_header'
|
6
|
+
require 'mobi/header/exth_header'
|
7
|
+
|
8
|
+
describe Mobi::Header::ExthHeader do
|
9
|
+
|
10
|
+
before :all do
|
11
|
+
file = File.open('spec/fixtures/sherlock.mobi')
|
12
|
+
|
13
|
+
record_zero_stream = Mobi::MetadataStreams.record_zero_stream(file)
|
14
|
+
mobi_header = Mobi::Header::MobiHeader.new record_zero_stream
|
15
|
+
exth_stream = Mobi::MetadataStreams.exth_stream(file, mobi_header.header_length)
|
16
|
+
|
17
|
+
@header = Mobi::Header::ExthHeader.new exth_stream
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'gets the author' do
|
21
|
+
@header.author.should == 'Sir Arthur Conan Doyle'
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'gets the book subject' do
|
25
|
+
@header.subject.should == 'Detective and mystery stories, English'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'gets the book rights' do
|
29
|
+
@header.rights.should == 'Public domain in the USA.'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'gets the book source' do
|
33
|
+
@header.source.should == 'http://www.gutenberg.org/files/2350/2350-h/2350-h.htm'
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'mobi/stream_slicer'
|
4
|
+
require 'mobi/metadata_streams'
|
5
|
+
require 'mobi/header/mobi_header'
|
6
|
+
|
7
|
+
describe Mobi::Header::MobiHeader do
|
8
|
+
before :all do
|
9
|
+
file = File.open('spec/fixtures/sherlock.mobi')
|
10
|
+
stream = Mobi::MetadataStreams.record_zero_stream(file)
|
11
|
+
|
12
|
+
@header = Mobi::Header::MobiHeader.new stream
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'gets the identifier' do
|
16
|
+
@header.identifier.should == 'MOBI'
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'gets the length of the MOBI header' do
|
20
|
+
@header.header_length.should == 232
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'gets the mobi type as an integer' do
|
24
|
+
@header.raw_mobi_type.should == 2
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'gets the mobi type as a string' do
|
28
|
+
@header.mobi_type.should == 'MOBIpocket Book'
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'gets the raw text encoding' do
|
32
|
+
@header.raw_text_encoding.should == 65001
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'gets the text encoding' do
|
36
|
+
@header.text_encoding.should == 'UTF-8'
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'gets the unique id' do
|
40
|
+
@header.unique_id.should == 1532466569
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'gets the file version' do
|
44
|
+
@header.file_version.should == 6
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'gets the first non book index' do
|
48
|
+
@header.first_non_book_index.should == 16
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'gets the full name offset' do
|
52
|
+
@header.full_name_offset.should == 688
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'gets the full name length' do
|
56
|
+
@header.full_name_length.should == 12
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'gets the raw locale code' do
|
60
|
+
@header.raw_locale_code.should == 9
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'gets the minimum supported mobipocket version' do
|
64
|
+
@header.minimum_supported_mobipocket_version.should == 6
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'gets the first image index record number' do
|
68
|
+
@header.first_image_index_record_number.should == 19
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'gets the EXTH header flag' do
|
72
|
+
@header.exth_flag.should == 1
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'checks if there an EXTH header exists' do
|
76
|
+
@header.exth_header?.should be_true
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|