mediafile 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 119b0a54c4c60f5422faa73912fefbcfd97390ec
4
+ data.tar.gz: b7d0d780c5a49fe57b7adbd9125723316d8bfa3b
5
+ SHA512:
6
+ metadata.gz: 9995f98f87546a394e5d26494442d1ecd3109a297d8e7596f36494ff41ff3b7eff4484fc23a17984a7ebdb42d65989271d4b5c6d702411632234b407ee72902d
7
+ data.tar.gz: 99e25a1163b071e326826a490b26e145b68fd67522b40ba591d2b8b1ff921fd37dc0fa856a3ad7d9d1d6cb51cc18a98d989f9fa85897924bc4c25cbc6c25c628
data/bin/music_cp ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env ruby
2
+ # vim:et sw=2 ts=2
3
+ #
4
+
5
+ require 'fileutils'
6
+ require 'optparse'
7
+ require 'digest/md5'
8
+ require 'timeout'
9
+ require 'mediafile'
10
+
11
+ PROGNAME = File.basename($0)
12
+
13
+ def die(msg)
14
+ abort "#{PROGNAME}: #{msg}"
15
+ end
16
+
17
+ kill = me = now = false
18
+ files = []
19
+ dest = "."
20
+ verbose = false
21
+ progress = true
22
+ count = `grep -c '^processor' /proc/cpuinfo`.strip.to_i/2|1
23
+ transcode = { flac: :mp3, wav: :mp3 }
24
+ exclude_patterns = []
25
+ file_types = "{flac,mp3,MP3,FLAC,wav,WAV,m4a,M4A}"
26
+ opts = OptionParser.new do |opt|
27
+
28
+ opt.on("-f", "--file FILE|DIR", "File or directory to copy.",
29
+ "If given a directory, will grab all files within non-recursively") do |e|
30
+ e.split(",").each do |f|
31
+ if File.file? f
32
+ files << f
33
+ elsif File.directory? f
34
+ files.concat Dir.glob( f + "/*.#{file_types}")
35
+ else
36
+ warn "#{f} is not a file or a directory!"
37
+ end
38
+ end
39
+ end
40
+ opt.on("-r", "--recursive DIR", "Directory to recursively scan and copy") do |r|
41
+ raise "Directory '#{r}' does not exist" unless File.directory? r
42
+ files.concat Dir.glob( r + "/**/*.#{file_types}")
43
+ end
44
+ opt.on("-d", "--destination PATH", "Where to copy file to. Default: '#{dest}'",
45
+ "Will be created if it doesn't exist.") do |d|
46
+ dest = d
47
+ end
48
+ opt.on("--transcode <from=to[,from1=to1]>", "A comma-seperated series of name=value pairs.",
49
+ "Default is #{transcode.to_a.map{|i| i.join("=")}.join(",")}") do |fmt|
50
+ kill = true
51
+ transcode = Hash[ *fmt.split(",").map{|e| e.split("=").map{ |t| t.downcase.to_sym } }.flatten ]
52
+ end
53
+ opt.on("-c", "--copy", "Turn off transcoding.") do
54
+ transcode = {}
55
+ me = true
56
+ end
57
+ opt.on("--[no-]progress", "Set show progress true/false. Default is #{progress}") do |t|
58
+ progress = t
59
+ end
60
+ opt.on("--exclude PATTERN", "-x PATTERN", String, "Exclude files that match the given pattern.",
61
+ "Can specify more than once, file is excluded if any pattern matches") do |p|
62
+ exclude_patterns << p
63
+ end
64
+ opt.on("-v", "--[no-]verbose", "Be verbose") do |v|
65
+ verbose = v
66
+ end
67
+ opt.on("-t", "--threads NUM",
68
+ "Number of threads to spawn, useful for transcoding. Default: #{count}" ) do |n|
69
+ count = n.to_i
70
+ end
71
+ opt.on_tail("-h", "--help", "Show this message") do
72
+ warn opt
73
+ exit
74
+ end
75
+ begin
76
+ opt.parse!
77
+ if kill and me
78
+ raise OptionParser::InvalidOption.new("--copy and --transcode are conflicting")
79
+ end
80
+ rescue OptionParser::InvalidOption
81
+ warn "#{PROGNAME}: #{$!}"
82
+ die opt
83
+ end
84
+ end
85
+
86
+ files = files.uniq.sort
87
+ if exclude_patterns.any?
88
+ pattern = Regexp.new(exclude_patterns.join("|"))
89
+ files.delete_if { |el| pattern.match el }
90
+ end
91
+ if files.empty?
92
+ warn "No file specified, exiting"
93
+ warn opts
94
+ exit
95
+ end
96
+ puts "Full list of files to transfer to #{dest}:"
97
+ files.each { |l| puts "\t"+l }
98
+ puts "#{files.count} files total"
99
+ puts "The following transcode table will be used:"
100
+ puts transcode.any? ? transcode : 'none'
101
+ puts "Do you wish procede? (Y/n)"
102
+ y = gets
103
+ if /n/i.match(y)
104
+ puts "User cancel."
105
+ exit
106
+ end
107
+ puts "Begin copy to #{dest}"
108
+ copier = MediaFile::BulkMediaCopy.new(files, destination_root: dest, verbose: verbose, transcode: transcode, progress: progress)
109
+
110
+ copier.run count
111
+
112
+ puts "Complete."
@@ -0,0 +1,148 @@
1
+ module MediaFile; class BulkMediaCopy
2
+ def initialize( source, destination_root: ".", verbose: false, progress: false, transcode: {} )
3
+ source = case source
4
+ when String
5
+ [source]
6
+ when Array
7
+ source
8
+ else
9
+ raise "Bad value for required first arg 'source': '#{source.class}'. Should be String or Array."
10
+ end
11
+
12
+ @copies = Hash.new { |h,k| h[k] = [] }
13
+ @destination_root = destination_root
14
+ @verbose = verbose
15
+ @progress = progress
16
+ @work = source.map { |s|
17
+ MediaFile.new(s,
18
+ base_dir: @destination_root,
19
+ verbose: @verbose,
20
+
21
+ printer: proc{ |msg| self.safe_print( msg ) }
22
+ )
23
+ }
24
+ @width = [@work.count.to_s.size, 2].max
25
+ @name_width = @work.max{ |a,b| a.name.size <=> b.name.size }.name.size
26
+ @transcode = transcode
27
+ @count = 0
28
+ @failed = []
29
+ end
30
+
31
+ def run(max=4)
32
+ puts "%#{@width + 8}s, %#{@width + 8}s,%#{@width + 8}s, %-#{@name_width}s => Destination Path" % [
33
+ "Remaining",
34
+ "Workers",
35
+ "Complete",
36
+ "File Name"
37
+ ]
38
+ puts "%#{@width}d ( 100%%), %#{@width}d (%4.1f%%), %#{@width}d ( 0.0%%)" % [
39
+ @work.count,
40
+ 0,
41
+ 0,
42
+ 0
43
+ ]
44
+ max > 1 ? mcopy(max) : scopy
45
+ dupes = @copies.select{ |k,a| a.size > 1 }
46
+ if dupes.any?
47
+ puts "dupes"
48
+ require 'pp'
49
+ pp dupes
50
+ end
51
+ if @failed.any?
52
+ puts "Some files timed out"
53
+ @failed.each { |f| puts f.to_s }
54
+ end
55
+ end
56
+
57
+ def safe_print(message='')
58
+ locked {
59
+ puts block_given? ? yield : message
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def locked
66
+ if @semaphore
67
+ @semaphore.synchronize {
68
+ yield
69
+ }
70
+ else
71
+ yield
72
+ end
73
+ end
74
+
75
+ def mcopy(max)
76
+ raise "Argument must repond to :times" unless max.respond_to? :times
77
+ raise "I haven't any work..." unless @work
78
+ require 'thread'
79
+ @semaphore = Mutex.new
80
+ queue = Queue.new
81
+ @work.each { |s| queue << s }
82
+ threads = []
83
+ max.times do
84
+ threads << Thread.new do
85
+ while ( s = queue.pop(true) rescue nil)
86
+ copy s
87
+ end
88
+ end
89
+ end
90
+ threads.each { |t| t.join }
91
+ @semaphore = nil
92
+ end
93
+
94
+ def scopy
95
+ raise "I haven't any work..." unless @work
96
+ @work.each do |f|
97
+ copy f
98
+ end
99
+ end
100
+
101
+ def copy(mediafile)
102
+ dest = mediafile.out_path transcode_table: @transcode
103
+ locked {
104
+ return unless copy_check? mediafile.source_md5, mediafile.source, dest
105
+ }
106
+
107
+ err = false
108
+ begin
109
+ mediafile.copy transcode_table: @transcode
110
+ rescue Timeout::Error
111
+ @failed << mediafile
112
+ err = true
113
+ end
114
+
115
+ locked {
116
+ @count += 1
117
+ if @progress
118
+ left = @work.count - @count
119
+ left_perc = left == 0 ? left : left.to_f / @work.count * 100
120
+ cur = @copies.count - @count
121
+ cur_perc = cur == 0 ? cur : cur.to_f / left * 100 # @work.count * 100
122
+ c = cur_perc == 100
123
+ finished = @count.to_f / @work.count * 100
124
+ f = finished == 100.0
125
+ puts "%#{@width}d (%4.1f%%), %#{@width}d (%4.#{c ? 0 : 1}f%%), %#{@width}d (%4.#{f ? 0 : 1}f%%) %-#{@name_width}s => %-s" % [
126
+ left,
127
+ left_perc,
128
+ cur,
129
+ cur_perc,
130
+ @count,
131
+ finished,
132
+ (mediafile.name + (err ? " **" : "") ),
133
+ mediafile.out_path(transcode_table:@transcode)
134
+ ]
135
+ end
136
+ }
137
+
138
+ end
139
+
140
+ def copy_check?(md5,name,dest)
141
+ # if multi-threaded, need to lock before calling
142
+ @copies[md5] << "#{name} => #{dest}"
143
+ # return true if this is the only one
144
+ @copies[md5].count == 1
145
+ end
146
+
147
+ end; end
148
+
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env ruby
2
+ # vim:et sw=2 ts=2
3
+
4
+ module MediaFile; class MediaFile
5
+
6
+ attr_reader :source, :type, :name, :base_dir
7
+
8
+ def initialize(path, verbose: false, base_dir: '.', printer: proc {|msg| puts msg})
9
+ @source = path
10
+ @base_dir = base_dir
11
+ @verbose = verbose
12
+ @name = File.basename( @source, File.extname( @source ) )
13
+ @type = path[/(\w+)$/].downcase.to_sym
14
+ @printer = printer
15
+ @destinations = Hash.new{ |k,v| k[v] = {} }
16
+ end
17
+
18
+ def source_md5
19
+ @source_md5 ||= Digest::MD5.hexdigest( @source )
20
+ end
21
+
22
+ def out_path(base_dir: @base_dir, transcode_table: {})
23
+ @destinations[base_dir][transcode_table] ||= File.join(
24
+ base_dir,
25
+ relative_path,
26
+ new_file_name,
27
+ ) << ".#{transcode_table[@type] || @type}"
28
+ end
29
+
30
+ def copy(dest: @base_dir, transcode_table: {})
31
+ destination = out_path base_dir: dest, transcode_table: transcode_table
32
+ temp_dest = tmp_path base_dir: dest, transcode_table: transcode_table
33
+ unless File.exists? destination
34
+ FileUtils.mkdir_p File.dirname destination
35
+ begin
36
+ if transcode_table.has_key? @type
37
+ transcode transcode_table, temp_dest
38
+ else
39
+ FileUtils.cp @source, temp_dest
40
+ end
41
+ FileUtils.mv temp_dest, destination
42
+ rescue => e
43
+ FileUtils.rm temp_dest if File.exists? temp_dest
44
+ raise e
45
+ end
46
+ end
47
+ end
48
+
49
+ def printit(msg)
50
+ @printer.call msg
51
+ end
52
+
53
+ def to_s
54
+ "#{@source}"
55
+ end
56
+
57
+ def self.tags(*args)
58
+ args.each do |arg|
59
+ define_method arg do
60
+ read_tags
61
+ instance_variable_get "@#{arg}"
62
+ end
63
+ end
64
+ end
65
+
66
+ tags :album, :artist, :album_artist, :title, :genre, :year, :track, :comment, :disc_number, :disc_total
67
+
68
+ private
69
+
70
+ def set_decoder()
71
+ case @type
72
+ when :flac
73
+ %W{flac -c -s -d #{@source}}
74
+ when :mp3
75
+ #%W{lame --decode #{@source} -}
76
+ %W{sox #{@source} -t wav -}
77
+ when :m4a
78
+ %W{ffmpeg -i #{@source} -f wav -}
79
+ when :wav
80
+ %W{cat #{@source}}
81
+ else
82
+ raise "Unknown type '#{@type}'. Cannot set decoder"
83
+ end
84
+ end
85
+
86
+ def set_encoder(to,destination)
87
+ comment = "; Transcoded by MediaFile on #{Time.now}"
88
+ case to
89
+ when :flac
90
+ raise "Please don't transcode to flac. It is broken right now"
91
+ %W{flac -7 -V -s -o #{destination}} +
92
+ (@artist ? ["-T", "artist=#{@artist}"] : [] ) +
93
+ (@title ? ["-T", "title=#{@title}"] : [] ) +
94
+ (@album ? ["-T", "album=#{@album}"] : [] ) +
95
+ (@track > 0 ? ["-T", "tracknumber=#{@track}"] : [] ) +
96
+ (@year ? ["-T", "date=#{@year}"] : [] ) +
97
+ (@genre ? ["-T", "genre=#{@genre}"] : [] ) +
98
+ ["-T", "comment=" + @comment + comment ] +
99
+ (@album_artist ? ["-T", "albumartist=#{@album_artist}"] : [] ) +
100
+ (@disc_number ? ["-T", "discnumber=#{@disc_number}"] : [] ) +
101
+ ["-"]
102
+ when :mp3
103
+ %W{/usr/bin/lame --quiet --preset extreme -h --add-id3v2 --id3v2-only} +
104
+ (@title ? ["--tt", @title] : [] ) +
105
+ (@artist ? ["--ta", @artist]: [] ) +
106
+ (@album ? ["--tl", @album] : [] ) +
107
+ (@track > 0 ? ["--tn", @track.to_s]: [] ) +
108
+ (@year ? ["--ty", @year.to_s ] : [] ) +
109
+ (@genre ? ["--tg", @genre ]: [] ) +
110
+ ["--tc", @comment + comment ] +
111
+ (@album_artist ? ["--tv", "TPE2=#{@album_artist}"] : [] ) +
112
+ (@disc_number ? ["--tv", "TPOS=#{@disc_number}"] : [] ) +
113
+ ["-", destination]
114
+ when :wav
115
+ %W{dd of=#{destination}}
116
+ else
117
+ raise "Unknown target '#{to}'. Cannot set encoder"
118
+ end
119
+ end
120
+
121
+ def transcode(trans , destination)
122
+ to = trans[@type]
123
+ printit "Attempting to transcode to the same format #{@source} from #{@type} to #{to}" if to == @type
124
+ FileUtils.mkdir_p File.dirname destination
125
+
126
+ decoder = set_decoder
127
+
128
+ encoder = set_encoder(to, destination)
129
+
130
+ printit "Decoder: '#{decoder.join(' ')}'\nEncoder: '#{encoder.join(' ')}'" if @verbose
131
+
132
+ pipes = Hash[[:encoder,:decoder].zip IO.pipe]
133
+ #readable, writeable = IO.pipe
134
+ pids = {
135
+ spawn(*decoder, :out=>pipes[:decoder], :err=>"/dev/null") => :decoder,
136
+ spawn(*encoder, :in =>pipes[:encoder], :err=>"/dev/null") => :encoder,
137
+ }
138
+ tpids = pids.keys
139
+ err = []
140
+ begin
141
+ Timeout::timeout(60 * ( File.size(@source) / 1024 / 1024 /2 ) ) {
142
+ #Timeout::timeout(3 ) {
143
+ while tpids.any? do
144
+ sleep 0.2
145
+ tpids.delete_if do |pid|
146
+ ret = false
147
+ p, stat = Process.wait2 pid, Process::WNOHANG
148
+ if stat
149
+ pipes[pids[pid]].close unless pipes[pids[pid]].closed?
150
+ ret = true
151
+ end
152
+ if stat and stat.exitstatus and stat.exitstatus != 0
153
+ err << [ pids[pid], stat ]
154
+ end
155
+ ret
156
+ end
157
+ end
158
+ }
159
+ rescue Timeout::Error
160
+ printit "Timeout exceeded!\n" << tpids.map { |p|
161
+ Process.kill 15, p
162
+ Process.kill 9, p
163
+ "#{p} #{Process.wait2( p )[1]}"
164
+ }.join(", ")
165
+ FileUtils.rm [destination]
166
+ raise
167
+ end
168
+ if err.any?
169
+ printit "Error with #{err.map{|it,stat| "#{it} EOT:#{stat.exitstatus} #{stat}" }.join(" and ")}"
170
+ #raise "Error #{@source} #{err}"
171
+ end
172
+ end
173
+
174
+ # directory names cannot end with a '.'
175
+ # it breaks windows (really!)
176
+
177
+ def relative_path
178
+ return @relpath if @relpath
179
+ read_tags
180
+ dest = File.join(
181
+ [(@album_artist||"UNKNOWN"), (@album||"UNKNOWN")].map { |word|
182
+ word.gsub(/^\.+|\.+$/,"").gsub(/\//,"_")
183
+ }
184
+ )
185
+ bool=true
186
+ @relpath = dest.gsub(/\s/,"_").gsub(/[,:)\]\[('"@$^*<>?!]/,"").gsub(/_[&]_/,"_and_").split('').map{ |c|
187
+ b = bool; bool = c.match('/|_'); b ? c.capitalize : c
188
+ }.join('').gsub(/__+/,'_')
189
+ end
190
+
191
+ def new_file_name
192
+ # this doesn't include the extension.
193
+ @newname ||= (
194
+ read_tags
195
+ bool=true
196
+ file= ( case
197
+ when (@disc_number && (@track > 0) && @title) && !(@disc_total && @disc_total == 1)
198
+ "%1d_%02d-" % [@disc_number, @track] + @title
199
+ when (@track > 0 && @title)
200
+ "%02d-" % @track + @title
201
+ when @title
202
+ @title
203
+ else
204
+ @name
205
+ end).gsub(
206
+ /^\.+|\.+$/,""
207
+ ).gsub(
208
+ /\//,"_"
209
+ ).gsub(
210
+ /\s/,"_"
211
+ ).gsub(
212
+ /[,:)\]\[('"@$^*<>?!]/,""
213
+ ).gsub(
214
+ /_[&]_/,"_and_"
215
+ ).split('').map{ |c|
216
+ b = bool; bool = c.match('/|_'); b ? c.capitalize : c
217
+ }.join('')
218
+ )
219
+ end
220
+
221
+ def tmp_file_name
222
+ "." + new_file_name
223
+ end
224
+
225
+ def tmp_path(base_dir: @base_dir, transcode_table: {})
226
+ File.join(
227
+ base_dir,
228
+ relative_path,
229
+ tmp_file_name,
230
+ ) << ".#{transcode_table[@type] || @type}"
231
+ end
232
+
233
+ def read_tags
234
+ return if @red
235
+ @album = @artist= @title = @genre = @year = nil
236
+ @track = 0
237
+ @comment = ""
238
+ TagLib::FileRef.open(@source) do |file|
239
+ unless file.null?
240
+ tag = file.tag
241
+ @album = tag.album if tag.album
242
+ @artist = tag.artist if tag.artist
243
+ @title = tag.title if tag.title
244
+ @genre = tag.genre if tag.genre
245
+ @comment= tag.comment if tag.comment
246
+ @track = tag.track if tag.track
247
+ @year = tag.year if tag.year
248
+ end
249
+ end
250
+ @album_artist = @artist
251
+ case @type
252
+ when :m4a
253
+ TagLib::MP4::File.open(@source) do |file|
254
+ @disc_number = file.tag.item_list_map["disk"].to_int_pair[0]
255
+ end
256
+ when :flac
257
+ TagLib::FLAC::File.open(@source) do |file|
258
+ if tag = file.xiph_comment
259
+ [
260
+ [:album_artist, ['ALBUMARTIST', 'ALBUM ARTIST', 'ALBUM_ARTIST'], :to_s ],
261
+ [:disc_number, ['DISCNUMBER'], :to_i ],
262
+ [:disc_total, ['DISCTOTAL'], :to_i ]
263
+ ].each do |field,list,func|
264
+ val = list.collect{ |i| tag.field_list_map[i] }.select{|i| i }.first
265
+ instance_variable_set("@#{field}", val[0].send(func)) if val
266
+ end
267
+ end
268
+ end
269
+ when :mp3
270
+ TagLib::MPEG::File.open(@source) do |file|
271
+ tag = file.id3v2_tag
272
+ if tag
273
+ [['TPE2', :@album_artist, :to_s], ['TPOS', :@disc_number, :to_i]].each do |t,v,m|
274
+ if tag.frame_list(t).first and tag.frame_list(t).first.to_s.size > 0
275
+ instance_variable_set( v, "#{tag.frame_list(t).first}".send( m.to_sym) )
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
281
+ @red = true
282
+ end
283
+ end; end
284
+
@@ -0,0 +1,3 @@
1
+ module MediaFile
2
+ VERSION = "0.0.3" unless defined?(::MediaFile::VERSION)
3
+ end
data/lib/mediafile.rb ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # vim:et sw=2 ts=2
3
+
4
+ require 'fileutils'
5
+ require 'digest/md5'
6
+ require 'timeout'
7
+ require 'taglib'
8
+ require 'mediafile/version'
9
+
10
+ module MediaFile
11
+
12
+ autoload :MediaFile, 'mediafile/mediafile.rb'
13
+ autoload :BulkMediaCopy, 'mediafile/bulkmediacopy.rb'
14
+
15
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mediafile
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Jeff Harvey-Smith
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: taglib-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '0.6'
20
+ - - '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 0.6.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '0.6'
30
+ - - '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.6.0
33
+ description: Parse media file metadata, copy or transcode mediafiles.
34
+ email:
35
+ - jharveysmith@gmail.com
36
+ executables:
37
+ - music_cp
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - ./bin/music_cp
42
+ - ./lib/mediafile.rb
43
+ - ./lib/mediafile/bulkmediacopy.rb
44
+ - ./lib/mediafile/mediafile.rb
45
+ - ./lib/mediafile/version.rb
46
+ - bin/music_cp
47
+ homepage: https://github.com/seginoviax/mediafile
48
+ licenses:
49
+ - MIT
50
+ metadata: {}
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ~>
58
+ - !ruby/object:Gem::Version
59
+ version: '2'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubyforge_project:
67
+ rubygems_version: 2.2.2
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Parse media file metadata.
71
+ test_files: []
72
+ has_rdoc: