Bottle0.13+jPlayer2.5で自分だけのミュージックプレイヤーを作ろう!

  • 19
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

 軽量フレームワークのBottleが人気らしいのでクラウドミュージックプレイヤーを練習がてらに作ります。なお、完成品はBitbucketにあげてあるので要点のみの解説になります。動作サンプルはこちらで公開しています。ちなみにPython2.6で動作確認しています。

クラウドミュージックプレイヤーとは・・

 iTunesやAmazon Cloud Playerなど、一度買ったらどの端末からでもダウンロードできて再生できるって便利ですよね。ですがCD購入厨としてはやっぱり手元の音源ファイルで再生したいものです。Google Musicは自分でファイルをアップロードできますが、日本の音楽業界は柔軟性が無いので同様のサービスを国内で運営するのは当分厳しいでしょう。
 そこで常々自分専用のクラウドミュージックプレイヤーを作成したいと構想していたので、この機会に実装してみることにします。

仕組みを考える

 別の手段として、SubsonicやIcecastを使ったストリーミングサーバを構築するという方法があります。これは手軽で再生可能プレイヤーも多い一方、サーバ側で音源をエンコードするため処理負荷が発生するなどのデメリットがあります。(というか今までこの方式で実用していたのだがいかんせん選曲操作に難あり)
 今回は再生の大部分をjQueryプラグインのjPlayerというHTML5ベースのメディアプレイヤーに任せます。ぶっちゃけPythonの処理部分は再生リストをリストアップするぐらいなので、だからこそ軽量フレームワークBottleの出番である・・・のかは分からないですが勉強にはちょうど良い内容だと思います。

Bottleの基本

 Bottleは実にシンプルな構文でウェブアプリを作成することができます。

au.py
# -*- coding: utf-8 -*-
from bottle import route, run #必要なメソッドだけ取り出すのが通っぽい(?)

@route('/hello') #下記関数を実行する対象パスを指定
def hello(): #関数名はなんでも良い
    return 'Hello World!' #ブラウザに出力する内容

if __name__ == "__main__": #ブロックしないと後述のApacheから実行した際にエラーとなるので注意
    run(host='0.0.0.0', port=8080, debug=True, reloader=True)

run()の引数については

  • host=ドメインを入れるとそのドメインでのアクセスしか応答しない模様。
  • port=VPSなどはFWで当該ポートの解放を忘れずに。
  • debug=エラーのトレースバックがブラウザに出力される。
  • reloader=スクリプト更新時にサーバが自動再起動する、がソケットエラーでわりとコケる(自分だけ?)。

上記スクリプトをコマンドラインから実行する。

python au.py

http://サーバアドレス:8080/hello にブラウザからアクセスすると「Hello World!」が表示されるはずです。

テンプレートを使う

 さあ、ここから目的のクラウドミュージックプレイヤーを実装していきます。BottleのテンプレートエンジンはDjangoライクに使えるのでDjango経験者は簡単に使えるのが嬉しいです。詳細な仕様は本家ドキュメントで大体わかります。

au.py
from bottle import route, template, TEMPLATE_PATH
import os

ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) #このスクリプトがあるフォルダの絶対パス
TEMPLATE_PATH.insert(0, ROOT_PATH+'/templates') #テンプレートファイルを格納するフォルダを指定

@route('/')
def index():
    return template('player.html') #テンプレートファイルに続けてパラメータを渡すこともできる

 テンプレートでは早速jPlayerを仕込んでいきます。見た目は二の次にするのでCSSはjPlayer付属品の[Blue Monday]を使用し、HTML部分はこちらのサイトの再生リスト付き音声プレイヤーを参考にさせて頂きました!
 また、各テンプレートの共通部分は`% rebase()で別のテンプレートファイルにまとめられます。これはDjangoと書式が異なるので注意。パラメータで渡しているjplayer.m3u.jsinit.jsは後で自作するスクリプトなので注意してください。

templates/player.html
% rebase('base.html', css=['jplayer.blue.monday.css'], js=['jquery.jplayer.min.js', 'jplayer.playlist.min.js', 'jplayer.m3u.js', 'init.js']) <!-- テンプレートファイルに続けてパラメータを渡せられるので追加で読み込むCSSとJSファイルを指定 -->
<div id="jquery_jplayer_N" class="jp-jplayer"></div>
<div id="jp_container_N" class="jp-audio">
    <div class="jp-type-playlist">
        <div class="jp-gui jp-interface">
            <ul class="jp-controls">
                <li><a href="javascript:;" class="jp-previous" tabindex="1">previous</a></li>
                <li><a href="javascript:;" class="jp-play" tabindex="1">play</a></li>
                <li><a href="javascript:;" class="jp-pause" tabindex="1">pause</a></li>
                <li><a href="javascript:;" class="jp-next" tabindex="1">next</a></li>
                <li><a href="javascript:;" class="jp-stop" tabindex="1">stop</a></li>
                <li><a href="javascript:;" class="jp-mute" tabindex="1" title="mute">mute</a></li>
                <li><a href="javascript:;" class="jp-unmute" tabindex="1" title="unmute">unmute</a></li>
                <li><a href="javascript:;" class="jp-volume-max" tabindex="1" title="max volume">max volume</a></li>
            </ul>
            <div class="jp-progress">
                <div class="jp-seek-bar">
                    <div class="jp-play-bar"></div>
                </div>
            </div>
            <div class="jp-volume-bar">
                <div class="jp-volume-bar-value"></div>
            </div>
            <div class="jp-time-holder">
                <div class="jp-current-time"></div>
                <div class="jp-duration"></div>
            </div>
            <ul class="jp-toggles">
                <li><a href="javascript:;" class="jp-shuffle" tabindex="1" title="shuffle">shuffle</a></li>
                <li><a href="javascript:;" class="jp-shuffle-off" tabindex="1" title="shuffle off">shuffle off</a></li>
                <li><a href="javascript:;" class="jp-repeat" tabindex="1" title="repeat">repeat</a></li>
                <li><a href="javascript:;" class="jp-repeat-off" tabindex="1" title="repeat off">repeat off</a></li>
            </ul>
        </div>
        <div class="jp-playlist">
            <ul>
                <li></li>
            </ul>
        </div>
    </div>
</div>
<div id="m3u_list"></div>

 続けて共通部分のテンプレートファイルを記述します。

templates/base.html
<!DOCTYPE html>
<html>
<head>
    <title>AuPy - Croud Music Player</title>
    <script type="text/javascript" src="js/jquery.min.js"></script>
% for item in css: <!-- パラメータで渡されたCSSファイルを読み込む -->
    <link rel="stylesheet" type="text/css" href="css/{{item}}"> 
% end
% for item in js: <!-- 同じくパラメータで渡されたJSファイルを読み込む -->
    <script type="text/javascript" src="js/{{item}}"></script>
% end
</head>
<body>
{{!base}} <!-- ここに呼び出し元のテンプレートファイルの内容が入る -->
</body>
</html>

 この段階ではまだjPlayerを実行できないのであしからず。

m3uファイルを読み込むjQueryプラグインを作る

 m3u/m3u8ファイルを渡すとjPlayerの再生リストにセットしてくれるプラグインを実装します。下記の書式で記述するとjQueryプラグインとして実装することができます。

;(function($) {
    $.fn.loadm3u = function (){
        //処理内容
    }
})(jQuery);

 今回は下記の様な使い方を想定したプラグインを作成します。
$().loadm3u(m3uファイルパス, サーバ側の音楽フォルダパス, 置換対象パス(オプション))
 プラグインには予め指定されたパスの自動置換機能を持たせます。これにより、foobar2000などで吐き出したm3u/m3u8ファイルをそのままクラウドミュージックプレイヤーで使用することができます。

js/jplayer.m3u.js
;(function($) {
    $.fn.loadm3u = function (filepath, server_music_dir, local_music_dir){
        $.get(filepath, function(data){ //引数で渡されたm3uファイルを取得する
            var data_array = data.split(/\r\n|\r|\n/); //改行で分割
            var playlists = [];
            for (var i = 0; i < data_array.length; i++){ //一行ずつ処理
                if(data_array[i] != ""){
                    if (typeof local_music_dir != "undefined") {
                        data_array[i] = data_array[i].replace(local_music_dir+"\\", "") //置換対象パスを除去
                    }
                    unix_dir_path = data_array[i].replace(/\\/g, "/") //バックスラッシュをスラッシュに修正
                    title = unix_dir_path.split("/"); //スラッシュで分割
                    playlists.push({"title":title[title.length-1], "free":true, "mp3":server_music_dir+"/"+unix_dir_path}); //音源のファイル名とファイルパスをリストに格納
                }
            }

            $("#jquery_jplayer_N").jPlayer("destroy"); //jPlayerを初期化する

            var Playlist = new jPlayerPlaylist( //jPlayerに読み込ませる再生リスト及びオプション
                {
                    jPlayer: "#jquery_jplayer_N",
                    cssSelectorAncestor: "#jp_container_N"
                }, playlists, {
                    supplied: "mp3" //対応音声ファイル形式
                }
            );
        });
    }
})(jQuery);

 なお、上記スクリプト内ではmp3のみを指定していますが、そのままファイルパスだけogg等の音楽ファイルを渡しても問題なく再生される模様です。flacはブラウザが再生に対応してないのでダメでした。

初期化用のJSファイルをPythonで生成させる

 まずは設定をまとめて記述する用のスクリプトを用意します。

setting.py
#! -*- coding: utf-8 -*-
import os

SERVER_MUSIC_ADDR = os.path.expandvars('audio') #音楽フォルダの相対/絶対URL
SERVER_PLAYLIST_ADDR = os.path.expandvars('playlist') #プレイリストフォルダの相対/絶対URL
SERVER_PLAYLIST_DIR = os.path.expandvars('/home/example/aupy/playlist') #プレイリストフォルダの絶対パス
LOCAL_MUSIC_DIR = os.path.expandvars('C:\Users\example\Music') #置換対象ローカル音楽フォルダの絶対パス

 次に、サーバのプレイリストフォルダ内ファイルをリストアップして<div id="m3u_list"></div>に追加し、ユーザが項目をクリックしたら先ほど作成したプラグインに読み込ませるJSファイルをPythonで出力させます。

au.py
from bottle import route, response
from setting import * #設定ファイルを読み込む

@route('/js/init.js') #このJSファイルへのリクエストを乗っ取る
def initjs():
    if LOCAL_MUSIC_DIR:
        local_music_dir = ', "'+LOCAL_MUSIC_DIR.replace('\\', '\\\\')+'"' #置換対象パスをセットする
    else:
        local_music_dir = ''

    output = '$(function(){\n' #出力内容の記述開始
    files = os.listdir(SERVER_PLAYLIST_DIR) #プレイリストフォルダ内ファイルをリストアップ
    files.sort()
    flg = True
    for file in files:
        if file.startswith('.'): #.htaccess等をスキップするため
            continue
        id = file.translate(None, '.')
        output += '$("#m3u_list").append("<a href=\\"#\\" id=\\"m3u_file_'+id+'\\" class=\\"list-group-item\\">'+file+'</a>");\n' #HTMLにプレイリストファイル追加
        output += '$("#m3u_file_'+id+'").click(function(){ $().loadm3u("'+SERVER_PLAYLIST_ADDR+'/'+file+'", "'+SERVER_MUSIC_ADDR+'"'+local_music_dir+'); });\n' #m3uファイルをプラグインに渡すクリックトリガ
        if flg:
            output += '$().loadm3u("'+SERVER_PLAYLIST_ADDR+'/'+file+'", "'+SERVER_MUSIC_ADDR+'"'+local_music_dir+');\n' #先頭のプレイリストを自動読み込み
            flg = False
    output += '\n});'

    response.content_type = 'text/javascript' #MIME TypeをJSに設定
    return output

 #文字列操作を実装するとコードが一気に難読化するのは自分だけでしょうか・・・。

スタティックファイルの扱い方

 最後に音楽ファイルとプレイリストをサーバにアップロードすればひとまずプレイヤーは完成です。これらのスタティックファイルは下記の様にBottleでリクエストを受け付ける事が出来ますが、きちんと後述のApacheで受け付ける様にしたほうが安定します。

au.py
from bottle import route, static_file

@route('/css/<filename>')
def css(filename):
    return static_file(filename, root=ROOT_PATH+'/css')

@route('/js/<filename>')
def js(filename):
    return static_file(filename, root=ROOT_PATH+'/js')

@route('/audio/<filepath:path>')
def audio(filepath):
    return static_file(filepath, root=ROOT_PATH+'/audio')

@route('/playlist/<filename>')
def playlist(filename):
    return static_file(filename, root=ROOT_PATH+'/playlist'

Basic認証

 誰でも音楽ファイルにアクセスできる状態にするのは著作権的にマズイのでパスワードを掛けましょう。BottleでもBasic認証をかける事ができます。今回はApacheのBasic認証用パスワードファイルである.htpasswdから認証情報を取得するようにします。ただし、ApacheのBasic認証と両方有効にすると正しく動作しないので必ずどちらかを無効にするようにしてください。

au.py
from bottle import route, auth_basic
from crypt import crypt

def check(user, passwd): #パスワードチェック用関数は自分で作る(つまりザル認証にもできる)
    for line in open(ROOT_PATH+'/.htpasswd', 'r'): #.htpasswdから一行ずつ取得する
        htpasswd = line[:-1].split(':') #ユーザ名と暗号化されたパスワードを分離
        if user is not None and htpasswd[1] is None: #.ユーザ名のみ設定されてても認証可能にする
            if user == htpasswd[0]:
                return True
        elif user is not None and passwd is not None:
            if user == htpasswd[0] and crypt(passwd, htpasswd[1][0:2]) == htpasswd[1]: #ユーザ名と暗号化パスワードを照合
                return True
    return False

@route('/')
@auth_basic(check) #認証を要求するリクエストに追記する
def index():
    return template('player.html')

Apache+WSGIから実行する

 Bottleのサーバ機能は安定性に難ありなのであくまで開発用に限定した方がいいでしょう。今回はApacheから呼び出す例を紹介しますが、環境によって大きく左右されるので参考程度にお願いします。まずは、Apacheから呼び出されBottleに引き渡すアダプターをWSGIファイルで作成します。

adapter.wsgi
import sys, os
sys.path.append(os.path.dirname(os.path.abspath(__file__))) #スクリプトが置かれているフォルダのパスを通す
import bottle, au #Bottle本体とアプリを読み込む
application = bottle.default_app()

 次にApacheのhttpd.confwsgi.conf、もしくはその他のヴァーチャルホスト設定ファイルなど環境によって適当なファイルに下記を記述します。

httpd.conf
WSGIScriptAlias /aupy /home/example/aupy/adapter.wsgi #アダプターを指定
<Directory "/home/example/aupy/">
   Options ExecCGI Indexes FollowSymlinks
   AllowOverride All #.htaccessを有効にする
   Order deny,allow
   Allow from all
</Directory>

Alias /aupy/audio /home/example/aupy/audio
<Directory "/home/example/aupy/audio">
   AllowOverride All
   Order deny,allow
   Allow from all
</Directory>

Alias /aupy/playlist /home/example/aupy/playlist
<Directory "/home/example/aupy/playlist">
   AllowOverride All
   Order deny,allow
   Allow from all
</Directory>

 また、音楽フォルダにBasic認証を掛けてしまうとAndroidのChromeで正しく再生開始されない様なので、直アクセスの対策としてリファラーかユーザエージェント(特定機種を指定)による制限で妥協しました。

SetEnvIf Referer "^http://example.com.*" allowReferer #クラウドミュージックプレイヤーからのアクセスなら許可
SetEnvIf User-Agent "example" allowReferer #特定のUAなら許可
order deny,allow
deny from all
allow from env=allowReferer

日本語ファイル名対応

 肝心な事を書き忘れてました。自分は洋楽HM厨なので問題ないですが、一般の方々は日本語ファイル名の音楽を再生したくなるのではないでしょうか。残念ながらBottleでマルチバイトURLをルーティングする方法は分りませんでしたのでやりかたあったら誰か教えてください。Apacheならmod_encodingを導入すればマルチバイトURLを扱うことができます。

 CentOS6.2で実施したコマンドを紹介します。ディストリビューションによってパス等が異なるので注意してください。まずはコンソールで必要なファイルをダウンロードします。

wget http://webdav.todo.gr.jp/download/mod_encoding-20021209.tar.gz
wget http://iij.dl.sourceforge.jp/webdav/13905/mod_encoding.c.apache2.20040616
wget http://www.aconus.com/~oyaji/faq/mod_encoding.c-apache2.2-20060520.patch

 ダウンロードしたファイルを展開してパッチを当てます。

tar zxvf mod_encoding-20021209.tar.gz
cd mod_encoding-20021209/
./configure --with-apxs=/usr/sbin/apxs
cp ../mod_encoding.c.apache2.20040616 mod_encoding.c
patch -p0 < ../mod_encoding.c-apache2.2-20060520.patch

 付属のiconv_hookライブラリをインストールします。

cd lib
./configure
make
sudo make install
sudo vi /etc/ld.so.conf
/usr/local/lib を追記
ldconfig

 いよいよ、mod_encodingのインストールをします。

cd ../
make
gcc -shared -o mod_encoding.so mod_encoding.o -Wc,-Wall -L/usr/local/lib -Llib -liconv_hook
sudo make install

 Apacheの設定ファイルに下記を追記します。

httpd.conf
<IfModule mod_encoding.c>
EncodingEngine On
SetServerEncoding UTF-8
DefaultClientEncoding UTF-8 CP932 EUCJP-MS

AddClientEncoding "RMA/*" CP932
AddClientEncoding "xdwin9x/" CP932
AddClientEncoding "cadaver/" UTF-8 EUCJP-MS
AddClientEncoding "Mozilla/" EUCJP-MS
</IfModule>

 最後にsudo service httpd restartでApacheを再起動すれば、マルチバイトを含むURLに正しくアクセスできるようになっているはずです。

おわりに

 以上、Bottleの紹介いかがでしたでしょうか。是非、ギークな皆さんも一家に一台クラウドミュージックプレイヤーを作ってみてください!