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