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