audioscrobbler 0.0.1 → 0.0.2

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 '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
+