bookbinder 0.3.4 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/bookbinder.rb +6 -5
- data/lib/bookbinder/file.rb +1 -2
- data/lib/bookbinder/file_system/zip_file.rb +4 -4
- data/lib/bookbinder/operations.rb +7 -1
- data/lib/bookbinder/package.rb +25 -27
- data/lib/bookbinder/package/openbook.rb +2 -3
- data/lib/bookbinder/scratch.rb +36 -0
- data/lib/bookbinder/transform/{openbook/json.rb → book_json.rb} +1 -1
- data/lib/bookbinder/version.rb +1 -1
- metadata +12 -63
- data/lib/bookbinder/package/epub.rb +0 -64
- data/lib/bookbinder/package/media_ripper.rb +0 -47
- data/lib/bookbinder/package/mp3_audiobook.rb +0 -43
- data/lib/bookbinder/transform/epub/audio_overlay.rb +0 -227
- data/lib/bookbinder/transform/epub/audio_soundtrack.rb +0 -73
- data/lib/bookbinder/transform/epub/contributor.rb +0 -11
- data/lib/bookbinder/transform/epub/cover_image.rb +0 -80
- data/lib/bookbinder/transform/epub/cover_page.rb +0 -148
- data/lib/bookbinder/transform/epub/creator.rb +0 -67
- data/lib/bookbinder/transform/epub/description.rb +0 -43
- data/lib/bookbinder/transform/epub/language.rb +0 -29
- data/lib/bookbinder/transform/epub/metadata.rb +0 -140
- data/lib/bookbinder/transform/epub/nav.rb +0 -60
- data/lib/bookbinder/transform/epub/nav_toc.rb +0 -177
- data/lib/bookbinder/transform/epub/ncx.rb +0 -63
- data/lib/bookbinder/transform/epub/ocf.rb +0 -33
- data/lib/bookbinder/transform/epub/opf.rb +0 -22
- data/lib/bookbinder/transform/epub/package_identifier.rb +0 -87
- data/lib/bookbinder/transform/epub/rendition.rb +0 -273
- data/lib/bookbinder/transform/epub/resources.rb +0 -38
- data/lib/bookbinder/transform/epub/spine.rb +0 -79
- data/lib/bookbinder/transform/epub/title.rb +0 -92
- data/lib/bookbinder/transform/epub/version.rb +0 -39
- data/lib/bookbinder/transform/media_ripper/cover_image.rb +0 -12
- data/lib/bookbinder/transform/media_ripper/eisbn.rb +0 -15
- data/lib/bookbinder/transform/media_ripper/metadata.rb +0 -27
- data/lib/bookbinder/transform/media_ripper/nav_toc.rb +0 -57
- data/lib/bookbinder/transform/media_ripper/publisher.rb +0 -15
- data/lib/bookbinder/transform/media_ripper/rendition.rb +0 -7
- data/lib/bookbinder/transform/media_ripper/spine.rb +0 -34
- data/lib/bookbinder/transform/media_ripper/title.rb +0 -16
- data/lib/bookbinder/transform/mp3_audiobook/cover_image.rb +0 -25
- data/lib/bookbinder/transform/mp3_audiobook/creator.rb +0 -27
- data/lib/bookbinder/transform/mp3_audiobook/description.rb +0 -18
- data/lib/bookbinder/transform/mp3_audiobook/metadata.rb +0 -20
- data/lib/bookbinder/transform/mp3_audiobook/nav_toc.rb +0 -71
- data/lib/bookbinder/transform/mp3_audiobook/publisher.rb +0 -18
- data/lib/bookbinder/transform/mp3_audiobook/rendition.rb +0 -7
- data/lib/bookbinder/transform/mp3_audiobook/spine.rb +0 -33
- data/lib/bookbinder/transform/mp3_audiobook/subject.rb +0 -18
- data/lib/bookbinder/transform/mp3_audiobook/title.rb +0 -18
@@ -1,38 +0,0 @@
|
|
1
|
-
class Bookbinder::Transform::EPUB_Resources < Bookbinder::Transform
|
2
|
-
|
3
|
-
def dependencies
|
4
|
-
[Bookbinder::Transform::EPUB_OPF]
|
5
|
-
end
|
6
|
-
|
7
|
-
|
8
|
-
# Find all the items in the manifest -- except all spine items, the NCX,
|
9
|
-
# and the NAV.
|
10
|
-
#
|
11
|
-
def to_map(package)
|
12
|
-
opf_doc = package.file(:opf).document('r')
|
13
|
-
items = opf_doc.search('opf|manifest > opf|item')
|
14
|
-
package.map['resources'] = items.collect { |item|
|
15
|
-
path = package.make_path(item['href'])
|
16
|
-
{
|
17
|
-
'path' => path,
|
18
|
-
'media-type' => item['media-type'],
|
19
|
-
'id' => item['id'] || package.make_id(path)
|
20
|
-
}
|
21
|
-
}
|
22
|
-
end
|
23
|
-
|
24
|
-
|
25
|
-
def from_map(package)
|
26
|
-
return unless package.map['resources']
|
27
|
-
opf_doc = package.file(:opf).document
|
28
|
-
manifest_tag = opf_doc.find('opf|manifest')
|
29
|
-
package.map['resources'].each { |rsrc|
|
30
|
-
opf_doc.new_node('item', :append => manifest_tag) { |manifest_item_tag|
|
31
|
-
manifest_item_tag['href'] = package.make_href(rsrc['path'])
|
32
|
-
manifest_item_tag['media-type'] = rsrc['media-type']
|
33
|
-
manifest_item_tag['id'] = rsrc['id']
|
34
|
-
}
|
35
|
-
}
|
36
|
-
end
|
37
|
-
|
38
|
-
end
|
@@ -1,79 +0,0 @@
|
|
1
|
-
class Bookbinder::Transform::EPUB_Spine < Bookbinder::Transform
|
2
|
-
|
3
|
-
def dependencies
|
4
|
-
[Bookbinder::Transform::EPUB_Resources]
|
5
|
-
end
|
6
|
-
|
7
|
-
|
8
|
-
def to_map(package)
|
9
|
-
opf_doc = package.file(:opf).document('r')
|
10
|
-
itemrefs = opf_doc.search('opf|spine > opf|itemref')
|
11
|
-
package.map['spine'] = itemrefs.collect { |itemref|
|
12
|
-
cmpt = package.map['resources'].detect { |r| r['id'] == itemref['idref'] }
|
13
|
-
if cmpt
|
14
|
-
package.map['resources'].delete(cmpt)
|
15
|
-
cmpt['linear'] = itemref['linear'] == 'no' ? false : true
|
16
|
-
cmpt
|
17
|
-
else
|
18
|
-
package.warn("No manifest item for spine idref: #{itemref['idref']}")
|
19
|
-
end
|
20
|
-
}.compact
|
21
|
-
end
|
22
|
-
|
23
|
-
|
24
|
-
def from_map(package)
|
25
|
-
opf_doc = package.file(:opf).document
|
26
|
-
|
27
|
-
package.map['spine'].each { |cmpt|
|
28
|
-
# Convert component to valid XHTML
|
29
|
-
cmpt_file = package.file(cmpt['path'])
|
30
|
-
cmpt_doc = cmpt_file.document
|
31
|
-
# Add the EPUB namespace:
|
32
|
-
# FIXME: only add the EPUB namespace if there are any epub:* attrs?
|
33
|
-
cmpt_doc.add_namespace('epub')
|
34
|
-
|
35
|
-
# Update the OPF manifest
|
36
|
-
opf_doc.new_node('item', :append => 'opf|manifest') { |manifest_item_tag|
|
37
|
-
manifest_item_tag['href'] = package.make_href(cmpt['path'])
|
38
|
-
manifest_item_tag['id'] = cmpt['id']
|
39
|
-
manifest_item_tag['media-type'] = cmpt['media-type']
|
40
|
-
props = component_properties(cmpt, cmpt_doc)
|
41
|
-
manifest_item_tag['properties'] = props.join(' ') if props.any?
|
42
|
-
}
|
43
|
-
|
44
|
-
# Update the OPF spine
|
45
|
-
opf_doc.new_node('itemref', :append => 'opf|spine') { |spine_item_tag|
|
46
|
-
spine_item_tag['idref'] = cmpt['id']
|
47
|
-
spine_item_tag['linear'] = 'no' unless cmpt['linear']
|
48
|
-
}
|
49
|
-
}
|
50
|
-
end
|
51
|
-
|
52
|
-
|
53
|
-
protected
|
54
|
-
|
55
|
-
# Assemble the component properties by inspecting the HTML...
|
56
|
-
# because some reading systems can't do this themselves, I guess.
|
57
|
-
#
|
58
|
-
# NB: the 'cover-image' and 'nav' properties will be set in
|
59
|
-
# other tranforms.
|
60
|
-
#
|
61
|
-
def component_properties(cmpt, cmpt_doc)
|
62
|
-
[].tap { |props|
|
63
|
-
# 'scripted': check whether there are any script tags in the component.
|
64
|
-
props.push('scripted') if cmpt_doc.find('script')
|
65
|
-
|
66
|
-
# 'mathml': look for a math element
|
67
|
-
props.push('mathml') if cmpt_doc.find('math, mathml:math')
|
68
|
-
|
69
|
-
# 'remote-resources': TODO
|
70
|
-
|
71
|
-
# 'svg': look for an svg element
|
72
|
-
props.push('svg') if cmpt_doc.find('svg, svg:svg')
|
73
|
-
|
74
|
-
# 'switch': look for an epub:switch element
|
75
|
-
props.push('switch') if cmpt_doc.find('epub|switch')
|
76
|
-
}
|
77
|
-
end
|
78
|
-
|
79
|
-
end
|
@@ -1,92 +0,0 @@
|
|
1
|
-
# EPUB2 spec:
|
2
|
-
#
|
3
|
-
# http://www.idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.2.1
|
4
|
-
#
|
5
|
-
# EPUB3 spec:
|
6
|
-
#
|
7
|
-
# http://www.idpf.org/epub/30/spec/epub30-publications.html#sec-opf-dctitle
|
8
|
-
#
|
9
|
-
# On title types:
|
10
|
-
#
|
11
|
-
# "When the title-type value is drawn from a code list or other formal
|
12
|
-
# enumeration, the scheme attribute should be attached to identify its source.
|
13
|
-
# When a scheme is not specified, Reading Systems should recognize the
|
14
|
-
# following title type values: main, subtitle, short, collection, edition
|
15
|
-
# and expanded."
|
16
|
-
#
|
17
|
-
class Bookbinder::Transform::EPUB_Title < Bookbinder::Transform
|
18
|
-
|
19
|
-
TITLE_TYPES = %w[main subtitle short collection edition expanded]
|
20
|
-
|
21
|
-
|
22
|
-
def dependencies
|
23
|
-
[Bookbinder::Transform::EPUB_Metadata, Bookbinder::Transform::EPUB_NCX]
|
24
|
-
end
|
25
|
-
|
26
|
-
|
27
|
-
def to_map(package)
|
28
|
-
package.map['title'] = titles = {}
|
29
|
-
return unless package.map['metadata']
|
30
|
-
return unless metadata_array = package.map['metadata']['title']
|
31
|
-
title_data = metadata_array.sort { |a, b|
|
32
|
-
if a['display-seq'] && b['display-seq']
|
33
|
-
a['display-seq']['@'].to_i <=> b['display-seq']['@'].to_i
|
34
|
-
else
|
35
|
-
0
|
36
|
-
end
|
37
|
-
}
|
38
|
-
title_data.each { |tdata|
|
39
|
-
t = tdata['@']
|
40
|
-
type = tdata['title-type'] ? tdata['title-type']['@'] : 'main'
|
41
|
-
next unless TITLE_TYPES.include?(type)
|
42
|
-
if titles[type]
|
43
|
-
package.warn("Existing title for '#{type}' - discarding '#{t}'")
|
44
|
-
else
|
45
|
-
titles[type] = t
|
46
|
-
metadata_array.delete(tdata)
|
47
|
-
end
|
48
|
-
}
|
49
|
-
# Now that we have "used" the raw metadata for titles, remove it.
|
50
|
-
package.map['metadata'].delete('title') if metadata_array.empty?
|
51
|
-
end
|
52
|
-
|
53
|
-
|
54
|
-
def from_map(package)
|
55
|
-
titles = package.map['title']
|
56
|
-
opf_doc = package.file(:opf).document
|
57
|
-
metadata_tag = opf_doc.find('opf|metadata')
|
58
|
-
seq = 0
|
59
|
-
titles.each_pair { |type, title|
|
60
|
-
title_tag = opf_doc.new_node('dc:title', :append => metadata_tag)
|
61
|
-
title_tag.content = title
|
62
|
-
if titles.length > 1
|
63
|
-
seq += 1
|
64
|
-
title_id = "dc-title-metadata-#{seq}"
|
65
|
-
title_tag['id'] = title_id
|
66
|
-
opf_doc.new_node('meta', :append => metadata_tag) { |type_meta_tag|
|
67
|
-
type_meta_tag.content = type
|
68
|
-
type_meta_tag['property'] = 'title-type'
|
69
|
-
type_meta_tag['refines'] = '#'+title_id
|
70
|
-
}
|
71
|
-
opf_doc.new_node('meta', :append => metadata_tag) { |seq_meta_tag|
|
72
|
-
seq_meta_tag.content = seq
|
73
|
-
seq_meta_tag['property'] = 'display-seq'
|
74
|
-
seq_meta_tag['refines'] = '#'+title_id
|
75
|
-
}
|
76
|
-
end
|
77
|
-
}
|
78
|
-
|
79
|
-
# Add it to the NCX if that file exists in the package
|
80
|
-
package.if_file(:ncx) { |ncx_file|
|
81
|
-
ncx_title = [titles['main'], titles['subtitle']].compact.join(': ')
|
82
|
-
ncx_doc = ncx_file.document
|
83
|
-
ncx_doc.new_node('docTitle') { |doc_title_tag|
|
84
|
-
ncx_doc.new_node('text', :append => doc_title_tag) { |text_tag|
|
85
|
-
text_tag.content = ncx_title
|
86
|
-
}
|
87
|
-
ncx_doc.find('ncx|head').add_next_sibling(doc_title_tag)
|
88
|
-
}
|
89
|
-
}
|
90
|
-
end
|
91
|
-
|
92
|
-
end
|
@@ -1,39 +0,0 @@
|
|
1
|
-
# See https://itunesconnect.apple.com/docs/iBooksAssetGuide5.1Revision2.pdf
|
2
|
-
# pages 22 and 23.
|
3
|
-
#
|
4
|
-
# "The version of your book is specified within a `meta` element
|
5
|
-
# in the Package Document. The `meta` element has a property value
|
6
|
-
# of `ibooks:version':
|
7
|
-
#
|
8
|
-
# <meta property="ibooks:version">1.1.2</meta>
|
9
|
-
#
|
10
|
-
class Bookbinder::Transform::EPUB_Version < Bookbinder::Transform
|
11
|
-
|
12
|
-
def dependencies
|
13
|
-
[Bookbinder::Transform::EPUB_Metadata]
|
14
|
-
end
|
15
|
-
|
16
|
-
|
17
|
-
def to_map(package)
|
18
|
-
ver_hashes = package.map['metadata'].delete('ibooks:version')
|
19
|
-
if ver_hashes && ver_hashes.any?
|
20
|
-
package.map['version'] = ver_hashes.first['@']
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
|
25
|
-
def from_map(package)
|
26
|
-
if package.map['version']
|
27
|
-
# Add the ibooks prefix to the package root.
|
28
|
-
opf_doc = package.file(:opf).document
|
29
|
-
opf_doc.add_prefix('ibooks')
|
30
|
-
|
31
|
-
# Create the meta node and append it to <metadata>
|
32
|
-
opf_doc.new_node('meta', :append => 'opf|metadata') { |ver_tag|
|
33
|
-
ver_tag['property'] = 'ibooks:version'
|
34
|
-
ver_tag.content = package.map['version']
|
35
|
-
}
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
end
|
@@ -1,15 +0,0 @@
|
|
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
|
@@ -1,27 +0,0 @@
|
|
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
|
@@ -1,57 +0,0 @@
|
|
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
|
-
package.xml_paths.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
|
-
# MediaMarker time string comes in like:
|
36
|
-
#
|
37
|
-
# 74:48.000
|
38
|
-
#
|
39
|
-
# We should translate to 74*60+48.0 ==> 4488
|
40
|
-
#
|
41
|
-
def translate_time(time_str)
|
42
|
-
if match = time_str.match(/^(\d+):(\d+):([\d\.]+)$/)
|
43
|
-
minutes = match[1].to_i*60 + match[2].to_i
|
44
|
-
seconds = match[3].to_f
|
45
|
-
elsif match = time_str.match(/^(\d+):([\d\.]+)$/)
|
46
|
-
minutes = match[1].to_i
|
47
|
-
seconds = match[2].to_f
|
48
|
-
else
|
49
|
-
puts("Unknown time format: #{time_str}")
|
50
|
-
return nil
|
51
|
-
end
|
52
|
-
seconds = seconds.round if seconds % 1 == 0
|
53
|
-
out = minutes * 60 + seconds
|
54
|
-
out > 0 ? out : nil
|
55
|
-
end
|
56
|
-
|
57
|
-
end
|
@@ -1,15 +0,0 @@
|
|
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
|
@@ -1,34 +0,0 @@
|
|
1
|
-
class Bookbinder::Transform::MediaRipper_Spine < Bookbinder::Transform
|
2
|
-
|
3
|
-
def to_map(package)
|
4
|
-
package.map['resources'] = []
|
5
|
-
package.map['spine'] = package.audio_paths.collect { |path|
|
6
|
-
{
|
7
|
-
'path' => path,
|
8
|
-
'media-type' => 'audio/mpeg'
|
9
|
-
}.tap { |cmpt|
|
10
|
-
break unless cmpt_info = audio_info(package, path)
|
11
|
-
cmpt.update('audio-duration' => cmpt_info.length)
|
12
|
-
cmpt.update('audio-bitrate' => cmpt_info.bitrate) unless cmpt_info.vbr
|
13
|
-
}
|
14
|
-
}
|
15
|
-
end
|
16
|
-
|
17
|
-
|
18
|
-
protected
|
19
|
-
|
20
|
-
def audio_info(package, path)
|
21
|
-
info = nil
|
22
|
-
begin
|
23
|
-
package.file_system.get_io(path) { |zip_io|
|
24
|
-
io = StringIO.new(zip_io.read)
|
25
|
-
Mp3Info.open(io) { |inf| info = inf }
|
26
|
-
}
|
27
|
-
rescue => e
|
28
|
-
# TODO - is there error handling to do here?
|
29
|
-
raise e
|
30
|
-
end
|
31
|
-
info
|
32
|
-
end
|
33
|
-
|
34
|
-
end
|
@@ -1,16 +0,0 @@
|
|
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
|