lastfm-m3u 0.2.3 → 0.2.4

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/.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