Class: Audioscrobbler

Inherits:
Object
  • Object
show all
Defined in:
lib/audioscrobbler.rb

Overview

Audioscrobbler

Description

Queue music tracks as they are played and submit the track information to Last.fm (www.last.fm) using the Audioscrobbler plugin protocol (www.audioscrobbler.net/).

Version 1.2 of the plugin protocol (www.audioscrobbler.net/development/protocol/) is currently used.

Usage

require "audioscrobbler"
scrob = Audioscrobbler.new("user",       # Audioscrobbler username
                           "pass",       # Audioscrobbler password
                           "queue.txt",  # file for storing queue
                          )

# Replace these with the client ID that's been assigned to your
# plugin by the Audioscrobbler folks and your plugin's version number.
scrob.client = "tst"
scrob.version = "1.0"

# If you don't start the submitter, tracks will just pile up in the
# submission queue.
scrob.start_submitter_thread

# Report the currently-playing song:
scrob.report_now_playing("Beach Boys",      # artist
                         "God Only Knows",  # title
                         175,               # track length, in seconds
                         "Pet Sounds",      # album (optional)
                         "",                # MusicBrainzID (optional)
                         "8",               # track number (optional)
                        )

# Now wait until the Audioscrobbler submission criteria has been met and
# the track has finished playing.

# And then queue the track for submission:
scrob.enqueue("Beach Boys",      # artist
              "God Only Knows",  # title
              175,               # track length, in seconds
              1125378558,        # track start time
              "Pet Sounds",      # album (optional)
              "",                # MusicBrainzID (optional)
              "8",               # track number (optional)
             )

Defined Under Namespace

Classes: SubmissionQueue

Constant Summary collapse

DEFAULT_HANDSHAKE_URL =

Default URL to connect to for the handshake.

"http://post.audioscrobbler.com/"
DEFAULT_SUBMIT_INTERVAL_SEC =

Default minimum interval to wait between successful submissions.

5
DEFAULT_CLIENT =

Default plugin name and version to report to the server. You MUST set these to the values that you’ve registered before you can distribute your plugin to anyone (including beta testers).

"tst"
DEFAULT_VERSION =
"1.0"
MAX_TRACKS_IN_SUBMISSION =

Maximum number of tracks that will be accepted in a single submission. This is a server-imposed limit.

10

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(username, password, filename = nil, proxy = nil) ⇒ Audioscrobbler

Create a new Audioscrobbler object.

Parameters:

  • username

    Audioscrobbler account username

  • password

    Audioscrobbler account password

  • filename (defaults to: nil)

    file used for on-disk storage of not-yet-submitted tracks

  • proxy (defaults to: nil)

    optional HTTP proxy to use for outgoing connections



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/audioscrobbler.rb', line 124

def initialize(username, password, filename=nil, proxy=nil)
  @username = username
  @password = password
  @queue = SubmissionQueue.new(filename)
  @logger = Logger.new($stdout)
  @client = DEFAULT_CLIENT
  @version = DEFAULT_VERSION

  @handshake_url = DEFAULT_HANDSHAKE_URL
  @last_handshake_time = nil
  @handshake_backoff_sec = 0
  @hard_failures = 0

  @session_id = nil
  @submit_url = nil
  @now_playing_url = nil
  @submit_interval_sec = DEFAULT_SUBMIT_INTERVAL_SEC

  @proxy = {}
  if proxy
    uri = URI.parse(proxy)
    @proxy[:host], @proxy[:port] = uri.host, uri.port
    if uri.userinfo
      @proxy[:username], @proxy[:password] = uri.userinfo.split(':', 1)
    end
  end
end

Instance Attribute Details

#clientObject

Returns the value of attribute client.



152
153
154
# File 'lib/audioscrobbler.rb', line 152

def client
  @client
end

#handshake_urlObject

Returns the value of attribute handshake_url.



152
153
154
# File 'lib/audioscrobbler.rb', line 152

def handshake_url
  @handshake_url
end

#loggerObject

Returns the value of attribute logger.



151
152
153
# File 'lib/audioscrobbler.rb', line 151

def logger
  @logger
end

#passwordObject

Returns the value of attribute password.



151
152
153
# File 'lib/audioscrobbler.rb', line 151

def password
  @password
end

#usernameObject

Returns the value of attribute username.



151
152
153
# File 'lib/audioscrobbler.rb', line 151

def username
  @username
end

#versionObject

Returns the value of attribute version.



152
153
154
# File 'lib/audioscrobbler.rb', line 152

def version
  @version
end

Instance Method Details

#enqueue(artist, title, length, start_time, album = "", mbid = "", track_num = nil) ⇒ Object

Enqueue a track for submission. Returns true if track was successfully queued and false otherwise (currently, it only fails if the track’s metadata didn’t meet the Audioscrobbler submission rules).

Parameters:

  • artist

    artist name

  • title

    track name

  • length

    track length

  • start_time

    track start time, as UTC unix time

  • album (defaults to: "")

    album name

  • mbid (defaults to: "")

    MusicBrainz ID

  • track_num (defaults to: nil)

    track number on album



456
457
458
459
460
461
462
463
464
# File 'lib/audioscrobbler.rb', line 456

def enqueue(artist, title, length, start_time, album="", mbid="",
            track_num=nil)
  if not valid_metadata?(artist, title, length, start_time)
    @logger.warn("Ignoring track with invalid metadata for submission")
    return false
  end
  @queue.append(artist, title, length, start_time, album, mbid, track_num)
  true
end

#report_now_playing(artist, title, length, album = "", mbid = "", track_num = '') ⇒ Object

Report the track that is currently playing. Returns true if the report was successful and false otherwise.

Parameters:

  • artist

    artist name

  • title

    track name

  • length

    track length

  • album (defaults to: "")

    album name

  • mbid (defaults to: "")

    MusicBrainz ID

  • track_num (defaults to: '')

    track number on album



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'lib/audioscrobbler.rb', line 384

def report_now_playing(artist, title, length, album="", mbid="",
                       track_num='')
  @logger.debug("Reporting \"#{artist} - #{title}\" as now-playing")

  if not valid_metadata?(artist, title, length, Time.now.to_i)
    @logger.warn("Ignoring track with invalid metadata for now-playing")
    return false
  end

  # FIXME(derat): Huge race condition here between us and the submission
  # thread, but I am to lazy to fix it right now.
  if not @session_id
    do_handshake(false)
  end

  # Construct our argument list.
  args = {
    's' => @session_id,
    'a' => artist,
    't' => title,
    'b' => album,
    'l' => length.to_i,
    'n' => track_num,
    'm' => mbid,
  }
  # Convert it into a single escaped string for the body.
  body = args.collect {|k, v| "#{k}=" + CGI.escape(v.to_s) }.join('&')

  success = false
  begin
    url = URI.parse(@now_playing_url)
  rescue Exception
    @logger.warn("Submission failed -- couldn't parse now-playing " +
                 "URL \"#@now_playing_url\"")
  else
    headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
    begin
      data = Net::HTTP.start(url.host, url.port,
                             @proxy[:host], @proxy[:port],
                             @proxy[:username], @proxy[:password]) do |http|
        http.post(url.path, body, headers).body
      end
    rescue Exception
      @logger.warn("Submission failed -- couldn't read " \
                   "#@now_playing_url: #{$!}")
    else
      data.chomp!
      if data == "OK"
        @logger.debug("Now-playing report was successful")
        success = true
      else
        @logger.warn("Now-playing report failed -- got \"#{data}\"")
      end
    end
  end
  success
end

#start_submitter_threadObject

Start the submitter thread and return.



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/audioscrobbler.rb', line 299

def start_submitter_thread
  @submit_thread = Thread.new do
    while true
      # Wait until there are some tracks in the queue.
      tracks = @queue.peek(MAX_TRACKS_IN_SUBMISSION)

      # Keep trying to handshake until we're successful.
      do_handshake while not @session_id

      # Might as well re-check in case more tracks have shown up
      # during the handshake.
      tracks = @queue.peek(MAX_TRACKS_IN_SUBMISSION)
      @logger.debug("Submitting #{tracks.length} track(s)")

      # Construct our argument list.
      args = { "s" => @session_id }
      for i in 0..tracks.length-1
        args.update({
          "a[#{i}]" => tracks[i].artist,
          "t[#{i}]" => tracks[i].title,
          "i[#{i}]" => Time.at(tracks[i].start_time).to_i,
          "o[#{i}]" => 'P',
          "r[#{i}]" => '',
          "l[#{i}]" => tracks[i].length.to_s,
          "b[#{i}]" => tracks[i].album,
          "n[#{i}]" => tracks[i].track_num,
          "m[#{i}]" => tracks[i].mbid,
        })
      end
      # Convert it into a single escaped string for the body.
      body = args.collect {|k, v| "#{k}=" + CGI.escape(v.to_s) }.join('&')

      begin
        url = URI.parse(@submit_url)
        headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
        data = Net::HTTP.start(url.host, url.port,
                               @proxy[:host], @proxy[:port],
                               @proxy[:username], @proxy[:password]) do |http|
          http.post(url.path, body, headers).body
        end
      rescue Exception
        @logger.warn("Submission failed -- couldn't read " \
                     "#@submit_url: #{$!}")
      else
        # Check whether the submission was successful.
        lines = data.split("\n")
        if not lines[0]
          @logger.warn("Submission failed -- got empty response")
        elsif lines[0] == "OK"
          @logger.debug("Submission was successful")
          @queue.delete(tracks.length)
        elsif lines[0] == "BADSESSION"
          @logger.warn("Submission failed -- session is invalid")
          # Unset the session ID so we'll re-handshake.
          @session_id = nil
        else
          @logger.warn("Submission failed -- got unknown response " \
                       "\"#{lines[0]}\"")
          @hard_failures += 1
        end
      end

      if @hard_failures >= 3
        @logger.warn("Got #@hard_failures failures; re-handshaking")
        @session_id = nil
      end

      @logger.debug("Sleeping #@submit_interval_sec sec before checking " \
                    "for more tracks")
      sleep(@submit_interval_sec)
    end
  end
end

#verboseObject



159
160
161
# File 'lib/audioscrobbler.rb', line 159

def verbose
  @logger.sev_threshold == Logger::DEBUG
end

#verbose=(v) ⇒ Object

Backwards-compatability methods to control the logging level.



156
157
158
# File 'lib/audioscrobbler.rb', line 156

def verbose=(v)
  @logger.sev_threshold = v ? Logger::DEBUG : Logger::WARN
end