sweeper 0.2 → 0.2.1
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.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
|