mikeplayer 1.0.0

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