audioscrobbler 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  require 'rubygems'
2
2
  SPEC = Gem::Specification.new do |s|
3
3
  s.name = "audioscrobbler"
4
- s.version = "0.1.0"
4
+ s.version = "0.1.1"
5
5
  s.author = "Daniel Erat"
6
6
  s.email = "dan-ruby@erat.org"
7
7
  s.homepage = "http://www.erat.org/ruby/"
@@ -22,20 +22,29 @@
22
22
  # GNU GPL; see COPYING
23
23
  #
24
24
  # == Changes
25
- # 0.0.1 Initial release
26
- # 0.0.2 Upgraded from v1.1 to v1.2 of the Audioscrobbler protocol. This means:
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 Catch an exception when the server gives us a bogus now-playing
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
- @verbose = false
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, :verbose
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
- vlog(message)
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
- vlog("Beginning handshake with #@handshake_url")
243
+ @logger.debug("Beginning handshake with #@handshake_url")
187
244
 
188
245
  begin
189
- data = Net::HTTP.get_response(URI.parse(url)).body
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
- vlog("Got session ID #@session_id, submission URL " \
226
- "#@submit_url, and now-playing URL #@now_playing_url")
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
- vlog("Submitting #{tracks.length} track(s)")
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) do |http|
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
- vlog("Submission failed -- couldn't read #@submit_url: #{$!}")
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
- vlog("Submission failed -- got empty response")
346
+ @logger.warn("Submission failed -- got empty response")
282
347
  elsif lines[0] == "OK"
283
- vlog("Submission was successful")
348
+ @logger.debug("Submission was successful")
284
349
  @queue.delete(tracks.length)
285
350
  elsif lines[0] == "BADSESSION"
286
- vlog("Submission failed -- session is invalid")
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
- vlog("Submission failed -- got unknown response \"#{lines[0]}\"")
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
- vlog("Got #@hard_failures failures; re-handshaking")
362
+ @logger.warn("Got #@hard_failures failures; re-handshaking")
297
363
  @session_id = nil
298
364
  end
299
365
 
300
- vlog("Sleeping #@submit_interval_sec sec before checking for " \
301
- "more tracks")
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
- vlog("Reporting \"#{artist} - #{title}\" as now-playing")
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
- vlog("Submission failed -- couldn't parse now-playing " +
346
- "URL \"#@now_playing_url\"")
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) do |http|
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
- vlog("Submission failed -- couldn't read #@now_playing_url: #{$!}")
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
- vlog("Now-playing report was successful")
432
+ @logger.debug("Now-playing report was successful")
359
433
  success = true
360
434
  else
361
- vlog("Now-playing report failed -- got \"#{data}\"")
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 length or length < 30
382
- log("Ignoring #{artist} - #{title}, as it is #{length} second(s) " \
383
- "long (min is 30 seconds)")
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.
@@ -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.verbose = false
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, t.mbid,
336
- t.track_num)
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
- t.track_num)
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.verbose = false
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.0
7
- date: 2007-10-11 00:00:00 -07:00
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