bookbinder 0.3.4 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/lib/bookbinder.rb +6 -5
  2. data/lib/bookbinder/file.rb +1 -2
  3. data/lib/bookbinder/file_system/zip_file.rb +4 -4
  4. data/lib/bookbinder/operations.rb +7 -1
  5. data/lib/bookbinder/package.rb +25 -27
  6. data/lib/bookbinder/package/openbook.rb +2 -3
  7. data/lib/bookbinder/scratch.rb +36 -0
  8. data/lib/bookbinder/transform/{openbook/json.rb → book_json.rb} +1 -1
  9. data/lib/bookbinder/version.rb +1 -1
  10. metadata +12 -63
  11. data/lib/bookbinder/package/epub.rb +0 -64
  12. data/lib/bookbinder/package/media_ripper.rb +0 -47
  13. data/lib/bookbinder/package/mp3_audiobook.rb +0 -43
  14. data/lib/bookbinder/transform/epub/audio_overlay.rb +0 -227
  15. data/lib/bookbinder/transform/epub/audio_soundtrack.rb +0 -73
  16. data/lib/bookbinder/transform/epub/contributor.rb +0 -11
  17. data/lib/bookbinder/transform/epub/cover_image.rb +0 -80
  18. data/lib/bookbinder/transform/epub/cover_page.rb +0 -148
  19. data/lib/bookbinder/transform/epub/creator.rb +0 -67
  20. data/lib/bookbinder/transform/epub/description.rb +0 -43
  21. data/lib/bookbinder/transform/epub/language.rb +0 -29
  22. data/lib/bookbinder/transform/epub/metadata.rb +0 -140
  23. data/lib/bookbinder/transform/epub/nav.rb +0 -60
  24. data/lib/bookbinder/transform/epub/nav_toc.rb +0 -177
  25. data/lib/bookbinder/transform/epub/ncx.rb +0 -63
  26. data/lib/bookbinder/transform/epub/ocf.rb +0 -33
  27. data/lib/bookbinder/transform/epub/opf.rb +0 -22
  28. data/lib/bookbinder/transform/epub/package_identifier.rb +0 -87
  29. data/lib/bookbinder/transform/epub/rendition.rb +0 -273
  30. data/lib/bookbinder/transform/epub/resources.rb +0 -38
  31. data/lib/bookbinder/transform/epub/spine.rb +0 -79
  32. data/lib/bookbinder/transform/epub/title.rb +0 -92
  33. data/lib/bookbinder/transform/epub/version.rb +0 -39
  34. data/lib/bookbinder/transform/media_ripper/cover_image.rb +0 -12
  35. data/lib/bookbinder/transform/media_ripper/eisbn.rb +0 -15
  36. data/lib/bookbinder/transform/media_ripper/metadata.rb +0 -27
  37. data/lib/bookbinder/transform/media_ripper/nav_toc.rb +0 -57
  38. data/lib/bookbinder/transform/media_ripper/publisher.rb +0 -15
  39. data/lib/bookbinder/transform/media_ripper/rendition.rb +0 -7
  40. data/lib/bookbinder/transform/media_ripper/spine.rb +0 -34
  41. data/lib/bookbinder/transform/media_ripper/title.rb +0 -16
  42. data/lib/bookbinder/transform/mp3_audiobook/cover_image.rb +0 -25
  43. data/lib/bookbinder/transform/mp3_audiobook/creator.rb +0 -27
  44. data/lib/bookbinder/transform/mp3_audiobook/description.rb +0 -18
  45. data/lib/bookbinder/transform/mp3_audiobook/metadata.rb +0 -20
  46. data/lib/bookbinder/transform/mp3_audiobook/nav_toc.rb +0 -71
  47. data/lib/bookbinder/transform/mp3_audiobook/publisher.rb +0 -18
  48. data/lib/bookbinder/transform/mp3_audiobook/rendition.rb +0 -7
  49. data/lib/bookbinder/transform/mp3_audiobook/spine.rb +0 -33
  50. data/lib/bookbinder/transform/mp3_audiobook/subject.rb +0 -18
  51. data/lib/bookbinder/transform/mp3_audiobook/title.rb +0 -18
@@ -1,47 +0,0 @@
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 audio_paths
30
- xml_paths.collect { |xml_path|
31
- next unless doc = file(xml_path).document('r')
32
- next unless markers_tag = doc.find('AudioBook > Markers')
33
- markers_tag['file']
34
- }.compact
35
- end
36
-
37
-
38
- def xml_paths
39
- xml_paths = []
40
- file_system.each { |path|
41
- next unless match = path.match(/Part(\d+)\.xml$/)
42
- xml_paths[match[1].to_i] = path
43
- }
44
- xml_paths.compact!
45
- end
46
-
47
- end
@@ -1,43 +0,0 @@
1
- class Bookbinder::Package::MP3Audiobook < Bookbinder::Package
2
-
3
- require_transforms('mp3_audiobook')
4
-
5
- DEFAULT_TRANSFORMS = [
6
- Bookbinder::Transform::MP3Audiobook_Rendition,
7
- Bookbinder::Transform::MP3Audiobook_Title,
8
- Bookbinder::Transform::MP3Audiobook_Creator,
9
- Bookbinder::Transform::MP3Audiobook_Description,
10
- Bookbinder::Transform::MP3Audiobook_Publisher,
11
- Bookbinder::Transform::MP3Audiobook_Subject,
12
- Bookbinder::Transform::MP3Audiobook_CoverImage,
13
- Bookbinder::Transform::MP3Audiobook_Spine,
14
- Bookbinder::Transform::MP3Audiobook_NavToc,
15
- Bookbinder::Transform::Organizer,
16
- Bookbinder::Transform::Generator
17
- ]
18
-
19
- MP3_EXT_RE = /.mp3$/i
20
-
21
-
22
- def self.recognize(path)
23
- File.directory?(path) && Dir.glob(File.join(path, '*')).all? { |path|
24
- path.match(MP3_EXT_RE)
25
- }
26
- end
27
-
28
-
29
- def self.transforms
30
- @transforms ||= DEFAULT_TRANSFORMS
31
- end
32
-
33
-
34
- def audio_paths
35
- [].tap { |aud_paths|
36
- file_system.each { |path|
37
- aud_paths << path if path.match(MP3_EXT_RE)
38
- }
39
- aud_paths.sort!
40
- }
41
- end
42
-
43
- end
@@ -1,227 +0,0 @@
1
- # EPUB 3 only. Spec:
2
- #
3
- # http://www.idpf.org/epub/301/spec/epub-mediaoverlays.html#sec-package-metadata
4
- #
5
- # Each spine item's manifest item may have a media-overlay attribute, which
6
- # is an idref pointing at a SMIL manifest item.
7
- #
8
- # Other properties to manage:
9
- #
10
- # <meta property="media:active-class">-epub-media-overlay-active</meta>
11
- # <meta property="media:playback-active-class">-epub-media-overlay-playing</meta>
12
- # <meta property="media:duration" refines="#ch1_audio">0:32:29</meta>
13
- # <meta property="media:duration">1:36:20</meta>
14
- # <meta property="media:narrator">Bill Speaker</meta>
15
- #
16
- # Note: "The Package Document must include the duration of each
17
- # Media Overlay as well as of the entire Publication."
18
- #
19
- class Bookbinder::Transform::EPUB_AudioOverlay < Bookbinder::Transform
20
-
21
- def dependencies
22
- [
23
- Bookbinder::Transform::EPUB_Metadata,
24
- Bookbinder::Transform::EPUB_Resources,
25
- Bookbinder::Transform::EPUB_Spine
26
- ]
27
- end
28
-
29
-
30
- def to_map(package)
31
- meta_to_prop(
32
- package,
33
- 'media:active-class',
34
- 'audio-overlay-active-class'
35
- )
36
- meta_to_prop(
37
- package,
38
- 'media:playback-active-class',
39
- 'audio-overlay-playback-active-class'
40
- )
41
- book_overlay_duration = meta_to_prop(
42
- package,
43
- 'media:duration',
44
- 'audio-overlay-duration'
45
- ) { |hashes|
46
- hashes.detect { |hash|
47
- unless hash.has_key?('refines')
48
- hash['@'] = smil_clock_value_to_seconds(hash['@'])
49
- true
50
- end
51
- }
52
- }
53
- # TODO: check that book does have a duration if there are any overlays.
54
- meta_to_prop(
55
- package,
56
- 'media:narrator',
57
- 'audio-overlay-narrator'
58
- ) { |hashes|
59
- hashes.detect { |hash| !hash.has_key?('refines') }
60
- }
61
- # Find each manifest item (SI) with media-overlay attribute:
62
- #
63
- # - find the map['resources'] item that matches the media-overlay id
64
- # - find the map['spine'] item that matches the SI id
65
- # - set 'audio-overlay' to the AO path
66
- # - set 'audio-overlay-duration' to the refined duration
67
- # - set 'audio-overlay-narrator' to the refined narrator
68
- #
69
- opf_doc = package.file(:opf).document('r')
70
- opf_doc.each('opf|manifest > opf|item[media-overlay]') { |cmpt_item|
71
- cmpt_id = cmpt_item['id']
72
- cmpt = package.map['spine'].detect { |c| c['id'] == cmpt_id }
73
- # TODO: nil check cmpt
74
- ao_id = cmpt_item['media-overlay']
75
- rsrc = package.map['resources'].detect { |r| r['id'] == ao_id }
76
- # TODO: nil check rsrc
77
- cmpt['audio-overlay'] = rsrc['path']
78
- cmpt_overlay_duration = meta_to_prop(
79
- package,
80
- 'media:duration',
81
- 'audio-overlay-duration',
82
- cmpt
83
- ) { |hashes|
84
- hashes.detect { |hash|
85
- if hash['refines']['@'] == '#'+ao_id
86
- hash['@'] = smil_clock_value_to_seconds(hash['@'])
87
- true
88
- end
89
- }
90
- }
91
- # TODO: nil check cmpt_overlay_duration
92
- meta_to_prop(
93
- package,
94
- 'media:narrator',
95
- 'audio-overlay-narrator',
96
- cmpt
97
- ) { |hashes|
98
- hash['refines']['@'] == '#'+ao_id
99
- }
100
- }
101
- end
102
-
103
-
104
- # Create a meta tag for:
105
- # audio-overlay-active-class => media:active-class
106
- # audio-overlay-playback-active-class => media:playback-active-class
107
- # audio-overlay-duration => media:duration
108
- # audio-overlay-narrator => media:narrator
109
- #
110
- # For each spine item with a 'media-overlay' key:
111
- #
112
- # Find the corresponding map resource
113
- # Find the corresponding manifest item for component:
114
- # - set 'media-overlay' to rsrc['id']
115
- # Create a top-level meta tag:
116
- # cmpt['audio-overlay-duration'] => meta[property='media:duration'][refines='rsrc["id"]]
117
- # Also create a top-level meta tag for media:narrator if cmpt has 'audio-overlay-narrator'
118
- #
119
- def from_map(package)
120
- opf_doc = package.file(:opf).document
121
- metadata_tag = opf_doc.find('opf|metadata')
122
- prop_to_meta(
123
- package.map,
124
- metadata_tag,
125
- 'audio-overlay-active-class',
126
- 'media:active-class'
127
- )
128
- prop_to_meta(
129
- package.map,
130
- metadata_tag,
131
- 'audio-overlay-playback-active-class',
132
- 'media:playback-active-class'
133
- )
134
- prop_to_meta(
135
- package.map,
136
- metadata_tag,
137
- 'audio-overlay-duration',
138
- 'media:duration'
139
- )
140
- prop_to_meta(
141
- package.map,
142
- metadata_tag,
143
- 'audio-overlay-narrator',
144
- 'media:narrator'
145
- )
146
- package.map['spine'].each { |cmpt|
147
- next unless cmpt['audio-overlay']
148
- rsrc = package.map['resources'].detect { |r|
149
- r['path'] == cmpt['audio-overlay']
150
- }
151
- cmpt_manifest_item = opf_doc.find("opf|manifest > opf|item##{cmpt['id']}")
152
- cmpt_manifest_item['media-overlay'] = rsrc['id']
153
- duration_meta_tag = prop_to_meta(
154
- cmpt,
155
- metadata_tag,
156
- 'audio-overlay-duration',
157
- 'media:duration'
158
- )
159
- duration_meta_tag['refines'] = '#'+rsrc['id']
160
- narrator_meta_tag = prop_to_meta(
161
- cmpt,
162
- metadata_tag,
163
- 'audio-overlay-narrator',
164
- 'media:narrator'
165
- )
166
- narrator_meta_tag['refines'] = '#'+rsrc['id'] if narrator_meta_tag
167
- }
168
- end
169
-
170
-
171
- protected
172
-
173
- def meta_to_prop(package, meta_name, prop_name, target = nil)
174
- return unless package.map['metadata']
175
- hashes = package.map['metadata'][meta_name]
176
- return unless hashes && hashes.any?
177
- hash = block_given? ? yield(hashes) : hashes.first
178
- if hash
179
- hashes.delete(hash)
180
- package.map['metadata'].delete(meta_name) if hashes.empty?
181
- (target || package.map)[prop_name] = hash['@']
182
- end
183
- end
184
-
185
-
186
- # 5:34:31.396 = 5 hours, 34 minutes, 31 seconds and 396 milliseconds
187
- # 124:59:36 = 124 hours, 59 minutes and 36 seconds
188
- # 0:05:01.2 = 5 minutes, 1 second and 200 milliseconds
189
- # 0:00:04 = 4 seconds
190
- # 09:58 = 9 minutes and 58 seconds
191
- # 00:56.78 = 56 seconds and 780 milliseconds
192
- # 76.2s = 76.2 seconds = 76 seconds and 200 milliseconds
193
- # 7.75h = 7.75 hours = 7 hours and 45 minutes
194
- # 13min = 13 minutes
195
- # 2345ms = 2345 milliseconds
196
- # 12.345 = 12 seconds and 345 milliseconds
197
- def smil_clock_value_to_seconds(clock)
198
- return clock.to_f if clock.kind_of?(Numeric)
199
- clock = clock.to_s
200
- if match = clock.match(/(\d+:)?(\d+:)(\d+\.?\d*)/)
201
- h = match[1].to_s.to_f
202
- m = match[2].to_s.to_f
203
- s = match[3].to_s.to_f
204
- h*60*60 + m*60 + s
205
- elsif match = clock.match(/^(\d+\.?\d*)h$/)
206
- match[1].to_f*60*60
207
- elsif match = clock.match(/^(\d+\.?\d*)min$/)
208
- match[1].to_f*60
209
- elsif match = clock.match(/^(\d+\.?\d*)s?$/)
210
- clock.to_f
211
- elsif match = clock.match(/^(\d+\.?\d*)ms$/)
212
- clock.to_f / 1000.0
213
- end
214
- end
215
-
216
-
217
- def prop_to_meta(scope, metadata_tag, prop_name, meta_name)
218
- if scope[prop_name]
219
- meta_tag = Nokogiri::XML::Node.new('meta', metadata_tag)
220
- meta_tag['property'] = meta_name
221
- meta_tag.content = scope[prop_name]
222
- metadata_tag.add_child(meta_tag)
223
- return meta_tag
224
- end
225
- end
226
-
227
- end
@@ -1,73 +0,0 @@
1
- # For the specification, see the iBooks Asset Guide, specifically the
2
- # section titled "Ambient Soundtrack" in version 5.1:
3
- #
4
- # https://itunesconnect.apple.com/docs/iBooksAssetGuide5.1Revision2.pdf
5
- #
6
- #
7
- class Bookbinder::Transform::EPUB_AudioSoundtrack < Bookbinder::Transform
8
-
9
- def dependencies
10
- [Bookbinder::Transform::EPUB_Spine]
11
- end
12
-
13
-
14
- # Iterate through each spine item, looking for
15
- #
16
- # <audio epub:type="ibooks:soundtrack" src="..." />
17
- #
18
- def to_map(package)
19
- package.map['spine'].each { |cmpt|
20
- find_soundtrack_in_component(cmpt, package.file(cmpt['path']))
21
- }
22
- end
23
-
24
-
25
- # Iterate through each spine item, adding an audio tag to components
26
- # that don't have one, or setting the src if the audio tag exists.
27
- #
28
- def from_map(package)
29
- package.map['spine'].each { |cmpt|
30
- if cmpt['audio-soundtrack']
31
- add_soundtrack_to_component(cmpt, package.file(cmpt['path']))
32
- end
33
- }
34
- end
35
-
36
-
37
- protected
38
-
39
- def find_soundtrack_in_component(cmpt, cmpt_file)
40
- soundtrack_tag = soundtrack_tag_in_document(cmpt_file.document)
41
- cmpt['audio-soundtrack'] = soundtrack_tag ? soundtrack_tag['src'] : nil
42
- end
43
-
44
-
45
- def add_soundtrack_to_component(cmpt, cmpt_file)
46
- cmpt_doc = cmpt_file.document
47
- unless soundtrack_tag = soundtrack_tag_in_document(cmpt_doc)
48
- cmpt_doc.add_namespace('epub')
49
- cmpt_doc.add_prefix('ibooks', 'epub:prefix')
50
-
51
- soundtrack_tag = cmpt_doc.new_node('audio', :append => 'body')
52
- soundtrack_tag['epub:type'] = 'ibooks:soundtrack'
53
-
54
- cmpt_doc.new_node('style', :append => 'head') { |style_tag|
55
- style_tag['type'] = 'text/css'
56
- style_tag['id'] = 'BB_HIDE_AUDIO_SOUNDTRACK'
57
- style_tag.content = [
58
- 'audio[epub|type="ibooks:soundtrack"] {',
59
- 'position: absolute;',
60
- 'top: -100px;',
61
- '}'
62
- ].join
63
- }
64
- end
65
- soundtrack_tag['src'] = cmpt['audio-soundtrack']
66
- end
67
-
68
-
69
- def soundtrack_tag_in_document(cmpt_doc)
70
- cmpt_doc.find('audio[epub|type="ibooks:soundtrack"]')
71
- end
72
-
73
- end
@@ -1,11 +0,0 @@
1
- require 'bookbinder/transform/epub/creator'
2
-
3
- class Bookbinder::Transform::EPUB_Contributor < Bookbinder::Transform::EPUB_Creator
4
-
5
- protected
6
-
7
- def actor_type
8
- 'contributor'
9
- end
10
-
11
- end
@@ -1,80 +0,0 @@
1
- # The best source of information about wading through the EPUB
2
- # cover image quagmire has always been Keith's article on the
3
- # Threepress blog:
4
- #
5
- # http://blog.safaribooksonline.com/2009/11/20/best-practices-in-epub-cover-images/
6
- #
7
- # He added an update for EPUB3, which follows the spec but is
8
- # a bit easier to grok:
9
- #
10
- # http://blog.safaribooksonline.com/2011/05/26/covers-in-epub3/
11
- #
12
- class Bookbinder::Transform::EPUB_CoverImage < Bookbinder::Transform
13
-
14
- def dependencies
15
- [
16
- Bookbinder::Transform::EPUB_Resources,
17
- Bookbinder::Transform::EPUB_Metadata
18
- ]
19
- end
20
-
21
-
22
- # If it's EPUB3, the cover will be in the 'properties' attribute
23
- # of the manifest item: 'cover-image'
24
- #
25
- # Otherwise, look for a manifest item with an 'id' of 'cover-image'.
26
- #
27
- # Or, look for a meta tag with a 'name' of 'cover', then find the
28
- # manifest item that has the 'id' that matches meta's 'content'.
29
- #
30
- # Set map['cover'] to this item (and remove it from map['resources']).
31
- #
32
- def to_map(package)
33
- opf_doc = package.file(:opf).document('r')
34
- cover_item = opf_doc.find('opf|manifest > opf|item[properties~="cover-image"]')
35
- cover_item ||= opf_doc.find('opf|manifest > opf|item[id="cover-image"]')
36
- cover_item ||= opf_doc.find('opf|manifest > opf|item[id="cover_image"]')
37
- cover_meta_props = (package.map['metadata'] || {}).delete('cover')
38
- if !cover_item && cover_meta_props && cover_meta_props.any?
39
- cover_image_id = cover_meta_props.first['content']['@']
40
- cover_item = opf_doc.find('opf|manifest > opf|item[id="'+cover_image_id+'"]')
41
- end
42
- covers = {}
43
- if cover_item
44
- rsrc = package.map['resources'].detect { |r|
45
- r['id'] == cover_item['id']
46
- }
47
- covers.update("front" => package.map['resources'].delete(rsrc))
48
- end
49
- package.map['cover'] = covers
50
- end
51
-
52
-
53
- # Belt and braces: give the manifest item a property of
54
- # 'cover-image', an 'id' of 'cover-image' (updating any
55
- # idrefs) and create a meta tag with 'name'='cover' and
56
- # 'content'='cover-image'.
57
- #
58
- def from_map(package)
59
- return unless package.map['cover'] && cover = package.map['cover']['front']
60
- opf_doc = package.file(:opf).document
61
-
62
- opf_doc.new_node('item', :append => 'opf|manifest') { |manifest_item_tag|
63
- manifest_item_tag['href'] = package.make_href(cover['path'])
64
- manifest_item_tag['media-type'] = cover['media-type']
65
- manifest_item_tag['id'] = 'cover-image'
66
- manifest_item_tag['properties'] = 'cover-image'
67
- }
68
-
69
- cover_id = package.make_id(cover['path'])
70
- opf_doc.each('[idref="'+cover_id+'"]') { |idref|
71
- idref['idref'] = cover_id
72
- }
73
-
74
- opf_doc.new_node('meta', :append => 'opf|metadata') { |cover_meta_tag|
75
- cover_meta_tag['name'] = 'cover'
76
- cover_meta_tag['content'] = 'cover-image'
77
- }
78
- end
79
-
80
- end