Last.fmラジオの自作

http://code.google.com/p/thelastripper/wiki/LastFM12UnofficialDocumentation
Last.fm のラジオを自分で作る際に必要な WebAPI のドキュメント.ただ...これをみて堂々とプレイヤーを公開するのは気が引けますね.再生までの簡単な手順はこんな感じ*1.最後の方に "まとめ" として簡単なRubyスクリプトも載せてみた.これくらいなら大丈夫だよね...きっと..

手順

ラジオとして機能させるにはざっくり分けると四つのステップを踏むことになります.具体的には,

Handshake
セッションIDの取得,リクエスト先のホスト名の取得
Adjusting Radio Station
ラジオの選択
Requesting an XSPF
プレイリストの取得
Play
音楽再生

となります.しかし,Handshake で取得する情報は,2回目以降でも使い回しができるみたいです.

Handshake

セッションを張る

要求

以下の URL に GET リクエストを送る.

http://ws.audioscrobbler.com/radio/handshake.php?version=1.3.1.1&username=[USERNAME]&passwordmd5=[PASSWORDMD5_HASH]
[USERNAME]
Last.fm に登録してあるアカウントのユーザ名
[PASSWORDMD5_HASH]
Last.fm に登録してあるアカウントのパスワードのMD5ハッシュ関数を通した文字列(md5sum コマンドとか,Rubyだったら"digest/md5"とか?)
応答

すると,ws.audioscrobbler.com からはこんな感じのが返ってくる

session=ae1eb54a11615e605d61d6e83dde71bc
stream_url=http://87.117.229.85:80/last.mp3?Session=ae1eb54a11615e605d61d6e83dde71bc
subscriber=0
framehack=0
base_url=ws.audioscrobbler.com
base_path=/radio
info_message=
fingerprint_upload_url=http://ws.audioscrobbler.com/fingerprint/upload.php

実際に必要なのは session, base_url, base_path の値だけ.

session
以降リクエストする時のセッションID
base_url
以降のリクエストを送るホスト
base_path
以降のりくs(ry

これら三つを保存しておく.

Adjusting Radio Station

ラジオ局の指定

要求

以下の URL に GET リクエストを送る.

http://[base_url][base_path]/adjust.php?session=[sessionID]&url=[LASTFMURI]
[base_url],[base_path],[sessionID]
先ほどの Handshake で取得した値
[LASTFMURI]
ラジオを選択する時のLast.fm独自のURI
Last.fm独自URI

僕が良く使うやつだけ挙げておく.他のは元のを参照してください.

lastfm://artist/$artistname
$artistname で指定されるアーティストに似た楽曲が集まった局を指定
lastfm://globaltags/$tag
$tag で指定されるタグがついた楽曲が集まった局を指定
応答

例えば, lastfm://globaltags/8bit とラジオ局を選択した時に,選択が成功するとこんな感じの応答が返ってくる.

response=OK
url=lastfm://globaltags/8bit
stationname= 8bit Tag Radio
discovery=true

ここで特に保存すべき内容は無い.

Requesting an XSPF

プレイリストを取得する.XSPF とはプレイリストのファイル形式の一つで,XML の形式をとっています.

要求

以下のように GET 要求を送る

http://[base_url][base_path]/xspf.php?sk=[SESSIONID]&desktop=1.3.1.1
[base_url],[base_path],[sessionID]
先ほどの Handshake で取得した値
応答

すると以下のXSPF形式で応答がある.(先ほどと同じ様に 8bit というタグで指定した場合)

<playlist version="1" xmlns:lastfm="http://www.audioscrobbler.net/dtd/xspf-lastfm">
<title>+8bit+Tag+Radio</title>
<creator>Last.fm</creator>
<link rel="http://www.last.fm/skipsLeft">6</link>
<trackList>
    <track>
        <location>http://play.last.fm/user/da4d7abd06d00e21b9b387b491cbf1fe.mp3</locatio
n>
        <title>Marbles</title>
        <id>36732</id>
        <album>Swarm &amp; Dither</album>
        <creator>Hrvatski</creator>
        <duration>227000</duration>
        <image>http://cdn.last.fm/coverart/130x130/7099.jpg</image>
        <lastfm:trackauth>92c11</lastfm:trackauth>
        <lastfm:albumId>7099</lastfm:albumId>
        <lastfm:artistId>2308</lastfm:artistId>
                <link rel="http://www.last.fm/artistpage">http://www.last.fm/music/Hrvat
ski</link>
        <link rel="http://www.last.fm/albumpage">http://www.last.fm/music/Hrvatski/Swarm
%2B%2526%2BDither</link>
        <link rel="http://www.last.fm/trackpage">http://www.last.fm/music/Hrvatski/_/Mar
bles</link>
        <link rel="http://www.last.fm/buyTrackURL"></link>
        <link rel="http://www.last.fm/buyAlbumURL">http://www.last.fm/affiliate_sendto.p
hp?link=catch&amp;prod=7099&amp;pos=65633c2c6d40fbe9c8bf27ce82d2ca5a</link>
        <link rel="http://www.last.fm/freeTrackURL"></link>
    </track>
    <track>
        <location>http://play.last.fm/user/f1702fa3845bec7817a62749a1fbc49a.mp3</location>
        <title>Pioneer</title>
        <id>69590509</id>
        <album>Pioneer</album>
        <creator>she</creator>
        <duration>204000</duration>
        <image>http://cdn.last.fm/coverart/130x130/3287460.jpg</image>
        <lastfm:trackauth>35a7f</lastfm:trackauth>
        <lastfm:albumId>3287460</lastfm:albumId>
        <lastfm:artistId>1083333</lastfm:artistId>
                <link rel="http://www.last.fm/artistpage">http://www.last.fm/music/she</link>
        <link rel="http://www.last.fm/albumpage">http://www.last.fm/music/she/Pioneer</link>
        <link rel="http://www.last.fm/trackpage">http://www.last.fm/music/she/_/Pioneer</link>
        <link rel="http://www.last.fm/buyTrackURL"></link>
        <link rel="http://www.last.fm/buyAlbumURL"></link>
        <link rel="http://www.last.fm/freeTrackURL">http://freedownloads.last.fm/download/69590509/Pioneer.mp3</link>
    </track>
</trackList>
</playlist>

とこんな感じ.

Play

先のレスポンスの中の,"/playlist/trackList/track/location" で指定される文字列を,mpg123 などに渡す.

まとめ

再生までの最小限のコードはこんな感じ(?)になるのかな.
コピペして,@user, @password に,Last.fm のアカウント情報を代入すれば音楽の再生はできる.実際,ラジオとして機能させるには,UIをもっとシッカリしないとないといけないですが,それが目的ではないのでごく簡単な実装まで.

require "net/http"
require "digest/md5"
require "rexml/document"

HANDSHAKE_HOST = 'ws.audioscrobbler.com'
PORT = '80'

HANDSHAKE = 'handshake.php'
HANDSHAKE_PATH = '/radio'
ADJUST = 'adjust.php'
PLAYLIST = 'xspf.php'

def main
  @user = 'ユーザ名'
  @password = 'パスワード'

  # タグ 8bit でラジオ局を指定
  @lastfm_uri = 'lastfm://globaltags/8bit'

  #セッションIDの取得,リクエスト先のホストURIの取得
  handshake

  #ラジオの選択
  adjusting_radio_station

  #プレイリストの取得
  requesting_an_XSPF

  #音楽再生
  play
end

#セッションIDの取得,リクエスト先のホストURIの取得
def handshake

  handshake = HANDSHAKE_PATH + '/' + HANDSHAKE
  query = '?' + {
    'version' => '1.3.1.1',
    'username' => @user,
    'passwordmd5' => Digest::MD5.hexdigest(@password),
  }.to_a.map{|a|a.join('=')}.join('&')

  Net::HTTP.new(HANDSHAKE_HOST,PORT).request_get(handshake+query) do |res|
    puts '--- Handshake Response ---', res.body
    @session = res.body.match(/^session=(.+)$/)[1]
    @base_url = res.body.match(/^base_url=(.+)$/)[1]
    @base_path = res.body.match(/^base_path=(.+)$/)[1]
  end

end


#ラジオの選択
def adjusting_radio_station

  adjust = @base_path+'/'+ADJUST
  query = '?' + {
    'session' => @session,
    'url' => @lastfm_uri,
  }.to_a.map{|a|a.join('=')}.join('&')

  Net::HTTP.new(@base_url,PORT).request_get(adjust+query) do |res|
    puts '--- Adjusting Radio Station Response ---', res.body
  end

end

#プレイリストの取得
def requesting_an_XSPF

  path = @base_path + '/' + PLAYLIST
  query = '?' + {
    'sk' => @session,
    'desktop' => '1.4.2.58376',
  }.map{|a|a.join('=')}.join('&')

  xpath = '/playlist/trackList/track/location'
  @tracks = Array.new # ここにMP3ファイルのURLをしまう
  Net::HTTP.new(@base_url,PORT).request_get(path+query) do |res|
    puts '--- Requesting An XSPF Response ---', res.body
    REXML::Document.new(res.body).elements.each(xpath) do |ele|
      @tracks << ele.text
    end
  end
  puts @tracks
end

def play
  cmd = "mpg123 #{@tracks.join(' ')}"
  puts "Execute '#{cmd}'"
  system cmd
end

main if $0 == __FILE__

ちなみに

上記 '/playlist/trackList/track/location' のURLに同時にアクセスすることができないようになってる.厳密に言うと,'/playlist/trackList/track/location' のURLへは複数のコネクションを張ることが不可能になっている.つまり「ダウンロードし放題だぜヒャッホーゥ!!」とはならないようになっている.

*1:ひょっとしたら,他のトコロでも過去に取り上げられているかも知れませんが,'08年の2〜4月あたりに少しAPIの仕様が変わっているようです.この記事は'08年5月5日現在のこのAPI紹介です.