bookbinder 0.3.1 → 0.3.2

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.
@@ -25,6 +25,11 @@ class Bookbinder::FileSystem::Directory
25
25
  end
26
26
 
27
27
 
28
+ def get_io(path, &blk)
29
+ get_file(path, 'r', &blk)
30
+ end
31
+
32
+
28
33
  def get_file(path, mode = 'r', &blk)
29
34
  fpath = full_path(path)
30
35
  must_exist(fpath) if mode[0] != 'w'
@@ -22,6 +22,11 @@ class Bookbinder::FileSystem::Memory
22
22
  end
23
23
 
24
24
 
25
+ def get_io(path, &blk)
26
+ blk.call(StringIO.new(read(path)))
27
+ end
28
+
29
+
25
30
  # Creates a tempfile so you can do memory-efficient stuff.
26
31
  #
27
32
  def get_file(path, mode = 'r', &blk)
@@ -26,6 +26,11 @@ class Bookbinder::FileSystem::ZipFile < Bookbinder::FileSystem
26
26
  end
27
27
 
28
28
 
29
+ def get_io(path, &blk)
30
+ @zipfile.get_entry(path).get_input_stream(&blk)
31
+ end
32
+
33
+
29
34
  def get_file(path, mode = 'r', &blk)
30
35
  read_before = mode[0] != 'w'
31
36
  write_after = mode[0] != 'r'
@@ -15,6 +15,11 @@ class Bookbinder::FileSystem
15
15
  end
16
16
 
17
17
 
18
+ def get_io(path, &blk)
19
+ # IMPLEMENT IN SUBCLASS
20
+ end
21
+
22
+
18
23
  def get_file(path, mode = 'r', &blk)
19
24
  # IMPLEMENT IN SUBCLASS
20
25
  end
@@ -2,12 +2,19 @@ class Bookbinder::Operations
2
2
 
3
3
  class << self
4
4
 
5
+ def package_classes
6
+ ObjectSpace.each_object(Class).select { |klass|
7
+ klass < Bookbinder::Package
8
+ }
9
+ end
10
+
11
+
5
12
  # This inspects a path and gives you the package class that
6
13
  # probably can read or write it.
7
14
  #
8
15
  def recognize(path)
9
- ObjectSpace.each_object(Class).detect { |klass|
10
- klass < Bookbinder::Package && klass.recognize(path)
16
+ package_classes.detect { |klass|
17
+ klass.recognize(path)
11
18
  } || raise(Bookbinder::UnknownFormat, path)
12
19
  end
13
20
 
@@ -33,12 +33,7 @@ class Bookbinder::Package::EPUB < Bookbinder::Package
33
33
 
34
34
 
35
35
  def self.transforms
36
- @transforms = DEFAULT_TRANSFORMS
37
- end
38
-
39
-
40
- def transforms
41
- self.class.transforms
36
+ @transforms ||= DEFAULT_TRANSFORMS
42
37
  end
43
38
 
44
39
 
@@ -22,12 +22,26 @@ class Bookbinder::Package::MediaRipper < Bookbinder::Package
22
22
 
23
23
 
24
24
  def self.transforms
25
- @transforms = DEFAULT_TRANSFORMS
25
+ @transforms ||= DEFAULT_TRANSFORMS
26
26
  end
27
27
 
28
28
 
29
- def transforms
30
- self.class.transforms
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!
31
45
  end
32
46
 
33
47
  end
@@ -0,0 +1,43 @@
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
@@ -16,8 +16,8 @@ class Bookbinder::Package::Openbook < Bookbinder::Package
16
16
  end
17
17
 
18
18
 
19
- def transforms
20
- [
19
+ def self.transforms
20
+ @transforms ||= [
21
21
  Bookbinder::Transform::Generator,
22
22
  Bookbinder::Transform::Openbook_JSON
23
23
  ]
@@ -29,6 +29,11 @@ class Bookbinder::Package
29
29
  end
30
30
 
31
31
 
32
+ def self.transforms
33
+ [] # Implement in subclasses
34
+ end
35
+
36
+
32
37
  # Creates a new package of this class pointing at the given
33
38
  # path (but not reading from it automatically).
34
39
  #
@@ -188,8 +193,7 @@ class Bookbinder::Package
188
193
  # this package.
189
194
  #
190
195
  def transforms
191
- # IMPLEMENT IN SUBCLASSES
192
- []
196
+ self.class.transforms
193
197
  end
194
198
 
195
199
 
@@ -14,7 +14,7 @@ class Bookbinder::Transform::MediaRipper_NavToc < Bookbinder::Transform
14
14
 
15
15
  def to_map(package)
16
16
  toc = []
17
- xml_paths_in_package(package).each { |xml_path|
17
+ package.xml_paths.each { |xml_path|
18
18
  doc = package.file(xml_path).document('r')
19
19
  markers_tag = doc.find('AudioBook > Markers')
20
20
  aud_path = markers_tag['file']
@@ -32,16 +32,6 @@ class Bookbinder::Transform::MediaRipper_NavToc < Bookbinder::Transform
32
32
 
33
33
  protected
34
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
35
  # MediaMarker time string comes in like:
46
36
  #
47
37
  # 74:48.000
@@ -2,7 +2,7 @@ class Bookbinder::Transform::MediaRipper_Spine < Bookbinder::Transform
2
2
 
3
3
  def to_map(package)
4
4
  package.map['resources'] = []
5
- package.map['spine'] = audio_paths_in_package(package).collect { |path|
5
+ package.map['spine'] = package.audio_paths.collect { |path|
6
6
  {
7
7
  'path' => path,
8
8
  'media-type' => package.file(path).media_type,
@@ -14,30 +14,12 @@ class Bookbinder::Transform::MediaRipper_Spine < Bookbinder::Transform
14
14
 
15
15
  protected
16
16
 
17
- def audio_paths_in_package(package)
18
- xml_paths_in_package(package).collect { |xml_path|
19
- next unless doc = package.file(xml_path).document('r')
20
- next unless markers_tag = doc.find('AudioBook > Markers')
21
- markers_tag['file']
22
- }.compact
23
- end
24
-
25
-
26
- def xml_paths_in_package(package)
27
- xml_paths = []
28
- package.file_system.each { |path|
29
- next unless match = path.match(/Part(\d+)\.xml$/)
30
- xml_paths[match[1].to_i] = path
31
- }
32
- xml_paths.compact!
33
- end
34
-
35
-
36
17
  def audio_length(package, path)
37
18
  length = 0
38
19
  begin
39
- package.file(path).get_file { |f|
40
- Mp3Info.open(f) { |info| length = info.length }
20
+ package.file_system.get_io(path) { |zip_io|
21
+ io = StringIO.new(zip_io.read(4096))
22
+ Mp3Info.open(io) { |info| length = info.length }
41
23
  }
42
24
  rescue => e
43
25
  # TODO - is there error handling to do here?
@@ -0,0 +1,25 @@
1
+ class Bookbinder::Transform::MP3Audiobook_CoverImage < Bookbinder::Transform
2
+
3
+ TAG = 'APIC'
4
+ PATH = 'cover.jpg'
5
+
6
+ def to_map(package)
7
+ return unless path = package.audio_paths.first
8
+ package.file(path).get_file { |mp3|
9
+ Mp3Info.open(mp3) { |info|
10
+ info.tag2.pictures.each { |desc, data|
11
+ parts = [File.basename(desc), File.extname(desc)]
12
+ path = Dir::Tmpname.create(parts) {}
13
+ File.binwrite(path, data)
14
+ package.file_system.set_file(desc, File.new(path))
15
+ package.map['cover'] = { 'front' => {
16
+ 'path' => desc,
17
+ 'media-type' => Bookbinder::MediaType.of(desc)
18
+ } }
19
+ return
20
+ }
21
+ }
22
+ }
23
+ end
24
+
25
+ end
@@ -0,0 +1,27 @@
1
+ class Bookbinder::Transform::MP3Audiobook_Creator < Bookbinder::Transform
2
+
3
+ TAG = 'TPE1'
4
+
5
+
6
+ def dependencies
7
+ [Bookbinder::Transform::MP3Audiobook_Metadata]
8
+ end
9
+
10
+
11
+ def to_map(package)
12
+ return unless md = package.map['metadata']
13
+ return unless md[TAG] && md[TAG].first
14
+ creator = md[TAG].first['@']
15
+ md.delete(TAG)
16
+ creators = creator.split('/')
17
+ package.map['creator'] = [{
18
+ 'name' => creators.shift,
19
+ 'role' => 'aut'
20
+ }]
21
+ package.map['creator'].push({
22
+ 'name' => creators.shift,
23
+ 'role' => 'nrt'
24
+ }) if creators.any?
25
+ end
26
+
27
+ end
@@ -0,0 +1,18 @@
1
+ class Bookbinder::Transform::MP3Audiobook_Description < Bookbinder::Transform
2
+
3
+ TAG = 'COMM'
4
+
5
+
6
+ def dependencies
7
+ [Bookbinder::Transform::MP3Audiobook_Metadata]
8
+ end
9
+
10
+
11
+ def to_map(package)
12
+ return unless md = package.map['metadata']
13
+ return unless md[TAG] && md[TAG].first
14
+ package.map['description'] = { 'full' => md[TAG].first['@'] }
15
+ md.delete(TAG)
16
+ end
17
+
18
+ end
@@ -0,0 +1,20 @@
1
+ class Bookbinder::Transform::MP3Audiobook_Metadata < Bookbinder::Transform
2
+
3
+ SKIP_FIELDS = ['TRCK', 'TENC', 'APIC']
4
+
5
+
6
+ def to_map(package)
7
+ return unless path = package.audio_paths.first
8
+ md = {}
9
+ package.file(path).get_file { |mp3|
10
+ Mp3Info.open(mp3) { |info|
11
+ info.tag2.each_pair { |name, value|
12
+ next if value == 'null' || SKIP_FIELDS.include?(name)
13
+ md[name] = [md[name], { '@' => value }].flatten.compact
14
+ }
15
+ }
16
+ }
17
+ package.map['metadata'] = md
18
+ end
19
+
20
+ end
@@ -0,0 +1,58 @@
1
+ # TXXX MediaMarker XML format is like this:
2
+ #
3
+ # OverDrive MediaMarkers\\x00<Markers><Marker><Name>Part I - Chapter 1</Name><Time>0:00.000</Time></Marker>... etc ...</Markers>
4
+ #
5
+ class Bookbinder::Transform::MP3Audiobook_NavToc < Bookbinder::Transform
6
+
7
+ TAG = 'TXXX'
8
+ MEDIAMARKER_PREFIX_RE = /^.*MediaMarkers[^<]*/
9
+
10
+
11
+ def dependencies
12
+ [Bookbinder::Transform::MP3Audiobook_Metadata]
13
+ end
14
+
15
+
16
+ def to_map(package)
17
+ toc = []
18
+ package.audio_paths.each { |aud_path|
19
+ xml = nil
20
+ package.file(aud_path).get_file { |mp3|
21
+ Mp3Info.open(mp3) { |info| xml = info.tag2[TAG] }
22
+ }
23
+ next unless xml && xml.slice!(MEDIAMARKER_PREFIX_RE)
24
+ doc = Nokogiri::XML(xml)
25
+ doc.root.css('Marker').each { |marker_tag|
26
+ seconds = translate_time(marker_tag.at_css('Time').content)
27
+ toc << {
28
+ 'title' => marker_tag.at_css('Name').content.strip,
29
+ 'path' => "#{aud_path}#{seconds ? "##{seconds}" : ''}"
30
+ }
31
+ }
32
+ }
33
+ package.map['nav'] = { 'toc' => toc }
34
+ if txxx = package.map['metadata'][TAG]
35
+ txxx.delete_if { |val| val['@'].match(MEDIAMARKER_PREFIX_RE) }
36
+ end
37
+ package.map['metadata'].delete(TAG) if txxx.empty?
38
+ end
39
+
40
+
41
+ protected
42
+
43
+ # MediaMarker time string comes in like:
44
+ #
45
+ # 74:48.000
46
+ #
47
+ # We should translate to 74*60+48.0 ==> 4488
48
+ #
49
+ def translate_time(time_str)
50
+ match = time_str.match(/^(\d+):([\d\.]+)$/)
51
+ minutes = match[1].to_i
52
+ seconds = match[2].to_f
53
+ seconds = seconds.round if seconds % 1 == 0
54
+ out = minutes * 60 + seconds
55
+ out > 0 ? out : nil
56
+ end
57
+
58
+ end
@@ -0,0 +1,18 @@
1
+ class Bookbinder::Transform::MP3Audiobook_Publisher < Bookbinder::Transform
2
+
3
+ TAG = 'TPUB'
4
+
5
+
6
+ def dependencies
7
+ [Bookbinder::Transform::MP3Audiobook_Metadata]
8
+ end
9
+
10
+
11
+ def to_map(package)
12
+ return unless md = package.map['metadata']
13
+ return unless md[TAG] && md[TAG].first
14
+ package.map['publisher'] = md[TAG].first['@']
15
+ md.delete(TAG)
16
+ end
17
+
18
+ end
@@ -0,0 +1,7 @@
1
+ class Bookbinder::Transform::MP3Audiobook_Rendition < Bookbinder::Transform
2
+
3
+ def to_map(package)
4
+ package.map['rendition-format'] = 'audiobook'
5
+ end
6
+
7
+ end
@@ -0,0 +1,30 @@
1
+ class Bookbinder::Transform::MP3Audiobook_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
+ 'audio-duration' => audio_length(package, path)
10
+ }
11
+ }
12
+ end
13
+
14
+
15
+ protected
16
+
17
+ def audio_length(package, path)
18
+ length = 0
19
+ begin
20
+ package.file(path).get_file { |mp3|
21
+ Mp3Info.open(mp3) { |info| length = info.length }
22
+ }
23
+ rescue => e
24
+ # TODO - is there error handling to do here?
25
+ raise e
26
+ end
27
+ length
28
+ end
29
+
30
+ end
@@ -0,0 +1,18 @@
1
+ class Bookbinder::Transform::MP3Audiobook_Subject < Bookbinder::Transform
2
+
3
+ TAG = 'TCON'
4
+
5
+
6
+ def dependencies
7
+ [Bookbinder::Transform::MP3Audiobook_Metadata]
8
+ end
9
+
10
+
11
+ def to_map(package)
12
+ return unless md = package.map['metadata']
13
+ return unless md[TAG] && md[TAG].first
14
+ package.map['subject'] = [md[TAG].collect { |h| h['@'] }]
15
+ md.delete(TAG)
16
+ end
17
+
18
+ end
@@ -0,0 +1,18 @@
1
+ class Bookbinder::Transform::MP3Audiobook_Title < Bookbinder::Transform
2
+
3
+ TAG = 'TALB'
4
+
5
+ def dependencies
6
+ [Bookbinder::Transform::MP3Audiobook_Metadata]
7
+ end
8
+
9
+
10
+ def to_map(package)
11
+ package.map['title'] = titles = {}
12
+ return unless md = package.map['metadata']
13
+ return unless md[TAG] && md[TAG].first
14
+ titles['main'] = md[TAG].first['@']
15
+ md.delete(TAG)
16
+ end
17
+
18
+ end
@@ -13,6 +13,7 @@ class Bookbinder::Transform::Organizer < Bookbinder::Transform
13
13
  eisbn
14
14
  pisbn
15
15
  publisher
16
+ subject
16
17
  rendition-*
17
18
  audio-*
18
19
  spine
@@ -1,5 +1,5 @@
1
1
  module Bookbinder
2
2
 
3
- VERSION = "0.3.1"
3
+ VERSION = "0.3.2"
4
4
 
5
5
  end
data/lib/bookbinder.rb CHANGED
@@ -29,3 +29,4 @@ require 'bookbinder/file_system/zip_file'
29
29
  require 'bookbinder/package/openbook'
30
30
  require 'bookbinder/package/epub'
31
31
  require 'bookbinder/package/media_ripper'
32
+ require 'bookbinder/package/mp3_audiobook'
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.3.1
4
+ version: 0.3.2
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: 2015-01-07 00:00:00.000000000 Z
12
+ date: 2015-01-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: nokogiri
16
- requirement: &22943580 !ruby/object:Gem::Requirement
16
+ requirement: &13080360 !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: *22943580
24
+ version_requirements: *13080360
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rubyzip
27
- requirement: &22940240 !ruby/object:Gem::Requirement
27
+ requirement: &13076700 !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: *22940240
35
+ version_requirements: *13076700
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: mime-types
38
- requirement: &22938680 !ruby/object:Gem::Requirement
38
+ requirement: &13075540 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *22938680
46
+ version_requirements: *13075540
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: ruby-mp3info
49
- requirement: &23389640 !ruby/object:Gem::Requirement
49
+ requirement: &13920580 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *23389640
57
+ version_requirements: *13920580
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rake
60
- requirement: &23389220 !ruby/object:Gem::Requirement
60
+ requirement: &13920160 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,7 +65,7 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *23389220
68
+ version_requirements: *13920160
69
69
  description: Convert easily between common ebook formats.
70
70
  email:
71
71
  - jpearson@overdrive.com
@@ -85,6 +85,7 @@ files:
85
85
  - lib/bookbinder/operations.rb
86
86
  - lib/bookbinder/package/epub.rb
87
87
  - lib/bookbinder/package/media_ripper.rb
88
+ - lib/bookbinder/package/mp3_audiobook.rb
88
89
  - lib/bookbinder/package/openbook.rb
89
90
  - lib/bookbinder/package.rb
90
91
  - lib/bookbinder/transform/epub/audio_overlay.rb
@@ -116,6 +117,16 @@ files:
116
117
  - lib/bookbinder/transform/media_ripper/rendition.rb
117
118
  - lib/bookbinder/transform/media_ripper/spine.rb
118
119
  - lib/bookbinder/transform/media_ripper/title.rb
120
+ - lib/bookbinder/transform/mp3_audiobook/cover_image.rb
121
+ - lib/bookbinder/transform/mp3_audiobook/creator.rb
122
+ - lib/bookbinder/transform/mp3_audiobook/description.rb
123
+ - lib/bookbinder/transform/mp3_audiobook/metadata.rb
124
+ - lib/bookbinder/transform/mp3_audiobook/nav_toc.rb
125
+ - lib/bookbinder/transform/mp3_audiobook/publisher.rb
126
+ - lib/bookbinder/transform/mp3_audiobook/rendition.rb
127
+ - lib/bookbinder/transform/mp3_audiobook/spine.rb
128
+ - lib/bookbinder/transform/mp3_audiobook/subject.rb
129
+ - lib/bookbinder/transform/mp3_audiobook/title.rb
119
130
  - lib/bookbinder/transform/openbook/json.rb
120
131
  - lib/bookbinder/transform/organizer.rb
121
132
  - lib/bookbinder/transform.rb