id3 0.5.0 → 1.0.0.pre4
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +14 -0
- data/LICENSE.html +8 -1
- data/README.md +39 -0
- data/docs/ID3_comparison.html +10 -2
- data/docs/ID3_comparison2.html +29 -21
- data/docs/ID3v2_frames_overview.txt +172 -35
- data/{lib → docs}/hexdump.rb +0 -0
- data/docs/index.html +29 -9
- data/lib/helpers/hash_extensions.rb +20 -0
- data/lib/helpers/hexdump.rb +136 -0
- data/lib/helpers/invert_hash.rb +128 -0
- data/lib/helpers/recursive_helper.rb +39 -0
- data/lib/helpers/restricted_ordered_hash.rb +88 -0
- data/lib/helpers/ruby_1.8_1.9_compatibility.rb +62 -0
- data/lib/id3.rb +23 -1252
- data/lib/id3/audiofile.rb +261 -0
- data/lib/id3/constants.rb +292 -0
- data/lib/id3/frame.rb +178 -0
- data/lib/id3/frame_array.rb +19 -0
- data/lib/id3/generic_tag.rb +73 -0
- data/lib/id3/id3.rb +159 -0
- data/lib/id3/io_extensions.rb +44 -0
- data/lib/id3/module_methods.rb +127 -0
- data/lib/id3/string_extensions.rb +40 -0
- data/lib/id3/tag1.rb +131 -0
- data/lib/id3/tag2.rb +261 -0
- metadata +87 -58
- data/README +0 -18
- data/docs/ID3v2_frames_comparison.txt +0 -197
- data/lib/invert_hash.rb +0 -105
@@ -0,0 +1,20 @@
|
|
1
|
+
#
|
2
|
+
# EXTENSIONS to Class Hash
|
3
|
+
#
|
4
|
+
|
5
|
+
# include Hash#inverse from Facets of Ruby or from our helper
|
6
|
+
# then Monkey-Patch the Hash#invert method:
|
7
|
+
|
8
|
+
require 'helpers/invert_hash'
|
9
|
+
|
10
|
+
class Hash
|
11
|
+
# original Hash#invert is still available as Hash#old_invert
|
12
|
+
alias old_invert invert
|
13
|
+
|
14
|
+
# monkey-patching Hash#invert method - it's backwards compatible, but preserves duplicate values in the hash
|
15
|
+
def invert
|
16
|
+
self.inverse
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# ==============================================================================
|
2
|
+
# EXTENDING CLASS STRING
|
3
|
+
# ==============================================================================
|
4
|
+
# --
|
5
|
+
# (C) Copyright 2004 by Tilo Sloboda <tools@unixgods.org>
|
6
|
+
#
|
7
|
+
# License:
|
8
|
+
# Freely available under the terms of the OpenSource "Artistic License"
|
9
|
+
# in combination with the Addendum A (below)
|
10
|
+
#
|
11
|
+
# In case you did not get a copy of the license along with the software,
|
12
|
+
# it is also available at: http://www.unixgods.org/~tilo/artistic-license.html
|
13
|
+
#
|
14
|
+
# Addendum A:
|
15
|
+
# THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU!
|
16
|
+
# SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
17
|
+
# REPAIR OR CORRECTION.
|
18
|
+
#
|
19
|
+
# IN NO EVENT WILL THE COPYRIGHT HOLDERS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
|
20
|
+
# SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY
|
21
|
+
# TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
|
22
|
+
# INACCURATE OR USELESS OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
23
|
+
# TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE COPYRIGHT HOLDERS OR OTHER PARTY HAS BEEN
|
24
|
+
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
25
|
+
#++
|
26
|
+
|
27
|
+
# this is written for Ruby version < 1.9 - unfortunately they changed the I/O and String classes significantly.
|
28
|
+
# have to fix this up for newer Ruby versions.
|
29
|
+
|
30
|
+
if RUBY_VERSION >= "1.9.0"
|
31
|
+
ZEROBYTE = "\x00".force_encoding(Encoding::BINARY) unless defined? ZEROBYTE
|
32
|
+
|
33
|
+
else # older Ruby versions:
|
34
|
+
|
35
|
+
class String
|
36
|
+
alias bytesize size
|
37
|
+
|
38
|
+
def getbyte(x) # when accessing a string and selecting x-th byte to do calculations , as defined in Ruby 1.9
|
39
|
+
self[x] # returns an integer
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
ZEROBYTE = "\0" unless defined? ZEROBYTE
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
class String
|
48
|
+
#
|
49
|
+
# prints out a good'ol hexdump of the data contained in the string
|
50
|
+
#
|
51
|
+
# parameters: sparse: true / false do we want to print multiple lines with zero values?
|
52
|
+
|
53
|
+
def hexdump(sparse = false)
|
54
|
+
|
55
|
+
selfsize = self.bytesize
|
56
|
+
|
57
|
+
first = true
|
58
|
+
|
59
|
+
print "\n index 0 1 2 3 4 5 6 7 8 9 A B C D E F\n\n"
|
60
|
+
|
61
|
+
lines,rest = selfsize.divmod(16)
|
62
|
+
address = 0; i = 0 # we count them independently for future extension.
|
63
|
+
|
64
|
+
while lines > 0
|
65
|
+
str = self[i..i+15]
|
66
|
+
|
67
|
+
# we don't print lines with all zeroes, unless it's the last line
|
68
|
+
|
69
|
+
if str == ZEROBYTE * 16 # if the 16 bytes are all zero
|
70
|
+
|
71
|
+
if (!sparse) || (sparse && lines == 1 && rest == 0)
|
72
|
+
str.tr!("\000-\037\177-\377",'.')
|
73
|
+
|
74
|
+
printf( "%08x %8s %8s %8s %8s %s\n",
|
75
|
+
address, self[i..i+3].unpack('H8').first, self[i+4..i+7].unpack('H8').first,
|
76
|
+
self[i+8..i+11].unpack('H8').first, self[i+12..i+15].unpack('H8').first, str)
|
77
|
+
else
|
78
|
+
print " .... 00 .. 00 00 .. 00 00 .. 00 00 .. 00 ................\n" if first
|
79
|
+
first = false
|
80
|
+
end
|
81
|
+
|
82
|
+
else # print string which is not all zeros
|
83
|
+
|
84
|
+
str.tr!("\000-\037\177-\377",'.')
|
85
|
+
|
86
|
+
printf( "%08x %8s %8s %8s %8s %s\n",
|
87
|
+
address, self[i..i+3].unpack('H8').first, self[i+4..i+7].unpack('H8').first,
|
88
|
+
self[i+8..i+11].unpack('H8').first, self[i+12..i+15].unpack('H8').first, str)
|
89
|
+
first = true
|
90
|
+
end
|
91
|
+
i += 16; address += 16; lines -= 1
|
92
|
+
end
|
93
|
+
|
94
|
+
# now do the remaining bytes, which don't fit a full line..
|
95
|
+
# yikes - this is truly ugly! REWRITE THIS!!
|
96
|
+
|
97
|
+
if rest > 0
|
98
|
+
chunks2,rest2 = rest.divmod(4)
|
99
|
+
j = i; k = 0
|
100
|
+
if (i < selfsize)
|
101
|
+
printf( "%08x ", address)
|
102
|
+
while (i < selfsize)
|
103
|
+
printf "%02x", self.getbyte(i)
|
104
|
+
i += 1; k += 1
|
105
|
+
print " " if ((i % 4) == 0)
|
106
|
+
end
|
107
|
+
for i in (k..15)
|
108
|
+
print " "
|
109
|
+
end
|
110
|
+
str = self[j..selfsize]
|
111
|
+
str.tr!("\000-\037\177-\377",'.')
|
112
|
+
print " " * (4 - chunks2+1)
|
113
|
+
printf(" %s\n", str)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
|
122
|
+
__END__
|
123
|
+
|
124
|
+
|
125
|
+
# you would use it like this:
|
126
|
+
|
127
|
+
require './hexdump'
|
128
|
+
|
129
|
+
s = "some random long string"
|
130
|
+
|
131
|
+
t = s + ZEROBYTE*80 + s + ZEROBYTE*64 + s + "bla bla bla!"
|
132
|
+
t.hexdump(true) # surpress consecutive lines with zero values
|
133
|
+
t.hexdump # same as t.hexdump(false)
|
134
|
+
|
135
|
+
|
136
|
+
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# ==============================================================================
|
2
|
+
# EXTENDING CLASS HASH
|
3
|
+
# ==============================================================================
|
4
|
+
#--
|
5
|
+
# (C) Copyright 2004 by Tilo Sloboda <tools@unixgods.org>
|
6
|
+
#
|
7
|
+
# updated: Time-stamp: <Mon, 24 Oct 2011, 23:03:29 PDT tilo>
|
8
|
+
#
|
9
|
+
# License:
|
10
|
+
# Freely available under the terms of the OpenSource "Artistic License"
|
11
|
+
# in combination with the Addendum A (below)
|
12
|
+
#
|
13
|
+
# In case you did not get a copy of the license along with the software,
|
14
|
+
# it is also available at: http://www.unixgods.org/~tilo/artistic-license.html
|
15
|
+
#
|
16
|
+
# Addendum A:
|
17
|
+
# THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU!
|
18
|
+
# SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
19
|
+
# REPAIR OR CORRECTION.
|
20
|
+
#
|
21
|
+
# IN NO EVENT WILL THE COPYRIGHT HOLDERS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
|
22
|
+
# SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY
|
23
|
+
# TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
|
24
|
+
# INACCURATE OR USELESS OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
25
|
+
# TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE COPYRIGHT HOLDERS OR OTHER PARTY HAS BEEN
|
26
|
+
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
27
|
+
#++
|
28
|
+
# ==============================================================================
|
29
|
+
|
30
|
+
# Project Homepage: http://www.unixgods.org/~tilo/Ruby/invert_hash.html
|
31
|
+
#
|
32
|
+
# This also appears in the "Facets of Ruby" library, and is mentioned in the O'Reilly Ruby Cookbook
|
33
|
+
#
|
34
|
+
# Ruby's Hash.invert method can't handle the common case that two or more hash entries have the same value.
|
35
|
+
#
|
36
|
+
# hash.invert.invert == h # => ? # is not generally true for Ruby's standard Hash#invert method
|
37
|
+
#
|
38
|
+
# hash.inverse.inverse == h # => true # is true, even if the hash has duplicate values
|
39
|
+
#
|
40
|
+
# If you have a Math background, you would expect that performing an "invert" operation twice would result in the original hash.
|
41
|
+
#
|
42
|
+
# The Hash#inverse method provides this.
|
43
|
+
#
|
44
|
+
#
|
45
|
+
# If you want to permanently overload Ruby's original invert method, you may want to do this:
|
46
|
+
#
|
47
|
+
# class Hash
|
48
|
+
# alias old_invert invert # old Hash#invert is still accessible as Hash#old_invert
|
49
|
+
#
|
50
|
+
# def invert
|
51
|
+
# self.inverse # Hash#invert is not using inverse method
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
|
55
|
+
class Hash
|
56
|
+
|
57
|
+
# Returns a new hash, using given hash's values as keys and using keys as values.
|
58
|
+
# If the input hash has duplicate values, the resulting hash will contain arrays as values.
|
59
|
+
# If you perform inverse twice, the output is identical to the original hash.
|
60
|
+
# e.g. no data is lost.
|
61
|
+
#
|
62
|
+
# hash = { 'zero' => 0 , 'one' => 1, 'two' => 2, 'three' => 3 , # English numbers
|
63
|
+
# 'null' => 0, 'eins' => 1, 'zwei' => 2 , 'drei' => 3 } # German numbers
|
64
|
+
#
|
65
|
+
# # Hash#inverse keeps track of duplicates, and preserves the input data
|
66
|
+
#
|
67
|
+
# hash.inverse # => { 0=>["null", "zero"], 1=>["eins", "one"], 2=>["zwei", "two"], 3=>["drei", "three"] }
|
68
|
+
#
|
69
|
+
# hash.inverse.inverse # => { "null"=>0, "zero"=>0, "eins"=>1, "one"=>1, "zwei"=>2, "two"=>2, "drei"=>3, "three"=>3 }
|
70
|
+
#
|
71
|
+
# hash.inverse.inverse == hash # => true # works as you'd expect
|
72
|
+
#
|
73
|
+
# # In Comparison:
|
74
|
+
# #
|
75
|
+
# # the standard Hash#invert loses data when dupclicate values are present
|
76
|
+
#
|
77
|
+
# hash.invert # => { 0=>"null", 1=>"eins", 2=>"zwei", 3=>"drei" }
|
78
|
+
# hash.invert.invert # => { "null"=>0, "eins"=>1, "zwei"=>2, "drei"=>3 } # data is lost
|
79
|
+
#
|
80
|
+
# hash.invert.invert == hash # => false # oops, data was lost!
|
81
|
+
#
|
82
|
+
|
83
|
+
def inverse
|
84
|
+
i = Hash.new
|
85
|
+
self.each_pair{ |k,v|
|
86
|
+
if (v.class == Array)
|
87
|
+
v.each{ |x|
|
88
|
+
if i.has_key?(x)
|
89
|
+
i[x] = [k,i[x]].flatten
|
90
|
+
else
|
91
|
+
i[x] = k
|
92
|
+
end
|
93
|
+
}
|
94
|
+
else
|
95
|
+
if i.has_key?(v)
|
96
|
+
i[v] = [k,i[v]].flatten
|
97
|
+
else
|
98
|
+
i[v] = k
|
99
|
+
end
|
100
|
+
end
|
101
|
+
}
|
102
|
+
return i
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
#--
|
109
|
+
#
|
110
|
+
# require 'active_support'
|
111
|
+
#
|
112
|
+
# class Hash
|
113
|
+
#
|
114
|
+
# def inverse
|
115
|
+
# i = ActiveSupport::OrderedHash.new
|
116
|
+
# self.each_pair{ |k,v|
|
117
|
+
# if (v.class == Array)
|
118
|
+
# v.each{ |x|
|
119
|
+
# i[x] = i.has_key?(x) ? [i[x],k].flatten : k
|
120
|
+
# }
|
121
|
+
# else
|
122
|
+
# i[v] = i.has_key?(v) ? [i[v],k].flatten : k
|
123
|
+
# end
|
124
|
+
# }
|
125
|
+
# return i
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# module ID3
|
2
|
+
# module Helpers
|
3
|
+
|
4
|
+
# ------------------------------------------------------------------------------
|
5
|
+
# recursiveDirectoryDescend
|
6
|
+
# do action for files matching regexp
|
7
|
+
#
|
8
|
+
# could be extended to array of (regexp,action) pairs
|
9
|
+
#
|
10
|
+
def recursive_dir_descend(dir,regexp,action)
|
11
|
+
# print "dir : #{dir}\n"
|
12
|
+
|
13
|
+
olddir = Dir.pwd
|
14
|
+
dirp = Dir.open(dir)
|
15
|
+
Dir.chdir(dir)
|
16
|
+
pwd = Dir.pwd
|
17
|
+
@dirN += 1
|
18
|
+
|
19
|
+
for file in dirp
|
20
|
+
file.chomp
|
21
|
+
next if file =~ /^\.\.?$/
|
22
|
+
filename = "#{pwd}/#{file}"
|
23
|
+
|
24
|
+
if File::directory?(filename)
|
25
|
+
recursive_dir_descend(filename,regexp,action)
|
26
|
+
else
|
27
|
+
@fileN += 1
|
28
|
+
if file =~ regexp
|
29
|
+
# evaluate action
|
30
|
+
eval action
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
Dir.chdir(olddir)
|
35
|
+
end
|
36
|
+
# ------------------------------------------------------------------------------
|
37
|
+
|
38
|
+
# end
|
39
|
+
# end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'id3/frame_array'
|
2
|
+
|
3
|
+
# ==============================================================================
|
4
|
+
# Class RestrictedOrderedHash
|
5
|
+
# this is a helper Class for ID3::Frame
|
6
|
+
#
|
7
|
+
# this is a helper Class for GenericTag
|
8
|
+
#
|
9
|
+
# this is from 2002 .. new Ruby Versions now have "OrderedHash" .. but I'll keep this class for now.
|
10
|
+
|
11
|
+
class RestrictedOrderedHash < ActiveSupport::OrderedHash
|
12
|
+
|
13
|
+
attr_accessor :locked
|
14
|
+
|
15
|
+
def lock
|
16
|
+
@locked = true
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@locked = false
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
alias old_store []=
|
25
|
+
|
26
|
+
def []= (key,val)
|
27
|
+
if self[key]
|
28
|
+
# self.old_store(key,val) # this would overwrite the old_value if a key already exists (duplicate ID3-Frames)
|
29
|
+
|
30
|
+
# strictly speaking, we only need this for the ID3v2 Tag class Tag2:
|
31
|
+
if self[key].class != ID3::FrameArray # Make this ID3::FrameArray < Array
|
32
|
+
old_value = self[key]
|
33
|
+
new_value = ID3::FrameArray.new
|
34
|
+
new_value << old_value # make old_value a FrameArray
|
35
|
+
self.old_store(key, new_value )
|
36
|
+
end
|
37
|
+
self[key] << val
|
38
|
+
|
39
|
+
else
|
40
|
+
if @locked
|
41
|
+
# we're not allowed to add new keys!
|
42
|
+
raise ArgumentError, "You can not add new keys! The ID3-frame #{@name} has fixed entries!\n" +
|
43
|
+
" valid key are: " + self.keys.join(",") +"\n"
|
44
|
+
else
|
45
|
+
self.old_store(key,val)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# users can not delete entries from a locked hash..
|
51
|
+
|
52
|
+
alias old_delete delete
|
53
|
+
|
54
|
+
def delete(key)
|
55
|
+
if !@locked
|
56
|
+
old_delete(key)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# ==============================================================================
|
63
|
+
# Class RestrictedOrderedHashWithMultipleValues
|
64
|
+
# this is a helper Class for ID3::Frame
|
65
|
+
#
|
66
|
+
# same as the parent class, but if a key is already present, it stores multiple values as an Array of values
|
67
|
+
|
68
|
+
# class RestrictedOrderedHashWithMultipleValues < RestrictedOrderedHash
|
69
|
+
# alias old_store2 []=
|
70
|
+
|
71
|
+
# # if key already in Hash, then replace value with [ old_value ] and append new value to it.
|
72
|
+
# def []= (key,val)
|
73
|
+
|
74
|
+
# puts "Key: #{key} , Val: #{val} , Class: #{self[key].class}"
|
75
|
+
|
76
|
+
# if self[key]
|
77
|
+
# if self[key].class == ID3::Frame
|
78
|
+
# old_value = self[key]
|
79
|
+
# self[key] = [ old_value ]
|
80
|
+
# end
|
81
|
+
# self[key] << value
|
82
|
+
# else
|
83
|
+
# self.old_store2(key,val)
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
|
87
|
+
# end
|
88
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# ==============================================================================
|
2
|
+
# Loading Libraries and Stuff needed for Ruby 1.9 vs 1.8 Compatibility
|
3
|
+
# ==============================================================================
|
4
|
+
# the idea here is to define a couple of go-between methods for different classes
|
5
|
+
# which are differently defined depending on which Ruby version it is -- thereby
|
6
|
+
# abstracting from the particular Ruby version's API of those classes
|
7
|
+
|
8
|
+
if RUBY_VERSION >= "1.9.0"
|
9
|
+
require "digest/md5"
|
10
|
+
require "digest/sha1"
|
11
|
+
include Digest
|
12
|
+
|
13
|
+
require 'fileutils' # replaces ftools
|
14
|
+
include FileUtils::Verbose
|
15
|
+
|
16
|
+
class File
|
17
|
+
def read_bytes(n) # returns a string containing bytes
|
18
|
+
# self.read(n)
|
19
|
+
# self.sysread(n)
|
20
|
+
self.bytes.take(n)
|
21
|
+
end
|
22
|
+
def write_bytes(bytes)
|
23
|
+
self.syswrite(bytes)
|
24
|
+
end
|
25
|
+
def get_byte
|
26
|
+
self.getbyte # returns a number 0..255
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
ZEROBYTE = "\x00".force_encoding(Encoding::BINARY) unless defined? ZEROBYTE
|
31
|
+
|
32
|
+
else # older Ruby versions:
|
33
|
+
require 'rubygems'
|
34
|
+
|
35
|
+
require "md5"
|
36
|
+
require "sha1"
|
37
|
+
|
38
|
+
require 'ftools'
|
39
|
+
def move(a,b)
|
40
|
+
File.move(a,b)
|
41
|
+
end
|
42
|
+
|
43
|
+
class String
|
44
|
+
def getbyte(x) # when accessing a string and selecting x-th byte to do calculations , as defined in Ruby 1.9
|
45
|
+
self[x] # returns an integer
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class File
|
50
|
+
def read_bytes(n)
|
51
|
+
self.read(n) # should use sysread here as well?
|
52
|
+
end
|
53
|
+
def write_bytes(bytes)
|
54
|
+
self.write(bytes) # should use syswrite here as well?
|
55
|
+
end
|
56
|
+
def get_byte # in older Ruby versions <1.9 getc returned a byte, e.g. a number 0..255
|
57
|
+
self.getc # returns a number 0..255
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
ZEROBYTE = "\0" unless defined? ZEROBYTE
|
62
|
+
end
|