flacinfo-rb 0.2 → 0.3
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.
- data/flacinfo.rb +309 -65
- metadata +2 -2
data/flacinfo.rb
CHANGED
@@ -3,22 +3,21 @@
|
|
3
3
|
# flacinfo-rb gives you access to low level information on Flac files.
|
4
4
|
# * It parses stream information (METADATA_BLOCK_STREAMINFO).
|
5
5
|
# * It parses Vorbis comments (METADATA_BLOCK_VORBIS_COMMENT).
|
6
|
+
# * It allows you to add/delete/edit Vorbis comments and write them to the Flac file.
|
6
7
|
# * It parses the seek table (METADATA_BLOCK_SEEKTABLE).
|
7
8
|
# * It parses the 'application metadata block' (METADATA_BLOCK_APPLICATION).
|
8
|
-
# * If application is ID 0x41544348 (Flac File)
|
9
|
-
# then we can parse that too.
|
9
|
+
# * If application is ID 0x41544348 (Flac File) then we can parse that too.
|
10
10
|
# * It recognizes (but does not yet parse) the cue sheet (METADATA_BLOCK_CUESHEET).
|
11
11
|
# * It parses zero or more picture blocks (METADATA_BLOCK_PICTURE)
|
12
|
-
# * It allows you to write embedded images to a file.
|
12
|
+
# * It allows you to write the embedded images to a file.
|
13
13
|
#
|
14
|
-
# My
|
15
|
-
# so in the future it should become quite a nice native Ruby library interface which will allow
|
14
|
+
# My goals are to create a nice native Ruby library interface which will allow
|
16
15
|
# the user to mimic most functionality of the 'metaflac' binary programmatically.
|
17
16
|
#
|
18
17
|
# = Copyright and Disclaimer
|
19
18
|
#
|
20
|
-
# Copyright:: (c) 2006 Darren Kirby
|
21
|
-
# FlacInfo is free software.
|
19
|
+
# Copyright:: (c) 2006, 2007 Darren Kirby
|
20
|
+
# FlacInfo is free software.
|
22
21
|
# No warranty is provided and the author cannot accept responsibility
|
23
22
|
# for lost or damaged files.
|
24
23
|
# License:: Ruby
|
@@ -35,14 +34,33 @@
|
|
35
34
|
# http://www.xiph.org/vorbis/doc/v-comment.html
|
36
35
|
|
37
36
|
|
38
|
-
# FlacInfoError is raised
|
39
|
-
# It will print
|
37
|
+
# FlacInfoError is raised for general user errors.
|
38
|
+
# It will print a string that describes the problem.
|
40
39
|
class FlacInfoError < StandardError
|
41
40
|
end
|
42
41
|
|
42
|
+
# FlacInfoReadError is raised when an error occurs parsing the Flac file.
|
43
|
+
# It will print a string that describes in which block the error occured.
|
44
|
+
class FlacInfoReadError < StandardError
|
45
|
+
end
|
46
|
+
|
47
|
+
# FlacInfoWriteError is raised when an error occurs writing the Flac file.
|
48
|
+
# It will print a string that describes where the error occured.
|
49
|
+
class FlacInfoWriteError < StandardError
|
50
|
+
end
|
51
|
+
|
43
52
|
# Note: STREAMINFO is the only block guaranteed to be present in the Flac file.
|
44
|
-
# All attributes will be present
|
53
|
+
# All attributes will be present but empty if the associated block is not present in the Flac file,
|
54
|
+
# except for 'picture' which will have the key 'n' with the value '0'.
|
55
|
+
# All 'offset' and 'block_size' values do not include the block header. All block headers are 4 bytes
|
56
|
+
# no matter the type, so if you need the offset including the header, subtract 4. If you need the size
|
57
|
+
# including the header, add 4.
|
45
58
|
class FlacInfo
|
59
|
+
# A list of 'standard field names' according to the Vorbis Comment specification. It is certainly
|
60
|
+
# possible to use a non-standard name, but the spec recommends against it.
|
61
|
+
# See: http://www.xiph.org/vorbis/doc/v-comment.html
|
62
|
+
STANDARD_FIELD_NAMES= %w/TITLE VERSION ALBUM TRACKNUMBER ARTIST PERFORMER COPYRIGHT LICENSE
|
63
|
+
ORGANIZATION DESCRIPTION GENRE DATE LOCATION CONTACT ISRC/
|
46
64
|
|
47
65
|
# Hash of values extracted from the STREAMINFO block. Keys are:
|
48
66
|
# 'offset':: The STREAMINFO block's offset from the beginning of the file (not including the block header).
|
@@ -70,7 +88,8 @@ class FlacInfo
|
|
70
88
|
attr_reader :seektable
|
71
89
|
|
72
90
|
# Array of "name=value" strings extracted from the VORBIS_COMMENT block. This is just the contents, metadata is in 'tags'.
|
73
|
-
|
91
|
+
# You should not normally operate on this array directly. Rather, use the comment_add and comment_del methods to make changes.
|
92
|
+
attr_accessor :comment
|
74
93
|
|
75
94
|
# Hash of the 'comment' values separated into "key => value" pairs as well as the keys:
|
76
95
|
# 'offset':: The VORBIS_COMMENT block's offset from the beginning of the file (not including the block header).
|
@@ -138,12 +157,17 @@ class FlacInfo
|
|
138
157
|
@tags["#{tag}"] ? true : false
|
139
158
|
end
|
140
159
|
|
141
|
-
# Pretty print
|
160
|
+
# Pretty print tags hash.
|
142
161
|
#
|
143
162
|
# :call-seq:
|
144
163
|
# FlacInfo.print_tags -> nil
|
145
164
|
#
|
165
|
+
# Raises FlacInfoError if METADATA_BLOCK_VORBIS_COMMENT is not present.
|
166
|
+
#
|
146
167
|
def print_tags
|
168
|
+
if @tags == {}
|
169
|
+
raise FlacInfoError, "METADATA_BLOCK_VORBIS_COMMENT not present"
|
170
|
+
end
|
147
171
|
@tags.each_pair { |key,val| puts "#{key}: #{val}" }
|
148
172
|
nil
|
149
173
|
end
|
@@ -154,6 +178,7 @@ class FlacInfo
|
|
154
178
|
# FlacInfo.print_streaminfo -> nil
|
155
179
|
#
|
156
180
|
def print_streaminfo
|
181
|
+
# No test: METADATA_BLOCK_STREAMINFO must be present in valid Flac file
|
157
182
|
@streaminfo.each_pair { |key,val| puts "#{key}: #{val}" }
|
158
183
|
nil
|
159
184
|
end
|
@@ -163,7 +188,12 @@ class FlacInfo
|
|
163
188
|
# :call-seq:
|
164
189
|
# FlacInfo.print_seektable -> nil
|
165
190
|
#
|
191
|
+
# Raises FlacInfoError if METADATA_BLOCK_SEEKTABLE is not present.
|
192
|
+
#
|
166
193
|
def print_seektable
|
194
|
+
if @seektable == {}
|
195
|
+
raise FlacInfoError, "METADATA_BLOCK_SEEKTABLE not present"
|
196
|
+
end
|
167
197
|
puts " seek points: #{@seektable['seek_points']}"
|
168
198
|
n = 0
|
169
199
|
@seektable['seek_points'].times do
|
@@ -178,7 +208,7 @@ class FlacInfo
|
|
178
208
|
# This method produces output similar to 'metaflac --list'.
|
179
209
|
#
|
180
210
|
# :call-seq:
|
181
|
-
# FlacInfo.meta_flac
|
211
|
+
# FlacInfo.meta_flac -> nil
|
182
212
|
#
|
183
213
|
def meta_flac
|
184
214
|
n = 0
|
@@ -216,7 +246,8 @@ class FlacInfo
|
|
216
246
|
# FlacInfo.raw_data_dump(outfile) -> nil
|
217
247
|
#
|
218
248
|
# If passed with 'outfile', the data will be written to a file with that name
|
219
|
-
# otherwise it is written to the console (even if binary!).
|
249
|
+
# otherwise it is written to the console (even if binary!). Raises FlacInfoError
|
250
|
+
# if there is no Flac File data present.
|
220
251
|
#
|
221
252
|
def raw_data_dump(outfile = nil)
|
222
253
|
if @flac_file == {}
|
@@ -240,10 +271,10 @@ class FlacInfo
|
|
240
271
|
# Writes embedded images to a file
|
241
272
|
#
|
242
273
|
# :call-seq:
|
243
|
-
# FlacInfo.write_picture()
|
244
|
-
# FlacInfo.write_picture(:outfile=>"str")
|
245
|
-
# FlacInfo.write_picture(:n=>int)
|
246
|
-
# FlacInfo.write_picture(:outfile=>"str", :n=>int)
|
274
|
+
# FlacInfo.write_picture() -> nil
|
275
|
+
# FlacInfo.write_picture(:outfile=>"str") -> nil
|
276
|
+
# FlacInfo.write_picture(:n=>int) -> nil
|
277
|
+
# FlacInfo.write_picture(:outfile=>"str", :n=>int) -> nil
|
247
278
|
#
|
248
279
|
# If passed with ':outfile', the image will be written to a file with that name
|
249
280
|
# otherwise it is written to the value of the 'album' tag if it exists, otherwise it
|
@@ -261,13 +292,14 @@ class FlacInfo
|
|
261
292
|
n = 1
|
262
293
|
end
|
263
294
|
|
264
|
-
#
|
295
|
+
# "image/jpeg" => "jpeg"
|
265
296
|
extension = @picture[n]["mime_type"].split("/")[1]
|
266
297
|
|
267
298
|
if not args.has_key?(:outfile)
|
268
299
|
if @tags["album"] == nil or @tags["album"] == ""
|
269
300
|
outfile = "flacimage#{n}.#{extension}"
|
270
301
|
else
|
302
|
+
# Try to use contents of "album" tag for the filename
|
271
303
|
outfile = "#{@tags["album"]}#{n}.#{extension}"
|
272
304
|
end
|
273
305
|
else
|
@@ -277,7 +309,7 @@ class FlacInfo
|
|
277
309
|
in_p = File.new(@filename, "rb")
|
278
310
|
out_p = File.new(outfile, "wb")
|
279
311
|
|
280
|
-
out_p.binmode
|
312
|
+
out_p.binmode # For Windows folks...
|
281
313
|
|
282
314
|
in_p.seek(@picture[n]['raw_data_offset'], IO::SEEK_CUR)
|
283
315
|
raw_data = in_p.read(@picture[n]['raw_data_length'])
|
@@ -289,7 +321,82 @@ class FlacInfo
|
|
289
321
|
nil
|
290
322
|
end
|
291
323
|
|
324
|
+
# Writes changes to disk
|
325
|
+
#
|
326
|
+
# :call-seq:
|
327
|
+
# FlacInfo.update -> bool
|
328
|
+
#
|
329
|
+
# Returns true if write was successful, false otherwise.
|
330
|
+
#
|
331
|
+
def update
|
332
|
+
if write_to_disk
|
333
|
+
return true
|
334
|
+
else
|
335
|
+
return false
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
# Adds a new comment to the comment array
|
340
|
+
#
|
341
|
+
# :call-seq:
|
342
|
+
# FlacInfo.comment_add(str) -> bool
|
343
|
+
#
|
344
|
+
# 'str' must be in the form 'name=value', or 'name=' if you want to
|
345
|
+
# set an empty value for a particular tag.
|
346
|
+
# Returns 'true' if successful, false otherwise.
|
347
|
+
#
|
348
|
+
def comment_add(name)
|
349
|
+
if name !~ /\w=/ # We accept 'name=' in case you want to leave the value empty
|
350
|
+
raise FlacInfoError, "comments must be in the form 'name=value'"
|
351
|
+
end
|
352
|
+
begin
|
353
|
+
@comment[@comment.length] = name
|
354
|
+
@comments_changed = 1
|
355
|
+
rescue
|
356
|
+
return false
|
357
|
+
end
|
358
|
+
return true
|
359
|
+
end
|
360
|
+
|
361
|
+
# Deletes a comment from the comment array
|
362
|
+
#
|
363
|
+
# :call-seq:
|
364
|
+
# FlacInfo.comment_del(str) -> bool
|
365
|
+
#
|
366
|
+
# If 'str' is in the form 'name=value' only exact matches
|
367
|
+
# will be deleted. If 'str' is in the form 'name' any and all
|
368
|
+
# comments named 'name' will be deleted. Returns 'true' if a
|
369
|
+
# comment was deleted, false otherwise.
|
370
|
+
#
|
371
|
+
def comment_del(name)
|
372
|
+
bc = Array.new(@comment) # We need a copy
|
373
|
+
if name.include? "="
|
374
|
+
nc = @comment.delete_if { |x| x == name }
|
375
|
+
else
|
376
|
+
nc = @comment.delete_if { |x| x.split("=")[0] == name }
|
377
|
+
end
|
378
|
+
|
379
|
+
if nc == bc
|
380
|
+
return false
|
381
|
+
else
|
382
|
+
@comments_changed = 1
|
383
|
+
return true
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
#--
|
388
|
+
# This cleans up the output when using FlacInfo in irb
|
389
|
+
def inspect #:nodoc:
|
390
|
+
s = "#<#{self.class}:0x#{(self.object_id*2).to_s(16)} "
|
391
|
+
@metadata_blocks.each do |blk|
|
392
|
+
s += "(#{blk[0].upcase} size=#{blk[4]} offset=#{blk[3]}) "
|
393
|
+
end
|
394
|
+
s += "\b>"
|
395
|
+
end
|
396
|
+
#++
|
397
|
+
|
292
398
|
private
|
399
|
+
|
293
400
|
# The following six methods are just helpers for meta_flac
|
294
401
|
def meta_stream
|
295
402
|
puts " length: #{@streaminfo['block_size']}"
|
@@ -361,16 +468,20 @@ class FlacInfo
|
|
361
468
|
end
|
362
469
|
|
363
470
|
|
364
|
-
#
|
471
|
+
# This is where the 'real' parsing starts
|
365
472
|
def parse_flac_meta_blocks
|
366
|
-
@fp = File.new(@filename, "rb")
|
367
|
-
@
|
368
|
-
|
369
|
-
|
370
|
-
@
|
473
|
+
@fp = File.new(@filename, "rb") # Our file pointer
|
474
|
+
@comments_changed = nil # Do we need to write a new VORBIS_BLOCK?
|
475
|
+
|
476
|
+
# These next 8 lines initialize our public data structures.
|
477
|
+
@streaminfo = {}
|
478
|
+
@comment = []
|
479
|
+
@tags = {}
|
480
|
+
@seektable = {}
|
481
|
+
@padding = {}
|
371
482
|
@application = {}
|
372
|
-
@cuesheet
|
373
|
-
@picture
|
483
|
+
@cuesheet = {}
|
484
|
+
@picture = {"n" => 0}
|
374
485
|
|
375
486
|
header = @fp.read(4)
|
376
487
|
# First 4 bytes must be 0x66, 0x4C, 0x61, and 0x43
|
@@ -384,22 +495,22 @@ class FlacInfo
|
|
384
495
|
|
385
496
|
@metadata_blocks = []
|
386
497
|
lastheader = 0
|
387
|
-
|
498
|
+
pos = 1
|
388
499
|
|
389
500
|
until lastheader == 1
|
390
|
-
#
|
391
|
-
#
|
501
|
+
# first bit = Last-metadata-block flag
|
502
|
+
# bits 2-8 = BLOCK_TYPE. See typetable above
|
392
503
|
block_header = @fp.read(1).unpack("B*")[0]
|
393
504
|
lastheader = block_header[0].to_i & 1
|
394
505
|
type = sprintf("%u", "0b#{block_header[1..7]}").to_i
|
395
|
-
@metadata_blocks
|
506
|
+
@metadata_blocks << [typetable[type], type, lastheader]
|
396
507
|
|
397
508
|
if type >= typetable.size
|
398
509
|
raise FlacInfoError, "Invalid block header type"
|
399
510
|
end
|
400
511
|
|
512
|
+
pos += 1
|
401
513
|
self.send "parse_#{typetable[type]}"
|
402
|
-
n += 1
|
403
514
|
end
|
404
515
|
|
405
516
|
@fp.close
|
@@ -407,9 +518,13 @@ class FlacInfo
|
|
407
518
|
|
408
519
|
def parse_seektable
|
409
520
|
begin
|
410
|
-
@seektable['block_size']
|
411
|
-
@seektable['offset']
|
521
|
+
@seektable['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
522
|
+
@seektable['offset'] = @fp.tell
|
412
523
|
@seektable['seek_points'] = @seektable['block_size'] / 18
|
524
|
+
|
525
|
+
@metadata_blocks[-1] << @seektable['offset']
|
526
|
+
@metadata_blocks[-1] << @seektable['block_size']
|
527
|
+
|
413
528
|
n = 0
|
414
529
|
@seektable['points'] = {}
|
415
530
|
|
@@ -430,8 +545,12 @@ class FlacInfo
|
|
430
545
|
# Not parsed yet, I have no flacs with a cuesheet!
|
431
546
|
def parse_cuesheet
|
432
547
|
begin
|
433
|
-
@cuesheet['block_size'] = @fp.read(3).
|
434
|
-
@cuesheet['offset']
|
548
|
+
@cuesheet['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
549
|
+
@cuesheet['offset'] = @fp.tell
|
550
|
+
|
551
|
+
@metadata_blocks[-1] << @cuesheet['offset']
|
552
|
+
@metadata_blocks[-1] << @cuesheet['block_size']
|
553
|
+
|
435
554
|
@fp.seek(@cuesheet['block_size'], IO::SEEK_CUR)
|
436
555
|
rescue
|
437
556
|
raise FlacInfoError, "Could not parse METADATA_BLOCK_CUESHEET"
|
@@ -441,7 +560,7 @@ class FlacInfo
|
|
441
560
|
def parse_picture
|
442
561
|
n = @picture["n"] + 1
|
443
562
|
@picture["n"] = n
|
444
|
-
@picture[n]
|
563
|
+
@picture[n] = {}
|
445
564
|
|
446
565
|
picture_type = ["Other", "32x32 pixels file icon", "Other file icon", "Cover (front)", "Cover (back)",
|
447
566
|
"Leaflet page", "Media", "Lead artist/lead performer/soloist", "Artist/performer",
|
@@ -450,20 +569,26 @@ class FlacInfo
|
|
450
569
|
coloured fish", "Illustration", "Band/artist logotype", "Publisher/Studio logotype"]
|
451
570
|
|
452
571
|
begin
|
453
|
-
@picture[n]['block_size'] = @fp.read(3).
|
454
|
-
@picture[n]['offset']
|
455
|
-
|
456
|
-
@
|
457
|
-
|
458
|
-
@picture[n]['
|
459
|
-
|
572
|
+
@picture[n]['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
573
|
+
@picture[n]['offset'] = @fp.tell
|
574
|
+
|
575
|
+
@metadata_blocks[-1] << @picture[n]['offset']
|
576
|
+
|
577
|
+
@picture[n]['type_int'] = @fp.read(4).reverse.unpack("v*")[0]
|
578
|
+
@picture[n]['type_string'] = picture_type[@picture[n]['type_int']]
|
579
|
+
mime_length = @fp.read(4).reverse.unpack("v*")[0]
|
580
|
+
@picture[n]['mime_type'] = @fp.read(mime_length).unpack("a*")[0]
|
581
|
+
description_length = @fp.read(4).reverse.unpack("v*")[0]
|
460
582
|
@picture[n]['description_string'] = @fp.read(description_length).unpack("M*")[0]
|
461
|
-
@picture[n]['width']
|
462
|
-
@picture[n]['height']
|
463
|
-
@picture[n]['colour_depth']
|
464
|
-
@picture[n]['n_colours']
|
465
|
-
@picture[n]['raw_data_length']
|
466
|
-
@picture[n]['raw_data_offset']
|
583
|
+
@picture[n]['width'] = @fp.read(4).reverse.unpack("v*")[0]
|
584
|
+
@picture[n]['height'] = @fp.read(4).reverse.unpack("v*")[0]
|
585
|
+
@picture[n]['colour_depth'] = @fp.read(4).reverse.unpack("v*")[0]
|
586
|
+
@picture[n]['n_colours'] = @fp.read(4).reverse.unpack("v*")[0]
|
587
|
+
@picture[n]['raw_data_length'] = @fp.read(4).reverse.unpack("V*")[0]
|
588
|
+
@picture[n]['raw_data_offset'] = @fp.tell
|
589
|
+
|
590
|
+
@metadata_blocks[-1] << @picture[n]['block_size']
|
591
|
+
|
467
592
|
@fp.seek((@picture[n]['raw_data_length']), IO::SEEK_CUR)
|
468
593
|
rescue
|
469
594
|
raise FlacInfoError, "Could not parse METADATA_BLOCK_PICTURE"
|
@@ -472,8 +597,12 @@ class FlacInfo
|
|
472
597
|
|
473
598
|
def parse_application
|
474
599
|
begin
|
475
|
-
@application['block_size'] = @fp.read(3).
|
476
|
-
@application['offset']
|
600
|
+
@application['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
601
|
+
@application['offset'] = @fp.tell
|
602
|
+
|
603
|
+
@metadata_blocks[-1] << @application['offset']
|
604
|
+
@metadata_blocks[-1] << @application['block_size']
|
605
|
+
|
477
606
|
@application['ID'] = @fp.read(4).unpack("H*")[0]
|
478
607
|
|
479
608
|
app_id = {"41544348" => "Flac File", "43756573" => "GoldWave Cue Points",
|
@@ -503,17 +632,20 @@ class FlacInfo
|
|
503
632
|
# separated into key=value pairs
|
504
633
|
def parse_vorbis_comment
|
505
634
|
begin
|
506
|
-
@tags =
|
507
|
-
@tags['
|
508
|
-
|
509
|
-
|
510
|
-
@tags['
|
511
|
-
user_comment_list_length = @fp.read(4).unpack("V")[0]
|
512
|
-
@comment = []
|
513
|
-
n = 0
|
635
|
+
@tags['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
636
|
+
@tags['offset'] = @fp.tell
|
637
|
+
|
638
|
+
@metadata_blocks[-1] << @tags['offset']
|
639
|
+
@metadata_blocks[-1] << @tags['block_size']
|
514
640
|
|
641
|
+
vendor_length = @fp.read(4).reverse.unpack("B*")[0].to_i(2)
|
642
|
+
|
643
|
+
@tags['vendor_tag'] = @fp.read(vendor_length)
|
644
|
+
user_comment_list_length = @fp.read(4).reverse.unpack("B*")[0].to_i(2)
|
645
|
+
|
646
|
+
n = 0
|
515
647
|
user_comment_list_length.times do
|
516
|
-
length = @fp.read(4).unpack("
|
648
|
+
length = @fp.read(4).reverse.unpack("B*")[0].to_i(2)
|
517
649
|
@comment[n] = @fp.read(length)
|
518
650
|
n += 1
|
519
651
|
end
|
@@ -539,8 +671,12 @@ class FlacInfo
|
|
539
671
|
# padding is just a bunch of '0' bytes
|
540
672
|
def parse_padding
|
541
673
|
begin
|
542
|
-
@padding['block_size'] = @fp.read(3).
|
543
|
-
@padding['offset']
|
674
|
+
@padding['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
675
|
+
@padding['offset'] = @fp.tell
|
676
|
+
|
677
|
+
@metadata_blocks[-1] << @padding['offset']
|
678
|
+
@metadata_blocks[-1] << @padding['block_size']
|
679
|
+
|
544
680
|
@fp.seek(@padding['block_size'], IO::SEEK_CUR)
|
545
681
|
rescue
|
546
682
|
raise FlacInfoError, "Could not parse METADATA_BLOCK_PADDING"
|
@@ -549,8 +685,12 @@ class FlacInfo
|
|
549
685
|
|
550
686
|
def parse_streaminfo
|
551
687
|
begin
|
552
|
-
@streaminfo['block_size'] = @fp.read(3).
|
688
|
+
@streaminfo['block_size'] = @fp.read(3).unpack("B*")[0].to_i(2)
|
553
689
|
@streaminfo['offset'] = @fp.tell
|
690
|
+
|
691
|
+
@metadata_blocks[-1] << @streaminfo['offset']
|
692
|
+
@metadata_blocks[-1] << @streaminfo['block_size']
|
693
|
+
|
554
694
|
@streaminfo['minimum_block'] = @fp.read(2).reverse.unpack("v*")[0]
|
555
695
|
@streaminfo['maximum_block'] = @fp.read(2).reverse.unpack("v*")[0]
|
556
696
|
@streaminfo['minimum_frame'] = @fp.read(3).reverse.unpack("v*")[0]
|
@@ -588,4 +728,108 @@ class FlacInfo
|
|
588
728
|
raise FlacInfoError, "Could not parse Flac File data"
|
589
729
|
end
|
590
730
|
end
|
731
|
+
|
732
|
+
# Here we begin the FlacInfo write methods
|
733
|
+
|
734
|
+
|
735
|
+
# Build a block header given a type, a size, and whether it is last
|
736
|
+
def build_block_header(type, size, last)
|
737
|
+
begin
|
738
|
+
bit_string = sprintf("%b%7b", last, type).gsub(" ","0")
|
739
|
+
block_header_s = [bit_string].pack("B*")
|
740
|
+
block_header_s += [size].pack("VX").reverse # size is 3 bytes
|
741
|
+
rescue
|
742
|
+
raise FlacInfoWriteError, "error building block header"
|
743
|
+
end
|
744
|
+
end
|
745
|
+
|
746
|
+
# Build a string of packed data for the Vorbis comments
|
747
|
+
def build_vorbis_comment_block
|
748
|
+
begin
|
749
|
+
vorbis_comm_s = [@tags["vendor_tag"].length].pack("V")
|
750
|
+
vorbis_comm_s += [@tags["vendor_tag"]].pack("A*")
|
751
|
+
vorbis_comm_s += [@comment.length].pack("V")
|
752
|
+
@comment.each do |c|
|
753
|
+
vorbis_comm_s += [c.length].pack("V")
|
754
|
+
vorbis_comm_s += [c].pack("A*")
|
755
|
+
end
|
756
|
+
vorbis_comm_s
|
757
|
+
rescue
|
758
|
+
raise FlacInfoWriteError, "error building vorbis comment block"
|
759
|
+
end
|
760
|
+
end
|
761
|
+
|
762
|
+
def write_to_disk
|
763
|
+
if @comments_changed == nil
|
764
|
+
raise FlacInfoWriteError, "No changes to write"
|
765
|
+
else
|
766
|
+
vcd = build_vorbis_comment_block # Build the VORBIS_COMMENT data
|
767
|
+
vch = build_block_header(4, vcd.length, 0) # Build the VORBIS_COMMENT header
|
768
|
+
end
|
769
|
+
|
770
|
+
# Determine if we can shuffle the data or if a rewrite is necessary
|
771
|
+
begin
|
772
|
+
if not @padding.has_key?("block_size") or vcd.length > @padding['block_size']
|
773
|
+
rewrite(vcd, vch) # Rewriting is simpler but more expensive
|
774
|
+
else
|
775
|
+
shuffle(vcd, vch) # Shuffling is more complicated but cheaper
|
776
|
+
end
|
777
|
+
parse_flac_meta_blocks # Parse the file again to update new values
|
778
|
+
return true
|
779
|
+
rescue
|
780
|
+
raise FlacInfoWriteError, "error writing new data to #{@filename}"
|
781
|
+
end
|
782
|
+
end
|
783
|
+
|
784
|
+
# Shuffle the data and update the PADDING block
|
785
|
+
def shuffle(vcd, vch)
|
786
|
+
flac = File.new(@filename, "r+b")
|
787
|
+
flac.binmode # For Windows folks...
|
788
|
+
|
789
|
+
# Position ourselves at end of current Vorbis block
|
790
|
+
flac.seek((@tags['offset'] + @tags['block_size']), IO::SEEK_CUR)
|
791
|
+
# The data we need to shuffle starts at current position and ends at
|
792
|
+
# the beginning of the padding block, so the size we need to read is:
|
793
|
+
#
|
794
|
+
# (offset of padding minus 4 bytes for the padding header) minus our current position
|
795
|
+
#
|
796
|
+
size_to_read = (@padding['offset'] - 4) - flac.tell
|
797
|
+
data_to_shuffle = flac.read(size_to_read)
|
798
|
+
|
799
|
+
flac.seek((@tags['offset'] - 4), IO::SEEK_SET)
|
800
|
+
flac.write(vch) # Write the VORBIS_COMMENT header
|
801
|
+
flac.write(vcd) # Write the VORBIS_COMMENT data
|
802
|
+
flac.write(data_to_shuffle) # Write the shuffled data
|
803
|
+
|
804
|
+
new_padding_size = @padding['block_size'] - (vcd.length - @tags['block_size'])
|
805
|
+
ph = build_block_header(1, new_padding_size, 1) # Build the new PADDING header
|
806
|
+
|
807
|
+
flac.write(ph) # Write the new PADDING header
|
808
|
+
flac.close # ...and we're done
|
809
|
+
end
|
810
|
+
|
811
|
+
# Rewrite the entire file
|
812
|
+
def rewrite(vcd, vch)
|
813
|
+
flac = File.new(@filename, "r+b")
|
814
|
+
flac.binmode # For Windows folks...
|
815
|
+
|
816
|
+
flac.seek((@tags['offset'] + @tags['block_size']), IO::SEEK_CUR)
|
817
|
+
rest_of_file = flac.read()
|
818
|
+
flac.seek((@tags['offset'] - 4), IO::SEEK_SET)
|
819
|
+
|
820
|
+
flac.write(vch) # Write the VORBIS_COMMENT header
|
821
|
+
flac.write(vcd) # Write the VORBIS_COMMENT data
|
822
|
+
flac.write(rest_of_file) # Write the rest of the file
|
823
|
+
|
824
|
+
flac.close
|
825
|
+
end
|
826
|
+
|
827
|
+
end
|
828
|
+
|
829
|
+
# If called directly from the command line, run meta_flac on each argument
|
830
|
+
if __FILE__ == $0
|
831
|
+
ARGV.each do |filename|
|
832
|
+
FlacInfo.new(filename).meta_flac
|
833
|
+
puts
|
834
|
+
end
|
591
835
|
end
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.4
|
|
3
3
|
specification_version: 1
|
4
4
|
name: flacinfo-rb
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: "0.
|
7
|
-
date: 2007-
|
6
|
+
version: "0.3"
|
7
|
+
date: 2007-07-01 00:00:00 -06:00
|
8
8
|
summary: Pure Ruby lib for accessing metadata (including Vorbis tags) from Flac files
|
9
9
|
require_paths:
|
10
10
|
- lib
|