audioscrobbler 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/audioscrobbler.gemspec +1 -1
- data/lib/audioscrobbler.rb +178 -104
- data/test/test_audioscrobbler.rb +142 -46
- data/test/test_queue.rb +30 -14
- metadata +29 -23
data/audioscrobbler.gemspec
CHANGED
data/lib/audioscrobbler.rb
CHANGED
@@ -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.
|
12
|
+
# 0.0.2
|
13
13
|
#
|
14
14
|
# == Author
|
15
15
|
# Daniel Erat <dan-ruby@erat.org>
|
16
16
|
#
|
17
17
|
# == Copyright
|
18
|
-
# Copyright
|
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.
|
40
|
-
# (http://www.audioscrobbler.net/
|
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
|
-
# #
|
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
|
70
|
-
|
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
|
-
@
|
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
|
-
@
|
107
|
-
@
|
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, :
|
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
|
141
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
178
|
+
arg_pairs = args.collect do |attr, value|
|
155
179
|
CGI.escape(attr.to_s) + '=' + CGI.escape(value.to_s)
|
156
|
-
|
180
|
+
end
|
181
|
+
url = @handshake_url + '?' + arg_pairs.join('&')
|
157
182
|
|
158
|
-
vlog("Beginning handshake with #@
|
183
|
+
vlog("Beginning handshake with #@handshake_url")
|
159
184
|
|
160
185
|
begin
|
161
|
-
data = Net::HTTP.get_response(URI.parse(
|
186
|
+
data = Net::HTTP.get_response(URI.parse(url)).body
|
162
187
|
rescue Exception
|
163
188
|
handle_handshake_failure(
|
164
|
-
"Read of #@
|
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 !=
|
184
|
-
handle_handshake_failure(
|
185
|
-
|
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
|
-
|
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
|
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
|
202
|
-
@
|
203
|
-
|
204
|
-
|
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 @
|
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
|
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
|
-
|
260
|
-
|
261
|
-
|
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 #@
|
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
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
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
|
-
|
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
|
-
|
293
|
-
|
294
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
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"
|
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
|
data/test/test_audioscrobbler.rb
CHANGED
@@ -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,
|
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
|
-
@
|
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.
|
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 = "
|
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}\
|
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,
|
144
|
+
def initialize(server, status, session_id)
|
132
145
|
@status = status
|
133
|
-
@
|
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\
|
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['
|
152
|
-
raise "
|
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 =~ /^([
|
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\
|
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}\
|
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", "
|
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
|
-
"
|
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.
|
235
|
-
a.verbose =
|
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
|
-
|
242
|
-
tracks.push(Track.new("Faith No More", "Midlife Crisis",
|
243
|
-
|
244
|
-
tracks.push(Track.new("Hans Zimmer", "Greed",
|
245
|
-
|
246
|
-
tracks.push(Track.new("M�m", "Sleepswim",
|
247
|
-
|
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
|
350
|
+
assert_equal(0, @handshake_status.failures)
|
259
351
|
assert(@handshake_status.successes > 0)
|
260
352
|
|
261
|
-
assert_equal(@submit_status.failures
|
353
|
+
assert_equal(0, @submit_status.failures)
|
262
354
|
assert(@submit_status.successes > 0)
|
263
355
|
|
264
|
-
assert_equal(@
|
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
|
data/test/test_queue.rb
CHANGED
@@ -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',
|
29
|
-
|
30
|
-
assert_equal(t.
|
31
|
-
assert_equal(t.
|
32
|
-
assert_equal(t.
|
33
|
-
assert_equal(t.
|
34
|
-
assert_equal(t.
|
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
|
73
|
-
assert_equal(tracks[0].artist
|
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
|
78
|
-
assert_equal(tracks[0].artist
|
79
|
-
assert_equal(tracks[1].artist
|
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.
|
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.
|
7
|
-
date:
|
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
|
-
|
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
|
-
|
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
|
-
|
30
|
+
- Daniel Erat
|
31
31
|
files:
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
55
|
+
|
56
|
+
dependencies: []
|
57
|
+
|