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 +1 -1
- data/lib/listlace/commands/library_commands.rb +7 -76
- data/lib/listlace/commands/player_commands.rb +9 -7
- data/lib/listlace/{array_ext.rb → core_ext/array.rb} +18 -1
- data/lib/listlace/library.rb +86 -3
- data/lib/listlace/player.rb +99 -74
- data/lib/listlace/single_player.rb +74 -0
- data/lib/listlace/single_players/mplayer.rb +144 -0
- data/lib/listlace.rb +14 -4
- data/listlace.gemspec +2 -2
- metadata +5 -6
- data/lib/listlace/commands.rb +0 -7
- data/lib/listlace/models.rb +0 -3
- data/lib/listlace/player/mplayer.rb +0 -53
data/Gemfile.lock
CHANGED
@@ -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
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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.
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
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
|
-
|
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
|
data/lib/listlace/library.rb
CHANGED
@@ -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
|
data/lib/listlace/player.rb
CHANGED
@@ -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
|
5
|
-
#
|
6
|
-
#
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
70
|
+
play_track @current_track
|
71
|
+
true
|
72
|
+
else
|
73
|
+
false
|
57
74
|
end
|
58
75
|
end
|
59
76
|
|
60
77
|
def stop
|
61
|
-
@
|
62
|
-
@mplayer = nil
|
78
|
+
@single_player.stop
|
63
79
|
@current_track = nil
|
64
80
|
@current_track_index = nil
|
65
|
-
@
|
81
|
+
@playlist_paused = false
|
66
82
|
@started = false
|
83
|
+
true
|
67
84
|
end
|
68
85
|
|
69
86
|
def pause
|
70
|
-
|
71
|
-
@paused = true
|
72
|
-
@mplayer.command "pause"
|
73
|
-
end
|
87
|
+
@single_player.pause
|
74
88
|
end
|
75
89
|
|
76
90
|
def resume
|
77
|
-
if
|
78
|
-
@
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
108
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
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
|
141
|
-
@
|
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
|
-
|
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
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
@current_track_index
|
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
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
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
|
205
|
-
@
|
206
|
-
|
207
|
-
|
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/
|
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
|
-
|
12
|
-
require "listlace/
|
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.
|
4
|
-
s.date = "2012-
|
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.
|
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-
|
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/
|
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:
|
data/lib/listlace/commands.rb
DELETED
data/lib/listlace/models.rb
DELETED
@@ -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
|