DAAPクライアントに応答してみる

mDNSは、Zeroconf.pyでできるようになったので、これを入れた簡単なWebサーバーデーモンにしてみる。yasusiiさめが導入ドキュメントを書いてくれているtwistedを使いたいところだけど、フットプリントの削減と、勉強をかねて、標準モジュールのみでやってみることに。
yasusiiさめのtwistedドキュメントは、(2)で終わりだろうか。まだ先もやってほしいな。
サーバーの終了時には、Zeroconf.unregisterService()とZeroconf.close()をしたいのだけど、終了処理はどう書いたらいいのかな。SIGTERMされたときに実行できればいいのだけど。try/finallyでやってみたけどダメ。シグナルハンドラを使って処理してみた。とりあえずうまく動いているけど、複数リクエストで複数プロセスになっている場合、親をKILLしても子プロセスが終われないことがあるようだ。なんでだろ? これは棚上げ。


Webサーバーとしては動作するようになったので、DAAPサーバーへの改造をはじめる。
プロトコルはHTTPと似ているのだけど、ヘッダ情報などは規格外になる。今後いろいろ追加が必要になりそうなので、SimpleHTTPServerを改造して、DaapHTTPServerを作る。
クラス名や関数名などを書き換え。
応答用ヘッダの一部はBasicHTTPServerで作られていたので、それを中でオーバーライド。
content-typeは"application/x-dmap-tagged"を固定で返せばいいので、その部分は潰してハードコード。
urlparseがdaap://を認識してくれなかったので、リクエストに応答できない。標準ライブラリをいじるのは気が引けるので、daap_urlparseを作ってimportする。
セッションはDAAPではHTTP1.1でやるのが普通のようだけど、1.0でも通信はできるようだ。BasicHTTPServerって、1.1には対応できてたかな?
変更部分は、まだその程度。
これでヘッダ部分がまともに応答できるようになった。

#!/usr/bin/python
#-*- coding: utf-8 -*-

EXECNAME = "daapd.py"
VERSION = "a0.0"

import Zeroconf
import socket
import BaseHTTPServer
import DaapHTTPServer
import signal, os

hostname = socket.gethostname()
hostaddr = socket.gethostbyname(socket.gethostname())
daapZConfType = "_daap._tcp.local."
daapZConfHost = hostname + "." + daapZConfType
daapPort = 3689

r = Zeroconf.Zeroconf()
info = Zeroconf.ServiceInfo(
        daapZConfType,
        daapZConfHost,
        socket.inet_aton(hostaddr),
        daapPort, 0, 0, '')
r.registerService(info)
print " daapd.py: regist mDNS."

def handler(signum, frame):
        print 'Signal handler called with signal', signum
        print " daapd.py: remove mDNS."
        r.unregisterService(info)
        r.close()

def daapServer(addr,
        HandlerClass = DaapHTTPServer.DaapHTTPRequestHandler,
        ServerClass = BaseHTTPServer.HTTPServer):
    HandlerClass.protocol_version = 'HTTP/1.0'
    httpd = ServerClass(addr, HandlerClass)
    sa = httpd.socket.getsockname()
    print "Serving HTTP on %s:%d" % (sa[0], sa[1])
    httpd.serve_forever()

if __name__ == '__main__':
    signal.signal(signal.SIGTERM, handler)

    addr = (hostaddr, daapPort)
    try:
        print " daapd.py: Server daemonized."
        daapServer(addr)

    except:
        print " daapd.py: Can't start Server."


クライアントからは、まず/server-infoと/content-codesに応答できれば良い。これらは、静的に送れれば通信はできるので、http://www.deleet.de/projekte/daap/からサンプルデータを落としてきて使う。
iTunesからは、「この共有された音楽ライブラリ[無題のサーバー]は、このバージョンのiTunesと互換性がありません。」と言われるので、とりあえずデータの中身は送信できているようだ。
中身をhexeditで見てもよくわからない。通信仕様を見ないとダメだね。
とりあえず、DAAPクライアントライブラリのpython-daapを使って、さくっと応答されている内容を見る。

>>> import daap
>>> d = daap.DAAPClient()
>>> d.connect('192.168.0.82', 3689)
>>> p = d.request('/server-info')
>>> p.printTree()
dmap.serverinforesponse (msrv)  c       None
        dmap.status (mstt)      i       200
        dmap.protocolversion (mpro)     v       1.0
        daap.protocolversion (apro)     v       1.0
        dmap.itemname (minm)    s       ray’s music
        dmap.loginrequired (mslr)       b       0
        dmap.timeoutinterval (mstm)     i       1800
        dmap.supportsautologout (msal)  b       0
        dmap.authenticationmethod (msau)        b       2
        dmap.supportsupdate (msup)      b       0
        dmap.supportspersistentids (mspi)       b       0
        dmap.supportsextensions (msex)  b       0
        dmap.supportsbrowse (msbr)      b       0
        dmap.supportsquery (msqy)       b       0
        dmap.supportsindex (msix)       b       0
        dmap.supportsresolve (msrs)     b       0

なるほど、こんな風になっているのね。4文字の名前とフラッグとデータみたいな作りなのか。
どうも、もらってきたデータでは、返しているバージョン情報が古すぎるようだ。たしか、iTunesは途中でv2以降にしか応答しなくなったのではなかったかな。
ついでに、iTunesとmt-daapdの応答も見ておく。
iTunes 6.0.5.20 の場合

dmap.serverinforesponse (msrv)  c       None
        dmap.status (mstt)      i       200
        dmap.protocolversion (mpro)     v       2.2
        daap.protocolversion (apro)     v       3.2
        None (aeSV)     None
        dmap.itemname (minm)    s       kinneko の音楽
        dmap.loginrequired (mslr)       b       1
        dmap.timeoutinterval (mstm)     i       1800
        dmap.supportsautologout (msal)  b       1
        None (msas)     None
        dmap.supportsupdate (msup)      b       1
        dmap.supportspersistentids (mspi)       b       1
        dmap.supportsextensions (msex)  b       1
        dmap.supportsbrowse (msbr)      b       1
        dmap.supportsquery (msqy)       b       1
        dmap.supportsindex (msix)       b       1
        dmap.supportsresolve (msrs)     b       1
        dmap.databasescount (msdc)      i       1

mt-daapdの場合

dmap.serverinforesponse (msrv)  c       None
        dmap.status (mstt)      i       200
        dmap.protocolversion (mpro)     v       2.0
        daap.protocolversion (apro)     v       3.0
        dmap.itemname (minm)    s       GLANTANK Music
        dmap.authenticationmethod (msau)        b       0
        dmap.timeoutinterval (mstm)     i       1800
        dmap.supportsextensions (msex)  b       0
        dmap.supportsindex (msix)       b       0
        dmap.supportsbrowse (msbr)      b       0
        dmap.supportsquery (msqy)       b       0
        dmap.supportsupdate (msup)      b       0
        dmap.databasescount (msdc)      i       1

どうも、必須の情報とそうでない情報があるようだ。

つづく。