thingfish 0.5.0.pre20160707192835
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.
- checksums.yaml +7 -0
- data/.simplecov +7 -0
- data/History.rdoc +5 -0
- data/LICENSE +29 -0
- data/Manifest.txt +39 -0
- data/Procfile +4 -0
- data/README.rdoc +92 -0
- data/Rakefile +92 -0
- data/bin/tfprocessord +6 -0
- data/bin/thingfish +10 -0
- data/etc/thingfish.conf.example +26 -0
- data/lib/strelka/app/metadata.rb +38 -0
- data/lib/strelka/httprequest/metadata.rb +70 -0
- data/lib/thingfish.rb +43 -0
- data/lib/thingfish/behaviors.rb +263 -0
- data/lib/thingfish/datastore.rb +55 -0
- data/lib/thingfish/datastore/memory.rb +93 -0
- data/lib/thingfish/handler.rb +728 -0
- data/lib/thingfish/metastore.rb +55 -0
- data/lib/thingfish/metastore/memory.rb +201 -0
- data/lib/thingfish/mixins.rb +57 -0
- data/lib/thingfish/processor.rb +79 -0
- data/lib/thingfish/processor/mp3.rb +167 -0
- data/lib/thingfish/processordaemon.rb +16 -0
- data/lib/thingfish/spechelpers.rb +165 -0
- data/spec/data/APIC-1-image.mp3 +0 -0
- data/spec/data/APIC-2-images.mp3 +0 -0
- data/spec/data/PIC-1-image.mp3 +0 -0
- data/spec/data/PIC-2-images.mp3 +0 -0
- data/spec/helpers.rb +67 -0
- data/spec/spec.opts +4 -0
- data/spec/thingfish/datastore/memory_spec.rb +19 -0
- data/spec/thingfish/datastore_spec.rb +64 -0
- data/spec/thingfish/handler_spec.rb +838 -0
- data/spec/thingfish/metastore/memory_spec.rb +17 -0
- data/spec/thingfish/metastore_spec.rb +96 -0
- data/spec/thingfish/mixins_spec.rb +63 -0
- data/spec/thingfish/processor/mp3_spec.rb +50 -0
- data/spec/thingfish/processor_spec.rb +65 -0
- data/spec/thingfish_spec.rb +23 -0
- metadata +244 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'pluggability'
|
5
|
+
require 'strelka'
|
6
|
+
require 'strelka/mixins'
|
7
|
+
|
8
|
+
require 'thingfish' unless defined?( Thingfish )
|
9
|
+
require 'thingfish/mixins'
|
10
|
+
|
11
|
+
# The base class for storage mechanisms used by Thingfish to store its data
|
12
|
+
# blobs.
|
13
|
+
class Thingfish::Metastore
|
14
|
+
extend Pluggability,
|
15
|
+
Strelka::AbstractClass
|
16
|
+
include Thingfish::Normalization
|
17
|
+
|
18
|
+
|
19
|
+
# Pluggability API -- set the prefix for implementations of Metastore
|
20
|
+
plugin_prefixes 'thingfish/metastore'
|
21
|
+
|
22
|
+
# AbstractClass API -- register some virtual methods that must be implemented
|
23
|
+
# in subclasses
|
24
|
+
pure_virtual :oids,
|
25
|
+
:each_oid,
|
26
|
+
:save,
|
27
|
+
:search,
|
28
|
+
:fetch,
|
29
|
+
:fetch_value,
|
30
|
+
:fetch_related_oids,
|
31
|
+
:merge,
|
32
|
+
:include?,
|
33
|
+
:remove,
|
34
|
+
:remove_except,
|
35
|
+
:size
|
36
|
+
|
37
|
+
### Return a representation of the object as a String suitable for debugging.
|
38
|
+
def inspect
|
39
|
+
return "#<%p:%#016x %d objects>" % [
|
40
|
+
self.class,
|
41
|
+
self.object_id * 2,
|
42
|
+
self.size
|
43
|
+
]
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
### Provide transactional consistency to the provided block. Concrete metastores should
|
48
|
+
### override this if they can implement it. By default it's a no-op.
|
49
|
+
def transaction
|
50
|
+
yield
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
end # class Thingfish::Metastore
|
55
|
+
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'thingfish' unless defined?( Thingfish )
|
5
|
+
require 'thingfish/metastore' unless defined?( Thingfish::Metastore )
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
# An in-memory metastore for testing and tryout purposes.
|
10
|
+
class Thingfish::Metastore::Memory < Thingfish::Metastore
|
11
|
+
extend Loggability
|
12
|
+
include Thingfish::Normalization
|
13
|
+
|
14
|
+
# Loggability API -- log to the :thingfish logger
|
15
|
+
log_to :thingfish
|
16
|
+
|
17
|
+
|
18
|
+
### Create a new MemoryMetastore, using the given +storage+ object to store
|
19
|
+
### data in. The +storage+ should quack like a Hash.
|
20
|
+
def initialize( storage={} )
|
21
|
+
@storage = storage
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
##
|
26
|
+
# The raw Hash of metadata
|
27
|
+
attr_reader :storage
|
28
|
+
|
29
|
+
|
30
|
+
### Return an Array of all stored OIDs.
|
31
|
+
def oids
|
32
|
+
return @storage.keys
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
### Iterate over the OID of each entry in the store, yielding to the block if one is given
|
37
|
+
### or returning an Enumerator if one is not.
|
38
|
+
def each_oid( &block )
|
39
|
+
return @storage.each_key( &block )
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
### Save the +metadata+ Hash for the specified +oid+.
|
44
|
+
def save( oid, metadata )
|
45
|
+
oid = normalize_oid( oid )
|
46
|
+
@storage[ oid ] = metadata.dup
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
### Fetch the data corresponding to the given +oid+ as a Hash-ish object.
|
51
|
+
def fetch( oid, *keys )
|
52
|
+
oid = normalize_oid( oid )
|
53
|
+
metadata = @storage[ oid ] or return nil
|
54
|
+
|
55
|
+
if keys.empty?
|
56
|
+
self.log.debug "Fetching metadata for OID %s" % [ oid ]
|
57
|
+
return metadata.dup
|
58
|
+
else
|
59
|
+
self.log.debug "Fetching metadata for %p for OID %s" % [ keys, oid ]
|
60
|
+
keys = normalize_keys( keys )
|
61
|
+
values = metadata.values_at( *keys )
|
62
|
+
return Hash[ [keys, values].transpose ]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
### Fetch the value of the metadata associated with the given +key+ for the
|
68
|
+
### specified +oid+.
|
69
|
+
def fetch_value( oid, key )
|
70
|
+
oid = normalize_oid( oid )
|
71
|
+
key = normalize_key( key )
|
72
|
+
data = @storage[ oid ] or return nil
|
73
|
+
|
74
|
+
return data[ key ]
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
### Fetch OIDs related to the given +oid+.
|
79
|
+
def fetch_related_oids( oid )
|
80
|
+
oid = normalize_oid( oid )
|
81
|
+
self.log.debug "Fetching OIDs of resources related to %s" % [ oid ]
|
82
|
+
return self.search( :criteria => {:relation => oid}, :include_related => true )
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
### Search the metastore for UUIDs which match the specified +criteria+ and
|
87
|
+
### return them as an iterator.
|
88
|
+
def search( options={} )
|
89
|
+
ds = @storage.each_key
|
90
|
+
self.log.debug "Starting search with %p" % [ ds ]
|
91
|
+
|
92
|
+
ds = self.omit_related_resources( ds, options )
|
93
|
+
ds = self.apply_search_criteria( ds, options )
|
94
|
+
ds = self.apply_search_order( ds, options )
|
95
|
+
ds = self.apply_search_direction( ds, options )
|
96
|
+
ds = self.apply_search_limit( ds, options )
|
97
|
+
|
98
|
+
return ds.to_a
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
### Omit related resources from the search dataset +ds+ unless the given
|
103
|
+
### +options+ specify otherwise.
|
104
|
+
def omit_related_resources( ds, options )
|
105
|
+
unless options[:include_related]
|
106
|
+
ds = ds.reject {|uuid| @storage[uuid]['relationship'] }
|
107
|
+
end
|
108
|
+
return ds
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
### Apply the search :criteria from the specified +options+ to the collection
|
113
|
+
### in +ds+ and return the modified dataset.
|
114
|
+
def apply_search_criteria( ds, options )
|
115
|
+
if (( criteria = options[:criteria] ))
|
116
|
+
criteria.each do |field, value|
|
117
|
+
self.log.debug " applying criteria: %p => %p" % [ field.to_s, value ]
|
118
|
+
ds = ds.select {|uuid| @storage[uuid][field.to_s] == value }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
return ds
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
### Apply the search :order from the specified +options+ to the collection in
|
127
|
+
### +ds+ and return the modified dataset.
|
128
|
+
def apply_search_order( ds, options )
|
129
|
+
if (( fields = options[:order] ))
|
130
|
+
ds = ds.to_a.sort_by do |uuid|
|
131
|
+
@storage[ uuid ].values_at( *fields.compact ).map {|val| val || ''}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
return ds
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
### Apply the search :direction from the specified +options+ to the collection
|
140
|
+
### in +ds+ and return the modified dataset.
|
141
|
+
def apply_search_direction( ds, options )
|
142
|
+
ds.reverse! if options[:direction] && options[:direction] == 'desc'
|
143
|
+
return ds
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
### Apply the search :limit from the specified +options+ to the collection in
|
148
|
+
### +ds+ and return the modified dataset.
|
149
|
+
def apply_search_limit( ds, options )
|
150
|
+
if (( limit = options[:limit] ))
|
151
|
+
self.log.debug " limiting to %s results" % [ limit ]
|
152
|
+
offset = options[:offset] || 0
|
153
|
+
ds = ds.to_a.slice( offset, limit )
|
154
|
+
end
|
155
|
+
|
156
|
+
return ds
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
### Update the metadata for the given +oid+ with the specified +values+ hash.
|
161
|
+
def merge( oid, values )
|
162
|
+
oid = normalize_oid( oid )
|
163
|
+
values = normalize_keys( values )
|
164
|
+
@storage[ oid ].merge!( values )
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
### Remove all metadata associated with +oid+ from the Metastore.
|
169
|
+
def remove( oid, *keys )
|
170
|
+
oid = normalize_oid( oid )
|
171
|
+
if keys.empty?
|
172
|
+
@storage.delete( oid )
|
173
|
+
else
|
174
|
+
keys = normalize_keys( keys )
|
175
|
+
@storage[ oid ].delete_if {|key, _| keys.include?(key) }
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
### Remove all metadata associated with +oid+ except for the specified +keys+.
|
181
|
+
def remove_except( oid, *keys )
|
182
|
+
oid = normalize_oid( oid )
|
183
|
+
keys = normalize_keys( keys )
|
184
|
+
@storage[ oid ].keep_if {|key,_| keys.include?(key) }
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
### Returns +true+ if the metastore has metadata associated with the specified +oid+.
|
189
|
+
def include?( oid )
|
190
|
+
oid = normalize_oid( oid )
|
191
|
+
return @storage.include?( oid )
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
### Returns the number of objects the store contains.
|
196
|
+
def size
|
197
|
+
return @storage.size
|
198
|
+
end
|
199
|
+
|
200
|
+
end # class Thingfish::Metastore::Memory
|
201
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# vim: set nosta noet ts=4 sw=4:
|
3
|
+
# encoding: utf-8
|
4
|
+
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
require 'thingfish' unless defined?( Thingfish )
|
8
|
+
|
9
|
+
module Thingfish
|
10
|
+
|
11
|
+
# A collection of functions for dealing with object IDs.
|
12
|
+
module Normalization
|
13
|
+
|
14
|
+
###############
|
15
|
+
module_function
|
16
|
+
###############
|
17
|
+
|
18
|
+
### Generate a new object ID.
|
19
|
+
def make_object_id
|
20
|
+
return normalize_oid( SecureRandom.uuid )
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
### Normalize the given +oid+.
|
25
|
+
def normalize_oid( oid )
|
26
|
+
return oid.to_s.downcase
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
### Return a copy of the given +collection+ after being normalized.
|
31
|
+
def normalize_keys( collection )
|
32
|
+
if collection.respond_to?( :keys )
|
33
|
+
return collection.each_with_object({}) do |(key,val),new_hash|
|
34
|
+
n_key = normalize_key( key )
|
35
|
+
new_hash[ n_key ] = val
|
36
|
+
end
|
37
|
+
|
38
|
+
elsif collection.respond_to?( :map )
|
39
|
+
return collection.map {|key| normalize_key(key) }
|
40
|
+
end
|
41
|
+
|
42
|
+
return nil
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
### Return a normalized copy of +key+.
|
47
|
+
def normalize_key( key )
|
48
|
+
return key.to_s.downcase.gsub( /[^\w:]+/, '_' )
|
49
|
+
end
|
50
|
+
|
51
|
+
end # module Normalization
|
52
|
+
|
53
|
+
|
54
|
+
end # module Thingfish
|
55
|
+
|
56
|
+
# vim: set nosta noet ts=4 sw=4:
|
57
|
+
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'pluggability'
|
5
|
+
require 'strelka/httprequest/acceptparams'
|
6
|
+
|
7
|
+
require 'thingfish' unless defined?( Thingfish )
|
8
|
+
|
9
|
+
|
10
|
+
# Thingfish asset processor base class.
|
11
|
+
class Thingfish::Processor
|
12
|
+
extend Pluggability
|
13
|
+
|
14
|
+
|
15
|
+
plugin_prefixes 'thingfish/processor'
|
16
|
+
|
17
|
+
|
18
|
+
### Get/set the list of media types this processor can handle.
|
19
|
+
def self::handled_types( *mediatypes )
|
20
|
+
if mediatypes.empty?
|
21
|
+
@handled_types ||= []
|
22
|
+
else
|
23
|
+
@handled_types = mediatypes.collect do |type|
|
24
|
+
Strelka::HTTPRequest::MediaType.parse(type)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
return @handled_types
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
### Filter hook for request, pass to processor if it is able to
|
33
|
+
### handle the +request+ content type.
|
34
|
+
def process_request( request )
|
35
|
+
return unless self.handled_path?( request )
|
36
|
+
if self.handled_type?( request.content_type )
|
37
|
+
on_request( request )
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
### Process the data and/or metadata in the +request+.
|
43
|
+
def on_request( request )
|
44
|
+
# No-op by default
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
### Filter hook for response, pass to processor if it is able to
|
49
|
+
### handle the +response+ content type.
|
50
|
+
def process_response( response )
|
51
|
+
return unless self.handled_path?( response.request )
|
52
|
+
if self.handled_type?( response.content_type )
|
53
|
+
on_response( response )
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
### Process the data and/or metadata in the +response+.
|
59
|
+
def on_response( response )
|
60
|
+
# No-op by default
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
### Returns +true+ if the given media +type+ is one the processor handles.
|
65
|
+
def handled_type?( type )
|
66
|
+
return true if self.class.handled_types.empty?
|
67
|
+
self.class.handled_types.find {|handled_type| type =~ handled_type }
|
68
|
+
end
|
69
|
+
alias_method :is_handled_type?, :handled_type?
|
70
|
+
|
71
|
+
|
72
|
+
### Returns +true+ if the given +request+'s path is one that should
|
73
|
+
### be processed.
|
74
|
+
def handled_path?( request )
|
75
|
+
return ! request.path.match( %r|^/?[\w\-]+/metadata| )
|
76
|
+
end
|
77
|
+
|
78
|
+
end # class Thingfish::Processor
|
79
|
+
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'mp3info'
|
5
|
+
|
6
|
+
require 'thingfish' unless defined?( Thingfish )
|
7
|
+
require 'thingfish/processor' unless defined?( Thingfish::Processor )
|
8
|
+
|
9
|
+
|
10
|
+
# Attach ID3 data to an mp3, along with any embedded album art as a related resource.
|
11
|
+
class Thingfish::Processor::MP3 < Thingfish::Processor
|
12
|
+
extend Loggability
|
13
|
+
|
14
|
+
# Loggability API -- log to the :thingfish logger
|
15
|
+
log_to :thingfish
|
16
|
+
|
17
|
+
# The list of handled types
|
18
|
+
handled_types 'audio/mpeg', 'audio/mpg', 'audio/mp3'
|
19
|
+
|
20
|
+
|
21
|
+
# Null character
|
22
|
+
NULL = "\x0"
|
23
|
+
|
24
|
+
# Attached picture "PIC"
|
25
|
+
# Frame size $xx xx xx
|
26
|
+
# ---- mp3info is 'helpfully' cropping out frame size.
|
27
|
+
# Text encoding $xx
|
28
|
+
# Image format $xx xx xx
|
29
|
+
# Picture type $xx
|
30
|
+
# Description <textstring> $00 (00)
|
31
|
+
# Picture data <binary data>
|
32
|
+
PIC_FORMAT = 'h a3 h Z* a*'
|
33
|
+
|
34
|
+
# Attached picture "APIC"
|
35
|
+
# Text encoding $xx
|
36
|
+
# MIME type <text string> $00
|
37
|
+
# Picture type $xx
|
38
|
+
# Description <text string according to encoding> $00 (00)
|
39
|
+
# Picture data <binary data>
|
40
|
+
APIC_FORMAT = 'h Z* h Z* a*'
|
41
|
+
|
42
|
+
|
43
|
+
### Synchronous processor API -- extract metadata from uploaded MP3s
|
44
|
+
def on_request( request )
|
45
|
+
mp3info = Mp3Info.new( request.body )
|
46
|
+
|
47
|
+
mp3_metadata = self.extract_id3_metadata( mp3info )
|
48
|
+
request.add_metadata( mp3_metadata )
|
49
|
+
|
50
|
+
self.extract_images( mp3info ) do |imageio, metadata|
|
51
|
+
metadata[:title] = "Album art for %s - %s" % mp3_metadata.values_at( 'mp3:artist', 'mp3:title' )
|
52
|
+
request.add_related_resource( imageio, metadata )
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
### Normalize metadata from the MP3Info object and return it as a hash.
|
58
|
+
def extract_id3_metadata( mp3info )
|
59
|
+
self.log.debug "Extracting MP3 metadata"
|
60
|
+
|
61
|
+
mp3_metadata = {
|
62
|
+
'mp3:frequency' => mp3info.samplerate,
|
63
|
+
'mp3:bitrate' => mp3info.bitrate,
|
64
|
+
'mp3:vbr' => mp3info.vbr,
|
65
|
+
'mp3:title' => mp3info.tag.title,
|
66
|
+
'mp3:artist' => mp3info.tag.artist,
|
67
|
+
'mp3:album' => mp3info.tag.album,
|
68
|
+
'mp3:year' => mp3info.tag.year,
|
69
|
+
'mp3:genre' => mp3info.tag.genre,
|
70
|
+
'mp3:tracknum' => mp3info.tag.tracknum,
|
71
|
+
'mp3:comments' => mp3info.tag.comments,
|
72
|
+
}
|
73
|
+
|
74
|
+
# ID3V2 2.2.0 has three-letter tags, so map those if the artist info isn't set
|
75
|
+
if mp3info.hastag2?
|
76
|
+
if mp3_metadata['mp3:artist'].nil?
|
77
|
+
self.log.debug " extracting old-style ID3v2 info" % [mp3info.tag2.version]
|
78
|
+
|
79
|
+
mp3_metadata.merge!({
|
80
|
+
'mp3:title' => mp3info.tag2.TT2,
|
81
|
+
'mp3:artist' => mp3info.tag2.TP1,
|
82
|
+
'mp3:album' => mp3info.tag2.TAL,
|
83
|
+
'mp3:year' => mp3info.tag2.TYE,
|
84
|
+
'mp3:tracknum' => mp3info.tag2.TRK,
|
85
|
+
'mp3:comments' => mp3info.tag2.COM,
|
86
|
+
'mp3:genre' => mp3info.tag2.TCO,
|
87
|
+
})
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
self.log.debug " raw metadata: %p" % [ mp3_metadata ]
|
92
|
+
return sanitize_values( mp3_metadata )
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
### Extract image data from ID3 information, supports both APIC (2.3) and the older style
|
97
|
+
### PIC (2.2). Return value is a hash with IO keys and mimetype values.
|
98
|
+
### {
|
99
|
+
### io => { format => 'image/jpeg' }
|
100
|
+
### io2 => { format => 'image/jpeg' }
|
101
|
+
### }
|
102
|
+
def extract_images( mp3info )
|
103
|
+
self.log.debug "Extracting embedded images"
|
104
|
+
raise LocalJumpError, "no block given" unless block_given?
|
105
|
+
|
106
|
+
unless mp3info.hastag2?
|
107
|
+
self.log.debug "...no id3v2 tag, so no embedded images possible."
|
108
|
+
return
|
109
|
+
end
|
110
|
+
|
111
|
+
self.log.debug "...id3v2 tag present..."
|
112
|
+
|
113
|
+
if mp3info.tag2.APIC
|
114
|
+
self.log.debug "...extracting APIC (id3v2.3+) image data."
|
115
|
+
|
116
|
+
images = [ mp3info.tag2.APIC ].flatten
|
117
|
+
images.each do |img|
|
118
|
+
blob, mime = img.unpack( APIC_FORMAT ).values_at( 4, 1 )
|
119
|
+
yield( StringIO.new(blob),
|
120
|
+
:format => mime,
|
121
|
+
:extent => blob.length,
|
122
|
+
:relationship => 'album-art' )
|
123
|
+
end
|
124
|
+
|
125
|
+
elsif mp3info.tag2.PIC
|
126
|
+
self.log.debug "...extracting PIC (id3v2.2) image data."
|
127
|
+
|
128
|
+
images = [ mp3info.tag2.PIC ].flatten
|
129
|
+
images.each do |img|
|
130
|
+
blob, type = img.unpack( PIC_FORMAT ).values_at( 4, 1 )
|
131
|
+
mime = Mongrel2::Config.mimetypes[ ".#{type.downcase}" ] or next
|
132
|
+
yield( StringIO.new(blob),
|
133
|
+
:format => mime,
|
134
|
+
:extent => blob.length,
|
135
|
+
:relationship => 'album-art' )
|
136
|
+
end
|
137
|
+
|
138
|
+
else
|
139
|
+
self.log.debug "...no known image tag types in tags: %p" % [ mp3info.tag2.keys.sort ]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
|
145
|
+
#######
|
146
|
+
private
|
147
|
+
#######
|
148
|
+
|
149
|
+
### Strip NULLs from the values of the given +metadata_hash+ and return it.
|
150
|
+
def sanitize_values( metadata_hash )
|
151
|
+
metadata_hash.each do |k,v|
|
152
|
+
case v
|
153
|
+
when String
|
154
|
+
metadata_hash[k] = v.chomp(NULL).strip
|
155
|
+
when Array
|
156
|
+
metadata_hash[k] = v.collect {|vv| vv.chomp(NULL).strip }
|
157
|
+
when Numeric, TrueClass, FalseClass
|
158
|
+
# No-op
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
return metadata_hash.delete_if {|_,v| v.nil? }
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
end # class Thingfish::Processor::MP3
|
167
|
+
|