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 CHANGED
Binary file
data/CHANGELOG CHANGED
@@ -1,4 +1,6 @@
1
1
 
2
+ v0.2.1. Better error handling.
3
+
2
4
  v0.2. Genre tagging; bugfixes.
3
5
 
4
6
  v0.1. First release.
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
- == Installation
27
+ == Usage
28
28
 
29
- Just install the gem:
29
+ First, install the gem:
30
30
  sudo gem install sweeper
31
31
 
32
- == Usage
33
-
34
- Change to the directory of mp3s you want to tag and run:
35
- sweeper
36
-
37
- That's all.
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 Directory to search (defaults to current).
44
- -r, --recursive Recurse directories.
45
- --dry-run Do a dry run (no files will be changed).
46
- -f, --force Overwrite all existing tags.
47
- -g, --genre Add genre and genre comments.
48
- -e, --force-genre Add genre and genre comments, overwriting e
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
@@ -1,3 +1,2 @@
1
1
 
2
- * Add genres via http://ws.audioscrobbler.com/1.0/artist/ARTIST/toptags.xml service.
3
- * Some kind of album detection?
2
+ * Album detection via set clustering on http://ws.audioscrobbler.com/1.0/album/ARTIST/ALBUM/info.xml .
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
- GENRES = ID3Lib::Info::Genres.sort
29
- GENRE_COUNT = 7
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) == ".mp3"
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 updated != current
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 #{File.basename(filename)}: #{e.message}"
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['force-genre'] or (GENRE_KEYS - tags.keys).any?)
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['force-genre']
118
- tags.merge!(updated.slice('genre', 'comment'))
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." unless object
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.each do |this|
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
- file.send("#{key}=", value)
219
+ song.send("#{key}=", value)
185
220
  puts " #{key.capitalize}: #{value}"
186
221
  end
187
- file.update! unless options['dry-run']
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
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sweeper
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.2"
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Weaver
metadata.gz.sig CHANGED
Binary file