listlace 0.0.9 → 0.1.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.
@@ -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