lastfm-m3u 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -18,6 +18,6 @@ tmp
18
18
  tags
19
19
  gems.tags
20
20
  lastfm.yml
21
- *m3u
21
+ *.m3u
22
22
  m3u
23
23
  coverage
data/Rakefile CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env rake
2
+ require 'rubygems'
3
+ require "bundler/setup"
2
4
  require "bundler/gem_tasks"
3
5
 
4
6
  # RSpec
5
- require 'rspec/core'
6
- require 'rspec/core/rake_task'
7
- RSpec::Core::RakeTask.new(:spec) do |spec|
8
- spec.pattern = FileList['spec/**/*_spec.rb']
9
- end
10
- task :default => :spec
7
+ require 'rspec/core'
8
+ require 'rspec/core/rake_task'
9
+ RSpec::Core::RakeTask.new(:spec) do |spec|
10
+ spec.pattern = FileList['spec/**/*_spec.rb']
11
+ end
12
+ task :default => :spec
data/bin/lastfm-m3u ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'optparse'
4
+ require 'ostruct'
5
+ require 'highline/import'
6
+ require 'ruby-progressbar'
7
+
8
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
9
+ require 'lastfm-m3u'
10
+ require 'lastfm-m3u/util'
11
+
12
+ options = OpenStruct.new
13
+ OptionParser.new do |opts|
14
+ opts.banner = "LastfmM3u Playlist Creator".color(:blue)
15
+ opts.define_head "To get started: lastfm-m3u --init".color(:green)
16
+ opts.separator ""
17
+ opts.separator "Usage: lastfm-m3u [options]".color(:yellow)
18
+ opts.separator "Note: defaults to filename-only search. id3/both search is more thorough, but much slower"
19
+ opts.separator ""
20
+ opts.separator "Examples:"
21
+ #opts.separator " lastfm-m3u # auto-search config-music_dir mp3 id3 tags for all unique artists, query last.fm for top tracks and create m3us for each artist from available track files"
22
+ opts.separator ' lastfm-m3u -a "Biosphere,Brian Eno"'
23
+ opts.separator " lastfm-m3u -a Biosphere"
24
+ opts.separator " lastfm-m3u -a Biosphere -t both"
25
+ opts.separator " lastfm-m3u -a Biosphere -d ~/music/Biosphere"
26
+ #opts.separator " lastfm-m3u -a Biosphere -o biosphere.m3u"
27
+ opts.separator ""
28
+ opts.separator ""
29
+ opts.separator "Options:"
30
+
31
+ # Type of search
32
+ types = %w[file id3 both]
33
+ type_aliases = { "f" => "file", "i" => "id3", "b" => "both" }
34
+ type_list = (type_aliases.keys + types).join(',')
35
+ opts.on("--type TYPE", "-t TYPE", types, type_aliases, "Select search type", " (#{type_list})") do |type|
36
+ options.type = type.to_sym
37
+ end
38
+ options.type = :file if options.type.blank? or options.type.nil?
39
+
40
+ # init config
41
+ opts.on('-i', '--init', "init lastfm API access config") do |opt|
42
+ options.init = true
43
+ end
44
+
45
+ # List of artists
46
+ opts.on('-a "x,y,z"', Array, "one or more artists separated by commas (enclose with quotes if any spaces)") do |artists|
47
+ options.artists = artists
48
+ end
49
+
50
+ # directory
51
+ options.directory = Dir.pwd
52
+ opts.on("-d DIR", "--directory DIR", "music directory") do |directory|
53
+ options.directory = directory
54
+ end
55
+
56
+ #opts.on('-o', '--output FILENAME', 'Write m3u to FILENAME') do |fn| # writes single m3u when multiple artists
57
+ #options.out_file = fn
58
+ #end
59
+
60
+ opts.on('-p', '--path PATH', 'Specify path to use for m3u file entries (e.g. MPD requires a path relative to its music directory - i.e. -d /home/user/music/Biosphere -p /home/user/music where the second one is MPD\'s root)') do |path|
61
+ options.path = Pathname path
62
+ end
63
+
64
+ opts.on('-l', '--limit NUM', 'Specify limit of tracks to fetch from Last.fm (defaults to all)') do |limit|
65
+ options.limit = limit.to_i
66
+ end
67
+
68
+ opts.on('-f', '--not-found', 'Show not found') do
69
+ options.not_found = true
70
+ end
71
+
72
+ #opts.on('-f', '--force', 'Overwrite existing output file(s)') do
73
+ #options.force = true
74
+ #end
75
+
76
+ opts.on('--debug [LEVEL]', 'Enable debugging output (Optional Level :: 0=all, 1=info,warn,error, 2=warn,error, 3=error)') do |level|
77
+ if level
78
+ options.debug = level.to_i
79
+ else
80
+ options.debug = 0
81
+ end
82
+ end
83
+
84
+ opts.on_tail('-v', '--version', 'Show LastfmM3u version') do
85
+ puts "LastfmM3u v#{LastfmM3u::VERSION}"
86
+ exit
87
+ end
88
+
89
+ opts.on_tail('-h', '--help', 'Show this help message') do
90
+ puts opts
91
+ exit
92
+ end
93
+ end.parse!
94
+
95
+ if options.init
96
+ lastfm = LastfmM3u::Lastfm.new('')
97
+ exit
98
+ end
99
+
100
+ unless options.artists
101
+ puts "Please provide at least one artist (-a Elvis).".color(:red) + " See".color(:green) + " --help".color(:yellow) + " for usage or".color(:green) + " --init".color(:yellow) + " to get started".color(:green)
102
+ exit
103
+ end
104
+
105
+ if options.debug
106
+ $logger.level = options.debug
107
+ end
108
+
109
+ options.artists.each do |artist|
110
+ not_found = []
111
+ lastfm = LastfmM3u::Lastfm.new(artist)
112
+ lastfm.root_dir = options.directory if options.directory
113
+ lastfm.limit = options.limit if options.limit
114
+ tracks = lastfm.artist.top_tracks
115
+
116
+ puts
117
+ puts "Searching filesystem with method: ".color(:green) + options.type.to_s.capitalize.color(:yellow)
118
+ puts
119
+ num_tracks = (options.limit.nil? ? tracks.size : options.limit)
120
+ progressbar = ProgressBar.create(starting_at: 0, total: num_tracks, format: "%a".color(:cyan) + "|".color(:magenta) + "%B".color(:green) + "|".color(:magenta) + "%p%%".color(:cyan))
121
+ lastfm.find_tracks(tracks, options.type, progressbar).each do |track, track_set|
122
+ if track_set.empty?
123
+ not_found << track
124
+ next
125
+ end
126
+
127
+ if track_set.size > 1
128
+ unless found_track = LastfmM3u::Util.choose_track(track, track_set)
129
+ not_found << track
130
+ next
131
+ end
132
+ puts
133
+ else
134
+ found_track = track_set.first
135
+ end
136
+ if options.path
137
+ lastfm.m3u.add_file(found_track.relative_path_from options.path)
138
+ else
139
+ lastfm.m3u.add_file(found_track)
140
+ end
141
+ end
142
+ puts
143
+ margin = 10
144
+ output_size = artist.length+10
145
+ puts " "*margin + ("="*output_size).color(:blue)
146
+ puts artist.center(output_size+margin*2).color(:green)
147
+ puts " "*margin + "#{num_tracks - not_found.size} of #{num_tracks} tracks found".center(output_size).color(:green)
148
+ puts " "*margin + ("="*output_size).color(:blue)
149
+
150
+ lastfm.m3u.write(File.join(lastfm.m3u_path, "#{artist}.m3u"))
151
+
152
+ unless not_found.empty? or options.not_found.nil?
153
+ puts
154
+ puts "Not Found:".color(:red)
155
+ not_found.each {|track| puts "#{track}".color(:yellow) }
156
+ end
157
+
158
+ end
data/lastfm-m3u.gemspec CHANGED
@@ -21,6 +21,7 @@ Gem::Specification.new do |gem|
21
21
  gem.add_development_dependency 'terminal-notifier-guard', '~> 1.5.3'
22
22
  gem.add_development_dependency 'rb-fsevent', '~> 0.9'
23
23
  gem.add_development_dependency 'debugger', '~> 1.5.0'
24
+ gem.add_development_dependency 'pry-debugger', '~> 0.2'
24
25
  gem.add_development_dependency 'simplecov', '~> 0.7.1'
25
26
 
26
27
  gem.add_dependency 'rockstar', '~> 0.7.1'
data/lib/lastfm-m3u.rb CHANGED
@@ -4,7 +4,6 @@ require 'pathname'
4
4
  require 'lastfm-m3u/version'
5
5
 
6
6
  require 'rockstar'
7
- require 'mp3info'
8
7
  require 'm3uzi'
9
8
  require 'rainbow'
10
9
  require 'ansi/logger'
@@ -12,6 +11,7 @@ require 'ansi/logger'
12
11
  require 'lastfm-m3u/base'
13
12
  require 'lastfm-m3u/lastfm'
14
13
  require 'lastfm-m3u/search/file_search'
14
+ require 'lastfm-m3u/mp3_info_ext'
15
15
 
16
16
  module LastfmM3u
17
17
  $logger = ANSI::Logger.new(STDOUT)
@@ -0,0 +1,11 @@
1
+ module LastfmM3u
2
+ class Base
3
+ attr_accessor :m3u, :m3u_path
4
+
5
+ def initialize
6
+ @m3u_path = Dir.pwd
7
+ @m3u = M3Uzi.new
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,48 @@
1
+ require 'fileutils'
2
+
3
+ module LastfmM3u
4
+ class Lastfm < Base
5
+ attr_accessor :artist, :root_dir, :limit, :tracks
6
+
7
+ def initialize(artist, root_dir='/home/jon/music/trance')
8
+ FileUtils.mkdir_p "#{ENV['HOME']}/.config"
9
+ config_path = "#{ENV['HOME']}/.config/lastfm-m3u.yml"
10
+ unless File.exists? config_path
11
+ puts "Copying empty config file to #{config_path}...".color(:green)
12
+ FileUtils.cp File.join(File.dirname(__FILE__), '../../config/lastfm.yml.dist'), config_path
13
+ end
14
+ config_hash = YAML.load_file(config_path)
15
+ Rockstar.lastfm = config_hash
16
+ unless config_hash.values.any? {|v| v==''}
17
+ @artist = Rockstar::Artist.new artist
18
+ @root_dir = root_dir
19
+ @limit = nil
20
+ super()
21
+ else
22
+ puts "Please add your api_key and api_secret to:\n #{config_path}\nGet your keys at http://www.last.fm/api/account".color(:yellow)
23
+ exit
24
+ end
25
+ end
26
+
27
+ def find_tracks(tracks, search_type = :file, progressbar = nil)
28
+ begin
29
+ raise InvalidSearchType unless SEARCH_TYPES.include?(search_type)
30
+ rescue InvalidSearchType
31
+ $logger.error("Invalid Search Type '#{search_type}', use one of #{SEARCH_TYPES.join(',')}")
32
+ exit
33
+ end
34
+ found_tracks = {}
35
+ search = LastfmM3u::FileSearch.new(root_dir)
36
+ self.limit ||= tracks.size
37
+ tracks[0..limit-1].each do |track|
38
+ track.name = CGI.unescapeHTML(track.name)
39
+ found_tracks[track.name] = search.find(track.name, {search_type: search_type})
40
+ progressbar.increment if progressbar
41
+ end
42
+ found_tracks
43
+ end
44
+
45
+ end
46
+
47
+ class InvalidSearchType < Exception; end
48
+ end
@@ -0,0 +1,57 @@
1
+ require 'mp3info'
2
+
3
+ class Mp3InfoExt < Mp3Info
4
+ attr_reader :query, :query_type
5
+
6
+ def initialize(query, query_type, entry)
7
+ @query = query
8
+ @query_type = query_type
9
+ super(entry)
10
+ end
11
+
12
+ def found?
13
+ return true if @query_type == :track && title_found?
14
+ return true if @query_type == :album && album_found?
15
+ return true if @query_type == :artist && artist_found?
16
+ return false
17
+ end
18
+
19
+ # ARTIST
20
+ def artist_found?
21
+ tag2_artist_found? || tag1_artist_found?
22
+ end
23
+
24
+ def tag2_artist_found?
25
+ hastag2? && tag2.TPE1.downcase == query.downcase
26
+ end
27
+
28
+ def tag1_artist_found?
29
+ hastag1? && tag1.artist.downcase == query.downcase
30
+ end
31
+
32
+ # ALBUM
33
+ def album_found?
34
+ tag2_album_found? || tag1_album_found?
35
+ end
36
+
37
+ def tag2_album_found?
38
+ hastag2? && tag2.TALB.downcase == query.downcase
39
+ end
40
+
41
+ def tag1_album_found?
42
+ hastag1? && tag1.album.downcase == query.downcase
43
+ end
44
+
45
+ # TITLE
46
+ def title_found?
47
+ tag2_title_found? || tag1_title_found?
48
+ end
49
+
50
+ def tag2_title_found?
51
+ hastag2? && tag2.TIT2.downcase == query.downcase
52
+ end
53
+
54
+ def tag1_title_found?
55
+ hastag1? and tag1.title.downcase == query.downcase
56
+ end
57
+ end
@@ -0,0 +1,101 @@
1
+ module LastfmM3u
2
+ require 'lastfm-m3u'
3
+ require 'pathname'
4
+
5
+ class Search < Base
6
+ end
7
+
8
+ class FileSearch < Search
9
+ attr_accessor :root_dir
10
+
11
+ def initialize(root_dir)
12
+ @root_dir = Pathname root_dir
13
+ super()
14
+ end
15
+
16
+ def find(query, options={})
17
+ query_type = options[:query_type] || :track
18
+ file_or_id3 = options[:search_type] || :both
19
+ prefer_flac = options[:prefer_flac] || true
20
+
21
+ files = find_unique_files(query, query_type, file_or_id3)
22
+ if prefer_flac && !(flac_files=trim_non_flac(files)).empty?
23
+ files = flac_files
24
+ end
25
+ $logger.debug "#{query} => #{files}"
26
+ files
27
+ end
28
+
29
+
30
+ private
31
+
32
+ def find_unique_files(query, query_type, file_or_id3)
33
+ files = id3_files = []
34
+ files = find_in_filesystem(query, query_type) if [:both, :file].include? file_or_id3
35
+ id3_files = find_by_id3(query, query_type) if [:both, :id3].include? file_or_id3
36
+ (files + id3_files).sort.uniq
37
+ end
38
+
39
+ def find_in_filesystem(name, query_type)
40
+ if query_type == :artist or query_type == :album
41
+ find_by_dir(name)
42
+ else
43
+ find_by_filename(name)
44
+ end
45
+ end
46
+
47
+ def find_by_dir(name)
48
+ found_entries = []
49
+ Dir.glob("#{root_dir}/**/*").each do |entry|
50
+ entry = Pathname.new entry
51
+ found_entries << entry.to_s if entry.directory? and normalize(entry).to_s =~ /.*#{Regexp.escape name}.*/i
52
+ end
53
+ found_entries
54
+ end
55
+
56
+ def find_by_filename(name)
57
+ found_entries = []
58
+ Dir.glob("#{root_dir}/**/*.{mp3,flac}").each do |entry|
59
+ begin
60
+ entry = Pathname.new entry
61
+ if normalize(entry) =~ /.*#{Regexp.escape name}.*/i
62
+ $logger.debug "#{name} => #{entry}"
63
+ found_entries << entry.to_s
64
+ end
65
+ rescue ArgumentError => e
66
+ # for some reason it gets 'invalid byte sequence in UTF-8
67
+ # even though replacing ascii below with utf-8 causes nothing to be replaced.
68
+ entry = Pathname entry.to_s.encode('ascii', invalid: :replace, undef: :replace, replace: '??')
69
+ $logger.warn "#{e.message} => #{entry.relative_path_from(root_dir)}"
70
+ end
71
+ end
72
+ found_entries
73
+ end
74
+
75
+ def find_by_id3(query, query_type)
76
+ query = CGI.unescapeHTML(query)
77
+ found_entries = []
78
+ $logger.debug query
79
+ Dir.glob("#{root_dir}/**/*.mp3").each do |entry|
80
+ begin
81
+ id3 = Mp3InfoExt.new(query, query_type, entry)
82
+ found_entries << id3.filename if id3.found?
83
+ rescue Mp3InfoError => e
84
+ $logger.warn "#{e.message} => #{(Pathname.new entry).relative_path_from(root_dir)}"
85
+ rescue NoMethodError => e
86
+ $logger.warn "No tags found => #{(Pathname.new entry).relative_path_from(root_dir)}"
87
+ end
88
+ end
89
+ found_entries
90
+ end
91
+
92
+ def normalize(pathname)
93
+ pathname.basename.to_s.gsub('-', ' ').gsub('_', ' ').sub(/#{pathname.extname}$/, '')
94
+ end
95
+
96
+ def trim_non_flac(pathnames)
97
+ pathnames.select{|pn| pn.to_s =~ /\.flac$/}
98
+ end
99
+ end
100
+
101
+ end
@@ -0,0 +1,19 @@
1
+ module LastfmM3u
2
+ module Util
3
+ def self.choose_track(track, track_set)
4
+ choose do |menu|
5
+ menu.index = :letter
6
+ menu.index_suffix = ") "
7
+
8
+ menu.prompt = "For #{track}, which file would you like to use? ".color(:green)
9
+
10
+ track_set.each do |track|
11
+ menu.choice track do
12
+ return track
13
+ end
14
+ end
15
+ menu.choice "none" do return nil end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module LastfmM3u
2
+ VERSION = "0.2.4"
3
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lastfm-m3u
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-17 00:00:00.000000000 Z
12
+ date: 2013-05-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -107,6 +107,22 @@ dependencies:
107
107
  - - ~>
108
108
  - !ruby/object:Gem::Version
109
109
  version: 1.5.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: pry-debugger
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: '0.2'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: '0.2'
110
126
  - !ruby/object:Gem::Dependency
111
127
  name: simplecov
112
128
  requirement: !ruby/object:Gem::Requirement
@@ -258,6 +274,7 @@ files:
258
274
  - lib/lastfm-m3u.rb
259
275
  - lib/lastfm-m3u/base.rb
260
276
  - lib/lastfm-m3u/lastfm.rb
277
+ - lib/lastfm-m3u/mp3_info_ext.rb
261
278
  - lib/lastfm-m3u/search/file_search.rb
262
279
  - lib/lastfm-m3u/util.rb
263
280
  - lib/lastfm-m3u/version.rb
@@ -284,7 +301,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
284
301
  version: '0'
285
302
  segments:
286
303
  - 0
287
- hash: 2601695685005095866
304
+ hash: 3207334904113421642
288
305
  required_rubygems_version: !ruby/object:Gem::Requirement
289
306
  none: false
290
307
  requirements:
@@ -293,7 +310,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
293
310
  version: '0'
294
311
  segments:
295
312
  - 0
296
- hash: 2601695685005095866
313
+ hash: 3207334904113421642
297
314
  requirements: []
298
315
  rubyforge_project:
299
316
  rubygems_version: 1.8.25