listlace 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +0,0 @@
1
- module Listlace
2
- class PlaylistItem < ActiveRecord::Base
3
- belongs_to :playlist
4
- belongs_to :track
5
- end
6
- end
@@ -1,21 +0,0 @@
1
- module Listlace
2
- class Track < ActiveRecord::Base
3
- has_many :playlist_items
4
- has_many :playlists, through: :playlist_items
5
-
6
- validates :location, presence: true, uniqueness: true
7
-
8
- before_create { |track| track.date_added = Time.now }
9
- before_save { |track| track.date_modified = Time.now }
10
-
11
- def increment_skip_count
12
- increment! :skip_count
13
- update_column :skip_date, Time.now
14
- end
15
-
16
- def increment_play_count
17
- increment! :play_count
18
- update_column :play_date, Time.now
19
- end
20
- end
21
- end
@@ -1,249 +0,0 @@
1
- module Listlace
2
- # This is the music box. It plays playlists. It contains a queue, which is
3
- # just a playlist. To tell it what to play, you add one or more playlists to
4
- # the queue, then start playing using the start method.
5
- #
6
- # Playback commands like pause, resume, seek, and so on are delegated to the
7
- # SinglePlayer, which takes care of playing each individual song.
8
- #
9
- # Each method that performs an action is like a button on a physical media
10
- # player: you can press the buttons even if they aren't applicable to the
11
- # current state of the player. If that's the case, the methods wil return
12
- # false. Otherwise, they'll return a truthy value.
13
- class Player
14
- DEFAULT_SINGLE_PLAYER = SinglePlayers::MPlayer
15
-
16
- attr_reader :current_track, :current_track_index, :repeat_mode
17
-
18
- def initialize
19
- @single_player = DEFAULT_SINGLE_PLAYER.new
20
- @queue = []
21
- @current_track = nil
22
- @current_track_index = nil
23
- @playlist_paused = false
24
- @started = false
25
- @repeat_mode = false
26
- end
27
-
28
- def queue(playlist = nil)
29
- if playlist.is_a? Array
30
- playlist = playlist.dup
31
- playlist.map! { |track| track.is_a?(String) ? SimpleTrack.new(track) : track }
32
- playlist.select! { |track| track.respond_to? :location }
33
- if @queue.empty?
34
- @queue = playlist
35
- else
36
- @queue += playlist
37
- end
38
- end
39
- @queue.dup
40
- end
41
-
42
- def clear
43
- stop
44
- @queue.clear
45
- true
46
- end
47
-
48
- def empty?
49
- @queue.empty?
50
- end
51
-
52
- def playlist_paused?
53
- @playlist_paused
54
- end
55
-
56
- def paused?
57
- playlist_paused? or @single_player.paused?
58
- end
59
-
60
- def started?
61
- @started
62
- end
63
-
64
- def start
65
- unless empty?
66
- @started = true
67
- @playlist_paused = false
68
- @current_track = @queue.first
69
- @current_track_index = 0
70
- play_track @current_track
71
- true
72
- else
73
- false
74
- end
75
- end
76
-
77
- def stop
78
- @single_player.stop
79
- @current_track = nil
80
- @current_track_index = nil
81
- @playlist_paused = false
82
- @started = false
83
- true
84
- end
85
-
86
- def pause
87
- @single_player.pause
88
- end
89
-
90
- def resume
91
- if playlist_paused?
92
- play_track @current_track
93
- @playlist_paused = false
94
- true
95
- else
96
- @single_player.resume
97
- end
98
- end
99
-
100
- def repeat(one_or_all_or_off)
101
- case one_or_all_or_off
102
- when :one
103
- @repeat_mode = :one
104
- when :all
105
- @repeat_mode = :all
106
- when :off
107
- @repeat_mode = false
108
- end
109
- true
110
- end
111
-
112
- def restart
113
- change_track(0)
114
- end
115
-
116
- def back(n = 1)
117
- change_track(-n)
118
- end
119
-
120
- def skip(n = 1)
121
- if @current_track.respond_to?(:increment_skip_count)
122
- @current_track.increment_skip_count
123
- end
124
- change_track(n)
125
- end
126
-
127
- def seek(where)
128
- if playlist_paused?
129
- resume
130
- pause
131
- seek where
132
- else
133
- case where
134
- when Integer
135
- @single_player.seek(where, :relative)
136
- when Range
137
- seconds = where.begin * 60 + where.end
138
- @single_player.seek(seconds * 1000, :absolute)
139
- when String
140
- @single_player.seek(Listlace.parse_time(where), :absolute)
141
- when Hash
142
- if where[:abs]
143
- if where[:abs].is_a? Integer
144
- @single_player.seek(where[:abs], :absolute)
145
- else
146
- seek(where[:abs])
147
- end
148
- elsif where[:percent]
149
- @single_player.seek(where[:percent], :percent)
150
- end
151
- end
152
- end
153
- end
154
-
155
- def speed
156
- @single_player.speed || 1.0
157
- end
158
-
159
- def speed=(new_speed)
160
- @single_player.speed = new_speed
161
- end
162
-
163
- def mute
164
- @single_player.mute
165
- end
166
-
167
- def unmute
168
- @single_player.unmute
169
- end
170
-
171
- def volume
172
- @single_player.volume
173
- end
174
-
175
- def volume=(new_volume)
176
- @single_player.volume = new_volume
177
- end
178
-
179
- def shuffle
180
- if started?
181
- @queue.shuffle_except! @current_track
182
- @current_track_index = 0
183
- else
184
- @queue.shuffle!
185
- end
186
- true
187
- end
188
-
189
- def sort(&by)
190
- @queue.sort! &by
191
- if started?
192
- @current_track_index = @queue.index(@current_track)
193
- end
194
- true
195
- end
196
-
197
- def current_time
198
- @single_player.current_time || 0
199
- end
200
-
201
- def total_time
202
- @single_player.total_time
203
- end
204
-
205
- private
206
-
207
- def change_track(by = 1, options = {})
208
- if started?
209
- if options[:auto] && @current_track.respond_to?(:increment_play_count)
210
- @current_track.increment_play_count
211
- end
212
- @current_track_index += by
213
- if options[:auto] && @repeat_mode
214
- case @repeat_mode
215
- when :one
216
- @current_track_index -= by
217
- when :all
218
- if @current_track_index >= @queue.length
219
- @current_track_index = 0
220
- end
221
- end
222
- end
223
- @current_track = @queue[@current_track_index]
224
- if @current_track && @current_track_index >= 0
225
- if @single_player.paused?
226
- @single_player.stop
227
- @playlist_paused = true
228
- elsif not playlist_paused?
229
- play_track @current_track
230
- end
231
- else
232
- stop
233
- end
234
- true
235
- else
236
- false
237
- end
238
- end
239
-
240
- def play_track(track)
241
- @single_player.play(track) do
242
- change_track(1, auto: true)
243
- ActiveRecord::Base.connection.close if defined?(ActiveRecord)
244
- end
245
- @playlist_paused = false
246
- true
247
- end
248
- end
249
- end
@@ -1,13 +0,0 @@
1
- module Listlace
2
- # The bare minimum needed to represent a track. This is used by the Player and
3
- # SinglePlayer, in case they get passed a String containing a path to an audio
4
- # file. That way, users don't have to worry about creating track objects, if
5
- # they want.
6
- class SimpleTrack
7
- attr_accessor :location
8
-
9
- def initialize(location = nil)
10
- @location = location
11
- end
12
- end
13
- end
@@ -1,129 +0,0 @@
1
- module Listlace
2
- # A SinglePlayer takes care of playing a single track, and controlling the
3
- # playback with commands like pause, resume, seek, and so on. It typically
4
- # starts an external audio player process to do this job. This class is an
5
- # interface that is to be implemented for different audio players. Then the
6
- # user can choose which SinglePlayer to use based on what audio players they
7
- # have installed.
8
- #
9
- # All methods that perform an action should return false if the action isn't
10
- # applicable, and return a truthy value otherwise.
11
- class SinglePlayer
12
- # Returns true if a track is currently loaded, i.e. either playing or
13
- # paused.
14
- def active?
15
- raise NotImplementedError
16
- end
17
-
18
- # Returns true if a track is loaded and is paused.
19
- def paused?
20
- raise NotImplementedError
21
- end
22
-
23
- # Get the current track object which was passed to the play method.
24
- def track
25
- raise NotImplementedError
26
- end
27
-
28
- # Get the title of the current track. First tries to call the title method on
29
- # the current track, if that doesn't work it tries to get the title from the
30
- # metadata of the audio file, and if that doesn't work it uses the filename.
31
- def track_title
32
- raise NotImplementedError
33
- end
34
-
35
- # Begin playing a track. The track should be either a String representing a
36
- # path to an audio file, or an object responding to #location. The &on_end
37
- # callback will be called when the track is finished playing.
38
- def play(track, &on_end)
39
- raise NotImplementedError
40
- end
41
-
42
- # Stop playback. Typically quits the external audio player process.
43
- def stop
44
- raise NotImplementedError
45
- end
46
-
47
- # Pauses playback.
48
- def pause
49
- raise NotImplementedError
50
- end
51
-
52
- # Resumes playback.
53
- def resume
54
- raise NotImplementedError
55
- end
56
-
57
- # Seek to a particular position in the track. Different types can be
58
- # supported, such as absolute, relative, or percent. All times are specified
59
- # in milliseconds. A NotImplementedError is raised when a certain type isn't
60
- # supported.
61
- def seek(where, type = :absolute)
62
- case type
63
- when :absolute
64
- raise NotImplementedError
65
- when :relative
66
- raise NotImplementedError
67
- when :percent
68
- raise NotImplementedError
69
- else
70
- raise NotImplementedError
71
- end
72
- end
73
-
74
- # Gets the current playback speed. The speed is a multiplier. For example,
75
- # double speed is 2 and half-speed is 0.5. Normal speed is 1.
76
- def speed
77
- raise NotImplementedError
78
- end
79
-
80
- # Sets the playback speed. The speed is a multiplier. For example, for
81
- # double speed you'd set it to 2 and for half-speed you'd set it to 0.5. And
82
- # for normal speed: 1.
83
- def speed=(new_speed)
84
- raise NotImplementedError
85
- end
86
-
87
- # Returns true if audio is muted.
88
- def muted?
89
- raise NotImplementedError
90
- end
91
-
92
- # Mutes the audio player.
93
- def mute
94
- raise NotImplementedError
95
- end
96
-
97
- # Unmutes the audio player.
98
- def unmute
99
- raise NotImplementedError
100
- end
101
-
102
- # Get the current volume as a percentage.
103
- def volume
104
- raise NotImplementedError
105
- end
106
-
107
- # Set the volume as a percentage. The player is automatically unmuted.
108
- def volume=(new_volume)
109
- raise NotImplementedError
110
- end
111
-
112
- # Returns the current time into the song, in milliseconds.
113
- def current_time
114
- raise NotImplementedError
115
- end
116
-
117
- # Returns the length of the current track, in milliseconds.
118
- def total_time
119
- raise NotImplementedError
120
- end
121
-
122
- # Get metadata for the current track from the audio player. Returns a Hash
123
- # with keys like :artist and :album. Values should be Strings, or nil if the
124
- # value is blank.
125
- def metadata
126
- raise NotImplementedError
127
- end
128
- end
129
- end
@@ -1,255 +0,0 @@
1
- module Listlace
2
- module SinglePlayers
3
- # This is the SinglePlayer implementation for mplayer. It requires mplayer
4
- # to be in your $PATH. It uses open4 to start up and communicate with the
5
- # mplayer process, and mplayer's slave protocol to issue commands to mplayer.
6
- class MPlayer < SinglePlayer
7
- # Create a new MPlayer. The mplayer process is only started when the #play
8
- # method is called to start playing a song. The process quits when the
9
- # song ends. Even though a new process is started for each song, the
10
- # MPlayer object keeps track of the volume, speed, and mute properties and
11
- # sets these properties when a new song is played.
12
- def initialize
13
- @paused = false
14
- @muted = false
15
- @volume = 50
16
- @speed = 1.0
17
- @track = nil
18
- end
19
-
20
- def active?
21
- not @track.nil?
22
- end
23
-
24
- def paused?
25
- @paused
26
- end
27
-
28
- def track
29
- @track
30
- end
31
-
32
- def track_title
33
- if active?
34
- if @track.respond_to? :title
35
- @track.title
36
- elsif title = metadata[:title]
37
- title
38
- else
39
- _command "get_file_name", expect_answer: /^ANS_FILENAME='(.+)'$/
40
- end
41
- else
42
- false
43
- end
44
- end
45
-
46
- def play(track, &on_end)
47
- # Make sure we're only playing one song at any one time.
48
- _quit
49
-
50
- # If a path to an audio file passed as the track, wrap it in a SimpleTrack.
51
- track = SimpleTrack.new(track) if track.is_a? String
52
-
53
- # The track object must respond to #location to be a track.
54
- if not track.respond_to? :location
55
- raise ArgumentError, "got a #{track.class} instead of a track"
56
- end
57
-
58
- if File.exists? track.location
59
- # Run the mplayer process in slave mode, passing it the location of
60
- # the track's audio file.
61
- cmd = ["mplayer", "-slave", "-quiet", track.location]
62
- @pid, @stdin, @stdout, @stderr = Open4.popen4(*cmd)
63
-
64
- # This should skip past mplayer's initial lines of output so we can
65
- # start reading its replies to our commands.
66
- until @stdout.gets["playback"]
67
- end
68
-
69
- @paused = false
70
- @track = track
71
-
72
- # Persist the previous speed, volume, and mute properties into this
73
- # process.
74
- self.speed = @speed
75
- self.volume = @volume
76
- mute if @muted
77
-
78
- # Start a thread that waits for the mplayer process to end, then calls
79
- # the end of song callback. If the #quit method is called, this thread
80
- # will be killed if it's still waiting for the process to end.
81
- @quit_hook_active = false
82
- @quit_hook = Thread.new do
83
- Process.wait(@pid)
84
- @quit_hook_active = true
85
- @paused = false
86
- @track = nil
87
- on_end.call
88
- end
89
-
90
- true
91
- else
92
- false
93
- end
94
- end
95
-
96
- def stop
97
- _quit
98
- end
99
-
100
- def pause
101
- if not @paused
102
- @paused = true
103
- _command "pause"
104
- else
105
- false
106
- end
107
- end
108
-
109
- def resume
110
- if @paused
111
- @paused = false
112
- _command "pause"
113
- else
114
- false
115
- end
116
- end
117
-
118
- def seek(where, type = :absolute)
119
- # mplayer talks seconds, not milliseconds.
120
- seconds = where.to_f / 1_000
121
- case type
122
- when :absolute
123
- _command "seek #{seconds} 2", expect_answer: true
124
- when :relative
125
- _command "seek #{seconds} 0", expect_answer: true
126
- when :percent
127
- _command "seek #{where} 1", expect_answer: true
128
- else
129
- raise NotImplementedError
130
- end
131
- end
132
-
133
- def speed
134
- @speed
135
- end
136
-
137
- def speed=(new_speed)
138
- @speed = new_speed.to_f
139
- answer = _command "speed_set #{@speed}", expect_answer: true
140
- !!answer
141
- end
142
-
143
- def muted?
144
- @muted
145
- end
146
-
147
- def mute
148
- @muted = true
149
- answer = _command "mute 1", expect_answer: true
150
- !!answer
151
- end
152
-
153
- def unmute
154
- @muted = false
155
- answer = _command "mute 0", expect_answer: true
156
- !!answer
157
- end
158
-
159
- def volume
160
- @volume
161
- end
162
-
163
- def volume=(new_volume)
164
- @muted = false
165
- @volume = new_volume.to_f
166
- answer = _command "volume #{@volume} 1", expect_answer: true
167
- !!answer
168
- end
169
-
170
- def current_time
171
- answer = _command "get_time_pos", expect_answer: /^ANS_TIME_POSITION=([0-9.]+)$/
172
- answer ? (answer.to_f * 1000).to_i : false
173
- end
174
-
175
- def total_time
176
- answer = _command "get_time_length", expect_answer: /^ANS_LENGTH=([0-9.]+)$/
177
- answer ? (answer.to_f * 1000).to_i : false
178
- end
179
-
180
- def metadata
181
- properties = %w(album artist comment genre title track year)
182
- properties.inject({}) do |hash, property|
183
- answer = _command "get_meta_#{property}", expect_answer: /^ANS_META_#{property.upcase}='(.+)'$/
184
- hash[property.to_sym] = answer || nil
185
- hash
186
- end
187
- end
188
-
189
- private
190
-
191
- # Issue a command to mplayer through the slave protocol. False is returned
192
- # if the process is dead (not playing anything).
193
- #
194
- # If :expect_answer option is set to true, this will wait for a legible
195
- # answer back from mplayer, and send it as a return value. If :expect_answer
196
- # is set to a Regexp, the answer mplayer gives back will be matched to that
197
- # Regexp and the first match will be returned. If there are no matches, nil
198
- # will be returned.
199
- def _command(cmd, options = {})
200
- if _alive? and active?
201
- # If the player is paused, prefix the command with "pausing ".
202
- # Otherwise it unpauses when it runs a command. The only exception to
203
- # this is when the "pause" command itself is issued.
204
- if paused? and cmd != "pause"
205
- cmd = "pausing #{cmd}"
206
- end
207
-
208
- # Send the command to mplayer.
209
- @stdin.puts cmd
210
-
211
- if options[:expect_answer]
212
- # Read lines of output from mplayer until we get an actual message.
213
- answer = "\n"
214
- while answer == "\n"
215
- answer = @stdout.gets.sub("\e[A\r\e[K", "")
216
- answer = "\n" if options[:expect_answer].is_a?(Regexp) && answer !~ options[:expect_answer]
217
- end
218
-
219
- if options[:expect_answer].is_a? Regexp
220
- matches = answer.match(options[:expect_answer])
221
- answer = matches && matches[1]
222
- end
223
-
224
- answer
225
- else
226
- true
227
- end
228
- else
229
- false
230
- end
231
- end
232
-
233
- # Quit the mplayer process, stopping playback. The end of song callback
234
- # will not be called if this method is called.
235
- def _quit
236
- if _alive?
237
- @quit_hook.kill unless @quit_hook_active
238
- _command "quit"
239
- @paused = false
240
- @track = nil
241
- end
242
- true
243
- end
244
-
245
- # Check if the mplayer process is still around.
246
- def _alive?
247
- return false if @pid.nil?
248
- Process.getpgid(@pid)
249
- true
250
- rescue Errno::ESRCH
251
- false
252
- end
253
- end
254
- end
255
- end
@@ -1,34 +0,0 @@
1
- module Listlace
2
- # Helper method to format a number of milliseconds as a string like
3
- # "1:03:56.555". The only option is :include_milliseconds, true by default. If
4
- # false, milliseconds won't be included in the formatted string.
5
- def self.format_time(milliseconds, options = {})
6
- ms = milliseconds % 1000
7
- seconds = (milliseconds / 1000) % 60
8
- minutes = (milliseconds / 60000) % 60
9
- hours = milliseconds / 3600000
10
-
11
- if ms.zero? || options[:include_milliseconds] == false
12
- ms_string = ""
13
- else
14
- ms_string = ".%03d" % [ms]
15
- end
16
-
17
- if hours > 0
18
- "%d:%02d:%02d%s" % [hours, minutes, seconds, ms_string]
19
- else
20
- "%d:%02d%s" % [minutes, seconds, ms_string]
21
- end
22
- end
23
-
24
- # Helper method to parse a string like "1:03:56.555" and return the number of
25
- # milliseconds that time length represents.
26
- def self.parse_time(string)
27
- parts = string.split(":").map(&:to_f)
28
- parts = [0] + parts if parts.length == 2
29
- hours, minutes, seconds = parts
30
- seconds = hours * 3600 + minutes * 60 + seconds
31
- milliseconds = seconds * 1000
32
- milliseconds.to_i
33
- end
34
- end