bookbinder 0.3.1 → 0.3.2

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