walkman 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +7 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +52 -0
- data/Rakefile +6 -0
- data/lib/walkman.rb +57 -0
- data/lib/walkman/cli.rb +128 -0
- data/lib/walkman/commands/controls.rb +33 -0
- data/lib/walkman/commands/information.rb +26 -0
- data/lib/walkman/commands/player.rb +15 -0
- data/lib/walkman/commands/playlist.rb +17 -0
- data/lib/walkman/commands/queueing.rb +31 -0
- data/lib/walkman/config.rb +94 -0
- data/lib/walkman/player.rb +105 -0
- data/lib/walkman/playlist.rb +139 -0
- data/lib/walkman/services/base.rb +25 -0
- data/lib/walkman/services/rdio.rb +64 -0
- data/lib/walkman/services/rdio/rdio_player.rb +85 -0
- data/lib/walkman/song.rb +11 -0
- data/spec/factories/echowrap.rb +79 -0
- data/spec/factories/playlist.rb +32 -0
- data/spec/factories/song.rb +13 -0
- data/spec/sinatra_helper.rb +12 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/factory_girl.rb +7 -0
- data/spec/walkman/commands/controls_spec.rb +26 -0
- data/spec/walkman/commands/information_spec.rb +45 -0
- data/spec/walkman/commands/player_spec.rb +19 -0
- data/spec/walkman/commands/queueing_spec.rb +62 -0
- data/spec/walkman/player_spec.rb +102 -0
- data/spec/walkman/playlist_spec.rb +150 -0
- data/spec/walkman/services/base_spec.rb +13 -0
- data/spec/walkman/services/rdio/rdio_player_spec.rb +24 -0
- data/spec/walkman/services/rdio_spec.rb +70 -0
- data/spec/walkman/song_spec.rb +33 -0
- data/walkman.gemspec +29 -0
- metadata +222 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module Walkman
|
4
|
+
def self.config
|
5
|
+
@@config ||= Config.new.tap do |config|
|
6
|
+
config.load_file("~/.walkman")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Config
|
11
|
+
# logger
|
12
|
+
attr_accessor :log_level
|
13
|
+
|
14
|
+
# cli/drb
|
15
|
+
attr_reader :server_host
|
16
|
+
attr_reader :server_port
|
17
|
+
attr_reader :drb_uri
|
18
|
+
|
19
|
+
# echonest
|
20
|
+
attr_reader :echonest_api_key
|
21
|
+
attr_reader :echonest_consumer_key
|
22
|
+
attr_reader :echonest_shared_secret
|
23
|
+
attr_reader :echonest_catalog_id
|
24
|
+
|
25
|
+
# rdio
|
26
|
+
attr_reader :rdio_player_url
|
27
|
+
attr_reader :rdio_playback_token
|
28
|
+
attr_reader :rdio_browser_path
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
# global
|
32
|
+
@log_level = "debug"
|
33
|
+
|
34
|
+
# server
|
35
|
+
@server_host = "localhost"
|
36
|
+
@server_port = 27001
|
37
|
+
@drb_uri = "druby://#{@server_host}:#{@server_port}"
|
38
|
+
|
39
|
+
# echo nest
|
40
|
+
@echonest_api_key = nil
|
41
|
+
@echonest_consumer_key = nil
|
42
|
+
@echonest_shared_secret = nil
|
43
|
+
@echonest_catalog_id = nil
|
44
|
+
|
45
|
+
# rdio service
|
46
|
+
@rdio_player_url = "http://localhost:4567/rdio"
|
47
|
+
@rdio_playback_token = nil
|
48
|
+
@rdio_browser_path = '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --no-process-singleton-dialog' # must be single quotes
|
49
|
+
end
|
50
|
+
|
51
|
+
def load_file(config_file)
|
52
|
+
file = File.expand_path(config_file)
|
53
|
+
|
54
|
+
if File.file?(file)
|
55
|
+
configs = YAML.load(File.read(file))
|
56
|
+
load_global_configs(configs)
|
57
|
+
load_server_configs(configs["server"])
|
58
|
+
load_echonest_configs(configs["echonest"])
|
59
|
+
load_rdio_configs(configs["rdio"])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
def load_global_configs(configs)
|
66
|
+
@log_level = configs["log_level"] if configs["log_level"]
|
67
|
+
end
|
68
|
+
|
69
|
+
def load_server_configs(configs)
|
70
|
+
return unless configs
|
71
|
+
|
72
|
+
@server_host = configs["host"] if configs["host"]
|
73
|
+
@server_port = configs["port"] if configs["port"]
|
74
|
+
@drb_uri = "druby://#{@server_host}:#{@server_port}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def load_echonest_configs(configs)
|
78
|
+
return unless configs
|
79
|
+
|
80
|
+
@echonest_api_key = configs["api_key"]
|
81
|
+
@echonest_consumer_key = configs["consumer_key"]
|
82
|
+
@echonest_shared_secret = configs["shared_secret"]
|
83
|
+
@echonest_catalog_id = configs["catalog_id"]
|
84
|
+
end
|
85
|
+
|
86
|
+
def load_rdio_configs(configs)
|
87
|
+
return unless configs
|
88
|
+
|
89
|
+
@rdio_playback_token = configs["playback_token"]
|
90
|
+
@rdio_player_url = configs["player_url"] if configs["player_url"]
|
91
|
+
@rdio_browser_path = configs["browser_path"] if configs["browser_path"]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Walkman
|
2
|
+
def self.player
|
3
|
+
@@player ||= Walkman::Player.new
|
4
|
+
end
|
5
|
+
|
6
|
+
class Player
|
7
|
+
attr_accessor :current_song, :playing
|
8
|
+
|
9
|
+
SERVICES = [Walkman::Services::Rdio]
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@current_song = nil
|
13
|
+
@playing = false
|
14
|
+
@running = false
|
15
|
+
end
|
16
|
+
|
17
|
+
def services
|
18
|
+
@services ||= begin
|
19
|
+
Hash[SERVICES.map { |service| [service.name, service.new] }]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def startup
|
24
|
+
services.each do |key, service|
|
25
|
+
service.startup
|
26
|
+
end
|
27
|
+
|
28
|
+
@running = true
|
29
|
+
|
30
|
+
@play_loop = Thread.new do
|
31
|
+
current_loop_song = nil
|
32
|
+
last_loop_song = nil
|
33
|
+
|
34
|
+
while @running
|
35
|
+
if @playing
|
36
|
+
current_loop_song = @current_song
|
37
|
+
|
38
|
+
if current_loop_song.nil?
|
39
|
+
self.next
|
40
|
+
elsif !last_loop_song.nil? && current_loop_song != last_loop_song
|
41
|
+
stop
|
42
|
+
current_loop_song = nil
|
43
|
+
@playing = true # have to reset this due to calling stop
|
44
|
+
elsif last_loop_song.nil?
|
45
|
+
play_song(current_loop_song)
|
46
|
+
end
|
47
|
+
|
48
|
+
last_loop_song = current_loop_song
|
49
|
+
else
|
50
|
+
last_loop_song = nil
|
51
|
+
end
|
52
|
+
|
53
|
+
sleep 0.1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def shutdown
|
59
|
+
@running = false
|
60
|
+
@play_loop.join if @play_loop
|
61
|
+
|
62
|
+
services.each do |key, service|
|
63
|
+
service.shutdown
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def play
|
68
|
+
Walkman.logger.debug("player play")
|
69
|
+
|
70
|
+
@playing = true
|
71
|
+
end
|
72
|
+
|
73
|
+
def stop
|
74
|
+
Walkman.logger.debug("player stop")
|
75
|
+
|
76
|
+
@playing = false
|
77
|
+
|
78
|
+
services.each do |key, service|
|
79
|
+
service.stop
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def next(count = 1)
|
84
|
+
if @current_song = playlist.next(count)
|
85
|
+
@playing = true
|
86
|
+
else
|
87
|
+
stop
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def playlist
|
92
|
+
@playlist ||= Walkman::Playlist.new
|
93
|
+
end
|
94
|
+
|
95
|
+
def playlist=(playlist)
|
96
|
+
@playlist = playlist
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def play_song(song)
|
102
|
+
services[song.source_type].play(song.source_id)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module Walkman
|
2
|
+
class Playlist
|
3
|
+
attr_accessor :session_id
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
songs = options.delete(:songs) || []
|
7
|
+
@queue = [songs].flatten # can add one or more Songs
|
8
|
+
@auto_queue = options.delete(:auto_queue) || false
|
9
|
+
|
10
|
+
if echonest_playlist = echonest_playlist_create(options)
|
11
|
+
@session_id = echonest_playlist.session_id
|
12
|
+
end
|
13
|
+
|
14
|
+
if @auto_queue && @session_id
|
15
|
+
auto_queue && self.next
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def queue
|
20
|
+
@queue
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear
|
24
|
+
@queue = []
|
25
|
+
end
|
26
|
+
|
27
|
+
def include?(song)
|
28
|
+
@queue.include?(song)
|
29
|
+
end
|
30
|
+
alias_method :queued?, :include?
|
31
|
+
|
32
|
+
def shuffle
|
33
|
+
@queue.shuffle!
|
34
|
+
end
|
35
|
+
|
36
|
+
def add(songs, position = -1)
|
37
|
+
index = @queue.size
|
38
|
+
index = [position, index].min if position >= 0
|
39
|
+
@queue.insert(index, songs).flatten!
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove(song)
|
43
|
+
@queue.delete_if { |s| s == song }
|
44
|
+
end
|
45
|
+
|
46
|
+
def next(count = 1)
|
47
|
+
# if the playlist is not empty, we can get one or more
|
48
|
+
# songs back so we need to make sure we get the last one
|
49
|
+
songs = @queue.shift(count)
|
50
|
+
songs = [songs].flatten
|
51
|
+
song = songs.pop # the last song skipped
|
52
|
+
|
53
|
+
# skip and unplay songs so our echonest catalog/profile stays true
|
54
|
+
skip(songs)
|
55
|
+
|
56
|
+
if @auto_queue && size <= 5
|
57
|
+
auto_queue(5) if @session_id
|
58
|
+
end
|
59
|
+
|
60
|
+
song
|
61
|
+
end
|
62
|
+
|
63
|
+
def size
|
64
|
+
@queue.size
|
65
|
+
end
|
66
|
+
|
67
|
+
def unplay(songs)
|
68
|
+
songs = [songs].flatten # one or more
|
69
|
+
|
70
|
+
songs.each do |song|
|
71
|
+
echonest_playlist_feedback({ unplay_song: song.echonest_song_id })
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def skip(songs)
|
76
|
+
songs = [songs].flatten # one or more
|
77
|
+
|
78
|
+
songs.each do |song|
|
79
|
+
echonest_playlist_feedback({ skip_song: song.echonest_song_id, unplay_song: song.echonest_song_id })
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def favorite(songs)
|
84
|
+
songs = [songs].flatten # one or more
|
85
|
+
|
86
|
+
songs.each do |song|
|
87
|
+
echonest_playlist_feedback({ favorite_song: song.echonest_song_id, favorite_artist: song.echonest_artist_id })
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def echonest_playlist_create(options = {})
|
94
|
+
return nil unless options.keys.include?(:type)
|
95
|
+
|
96
|
+
options[:bucket] = ["id:rdio-US", "tracks"]
|
97
|
+
options[:seed_catalog] = Walkman.config.echonest_catalog_id
|
98
|
+
options[:session_catalog] = Walkman.config.echonest_catalog_id
|
99
|
+
|
100
|
+
if remote_playlist = Walkman.echowrap.playlist_dynamic_create(options)
|
101
|
+
remote_playlist
|
102
|
+
else
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def echonest_playlist_feedback(args = {})
|
108
|
+
args[:session_id] = @session_id
|
109
|
+
|
110
|
+
Walkman.echowrap.playlist_dynamic_feedback(args)
|
111
|
+
end
|
112
|
+
|
113
|
+
def auto_queue(count = 5)
|
114
|
+
return 0 unless @session_id
|
115
|
+
|
116
|
+
result = Walkman.echowrap.playlist_dynamic_next(session_id: @session_id, results: count)
|
117
|
+
songs = []
|
118
|
+
|
119
|
+
result.songs.each do |song|
|
120
|
+
# find the first track with a rdio foreign key
|
121
|
+
track = song.tracks.find do |t|
|
122
|
+
t.foreign_id && t.foreign_id.split(":")[0] == "rdio-US"
|
123
|
+
end
|
124
|
+
|
125
|
+
next unless track
|
126
|
+
|
127
|
+
songs << Walkman::Song.new(artist: song.artist_name,
|
128
|
+
title: song.title,
|
129
|
+
source_type: "Walkman::Services::Rdio",
|
130
|
+
source_id: track.foreign_id.split(":").last,
|
131
|
+
echonest_artist_id: song.artist_id,
|
132
|
+
echonest_song_id: song.id)
|
133
|
+
end
|
134
|
+
|
135
|
+
add(songs)
|
136
|
+
songs.count
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Walkman
|
2
|
+
module Services
|
3
|
+
class Base
|
4
|
+
def startup
|
5
|
+
raise("Implement in Service")
|
6
|
+
end
|
7
|
+
|
8
|
+
def shutdown
|
9
|
+
raise("Implement in Service")
|
10
|
+
end
|
11
|
+
|
12
|
+
def restart
|
13
|
+
raise("Implement in Service")
|
14
|
+
end
|
15
|
+
|
16
|
+
def play(song)
|
17
|
+
raise("Implement in Service")
|
18
|
+
end
|
19
|
+
|
20
|
+
def stop
|
21
|
+
raise("Implement in Service")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Walkman
|
2
|
+
module Services
|
3
|
+
class Rdio < Walkman::Services::Base
|
4
|
+
def startup
|
5
|
+
Walkman.logger.debug "starting Rdio service"
|
6
|
+
|
7
|
+
@player_thread = Thread.new do
|
8
|
+
RdioPlayer.run!
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def shutdown
|
13
|
+
Walkman.logger.debug "stopping Rdio service"
|
14
|
+
|
15
|
+
@player_thread.terminate if @player_thread
|
16
|
+
end
|
17
|
+
|
18
|
+
def restart
|
19
|
+
shutdown
|
20
|
+
startup
|
21
|
+
end
|
22
|
+
|
23
|
+
def play(source_id)
|
24
|
+
quit_browser
|
25
|
+
launch_browser(source_id)
|
26
|
+
end
|
27
|
+
|
28
|
+
def stop
|
29
|
+
quit_browser
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def launch_browser(source_id)
|
35
|
+
data_dir = "/tmp/walkman/chrome/#{rand(999999999999)}"
|
36
|
+
launch_cmd = "#{Walkman.config.rdio_browser_path} \"#{Walkman.config.rdio_player_url}/#{source_id}\" --user-data-data-dir=#{data_dir}"
|
37
|
+
|
38
|
+
@browser_pid = Process.fork do
|
39
|
+
Signal.trap("TERM") { exit }
|
40
|
+
|
41
|
+
Command.run(launch_cmd)
|
42
|
+
end
|
43
|
+
|
44
|
+
unless @browser_pid.nil?
|
45
|
+
Walkman.logger.debug("detaching new browser process with pid #{@browser_pid}")
|
46
|
+
|
47
|
+
Process.detach(@browser_pid)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def quit_browser(source_id = "")
|
52
|
+
Walkman.logger.debug("killing browser process")
|
53
|
+
|
54
|
+
kill_cmd = "kill $(ps ax | grep \"#{Walkman.config.rdio_player_url}/#{source_id}\" | grep -v grep | sed -e 's/^[ \t]*//' | cut -d ' ' -f 1)"
|
55
|
+
Command.run(kill_cmd)
|
56
|
+
|
57
|
+
if @browser_pid
|
58
|
+
Walkman.logger.debug("killing browser pid #{@browser_pid}")
|
59
|
+
Process.kill("TERM", @browser_pid) rescue nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require "sinatra"
|
2
|
+
require "command"
|
3
|
+
|
4
|
+
class RdioPlayer < Sinatra::Base
|
5
|
+
get "/rdio/:song_id" do |song_id|
|
6
|
+
erb :player, locals: { playback_token: Walkman.config.rdio_playback_token, song_id: song_id }
|
7
|
+
end
|
8
|
+
|
9
|
+
get "/rdio/:song_id/done" do |song_id|
|
10
|
+
Walkman.player.current_song = nil # TODO: make this not suck
|
11
|
+
"Done"
|
12
|
+
end
|
13
|
+
|
14
|
+
template :player do
|
15
|
+
<<-EOS
|
16
|
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js"></script>
|
17
|
+
<script src="https://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js"></script>
|
18
|
+
<script type="text/javascript">
|
19
|
+
window.resizeTo(450, 450);
|
20
|
+
|
21
|
+
var playback_token = '<%= playback_token %>';
|
22
|
+
var domain = 'localhost'
|
23
|
+
var track_key = '<%= song_id %>';
|
24
|
+
|
25
|
+
var apiswf = null;
|
26
|
+
var playing = null;
|
27
|
+
var done = false;
|
28
|
+
|
29
|
+
$(document).ready(function() {
|
30
|
+
// on page load use SWFObject to load the API swf into div#apiswf
|
31
|
+
var flashvars = {
|
32
|
+
'playbackToken': playback_token, // from token.js
|
33
|
+
'domain': domain, // from token.js
|
34
|
+
'listener': 'callback_object' // the global name of the object that will receive callbacks from the SWF
|
35
|
+
};
|
36
|
+
var params = {
|
37
|
+
'allowScriptAccess': 'always'
|
38
|
+
};
|
39
|
+
var attributes = {};
|
40
|
+
swfobject.embedSWF('http://www.rdio.com/api/swf/', // the location of the Rdio Playback API SWF
|
41
|
+
'apiswf', // the ID of the element that will be replaced with the SWF
|
42
|
+
1, 1, '9.0.0', 'expressInstall.swf', flashvars, params, attributes);
|
43
|
+
});
|
44
|
+
|
45
|
+
// the global callback object
|
46
|
+
var callback_object = {};
|
47
|
+
callback_object.ready = function ready(user) {
|
48
|
+
apiswf = $('#apiswf').get(0);
|
49
|
+
apiswf.rdio_play(track_key);
|
50
|
+
}
|
51
|
+
|
52
|
+
callback_object.playStateChanged = function playStateChanged(playState) {
|
53
|
+
if(playState == 2 && playing != null) {
|
54
|
+
$.get('/rdio/' + track_key + '/done');
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
callback_object.playingTrackChanged = function playingTrackChanged(playingTrack, sourcePosition) {
|
59
|
+
if(playing == null) {
|
60
|
+
playing = playingTrack;
|
61
|
+
}
|
62
|
+
|
63
|
+
if (playingTrack != null) {
|
64
|
+
$('#track').text(playingTrack['name']);
|
65
|
+
$('#album').text(playingTrack['album']);
|
66
|
+
$('#artist').text(playingTrack['artist']);
|
67
|
+
$('#art').attr('src', playingTrack['icon']);
|
68
|
+
}
|
69
|
+
}
|
70
|
+
</script>
|
71
|
+
|
72
|
+
<div id="apiswf"></div>
|
73
|
+
<dl>
|
74
|
+
<dt>track</dt>
|
75
|
+
<dd id="track"></dd>
|
76
|
+
<dt>album</dt>
|
77
|
+
<dd id="album"></dd>
|
78
|
+
<dt>artist</dt>
|
79
|
+
<dd id="artist"></dd>
|
80
|
+
<dt>art</dt>
|
81
|
+
<dd><img src="" id="art"></dd>
|
82
|
+
</dl>
|
83
|
+
EOS
|
84
|
+
end
|
85
|
+
end
|