listlace 0.0.6 → 0.0.7

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