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.
@@ -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