turntabler 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  require 'turntabler/resource'
2
2
 
3
3
  module Turntabler
4
- # Represents an instance where a user has been booted from a room
4
+ # Represents an event where a user has been booted from a room
5
5
  class Boot < Resource
6
6
  # The user that was booted from the room
7
7
  # @return [Turntabler::User]
@@ -24,8 +24,8 @@ module Turntabler
24
24
  attr_reader :id
25
25
 
26
26
  # Sets the current room the user is in
27
- # @param [Turntabler::Room] value The new room
28
27
  # @api private
28
+ # @param [Turntabler::Room] value The new room
29
29
  attr_writer :room
30
30
 
31
31
  # The directory for looking up / creating rooms
@@ -37,29 +37,31 @@ module Turntabler
37
37
  attr_reader :timeout
38
38
 
39
39
  # Creates a new client for communicating with Turntable.fm with the given
40
- # user id / auth token.
40
+ # email / password.
41
41
  #
42
- # @param [String] user_id The user to authenticate with
43
- # @param [String] auth The authentication token for the user
42
+ # @param [String] email The e-mail address of the user to authenticate with
43
+ # @param [String] password The Turntable password associated with the email address
44
44
  # @param [Hash] options The configuration options for the client
45
45
  # @option options [String] :id The unique identifier representing this client
46
46
  # @option options [String] :room The id of the room to initially enter
47
+ # @option options [String] :user_id The Turntable id for the authenticating user (required if the user does not have an associated password)
48
+ # @option options [String] :auth The authentication token for the user (required if the user does not have an associated password)
47
49
  # @option options [Fixnum] :timeout (10) The amount of seconds to allow to elapse for requests before timing out
48
50
  # @option options [Boolean] :reconnect (false) Whether to allow the client to automatically reconnect when disconnected either by Turntable or by the network
49
51
  # @option options [Fixnum] :reconnect_wait (5) The amount of seconds to wait before reconnecting
50
52
  # @raise [Turntabler::Error] if an invalid option is specified
51
53
  # @yield Runs the given block within the context if the client (for DSL-type usage)
52
- def initialize(user_id, auth, options = {}, &block)
54
+ def initialize(email, password, options = {}, &block)
53
55
  options = {
54
56
  :id => "#{Time.now.to_i}-#{rand}",
55
57
  :timeout => 10,
56
58
  :reconnect => false,
57
59
  :reconnect_wait => 5
58
60
  }.merge(options)
59
- assert_valid_keys(options, :id, :room, :url, :timeout, :reconnect, :reconnect_wait)
61
+ assert_valid_keys(options, :id, :room, :url, :user_id, :auth, :timeout, :reconnect, :reconnect_wait)
60
62
 
61
63
  @id = options[:id]
62
- @user = AuthorizedUser.new(self, :_id => user_id, :auth => auth)
64
+ @user = AuthorizedUser.new(self, :email => email, :password => password, :_id => options[:user_id], :userauth => options[:auth])
63
65
  @rooms = RoomDirectory.new(self)
64
66
  @event_handlers = {}
65
67
  @timeout = options[:timeout]
@@ -101,8 +103,8 @@ module Turntabler
101
103
  @connection.start
102
104
 
103
105
  # Wait until the connection is authenticated
104
- wait do |fiber|
105
- on(:session_missing, :once => true) { fiber.resume }
106
+ wait do |&resume|
107
+ on(:session_missing, :once => true) { resume.call }
106
108
  end
107
109
  end
108
110
 
@@ -118,8 +120,8 @@ module Turntabler
118
120
  @update_timer = nil
119
121
  @connection.close
120
122
 
121
- wait do |fiber|
122
- on(:session_ended, :once => true) { fiber.resume }
123
+ wait do |&resume|
124
+ on(:session_ended, :once => true) { resume.call }
123
125
  end
124
126
 
125
127
  on_session_ended(allow_reconnect)
@@ -149,8 +151,8 @@ module Turntabler
149
151
  message_id = @connection.publish(params.merge(:api => command))
150
152
 
151
153
  # Wait until we get a response for the given message
152
- data = wait do |fiber|
153
- on(:response_received, :once => true, :if => {'msgid' => message_id}) {|data| fiber.resume(data)}
154
+ data = wait do |&resume|
155
+ on(:response_received, :once => true, :if => {'msgid' => message_id}) {|data| resume.call(data)}
154
156
  end
155
157
 
156
158
  if data['success']
@@ -169,6 +171,16 @@ module Turntabler
169
171
  # @option options [Boolean] :once (false) Whether to only run the handler once
170
172
  # @return [true]
171
173
  #
174
+ # == Client Events
175
+ #
176
+ # * +:reconnected+ - The client reconnected (and re-entered any room that the user was previously in)
177
+ #
178
+ # @example
179
+ # client.on :reconnected do
180
+ # client.room.dj
181
+ # # ...
182
+ # end
183
+ #
172
184
  # == Room Events
173
185
  #
174
186
  # * +:room_updated+ - Information about the room was updated
@@ -334,6 +346,21 @@ module Turntabler
334
346
  user_id ? User.new(self, :_id => user_id) : @user
335
347
  end
336
348
 
349
+ # Gets the user with the given DJ name. This should only be used if the id
350
+ # of the user is unknown.
351
+ #
352
+ # @param [String] name The user's DJ name
353
+ # @return [Turntabler::User]
354
+ # @raise [Turntabler::Error] if the command fails
355
+ # @example
356
+ # client.user_by_name('DJSpinster') # => #<Turntabler::User id="a34bd..." ...>
357
+ def user_by_name(name)
358
+ data = api('user.get_id', :name => name)
359
+ user = self.user(data['userid'])
360
+ user.attributes = {'name' => name}
361
+ user
362
+ end
363
+
337
364
  # Get all avatars availble on Turntable.
338
365
  #
339
366
  # @return [Array<Turntabler::Avatar>]
@@ -389,17 +416,20 @@ module Turntabler
389
416
  api('file.search', :query => query, :page => options[:page])
390
417
 
391
418
  # Wait for the async callback
392
- songs = wait do |fiber|
393
- on(:search_completed, :once => true, :if => {'query' => query}) {|songs| fiber.resume(songs)}
394
- on(:search_failed, :once => true, :if => {'query' => query}) { fiber.resume }
419
+ songs = wait do |&resume|
420
+ on(:search_completed, :once => true, :if => {'query' => query}) {|songs| resume.call(songs)}
421
+ on(:search_failed, :once => true, :if => {'query' => query}) { resume.call }
395
422
  end
396
423
 
397
424
  songs || raise(Error, 'Search failed to complete')
398
425
  end
399
426
 
400
- private
401
427
  # Callback when a message has been received from Turntable. This will run
402
428
  # any handlers registered for the event associated with the message.
429
+ #
430
+ # @api private
431
+ # @param [Hash<String, Object>] data The message data received
432
+ # @return nil
403
433
  def on_message(data)
404
434
  if Event.command?(data['command'])
405
435
  event = Event.new(self, data)
@@ -411,6 +441,7 @@ module Turntabler
411
441
  end
412
442
  end
413
443
 
444
+ private
414
445
  # Callback when a heartbeat message has been received from Turntable determining
415
446
  # whether this client is still alive.
416
447
  def on_heartbeat
@@ -442,16 +473,29 @@ module Turntabler
442
473
  if @reconnect && allow_reconnect
443
474
  EM::Synchrony.add_timer(@reconnect_wait) do
444
475
  room ? room.enter : connect(url)
476
+ on_message('command' => 'reconnected')
445
477
  end
446
478
  end
447
479
  end
448
480
 
449
481
  # Pauses the current fiber until it is resumed with response data. This
450
482
  # can only get resumed explicitly by the provided block.
451
- def wait
483
+ def wait(&block)
452
484
  fiber = Fiber.current
453
- yield(fiber)
454
- Fiber.yield
485
+
486
+ # Resume the fiber when a response is received
487
+ allow_resume = true
488
+ block.call do |*args|
489
+ fiber.resume(*args) if allow_resume
490
+ end
491
+
492
+ # Attempt to pause the fiber until a response is received
493
+ begin
494
+ Fiber.yield
495
+ rescue FiberError => ex
496
+ allow_resume = false
497
+ raise Error, 'Turntabler APIs cannot be called from root fiber; use Turntabler.run { ... } instead'
498
+ end
455
499
  end
456
500
  end
457
501
  end
@@ -78,7 +78,7 @@ module Turntabler
78
78
  def publish(params)
79
79
  params[:msgid] = message_id = next_message_id
80
80
  params = @default_params.merge(params)
81
-
81
+
82
82
  logger.debug "Message sent: #{params.inspect}"
83
83
 
84
84
  if HTTP_APIS.include?(params[:api])
@@ -86,7 +86,7 @@ module Turntabler
86
86
  else
87
87
  publish_to_socket(params)
88
88
  end
89
-
89
+
90
90
  # Add timeout handler
91
91
  EventMachine.add_timer(@timeout) do
92
92
  dispatch('msgid' => message_id, 'command' => 'response_received', 'error' => 'timed out')
@@ -102,7 +102,7 @@ module Turntabler
102
102
  data = "~m~#{message.length}~m~#{message}"
103
103
  @socket.send(data)
104
104
  end
105
-
105
+
106
106
  # Publishes the given params to the HTTP API
107
107
  def publish_to_http(params)
108
108
  api = params.delete(:api)
@@ -124,7 +124,7 @@ module Turntabler
124
124
  event = Faye::WebSocket::API::Event.new('message', :data => "~m~#{Time.now.to_i}~m~#{JSON.generate(message)}")
125
125
  on_message(event)
126
126
  end
127
-
127
+
128
128
  # Runs the configured handler with the given message
129
129
  def dispatch(message)
130
130
  Turntabler.run { @handler.call(message) } if @handler
@@ -1,8 +1,10 @@
1
+ require 'digest/sha1'
2
+
1
3
  module Turntabler
2
4
  # Provides a set of helper functions for dealing with message digests
3
5
  # @api private
4
6
  module DigestHelpers
5
- # Generates a SHA1 hash from the given data
7
+ # Generates a SHA1 hash from the given data.
6
8
  #
7
9
  # @param [String] data The data to create a hash from
8
10
  # @return [String]
@@ -29,8 +29,8 @@ module Turntabler
29
29
  define_method("typecast_#{command}_event", &block)
30
30
  protected :"typecast_#{command}_event"
31
31
  end
32
-
33
- # Determines whether the given command is handled
32
+
33
+ # Determines whether the given command is handled.
34
34
  #
35
35
  # @param [String] command The command to check for the existence of
36
36
  # @return [Boolean] +true+ if the command exists, otherwise +false+
@@ -56,6 +56,9 @@ module Turntabler
56
56
  # The client's connection has closed
57
57
  handle :session_ended
58
58
 
59
+ # The client re-connected after previously being disconnected
60
+ handle :reconnected
61
+
59
62
  # A heartbeat was received from Turntable to ensure the client's connection
60
63
  # is still valid
61
64
  handle :heartbeat
@@ -119,7 +122,6 @@ module Turntabler
119
122
  new_djs = []
120
123
  data['user'].each_with_index do |attrs, index|
121
124
  new_djs << user = room.build_user(attrs.merge('placements' => data['placements'][index]))
122
- room.listeners << user
123
125
  room.djs << user
124
126
  end
125
127
  new_djs
@@ -129,7 +131,6 @@ module Turntabler
129
131
  handle :dj_removed, :rem_dj do
130
132
  data['user'].map do |attrs|
131
133
  user = room.build_user(attrs)
132
- room.listeners << user
133
134
  room.djs.delete(user)
134
135
  user
135
136
  end
@@ -138,7 +139,6 @@ module Turntabler
138
139
  # A new moderator was added to the room
139
140
  handle :moderator_added, :new_moderator do
140
141
  user = room.build_user(data)
141
- room.listeners << user
142
142
  room.moderators << user
143
143
  user
144
144
  end
@@ -152,26 +152,34 @@ module Turntabler
152
152
 
153
153
  # There are no more songs to play in the room
154
154
  handle :song_unavailable, :nosong do
155
+ client.on_message('command' => 'song_ended') if room.current_song
155
156
  room.attributes = data['room'].merge('current_song' => nil)
156
157
  nil
157
158
  end
158
159
 
159
160
  # A new song has started playing
160
161
  handle :song_started, :newsong do
162
+ client.on_message('command' => 'song_ended') if room.current_song
161
163
  room.attributes = data['room']
162
164
  room.current_song
163
165
  end
164
166
 
165
167
  # The current song has ended
166
- handle :song_ended, :endsong do
167
- room.attributes = data['room']
168
+ handle :song_ended do
168
169
  room.current_song
169
170
  end
170
171
 
171
172
  # A vote was cast for the song
172
173
  handle :song_voted, :update_votes do
174
+ song = room.current_song
175
+ initial_up_votes_count = song.up_votes_count
173
176
  room.attributes = data['room']
174
- room.current_song
177
+
178
+ # Update DJ point count
179
+ dj = room.current_dj
180
+ dj.attributes = {'points' => dj.points + song.up_votes_count - initial_up_votes_count}
181
+
182
+ song
175
183
  end
176
184
 
177
185
  # A user in the room has queued the current song onto their playlist
@@ -181,11 +189,13 @@ module Turntabler
181
189
 
182
190
  # A song was skipped due to a copyright claim
183
191
  handle :song_blocked do
192
+ client.on_message('command' => 'song_ended') if room.current_song
184
193
  Song.new(client, data)
185
194
  end
186
195
 
187
196
  # A song was skipped due to a limit on # of plays per hour
188
197
  handle :song_limited, :dmca_error do
198
+ client.on_message('command' => 'song_ended') if room.current_song
189
199
  Song.new(client, data)
190
200
  end
191
201
 
@@ -201,19 +211,19 @@ module Turntabler
201
211
 
202
212
  # A song search failed to complete
203
213
  handle :search_failed
204
-
214
+
205
215
  # The name of the event that was triggered
206
216
  # @return [String]
207
217
  attr_reader :name
208
-
218
+
209
219
  # The raw hash of data parsed from the event
210
- # @return [Hash]
220
+ # @return [Hash<String, Object>]
211
221
  attr_reader :data
212
-
222
+
213
223
  # The typecasted results parsed from the data
214
224
  # @return [Array]
215
225
  attr_reader :results
216
-
226
+
217
227
  # Creates a new event triggered with the given data
218
228
  #
219
229
  # @param [Turntabler::Client] client The client that this event is bound to
@@ -226,11 +236,11 @@ module Turntabler
226
236
  @results = __send__("typecast_#{command}_event")
227
237
  @results = [@results] unless @results.is_a?(Array)
228
238
  end
229
-
239
+
230
240
  private
231
241
  # The client that all APIs filter through
232
242
  attr_reader :client
233
-
243
+
234
244
  # Gets the current room the user is in
235
245
  def room
236
246
  client.room
@@ -1,23 +1,26 @@
1
+ require 'turntabler/assertions'
1
2
  require 'turntabler/event'
3
+ require 'turntabler/loggable'
2
4
 
3
5
  module Turntabler
4
6
  # Represents a callback that's been bound to a particular event
5
7
  # @api private
6
8
  class Handler
7
9
  include Assertions
10
+ include Loggable
8
11
 
9
12
  # The event this handler is bound to
10
13
  # @return [String]
11
14
  attr_reader :event
12
-
15
+
13
16
  # Whether to only call the handler once and then never again
14
17
  # @return [Boolean] +true+ if only called once, otherwise +false+
15
18
  attr_reader :once
16
-
19
+
17
20
  # The data that must be matched in order for the handler to run
18
21
  # @return [Hash<String, Object>]
19
22
  attr_reader :conditions
20
-
23
+
21
24
  # Builds a new handler bound to the given event.
22
25
  #
23
26
  # @param [String] event The name of the event to bind to
@@ -35,18 +38,22 @@ module Turntabler
35
38
  @conditions = options[:if]
36
39
  @block = block
37
40
  end
38
-
39
- # Runs this handler with results from the given event.
41
+
42
+ # Runs this handler for each result from the given event.
40
43
  #
41
- # @param [Array] event The event being triggered
42
- # @return [Boolean] +true+ if conditions were matcher to run the handler, otherwise +false+
44
+ # @param [Turntabler::Event] event The event being triggered
45
+ # @return [Boolean] +true+ if conditions were matched to run the handler, otherwise +false+
43
46
  def run(event)
44
47
  if conditions_match?(event.data)
45
48
  # Run the block for each individual result
46
49
  event.results.each do |result|
47
- @block.call(*[result].compact)
50
+ begin
51
+ @block.call(*[result].compact)
52
+ rescue StandardError => ex
53
+ logger.error(([ex.message] + ex.backtrace) * "\n")
54
+ end
48
55
  end
49
-
56
+
50
57
  true
51
58
  else
52
59
  false
@@ -4,6 +4,8 @@ module Turntabler
4
4
  module Loggable
5
5
  private
6
6
  # Delegates access to the logger to Turntabler.logger
7
+ #
8
+ # @return [Logger] The logger configured for this library
7
9
  def logger
8
10
  Turntabler.logger
9
11
  end
@@ -29,7 +29,7 @@ module Turntabler
29
29
  self.attributes = data
30
30
  super()
31
31
  end
32
-
32
+
33
33
  # Gets the song with the given id.
34
34
  #
35
35
  # @param [String] song_id The id for the song
@@ -6,47 +6,48 @@ module Turntabler
6
6
  # Send e-mails if a fan starts DJing
7
7
  # @return [Boolean]
8
8
  attribute :notify_dj
9
-
9
+
10
10
  # Send e-mails when someone becomes a fan
11
11
  # @return [Boolean]
12
12
  attribute :notify_fan
13
-
13
+
14
14
  # Sends infrequent e-mails about news
15
15
  # @return [Boolean]
16
16
  attribute :notify_news
17
-
17
+
18
18
  # Sends e-mails at random times with a different subsection of the digits of pi
19
19
  # @return [Boolean]
20
20
  attribute :notify_random
21
-
22
- # Publishes to facebook songs awesomed in public rooms
21
+
22
+ # Publishes to facebook songs voted up in public rooms
23
23
  # @return [Boolean]
24
24
  attribute :facebook_awesome
25
-
25
+
26
26
  # Publishes to facebook when a public room is joined
27
27
  # @return [Boolean]
28
28
  attribute :facebook_join
29
-
29
+
30
30
  # Publishes to facebook when DJing in a public room
31
31
  # @return [Boolean]
32
32
  attribute :facebook_dj
33
-
33
+
34
34
  # Loads the user's current Turntable preferences.
35
35
  #
36
36
  # @return [true]
37
37
  # @raise [Turntabler::Error] if the command fails
38
38
  # @example
39
- # preferences.load # => true
39
+ # preferences.load # => true
40
+ # preferences.notify_dj # => false
40
41
  def load
41
42
  data = api('user.get_prefs')
42
- self.attributes = data['result'].inject({}) do |result, (preference, value, id, description)|
43
+ self.attributes = data['result'].inject({}) do |result, (preference, value, *)|
43
44
  result[preference] = value
44
45
  result
45
46
  end
46
47
  super
47
48
  end
48
49
 
49
- # Updates the preferences.
50
+ # Updates the user's preferences.
50
51
  #
51
52
  # @param [Hash] attributes The attributes to update
52
53
  # @option attributes [Boolean] :notify_dj
@@ -1,5 +1,4 @@
1
1
  require 'pp'
2
- require 'digest/sha1'
3
2
  require 'turntabler/assertions'
4
3
  require 'turntabler/digest_helpers'
5
4
 
@@ -14,18 +13,18 @@ module Turntabler
14
13
 
15
14
  class << self
16
15
  include Assertions
17
-
16
+
18
17
  # Defines a new Turntable attribute on this class. By default, the name
19
18
  # of the attribute is assumed to be the same name that Turntable specifies
20
19
  # in its API. If the names are different, this can be overridden on a
21
20
  # per-attribute basis.
22
21
  #
23
- # Configuration options:
24
- # * +:load+ - Whether the resource should be loaded remotely from
25
- # Turntable in order to access the attribute. Default is true.
26
- #
27
- # == Examples
28
- #
22
+ # @api private
23
+ # @param [String] name The public name for the attribute
24
+ # @param [Hash] options The configuration options
25
+ # @option options [Boolean] :load (true) Whether the resource should be loaded remotely from Turntable in order to access the attribute
26
+ # @raise [ArgumentError] if an invalid option is specified
27
+ # @example
29
28
  # # Define a "name" attribute that maps to a Turntable "name" attribute
30
29
  # attribute :name
31
30
  #
@@ -51,7 +50,6 @@ module Turntabler
51
50
  # # when accessed
52
51
  # attribute :friends, :load => false
53
52
  #
54
- # @api private
55
53
  # @!macro [attach] attribute
56
54
  # @!attribute [r] $1
57
55
  def attribute(name, *turntable_names, &block)
@@ -64,12 +62,12 @@ module Turntabler
64
62
  load if instance_variable_get("@#{name}").nil? && !loaded? && options[:load]
65
63
  instance_variable_get("@#{name}")
66
64
  end
67
-
65
+
68
66
  # Query
69
67
  define_method("#{name}?") do
70
68
  !!__send__(name)
71
69
  end
72
-
70
+
73
71
  # Typecasting
74
72
  block ||= lambda {|value| value}
75
73
  define_method("typecast_#{name}", &block)
@@ -123,6 +121,7 @@ module Turntabler
123
121
  # "metadata" properties as additional attributes.
124
122
  #
125
123
  # @api private
124
+ # @param [Hash] attributes The updated attributes for the resource
126
125
  def attributes=(attributes)
127
126
  if attributes
128
127
  attributes.each do |attribute, value|
@@ -135,19 +134,21 @@ module Turntabler
135
134
  end
136
135
  end
137
136
  end
138
-
137
+
139
138
  # Forces this object to use PP's implementation of inspection.
140
139
  #
141
140
  # @api private
141
+ # @return [String]
142
142
  def pretty_print(q)
143
143
  q.pp_object(self)
144
144
  end
145
145
  alias inspect pretty_print_inspect
146
146
 
147
147
  # Defines the instance variables that should be printed when inspecting this
148
- # object. This ignores the +client+ and +loaded+ attributes.
148
+ # object. This ignores the +@client+ and +@loaded+ variables.
149
149
  #
150
150
  # @api private
151
+ # @return [Array<Symbol>]
151
152
  def pretty_print_instance_variables
152
153
  (instance_variables - [:'@client', :'@loaded']).sort
153
154
  end
@@ -155,6 +156,7 @@ module Turntabler
155
156
  # Determines whether this resource is equal to another based on their
156
157
  # unique identifiers.
157
158
  #
159
+ # @param [Object] other The object this resource is being compared against
158
160
  # @return [Boolean] +true+ if the resource ids are equal, otherwise +false+
159
161
  def ==(other)
160
162
  if other && other.respond_to?(:id) && other.id