listlace 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- listlace (0.0.5)
4
+ listlace (0.0.6)
5
5
  activerecord
6
6
  activesupport
7
7
  open4
@@ -1,89 +1,20 @@
1
1
  module Listlace
2
2
  module Commands
3
3
  module LibraryCommands
4
+ # Save a playlist to the database. As a shortcut, you can pass the name
5
+ # to save it as, instead of setting the name on the playlist and then
6
+ # saving it.
4
7
  def save(playlist, name = nil)
5
8
  playlist.name = name if name
6
- if model = library.playlists.where(name: playlist.name).first
7
- model.playlist_items.destroy_all
8
- else
9
- model = library.playlists.new(name: playlist.name)
10
- model.save!
11
- end
12
-
13
- playlist.each.with_index do |track, i|
14
- item = PlaylistItem.new(position: i)
15
- item.playlist = model
16
- item.track = track
17
- item.save!
18
- end
19
- playlist
9
+ library.save_playlist(playlist)
20
10
  end
21
11
 
22
12
  # Imports the music library from another program. Currently only iTunes is
23
13
  # supported.
24
14
  def import(from, path)
25
- if not File.exists?(path)
26
- puts "File '%s' doesn't exist." % [path]
27
- elsif from == :itunes
28
- puts "Parsing XML..."
29
- data = Plist::parse_xml(path)
30
-
31
- puts "Importing #{data['Tracks'].length} tracks..."
32
- num_tracks = 0
33
- whitelist = library.tracks.new.attributes.keys
34
- data["Tracks"].each do |track_id, row|
35
- # row already contains a hash of attributes almost ready to be passed to
36
- # ActiveRecord. We just need to modify the keys, e.g. change "Play Count"
37
- # to "play_count".
38
- attributes = row.inject({}) do |acc, (key, value)|
39
- attribute = key.gsub(" ", "").underscore
40
- attribute = "original_id" if attribute == "track_id"
41
- acc[attribute] = value if whitelist.include? attribute
42
- acc
43
- end
44
-
45
- # change iTunes' URL-style locations into simple paths
46
- if attributes["location"] && attributes["location"] =~ /^file:\/\//
47
- attributes["location"].sub! /^file:\/\/localhost/, ""
48
-
49
- # CGI::unescape changes plus signs to spaces. This is a work around to
50
- # keep the plus signs.
51
- attributes["location"].gsub! "+", "%2B"
52
-
53
- attributes["location"] = CGI::unescape(attributes["location"])
54
- end
55
-
56
- track = library.tracks.new(attributes)
57
-
58
- if track.kind =~ /audio/
59
- if track.save
60
- num_tracks += 1
61
- end
62
- else
63
- puts "[skipping non-audio file]"
64
- end
65
- end
66
- puts "Imported #{num_tracks} tracks successfully."
67
-
68
- puts "Importing #{data['Playlists'].length} playlists..."
69
- num_playlists = 0
70
- data["Playlists"].each do |playlist_data|
71
- playlist = []
72
- playlist.name = playlist_data["Name"]
73
-
74
- if ["Library", "Music", "Movies", "TV Shows", "iTunes DJ"].include? playlist.name
75
- puts "[skipping \"#{playlist.name}\" playlist]"
76
- else
77
- playlist_data["Playlist Items"].map(&:values).flatten.each do |original_id|
78
- playlist << library.tracks.where(original_id: original_id).first
79
- end
80
- playlist.compact!
81
- save playlist
82
- num_playlists += 1
83
- end
84
- end
85
- puts "Imported #{num_playlists} playlists successfully."
86
- end
15
+ library.import(from, path, logger: method(:puts))
16
+ rescue Library::FileNotFoundError => e
17
+ puts e.message
87
18
  end
88
19
 
89
20
  # Wipes the database. With no arguments, it just asks "Are you sure?" without
@@ -17,7 +17,7 @@ module Listlace
17
17
  player.pause
18
18
  status
19
19
  else
20
- player.set_speed 1
20
+ player.speed = 1
21
21
  status
22
22
  end
23
23
  else
@@ -29,7 +29,6 @@ module Listlace
29
29
  end
30
30
  end
31
31
  else
32
- player.stop
33
32
  player.clear
34
33
  q *tracks
35
34
  p
@@ -51,7 +50,8 @@ module Listlace
51
50
 
52
51
  # Go back one song in the queue.
53
52
  def back(n = 1)
54
- if player.back(n)
53
+ player.back(n)
54
+ if player.started?
55
55
  status
56
56
  else
57
57
  puts "End of queue."
@@ -60,7 +60,8 @@ module Listlace
60
60
 
61
61
  # Go directly to the next song in the queue.
62
62
  def skip(n = 1)
63
- if player.skip(n)
63
+ player.skip(n)
64
+ if player.started?
64
65
  status
65
66
  else
66
67
  puts "End of queue."
@@ -83,7 +84,7 @@ module Listlace
83
84
  # find agreeable. Call p to go back to normal. You can also pass a value
84
85
  # smaller than one to slow down.
85
86
  def ff(speed = 2)
86
- player.set_speed(speed)
87
+ player.speed = speed
87
88
  status
88
89
  end
89
90
 
@@ -112,7 +113,7 @@ module Listlace
112
113
  case type
113
114
  when :playlist
114
115
  if player.started?
115
- track_number = player.current_track_index + 1
116
+ track_number = player.current_track_index + 1
116
117
  num_tracks = q.length
117
118
  repeat_one = player.repeat_mode == :one ? REPEAT_SYMBOL : ""
118
119
  repeat_all = player.repeat_mode == :all ? REPEAT_SYMBOL : ""
@@ -127,7 +128,8 @@ module Listlace
127
128
  time = player.formatted_current_time
128
129
  total_time = player.current_track.formatted_total_time
129
130
  paused = player.paused? ? "|| " : ""
130
- speed = player.speed != 1 ? "#{TIMES_SYMBOL}#{player.speed} " : ""
131
+ speed = player.speed
132
+ speed = speed != 1 ? "#{TIMES_SYMBOL}#{speed} " : ""
131
133
  puts "%s - %s (%s / %s) %s%s" % [name, artist, time, total_time, paused, speed]
132
134
  else
133
135
  puts "Stopped."
@@ -1,6 +1,8 @@
1
1
  class Array
2
2
  attr_accessor :name
3
3
 
4
+ # Check if this array is a playlist. It's a playlist if it has
5
+ # a name attribute set or consists entirely of Track instances.
4
6
  def playlist?
5
7
  if @name || all? { |x| x.is_a? Listlace::Track }
6
8
  @name ||= ""
@@ -10,16 +12,29 @@ class Array
10
12
  end
11
13
  end
12
14
 
15
+ # Returns a new array that is shuffled, but with elem at the top.
16
+ # This is how playlists that are currently playing are shuffled.
17
+ # The currently playing track goes to the top, the rest of the
18
+ # tracks are shuffled.
13
19
  def shuffle_except(elem)
14
20
  ary = dup
15
21
  dup.shuffle_except! elem
16
22
  dup
17
23
  end
18
24
 
25
+ # Like shuffle_except, but shuffles in-place.
19
26
  def shuffle_except!(elem)
20
- replace([elem] + (self - [elem]).shuffle)
27
+ if i = index(elem)
28
+ delete_at(i)
29
+ shuffle!
30
+ unshift(elem)
31
+ else
32
+ shuffle!
33
+ end
21
34
  end
22
35
 
36
+ # Override to_s to check if the array is a playlist, and format
37
+ # it accordingly.
23
38
  alias _original_to_s to_s
24
39
  def to_s
25
40
  if playlist?
@@ -29,11 +44,13 @@ class Array
29
44
  end
30
45
  end
31
46
 
47
+ # Override inspect for nice pry output.
32
48
  alias _original_inspect inspect
33
49
  def inspect
34
50
  playlist? ? to_s : _original_inspect
35
51
  end
36
52
 
53
+ # Override pretty_inspect for nice pry output.
37
54
  alias _original_pretty_inspect pretty_inspect
38
55
  def pretty_inspect
39
56
  playlist? ? inspect : _original_pretty_inspect
@@ -1,8 +1,7 @@
1
- require "listlace/library/database"
2
- require "listlace/library/selectors"
3
-
4
1
  module Listlace
5
2
  class Library
3
+ class FileNotFoundError < ArgumentError; end
4
+
6
5
  def initialize(options = {})
7
6
  options[:db_path] ||= "library"
8
7
  options[:db_adapter] ||= "sqlite3"
@@ -37,5 +36,89 @@ module Listlace
37
36
  def wipe
38
37
  Database.wipe(@db_adapter, @db_path)
39
38
  end
39
+
40
+ def save_playlist(playlist)
41
+ playlist_table = playlists.arel_table
42
+ if model = playlists.where(playlist_table[:name].matches(playlist.name)).first
43
+ model.playlist_items.destroy_all
44
+ else
45
+ model = playlists.new(name: playlist.name)
46
+ model.save!
47
+ end
48
+
49
+ playlist.each.with_index do |track, i|
50
+ item = PlaylistItem.new(position: i)
51
+ item.playlist = model
52
+ item.track = track
53
+ item.save!
54
+ end
55
+ playlist
56
+ end
57
+
58
+ def import(from, path, options = {})
59
+ logger = options[:logger]
60
+ if not File.exists?(path)
61
+ raise FileNotFoundError, "File '%s' doesn't exist." % [path]
62
+ elsif from == :itunes
63
+ logger.("Parsing XML...") if logger
64
+ data = Plist::parse_xml(path)
65
+
66
+ logger.("Importing #{data['Tracks'].length} tracks...") if logger
67
+ num_tracks = 0
68
+ whitelist = tracks.new.attributes.keys
69
+ data["Tracks"].each do |track_id, row|
70
+ # row already contains a hash of attributes almost ready to be passed to
71
+ # ActiveRecord. We just need to modify the keys, e.g. change "Play Count"
72
+ # to "play_count".
73
+ attributes = row.inject({}) do |acc, (key, value)|
74
+ attribute = key.gsub(" ", "").underscore
75
+ attribute = "original_id" if attribute == "track_id"
76
+ acc[attribute] = value if whitelist.include? attribute
77
+ acc
78
+ end
79
+
80
+ # change iTunes' URL-style locations into simple paths
81
+ if attributes["location"] && attributes["location"] =~ /^file:\/\//
82
+ attributes["location"].sub! /^file:\/\/localhost/, ""
83
+
84
+ # CGI::unescape changes plus signs to spaces. This is a work around to
85
+ # keep the plus signs.
86
+ attributes["location"].gsub! "+", "%2B"
87
+
88
+ attributes["location"] = CGI::unescape(attributes["location"])
89
+ end
90
+
91
+ track = tracks.new(attributes)
92
+
93
+ if track.kind =~ /audio/
94
+ if track.save
95
+ num_tracks += 1
96
+ end
97
+ else
98
+ logger.("[skipping non-audio file]") if logger
99
+ end
100
+ end
101
+ logger.("Imported #{num_tracks} tracks successfully.") if logger
102
+
103
+ logger.("Importing #{data['Playlists'].length} playlists...") if logger
104
+ num_playlists = 0
105
+ data["Playlists"].each do |playlist_data|
106
+ playlist = []
107
+ playlist.name = playlist_data["Name"]
108
+
109
+ if ["Library", "Music", "Movies", "TV Shows", "iTunes DJ"].include? playlist.name
110
+ logger.("[skipping \"#{playlist.name}\" playlist]") if logger
111
+ else
112
+ playlist_data["Playlist Items"].map(&:values).flatten.each do |original_id|
113
+ playlist << tracks.where(original_id: original_id).first
114
+ end
115
+ playlist.compact!
116
+ save_playlist playlist
117
+ num_playlists += 1
118
+ end
119
+ end
120
+ logger.("Imported #{num_playlists} playlists successfully.") if logger
121
+ end
122
+ end
40
123
  end
41
124
  end
@@ -1,19 +1,27 @@
1
- require "listlace/player/mplayer"
2
-
3
1
  module Listlace
4
- # This is the music box. It contains a queue, which is an array of tracks. It
5
- # then plays these tracks sequentially. The buttons for play, pause, next,
6
- # previous, etc. are all located here.
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.
7
13
  class Player
14
+ DEFAULT_SINGLE_PLAYER = SinglePlayers::MPlayer
15
+
8
16
  attr_reader :current_track, :current_track_index, :repeat_mode
9
17
 
10
18
  def initialize
11
- @mplayer = nil
19
+ @single_player = DEFAULT_SINGLE_PLAYER.new
12
20
  @queue = []
13
21
  @queue.name = :queue
14
22
  @current_track = nil
15
23
  @current_track_index = nil
16
- @paused = false
24
+ @playlist_paused = false
17
25
  @started = false
18
26
  @repeat_mode = false
19
27
  end
@@ -34,14 +42,19 @@ module Listlace
34
42
  stop
35
43
  @queue.clear
36
44
  @queue.name = :queue
45
+ true
37
46
  end
38
47
 
39
48
  def empty?
40
49
  @queue.empty?
41
50
  end
42
51
 
52
+ def playlist_paused?
53
+ @playlist_paused
54
+ end
55
+
43
56
  def paused?
44
- @paused
57
+ playlist_paused? or @single_player.paused?
45
58
  end
46
59
 
47
60
  def started?
@@ -51,36 +64,36 @@ module Listlace
51
64
  def start
52
65
  unless empty?
53
66
  @started = true
67
+ @playlist_paused = false
54
68
  @current_track = @queue.first
55
69
  @current_track_index = 0
56
- load_track(@current_track)
70
+ play_track @current_track
71
+ true
72
+ else
73
+ false
57
74
  end
58
75
  end
59
76
 
60
77
  def stop
61
- @mplayer.quit if @mplayer
62
- @mplayer = nil
78
+ @single_player.stop
63
79
  @current_track = nil
64
80
  @current_track_index = nil
65
- @paused = false
81
+ @playlist_paused = false
66
82
  @started = false
83
+ true
67
84
  end
68
85
 
69
86
  def pause
70
- if not paused?
71
- @paused = true
72
- @mplayer.command "pause"
73
- end
87
+ @single_player.pause
74
88
  end
75
89
 
76
90
  def resume
77
- if paused?
78
- @paused = false
79
- if @mplayer && @mplayer.alive?
80
- @mplayer.command "pause"
81
- else
82
- load_track @current_track
83
- end
91
+ if playlist_paused?
92
+ play_track @current_track
93
+ @playlist_paused = false
94
+ true
95
+ else
96
+ @single_player.resume
84
97
  end
85
98
  end
86
99
 
@@ -93,6 +106,7 @@ module Listlace
93
106
  when :off
94
107
  @repeat_mode = false
95
108
  end
109
+ true
96
110
  end
97
111
 
98
112
  def restart
@@ -104,41 +118,47 @@ module Listlace
104
118
  end
105
119
 
106
120
  def skip(n = 1)
107
- @current_track.increment! :skip_count
108
- @current_track.update_column :skip_date, Time.now
121
+ if @current_track
122
+ @current_track.increment! :skip_count
123
+ @current_track.update_column :skip_date, Time.now
124
+ end
109
125
  change_track(n)
110
126
  end
111
127
 
112
128
  def seek(where)
113
- case where
114
- when Integer
115
- @mplayer.command("seek %d 0" % [where], expect_answer: true)
116
- when Range
117
- @mplayer.command("seek %d 2" % [where.begin * 60 + where.end], expect_answer: true)
118
- when String
119
- @mplayer.command("seek %d 2" % [Track.parse_time(where) / 1000], expect_answer: true)
120
- when Hash
121
- if where[:abs]
122
- if where[:abs].is_a? Integer
123
- @mplayer.command("seek %d 2" % [where[:abs]], expect_answer: true)
124
- else
125
- seek(where[:abs])
129
+ if playlist_paused?
130
+ resume
131
+ pause
132
+ seek where
133
+ else
134
+ case where
135
+ when Integer
136
+ @single_player.seek(where, :relative)
137
+ when Range
138
+ seconds = where.begin * 60 + where.end
139
+ @single_player.seek(seconds * 1000, :absolute)
140
+ when String
141
+ @single_player.seek(Track.parse_time(where), :absolute)
142
+ when Hash
143
+ if where[:abs]
144
+ if where[:abs].is_a? Integer
145
+ @single_player.seek(where[:abs], :absolute)
146
+ else
147
+ seek(where[:abs])
148
+ end
149
+ elsif where[:percent]
150
+ @single_player.seek(where[:percent], :percent)
126
151
  end
127
- elsif where[:percent]
128
- @mplayer.command("seek %d 1" % [where[:percent]], expect_answer: true)
129
152
  end
130
153
  end
131
154
  end
132
155
 
133
156
  def speed
134
- answer = @mplayer.command("get_property speed", expect_answer: true)
135
- if answer =~ /^ANS_speed=([0-9.]+)$/
136
- $1.to_f
137
- end
157
+ @single_player.active? ? @single_player.speed : 1.0
138
158
  end
139
159
 
140
- def set_speed(speed)
141
- @mplayer.command("speed_set %f" % [speed], expect_answer: true)
160
+ def speed=(new_speed)
161
+ @single_player.speed(new_speed)
142
162
  end
143
163
 
144
164
  def shuffle
@@ -148,21 +168,19 @@ module Listlace
148
168
  else
149
169
  @queue.shuffle!
150
170
  end
171
+ true
151
172
  end
152
173
 
153
174
  def sort(&by)
154
175
  @queue.sort! &by
155
-
156
176
  if started?
157
177
  @current_track_index = @queue.index(@current_track)
158
178
  end
179
+ true
159
180
  end
160
181
 
161
182
  def current_time
162
- answer = @mplayer.command "get_time_pos", expect_answer: true
163
- if answer =~ /^ANS_TIME_POSITION=([0-9.]+)$/
164
- ($1.to_f * 1000).to_i
165
- end
183
+ @single_player.active? ? @single_player.current_time : 0
166
184
  end
167
185
 
168
186
  def formatted_current_time
@@ -172,39 +190,46 @@ module Listlace
172
190
  private
173
191
 
174
192
  def change_track(by = 1, options = {})
175
- if options[:auto]
176
- @current_track.increment! :play_count
177
- @current_track.update_column :play_date_utc, Time.now
178
- end
179
- @current_track_index += by
180
- if options[:auto] && @repeat_mode
181
- case @repeat_mode
182
- when :one
183
- @current_track_index -= by
184
- when :all
185
- if @current_track_index >= @queue.length
186
- @current_track_index = 0
193
+ if started?
194
+ if options[:auto]
195
+ @current_track.increment! :play_count
196
+ @current_track.update_column :play_date_utc, Time.now
197
+ end
198
+ @current_track_index += by
199
+ if options[:auto] && @repeat_mode
200
+ case @repeat_mode
201
+ when :one
202
+ @current_track_index -= by
203
+ when :all
204
+ if @current_track_index >= @queue.length
205
+ @current_track_index = 0
206
+ end
187
207
  end
188
208
  end
189
- end
190
- @current_track = @queue[@current_track_index]
191
- if @current_track && @current_track_index >= 0
192
- if paused?
193
- @mplayer.quit if @mplayer
209
+ @current_track = @queue[@current_track_index]
210
+ if @current_track && @current_track_index >= 0
211
+ if @single_player.paused?
212
+ @single_player.stop
213
+ @playlist_paused = true
214
+ elsif not playlist_paused?
215
+ play_track @current_track
216
+ end
194
217
  else
195
- load_track(@current_track)
218
+ stop
196
219
  end
197
220
  true
198
221
  else
199
- stop
200
222
  false
201
223
  end
202
224
  end
203
225
 
204
- def load_track(track)
205
- @mplayer.quit if @mplayer
206
- @mplayer = MPlayer.new(track) { send(:change_track, 1, auto: true) }
207
- @paused = false
226
+ def play_track(track)
227
+ @single_player.play(track) do
228
+ change_track(1, auto: true)
229
+ ActiveRecord::Base.connection.close if defined?(ActiveRecord)
230
+ end
231
+ @playlist_paused = false
232
+ true
208
233
  end
209
234
  end
210
235
  end
@@ -0,0 +1,74 @@
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
+ # Begin playing a track. The track should respond to #location, which is the
24
+ # path where the audio file is located. The &on_end callback will be called
25
+ # when the track is finished playing.
26
+ def play(track, &on_end)
27
+ raise NotImplementedError
28
+ end
29
+
30
+ # Stop playback. Typically quits the external audio player process.
31
+ def stop
32
+ raise NotImplementedError
33
+ end
34
+
35
+ # Pauses playback.
36
+ def pause
37
+ raise NotImplementedError
38
+ end
39
+
40
+ # Resumes playback.
41
+ def resume
42
+ raise NotImplementedError
43
+ end
44
+
45
+ # Seek to a particular position in the track. Different types can be
46
+ # supported, such as absolute, relative, or percent. All times are specified
47
+ # in milliseconds. A NotImplementedError is raised when a certain type isn't
48
+ # supported.
49
+ def seek(where, type = :absolute)
50
+ case type
51
+ when :absolute
52
+ raise NotImplementedError
53
+ when :relative
54
+ raise NotImplementedError
55
+ when :perent
56
+ raise NotImplementedError
57
+ else
58
+ raise NotImplementedError
59
+ end
60
+ end
61
+
62
+ # Gets or sets the playback speed. If a new speed is passed as an argument,
63
+ # it sets it to that speed. The speed is a multiplier. For example, for
64
+ # double speed you'd call speed(2) and for half-speed you'd call speed(0.5).
65
+ def speed(new_speed = nil)
66
+ raise NotImplementedError
67
+ end
68
+
69
+ # Returns the current time into the song, in milliseconds.
70
+ def current_time
71
+ raise NotImplementedError
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,144 @@
1
+ module Listlace
2
+ module SinglePlayers
3
+ class MPlayer < SinglePlayer
4
+ def initialize
5
+ @active = false
6
+ @paused = false
7
+ end
8
+
9
+ def active?
10
+ @active
11
+ end
12
+
13
+ def paused?
14
+ @paused
15
+ end
16
+
17
+ def play(track, &on_end)
18
+ _quit
19
+
20
+ if File.exists? track.location
21
+ cmd = ["mplayer", "-slave", "-quiet", track.location]
22
+ @pid, @stdin, @stdout, @stderr = Open4.popen4(*cmd)
23
+
24
+ until @stdout.gets["playback"]
25
+ end
26
+
27
+ @active = true
28
+ @paused = false
29
+
30
+ @quit_hook_active = false
31
+ @quit_hook = Thread.new do
32
+ Process.wait(@pid)
33
+ @quit_hook_active = true
34
+ @active = false
35
+ on_end.call
36
+ end
37
+
38
+ true
39
+ else
40
+ false
41
+ end
42
+ end
43
+
44
+ def stop
45
+ _quit
46
+ end
47
+
48
+ def pause
49
+ if not @paused
50
+ _command "pause"
51
+ else
52
+ false
53
+ end
54
+ end
55
+
56
+ def resume
57
+ if @paused
58
+ _command "pause"
59
+ else
60
+ false
61
+ end
62
+ end
63
+
64
+ def seek(where, type = :absolute)
65
+ seconds = where.to_f / 1000
66
+ case type
67
+ when :absolute
68
+ _command "seek #{seconds} 2", expect_answer: true
69
+ when :relative
70
+ _command "seek #{seconds} 0", expect_answer: true
71
+ when :percent
72
+ _command "seek #{where} 1", expect_answer: true
73
+ else
74
+ raise NotImplementedError
75
+ end
76
+ end
77
+
78
+ def speed(new_speed = nil)
79
+ if new_speed
80
+ answer = _command "speed_set #{new_speed.to_f}", expect_answer: true
81
+ !!answer
82
+ else
83
+ answer = _command "get_property speed", expect_answer: true
84
+ if answer && answer =~ /^ANS_speed=([0-9.]+)$/
85
+ $1.to_f
86
+ else
87
+ false
88
+ end
89
+ end
90
+ end
91
+
92
+ def current_time
93
+ answer = _command "get_time_pos", expect_answer: true
94
+ if answer && answer =~ /^ANS_TIME_POSITION=([0-9.]+)$/
95
+ ($1.to_f * 1000).to_i
96
+ else
97
+ false
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def _command(cmd, options = {})
104
+ if _alive? and active?
105
+ if cmd == "pause"
106
+ @paused = !@paused
107
+ elsif @paused
108
+ cmd = "pausing #{cmd}"
109
+ end
110
+
111
+ @stdin.puts cmd
112
+
113
+ if options[:expect_answer]
114
+ answer = "\n"
115
+ answer = @stdout.gets.sub("\e[A\r\e[K", "") while answer == "\n"
116
+ answer
117
+ else
118
+ true
119
+ end
120
+ else
121
+ false
122
+ end
123
+ end
124
+
125
+ def _quit
126
+ if _alive?
127
+ @quit_hook.kill unless @quit_hook_active
128
+ _command "quit"
129
+ @active = false
130
+ @paused = false
131
+ end
132
+ true
133
+ end
134
+
135
+ def _alive?
136
+ return false if @pid.nil?
137
+ Process.getpgid(@pid)
138
+ true
139
+ rescue Errno::ESRCH
140
+ false
141
+ end
142
+ end
143
+ end
144
+ end
data/lib/listlace.rb CHANGED
@@ -1,15 +1,25 @@
1
1
  require "open4"
2
- require "shellwords"
3
2
  require "active_record"
4
3
  require "fileutils"
5
4
  require "plist"
6
5
  require "active_support/core_ext/string"
7
6
 
8
- require "listlace/array_ext"
7
+ require "listlace/core_ext/array"
8
+
9
+ require "listlace/models/track"
10
+ require "listlace/models/playlist"
11
+ require "listlace/models/playlist_item"
12
+
9
13
  require "listlace/library"
14
+ require "listlace/library/database"
15
+ require "listlace/library/selectors"
16
+
17
+ require "listlace/single_player"
18
+ require "listlace/single_players/mplayer"
10
19
  require "listlace/player"
11
- require "listlace/commands"
12
- require "listlace/models"
20
+
21
+ require "listlace/commands/library_commands"
22
+ require "listlace/commands/player_commands"
13
23
 
14
24
  module Listlace
15
25
  extend Listlace::Library::Selectors
data/listlace.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "listlace"
3
- s.version = "0.0.6"
4
- s.date = "2012-08-31"
3
+ s.version = "0.0.7"
4
+ s.date = "2012-09-03"
5
5
  s.summary = "A music player in a REPL."
6
6
  s.description = "Listlace is a music player which is interacted with through a Ruby REPL."
7
7
  s.author = "Jeremy Ruten"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: listlace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
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: 2012-08-31 00:00:00.000000000 Z
12
+ date: 2012-09-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: pry
@@ -137,19 +137,18 @@ files:
137
137
  - README.md
138
138
  - README.old
139
139
  - bin/listlace
140
- - lib/listlace/array_ext.rb
141
140
  - lib/listlace/commands/library_commands.rb
142
141
  - lib/listlace/commands/player_commands.rb
143
- - lib/listlace/commands.rb
142
+ - lib/listlace/core_ext/array.rb
144
143
  - lib/listlace/library/database.rb
145
144
  - lib/listlace/library/selectors.rb
146
145
  - lib/listlace/library.rb
147
146
  - lib/listlace/models/playlist.rb
148
147
  - lib/listlace/models/playlist_item.rb
149
148
  - lib/listlace/models/track.rb
150
- - lib/listlace/models.rb
151
- - lib/listlace/player/mplayer.rb
152
149
  - lib/listlace/player.rb
150
+ - lib/listlace/single_player.rb
151
+ - lib/listlace/single_players/mplayer.rb
153
152
  - lib/listlace.rb
154
153
  homepage: http://github.com/yjerem/listlace
155
154
  licenses:
@@ -1,7 +0,0 @@
1
- require "listlace/commands/library_commands"
2
- require "listlace/commands/player_commands"
3
-
4
- module Listlace
5
- module Commands
6
- end
7
- end
@@ -1,3 +0,0 @@
1
- require "listlace/models/track"
2
- require "listlace/models/playlist"
3
- require "listlace/models/playlist_item"
@@ -1,53 +0,0 @@
1
- module Listlace
2
- class Player
3
- # This is a simple MPlayer wrapper, it just handles opening the MPlayer
4
- # process, hooking into when mplayer exits (when the song is done), and
5
- # issuing commands through the slave protocol.
6
- class MPlayer
7
- def initialize(track, &on_quit)
8
- cmd = "/usr/bin/mplayer -slave -quiet #{Shellwords.shellescape(track.location)}"
9
- @pid, @stdin, @stdout, @stderr = Open4.popen4(cmd)
10
- @paused = false
11
- @extra_lines = 0
12
-
13
- until @stdout.gets["playback"]
14
- end
15
-
16
- @quit_hook_active = false
17
- @quit_hook = Thread.new do
18
- Process.wait(@pid)
19
- @quit_hook_active = true
20
- on_quit.call
21
- end
22
- end
23
-
24
- def command(cmd, options = {})
25
- if cmd == "pause"
26
- @paused = !@paused
27
- elsif @paused
28
- cmd = "pausing #{cmd}"
29
- end
30
-
31
- @stdin.puts cmd
32
-
33
- if options[:expect_answer]
34
- answer = "\n"
35
- answer = @stdout.gets.sub("\e[A\r\e[K", "") while answer == "\n"
36
- answer
37
- end
38
- end
39
-
40
- def quit
41
- @quit_hook.kill unless @quit_hook_active
42
- command "quit" if alive?
43
- end
44
-
45
- def alive?
46
- Process.getpgid(@pid)
47
- true
48
- rescue Errno::ESRCH
49
- false
50
- end
51
- end
52
- end
53
- end