imp3 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -13,18 +13,42 @@ iMp3 is an application for batch processing and fixing common issues when dealin
13
13
 
14
14
  == SYNOPSIS
15
15
 
16
- Application for batch processing and fixing common issues when dealing with a large iTunes library
17
-
18
- Commands:
19
- fix genres Tags all tracks genres using last.fm
20
- fix misspelled-artists Fixes misspelled artist names
21
- help Display global or [command] help documentation.
22
- stats genres Lists all genres tagged in iTunes
23
-
24
- Global Options:
25
- -h, --help Display help documentation
26
- -v, --version Display version information
27
- -t, --trace Display backtrace when an error occurs
16
+ NAME:
17
+
18
+ imp3
19
+
20
+ DESCRIPTION:
21
+
22
+ Application for batch processing and fixing common issues when dealing with a large iTunes library
23
+
24
+ COMMANDS:
25
+
26
+ artists misspelled Fix misspelled artist names
27
+ genres fetch Fetch last.fm artist top tags and tag track genre
28
+ genres fetch-cache Lists genres present on the cache and not in the ignore list
29
+ genres ignore-add Add specified genre to ignore list so is no longer used for tagging tracks
30
+ genres ignore-del Remove specified genre from ignore list
31
+ genres ignore-list Lists all ignored genres
32
+ genres list Lists all genres tagged in iTunes
33
+ help Display global or [command] help documentation.
34
+
35
+ GLOBAL OPTIONS:
36
+
37
+ -s, --selection
38
+ Apply only to current iTunes selection
39
+
40
+ -h, --help
41
+ Display help documentation
42
+
43
+ -v, --version
44
+ Display version information
45
+
46
+ -t, --trace
47
+ Display backtrace when an error occurs
48
+
49
+ AUTHORS:
50
+
51
+ Víctor Martínez
28
52
 
29
53
  == FEATURES
30
54
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.2
data/bin/imp3 CHANGED
@@ -10,39 +10,52 @@ require 'commander/import'
10
10
  program :name, 'imp3'
11
11
  program :version, IMP3::VERSION
12
12
  program :description, 'Application for batch processing and fixing common issues when dealing with a large iTunes library'
13
- program :help_formatter, :compact
13
+ program :help, 'Authors', 'Víctor Martínez'
14
14
 
15
15
  default_command :help
16
16
 
17
+ global_option('-s', '--selection', "Apply only to current iTunes selection") { IMP3::CLI.instance.source_origin = :selection }
18
+
17
19
  command "genres list" do |c|
18
20
  c.syntax = 'genres list'
19
21
  c.description = 'Lists all genres tagged in iTunes'
20
22
  c.when_called do |args, options|
21
- IMP3::CLI.new.genres_list
23
+ IMP3::CLI.instance.genres_list_command
22
24
  end
23
25
  end
24
26
 
25
27
  command "genres fetch" do |c|
26
- c.syntax = 'genres fetch'
27
- c.description = 'Fetch and tag track genres from last.fm top tag'
28
+ c.syntax = 'genres fetch [options]'
29
+ c.description = 'Fetch last.fm artist top tags and tag track genre'
30
+ c.option '--flush-cache', 'Re-create last.fm artist tags cache'
31
+ c.when_called do |args, options|
32
+ IMP3::CLI.instance.genres_fetch_command(options)
33
+ end
34
+ end
35
+
36
+ command "genres fetch-cache" do |c|
37
+ c.syntax = 'genres fetch-cache [options]'
38
+ c.description = 'Lists genres present on the cache and not in the ignore list'
39
+ c.option '--limit VALUE', Integer, 'Limit to [VALUE] results'
28
40
  c.when_called do |args, options|
29
- IMP3::CLI.new.genres_tag
41
+ options.default :limit => 25
42
+ IMP3::CLI.instance.genres_fetch_cache_command(options)
30
43
  end
31
44
  end
32
45
 
33
46
  command "genres ignore-add" do |c|
34
47
  c.syntax = 'genres ignore-add GENRE'
35
- c.description = 'Ignore specified genre so is no longer used and the next one is taken for tagging tracks'
48
+ c.description = 'Add specified genre to ignore list so is no longer used for tagging tracks'
36
49
  c.when_called do |args, options|
37
- IMP3::CLI.new.genres_ignore_add(args.first)
50
+ IMP3::CLI.instance.genres_ignore_add_command(args.first)
38
51
  end
39
52
  end
40
53
 
41
54
  command "genres ignore-del" do |c|
42
55
  c.syntax = 'genres ignore-del GENRE'
43
- c.description = 'Ignore specified genre so is no longer used and the next one is taken for tagging tracks'
56
+ c.description = 'Remove specified genre from ignore list'
44
57
  c.when_called do |args, options|
45
- IMP3::CLI.new.genres_ignore_del(args.first)
58
+ IMP3::CLI.instance.genres_ignore_del_command(args.first)
46
59
  end
47
60
  end
48
61
 
@@ -50,7 +63,7 @@ command "genres ignore-list" do |c|
50
63
  c.syntax = 'genres ignore-list'
51
64
  c.description = 'Lists all ignored genres'
52
65
  c.when_called do |args, options|
53
- IMP3::CLI.new.genres_ignore_list
66
+ IMP3::CLI.instance.genres_ignore_list_command
54
67
  end
55
68
  end
56
69
 
@@ -58,6 +71,6 @@ command "artists misspelled" do |c|
58
71
  c.syntax = 'artists misspelled'
59
72
  c.description = 'Fix misspelled artist names'
60
73
  c.when_called do |args, options|
61
- IMP3::CLI.new.artists_misspelled
74
+ IMP3::CLI.instance.artists_misspelled_command
62
75
  end
63
76
  end
data/lib/imp3/cache.rb ADDED
@@ -0,0 +1,28 @@
1
+ require "singleton"
2
+
3
+ class IMP3::Cache
4
+ include Singleton
5
+
6
+ CACHE_FILE = File.join(IMP3::APP_DIR, "cache.yml")
7
+
8
+ def initialize
9
+ @data = {}
10
+
11
+ if File.exist?(CACHE_FILE)
12
+ begin
13
+ @data = YAML.load_file(CACHE_FILE)
14
+ rescue
15
+ raise "Unable to read cache file #{CACHE_FILE}"
16
+ end
17
+ end
18
+ end
19
+
20
+ def artist_genres
21
+ @data[:artist_genres] ||= {}
22
+ @data[:artist_genres]
23
+ end
24
+
25
+ def save
26
+ File.new(CACHE_FILE, "w+").write(@data.to_yaml)
27
+ end
28
+ end
data/lib/imp3/cli.rb CHANGED
@@ -7,166 +7,55 @@ require 'friendly_id/slug_string'
7
7
  require 'rbosa'
8
8
  require 'pp'
9
9
  require 'commander/user_interaction'
10
+ require 'singleton'
11
+ require 'lib/imp3/config'
12
+ require 'lib/imp3/cache'
13
+ require 'lib/imp3/commands.rb'
10
14
 
11
15
  OSA.utf8_strings = true
12
16
 
13
17
  class IMP3::CLI
14
- CONFIG_FILE = File.expand_path(File.join("~", ".imp3"))
18
+ include Singleton
19
+ attr_reader :config, :cache
20
+ attr_accessor :source_origin
15
21
 
16
- def genres_tag
17
- artist_genres = {}
18
- tagged, requests, errors = 0, 0, 0
19
-
20
- progress_bar tracks do |track, bar|
21
- normalized_artist_name = track.artist.normalize
22
-
23
- begin
24
- unless artist_genres.has_key?(normalized_artist_name)
25
- requests += 1
26
- bar.refresh(:title => "Quering last.fm for '#{track.artist}'")
27
- tags = Nokogiri(open("http://ws.audioscrobbler.com/1.0/artist/#{URI.escape(CGI.escape(track.artist))}/toptags.xml")).search("//toptags/tag/name").map{|t| t.text.normalize }.uniq
28
- tags = tags - config[:ignore_genres] if config[:ignore_genres]
29
- artist_genres[normalized_artist_name] = tags.first
30
- end
31
- rescue => e
32
- errors += 1
33
- bar.refresh(:title => e)
34
- artist_genres[normalized_artist_name] = nil
35
- end
36
-
37
- if (artist_genres.has_key?(normalized_artist_name) and not artist_genres[normalized_artist_name].nil?)
38
- tagged += 1
39
- bar.increment(:title => "Tagging track #{track.persistent_id} with genre '#{artist_genres[normalized_artist_name]}'")
40
- track.genre = ""
41
- track.genre = artist_genres[normalized_artist_name]
42
- end
43
-
44
- end
45
- summary(:tracks_tagged => tagged, :requests => requests, :errors => errors)
46
- end
47
-
48
- def genres_list
49
- genres = {}
50
- tracks.each do |track|
51
- genres[track.genre] ||= 0
52
- genres[track.genre] += 1
53
- end
54
-
55
- genre_table = table do |t|
56
- t.headings = 'Genre', 'Tracks'
57
- genres.sort{|a, b| b[1] <=> a[1]}.each do |g, c|
58
- t << [g, c]
59
- end
60
- end
61
-
62
- puts genre_table
63
- summary
64
- end
65
-
66
- def genres_ignore_add(genre)
67
- raise "Please specify a genre" unless genre
68
- genre.strip!
69
- config[:ignore_genres] ||= []
70
- config[:ignore_genres] << genre unless config[:ignore_genres].include?(genre)
71
- save_config
72
- puts "Genre '#{genre}' added to ignore list"
73
- end
74
-
75
- def genres_ignore_del(genre)
76
- raise "Please specify a genre" unless genre
77
- genre.strip!
78
- config[:ignore_genres].delete(genre) if config[:ignore_genres].include?(genre)
79
- save_config
80
- puts "Genre '#{genre}' deleted from ignore list"
81
- end
82
-
83
- def genres_ignore_list
84
- if config[:ignore_genres].any?
85
- genre_table = table do |t|
86
- t.headings = 'Genre'
87
- config[:ignore_genres].each do |g|
88
- t << [g]
89
- end
90
- end
91
- puts genre_table
92
- else
93
- puts "No ignored genres."
94
- end
22
+ def initialize
23
+ @source_origin = :library
24
+ @config = IMP3::Config.instance
25
+ @cache = IMP3::Cache.instance
95
26
  end
96
27
 
97
- def artists_misspelled
98
- artist_choices = {}
99
- tagged = 0
100
-
101
- progress_bar artists, :complete_message => "Misspelled artist name scan complete." do |artist, bar|
102
- bar.increment :title => "Scanning artist '#{artist}'"
103
- normalized_artist_name = artist.normalized_without_words
104
- artist_choices[normalized_artist_name] ||= []
105
- artist_choices[normalized_artist_name] << artist.strip unless artist_choices[normalized_artist_name].include?(artist)
106
- end
107
-
108
- artist_names = {}
109
- tracks.each do |track|
110
- normalized_artist_name = track.artist.normalized_without_words
111
- unless artist_names.has_key?(normalized_artist_name)
112
- if artist_choices[normalized_artist_name] && artist_choices[normalized_artist_name].size > 1
113
- puts
114
- artist_name = choose("What is the correct artist name for #{track.artist}", *(artist_choices[normalized_artist_name] << :"(Skip)"))
115
- artist_names[normalized_artist_name] = artist_name
116
- else
117
- artist_names[normalized_artist_name] = track.artist
118
- end
119
- end
120
-
121
- unless artist_names[normalized_artist_name].eql?(:"(Skip)") or track.artist.eql?(artist_names[normalized_artist_name])
122
- tagged += 1
123
- puts "Tagging track #{track.persistent_id} with artist name '#{artist_names[normalized_artist_name]}'"
124
- track.artist = ""
125
- track.artist = artist_names[normalized_artist_name]
126
- end
127
- end
128
-
129
- summary :tracks_tagged => tagged
130
- end
28
+ include IMP3::Commands
131
29
 
132
30
  protected
133
31
 
134
32
  def itunes
135
- @itunes ||= OSA.app('iTunes')
136
- end
137
-
138
- def library
139
- @library ||= itunes.sources.find {|s| s.kind == OSA::ITunes::ESRC::LIBRARY }.playlists[0]
33
+ OSA.app('iTunes') rescue raise "Can't locate iTunes! It's installed?"
140
34
  end
141
35
 
142
36
  def tracks
143
- @tracks ||= library.tracks
37
+ case source_origin
38
+ when :library
39
+ @library ||= itunes.sources.find {|s| s.kind == OSA::ITunes::ESRC::LIBRARY }.playlists[0] rescue raise "Unable to contact iTunes, please make sure it is open."
40
+ @library.tracks
41
+ when :selection
42
+ itunes.selection
43
+ else
44
+ raise "Invalid source origin: #{source_origin}"
45
+ end
144
46
  end
145
47
 
146
48
  def artists
147
49
  @artists ||= tracks.map{|t| t.artist}.uniq.sort
148
50
  end
149
51
 
150
- def config
151
- return @config if @config
152
- if File.exist?(CONFIG_FILE)
153
- begin
154
- @config = YAML.load_file(CONFIG_FILE)
155
- rescue
156
- raise "Unable to read config file #{CONFIG_FILE}"
157
- end
158
- else
159
- @config = { :ignore_genres => [] }
160
- end
161
- end
162
-
163
- def save_config
164
- File.new(CONFIG_FILE, "w+").write(config.to_yaml)
52
+ def create_app_dir
53
+ Dir.mkdir(APP_DIR) unless File.directory?(APP_DIR) rescue "Unable to create application directory: #{APP_DIR}"
165
54
  end
166
55
 
167
56
  def summary(opts = {})
168
57
  puts
169
- puts "#{opts[:artist] || tracks.map{|t| t.artist}.uniq.size} artists."
58
+ puts "#{opts[:artist] || artists.size} artists."
170
59
  puts "#{opts[:tracks] || tracks.size} tracks."
171
60
  puts "#{opts[:tracks_tagged] || 0} tracks tagged."
172
61
  puts "#{opts[:requests] || 0} requests."
@@ -175,11 +64,9 @@ class IMP3::CLI
175
64
  end
176
65
 
177
66
  class String
178
- STRIP_WORDS = %w(the a of in)
179
-
180
67
  def strip_meaningless_words
181
68
  string = self
182
- STRIP_WORDS.each do |w|
69
+ Config.instance.strip_words.each do |w|
183
70
  string = string.gsub(/\b#{w}\b/i, '')
184
71
  end
185
72
  string.strip
@@ -0,0 +1,39 @@
1
+ module IMP3::Commands::Artists
2
+
3
+ #
4
+ # For every artist in iTunes,
5
+ def artists_misspelled_command
6
+ artist_choices = {}
7
+ tagged = 0
8
+
9
+ progress_bar artists, :complete_message => "Misspelled artist name scan complete." do |artist, bar|
10
+ bar.increment :title => "Scanning artist '#{artist}'"
11
+ normalized_artist_name = artist.normalized_without_words
12
+ artist_choices[normalized_artist_name] ||= []
13
+ artist_choices[normalized_artist_name] << artist.strip unless artist_choices[normalized_artist_name].include?(artist)
14
+ end
15
+
16
+ artist_names = {}
17
+ tracks.each do |track|
18
+ normalized_artist_name = track.artist.normalized_without_words
19
+ unless artist_names.has_key?(normalized_artist_name)
20
+ if artist_choices[normalized_artist_name] && artist_choices[normalized_artist_name].size > 1
21
+ puts
22
+ artist_name = choose("What is the correct artist name for #{track.artist}?", *(artist_choices[normalized_artist_name] << :"(Skip)"))
23
+ artist_names[normalized_artist_name] = artist_name
24
+ else
25
+ artist_names[normalized_artist_name] = track.artist
26
+ end
27
+ end
28
+
29
+ unless artist_names[normalized_artist_name].eql?(:"(Skip)") or track.artist.eql?(artist_names[normalized_artist_name])
30
+ tagged += 1
31
+ puts "Tagging track #{track.persistent_id} with artist name '#{artist_names[normalized_artist_name]}'"
32
+ track.artist = ""
33
+ track.artist = artist_names[normalized_artist_name]
34
+ end
35
+ end
36
+
37
+ summary :tracks_tagged => tagged
38
+ end
39
+ end
@@ -0,0 +1,116 @@
1
+ module IMP3::Commands::Genres
2
+ def genres_fetch_command (options)
3
+ if options.flush_cache
4
+ cache.artist_genres.clear
5
+ puts "Flushing artist genres cache."
6
+ end
7
+
8
+ tagged, requests, errors = 0, 0, 0
9
+
10
+ progress_bar tracks do |track, bar|
11
+ normalized_artist_name = track.artist.normalize
12
+ begin
13
+ unless cache.artist_genres.has_key?(normalized_artist_name)
14
+ raise "Track has no artist name set" if track.artist.empty?
15
+ requests += 1
16
+ bar.refresh(:title => "Quering last.fm for '#{track.artist}'")
17
+ response = Nokogiri(open("http://ws.audioscrobbler.com/1.0/artist/#{URI.escape(CGI.escape(track.artist))}/toptags.xml"))
18
+ cache.artist_genres[normalized_artist_name] = response.search("//toptags/tag/name").map{|t| t.text.normalize }.uniq
19
+ cache.save
20
+ end
21
+ rescue => e
22
+ errors += 1
23
+ bar.refresh(:title => "Error: #{e}")
24
+ cache.artist_genres[normalized_artist_name] = nil
25
+ end
26
+
27
+ if (cache.artist_genres.has_key?(normalized_artist_name) and not cache.artist_genres[normalized_artist_name].nil?)
28
+ genre = (cache.artist_genres[normalized_artist_name] - config.ignore_genres).first
29
+
30
+ if track.genre.eql?(genre)
31
+ bar.refresh(:title => "Skipping track #{track.persistent_id}. Genre is already '#{genre}'")
32
+ else
33
+ bar.refresh(:title => "Tagging track #{track.persistent_id} with genre '#{genre}'")
34
+ tagged += 1
35
+ track.genre = ""
36
+ track.genre = genre
37
+ end
38
+ end
39
+
40
+ bar.increment
41
+ end
42
+ summary(:tracks_tagged => tagged, :requests => requests, :errors => errors)
43
+ end
44
+
45
+ def genres_fetch_cache_command(options)
46
+ genres = {}
47
+ (cache.artist_genres.map{|artist, tags| tags}.flatten - config.ignore_genres).each do |tag|
48
+ genres[tag] ||= 0
49
+ genres[tag] += 1
50
+ end
51
+
52
+ genres = genres.sort{|a, b| b[1] <=> a[1]}
53
+
54
+ if genres.any?
55
+ genre_table = table do |t|
56
+ t.headings = 'Genre', 'Artist count'
57
+ genres.to_a[0..[genres.size, options.limit].min].each do |g, c|
58
+ t << [g, c]
59
+ end
60
+ end
61
+ puts genre_table
62
+ else
63
+ puts "No genres cached yet."
64
+ end
65
+
66
+ summary
67
+ end
68
+
69
+ def genres_list_command
70
+ genres = {}
71
+ tracks.each do |track|
72
+ genres[track.genre] ||= 0
73
+ genres[track.genre] += 1
74
+ end
75
+
76
+ genre_table = table do |t|
77
+ t.headings = 'Genre', 'Tracks'
78
+ genres.sort{|a, b| b[1] <=> a[1]}.each do |g, c|
79
+ t << [g, c]
80
+ end
81
+ end
82
+
83
+ puts genre_table
84
+ summary
85
+ end
86
+
87
+ def genres_ignore_add_command (genre)
88
+ raise "Please specify a genre" unless genre
89
+ genre.strip!
90
+ config.ignore_genres << genre unless config.ignore_genres.include?(genre)
91
+ config.save
92
+ puts "Genre '#{genre}' added to ignore list"
93
+ end
94
+
95
+ def genres_ignore_del_command (genre)
96
+ raise "Please specify a genre" unless genre
97
+ genre.strip!
98
+ config.ignore_genres.delete(genre) if config.ignore_genres.include?(genre)
99
+ config.save
100
+ puts "Genre '#{genre}' deleted from ignore list"
101
+ end
102
+
103
+ def genres_ignore_list_command
104
+ if config.ignore_genres.any?
105
+ genre_table = table do |t|
106
+ t.headings = 'Genre'
107
+ config.ignore_genres.each do |g|
108
+ t << [g]
109
+ end
110
+ end
111
+ puts genre_table
112
+ else
113
+ puts "No ignored genres."
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,8 @@
1
+ module IMP3::Commands
2
+ require "lib/imp3/commands/genres"
3
+ require "lib/imp3/commands/artists"
4
+
5
+ include IMP3::Commands::Artists
6
+ include IMP3::Commands::Genres
7
+ end
8
+
@@ -0,0 +1,33 @@
1
+ require "singleton"
2
+
3
+ class IMP3::Config
4
+ include Singleton
5
+
6
+ CONFIG_FILE = File.join(IMP3::APP_DIR, "config.yml")
7
+
8
+ def initialize
9
+ @data = {}
10
+
11
+ if File.exist?(CONFIG_FILE)
12
+ begin
13
+ @data = YAML.load_file(CONFIG_FILE)
14
+ rescue
15
+ raise "Unable to read config file #{CONFIG_FILE}"
16
+ end
17
+ end
18
+ end
19
+
20
+ def ignore_genres
21
+ @data[:ignore_genres] ||= {}
22
+ @data[:ignore_genres]
23
+ end
24
+
25
+ def strip_words
26
+ @data[:strip_words] ||= %w(the a of in)
27
+ @data[:strip_words]
28
+ end
29
+
30
+ def save
31
+ File.new(CONFIG_FILE, "w+").write(@data.to_yaml)
32
+ end
33
+ end
data/lib/imp3.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  module IMP3
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2".freeze
3
+ APP_DIR = File.expand_path(File.join("~", ".imp3"))
3
4
  end
4
5
 
5
6
  require 'lib/imp3/cli'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imp3
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - "V\xC3\xADctor Mart\xC3\xADnez"
@@ -80,7 +80,12 @@ files:
80
80
  - VERSION
81
81
  - bin/imp3
82
82
  - lib/imp3.rb
83
+ - lib/imp3/cache.rb
83
84
  - lib/imp3/cli.rb
85
+ - lib/imp3/commands.rb
86
+ - lib/imp3/commands/artists.rb
87
+ - lib/imp3/commands/genres.rb
88
+ - lib/imp3/config.rb
84
89
  - test/helper.rb
85
90
  - test/test_normalization.rb
86
91
  has_rdoc: true