radikoスケジューラー

聴きたいradikoの番組を聴き逃さないために、時間になったら自動的に番組を再生してくれるプログラムを作ってみました。

再生にはブラウザを使用するので、Schemeで番組表APIを叩いて検索、番組データをJSON化してブラウザに渡す、という流れになります。

ブラウザとSchemeの通信に関して、JSONPを使うかどうか悩んだんですが、Racket(PLT Scheme)のテンプレート・システムでHTMLデータを生成する方法を選びました。


メイン関数

(fun (radiko info)
  (aand (filter-map (find-progs info)
                    ((sxpath '(radiko stations station)) (timetable)))
        (progs.json it)
        (serve it)))
(radiko '((pfm "山里亮太")))

のように呼び出します。pfm(performer)やtitle等の検索項目の後に文字列のリストを並べると、OR検索が行われるようにしています。

ちなみに、個々の番組情報は以下のようなXMLデータです。

(prog
 (@
  (dur "7200")
  (ft "20101202010000")
  (ftl "2500")
  (to "20101202030000")
  (tol "2700"))
 (title "<![CDATA[JUNK〜山里亮太の不毛な議論〜]]>")
 (pfm "<![CDATA[山里亮太(南海キャンディーズ)]]>")
 (url "<![CDATA[http://abc1008.com]]>"))

TBSの番組なのにテレ朝系でネットされているという...。ラジオって自由で良いですね。


検索関数

(fun (find-progs info station)
  (aand (filter (finder info)
                ((sxpath '(prog)) station))
        (cons (station-id station) it)))

(fun (finder info)
  (apply orf
         (filter-map (fn ((cons key ref))
                       (aand (lookup key info)
                             (matcher ref it)))
                     `((pfm   . ,prog-pfm)
                       (title . ,prog-title)
                       (desc  . ,prog-desc)
                       (info  . ,prog-info)))))

(fun (matcher ref target prog)
  (set rx (regexp (string-join #\| target)))
  (aand (ref prog) (regexp-match? rx it)))

finder関数によって、ユーザーが与えた全ての検索条件が1つの検索関数にまとめられます。orf:

(fun (orf . fns)
  (if (ormap [eqv? (procedure-arity _) 1]
             fns)
      (fn (x)
        (ormap [_ x] fns))
      (fn args
        ((afn (fns)
           (and fns
                (or (apply (car fns) args)
                    (self (cdr fns)))))
         fns))))


JSON化関数

(fun (progs.json data)
  (aand (append-map (fn ((cons stid progs))
                      (map (fn (prog)
                             (make-immutable-hasheq
                              `((station . ,stid)
                                (start   . ,(prog-start prog))
                                (end     . ,(prog-end prog))
                                (pfm     . ,(prog-pfm prog))
                                (title   . ,(prog-title prog)))))
                           progs))
                    data)
        (filter [< (current-seconds) (hash-ref _ 'end)] it)
        (sort [< (hash-ref _1 'start) (hash-ref _2 'start)] it)
        (call-with-output-string [write-json it _])))

既に放送が終わっているものは削除したり、放送順に並べたりもしています。


HTML生成+ブラウザ起動関数

(fun (serve progs)
  (send-url/contents (include-template "radiko.tmpl")))

include-templateというのはweb-server/templatesというライブラリのマクロで、コンパイル時にテンプレート・ファイルの解析を行います。

テンプレート中に、この例の場合@progsというシンボルがあると、スコープ内のprogsという変数の中身と置き換えられる仕組みです。

net/sendurlライブラリのsend-url/contents関数は、HTML文字列をファイル化してデフォルトのブラウザに開かせる、というものです。


その他

(fun (api path)
  (format "http://radiko.jp/~a" path))

(fun (area)
  (get-url (api "area")
    [regexp-match1 #rx"class=\"(.+?)\"" _]))

(fun (epg when)
  (sxml:document
   (api
    (format "epg/newepg/epgapi.php?area_id=~a&mode=~a" (area) when))))

(fun (timetable)
  (epg "today"))

(fun (nowplaying)
  (epg "now"))

(set cdata (regexp-match1 #rx"^<!\\[CDATA\\[(.+?)\\]\\]>$"))

(fun (string.path path)
  [aand ((sxpath path) _)
        (sxml:string it)
        (or (cdata it) it)])

(set station-id (string.path '(@ id)))

(set prog-pfm (string.path '(pfm)))
(set prog-title (string.path '(title)))
(set prog-desc (string.path '(desc)))
(set prog-info (string.path '(info)))

(fun (prog-time param prog)
  (time-second
   (date->time-utc
    (string->date ((string.path `(@ ,param)) prog) "~Y~m~d~H~M"))))

(set prog-start (prog-time 'ft))
(set prog-end (prog-time 'to))

いろいろと俺々なライブラリに依存しまくっていて申し訳ないんですが、ポータブルなコードよりもコンセプトの提示の方に関心がある、ということの表れなのでご容赦ください。


テンプレート:radiko.tmpl