bookbinder 0.2.0 → 0.3.0

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