turntabler 0.0.1 → 0.1.0

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