imp3 0.1.1 → 0.1.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/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