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.
@@ -1,79 +1,56 @@
1
+ require 'forwardable'
2
+
1
3
  module Mobi
2
4
  class Metadata
3
- DRM_KEY_SIZE = 48
4
-
5
- EXTH_RECORDS = { 100 => :author, 101 => :publisher, 102 => :imprint, 103 => :description, 104 => :isbn, 105 => :subject,
6
- 106 => :published_at, 107 => :review, 108 => :contributor, 109 => :rights, 110 => :subject_code,
7
- 111 => :type, 112 => :source, 113 => :asin, 114 => :version}
8
- attr_reader *EXTH_RECORDS.values
9
- attr_reader :title
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
- attr_accessor :stream, :data, :mobi, :exth
12
- attr_reader :exth_records
36
+ offset = @mobi_header.full_name_offset
37
+ length = @mobi_header.full_name_length
13
38
 
14
- def initialize(stream)
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
- def mobi_stream
31
- offset = 78
32
- start, = @data[offset, 4].unpack('N*')
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
@@ -1,67 +1,44 @@
1
1
  module Mobi
2
2
  class StreamSlicer
3
3
 
4
- attr_reader :stream, :length
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
- @stop = stop
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 = @start
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 "" if size <= 0
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
@@ -4,20 +4,22 @@
4
4
  # -*- encoding: utf-8 -*-
5
5
 
6
6
  Gem::Specification.new do |s|
7
- s.name = %q{mobi}
8
- s.version = "0.1.2"
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 = %q{2011-04-10}
13
- s.description = %q{Mobi is a Rubygem that allows you to easily read MOBI e-book format metadata }
14
- s.email = %q{jkongie@gmail.com}
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
- "test/helper.rb",
32
- "test/test_mobi.rb"
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 = %q{http://github.com/jkongie/mobi}
46
+ s.homepage = "http://github.com/jkongie/mobi"
35
47
  s.licenses = ["MIT"]
36
48
  s.require_paths = ["lib"]
37
- s.rubygems_version = %q{1.6.2}
38
- s.summary = %q{A Rubygem that inspects MOBI metadata}
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<shoulda>, [">= 0"])
49
- s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
50
- s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
51
- s.add_development_dependency(%q<rcov>, [">= 0"])
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<shoulda>, [">= 0"])
54
- s.add_dependency(%q<bundler>, ["~> 1.0.0"])
55
- s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
56
- s.add_dependency(%q<rcov>, [">= 0"])
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<shoulda>, [">= 0"])
60
- s.add_dependency(%q<bundler>, ["~> 1.0.0"])
61
- s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
62
- s.add_dependency(%q<rcov>, [">= 0"])
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
 
@@ -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