audioscrobbler 0.1.0 → 0.1.1
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 +109 -62
- data/test/test_audioscrobbler.rb +15 -7
- metadata +2 -2
data/audioscrobbler.gemspec
CHANGED
data/lib/audioscrobbler.rb
CHANGED
@@ -22,20 +22,29 @@
|
|
22
22
|
# GNU GPL; see COPYING
|
23
23
|
#
|
24
24
|
# == Changes
|
25
|
-
# 0.0.1
|
26
|
-
#
|
25
|
+
# 0.0.1 20051002
|
26
|
+
# Initial release
|
27
|
+
# 0.0.2 20070826
|
28
|
+
# Upgraded from v1.1 to v1.2 of the Audioscrobbler protocol. This means:
|
27
29
|
# - "Now Playing" notifications
|
28
30
|
# - callers should now submit when tracks stop playing instead of as
|
29
31
|
# soon as the submission criteria is met
|
30
32
|
# - track numbers can be submitted
|
31
33
|
# Also added a race condition that I haven't bothered fixing (I
|
32
34
|
# think I'm the only person using this library?).
|
33
|
-
# 0.1.0
|
35
|
+
# 0.1.0 20071011
|
36
|
+
# Catch an exception when the server gives us a bogus now-playing
|
34
37
|
# URL, as happened to me yesterday. :-P
|
38
|
+
# 0.1.1 20071030
|
39
|
+
# Merge patches from Patrick Sinclair <Patrick.Sinclair@bbc.co.uk>
|
40
|
+
# adding HTTP proxy support and improving logging. Add validation
|
41
|
+
# of now-playing tracks' metadata, similar to what was already
|
42
|
+
# being done for submitted tracks.
|
35
43
|
|
36
44
|
require "cgi"
|
37
45
|
require "md5"
|
38
46
|
require "net/http"
|
47
|
+
require "logger"
|
39
48
|
require "thread"
|
40
49
|
require "uri"
|
41
50
|
|
@@ -110,12 +119,13 @@ class Audioscrobbler
|
|
110
119
|
# @param password Audioscrobbler account password
|
111
120
|
# @param filename file used for on-disk storage of not-yet-submitted
|
112
121
|
# tracks
|
122
|
+
# @param proxy optional HTTP proxy to use for outgoing connections
|
113
123
|
#
|
114
|
-
def initialize(username, password, filename=nil)
|
124
|
+
def initialize(username, password, filename=nil, proxy=nil)
|
115
125
|
@username = username
|
116
126
|
@password = password
|
117
127
|
@queue = SubmissionQueue.new(filename)
|
118
|
-
@
|
128
|
+
@logger = Logger.new($stdout)
|
119
129
|
@client = DEFAULT_CLIENT
|
120
130
|
@version = DEFAULT_VERSION
|
121
131
|
|
@@ -128,10 +138,57 @@ class Audioscrobbler
|
|
128
138
|
@submit_url = nil
|
129
139
|
@now_playing_url = nil
|
130
140
|
@submit_interval_sec = DEFAULT_SUBMIT_INTERVAL_SEC
|
141
|
+
|
142
|
+
@proxy = {}
|
143
|
+
if proxy
|
144
|
+
uri = URI.parse(proxy)
|
145
|
+
@proxy[:host], @proxy[:port] = uri.host, uri.port
|
146
|
+
if uri.userinfo
|
147
|
+
@proxy[:username], @proxy[:password] = uri.userinfo.split(':', 1)
|
148
|
+
end
|
149
|
+
end
|
131
150
|
end
|
132
|
-
attr_accessor :username, :password, :
|
151
|
+
attr_accessor :username, :password, :logger
|
133
152
|
attr_accessor :client, :version, :handshake_url
|
134
153
|
|
154
|
+
##
|
155
|
+
# Backwards-compatability methods to control the logging level.
|
156
|
+
def verbose=(v)
|
157
|
+
@logger.sev_threshold = v ? Logger::DEBUG : Logger::WARN
|
158
|
+
end
|
159
|
+
def verbose
|
160
|
+
@logger.sev_threshold == Logger::DEBUG
|
161
|
+
end
|
162
|
+
|
163
|
+
##
|
164
|
+
# Checks that a track's metadata meets the Audioscrobbler submission
|
165
|
+
# rules, in case the caller isn't already doing this.
|
166
|
+
#
|
167
|
+
# @param artist artist name
|
168
|
+
# @param title track name
|
169
|
+
# @param length track length
|
170
|
+
# @param start_time track start time, as UTC unix time
|
171
|
+
#
|
172
|
+
def valid_metadata?(artist, title, length, start_time)
|
173
|
+
if not length or length < 30
|
174
|
+
@logger.warn("Track \"#{artist} - #{title}\" is #{length} " \
|
175
|
+
"second(s) long (min is 30 seconds)")
|
176
|
+
return false
|
177
|
+
elsif not artist or artist == ''
|
178
|
+
@logger.warn("Track is missing artist tag (title is \"#{title}\")")
|
179
|
+
return false
|
180
|
+
elsif not title or title == ''
|
181
|
+
@logger.warn("Track is missing title tag (artist is \"#{artist}\")")
|
182
|
+
return false
|
183
|
+
elsif not start_time or start_time <= 0
|
184
|
+
@logger.warn("Track \"#{artist} - #{title}\" has bogus start time " \
|
185
|
+
"#{start_time}")
|
186
|
+
return false
|
187
|
+
end
|
188
|
+
true
|
189
|
+
end
|
190
|
+
private :valid_metadata?
|
191
|
+
|
135
192
|
##
|
136
193
|
# Update the backoff interval after handshake failure. If we haven't
|
137
194
|
# failed yet, we wait a minute; otherwise, we wait twice as long as last
|
@@ -140,7 +197,7 @@ class Audioscrobbler
|
|
140
197
|
# @param message string logged to stderr if verbose logging is enabled
|
141
198
|
#
|
142
199
|
def handle_handshake_failure(message)
|
143
|
-
|
200
|
+
@logger.debug(message)
|
144
201
|
if @handshake_backoff_sec < 60
|
145
202
|
@handshake_backoff_sec = 60
|
146
203
|
elsif @handshake_backoff_sec < 2 * 60 * 60
|
@@ -183,10 +240,15 @@ class Audioscrobbler
|
|
183
240
|
end
|
184
241
|
url = @handshake_url + '?' + arg_pairs.join('&')
|
185
242
|
|
186
|
-
|
243
|
+
@logger.debug("Beginning handshake with #@handshake_url")
|
187
244
|
|
188
245
|
begin
|
189
|
-
|
246
|
+
uri = URI.parse(url)
|
247
|
+
data = Net::HTTP.start(uri.host, uri.port,
|
248
|
+
@proxy[:host], @proxy[:port],
|
249
|
+
@proxy[:username], @proxy[:password]) do |http|
|
250
|
+
http.get(uri.request_uri).body
|
251
|
+
end
|
190
252
|
rescue Exception
|
191
253
|
handle_handshake_failure(
|
192
254
|
"Read of #@handshake_url for handshake failed: #{$!}")
|
@@ -222,8 +284,8 @@ class Audioscrobbler
|
|
222
284
|
# Create our response based on the server's challenge and
|
223
285
|
# save the submission URL.
|
224
286
|
@session_id, @now_playing_url, @submit_url = lines[1,3]
|
225
|
-
|
226
|
-
|
287
|
+
@logger.debug("Got session ID #@session_id, submission URL " \
|
288
|
+
"#@submit_url, and now-playing URL #@now_playing_url")
|
227
289
|
|
228
290
|
@handshake_backoff_sec = 0
|
229
291
|
@hard_failures = 0
|
@@ -246,7 +308,7 @@ class Audioscrobbler
|
|
246
308
|
# Might as well re-check in case more tracks have shown up
|
247
309
|
# during the handshake.
|
248
310
|
tracks = @queue.peek(MAX_TRACKS_IN_SUBMISSION)
|
249
|
-
|
311
|
+
@logger.debug("Submitting #{tracks.length} track(s)")
|
250
312
|
|
251
313
|
# Construct our argument list.
|
252
314
|
args = { "s" => @session_id }
|
@@ -269,36 +331,40 @@ class Audioscrobbler
|
|
269
331
|
begin
|
270
332
|
url = URI.parse(@submit_url)
|
271
333
|
headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
|
272
|
-
data = Net::HTTP.start(url.host, url.port
|
334
|
+
data = Net::HTTP.start(url.host, url.port,
|
335
|
+
@proxy[:host], @proxy[:port],
|
336
|
+
@proxy[:username], @proxy[:password]) do |http|
|
273
337
|
http.post(url.path, body, headers).body
|
274
338
|
end
|
275
339
|
rescue Exception
|
276
|
-
|
340
|
+
@logger.warn("Submission failed -- couldn't read " \
|
341
|
+
"#@submit_url: #{$!}")
|
277
342
|
else
|
278
343
|
# Check whether the submission was successful.
|
279
344
|
lines = data.split("\n")
|
280
345
|
if not lines[0]
|
281
|
-
|
346
|
+
@logger.warn("Submission failed -- got empty response")
|
282
347
|
elsif lines[0] == "OK"
|
283
|
-
|
348
|
+
@logger.debug("Submission was successful")
|
284
349
|
@queue.delete(tracks.length)
|
285
350
|
elsif lines[0] == "BADSESSION"
|
286
|
-
|
351
|
+
@logger.warn("Submission failed -- session is invalid")
|
287
352
|
# Unset the session ID so we'll re-handshake.
|
288
353
|
@session_id = nil
|
289
354
|
else
|
290
|
-
|
355
|
+
@logger.warn("Submission failed -- got unknown response " \
|
356
|
+
"\"#{lines[0]}\"")
|
291
357
|
@hard_failures += 1
|
292
358
|
end
|
293
359
|
end
|
294
360
|
|
295
361
|
if @hard_failures >= 3
|
296
|
-
|
362
|
+
@logger.warn("Got #@hard_failures failures; re-handshaking")
|
297
363
|
@session_id = nil
|
298
364
|
end
|
299
365
|
|
300
|
-
|
301
|
-
|
366
|
+
@logger.debug("Sleeping #@submit_interval_sec sec before checking " \
|
367
|
+
"for more tracks")
|
302
368
|
sleep(@submit_interval_sec)
|
303
369
|
end
|
304
370
|
end
|
@@ -317,7 +383,12 @@ class Audioscrobbler
|
|
317
383
|
#
|
318
384
|
def report_now_playing(artist, title, length, album="", mbid="",
|
319
385
|
track_num='')
|
320
|
-
|
386
|
+
@logger.debug("Reporting \"#{artist} - #{title}\" as now-playing")
|
387
|
+
|
388
|
+
if not valid_metadata?(artist, title, length, Time.now.to_i)
|
389
|
+
@logger.warn("Ignoring track with invalid metadata for now-playing")
|
390
|
+
return false
|
391
|
+
end
|
321
392
|
|
322
393
|
# FIXME(derat): Huge race condition here between us and the submission
|
323
394
|
# thread, but I am to lazy to fix it right now.
|
@@ -342,23 +413,26 @@ class Audioscrobbler
|
|
342
413
|
begin
|
343
414
|
url = URI.parse(@now_playing_url)
|
344
415
|
rescue Exception
|
345
|
-
|
346
|
-
|
416
|
+
@logger.warn("Submission failed -- couldn't parse now-playing " +
|
417
|
+
"URL \"#@now_playing_url\"")
|
347
418
|
else
|
348
419
|
headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
|
349
420
|
begin
|
350
|
-
data = Net::HTTP.start(url.host, url.port
|
421
|
+
data = Net::HTTP.start(url.host, url.port,
|
422
|
+
@proxy[:host], @proxy[:port],
|
423
|
+
@proxy[:username], @proxy[:password]) do |http|
|
351
424
|
http.post(url.path, body, headers).body
|
352
425
|
end
|
353
426
|
rescue Exception
|
354
|
-
|
427
|
+
@logger.warn("Submission failed -- couldn't read " \
|
428
|
+
"#@now_playing_url: #{$!}")
|
355
429
|
else
|
356
430
|
data.chomp!
|
357
431
|
if data == "OK"
|
358
|
-
|
432
|
+
@logger.debug("Now-playing report was successful")
|
359
433
|
success = true
|
360
434
|
else
|
361
|
-
|
435
|
+
@logger.warn("Now-playing report failed -- got \"#{data}\"")
|
362
436
|
end
|
363
437
|
end
|
364
438
|
end
|
@@ -367,6 +441,9 @@ class Audioscrobbler
|
|
367
441
|
|
368
442
|
##
|
369
443
|
# Enqueue a track for submission.
|
444
|
+
# Returns true if track was successfully queued and false otherwise
|
445
|
+
# (currently, it only fails if the track's metadata didn't meet the
|
446
|
+
# Audioscrobbler submission rules).
|
370
447
|
#
|
371
448
|
# @param artist artist name
|
372
449
|
# @param title track name
|
@@ -378,44 +455,14 @@ class Audioscrobbler
|
|
378
455
|
#
|
379
456
|
def enqueue(artist, title, length, start_time, album="", mbid="",
|
380
457
|
track_num=nil)
|
381
|
-
if not
|
382
|
-
|
383
|
-
|
384
|
-
return
|
385
|
-
elsif not artist or artist == ''
|
386
|
-
log("Ignoring #{title}, as it is missing an artist tag")
|
387
|
-
return
|
388
|
-
elsif not title or title == ''
|
389
|
-
log("Ignoring #{artist}, as it is missing a title tag")
|
390
|
-
return
|
391
|
-
elsif not start_time or start_time <= 0
|
392
|
-
log("Ignoring #{artist} - #{title} with bogus start time #{start_time}")
|
393
|
-
return
|
458
|
+
if not valid_metadata?(artist, title, length, start_time)
|
459
|
+
@logger.warn("Ignoring track with invalid metadata for submission")
|
460
|
+
return false
|
394
461
|
end
|
395
|
-
|
396
462
|
@queue.append(artist, title, length, start_time, album, mbid, track_num)
|
463
|
+
true
|
397
464
|
end
|
398
465
|
|
399
|
-
##
|
400
|
-
# Log a message, along with the current time, to stderr.
|
401
|
-
#
|
402
|
-
# @param message message to log
|
403
|
-
#
|
404
|
-
def log(message)
|
405
|
-
STDERR.puts(Time.now.strftime("%Y-%m-%d %H:%M:%S") + " " + message.to_s)
|
406
|
-
end
|
407
|
-
private :log
|
408
|
-
|
409
|
-
##
|
410
|
-
# Only log if verbose logging is enabled.
|
411
|
-
#
|
412
|
-
# @param message message to log
|
413
|
-
#
|
414
|
-
def vlog(message)
|
415
|
-
log(message) if @verbose
|
416
|
-
end
|
417
|
-
private :vlog
|
418
|
-
|
419
466
|
##
|
420
467
|
# A synchronized, backed-up-to-disk queue holding tracks for submission.
|
421
468
|
# The synchronization is only sufficient for a single reader and writer.
|
data/test/test_audioscrobbler.rb
CHANGED
@@ -312,8 +312,7 @@ class TestAudioscrobbler < Test::Unit::TestCase
|
|
312
312
|
a.client = "tst"
|
313
313
|
a.version = "1.1"
|
314
314
|
a.handshake_url = "http://127.0.0.1:#@http_port/handshake"
|
315
|
-
a.
|
316
|
-
|
315
|
+
a.logger.sev_threshold = Logger::UNKNOWN
|
317
316
|
a.start_submitter_thread
|
318
317
|
|
319
318
|
tracks = []
|
@@ -332,16 +331,25 @@ class TestAudioscrobbler < Test::Unit::TestCase
|
|
332
331
|
'Airdrawndagger', 'def', 7))
|
333
332
|
|
334
333
|
tracks.each do |t|
|
335
|
-
a.enqueue(t.artist, t.title, t.length, t.start_time, t.album,
|
336
|
-
|
334
|
+
assert(a.enqueue(t.artist, t.title, t.length, t.start_time, t.album,
|
335
|
+
t.mbid, t.track_num))
|
337
336
|
end
|
338
337
|
|
339
338
|
sleep 0.1 # avoid race condition :-/
|
340
339
|
now_playing.each do |t|
|
341
|
-
a.report_now_playing(t.artist, t.title, t.length, t.album, t.mbid,
|
342
|
-
|
340
|
+
assert(a.report_now_playing(t.artist, t.title, t.length, t.album, t.mbid,
|
341
|
+
t.track_num))
|
343
342
|
end
|
344
343
|
|
344
|
+
# This is lame, but since we have everything set up already, we might
|
345
|
+
# as well make sure that we're not able to submit tracks with broken
|
346
|
+
# metadata. The four things we check are missing artists, missing
|
347
|
+
# titles, tracks shorter than 30 seconds, and invalid start times.
|
348
|
+
assert(!a.report_now_playing('', 'Title', 60, Time.now.to_i))
|
349
|
+
assert(!a.report_now_playing('Artist', '', 60, Time.now.to_i))
|
350
|
+
assert(!a.enqueue('Artist', 'Title', 25, Time.now.to_i))
|
351
|
+
assert(!a.enqueue('Artist', 'Title', 60, 0))
|
352
|
+
|
345
353
|
# FIXME(derat): This is awful. I should add functionality to
|
346
354
|
# Audioscrobbler.enqueue to block until an attempt has been made to
|
347
355
|
# submit the just-enqueued track.
|
@@ -389,7 +397,7 @@ class TestAudioscrobbler < Test::Unit::TestCase
|
|
389
397
|
a.client = "tst"
|
390
398
|
a.version = "1.1"
|
391
399
|
a.handshake_url = "http://127.0.0.1:#@http_port/handshake"
|
392
|
-
a.
|
400
|
+
a.logger.sev_threshold = Logger::UNKNOWN
|
393
401
|
a.start_submitter_thread
|
394
402
|
|
395
403
|
assert(!a.report_now_playing('artist', 'title', 100, 'album', 'mbid', 1))
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.3
|
|
3
3
|
specification_version: 1
|
4
4
|
name: audioscrobbler
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.1.
|
7
|
-
date: 2007-10-
|
6
|
+
version: 0.1.1
|
7
|
+
date: 2007-10-30 00:00:00 -07:00
|
8
8
|
summary: Library to submit music playlists to Last.fm
|
9
9
|
require_paths:
|
10
10
|
- lib
|