sweeper 0.1 → 0.2

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,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