sweeper 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
data.tar.gz.sig CHANGED
Binary file
data/CHANGELOG CHANGED
@@ -1,2 +1,4 @@
1
1
 
2
+ v0.2. Genre tagging; bugfixes.
3
+
2
4
  v0.1. First release.
data/Manifest CHANGED
@@ -7,6 +7,7 @@ LICENSE
7
7
  Manifest
8
8
  README
9
9
  test/integration/sweeper_test.rb
10
+ TODO
10
11
  vendor/lastfm.fpclient.beta2.linux-32/lastfmfpclient
11
12
  vendor/lastfm.fpclient.beta2.linux-32/readme.txt
12
13
  vendor/lastfm.fpclient.beta2.OSX-intel/lastfmfpclient
data/README CHANGED
@@ -3,7 +3,7 @@ Automatically tag your music collection with metadata from Last.fm.
3
3
 
4
4
  == License
5
5
 
6
- Copyright 2008 Cloudburst, LLC. Licensed under the AFL 3. See the included LICENSE file. Unlinked portions copyright 2008 Last.fm Ltd., used by permission, and licensed under the GPL2.
6
+ Copyright 2008 Cloudburst, LLC. Licensed under the AFL 3. See the included LICENSE file. Unlinked portions copyright 2008 Last.fm Ltd., used by permission, and licensed under the GPL 2.
7
7
 
8
8
  The public certificate for the gem is here[http://rubyforge.org/frs/download.php/25331/evan_weaver-original-public_cert.pem].
9
9
 
@@ -11,15 +11,27 @@ If you use this software, please {make a donation}[http://blog.evanweaver.com/do
11
11
 
12
12
  == Features
13
13
 
14
- * options
15
- * stuff
14
+ * faster than MusicBrainz
15
+ * better results than MusicBrainz
16
+ * smart genre tagging
16
17
 
17
- == Usage
18
+ <b>WARNING: This is beta software. Backup your music collection first.</b>
19
+
20
+ == Requirements
21
+
22
+ * Linux, Intel OS X, or Windows
23
+ * Ruby and Rubygems
24
+ * id3lib (Linux and OS X only)
25
+ * some mp3 files
18
26
 
19
- First, install the gem:
27
+ == Installation
28
+
29
+ Just install the gem:
20
30
  sudo gem install sweeper
21
31
 
22
- Now, change to the directory of mp3s you want to tag and run:
32
+ == Usage
33
+
34
+ Change to the directory of mp3s you want to tag and run:
23
35
  sweeper
24
36
 
25
37
  That's all.
@@ -31,8 +43,10 @@ Sweeper takes a few command line options:
31
43
  -d, --dir Directory to search (defaults to current).
32
44
  -r, --recursive Recurse directories.
33
45
  --dry-run Do a dry run (no files will be changed).
34
- -f, --force Overwrite existing tags.
35
-
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
+
36
50
  == Reporting problems
37
51
 
38
52
  The support forum is here[http://rubyforge.org/forum/forum.php?forum_id=23599].
data/TODO ADDED
@@ -0,0 +1,3 @@
1
+
2
+ * Add genres via http://ws.audioscrobbler.com/1.0/artist/ARTIST/toptags.xml service.
3
+ * Some kind of album detection?
data/bin/sweeper CHANGED
@@ -19,7 +19,7 @@ Choice.options do
19
19
  desc 'Recurse directories.'
20
20
  end
21
21
 
22
- option :dry do
22
+ option :"dry-run" do
23
23
  long '--dry-run'
24
24
  desc 'Do a dry run (no files will be changed).'
25
25
  end
@@ -27,14 +27,20 @@ Choice.options do
27
27
  option :force do
28
28
  short '-f'
29
29
  long '--force'
30
- desc 'Overwrite existing tags.'
30
+ desc 'Overwrite all existing tags.'
31
31
  end
32
32
 
33
- # option :genre do
34
- # short '-g'
35
- # long '--genre'
36
- # desc 'Add genre tags from Last.fm.'
37
- # end
33
+ option :genre do
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
38
44
  end
39
45
 
40
46
  Sweeper.new(Choice.choices).run
data/lib/sweeper.rb CHANGED
@@ -3,31 +3,59 @@ require 'rubygems'
3
3
  require 'id3lib'
4
4
  require 'xsd/mapping'
5
5
  require 'activesupport'
6
+ require 'open-uri'
7
+ require 'uri'
8
+ require 'amatch'
6
9
 
7
10
  class ID3Lib::Tag
8
- alias :url :comment
9
- alias :url= :comment=
11
+ def url
12
+ f = frame(:WORS)
13
+ f ? f[:url] : nil
14
+ end
15
+
16
+ def url=(s)
17
+ remove_frame(:WORS)
18
+ self << {:id => :WORS, :url => s} if s.any?
19
+ end
10
20
  end
11
21
 
12
22
  class Sweeper
13
23
 
14
24
  class Problem < RuntimeError; end
15
25
 
16
- KEYS = ['artist', 'title', 'url']
26
+ BASIC_KEYS = ['artist', 'title', 'url']
27
+ GENRE_KEYS = ['genre', 'comment']
28
+ GENRES = ID3Lib::Info::Genres.sort
29
+ GENRE_COUNT = 7
30
+ DEFAULT_GENRE = {'genre' => 'Other', 'comment' => 'other'}
17
31
 
18
32
  attr_reader :options
19
33
 
20
34
  def initialize(options = {})
35
+ options['genre'] ||= options['force-genre']
21
36
  @dir = File.expand_path(options['dir'] || Dir.pwd)
22
37
  @options = options
23
38
  end
24
39
 
25
- def run
26
- @processed = 0
27
- recurse(@dir)
28
- if @processed == 0
29
- puts "No files found."
30
- exec "#{$0} --help"
40
+ def run
41
+ @read = 0
42
+ @updated = 0
43
+ @failed = 0
44
+
45
+ Kernel.at_exit do
46
+ if @read == 0
47
+ puts "No files found. Maybe you meant --recursive?"
48
+ exec "#{$0} --help"
49
+ else
50
+ puts "Read: #{@read}\nUpdated: #{@updated}\nFailed: #{@failed}"
51
+ end
52
+ end
53
+
54
+ begin
55
+ recurse(@dir)
56
+ rescue Object => e
57
+ puts "Unknown error: #{e.inspect}"
58
+ ENV['DEBUG'] ? raise : exit
31
59
  end
32
60
  end
33
61
 
@@ -38,19 +66,21 @@ class Sweeper
38
66
  if File.directory? filename and options['recursive']
39
67
  recurse(filename)
40
68
  elsif File.extname(filename) == ".mp3"
41
- @processed += 1
69
+ @read += 1
42
70
  tries = 0
43
71
  begin
44
- current = read(filename)
45
-
46
- if options['force']
47
- write(filename, lookup(filename))
48
- elsif current.keys.size < KEYS.size
49
- write(filename, lookup(filename).except(*current.keys))
72
+ current = read(filename)
73
+ updated = lookup(filename, current)
74
+
75
+ if updated != current
76
+ write(filename, updated)
77
+ @updated += 1
50
78
  end
79
+
51
80
  rescue Problem => e
52
81
  tries += 1 and retry if tries < 2
53
- puts "Skipped #{filename}: #{e.message}"
82
+ puts "Skipped #{File.basename(filename)}: #{e.message}"
83
+ @failed += 1
54
84
  end
55
85
  end
56
86
  end
@@ -58,44 +88,103 @@ class Sweeper
58
88
 
59
89
  def read(filename)
60
90
  tags = {}
61
- song = ID3Lib::Tag.new(filename)
91
+
92
+ song = ID3Lib::Tag.new(filename, ID3Lib::V2)
93
+ if song.empty?
94
+ song = ID3Lib::Tag.new(filename, ID3Lib::V1)
95
+ end
62
96
 
63
- KEYS.each do |key|
97
+ (BASIC_KEYS + GENRE_KEYS).each do |key|
64
98
  tags[key] = song.send(key) if !song.send(key).blank?
65
99
  end
100
+
66
101
  tags
67
102
  end
68
103
 
69
- def lookup(filename)
104
+ def lookup(filename, tags = {})
105
+ updated = {}
106
+ if options['force'] or
107
+ (BASIC_KEYS - tags.keys).any?
108
+ updated.merge!(lookup_basic(filename))
109
+ end
110
+ if options['genre'] and
111
+ (options['force'] or options['force-genre'] or (GENRE_KEYS - tags.keys).any?)
112
+ updated.merge!(lookup_genre(updated.merge(tags)))
113
+ end
114
+
115
+ if options['force']
116
+ tags.merge!(updated)
117
+ elsif options['force-genre']
118
+ tags.merge!(updated.slice('genre', 'comment'))
119
+ end
120
+
121
+ updated.merge(tags)
122
+ end
123
+
124
+ def lookup_basic(filename)
70
125
  Dir.chdir File.dirname(binary) do
71
- response = silence { `./#{File.basename(binary)} #{filename}` }
126
+ response = silence { `./#{File.basename(binary)} #{filename.inspect}` }
72
127
  object = begin
73
128
  XSD::Mapping.xml2obj(response)
74
129
  rescue REXML::ParseException
75
- raise Problem, "Invalid response."
130
+ raise Problem, "Server sent invalid response."
76
131
  end
77
- raise Problem, "Not found." unless object
132
+ raise Problem, "Fingerprint failed or not found." unless object
78
133
 
79
134
  tags = {}
80
135
  song = Array(object.track).first
81
136
 
82
- KEYS.each do |key|
137
+ BASIC_KEYS.each do |key|
83
138
  tags[key] = song.send(key) if song.respond_to? key
84
139
  end
85
140
  tags
86
141
  end
87
142
  end
88
143
 
144
+ def lookup_genre(tags)
145
+ return DEFAULT_GENRE if tags['artist'].blank?
146
+
147
+ response = begin
148
+ open("http://ws.audioscrobbler.com/1.0/artist/#{URI.encode(tags['artist'])}/toptags.xml").read
149
+ rescue OpenURI::HTTPError
150
+ return DEFAULT_GENRE
151
+ end
152
+
153
+ object = XSD::Mapping.xml2obj(response)
154
+ return DEFAULT_GENRE if !object.respond_to? :tag
155
+
156
+ genres = Array(object.tag)[0..(GENRE_COUNT - 1)].map(&:name)
157
+ return DEFAULT_GENRE if !genres.any?
158
+
159
+ primary = nil
160
+ genres.each do |this|
161
+ match_results = Amatch::Levenshtein.new(this).similar(GENRES)
162
+ max = match_results.max
163
+ match = GENRES[match_results.index(max)]
164
+
165
+ if ['Rock', 'Pop', 'Rap'].include? match
166
+ # Penalize useless genres
167
+ max = max / 3.0
168
+ end
169
+
170
+ if !primary or primary.first < max
171
+ primary = [max, match]
172
+ end
173
+ end
174
+
175
+ {'genre' => primary.last, 'comment' => genres.join(" ")}
176
+ end
177
+
89
178
  def write(filename, tags)
90
179
  return if tags.empty?
91
- puts filename
180
+ puts File.basename(filename)
92
181
 
93
- file = ID3Lib::Tag.new(filename)
182
+ file = ID3Lib::Tag.new(filename, ID3Lib::V2)
94
183
  tags.each do |key, value|
95
184
  file.send("#{key}=", value)
96
- puts " #{value}"
185
+ puts " #{key.capitalize}: #{value}"
97
186
  end
98
- file.update! unless options['dry']
187
+ file.update! unless options['dry-run']
99
188
  end
100
189
 
101
190
  def binary
data/sweeper.gemspec CHANGED
@@ -1,10 +1,10 @@
1
1
 
2
- # Gem::Specification for Sweeper-0.1
2
+ # Gem::Specification for Sweeper-0.2
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.1"
7
+ s.version = "0.2"
8
8
 
9
9
  s.specification_version = 2 if s.respond_to? :specification_version=
10
10
 
@@ -16,19 +16,20 @@ Gem::Specification.new do |s|
16
16
  s.email = %q{}
17
17
  s.executables = ["sweeper"]
18
18
  s.extensions = ["ext/extconf.rb"]
19
- s.extra_rdoc_files = ["CHANGELOG", "lib/sweeper.rb", "LICENSE", "README"]
20
- s.files = ["bin/sweeper", "CHANGELOG", "ext/extconf.rb", "ext/Makefile", "lib/sweeper.rb", "LICENSE", "Manifest", "README", "test/integration/sweeper_test.rb", "vendor/lastfm.fpclient.beta2.linux-32/lastfmfpclient", "vendor/lastfm.fpclient.beta2.linux-32/readme.txt", "vendor/lastfm.fpclient.beta2.OSX-intel/lastfmfpclient", "vendor/lastfm.fpclient.beta2.OSX-intel/libcurl.dylib", "vendor/lastfm.fpclient.beta2.OSX-intel/libmad.dylib", "vendor/lastfm.fpclient.beta2.OSX-intel/libsamplerate.dylib", "vendor/lastfm.fpclient.beta2.OSX-intel/libtag.dylib", "vendor/lastfm.fpclient.beta2.OSX-intel/readme.txt", "vendor/lastfm.fpclient.beta2.win32/lastfmfpclient.exe", "vendor/lastfm.fpclient.beta2.win32/libcurl.dll", "vendor/lastfm.fpclient.beta2.win32/libfftw3f-3.dll", "vendor/lastfm.fpclient.beta2.win32/Microsoft.VC80.CRT.manifest", "vendor/lastfm.fpclient.beta2.win32/msvcm80.dll", "vendor/lastfm.fpclient.beta2.win32/msvcp80.dll", "vendor/lastfm.fpclient.beta2.win32/msvcr80.dll", "vendor/lastfm.fpclient.beta2.win32/taglib.dll", "vendor/lastfm.fpclient.beta2.win32/zlib1.dll", "sweeper.gemspec"]
19
+ s.extra_rdoc_files = ["CHANGELOG", "lib/sweeper.rb", "LICENSE", "README", "TODO"]
20
+ s.files = ["bin/sweeper", "CHANGELOG", "ext/extconf.rb", "ext/Makefile", "lib/sweeper.rb", "LICENSE", "Manifest", "README", "test/integration/sweeper_test.rb", "TODO", "vendor/lastfm.fpclient.beta2.linux-32/lastfmfpclient", "vendor/lastfm.fpclient.beta2.linux-32/readme.txt", "vendor/lastfm.fpclient.beta2.OSX-intel/lastfmfpclient", "vendor/lastfm.fpclient.beta2.OSX-intel/libcurl.dylib", "vendor/lastfm.fpclient.beta2.OSX-intel/libmad.dylib", "vendor/lastfm.fpclient.beta2.OSX-intel/libsamplerate.dylib", "vendor/lastfm.fpclient.beta2.OSX-intel/libtag.dylib", "vendor/lastfm.fpclient.beta2.OSX-intel/readme.txt", "vendor/lastfm.fpclient.beta2.win32/lastfmfpclient.exe", "vendor/lastfm.fpclient.beta2.win32/libcurl.dll", "vendor/lastfm.fpclient.beta2.win32/libfftw3f-3.dll", "vendor/lastfm.fpclient.beta2.win32/Microsoft.VC80.CRT.manifest", "vendor/lastfm.fpclient.beta2.win32/msvcm80.dll", "vendor/lastfm.fpclient.beta2.win32/msvcp80.dll", "vendor/lastfm.fpclient.beta2.win32/msvcr80.dll", "vendor/lastfm.fpclient.beta2.win32/taglib.dll", "vendor/lastfm.fpclient.beta2.win32/zlib1.dll", "sweeper.gemspec"]
21
21
  s.has_rdoc = true
22
22
  s.homepage = %q{http://blog.evanweaver.com/files/doc/fauna/sweeper/}
23
23
  s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Sweeper", "--main", "README"]
24
24
  s.require_paths = ["lib", "ext"]
25
25
  s.rubyforge_project = %q{fauna}
26
- s.rubygems_version = %q{1.0.1}
26
+ s.rubygems_version = %q{1.1.0}
27
27
  s.summary = %q{Automatically tag your music collection with metadata from Last.fm.}
28
28
  s.test_files = ["test/integration/sweeper_test.rb"]
29
29
 
30
30
  s.add_dependency(%q<id3lib-ruby>, [">= 0"])
31
31
  s.add_dependency(%q<choice>, [">= 0"])
32
+ s.add_dependency(%q<amatch>, [">= 0"])
32
33
  s.add_dependency(%q<activesupport>, [">= 0"])
33
34
  end
34
35
 
@@ -43,7 +44,7 @@ end
43
44
  # p.summary = "Automatically tag your music collection with metadata from Last.fm."
44
45
  # p.url = "http://blog.evanweaver.com/files/doc/fauna/sweeper/"
45
46
  # p.docs_host = "blog.evanweaver.com:~/www/bax/public/files/doc/"
46
- # p.dependencies = ['id3lib-ruby', 'choice', 'activesupport']
47
+ # p.dependencies = ['id3lib-ruby', 'choice', 'amatch', 'activesupport']
47
48
  # p.clean_pattern = ['doc', 'pkg', 'test/integration/songs']
48
49
  # p.rdoc_pattern = ['README', 'LICENSE', 'CHANGELOG', 'TODO', 'lib/*']
49
50
  # end
@@ -11,25 +11,36 @@ class SweeperTest < Test::Unit::TestCase
11
11
  @found_many = "#{@dir}/1_001.mp3"
12
12
  @found_one = "#{@dir}/1_010.mp3"
13
13
  @not_found = "#{@dir}/1_003.mp3"
14
- @s = Sweeper.new('dir' => @dir)
14
+ @s = Sweeper.new('dir' => @dir, 'genre' => true)
15
15
  end
16
16
 
17
- def test_lookup
17
+ def test_lookup_basic
18
18
  assert_equal(
19
19
  {"artist"=>"Photon Band",
20
20
  "title"=>"To Sing For You",
21
21
  "url"=>"http://www.last.fm/music/Photon+Band/_/To+Sing+For+You"},
22
- @s.lookup(@found_many))
22
+ @s.lookup_basic(@found_many))
23
23
  assert_equal(
24
24
  {"artist"=>"Various Artists - Vagabond Productions",
25
25
  "title"=>"Sugar Man - Tom Heyman",
26
26
  "url"=> "http://www.last.fm/music/Various+Artists+-+Vagabond+Productions/_/Sugar+Man+-+Tom+Heyman"},
27
- @s.lookup(@found_one))
27
+ @s.lookup_basic(@found_one))
28
28
  assert_raises(Sweeper::Problem) do
29
- @s.lookup(@not_found)
29
+ @s.lookup_basic(@not_found)
30
30
  end
31
31
  end
32
32
 
33
+ def test_lookup_genre
34
+ assert_equal(
35
+ {"genre"=>"Psychadelic", "comment"=>"rock psychedelic mod Philly"},
36
+ @s.lookup_genre(@s.lookup_basic(@found_many))
37
+ )
38
+ assert_equal(
39
+ Sweeper::DEFAULT_GENRE,
40
+ @s.lookup_genre({})
41
+ )
42
+ end
43
+
33
44
  def test_read
34
45
  assert_equal({},
35
46
  @s.read(@found_many))
File without changes
File without changes
File without changes
File without changes
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.1"
4
+ version: "0.2"
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Weaver
@@ -51,6 +51,15 @@ dependencies:
51
51
  - !ruby/object:Gem::Version
52
52
  version: "0"
53
53
  version:
54
+ - !ruby/object:Gem::Dependency
55
+ name: amatch
56
+ version_requirement:
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
54
63
  - !ruby/object:Gem::Dependency
55
64
  name: activesupport
56
65
  version_requirement:
@@ -71,6 +80,7 @@ extra_rdoc_files:
71
80
  - lib/sweeper.rb
72
81
  - LICENSE
73
82
  - README
83
+ - TODO
74
84
  files:
75
85
  - bin/sweeper
76
86
  - CHANGELOG
@@ -81,6 +91,7 @@ files:
81
91
  - Manifest
82
92
  - README
83
93
  - test/integration/sweeper_test.rb
94
+ - TODO
84
95
  - vendor/lastfm.fpclient.beta2.linux-32/lastfmfpclient
85
96
  - vendor/lastfm.fpclient.beta2.linux-32/readme.txt
86
97
  - vendor/lastfm.fpclient.beta2.OSX-intel/lastfmfpclient
@@ -127,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
138
  requirements: []
128
139
 
129
140
  rubyforge_project: fauna
130
- rubygems_version: 1.0.1
141
+ rubygems_version: 1.1.0
131
142
  signing_key:
132
143
  specification_version: 2
133
144
  summary: Automatically tag your music collection with metadata from Last.fm.
metadata.gz.sig CHANGED
Binary file