sweeper 0.2 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/CHANGELOG +2 -0
- data/README +43 -15
- data/TODO +1 -2
- data/bin/sweeper +3 -9
- data/lib/sweeper.rb +73 -25
- data/sweeper.gemspec +2 -2
- data/test/integration/sweeper_test.rb +1 -1
- metadata +1 -1
- metadata.gz.sig +0 -0
data.tar.gz.sig
CHANGED
Binary file
|
data/CHANGELOG
CHANGED
data/README
CHANGED
@@ -24,29 +24,57 @@ If you use this software, please {make a donation}[http://blog.evanweaver.com/do
|
|
24
24
|
* id3lib (Linux and OS X only)
|
25
25
|
* some mp3 files
|
26
26
|
|
27
|
-
==
|
27
|
+
== Usage
|
28
28
|
|
29
|
-
|
29
|
+
First, install the gem:
|
30
30
|
sudo gem install sweeper
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
32
|
+
Now, change to the directory of mp3s you want to tag and run:
|
33
|
+
sweeper --genre --recursive
|
34
|
+
|
35
|
+
Your mp3s will be updated with missing artist, genre, and trackname information, and a list of all tagged genres will be added to the <tt>comment</tt> field, for use in smart playlists.
|
36
|
+
|
37
|
+
== Demo
|
38
|
+
|
39
|
+
Unknown song before sweeping:
|
40
|
+
|
41
|
+
$ id3info 1_001.mp3
|
42
|
+
*** Tag information for 1_001.mp3
|
43
|
+
*** mp3 info
|
44
|
+
MPEG1/layer III
|
45
|
+
Bitrate: 128KBps
|
46
|
+
Frequency: 44KHz
|
47
|
+
|
48
|
+
Same song after running <tt>sweeper ----genre</tt>:
|
49
|
+
|
50
|
+
$ id3info 1_001.mp3
|
51
|
+
*** Tag information for 1_001.mp3
|
52
|
+
=== TPE1 (Lead performer(s)/Soloist(s)): Photon Band
|
53
|
+
=== TIT2 (Title/songname/content description): To Sing For You
|
54
|
+
=== WORS (Official internet radio station homepage): http://www.last.fm/music/Ph
|
55
|
+
oton+Band/_/To+Sing+For+You
|
56
|
+
=== TCON (Content type): Psychadelic
|
57
|
+
=== COMM (Comments): ()[]: rock, psychedelic, mod, Philly
|
58
|
+
*** mp3 info
|
59
|
+
MPEG1/layer III
|
60
|
+
Bitrate: 128KBps
|
61
|
+
Frequency: 44KHz
|
38
62
|
|
39
63
|
== Options
|
40
64
|
|
41
65
|
Sweeper takes a few command line options:
|
42
66
|
|
43
|
-
-d, --dir
|
44
|
-
-r, --recursive
|
45
|
-
--dry-run
|
46
|
-
-f, --force
|
47
|
-
-g, --genre
|
48
|
-
|
49
|
-
|
67
|
+
-d, --dir Directory to search (defaults to current).
|
68
|
+
-r, --recursive Recurse directories.
|
69
|
+
--dry-run Do a dry run (no files will be changed).
|
70
|
+
-f, --force Overwrite all existing tags.
|
71
|
+
-g, --genre=[force] Add genre tag and genre comments, optionally overwriting
|
72
|
+
existing ones.
|
73
|
+
|
74
|
+
== Notes
|
75
|
+
|
76
|
+
You will get cleaner tags if you convert your files to ID3v2 first. Sweeper reads both tag versions, but only outputs ID3v2.
|
77
|
+
|
50
78
|
== Reporting problems
|
51
79
|
|
52
80
|
The support forum is here[http://rubyforge.org/forum/forum.php?forum_id=23599].
|
data/TODO
CHANGED
data/bin/sweeper
CHANGED
@@ -32,15 +32,9 @@ Choice.options do
|
|
32
32
|
|
33
33
|
option :genre do
|
34
34
|
short '-g'
|
35
|
-
long '--genre'
|
36
|
-
desc "Add genre and genre comments."
|
37
|
-
end
|
38
|
-
|
39
|
-
option :"force-genre" do
|
40
|
-
short '-e'
|
41
|
-
long '--force-genre'
|
42
|
-
desc "Add genre and genre comments, overwriting existing ones."
|
43
|
-
end
|
35
|
+
long '--genre=[force]'
|
36
|
+
desc "Add genre tag and genre comments, optionally overwriting existing ones."
|
37
|
+
end
|
44
38
|
end
|
45
39
|
|
46
40
|
Sweeper.new(Choice.choices).run
|
data/lib/sweeper.rb
CHANGED
@@ -25,18 +25,20 @@ class Sweeper
|
|
25
25
|
|
26
26
|
BASIC_KEYS = ['artist', 'title', 'url']
|
27
27
|
GENRE_KEYS = ['genre', 'comment']
|
28
|
-
|
29
|
-
|
28
|
+
ALBUM_KEYS = ['album', 'track']
|
29
|
+
GENRES = ID3Lib::Info::Genres
|
30
|
+
GENRE_COUNT = 10
|
30
31
|
DEFAULT_GENRE = {'genre' => 'Other', 'comment' => 'other'}
|
31
32
|
|
32
33
|
attr_reader :options
|
33
34
|
|
35
|
+
# Instantiate a new Sweeper. See <tt>bin/sweeper</tt> for <tt>options</tt> details.
|
34
36
|
def initialize(options = {})
|
35
|
-
options['genre'] ||= options['force-genre']
|
36
37
|
@dir = File.expand_path(options['dir'] || Dir.pwd)
|
37
38
|
@options = options
|
38
39
|
end
|
39
40
|
|
41
|
+
# Run the Sweeper according to the <tt>options</tt>.
|
40
42
|
def run
|
41
43
|
@read = 0
|
42
44
|
@updated = 0
|
@@ -44,7 +46,7 @@ class Sweeper
|
|
44
46
|
|
45
47
|
Kernel.at_exit do
|
46
48
|
if @read == 0
|
47
|
-
puts "No files found. Maybe you meant --recursive?"
|
49
|
+
puts "No mp3 files found. Maybe you meant --recursive?"
|
48
50
|
exec "#{$0} --help"
|
49
51
|
else
|
50
52
|
puts "Read: #{@read}\nUpdated: #{@updated}\nFailed: #{@failed}"
|
@@ -61,75 +63,99 @@ class Sweeper
|
|
61
63
|
|
62
64
|
#private
|
63
65
|
|
66
|
+
# Recurse one directory, reading, looking up, and writing each file, if appropriate. Accepts a directory path.
|
64
67
|
def recurse(dir)
|
68
|
+
# Hackishly avoid problems with metacharacters in the Dir[] string.
|
69
|
+
dir = dir.gsub(/[^\s\w\.\/\\\-]/, '?')
|
70
|
+
# p dir if ENV['DEBUG']
|
65
71
|
Dir["#{dir}/*"].each do |filename|
|
66
72
|
if File.directory? filename and options['recursive']
|
67
73
|
recurse(filename)
|
68
|
-
elsif File.extname(filename)
|
74
|
+
elsif File.extname(filename) =~ /\.mp3$/i
|
69
75
|
@read += 1
|
70
76
|
tries = 0
|
71
77
|
begin
|
72
78
|
current = read(filename)
|
73
79
|
updated = lookup(filename, current)
|
74
80
|
|
75
|
-
if
|
81
|
+
if ENV['DEBUG']
|
82
|
+
p current, updated
|
83
|
+
end
|
84
|
+
|
85
|
+
if updated != current
|
86
|
+
# Don't bother updating identical metadata.
|
76
87
|
write(filename, updated)
|
77
88
|
@updated += 1
|
89
|
+
else
|
90
|
+
puts "Unchanged: #{File.basename(filename)}"
|
78
91
|
end
|
79
92
|
|
80
93
|
rescue Problem => e
|
81
94
|
tries += 1 and retry if tries < 2
|
82
|
-
puts "Skipped #{
|
95
|
+
puts "Skipped (#{e.message}): #{File.basename(filename)}"
|
83
96
|
@failed += 1
|
84
97
|
end
|
85
98
|
end
|
86
|
-
end
|
99
|
+
end
|
87
100
|
end
|
88
101
|
|
102
|
+
# Read tags from an mp3 file. Returns a tag hash.
|
89
103
|
def read(filename)
|
90
104
|
tags = {}
|
91
|
-
|
92
|
-
song = ID3Lib::Tag.new(filename, ID3Lib::V2)
|
93
|
-
if song.empty?
|
94
|
-
song = ID3Lib::Tag.new(filename, ID3Lib::V1)
|
95
|
-
end
|
105
|
+
song = load(filename)
|
96
106
|
|
97
107
|
(BASIC_KEYS + GENRE_KEYS).each do |key|
|
98
108
|
tags[key] = song.send(key) if !song.send(key).blank?
|
99
109
|
end
|
100
110
|
|
111
|
+
# Change numeric genres into TCON strings
|
112
|
+
# XXX Might not work well
|
113
|
+
if tags['genre'] =~ /(\d+)/
|
114
|
+
tags['genre'] = GENRES[$1.to_i]
|
115
|
+
end
|
116
|
+
|
101
117
|
tags
|
102
118
|
end
|
103
119
|
|
120
|
+
# Lookup all available remote metadata for an mp3 file. Accepts a pathname and an optional hash of existing tags. Returns a tag hash.
|
104
121
|
def lookup(filename, tags = {})
|
122
|
+
tags = tags.dup
|
105
123
|
updated = {}
|
124
|
+
|
125
|
+
# Are there any empty basic tags we need to lookup?
|
106
126
|
if options['force'] or
|
107
127
|
(BASIC_KEYS - tags.keys).any?
|
108
128
|
updated.merge!(lookup_basic(filename))
|
109
129
|
end
|
130
|
+
|
131
|
+
# Are there any empty genre tags we need to lookup?
|
110
132
|
if options['genre'] and
|
111
|
-
(options['force'] or options['
|
133
|
+
(options['force'] or options['genre'] == 'force' or (GENRE_KEYS - tags.keys).any?)
|
112
134
|
updated.merge!(lookup_genre(updated.merge(tags)))
|
113
135
|
end
|
114
136
|
|
115
137
|
if options['force']
|
138
|
+
# Force all remote tags.
|
116
139
|
tags.merge!(updated)
|
117
|
-
elsif options['
|
118
|
-
tags.
|
140
|
+
elsif options['genre'] == 'force'
|
141
|
+
# Force remote genre tags only.
|
142
|
+
tags.merge!(updated.slice(*GENRE_KEYS))
|
119
143
|
end
|
120
144
|
|
145
|
+
# Merge back in existing tags.
|
121
146
|
updated.merge(tags)
|
122
147
|
end
|
123
148
|
|
149
|
+
# Lookup the basic metadata for an mp3 file. Accepts a pathname. Returns a tag hash.
|
124
150
|
def lookup_basic(filename)
|
125
151
|
Dir.chdir File.dirname(binary) do
|
126
152
|
response = silence { `./#{File.basename(binary)} #{filename.inspect}` }
|
127
153
|
object = begin
|
128
154
|
XSD::Mapping.xml2obj(response)
|
129
155
|
rescue REXML::ParseException
|
130
|
-
raise Problem, "Server sent invalid response
|
156
|
+
raise Problem, "Server sent invalid response"
|
131
157
|
end
|
132
|
-
raise Problem, "Fingerprint failed or not found
|
158
|
+
raise Problem, "Fingerprint failed or not found" unless object
|
133
159
|
|
134
160
|
tags = {}
|
135
161
|
song = Array(object.track).first
|
@@ -140,7 +166,8 @@ class Sweeper
|
|
140
166
|
tags
|
141
167
|
end
|
142
168
|
end
|
143
|
-
|
169
|
+
|
170
|
+
# Lookup the genre metadata for a set of basic metadata. Accepts a tag hash. Returns a genre tag hash.
|
144
171
|
def lookup_genre(tags)
|
145
172
|
return DEFAULT_GENRE if tags['artist'].blank?
|
146
173
|
|
@@ -157,36 +184,51 @@ class Sweeper
|
|
157
184
|
return DEFAULT_GENRE if !genres.any?
|
158
185
|
|
159
186
|
primary = nil
|
160
|
-
genres.
|
187
|
+
genres.each_with_index do |this, index|
|
161
188
|
match_results = Amatch::Levenshtein.new(this).similar(GENRES)
|
189
|
+
# Get the levenshtein best-match weight
|
162
190
|
max = match_results.max
|
191
|
+
# Reverse lookup the canonical genre
|
163
192
|
match = GENRES[match_results.index(max)]
|
193
|
+
# Bias slightly towards higher tagging counts
|
194
|
+
max += ((GENRE_COUNT - index) / GENRE_COUNT / 4.0)
|
164
195
|
|
165
196
|
if ['Rock', 'Pop', 'Rap'].include? match
|
166
197
|
# Penalize useless genres
|
167
198
|
max = max / 3.0
|
168
199
|
end
|
200
|
+
|
201
|
+
p [max, match] if ENV['DEBUG']
|
169
202
|
|
170
203
|
if !primary or primary.first < max
|
171
204
|
primary = [max, match]
|
172
205
|
end
|
173
206
|
end
|
174
207
|
|
175
|
-
{'genre' => primary.last, 'comment' => genres.join(" ")}
|
208
|
+
{'genre' => primary.last, 'comment' => genres.join(", ")}
|
176
209
|
end
|
177
210
|
|
211
|
+
# Write tags to an mp3 file. Accepts a pathname and a tag hash.
|
178
212
|
def write(filename, tags)
|
179
213
|
return if tags.empty?
|
180
|
-
puts File.basename(filename)
|
214
|
+
puts "Updated: #{File.basename(filename)}"
|
215
|
+
|
216
|
+
song = load(filename)
|
181
217
|
|
182
|
-
file = ID3Lib::Tag.new(filename, ID3Lib::V2)
|
183
218
|
tags.each do |key, value|
|
184
|
-
|
219
|
+
song.send("#{key}=", value)
|
185
220
|
puts " #{key.capitalize}: #{value}"
|
186
221
|
end
|
187
|
-
|
222
|
+
ALBUM_KEYS.each do |key|
|
223
|
+
puts " #{key.capitalize}: #{song.send(key)}"
|
224
|
+
end
|
225
|
+
|
226
|
+
unless options['dry-run']
|
227
|
+
song.update!(ID3Lib::V2)
|
228
|
+
end
|
188
229
|
end
|
189
230
|
|
231
|
+
# Returns the path to the fingerprinter binary for this platform.
|
190
232
|
def binary
|
191
233
|
@binary ||= "#{File.dirname(__FILE__)}/../vendor/" +
|
192
234
|
case RUBY_PLATFORM
|
@@ -199,6 +241,12 @@ class Sweeper
|
|
199
241
|
end
|
200
242
|
end
|
201
243
|
|
244
|
+
# Loads metadata for an mp3 file. Looks for which ID3 version is already populated, instead of just the existence of frames.
|
245
|
+
def load(filename)
|
246
|
+
ID3Lib::Tag.new(filename, ID3Lib::V_ALL)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Silence STDERR and STDOUT for the duration of the block.
|
202
250
|
def silence(outf = nil, errf = nil)
|
203
251
|
# This method is annoying.
|
204
252
|
outf, errf = Tempfile.new("stdout"), Tempfile.new("stderr")
|
data/sweeper.gemspec
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
|
2
|
-
# Gem::Specification for Sweeper-0.2
|
2
|
+
# Gem::Specification for Sweeper-0.2.1
|
3
3
|
# Originally generated by Echoe
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = %q{sweeper}
|
7
|
-
s.version = "0.2"
|
7
|
+
s.version = "0.2.1"
|
8
8
|
|
9
9
|
s.specification_version = 2 if s.respond_to? :specification_version=
|
10
10
|
|
@@ -32,7 +32,7 @@ class SweeperTest < Test::Unit::TestCase
|
|
32
32
|
|
33
33
|
def test_lookup_genre
|
34
34
|
assert_equal(
|
35
|
-
{"genre"=>"Psychadelic", "comment"=>"rock psychedelic mod Philly"},
|
35
|
+
{"genre"=>"Psychadelic", "comment"=>"rock, psychedelic, mod, Philly"},
|
36
36
|
@s.lookup_genre(@s.lookup_basic(@found_many))
|
37
37
|
)
|
38
38
|
assert_equal(
|
metadata
CHANGED
metadata.gz.sig
CHANGED
Binary file
|