bookbinder 0.2.0 → 0.3.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.
@@ -64,7 +64,7 @@ class Bookbinder::FileSystem::ZipFile < Bookbinder::FileSystem
64
64
 
65
65
 
66
66
  def each(&blk)
67
- @zipfile.each(&blk)
67
+ @zipfile.each { |entry| yield(entry.to_s) }
68
68
  end
69
69
 
70
70
 
@@ -0,0 +1,33 @@
1
+ class Bookbinder::Package::MediaRipper < Bookbinder::Package
2
+
3
+ require_transforms('media_ripper')
4
+
5
+ DEFAULT_TRANSFORMS = [
6
+ Bookbinder::Transform::MediaRipper_Rendition,
7
+ Bookbinder::Transform::MediaRipper_Title,
8
+ Bookbinder::Transform::MediaRipper_Publisher,
9
+ Bookbinder::Transform::MediaRipper_EISBN,
10
+ Bookbinder::Transform::MediaRipper_Spine,
11
+ Bookbinder::Transform::MediaRipper_NavToc,
12
+ Bookbinder::Transform::MediaRipper_CoverImage,
13
+ Bookbinder::Transform::Organizer,
14
+ Bookbinder::Transform::Generator
15
+ ]
16
+
17
+
18
+ def self.recognize(path)
19
+ return path.match(/.odmr.zip$/);
20
+ # TODO: detect unzipped media-ripper audiobook?
21
+ end
22
+
23
+
24
+ def self.transforms
25
+ @transforms = DEFAULT_TRANSFORMS
26
+ end
27
+
28
+
29
+ def transforms
30
+ self.class.transforms
31
+ end
32
+
33
+ end
@@ -32,6 +32,7 @@ class Bookbinder::Transform::EPUB_Rendition < Bookbinder::Transform
32
32
 
33
33
 
34
34
  def to_map(package)
35
+ package.map['rendition-format'] = 'ebook'
35
36
  book_properties = book_properties_from_apple_display_options(package)
36
37
  book_properties.update(book_properties_from_opf(package))
37
38
  component_rendition_properties_from_opf(package, book_properties)
@@ -139,8 +140,12 @@ class Bookbinder::Transform::EPUB_Rendition < Bookbinder::Transform
139
140
  end
140
141
  }
141
142
  spent.each { |key| package.map['metadata'].delete(key) }
142
- if vpt = book_properties['rendition-viewport']
143
- book_properties['rendition-viewport'] = parse_viewport_string(vpt)
143
+ if vpt_str = book_properties['rendition-viewport']
144
+ if vpt = parse_viewport_string(vpt_str)
145
+ book_properties['rendition-viewport'] = vpt
146
+ else
147
+ package.warn("Viewport string unparseable: #{vpt_str}")
148
+ end
144
149
  end
145
150
  }
146
151
  end
@@ -226,6 +231,9 @@ class Bookbinder::Transform::EPUB_Rendition < Bookbinder::Transform
226
231
  'width' => icb.match(/width\s*=\s*(\d+)/)[1].to_i,
227
232
  'height' => icb.match(/height\s*=\s*(\d+)/)[1].to_i
228
233
  }
234
+ rescue
235
+ # package.warn("Viewport string unparseable: #{icb}")
236
+ nil
229
237
  end
230
238
 
231
239
 
@@ -0,0 +1,12 @@
1
+ class Bookbinder::Transform::MediaRipper_CoverImage < Bookbinder::Transform
2
+
3
+ def to_map(package)
4
+ if package.file('cover.jpg')
5
+ package.map['cover'] = { 'front' => {
6
+ 'path' => 'cover.jpg',
7
+ 'media-type' => 'image/jpeg'
8
+ } }
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,15 @@
1
+ class Bookbinder::Transform::MediaRipper_EISBN < Bookbinder::Transform
2
+
3
+ def dependencies
4
+ [Bookbinder::Transform::MediaRipper_Metadata]
5
+ end
6
+
7
+
8
+ def to_map(package)
9
+ return unless md = package.map['metadata']
10
+ return unless md['Identifier'] && (id = md['Identifier'].first)
11
+ package.map['eisbn'] = id['@'] if id['type'] && id['type']['@'] == 'ISBN'
12
+ md.delete('Identifier')
13
+ end
14
+
15
+ end
@@ -0,0 +1,27 @@
1
+ class Bookbinder::Transform::MediaRipper_Metadata < Bookbinder::Transform
2
+
3
+ def to_map(package)
4
+ return unless mm_file = find_first_mediamarker_file(package)
5
+ mm_doc = mm_file.document('r')
6
+ md = {}
7
+ mm_doc.each('AudioBook > TitleInfo > *') { |tag|
8
+ name = tag.node_name
9
+ value = { '@' => tag.content.strip }
10
+ tag.attributes.each_pair { |key, attr|
11
+ value[key] = { '@' => attr.value }
12
+ }
13
+ md[name] = [md[name], value].flatten.compact
14
+ }
15
+ package.map['metadata'] = md
16
+ end
17
+
18
+
19
+ protected
20
+
21
+ def find_first_mediamarker_file(package)
22
+ package.file_system.each { |path|
23
+ return package.file(path) if File.extname(path) == '.xml'
24
+ }
25
+ end
26
+
27
+ end
@@ -0,0 +1,60 @@
1
+ # MediaMarker XML format is like this:
2
+ #
3
+ # <AudioBook>
4
+ # <Markers file="HP-136127-Part08.mp3">
5
+ # <Marker>
6
+ # <Name>Chapter 15</Name>
7
+ # <Time>74:48.000</Time>
8
+ # </Marker>
9
+ # ... etc ...
10
+ # </Markers>
11
+ # </AudioBook>
12
+ #
13
+ class Bookbinder::Transform::MediaRipper_NavToc < Bookbinder::Transform
14
+
15
+ def to_map(package)
16
+ toc = []
17
+ xml_paths_in_package(package).each { |xml_path|
18
+ doc = package.file(xml_path).document('r')
19
+ markers_tag = doc.find('AudioBook > Markers')
20
+ aud_path = markers_tag['file']
21
+ doc.each_within(markers_tag, 'Marker') { |marker_tag|
22
+ seconds = translate_time(doc.find_within(marker_tag, 'Time').content)
23
+ toc << {
24
+ 'title' => doc.find_within(marker_tag, 'Name').content.strip,
25
+ 'path' => "#{aud_path}#{seconds ? "##{seconds}" : ''}"
26
+ }
27
+ }
28
+ }
29
+ package.map['nav'] = { 'toc' => toc }
30
+ end
31
+
32
+
33
+ protected
34
+
35
+ def xml_paths_in_package(package)
36
+ xml_paths = []
37
+ package.file_system.each { |path|
38
+ next unless match = path.match(/Part(\d+)\.xml$/)
39
+ xml_paths[match[1].to_i] = path
40
+ }
41
+ xml_paths.compact!
42
+ end
43
+
44
+
45
+ # MediaMarker time string comes in like:
46
+ #
47
+ # 74:48.000
48
+ #
49
+ # We should translate to 74*60+48.0 ==> 4488
50
+ #
51
+ def translate_time(time_str)
52
+ match = time_str.match(/^(\d+):([\d\.]+)$/)
53
+ minutes = match[1].to_i
54
+ seconds = match[2].to_f
55
+ seconds = seconds.round if seconds % 1 == 0
56
+ out = minutes * 60 + seconds
57
+ out > 0 ? out : nil
58
+ end
59
+
60
+ end
@@ -0,0 +1,15 @@
1
+ class Bookbinder::Transform::MediaRipper_Publisher < Bookbinder::Transform
2
+
3
+ def dependencies
4
+ [Bookbinder::Transform::MediaRipper_Metadata]
5
+ end
6
+
7
+
8
+ def to_map(package)
9
+ return unless md = package.map['metadata']
10
+ return unless md['Publisher'] && md['Publisher'].first
11
+ package.map['publisher'] = md['Publisher'].first['@']
12
+ md.delete('Publisher')
13
+ end
14
+
15
+ end
@@ -0,0 +1,7 @@
1
+ class Bookbinder::Transform::MediaRipper_Rendition < Bookbinder::Transform
2
+
3
+ def to_map(package)
4
+ package.map['rendition-format'] = 'audiobook'
5
+ end
6
+
7
+ end
@@ -0,0 +1,44 @@
1
+ class Bookbinder::Transform::MediaRipper_Spine < Bookbinder::Transform
2
+
3
+ def to_map(package)
4
+ package.map['spine'] = audio_paths_in_package(package).collect { |path|
5
+ { 'path' => path, 'audio-duration' => audio_length(package, path) }
6
+ }
7
+ end
8
+
9
+
10
+ protected
11
+
12
+ def audio_paths_in_package(package)
13
+ xml_paths_in_package(package).collect { |xml_path|
14
+ next unless doc = package.file(xml_path).document('r')
15
+ next unless markers_tag = doc.find('AudioBook > Markers')
16
+ markers_tag['file']
17
+ }.compact
18
+ end
19
+
20
+
21
+ def xml_paths_in_package(package)
22
+ xml_paths = []
23
+ package.file_system.each { |path|
24
+ next unless match = path.match(/Part(\d+)\.xml$/)
25
+ xml_paths[match[1].to_i] = path
26
+ }
27
+ xml_paths.compact!
28
+ end
29
+
30
+
31
+ def audio_length(package, path)
32
+ length = 0
33
+ begin
34
+ package.file(path).get_file { |f|
35
+ Mp3Info.open(f) { |info| length = info.length }
36
+ }
37
+ rescue => e
38
+ # TODO - is there error handling to do here?
39
+ raise e
40
+ end
41
+ length
42
+ end
43
+
44
+ end
@@ -0,0 +1,16 @@
1
+ class Bookbinder::Transform::MediaRipper_Title < Bookbinder::Transform
2
+
3
+ def dependencies
4
+ [Bookbinder::Transform::MediaRipper_Metadata]
5
+ end
6
+
7
+
8
+ def to_map(package)
9
+ package.map['title'] = titles = {}
10
+ return unless md = package.map['metadata']
11
+ return unless md['Title'] && md['Title'].first
12
+ titles['main'] = md['Title'].first['@']
13
+ md.delete('Title')
14
+ end
15
+
16
+ end
@@ -10,6 +10,9 @@ class Bookbinder::Transform::Organizer < Bookbinder::Transform
10
10
  unique-identifier
11
11
  modified
12
12
  version
13
+ eisbn
14
+ pisbn
15
+ publisher
13
16
  rendition-*
14
17
  audio-*
15
18
  spine
@@ -1,5 +1,5 @@
1
1
  module Bookbinder
2
2
 
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.0"
4
4
 
5
5
  end
data/lib/bookbinder.rb CHANGED
@@ -4,6 +4,7 @@ require 'zip'
4
4
  require 'json'
5
5
  require 'time'
6
6
  require 'cgi'
7
+ require 'mp3info'
7
8
 
8
9
  module Bookbinder
9
10
  end
@@ -25,5 +26,6 @@ require 'bookbinder/file_system/directory'
25
26
  require 'bookbinder/file_system/zip_file'
26
27
 
27
28
  # Package types
28
- require 'bookbinder/package/epub'
29
29
  require 'bookbinder/package/openbook'
30
+ require 'bookbinder/package/epub'
31
+ require 'bookbinder/package/media_ripper'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bookbinder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-08-06 00:00:00.000000000 Z
12
+ date: 2015-01-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: nokogiri
16
- requirement: &11209620 !ruby/object:Gem::Requirement
16
+ requirement: &16273180 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *11209620
24
+ version_requirements: *16273180
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rubyzip
27
- requirement: &11208800 !ruby/object:Gem::Requirement
27
+ requirement: &16272680 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - =
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 1.0.0
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *11208800
35
+ version_requirements: *16272680
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: mime-types
38
- requirement: &11208160 !ruby/object:Gem::Requirement
38
+ requirement: &16272260 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,21 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *11208160
46
+ version_requirements: *16272260
47
+ - !ruby/object:Gem::Dependency
48
+ name: ruby-mp3info
49
+ requirement: &16271800 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *16271800
47
58
  - !ruby/object:Gem::Dependency
48
59
  name: rake
49
- requirement: &11207480 !ruby/object:Gem::Requirement
60
+ requirement: &16271380 !ruby/object:Gem::Requirement
50
61
  none: false
51
62
  requirements:
52
63
  - - ! '>='
@@ -54,7 +65,7 @@ dependencies:
54
65
  version: '0'
55
66
  type: :development
56
67
  prerelease: false
57
- version_requirements: *11207480
68
+ version_requirements: *16271380
58
69
  description: Convert easily between common ebook formats.
59
70
  email:
60
71
  - jpearson@overdrive.com
@@ -73,6 +84,7 @@ files:
73
84
  - lib/bookbinder/media_type.rb
74
85
  - lib/bookbinder/operations.rb
75
86
  - lib/bookbinder/package/epub.rb
87
+ - lib/bookbinder/package/media_ripper.rb
76
88
  - lib/bookbinder/package/openbook.rb
77
89
  - lib/bookbinder/package.rb
78
90
  - lib/bookbinder/transform/epub/audio_overlay.rb
@@ -96,6 +108,14 @@ files:
96
108
  - lib/bookbinder/transform/epub/title.rb
97
109
  - lib/bookbinder/transform/epub/version.rb
98
110
  - lib/bookbinder/transform/generator.rb
111
+ - lib/bookbinder/transform/media_ripper/cover_image.rb
112
+ - lib/bookbinder/transform/media_ripper/eisbn.rb
113
+ - lib/bookbinder/transform/media_ripper/metadata.rb
114
+ - lib/bookbinder/transform/media_ripper/nav_toc.rb
115
+ - lib/bookbinder/transform/media_ripper/publisher.rb
116
+ - lib/bookbinder/transform/media_ripper/rendition.rb
117
+ - lib/bookbinder/transform/media_ripper/spine.rb
118
+ - lib/bookbinder/transform/media_ripper/title.rb
99
119
  - lib/bookbinder/transform/openbook/json.rb
100
120
  - lib/bookbinder/transform/organizer.rb
101
121
  - lib/bookbinder/transform.rb