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