audioscrobbler 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  require 'rubygems'
2
2
  SPEC = Gem::Specification.new do |s|
3
3
  s.name = "audioscrobbler"
4
- s.version = "0.0.1"
4
+ s.version = "0.0.2"
5
5
  s.author = "Daniel Erat"
6
6
  s.email = "dan-ruby@erat.org"
7
7
  s.homepage = "http://www.erat.org/ruby/"
@@ -9,19 +9,26 @@
9
9
  # description is located at http://www.audioscrobbler.net/wiki/Protocol1.1 .
10
10
  #
11
11
  # == Version
12
- # 0.0.1
12
+ # 0.0.2
13
13
  #
14
14
  # == Author
15
15
  # Daniel Erat <dan-ruby@erat.org>
16
16
  #
17
17
  # == Copyright
18
- # Copyright 2005 Daniel Erat
18
+ # Copyright 2007 Daniel Erat
19
19
  #
20
20
  # == License
21
21
  # GNU GPL; see COPYING
22
22
  #
23
23
  # == Changes
24
24
  # 0.0.1 Initial release
25
+ # 0.0.2 Upgraded from v1.1 to v1.2 of the Audioscrobbler protocol. This means:
26
+ # - "Now Playing" notifications
27
+ # - callers should now submit when tracks stop playing instead of as
28
+ # soon as the submission criteria is met
29
+ # - track numbers can be submitted
30
+ # Also added a race condition that I haven't bothered fixing (I
31
+ # think I'm the only person using this library?).
25
32
 
26
33
  require "cgi"
27
34
  require "md5"
@@ -36,8 +43,8 @@ require "uri"
36
43
  # Last.fm (http://www.last.fm) using the Audioscrobbler plugin protocol
37
44
  # (http://www.audioscrobbler.net/).
38
45
  #
39
- # Version 1.1 of the plugin protocol
40
- # (http://www.audioscrobbler.net/wiki/Protocol1.1) is currently used.
46
+ # Version 1.2 of the plugin protocol
47
+ # (http://www.audioscrobbler.net/development/protocol/) is currently used.
41
48
  #
42
49
  # == Usage
43
50
  # require "audioscrobbler"
@@ -55,7 +62,17 @@ require "uri"
55
62
  # # submission queue.
56
63
  # scrob.start_submitter_thread
57
64
  #
58
- # # Now wait until the Audioscrobbler submission criteria has been met.
65
+ # # Report the currently-playing song:
66
+ # scrob.report_now_playing("Beach Boys", # artist
67
+ # "God Only Knows", # title
68
+ # 175, # track length, in seconds
69
+ # "Pet Sounds", # album (optional)
70
+ # "", # MusicBrainzID (optional)
71
+ # "8", # track number (optional)
72
+ # )
73
+ #
74
+ # # Now wait until the Audioscrobbler submission criteria has been met and
75
+ # # the track has finished playing.
59
76
  #
60
77
  # # And then queue the track for submission:
61
78
  # scrob.enqueue("Beach Boys", # artist
@@ -64,13 +81,13 @@ require "uri"
64
81
  # 1125378558, # track start time
65
82
  # "Pet Sounds", # album (optional)
66
83
  # "", # MusicBrainzID (optional)
84
+ # "8", # track number (optional)
67
85
  # )
68
86
  class Audioscrobbler
69
- # Default URI to connect to for the handshake.
70
- DEFAULT_HANDSHAKE_URI = "http://post.audioscrobbler.com/"
87
+ # Default URL to connect to for the handshake.
88
+ DEFAULT_HANDSHAKE_URL = "http://post.audioscrobbler.com/"
71
89
 
72
90
  # Default minimum interval to wait between successful submissions.
73
- # This will be overriden if the server supplies an interval.
74
91
  DEFAULT_SUBMIT_INTERVAL_SEC = 5
75
92
 
76
93
  # Default plugin name and version to report to the server.
@@ -99,16 +116,18 @@ class Audioscrobbler
99
116
  @client = DEFAULT_CLIENT
100
117
  @version = DEFAULT_VERSION
101
118
 
102
- @handshake_uri = DEFAULT_HANDSHAKE_URI
119
+ @handshake_url = DEFAULT_HANDSHAKE_URL
103
120
  @last_handshake_time = nil
104
121
  @handshake_backoff_sec = 0
122
+ @hard_failures = 0
105
123
 
106
- @md5_resp = nil
107
- @submit_uri = nil
124
+ @session_id = nil
125
+ @submit_url = nil
126
+ @now_playing_url = nil
108
127
  @submit_interval_sec = DEFAULT_SUBMIT_INTERVAL_SEC
109
128
  end
110
129
  attr_accessor :username, :password, :verbose
111
- attr_accessor :client, :version, :handshake_uri
130
+ attr_accessor :client, :version, :handshake_url
112
131
 
113
132
  ##
114
133
  # Update the backoff interval after handshake failure. If we haven't
@@ -135,33 +154,39 @@ class Audioscrobbler
135
154
  # If the connection fails, @handshake_backoff_sec is also updated
136
155
  # appropriately. Wait this long before trying to handshake again.
137
156
  #
138
- def do_handshake
157
+ def do_handshake(can_sleep=true)
139
158
  # Sleep before trying again if needed.
140
- if @last_handshake_time != nil and \
141
- @last_handshake_time + @handshake_backoff_sec > Time.now
159
+ if can_sleep and
160
+ @last_handshake_time != nil and
161
+ @last_handshake_time + @handshake_backoff_sec > Time.now
142
162
  sleep(@last_handshake_time + @handshake_backoff_sec - Time.now)
143
163
  end
144
164
 
145
165
  @last_handshake_time = Time.now
166
+ timestamp = Time.now.to_i.to_s
167
+ auth_token = MD5.hexdigest(MD5.hexdigest(@password) + timestamp)
146
168
 
147
169
  args = {
148
- "hs" => "true",
149
- "p" => "1.1",
150
- "c" => @client,
151
- "v" => @version,
152
- "u" => @username,
170
+ 'hs' => 'true',
171
+ 'p' => '1.2',
172
+ 'c' => @client,
173
+ 'v' => @version,
174
+ 'u' => @username,
175
+ 't' => timestamp,
176
+ 'a' => auth_token,
153
177
  }
154
- uri = @handshake_uri + '?' + args.collect {|attr, value|
178
+ arg_pairs = args.collect do |attr, value|
155
179
  CGI.escape(attr.to_s) + '=' + CGI.escape(value.to_s)
156
- }.join('&')
180
+ end
181
+ url = @handshake_url + '?' + arg_pairs.join('&')
157
182
 
158
- vlog("Beginning handshake with #@handshake_uri")
183
+ vlog("Beginning handshake with #@handshake_url")
159
184
 
160
185
  begin
161
- data = Net::HTTP.get_response(URI.parse(uri)).body
186
+ data = Net::HTTP.get_response(URI.parse(url)).body
162
187
  rescue Exception
163
188
  handle_handshake_failure(
164
- "Read of #@handshake_uri for handshake failed: #{$!}")
189
+ "Read of #@handshake_url for handshake failed: #{$!}")
165
190
  return false
166
191
  end
167
192
 
@@ -172,47 +197,33 @@ class Audioscrobbler
172
197
  return false
173
198
  end
174
199
 
200
+ # The expected response is:
201
+ # OK
202
+ # Session ID
203
+ # Now-Playing URL
204
+ # Submission URL
175
205
  lines = data.split("\n")
176
-
177
- # Check whether the server says we can talk to it.
178
- # INTERVAL is supposed to be returned even on failure, but we don't
179
- # bother looking at it in this case because we already back off
180
- # extremely aggressively from handshake failures, and we'll end up
181
- # getting a new submission interval when we handshake successfully.
182
206
  response = lines[0].split[0]
183
- if response != "UPTODATE" and response != "UPDATE"
184
- handle_handshake_failure("Got \"#{lines[0]}\" from server during " \
185
- "handshake (expected UPTODATE or UPDATE)")
207
+ if response != 'OK'
208
+ handle_handshake_failure(
209
+ "Got \"#{lines[0]}\" from server during handshake (expected OK)")
186
210
  return false
187
211
  end
188
212
 
189
- # The expected response is something along the lines of:
190
- # UPTODATE or UPDATE <plugin uri>
191
- # <md5 challenge>
192
- # <uri to submit script>
193
- # INTERVAL <num>
194
- if lines.length < 3
213
+ if lines.length != 4
195
214
  handle_handshake_failure(
196
- "Got #{lines.length} during handshake (expected >=3)")
215
+ "Got #{lines.length} during handshake (expected 4)")
197
216
  return false
198
217
  end
199
218
 
200
219
  # Create our response based on the server's challenge and
201
- # save the submission URI.
202
- @md5_resp = MD5.hexdigest(MD5.hexdigest(@password) + lines[1])
203
- @submit_uri = lines[2]
204
- vlog("Using MD5 response #@md5_resp")
205
- vlog("Got submission URI #@submit_uri")
206
-
207
- # Get submission interval.
208
- if lines.length < 4 or lines[3].split.length != 2 or \
209
- lines[3].split[0] != "INTERVAL"
210
- vlog("Didn't get submission interval; using #@submit_interval_sec sec")
211
- else
212
- @submit_interval_sec = lines[3].split[0].to_i
213
- vlog("Got submission interval of #@submit_interval_sec sec")
214
- end
220
+ # save the submission URL.
221
+ @session_id, @now_playing_url, @submit_url = lines[1,3]
222
+ vlog("Got session ID #@session_id, submission URL " \
223
+ "#@submit_url, and now-playing URL #@now_playing_url")
215
224
 
225
+ @handshake_backoff_sec = 0
226
+ @hard_failures = 0
216
227
  return true
217
228
  end
218
229
  private :do_handshake
@@ -227,41 +238,39 @@ class Audioscrobbler
227
238
  tracks = @queue.peek(MAX_TRACKS_IN_SUBMISSION)
228
239
 
229
240
  # Keep trying to handshake until we're successful.
230
- do_handshake while not @md5_resp
241
+ do_handshake while not @session_id
231
242
 
232
243
  # Might as well re-check in case more tracks have shown up
233
- # during the handshaking.
244
+ # during the handshake.
234
245
  tracks = @queue.peek(MAX_TRACKS_IN_SUBMISSION)
235
246
  vlog("Submitting #{tracks.length} track(s)")
236
247
 
237
248
  # Construct our argument list.
238
- args = {
239
- "u" => @username,
240
- "s" => @md5_resp,
241
- }
249
+ args = { "s" => @session_id }
242
250
  for i in 0..tracks.length-1
243
251
  args.update({
244
252
  "a[#{i}]" => tracks[i].artist,
245
253
  "t[#{i}]" => tracks[i].title,
254
+ "i[#{i}]" => Time.at(tracks[i].start_time).to_i,
255
+ "o[#{i}]" => 'P',
256
+ "r[#{i}]" => '',
257
+ "l[#{i}]" => tracks[i].length.to_s,
246
258
  "b[#{i}]" => tracks[i].album,
259
+ "n[#{i}]" => tracks[i].track_num,
247
260
  "m[#{i}]" => tracks[i].mbid,
248
- "l[#{i}]" => tracks[i].length.to_s,
249
- "i[#{i}]" => Time.at(tracks[i].start_time).utc.
250
- strftime('%Y-%m-%d %H:%M:%S')
251
261
  })
252
262
  end
253
263
  # Convert it into a single escaped string for the body.
254
264
  body = args.collect {|k, v| "#{k}=" + CGI.escape(v.to_s) }.join('&')
255
265
 
256
- success = false
257
-
258
266
  begin
259
- uri = URI.parse(@submit_uri)
260
- data = Net::HTTP.start(uri.host, uri.port) do |http|
261
- http.post(uri.path, body).body
267
+ url = URI.parse(@submit_url)
268
+ headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
269
+ data = Net::HTTP.start(url.host, url.port) do |http|
270
+ http.post(url.path, body, headers).body
262
271
  end
263
272
  rescue Exception
264
- vlog("Submission failed -- couldn't read #@submit_uri: #{$!}")
273
+ vlog("Submission failed -- couldn't read #@submit_url: #{$!}")
265
274
  else
266
275
  # Check whether the submission was successful.
267
276
  lines = data.split("\n")
@@ -270,43 +279,83 @@ class Audioscrobbler
270
279
  elsif lines[0] == "OK"
271
280
  vlog("Submission was successful")
272
281
  @queue.delete(tracks.length)
273
- success = true
274
- elsif lines[0] == "BADAUTH"
275
- vlog("Submission failed -- authorization failed")
276
- # Unset the hash so we'll re-handshake.
277
- @md5_resp = nil
282
+ elsif lines[0] == "BADSESSION"
283
+ vlog("Submission failed -- session is invalid")
284
+ # Unset the session ID so we'll re-handshake.
285
+ @session_id = nil
278
286
  else
279
287
  vlog("Submission failed -- got unknown response \"#{lines[0]}\"")
280
- end
281
-
282
- # Use the server-supplied submission interval, or our default
283
- # if the server didn't supply one.
284
- if lines.length > 1 and lines[1].split[0] and \
285
- lines[1].split[0] == "INTERVAL" and lines[1].split[1]
286
- @submit_interval_sec = lines[1].split[1].to_i
287
- else
288
- @submit_interval_sec = DEFAULT_SUBMIT_INTERVAL_SEC
288
+ @hard_failures += 1
289
289
  end
290
290
  end
291
291
 
292
- # If we failed, do exponential backoff starting at 60 seconds.
293
- # Better safe than blocked!
294
- if not success
295
- if @submit_interval_sec < 60
296
- @submit_interval_sec = 60
297
- else
298
- @submit_interval_sec *= 2
299
- end
300
- vlog("Because of failure, backing off to #@submit_interval_sec sec")
292
+ if @hard_failures >= 3
293
+ vlog("Got #@hard_failures failures; re-handshaking")
294
+ @session_id = nil
301
295
  end
302
296
 
303
297
  vlog("Sleeping #@submit_interval_sec sec before checking for " \
304
- "more tracks")
298
+ "more tracks")
305
299
  sleep(@submit_interval_sec)
306
300
  end
307
301
  end
308
302
  end
309
303
 
304
+ ##
305
+ # Report the track that is currently playing.
306
+ # Returns true if the report was successful and false otherwise.
307
+ #
308
+ # @param artist artist name
309
+ # @param title track name
310
+ # @param length track length
311
+ # @param album album name
312
+ # @param mbid MusicBrainz ID
313
+ # @param track_num track number on album
314
+ #
315
+ def report_now_playing(artist, title, length, album="", mbid="",
316
+ track_num='')
317
+ vlog("Reporting \"#{artist} - #{title}\" as now-playing")
318
+
319
+ # FIXME(derat): Huge race condition here between us and the submission
320
+ # thread, but I am to lazy to fix it right now.
321
+ if not @session_id
322
+ do_handshake(false)
323
+ end
324
+
325
+ # Construct our argument list.
326
+ args = {
327
+ 's' => @session_id,
328
+ 'a' => artist,
329
+ 't' => title,
330
+ 'b' => album,
331
+ 'l' => length.to_i,
332
+ 'n' => track_num,
333
+ 'm' => mbid,
334
+ }
335
+ # Convert it into a single escaped string for the body.
336
+ body = args.collect {|k, v| "#{k}=" + CGI.escape(v.to_s) }.join('&')
337
+
338
+ success = false
339
+ url = URI.parse(@now_playing_url)
340
+ headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
341
+ begin
342
+ data = Net::HTTP.start(url.host, url.port) do |http|
343
+ http.post(url.path, body, headers).body
344
+ end
345
+ rescue Exception
346
+ vlog("Submission failed -- couldn't read #@now_playing_url: #{$!}")
347
+ else
348
+ data.chomp!
349
+ if data == "OK"
350
+ vlog("Now-playing report was successful")
351
+ success = true
352
+ else
353
+ vlog("Now-playing report failed -- got \"#{data}\"")
354
+ end
355
+ end
356
+ success
357
+ end
358
+
310
359
  ##
311
360
  # Enqueue a track for submission.
312
361
  #
@@ -316,9 +365,26 @@ class Audioscrobbler
316
365
  # @param start_time track start time, as UTC unix time
317
366
  # @param album album name
318
367
  # @param mbid MusicBrainz ID
368
+ # @param track_num track number on album
319
369
  #
320
- def enqueue(artist, title, length, start_time, album="", mbid="")
321
- @queue.append(artist, title, length, start_time, album, mbid)
370
+ def enqueue(artist, title, length, start_time, album="", mbid="",
371
+ track_num=nil)
372
+ if not length or length < 30
373
+ log("Ignoring #{artist} - #{title}, as it is #{length} second(s) " \
374
+ "long (min is 30 seconds)")
375
+ return
376
+ elsif not artist or artist == ''
377
+ log("Ignoring #{title}, as it is missing an artist tag")
378
+ return
379
+ elsif not title or title == ''
380
+ log("Ignoring #{artist}, as it is missing a title tag")
381
+ return
382
+ elsif not start_time or start_time <= 0
383
+ log("Ignoring #{artist} - #{title} with bogus start time #{start_time}")
384
+ return
385
+ end
386
+
387
+ @queue.append(artist, title, length, start_time, album, mbid, track_num)
322
388
  end
323
389
 
324
390
  ##
@@ -375,10 +441,13 @@ class Audioscrobbler
375
441
  # @param start_time track start time, as UTC unix time
376
442
  # @param album album name
377
443
  # @param mbid MusicBrainz ID
444
+ # @param track_num track number on album
378
445
  #
379
- def append(artist, title, length, start_time, album="", mbid="")
446
+ def append(artist, title, length, start_time, album="", mbid="",
447
+ track_num=nil)
380
448
  @mutex.synchronize do
381
- track = PlayedTrack.new(artist, title, length, start_time, album, mbid)
449
+ track = PlayedTrack.new(artist, title, length, start_time, album, mbid,
450
+ track_num)
382
451
  File.open(@filename, "a") {|f| f.puts(track.serialize) } if @filename
383
452
  @queue.push(track)
384
453
  @condvar.signal
@@ -433,24 +502,29 @@ class Audioscrobbler
433
502
  # @param start_time track start time, as UTC unix time
434
503
  # @param album album name
435
504
  # @param mbid MusicBrainz ID
505
+ # @param track_num track number on album
436
506
  #
437
- def initialize(artist, title, length, start_time, album="", mbid="")
507
+ def initialize(artist, title, length, start_time, album="", mbid="",
508
+ track_num=nil)
438
509
  @artist = artist.to_s
439
510
  @title = title.to_s
440
511
  @length = length.to_i
441
512
  @start_time = start_time.to_i
442
513
  @album = album ? album.to_s : ""
443
514
  @mbid = mbid ? mbid.to_s : ""
515
+ @track_num = track_num ? track_num.to_s : ""
444
516
  end
445
- attr_reader :artist, :title, :length, :start_time, :album, :mbid
517
+ attr_reader :artist, :title, :length, :start_time, :album, \
518
+ :mbid, :track_num
446
519
 
447
520
  ##
448
521
  # Convert this track's information into a form suitable for writing to
449
522
  # a text file. Returns a string.
450
523
  #
451
524
  def serialize
452
- [@artist, @title, @length.to_s, @start_time.to_s,
453
- @album, @mbid].collect {|x| x ? CGI.escape(x) : "" }.join("\t")
525
+ parts = [@artist, @title, @length.to_s, @start_time.to_s,
526
+ @album, @mbid, @track_num]
527
+ parts.collect {|x| x ? CGI.escape(x) : "" }.join("\t")
454
528
  end
455
529
 
456
530
  ##
@@ -460,7 +534,7 @@ class Audioscrobbler
460
534
  # @param str serialized PlayedTrack to deserialize (string)
461
535
  #
462
536
  def self.deserialize(str)
463
- PlayedTrack.new(*str.split("\t", 6).collect {|x|
537
+ PlayedTrack.new(*str.split("\t").collect {|x|
464
538
  x ? CGI.unescape(x) : "" })
465
539
  end
466
540
 
@@ -468,7 +542,7 @@ class Audioscrobbler
468
542
  other.class == PlayedTrack and @artist == other.artist and
469
543
  @title == other.title and @length == other.length and
470
544
  @start_time == other.start_time and @album == other.album and
471
- @mbid == other.mbid
545
+ @mbid == other.mbid and @track_num == other.track_num
472
546
  end
473
547
  end # class PlayedTrack
474
548
  end # class SubmissionQueue
@@ -17,6 +17,8 @@
17
17
  # == License
18
18
  # GNU GPL; see COPYING
19
19
 
20
+ $:.unshift File.join(File.dirname(__FILE__), "..", "lib")
21
+
20
22
  require 'audioscrobbler'
21
23
  require 'cgi'
22
24
  require 'md5'
@@ -44,15 +46,16 @@ end
44
46
 
45
47
  # Implements the handshake portion of the Audioscrobbler protocol.
46
48
  class AudioscrobblerHandshakeServlet < WEBrick::HTTPServlet::AbstractServlet
47
- def initialize(server, status, user, challenge, submit_url,
48
- plugin_name='tst', plugin_version=0.1)
49
+ def initialize(server, status, user, password, session_id, submit_url,
50
+ now_playing_url, plugin_name='tst', plugin_version=0.1)
49
51
  @status = status
50
52
  @user = user
51
- @challenge = challenge
53
+ @password = password
54
+ @session_id = session_id
52
55
  @submit_url = submit_url
56
+ @now_playing_url = now_playing_url
53
57
  @plugin_name = plugin_name
54
58
  @plugin_version = plugin_version
55
- @failure_interval = 1
56
59
  end
57
60
 
58
61
  def do_GET(req, res)
@@ -64,7 +67,7 @@ class AudioscrobblerHandshakeServlet < WEBrick::HTTPServlet::AbstractServlet
64
67
  begin
65
68
  if req.query['hs'] != 'true'
66
69
  throw "FAILED Handshake not requested"
67
- elsif req.query['p'] != '1.1'
70
+ elsif req.query['p'] != '1.2'
68
71
  throw "FAILED Wrong or missing protocol version"
69
72
  elsif req.query['c'] != @plugin_name
70
73
  throw "FAILED Wrong or missing plugin name"
@@ -72,15 +75,19 @@ class AudioscrobblerHandshakeServlet < WEBrick::HTTPServlet::AbstractServlet
72
75
  throw "FAILED Missing plugin version"
73
76
  elsif req.query['u'] != @user
74
77
  throw "BADUSER"
78
+ elsif not req.query['t']
79
+ throw "BADTIME Missing timestamp"
80
+ elsif req.query['a'] !=
81
+ MD5.hexdigest(MD5.hexdigest(@password) + req.query['t'])
82
+ throw "BADAUTH Invalid auth token"
75
83
  end
76
84
 
77
- # TODO(derat): Test UPDATE responses?
78
85
  @status.successes += 1
79
- res.body = "UPTODATE\n#@challenge\n#@submit_url\nINTERVAL 0"
86
+ res.body = "OK\n#@session_id\n#@now_playing_url\n#@submit_url\n"
80
87
 
81
88
  rescue RuntimeError
82
89
  @status.failures += 1
83
- res.body = "#{$!.message}\nINTERVAL #@failure_interval"
90
+ res.body = "#{$!.message}\n"
84
91
  end
85
92
  end # do_GET
86
93
  end # AudioscrobblerHandshakeServlet
@@ -89,21 +96,27 @@ end # AudioscrobblerHandshakeServlet
89
96
  # Stores a submitted track. Used by AudioscrobblerSubmitStatus.
90
97
  class Track
91
98
  def initialize(artist=nil, title=nil, length=nil, start_time=nil,
92
- album=nil, mbid=nil)
99
+ album=nil, mbid=nil, track_num=nil, source=nil,
100
+ rating=nil)
93
101
  @artist = artist
94
102
  @title = title
95
103
  @length = length
96
104
  @start_time = start_time
97
105
  @album = album
98
106
  @mbid = mbid
107
+ @track_num = track_num
108
+ @source = source
109
+ @rating = rating
99
110
  end
100
- attr_accessor :artist, :title, :length, :start_time, :album, :mbid
111
+ attr_accessor :artist, :title, :length, :start_time, :album, :mbid, \
112
+ :track_num, :source, :rating
101
113
 
102
114
  def ==(other)
103
115
  other.class == Track and @artist == other.artist and
104
116
  @title == other.title and @length == other.length and
105
117
  @start_time == other.start_time and @album == other.album and
106
- @mbid == other.mbid
118
+ @mbid == other.mbid and @track_num == other.track_num and
119
+ @source == other.source and @rating == other.rating
107
120
  end
108
121
  end
109
122
 
@@ -128,18 +141,16 @@ end
128
141
 
129
142
  # Implements the track submission portion of the Audioscrobbler protocol.
130
143
  class AudioscrobblerSubmitServlet < WEBrick::HTTPServlet::AbstractServlet
131
- def initialize(server, status, user, password, challenge)
144
+ def initialize(server, status, session_id)
132
145
  @status = status
133
- @user = user
134
- @md5 = MD5.hexdigest(MD5.hexdigest(password) + challenge)
135
- @failure_interval = 1
146
+ @session_id = session_id
136
147
  end
137
148
 
138
149
  # POST is supposed to be used rather than GET.
139
150
  def do_GET(req, res)
140
151
  @status.attempts += 1
141
152
  @status.failures += 1
142
- res.body = "FAILED Use POST, not GET\nINTERVAL #@failure_interval"
153
+ res.body = "FAILED Use POST, not GET\n"
143
154
  end
144
155
 
145
156
  def do_POST(req, res)
@@ -148,29 +159,26 @@ class AudioscrobblerSubmitServlet < WEBrick::HTTPServlet::AbstractServlet
148
159
 
149
160
  begin
150
161
  # Make sure that they authenticated correctly.
151
- if req.query['u'] != @user
152
- raise "BADAUTH"
153
- elsif req.query['s'] != @md5
154
- raise "BADAUTH"
162
+ if req.query['s'] != @session_id
163
+ raise "BADSESSION"
155
164
  end
156
165
 
157
166
  # Handle the track parameters.
158
167
  tracks = []
159
168
  req.query.each_pair do |k, v|
160
- if k =~ /^([atbmli])\[(\d+)\]$/
169
+ if k =~ /^([atiorlbnm])\[(\d+)\]$/
161
170
  track = (tracks[$2.to_i] ||= Track.new)
162
171
  v = CGI.unescape(v)
163
172
  case
164
173
  when $1 == 'a': track.artist = v
165
174
  when $1 == 't': track.title = v
175
+ when $1 == 'i': track.start_time = v.to_i
176
+ when $1 == 'o': track.source = v
177
+ when $1 == 'r': track.rating = v
178
+ when $1 == 'l': track.length = v.to_i
166
179
  when $1 == 'b': track.album = v
180
+ when $1 == 'n': track.track_num = v.to_i
167
181
  when $1 == 'm': track.mbid = v
168
- when $1 == 'l': track.length = v.to_i
169
- when $1 == 'i':
170
- # We get start times in a strange format, but it's more
171
- # convenient to store them as seconds since the epoch.
172
- v =~ /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/
173
- track.start_time = Time.gm($1, $2, $3, $4, $5, $6).to_i
174
182
  end
175
183
  end
176
184
  end
@@ -180,7 +188,8 @@ class AudioscrobblerSubmitServlet < WEBrick::HTTPServlet::AbstractServlet
180
188
  if not track
181
189
  raise "FAILED Missing track"
182
190
  elsif not track.artist or not track.title or not track.album or
183
- not track.mbid or not track.length or not track.start_time
191
+ not track.mbid or not track.length or not track.start_time or
192
+ not track.track_num or not track.source or not track.rating
184
193
  raise "FAILED Missing parameter"
185
194
  elsif track.artist.length == 0 or track.title.length == 0 or
186
195
  track.length == 0 or track.start_time == 0
@@ -198,32 +207,103 @@ class AudioscrobblerSubmitServlet < WEBrick::HTTPServlet::AbstractServlet
198
207
  # If we get here, then we didn't find any problems.
199
208
  @status.tracks += tracks
200
209
  @status.successes += 1
201
- res.body = "OK\nINTERVAL 0"
210
+ res.body = "OK\n"
202
211
 
203
212
  # If we threw an exception, return an error.
204
213
  rescue RuntimeError
205
214
  @status.failures += 1
206
- res.body = "#{$!.message}\nINTERVAL #@failure_interval"
215
+ res.body = "#{$!.message}\n"
207
216
  end
208
217
  end # do_POST
209
218
  end # AudioscrobblerSubmitServlet
210
219
 
211
220
 
221
+ # Implements the now-playing portion of the Audioscrobbler protocol.
222
+ class AudioscrobblerNowPlayingServlet < WEBrick::HTTPServlet::AbstractServlet
223
+ def initialize(server, status, session_id)
224
+ @status = status
225
+ @session_id = session_id
226
+ end
227
+
228
+ # POST is supposed to be used rather than GET.
229
+ def do_GET(req, res)
230
+ @status.attempts += 1
231
+ @status.failures += 1
232
+ res.body = "FAILED Use POST, not GET\n"
233
+ end
234
+
235
+ def do_POST(req, res)
236
+ @status.attempts += 1
237
+ res['Content-Type'] = "text/plain"
238
+
239
+ begin
240
+ # Make sure that they authenticated correctly.
241
+ if req.query['s'] != @session_id
242
+ raise "BADSESSION"
243
+ end
244
+
245
+ # Handle the track parameters.
246
+ track = Track.new
247
+ req.query.each_pair do |k, v|
248
+ if %w{a t b l n m}.member? k
249
+ v = CGI.unescape(v)
250
+ case
251
+ when k == 'a': track.artist = v
252
+ when k == 't': track.title = v
253
+ when k == 'b': track.album = v
254
+ when k == 'l': track.length = v.to_i
255
+ when k == 'n': track.track_num = v.to_i
256
+ when k == 'm': track.mbid = v
257
+ end
258
+ end
259
+ end
260
+
261
+ # Make sure that no data was missing from the submitted tracks.
262
+ if not track.artist or not track.title or not track.album or
263
+ not track.mbid or not track.length or not track.track_num
264
+ raise "FAILED Missing parameter"
265
+ elsif track.artist.length == 0 or track.title.length == 0 or
266
+ track.length == 0
267
+ raise "FAILED Empty required parameter"
268
+ end
269
+
270
+ # If we get here, then we didn't find any problems.
271
+ @status.tracks << track
272
+ @status.successes += 1
273
+ res.body = "OK\n"
274
+
275
+ # If we threw an exception, return an error.
276
+ rescue RuntimeError
277
+ @status.failures += 1
278
+ res.body = "#{$!.message}\n"
279
+ end
280
+ end # do_POST
281
+ end # AudioscrobblerNowPlayingServlet
282
+
283
+
212
284
  # Regression test for the Audioscrobbler class.
213
285
  class TestAudioscrobbler < Test::Unit::TestCase
214
286
  def test_audioscrobbler
215
287
  @handshake_status = AudioscrobblerHandshakeStatus.new
216
288
  @submit_status = AudioscrobblerSubmitStatus.new
289
+ @now_playing_status = AudioscrobblerSubmitStatus.new
217
290
  # FIXME(derat): Is there some better way to choose a port here?
218
291
  @http_port = 16349
219
292
 
220
293
  @server_thread = Thread.new do
221
294
  s = WEBrick::HTTPServer.new(:BindAddress => "127.0.0.1",
222
- :Port => @http_port)
295
+ :Port => @http_port,
296
+ :Logger => WEBrick::Log.new(
297
+ nil, WEBrick::BasicLog::WARN),
298
+ :AccessLog => [])
223
299
  s.mount("/handshake", AudioscrobblerHandshakeServlet, @handshake_status,
224
- "username", "challenge", "http://127.0.0.1:#@http_port/submit")
300
+ "username", "password", "sessionid",
301
+ "http://127.0.0.1:#@http_port/submit",
302
+ "http://127.0.0.1:#@http_port/nowplaying")
225
303
  s.mount("/submit", AudioscrobblerSubmitServlet, @submit_status,
226
- "username", "password", "challenge")
304
+ "sessionid")
305
+ s.mount("/nowplaying", AudioscrobblerNowPlayingServlet,
306
+ @now_playing_status, "sessionid")
227
307
  trap("INT") { s.shutdown }
228
308
  s.start
229
309
  end
@@ -231,23 +311,35 @@ class TestAudioscrobbler < Test::Unit::TestCase
231
311
  a = Audioscrobbler.new("username", "password")
232
312
  a.client = "tst"
233
313
  a.version = "1.1"
234
- a.handshake_uri = "http://127.0.0.1:#@http_port/handshake"
235
- a.verbose = true
314
+ a.handshake_url = "http://127.0.0.1:#@http_port/handshake"
315
+ a.verbose = false
236
316
 
237
317
  a.start_submitter_thread
238
318
 
239
319
  tracks = []
240
320
  tracks.push(Track.new("Beck", "Devil's Haircut", 100, 1128285297,
241
- "Odelay", ''))
242
- tracks.push(Track.new("Faith No More", "Midlife Crisis", 100, 1128285297,
243
- "Angel Dust", ''))
244
- tracks.push(Track.new("Hans Zimmer", "Greed", 100, 1128285297,
245
- "Broken Arrow", ''))
246
- tracks.push(Track.new("M�m", "Sleepswim", 100, 1128285297,
247
- "Finally We Are No One", ''))
321
+ "Odelay", 'abc', 1, 'P', ''))
322
+ tracks.push(Track.new("Faith No More", "Midlife Crisis", 101, 1128285298,
323
+ "Angel Dust", 'def', 2, 'P', ''))
324
+ tracks.push(Track.new("Hans Zimmer", "Greed", 102, 1128285299,
325
+ "Broken Arrow", 'ghi', 3, 'P', ''))
326
+ tracks.push(Track.new("M�m", "Sleepswim", 103, 1128285300,
327
+ "Finally We Are No One", 'jkl', 4, 'P', ''))
328
+ now_playing = []
329
+ now_playing.push(Track.new('Harold Budd', 'Sandtreader', 334, nil,
330
+ 'Lovely Thunder', 'abc', 2))
331
+ now_playing.push(Track.new('Sasha', 'Boileroom', 424, nil,
332
+ 'Airdrawndagger', 'def', 7))
248
333
 
249
334
  tracks.each do |t|
250
- a.enqueue(t.artist, t.title, t.length, t.start_time, t.album, t.mbid)
335
+ a.enqueue(t.artist, t.title, t.length, t.start_time, t.album, t.mbid,
336
+ t.track_num)
337
+ end
338
+
339
+ sleep 0.1 # avoid race condition :-/
340
+ now_playing.each do |t|
341
+ a.report_now_playing(t.artist, t.title, t.length, t.album, t.mbid,
342
+ t.track_num)
251
343
  end
252
344
 
253
345
  # FIXME(derat): This is awful. I should add functionality to
@@ -255,12 +347,16 @@ class TestAudioscrobbler < Test::Unit::TestCase
255
347
  # submit the just-enqueued track.
256
348
  sleep 3
257
349
 
258
- assert_equal(@handshake_status.failures, 0)
350
+ assert_equal(0, @handshake_status.failures)
259
351
  assert(@handshake_status.successes > 0)
260
352
 
261
- assert_equal(@submit_status.failures, 0)
353
+ assert_equal(0, @submit_status.failures)
262
354
  assert(@submit_status.successes > 0)
263
355
 
264
- assert_equal(@submit_status.tracks, tracks)
356
+ assert_equal(0, @now_playing_status.failures)
357
+ assert_equal(2, @now_playing_status.successes)
358
+
359
+ assert_equal(tracks, @submit_status.tracks)
360
+ assert_equal(now_playing, @now_playing_status.tracks)
265
361
  end # test_audioscrobbler
266
362
  end # TestAudioscrobbler
@@ -16,6 +16,8 @@
16
16
  # == License
17
17
  # GNU GPL; see COPYING
18
18
 
19
+ $:.unshift File.join(File.dirname(__FILE__), "..", "lib")
20
+
19
21
  require 'audioscrobbler'
20
22
  require 'tempfile'
21
23
  require 'test/unit'
@@ -25,23 +27,37 @@ class Audioscrobbler
25
27
  class TestPlayedTrack < Test::Unit::TestCase
26
28
  def test_members
27
29
  t = PlayedTrack.new(
28
- 'Cylob', 'Stomping FM', 100, 1128285297, 'Lobster Tracks', nil)
29
- assert_equal(t.artist, 'Cylob')
30
- assert_equal(t.title, 'Stomping FM')
31
- assert_equal(t.length, 100)
32
- assert_equal(t.start_time, 1128285297)
33
- assert_equal(t.album, 'Lobster Tracks')
34
- assert_equal(t.mbid, '') # nil should be converted to ''
30
+ 'Cylob', 'Stomping FM', 100, 1128285297, 'Lobster Tracks',
31
+ 'abc', 3)
32
+ assert_equal('Cylob', t.artist)
33
+ assert_equal('Stomping FM', t.title)
34
+ assert_equal(100, t.length)
35
+ assert_equal(1128285297, t.start_time)
36
+ assert_equal('Lobster Tracks', t.album)
37
+ assert_equal('abc', t.mbid)
38
+ assert_equal('3', t.track_num)
39
+ end
40
+
41
+ def test_empty_members
42
+ t = PlayedTrack.new(
43
+ 'Cylob', 'Stomping FM', 200, 1128285298, nil, nil, nil)
44
+ assert_equal('Cylob', t.artist)
45
+ assert_equal('Stomping FM', t.title)
46
+ assert_equal(200, t.length)
47
+ assert_equal(1128285298, t.start_time)
48
+ assert_equal('', t.album)
49
+ assert_equal('', t.mbid)
50
+ assert_equal('', t.track_num)
35
51
  end
36
52
 
37
53
  def test_serialization
38
54
  t = PlayedTrack.new(
39
- 'Katamari Damacy', 'WANDA WANDA', 100, 1128285297, 'OST', 'blah')
55
+ 'Katamari Damacy', 'WANDA WANDA', 100, 1128285297, 'OST', 'blah', 5)
40
56
  assert_equal(t,
41
57
  PlayedTrack.deserialize(t.serialize))
42
58
 
43
59
  t = PlayedTrack.new(
44
- 'Def Leppard', 'Photograph', 100, 1128285297, nil, 'blah')
60
+ 'Def Leppard', 'Photograph', 100, 1128285297, nil, 'blah', 6)
45
61
  assert_equal(t,
46
62
  PlayedTrack.deserialize(t.serialize))
47
63
  end
@@ -69,14 +85,14 @@ class Audioscrobbler
69
85
  q = SubmissionQueue.new(@filename)
70
86
 
71
87
  tracks = q.peek(1)
72
- assert_equal(tracks.length, 1)
73
- assert_equal(tracks[0].artist, 'Kraftwerk')
88
+ assert_equal(1, tracks.length)
89
+ assert_equal('Kraftwerk', tracks[0].artist)
74
90
 
75
91
  q.delete(1)
76
92
  tracks = q.peek(5)
77
- assert_equal(tracks.length, 2)
78
- assert_equal(tracks[0].artist, 'Iron Maiden')
79
- assert_equal(tracks[1].artist, 'Herbie Hancock')
93
+ assert_equal(2, tracks.length)
94
+ assert_equal('Iron Maiden', tracks[0].artist)
95
+ assert_equal('Herbie Hancock', tracks[1].artist)
80
96
  end
81
97
 
82
98
  def teardown
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.8.11
2
+ rubygems_version: 0.9.3
3
3
  specification_version: 1
4
4
  name: audioscrobbler
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.0.1
7
- date: 2005-10-02 00:00:00 -07:00
6
+ version: 0.0.2
7
+ date: 2007-08-26 00:00:00 -07:00
8
8
  summary: Library to submit music playlists to Last.fm
9
9
  require_paths:
10
- - lib
10
+ - lib
11
11
  email: dan-ruby@erat.org
12
12
  homepage: http://www.erat.org/ruby/
13
13
  rubyforge_project:
@@ -18,34 +18,40 @@ bindir: bin
18
18
  has_rdoc: true
19
19
  required_ruby_version: !ruby/object:Gem::Version::Requirement
20
20
  requirements:
21
- -
22
- - ">"
23
- - !ruby/object:Gem::Version
24
- version: 0.0.0
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
25
24
  version:
26
25
  platform: ruby
27
26
  signing_key:
28
27
  cert_chain:
28
+ post_install_message:
29
29
  authors:
30
- - Daniel Erat
30
+ - Daniel Erat
31
31
  files:
32
- - MANIFEST
33
- - COPYING
34
- - INSTALL
35
- - setup.rb
36
- - README
37
- - lib
38
- - test
39
- - audioscrobbler.gemspec
40
- - lib/audioscrobbler.rb
41
- - test/test_audioscrobbler.rb
42
- - test/test_queue.rb
32
+ - MANIFEST
33
+ - COPYING
34
+ - INSTALL
35
+ - setup.rb
36
+ - README
37
+ - lib
38
+ - test
39
+ - audioscrobbler.gemspec
40
+ - lib/audioscrobbler.rb
41
+ - test/test_audioscrobbler.rb
42
+ - test/test_queue.rb
43
43
  test_files:
44
- - test/test_audioscrobbler.rb
45
- - test/test_queue.rb
44
+ - test/test_audioscrobbler.rb
45
+ - test/test_queue.rb
46
46
  rdoc_options: []
47
+
47
48
  extra_rdoc_files: []
49
+
48
50
  executables: []
51
+
49
52
  extensions: []
53
+
50
54
  requirements: []
51
- dependencies: []
55
+
56
+ dependencies: []
57
+