mikeplayer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task default: :test
data/bin/MikePlayer.rb ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mikeplayer'
4
+
5
+ options = {}
6
+ OptionParser.new do |opt|
7
+ opt.banner = <<-EOF
8
+ Usage: MikePlayer.rb [options] <search || file>
9
+
10
+ Example: `MikePlayer.rb --shuffle --directory /User/Catman/Music cats /MyMusic/GreatestBand-Song.mp3`
11
+ Finds all songs matching 'cats' in directory /User/Catman/Music ignoring case
12
+ and specific song /MyMusic/GreatestBand-Song.mp3
13
+ and randomizes the order
14
+
15
+ EOF
16
+ opt.on('-s', '--shuffle', 'Shuffle playlist.') { |o| options[:shuffle] = true }
17
+ opt.on('-r', '--random n', 'Create playlist with randomly picked n songs.') { |o| options[:random] = o.to_i }
18
+ opt.on('-o', '--overwrite', 'Overwrite playlist.') { |o| options[:overwrite] = true }
19
+ opt.on('-v', '--volume n', 'Changes default volume.') { |o| options[:volume] = o }
20
+ opt.on('-p', '--playlist name', 'Play playlist name.') { |o| options[:playlist] = o }
21
+ opt.on('-l', '--list', 'List songs in playlist.') { |o| options[:list] = true; }
22
+ opt.on('-d', '--directory name', 'Directory to find mp3s.') { |o| options[:directory] = o }
23
+ opt.on('-t', '--time minutes', 'Limit time to number of minutes.') { |o| options[:minutes] = o }
24
+ end.parse!
25
+
26
+ MikePlayer::Player.new(options, ARGV).play
@@ -0,0 +1,27 @@
1
+ module MikePlayer
2
+ class PlayThread
3
+ def initialize(filename)
4
+ @filename = filename
5
+ @mp3info = Mp3Info.new(filename)
6
+ end
7
+
8
+ def info
9
+ artist = "#{@mp3info.tag.artist}"
10
+ title = "#{@mp3info.tag.title}"
11
+
12
+ if (true == artist.empty?) && (true == title.empty?)
13
+ return File.basename(filename, '.mp3')
14
+ elsif (true == artist.empty?)
15
+ artist = "?????"
16
+ elsif (true == title.empty?)
17
+ title = "?????"
18
+ end
19
+
20
+ return "#{artist} - #{title}"
21
+ end
22
+
23
+ def to_json
24
+ return @filename
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ module MikePlayer
2
+ class PlayThread
3
+ def self.run(file, volume)
4
+ args = [
5
+ :play,
6
+ '--no-show-progress',
7
+ '--volume', volume,
8
+ song
9
+ ]
10
+ Open3.popen2e(*args) do |stdin, out, wait_thr|
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,101 @@
1
+ module MikePlayer
2
+ class Playlist
3
+ attr_reader :songs
4
+
5
+ def initialize(filename)
6
+ @filename = filename
7
+ @songs = []
8
+ @song_i = 0
9
+ @length = 0
10
+
11
+ load_songs
12
+
13
+ @loaded_song_count = @songs.size
14
+ end
15
+
16
+ def <<(song)
17
+ if ((true == File.file?(song)) && (false == @songs.any? { |s| s.filename == song }))
18
+ @songs << Song.new(song)
19
+ @length += @songs.last.length
20
+ end
21
+
22
+ return self
23
+ end
24
+
25
+ def add_random(n, directory)
26
+ files = Dir.glob(File.join(directory, "**", "*.mp3"), File::FNM_CASEFOLD) - @songs
27
+
28
+ files.sample(n).each do |file|
29
+ self << file
30
+ end
31
+
32
+ return self
33
+ end
34
+
35
+ def find_song(file, directory)
36
+ if (true == File.file?(file))
37
+ self << file
38
+ else
39
+ Dir.glob(File.join(directory, "**", "*#{file}*"), File::FNM_CASEFOLD).each do |f|
40
+ self << f
41
+ end
42
+ end
43
+
44
+ return self
45
+ end
46
+
47
+ def current
48
+ return @songs[@song_i]
49
+ end
50
+
51
+ def next
52
+ @song_i += 1
53
+
54
+ return self.current
55
+ end
56
+
57
+ def previous
58
+ @song_i -= 1 unless @song_i <= 0
59
+
60
+ return self.current
61
+ end
62
+
63
+ def shuffle!
64
+ @songs.shuffle!
65
+
66
+ return self
67
+ end
68
+
69
+ def save
70
+ File.open(@filename, 'w') do |f|
71
+ f.puts(JSON.pretty_generate(@songs.map {|s| s.to_json }))
72
+ end
73
+ end
74
+
75
+ def name
76
+ return File.basename(@filename, Settings::PL_FILE_ENDING)
77
+ end
78
+
79
+ def info
80
+ return "#{self.name} loaded #{@loaded_song_count} songs with length #{Song.as_duration_str(@length)}, added #{@songs.size - @loaded_song_count}"
81
+ end
82
+
83
+ def current_song_info
84
+ song_i_str = "#{@song_i + 1}".rjust(@songs.size.to_s.size)
85
+
86
+ return "Playing (#{song_i_str}/#{@songs.size}): #{current.info}".freeze
87
+ end
88
+
89
+ def finished?
90
+ return @song_i == @songs.size
91
+ end
92
+
93
+ private
94
+
95
+ def load_songs
96
+ if File.file?(@filename)
97
+ JSON.parse(File.read(@filename)).each {|song| self << song}
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,74 @@
1
+ module MikePlayer
2
+ class Settings
3
+ DEFAULT_DIRECTORY = 'Music'.freeze
4
+ DEFAULT_VOLUME = '0.1'
5
+ SETTINGS_DIRECTORY = '.mikeplayer'.freeze
6
+ PL_FILE_ENDING = '.mpl'.freeze
7
+
8
+ attr_reader :random, :music_dir, :playlist, :volume
9
+
10
+ def initialize(options)
11
+ @shuffle = options[:shuffle]
12
+ @overwrite = options[:overwrite]
13
+ @list = options[:list]
14
+ @home = options[:home] || Dir.home
15
+ @volume = options[:volume] || DEFAULT_VOLUME
16
+ @music_dir = options[:directory] || File.join(@home, DEFAULT_DIRECTORY)
17
+ @settings_dir = options[:settings] || File.join(@home, SETTINGS_DIRECTORY)
18
+ @minutes = options[:minutes].to_i
19
+ @random = options[:random].to_i
20
+
21
+ if (false == Dir.exist?(@settings_dir))
22
+ Dir.mkdir(@settings_dir)
23
+ end
24
+
25
+ @playlist = find_playlist(options[:playlist])
26
+
27
+ remove_playlist_if_needed(@playlist)
28
+ end
29
+
30
+ def shuffle?
31
+ return true == @shuffle
32
+ end
33
+
34
+ def random?
35
+ return 0 < @random
36
+ end
37
+
38
+ def overwrite?
39
+ return true == @overwrite
40
+ end
41
+
42
+ def list?
43
+ return true == @list
44
+ end
45
+
46
+ private
47
+
48
+ def find_playlist(user_option)
49
+ name = nil
50
+
51
+ if (false == user_option.nil?)
52
+ if (true == File.file?(user_option))
53
+ return user_option
54
+ else
55
+ name = File.basename(user_option, PL_FILE_ENDING)
56
+ end
57
+ elsif (true == self.random?)
58
+ name = "random_n#{@random}"
59
+ else
60
+ name = 'default'
61
+ end
62
+
63
+ return File.join(@settings_dir, "#{name}#{PL_FILE_ENDING}")
64
+ end
65
+
66
+ def remove_playlist_if_needed(filename)
67
+ if (true == File.file?(filename))
68
+ if ((true == overwrite?) || (true == random?))
69
+ File.delete(filename)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,51 @@
1
+ module MikePlayer
2
+ class Song
3
+ attr_reader :filename
4
+
5
+ def initialize(filename)
6
+ @filename = filename
7
+ @mp3info = Mp3Info.new(filename)
8
+ end
9
+
10
+ def info
11
+ artist = "#{@mp3info.tag.artist}"
12
+ title = "#{@mp3info.tag.title}"
13
+
14
+ if (true == artist.empty?) && (true == title.empty?)
15
+ return File.basename(@filename, '.mp3')
16
+ elsif (true == artist.empty?)
17
+ artist = "?????"
18
+ elsif (true == title.empty?)
19
+ title = "?????"
20
+ end
21
+
22
+ return "#{artist} - #{title}"
23
+ end
24
+
25
+ def length
26
+ return @mp3info.length
27
+ end
28
+
29
+ def length_str(elapsed_time)
30
+ return Song.as_duration_str(self.length, elapsed_time)
31
+ end
32
+
33
+ def to_json
34
+ return @filename.to_s
35
+ end
36
+
37
+ def self.as_duration_str(l, t = nil)
38
+ l_hr = "%02d" % (l / 3600).floor
39
+ l_min = "%02d" % ((l % 3600 )/ 60).floor
40
+ l_sec = "%02d" % (l % 60)
41
+ e_min = "%02d" % (t / 60).floor unless t.nil?
42
+ e_sec = "%02d" % (t % 60) unless t.nil?
43
+
44
+ result = "#{l_min}:#{l_sec}"
45
+ result = "#{l_hr}:#{result}" if l >= 3600
46
+ result = "#{e_min}:#{e_sec} [#{result}]" unless t.nil?
47
+
48
+ return result
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ module MikePlayer
2
+ VERSION = '1.0.0'.freeze
3
+ end
data/lib/mikeplayer.rb ADDED
@@ -0,0 +1,254 @@
1
+ require 'optparse'
2
+ require 'json'
3
+ require 'open3'
4
+ require 'io/console'
5
+ require 'mp3info'
6
+ require 'mikeplayer/version'
7
+ require 'mikeplayer/settings'
8
+ require 'mikeplayer/playlist'
9
+ require 'mikeplayer/song'
10
+
11
+ module MikePlayer
12
+ class Player
13
+ PAUSE_INDICATOR = " ||".freeze
14
+ SLEEP_SETTING = 0.5
15
+ STOPPED = :stopped
16
+ PLAYING = :playing
17
+ PAUSED = :paused
18
+
19
+ def initialize(options, *args)
20
+ @settings = Settings.new(options)
21
+ @playlist = Playlist.new(@settings.playlist)
22
+ @command = ''
23
+ @pid = nil
24
+ @timer_start = nil
25
+ @state = STOPPED
26
+
27
+ check_system
28
+
29
+ if (true == @settings.list?)
30
+ @songs.map { |song| File.basename(song) }.sort.each {|song| puts "#{File.basename(song)}"}
31
+
32
+ exit 0
33
+ end
34
+
35
+ args.flatten.each do |arg|
36
+ @playlist.find_song(arg, @settings.music_dir)
37
+ end
38
+
39
+ if (true == @settings.random?)
40
+ @playlist.add_random(@settings.random, @settings.music_dir)
41
+ end
42
+
43
+ @playlist.save
44
+ end
45
+
46
+ def play
47
+ @playlist.shuffle! if @settings.shuffle?
48
+
49
+ print "Playlist #{@playlist.info}\n"
50
+
51
+ if @playlist.finished?
52
+ puts "No songs in playlist."
53
+
54
+ exit 1
55
+ end
56
+
57
+ @thread = Thread.new do
58
+ display_width = 0
59
+
60
+ while (false == @playlist.finished?)
61
+ song = @playlist.current
62
+ @song_start = Time.now
63
+ info_prefix = "\r#{@playlist.current_song_info}".freeze
64
+
65
+ stdin, stdother, thread_info = Open3.popen2e('play', '--no-show-progress', '--volume', @settings.volume, song.filename)
66
+
67
+ @state = PLAYING
68
+ @pid = thread_info.pid
69
+ indicator = ''
70
+
71
+ while (true == pid_alive?)
72
+ pause_if_over_time_limit
73
+
74
+ if (true == playing?)
75
+ indicator = "#{'>' * (playing_time % 4)}"
76
+ info_changed = true
77
+ elsif (true == paused?) && (PAUSE_INDICATOR != indicator)
78
+ indicator = PAUSE_INDICATOR
79
+ info_changed = true
80
+ end
81
+
82
+ if (true == info_changed)
83
+ mindicator = ""
84
+
85
+ if (0 < minutes_remaining)
86
+ mindicator = "(#{minutes_remaining}↓) "
87
+ end
88
+
89
+ info = "#{info_prefix} #{song.length_str(playing_time)} #{mindicator}#{indicator}".ljust(display_width)
90
+
91
+ display_width = info.size
92
+
93
+ print(info)
94
+
95
+ $stdout.flush
96
+ end
97
+
98
+ sleep SLEEP_SETTING
99
+ end
100
+
101
+ stdin.close
102
+ stdother.close
103
+
104
+ @pid = nil
105
+
106
+ if (true == playing?)
107
+ next_song
108
+ end
109
+ end
110
+
111
+ @pid = nil
112
+ exit
113
+ end
114
+
115
+ wait_on_user
116
+
117
+ puts ""
118
+ end
119
+
120
+ def cmd_exist?(cmd)
121
+ if (true != system('command'))
122
+ raise "Missing 'command' command, which is used to test compatibility."
123
+ end
124
+
125
+ if (true != system("command -v #{cmd} >/dev/null 2>&1"))
126
+ return false
127
+ end
128
+
129
+ return true
130
+ end
131
+
132
+ private
133
+
134
+ def check_system
135
+ %w(play).each do |cmd|
136
+ if (false == cmd_exist?(cmd))
137
+ raise "#{cmd} failed, do you have sox installed?"
138
+ end
139
+ end
140
+
141
+ return nil
142
+ end
143
+
144
+ def wait_on_user
145
+ while ('q' != @command)
146
+ @command = STDIN.getch
147
+
148
+ if ('c' == @command)
149
+ press_pause
150
+ elsif ('v' == @command)
151
+ next_song
152
+ elsif ('z' == @command)
153
+ previous_song
154
+ elsif ('q' == @command) && (false == @pid.nil?)
155
+ stop_song
156
+ @thread.kill
157
+ elsif ('t' == @command)
158
+ @timer_start = Time.now
159
+ elsif (false == @timer_start.nil?) && ("#{@command.to_i}" == @command)
160
+ if (0 == @minutes)
161
+ @minutes = @command.to_i
162
+ else
163
+ @minutes *= 10
164
+ @minutes += @command.to_i
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ def playing?
171
+ return (PLAYING == @state)
172
+ end
173
+
174
+ def paused?
175
+ return (PAUSED == @state)
176
+ end
177
+
178
+ def press_pause
179
+ if (true == playing?)
180
+ kill("STOP")
181
+ @state = PAUSED
182
+ elsif (true == paused?)
183
+ kill("CONT")
184
+ @state = PLAYING
185
+ else
186
+ print("Confused state #{@state}.")
187
+ end
188
+ end
189
+
190
+ def stop_song
191
+ if (true == paused?)
192
+ kill("CONT")
193
+ end
194
+
195
+ kill("INT")
196
+
197
+ sleep 0.2
198
+
199
+ if (true == pid_alive?)
200
+ kill("KILL")
201
+ end
202
+
203
+ @state = STOPPED
204
+ end
205
+
206
+ def pid_alive?(pid = @pid)
207
+ if (false == pid.nil?)
208
+ return system("ps -p #{pid} > /dev/null")
209
+ end
210
+
211
+ return false
212
+ end
213
+
214
+ def next_song
215
+ stop_song
216
+
217
+ @playlist.next
218
+ end
219
+
220
+ def previous_song
221
+ stop_song
222
+
223
+ @playlist.previous
224
+ end
225
+
226
+ def kill(signal)
227
+ if (false == @pid.nil?)
228
+ Process.kill(signal, @pid)
229
+ end
230
+ end
231
+
232
+ def pause_if_over_time_limit
233
+ if (false == @timer_start.nil?) && (0 < @minutes) && (true == playing?)
234
+ if (0 > minutes_remaining)
235
+ press_pause
236
+ @timer_start = nil
237
+ @minutes = 0
238
+ end
239
+ end
240
+ end
241
+
242
+ def playing_time
243
+ return (Time.now - @song_start).to_i
244
+ end
245
+
246
+ def minutes_remaining
247
+ if ((0 == @minutes) || (@timer_start.nil?))
248
+ return -1
249
+ else
250
+ return (@minutes - ((Time.now - @timer_start).to_i / 60).to_i)
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mikeplayer/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'mikeplayer'
8
+ gem.version = MikePlayer::VERSION
9
+ gem.authors = ['Mike Crockett']
10
+ gem.email = ['rubygems@mmcrockett.com']
11
+ gem.summary = 'Wraps Sox\'s `play` command, allowing playslists, find, random and time limit.'
12
+ gem.executables << 'MikePlayer.rb'
13
+ gem.description = <<-EOF.gsub(/^\s+/, '')
14
+ #{gem.summary}
15
+
16
+ Once a song is playing you can:
17
+ 'x' to previous
18
+ 'c' to pause/play
19
+ 'v' to next
20
+ 'q' to quit
21
+ 't' n to set a timer that will pause the music after n minutes
22
+ EOF
23
+ gem.homepage = 'https://github.com/mmcrockett/mikeplayer'
24
+
25
+ gem.files = `git ls-files`.split($/)
26
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
27
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
28
+ gem.require_paths = ['lib']
29
+ gem.licenses = ['MIT']
30
+
31
+ gem.add_dependency 'json', '~> 1.8'
32
+ gem.add_dependency 'ruby-mp3info', '~> 0.8'
33
+ gem.add_dependency 'minitest', '~> 5'
34
+ gem.add_development_dependency 'rake', '~> 12'
35
+ gem.add_development_dependency 'byebug', '~> 11'
36
+ end
@@ -0,0 +1,17 @@
1
+ require 'test_helper'
2
+
3
+ class SettingsTest < Minitest::Test
4
+ describe 'settings' do
5
+ let(:home) { File.join('', 'tmp') }
6
+ let(:sdir) { File.join(home, MikePlayer::Settings::SETTINGS_DIRECTORY) }
7
+
8
+ before do
9
+ Dir.delete(sdir) if Dir.exist?(sdir)
10
+ end
11
+
12
+ it 'creates a settings directory' do
13
+ MikePlayer::Settings.new(home: home)
14
+ assert(Dir.exist?(sdir))
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+
3
+ class MikePlayerTest < Minitest::Test
4
+ describe 'mikeplayer' do
5
+ it 'has a version' do
6
+ assert(MikePlayer::VERSION)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
2
+ require 'mikeplayer'
3
+
4
+ require 'minitest/autorun'
5
+ require 'byebug'