robut-rdio 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ ### 0.2 / 2010-12-02
2
+
3
+ #### Searching and Playing:
4
+
5
+ * Defaults to searching for tracks
6
+ * Ability to skip to next album
7
+ * Added the ability to search by artist
8
+ * Queuing songs from result set allows the specification of multiple songs
9
+
10
+ #### Server page:
11
+
12
+ * Shortcut keys to control playback
13
+ * Images to describe state of connection and playback
14
+
15
+ #### Notifications within Hipchat for when:
16
+
17
+ * song starts playing
18
+ * plugin has stopped playing
19
+ * Rdio is being used elsewhere by a user
20
+
21
+ #### Testing and Documentation
22
+
23
+ * Added cucumber tests for integration
24
+ * Added more unit tests
25
+ * Better usage and installation instructions
26
+
27
+ ### 0.1 / 2011-11-10
28
+
29
+ * Initial Release
data/Gemfile CHANGED
@@ -3,16 +3,25 @@ source "http://rubygems.org"
3
3
  # Example:
4
4
  # gem "activesupport", ">= 2.3.5"
5
5
 
6
- # Add dependencies to develop your gem here.
7
- # Include everything needed to run rake, tests, features, etc.
8
- gem 'robut'
6
+ gem 'robut', :git => 'git://github.com/substantial/robut.git',
7
+ :ref => '37de65af7c42ea691f3e76e8952a5821b8170be0'
8
+
9
+ gem 'rdio', '0.0.91' # .92 is horked
10
+ gem 'sinatra'
11
+ gem 'thin'
12
+
9
13
  group :development do
10
- gem 'thin'
11
- gem "rspec", "~> 2.3.0"
14
+ gem "rspec"
12
15
  gem "bundler", "~> 1.0.0"
13
16
  gem "jeweler", "~> 1.6.4"
14
17
  gem "rcov", ">= 0"
15
- gem 'rdio', '0.0.91' # .92 is horked
16
- gem 'sinatra'
17
18
  gem 'highline'
19
+ gem 'guard'
20
+ gem 'guard-rspec'
21
+ gem 'ruby-debug19', :require => 'ruby-debug'
22
+ end
23
+
24
+ group :test do
25
+ gem 'rack-test'
26
+
18
27
  end
data/Gemfile.lock CHANGED
@@ -1,41 +1,68 @@
1
+ GIT
2
+ remote: git://github.com/substantial/robut.git
3
+ revision: 37de65af7c42ea691f3e76e8952a5821b8170be0
4
+ ref: 37de65af7c42ea691f3e76e8952a5821b8170be0
5
+ specs:
6
+ robut (0.3.0)
7
+ xmpp4r (~> 0.5.0)
8
+
1
9
  GEM
2
10
  remote: http://rubygems.org/
3
11
  specs:
12
+ archive-tar-minitar (0.5.2)
13
+ columnize (0.3.5)
4
14
  daemons (1.1.4)
5
15
  diff-lcs (1.1.3)
6
16
  eventmachine (0.12.10)
7
17
  git (1.2.5)
8
- highline (1.6.5)
18
+ guard (0.8.8)
19
+ thor (~> 0.14.6)
20
+ guard-rspec (0.5.8)
21
+ guard (>= 0.8.4)
22
+ highline (1.6.8)
9
23
  jeweler (1.6.4)
10
24
  bundler (~> 1.0)
11
25
  git (>= 1.2.5)
12
26
  rake
27
+ linecache19 (0.5.12)
28
+ ruby_core_source (>= 0.1.4)
13
29
  oauth (0.4.5)
14
30
  rack (1.3.5)
15
31
  rack-protection (1.1.4)
16
32
  rack
33
+ rack-test (0.6.1)
34
+ rack (>= 1.0)
17
35
  rake (0.9.2.2)
18
36
  rcov (0.9.11)
19
37
  rdio (0.0.91)
20
38
  oauth (>= 0.3.0)
21
- robut (0.3.0)
22
- xmpp4r (~> 0.5.0)
23
- rspec (2.3.0)
24
- rspec-core (~> 2.3.0)
25
- rspec-expectations (~> 2.3.0)
26
- rspec-mocks (~> 2.3.0)
27
- rspec-core (2.3.1)
28
- rspec-expectations (2.3.0)
39
+ rspec (2.7.0)
40
+ rspec-core (~> 2.7.0)
41
+ rspec-expectations (~> 2.7.0)
42
+ rspec-mocks (~> 2.7.0)
43
+ rspec-core (2.7.1)
44
+ rspec-expectations (2.7.0)
29
45
  diff-lcs (~> 1.1.2)
30
- rspec-mocks (2.3.0)
46
+ rspec-mocks (2.7.0)
47
+ ruby-debug-base19 (0.11.25)
48
+ columnize (>= 0.3.1)
49
+ linecache19 (>= 0.5.11)
50
+ ruby_core_source (>= 0.1.4)
51
+ ruby-debug19 (0.11.6)
52
+ columnize (>= 0.3.1)
53
+ linecache19 (>= 0.5.11)
54
+ ruby-debug-base19 (>= 0.11.19)
55
+ ruby_core_source (0.1.5)
56
+ archive-tar-minitar (>= 0.5.2)
31
57
  sinatra (1.3.1)
32
58
  rack (~> 1.3, >= 1.3.4)
33
59
  rack-protection (~> 1.1, >= 1.1.2)
34
60
  tilt (~> 1.3, >= 1.3.3)
35
- thin (1.2.11)
61
+ thin (1.3.1)
36
62
  daemons (>= 1.0.9)
37
63
  eventmachine (>= 0.12.6)
38
64
  rack (>= 1.0.0)
65
+ thor (0.14.6)
39
66
  tilt (1.3.3)
40
67
  xmpp4r (0.5)
41
68
 
@@ -44,11 +71,15 @@ PLATFORMS
44
71
 
45
72
  DEPENDENCIES
46
73
  bundler (~> 1.0.0)
74
+ guard
75
+ guard-rspec
47
76
  highline
48
77
  jeweler (~> 1.6.4)
78
+ rack-test
49
79
  rcov
50
80
  rdio (= 0.0.91)
51
- robut
52
- rspec (~> 2.3.0)
81
+ robut!
82
+ rspec
83
+ ruby-debug19
53
84
  sinatra
54
85
  thin
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2, :cli => "--color --format d" do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ watch(%r{^spec/support/.+\.rb$}) { "spec" }
9
+ end
10
+
data/README.md CHANGED
@@ -1,12 +1,126 @@
1
- robut-rdio
2
- ====================
1
+ # robut-rdio
3
2
 
4
- A web-powered RDIO plugin for robut
3
+ Robut-rdio gives the ability for individuals within a [Hipchat](http://www.hipchat.com) chat room to enqueue, play, and manage songs through a web-based [rdio](http://www.rdio.com/) page. Robut-rdio is a plugin for [robut](https://github.com/justinweiss/robut).
5
4
 
6
- How to test Robut-Rdio interactively
7
- ------------------
5
+ ## Usage
8
6
 
9
- You can test the plugin in an interative shell with:
7
+ ### Controls
8
+
9
+ * `find <ARTIST, ALBUM, TRACK>` - searches for the given term and returns a result set
10
+ * `play <INDEX>` - play the track with the given index in the result set
11
+ * `pause` - pause the current playing song
12
+ * `unpause` or `play` - will resume playing the current track
13
+ * `next` will move to the next avaiable track
14
+ * `back` will move to the previous track
15
+ * `restart` will restart the current track over at the beginning
16
+ * `clear` will remove all the currently enqueued songs
17
+
18
+ #### `find` and `play`
19
+
20
+ As a user within a Hipchat channel, you will likely spend your time searching for and queuing music.
21
+
22
+ ```
23
+ @dj find <Track>
24
+ ```
25
+
26
+ A list of matching results will be returned and presented in the chat room.
27
+
28
+ ```
29
+ <Index>: <Artist> - <Album> - <Track>
30
+ ```
31
+
32
+ * You can enqueue one of the results by simply referencing the index number.
33
+ * Any user can make a selection from the results that are returned.
34
+ * Making new requests of robut will replace any previously specified indexes.
35
+
36
+
37
+ ##### Search By Album
38
+
39
+ ```
40
+ @dj find album <Album>
41
+ ```
42
+
43
+ ##### Search By Artist
44
+
45
+ ```
46
+ @dj find artist <Artist>
47
+ ```
48
+
49
+ ##### Example
50
+
51
+ ```
52
+ user > @dj find Beck - Guero
53
+
54
+ dj > Searching for: guero...
55
+ 0: Beck - Guero
56
+ 1: Beck - Guero - E-Pro
57
+ 2: Beck - Guero - Girl
58
+ 3: Beck - Guero - Que' Onda Guero
59
+ 4: Beck - Guero - Black Tambourine
60
+ 5: Beck - Guero - Missing
61
+ 6: Beck - Guero - Hell Yes
62
+ 7: Beck - Guero - Earthquake Weather
63
+ 8: Beck - Guero - Go It Alone
64
+ 9: Beck - Guero - Broken Drum
65
+
66
+ user > @dj play 0
67
+ ```
68
+
69
+ ### Web
70
+
71
+ Along with the bot within the hipchat channel to provide you with feedback information, the webpage playing the music also provides information about the state of the current track and playlist. Most of the images used are standard images to represent: play; paused; and stopped. There are two other states that are important to note:
72
+
73
+ ![Buffering](https://github.com/radamant/robut-rdio/raw/event-reporting/lib/server/public/images/buffering.png) This appears when a track is first loaded and the audio data is being loaded for playback.
74
+
75
+ ![Disconnected](https://github.com/radamant/robut-rdio/raw/event-reporting/lib/server/public/images/disconnected.png) This appears when the current Rdio account is being used in another location. This will prevent all playback.
76
+
77
+
78
+ ### Web Controls
79
+
80
+ The webpage itself allows for keyboard input. This allows for the system running the webpage to expose the ability to control the music similar to individuals within the chatroom. This has been tested with [Remote Buddy](http://www.iospirit.com/products/remotebuddy/) with custom keys mapped to the remote's buttons.
81
+
82
+ * `space` to toggle playing and pausing the current playing track
83
+ * `<-` or `p` to restart the current track or twice to move to the previous track
84
+ * `->` or `n` to move to the next track
85
+
86
+ ## Installation
87
+
88
+ ### Requirements
89
+
90
+ * [Robut](https://github.com/justinweiss/robut)
91
+ * [Rdio API](http://developer.rdio.com/) (Key & Secret)
92
+
93
+ ### Robut Chatfile
94
+
95
+ ```ruby
96
+ # Require the plugin into your chat file
97
+ require 'robut-rdio'
98
+
99
+ # Specify your RDIO_KEY and RDIO_SECRET
100
+ Robut::Plugin::Rdio.key = "RDIO_KEY"
101
+ Robut::Plugin::Rdio.secret = "RDIO_SECRET"
102
+
103
+ # Start the Sinatra Server required to stream the music from Rdio
104
+ Robut::Plugin::Rdio.start_server
105
+
106
+ # Add the plugin to the list of available plugins
107
+ Robut::Plugin.plugins << Robut::Plugin::Rdio
108
+
109
+ # ... other robut configuration information ...
110
+ ```
111
+
112
+
113
+ ## Development
114
+
115
+ ### Robut-Rdio without Hipchat
116
+
117
+ Robut-Rdio comes with an interactive shell mode that makes it wasy to interact with the Rdio service. This functionaliy allows you to interact with the plugin without the requirement of Hipchat.
118
+
119
+ ### Start the interactive shell
120
+
121
+ * Clone the repository
122
+
123
+ * Execute the following command:
10
124
 
11
125
  ```shell
12
126
  export RDIO_KEY=<your key>
@@ -14,9 +128,7 @@ export RDIO_SECRET=<your secret>
14
128
  rake shell
15
129
  ```
16
130
 
17
- This will open a pseudo-robut environment where anything entered into the shell will be responded to as if it were a hipchat message.
18
-
19
- You shoule see something like:
131
+ * Visit the server and port specified in start up:
20
132
 
21
133
  ```shell
22
134
  == Sinatra/1.3.1 has taken the stage on 4567 for development with backup from Thin
@@ -24,20 +136,13 @@ You shoule see something like:
24
136
  >> Maximum connections set to 1024
25
137
  >> Listening on 0.0.0.0:4567, CTRL+C to stop
26
138
  Welcome to the robut plugin test environment.
27
-
28
- You can direct your messages to the bot using:
29
- @dj
30
-
31
- Type 'exit' or 'quit' to exit this session
32
-
33
- hipchat>
34
-
35
139
  ```
36
140
 
37
- You can now point a web browser to the url that Rack gives you to run the client as well while you test the chat input.
141
+ By default the server will start at [localhost:4567](http://localhost:4567). It is important that the page is open to allow requests that are made to be added to the queue maintained by the Rdio object within the page.
38
142
 
143
+ * Make requests to the DJ within the interactive shell:
39
144
 
40
- Then you can do the following while your web browser responds to the changes:
145
+ You can make any requests that you would normally make within a Hipchat channel. The bot will read and respond to requests prefaced with the username `@dj`.
41
146
 
42
147
  ```shell
43
148
  hipchat> @dj find guero
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.2
data/lib/robut-rdio.rb CHANGED
@@ -45,6 +45,17 @@ class Robut::Plugin::Rdio
45
45
  Server.token = self.token || "GAlNi78J_____zlyYWs5ZG02N2pkaHlhcWsyOWJtYjkyN2xvY2FsaG9zdEbwl7EHvbylWSWFWYMZwfc="
46
46
  Server.domain = self.domain || "localhost"
47
47
  end
48
+
49
+ #
50
+ # Because an instance of this plugin is not created until the Robut client has
51
+ # recieved at least one message. The server callbacks need to be created during
52
+ # the #handle request. This allows for the server to communicate back through
53
+ # the Robut communication channel that it receives the messages.
54
+ #
55
+ def establish_server_callbacks!
56
+ Server.reply_callback ||= lambda{|message| reply(message, :room)}
57
+ Server.state_callback ||= lambda{|message| reply("/me #{message}", :room)}
58
+ end
48
59
 
49
60
  # Returns a description of how to use this plugin
50
61
  def usage
@@ -54,45 +65,158 @@ class Robut::Plugin::Rdio
54
65
  "#{at_nick} play track <track> - queues <track> for playing",
55
66
  "#{at_nick} play/unpause - unpauses the track that is currently playing",
56
67
  "#{at_nick} next - move to the next track",
68
+ "#{at_nick} next|skip album - skip all tracks in the current album group",
57
69
  "#{at_nick} restart - restart the current track"
58
70
  ]
59
71
  end
72
+
73
+
74
+ #
75
+ # @param [String,Array] request that is being evaluated as a play request
76
+ # @return [Boolean]
77
+ #
78
+ def play_results_regex
79
+ /^(?:play)?\s?(?:result)?\s?((?:\d[\s,-]*)+|all)$/
80
+ end
81
+
82
+ #
83
+ # @param [String,Array] request that is being evaluated as a play request
84
+ # @return [Boolean]
85
+ #
86
+ def play?(request)
87
+ Array(request).join(' ') =~ play_results_regex
88
+ end
89
+
90
+ #
91
+ # @param [Array,String] track_request the play request that is going to be
92
+ # parsed for available tracks.
93
+ #
94
+ # @return [Array] track numbers that were identified.
95
+ #
96
+ # @example Requesting multiple tracks
97
+ #
98
+ # "play 1"
99
+ # "play 1 2"
100
+ # "play 1,2"
101
+ # "play 1-3"
102
+ # "play 1, 2 4-6"
103
+ # "play all"
104
+ #
105
+ def parse_tracks_to_play(track_request)
106
+ if Array(track_request).join(' ') =~ /play all/
107
+ [ 'all' ]
108
+ else
109
+ Array(track_request).join(' ')[play_results_regex,-1].to_s.split(/(?:\s|,\s?)/).map do |track|
110
+ tracks = track.split("-")
111
+ (tracks.first.to_i..tracks.last.to_i).to_a
112
+ end.flatten
113
+ end
114
+ end
115
+
116
+ #
117
+ # @return [Regex] that is used to match searches for their parameters
118
+ # @see http://rubular.com/?regex=(find%7Cdo%20you%20have(%5Csany)%3F)%5Cs%3F(.%2B%5B%5E%3F%5D)%5C%3F%3F
119
+ #
120
+ def search_regex
121
+ /(find|do you have(\sany)?)\s?(.+[^?])\??/
122
+ end
123
+
124
+ #
125
+ # @param [String,Array] request that is being evaluated as a search request
126
+ # @return [Boolean]
127
+ #
128
+ def search?(request)
129
+ Array(request).join(' ') =~ search_regex
130
+ end
131
+
132
+ #
133
+ # @param [String,Array] request that is being evaluated as a search and playback
134
+ # request
135
+ # @return [Boolean]
136
+ #
137
+ def search_and_play?(request)
138
+ Array(request).join(' ') =~ /^play\b[^\b]+/
139
+ end
140
+
141
+ #
142
+ # @param [String,Array] request that is being evaluated as a command request
143
+ # @return [Boolean]
144
+ #
145
+ def command?(request)
146
+ Array(request).join(' ') =~ /^(?:play|(?:un)?pause|next|restart|back|clear)$/
147
+ end
60
148
 
149
+ #
150
+ # @param [String,Array] request that is being evaluated as a skip album request
151
+ # @return [Boolean]
152
+ #
153
+ def skip_album?(message)
154
+ message =~ /(next|skip) album/
155
+ end
156
+
61
157
  # Queues songs into the Rdio web player. @nick play search query
62
158
  # will queue the first search result matching 'search query' into
63
159
  # the web player. It can be an artist, album, or song.
64
160
  def handle(time, sender_nick, message)
65
- ::Rdio.init(self.class.key, self.class.secret)
161
+
162
+ establish_server_callbacks!
163
+
66
164
  words = words(message)
67
165
 
68
166
  if sent_to_me?(message)
69
- if words.join(' ') =~ /^(play)?\s?(result)?\s?\d/
70
- play_result(words.last.to_i)
71
- elsif words.first == 'play' and words.length > 1
72
- results = search(words)
73
- result = results.first
74
- if result
75
- queue(result)
76
- else
77
- reply("I couldn't find #{words.join(" ")} on Rdio.")
167
+
168
+ if play?(words)
169
+
170
+ play_result *parse_tracks_to_play(words)
171
+
172
+ elsif search_and_play?(words)
173
+
174
+ search_and_play_criteria = words[1..-1].join(" ")
175
+
176
+ unless search_and_play_result search_and_play_criteria
177
+ reply("I couldn't find '#{search_and_play_criteria}' on Rdio.")
78
178
  end
79
- elsif words.join(' ') =~ /(find|do you have(\sany)?)\s?(.+[^?])\??/
80
- find(['',Regexp.last_match[-1]])
81
- else words.first =~ /play|(?:un)?pause|next|restart|back|clear/
82
- run_command(words.first)
179
+
180
+ elsif search?(words)
181
+
182
+ find words.join(' ')[search_regex,-1]
183
+
184
+ elsif skip_album?(message)
185
+
186
+ run_command("next_album")
187
+
188
+ else command?(words)
189
+
190
+ run_command(words.join("_"))
191
+
83
192
  end
84
193
 
85
194
  end
195
+
86
196
  end
87
197
 
198
+ #
199
+ # As the plugin is initialized each time a request is made, the plugin maintains
200
+ # the state of the results from the last search request to ensure that it will
201
+ # be available when someone makes another request.
202
+ #
88
203
  def results
89
204
  @@results
90
205
  end
91
-
206
+
207
+
92
208
  private
93
209
  RESULT_DISPLAYER = {
94
- ::Rdio::Album => lambda{|album| "#{album.artist.name} - #{album.name}"},
95
- ::Rdio::Track => lambda{|track| "#{track.artist.name} - #{track.album.name} - #{track.name}"}
210
+ ::Rdio::Album => lambda{|album| "#{album.artist_name} - #{album.name}"},
211
+ ::Rdio::Track => lambda{|track| "#{track.artist_name} - #{track.album_name} - #{track.name}"},
212
+ ::Rdio::Artist => lambda do |artist|
213
+ tracks = artist.tracks(nil,0,1)
214
+ unless tracks.empty?
215
+ "#{artist.name} - #{tracks.first.album_name} - #{tracks.first.name}"
216
+ else
217
+ "#{artist.name}"
218
+ end
219
+ end
96
220
  }
97
221
 
98
222
  def run_command(command)
@@ -100,7 +224,7 @@ class Robut::Plugin::Rdio
100
224
  end
101
225
 
102
226
  def find(query)
103
- reply("Searching for: #{query[1..-1].join(' ')}...")
227
+ reply("Searching for: #{query}...")
104
228
  @@results = search(query)
105
229
 
106
230
  result_display = format_results(@@results)
@@ -115,16 +239,40 @@ class Robut::Plugin::Rdio
115
239
  result_display
116
240
  end
117
241
 
118
- def play_result(number)
242
+ def play_result(*requests)
243
+
119
244
  if !has_results?
120
245
  reply("I don't have any search results") and return
121
246
  end
122
-
123
- if !has_result?(number)
124
- reply("I don't have that result") and return
247
+
248
+ requests = requests.flatten.compact
249
+
250
+ # Queue all the songs when the request is 'all'
251
+
252
+ if requests.first == "all"
253
+ results.length.times {|index| queue result_at(index) }
254
+ return
125
255
  end
126
-
127
- queue result_at(number)
256
+
257
+ requests.flatten.each do |request|
258
+
259
+ if has_result?(request.to_i)
260
+ queue result_at(request.to_i)
261
+ else
262
+ reply("I don't have that result")
263
+ end
264
+
265
+ end
266
+
267
+ end
268
+
269
+ def search_and_play_result(message)
270
+
271
+ if result = Array(search(message)).first
272
+ queue(result)
273
+ true
274
+ end
275
+
128
276
  end
129
277
 
130
278
  def has_results?
@@ -153,21 +301,30 @@ class Robut::Plugin::Rdio
153
301
  "#{index}: #{response}"
154
302
  end
155
303
 
156
- # Searches Rdio for sources matching +words+. If the first word is
157
- # 'track', it only searches tracks, same for 'album'. Otherwise,
158
- # matches both albums and tracks.
304
+ # Searches Rdio for sources matching +words+.
305
+ #
306
+ # Given an array of Strings, which are the search terms, use Rdio to find any
307
+ # tracks that match. If the first word happens be `album` then search for
308
+ # albums that match the criteria.
309
+ #
310
+ #
159
311
  def search(words)
312
+ ::Rdio.init(self.class.key, self.class.secret)
160
313
  api = ::Rdio::Api.new(self.class.key, self.class.secret)
161
-
162
- if words[1] == "album"
163
- query_string = words[2..-1].join(' ')
314
+ words = words.split(' ')
315
+
316
+ if words.first == "album"
317
+ query_string = words[1..-1].join(' ')
164
318
  results = api.search(query_string, "Album")
165
- elsif words[1] == "track"
166
- query_string = words[2..-1].join(' ')
319
+ elsif words.first == "track"
320
+ query_string = words[1..-1].join(' ')
167
321
  results = api.search(query_string, "Track")
168
- else
322
+ elsif words.first == "artist"
169
323
  query_string = words[1..-1].join(' ')
170
- results = api.search(query_string, "Album,Track")
324
+ results = api.search(query_string, "Artist")
325
+ else
326
+ query_string = words.join(' ')
327
+ results = api.search(query_string, "Track")
171
328
  end
172
329
  end
173
330