• プラグイン開発ガイド

    2.プラグインのパーツ

    前項で作成したディレクトリに以下3つのパーツ(ファイル)を配置します。
    本項では各ファイルの簡単な説明を記載し、詳細については別項以降で説明します。

    ファイル名 詳細
    app.json プラグインの説明を記載したファイル。必須です。
    form.html プラグインの外観並びに当該プラグイン専用のプログラムを記載したHTMLファイルとなる。
    通常は必須であるが、結果だけを表示するような特殊なプラグインの場合、不要な場合もある。
    run.sh プラグインでの入力に対して「何らかの処理をsmpdサーバ内で実施」する際のシェルスクリプトである。
    必須ではない。
    ※app.json・form.htmlのパーミッションは 644 でOKですが、run.shは 755 の権限が必要になります。
  • 3.app.json

    {
    	"menu": "UPDATE LIBRARY",
    	"icon": "restore_page",
    	"note": "MPD command"
    }
    

    上記に11-update_libraryのapp.jsonを例示します。
    本ファイルは JavaScript で使用される json 形式のデータです。
    ただし、データの形式としてはオブジェクト型(記載の先頭と末尾が必ず {, } となり、これに囲まれた部分にキーと値を記録する)となります。
    更にデータの記載方法を解説すると、値の定義(キーと値の組み合わせ)はキーの文字列、値の文字列を ”(ダブルコーテーション)で囲み、キーと値の間にコロンを入力する形となります。
    また、オブジェクト内の各行の最後には , を入れます。
    app.jsonが使用するキーと記載文字列は以下の通りです。

    キー 値
    menu Webuiのメニューや、プラグインの表題で表示される文字列を指定
    icon Webuiのメニューや、プラグインの表題で表示されるアイコンを指定(後述)
    note PLUGINS プラグインで一覧表に表示されるプラグイン説明文字列をします。

    ※通常JavaScriptのプログラミングに使用されているJSON形式では自由にキーを設定可能だが、
    Webui のapp.json(プラグイン説明用)としては menu, icon, note 以外のキー値は設定しても反映されない。

    ※iconで指定するアイコン文字列は以下の方法で特定する。
    Google が提供する Material Icons で表示されているもので、
    一覧画像に表示されたアイコンのうち希望のものをクリックするとウィンドウ右側にダイアログが表示されます。
    このダイアログの「Icon font」欄に表示されるサンプルでspanタグに囲まれた文字列で指定します。
    例えばSearchをクリックするとIcon fontのサンプル文字列は

    <span class="material-icons">
    search
    </span>
    

    と記載されているため、searchを指定します。
    Screenshot from 2023-02-19 21-44-10.png

  • 4.form.html(その1)

    11-update_library.png

    <!-- Copyright © 2020 symphonic-mpd Research and Development Club -->
    
    <div id="mpd_database_form" class="form-horizontal">
      <h4>Store tag_cache and playlists in the NAS</h4>
      <div class="form-group">
        <div class="col-sm-6">
          <button type="button" id="plugin_run_backup" class="btn btn-default">BACKUP</button>
          <button type="button" id="plugin_run_restore" class="btn btn-default">RESTORE</button>
        </div>
        <div class="col-sm-6">
          <div class="select-wrap">
            <select id="mpdnasno2" class="form-control">
              <option value="">NAS0</option>
              <option value="1">NAS1</option>
              <option value="2">NAS2</option>
              <option value="3">NAS3</option>
              <option value="4">NAS4</option>
              <option value="5">NAS5</option>
              <option value="6">NAS6</option>
              <option value="7">NAS7</option>
              <option value="8">NAS8</option>
              <option value="9">NAS9</option>
            </select>
          </div>
        </div>
      </div>
      <hr />
      <h4>Scan music directory</h4>
      <div class="form-group">
        <div class="col-xs-12">
          <input type="text" class="run" />
          <div class="checkbox-inline">
            <input type="checkbox" id="rescan-flg"/> rescan this directory
          </div>
        </div>
      </div>
    </div>
    
    <script>
      $(function(){
        if (config.browsepath.trim() == "") {
          $("input.run").val("/");
        } else {
          $("input.run").val(config.browsepath.trim());
        }
      });
    
      $('#plugin-form').on('shown.bs.modal', function() {
        socket.send('YMPD_GET_MPDHOST');
      });
    
      $("#mpd_database_form button").on('click', function() {
        $(this).off('click');
        var v = 'cmd:' + $(this).attr('id') + (($('#mpdnasno2').val() === '') ? '' : $('#mpdnasno2').val());
        $('input.run').val(v);
        plugin_form_ok();
        $('#plugin-form').modal('hide');
      });
      $('#rescan-flg').on('click', function() {
        var v = $('input.run').val().replace('rescan-', '');
        if ($('#rescan-flg').prop('checked')) v = 'rescan-' + v;
        $('input.run').val(v);
      });
    </script>
    
    

    上記に比較的ファイル量が少なく構造がわかりやすい例として「11-update_library」の form.html で説明します。

    form.html ファイルは名前の通りHTMLファイルですが、通常のブラウザ上で直接表示するHTMLファイルと異なり、HTML・HEADER・BODYタグは含まれていません。
    これは本ファイルはWebui で プラグインのメニュークリック時にindex.html(ツインビューの場合はindex2.html)body タグ内部の<div class="modal-body" id="plugin-form-body"> (plugin-form-bodyというIDを持つdivタグ)に読み込みされる前提で設計されているためです(正確には jquery の load() で読み込みされます)。
    ↑
    当該タグはプラグイン専用のダイアログ部分の本体として提供されているものです。

    (1)プラグインの外観の流儀
    原則「PC・タブレット・スマートフォンを問わず入出力画面を提供出来ること」が必要です。
    プラグイン専用ダイアログは「PC・タブレット向け」には横幅600px、これより画面の狭い場合(スマートフォン等)は画面解像度に合わせて自動で幅が設定されます。
    高さは内部のHTMLパーツ数に応じて自動的に拡張されます。

    また、WebuiではBootstrap3のグリッドレイアウト
    を採用し、プラグイン内部のテキストボックス・ボタンの配置が行なわれているため、本設計を準拠することを推奨します。

  • 5.form.html(その2)

    (2)form-horizontal
    form-horizontal.png
    form.htmlの最初のタグは<div class="form-horizontal">と記載します。
    これにより、実際のプラグインの中央パーツ部分 plugin-form-body 内部に余白が上・左右余白20px,下余白5pxの描画領域が生成される。
    (プラグインの各パーツは余白内に配置される)
    上の画像では開発者ツールでの当該領域を選択した際の画面のハイライトを表示しています。緑外枠部分は "padding" という余白領域になります。

    (3)form-group
    form-group.png
    form-horizontal 内部に後述のグリッドレイアウトでサイズを指定したパーツを配置する場合、まずは<div class="form-group">と指定したパーツを配置する。
    本パーツはスタイルとして次のパーツとの下余白:15px、左右余白:-15px設定されており、
    各パーツ間の下余白を設けることで見た目のバランスを整え、左右余白を-15px切り詰めすることで、後述のグリッドレイアウトの場合の最左・最右の余白をキャンセルする効果もある。
    なお、 h* ・p・pre タグなどで直接説明やタイトルを記載する場合はform-groupで囲まなくとも良い(逆にform-groupで囲むとform-horizontalで指定した左右余白が15ピクセル足りなくなる)。

    <h4>Store tag_cache and playlists in the NAS</h4>
    

    h4.png

  • 6.form.html(その3)

    (3)グリッドレイアウト

      <div class="form-group">
        <div class="col-sm-6">
    

    col-sm-6.png
    form-group内部にはクラスとしてグリッドレイアウトを指定したdiv要素を配置する。
    例えば、1行ぶち抜きでタイトル化したい場合は <div class="col-sm-12">という形で指定する。
    このときに指定したクラスの文字列「col-sm-12」のうち、colはcolumn(行)、smはsmall(画面サイズがsmall以上の場合適用)、12は当該パーツのサイズ(グリッドの横領域を最大12とした場合、どのくらい割当するか)を指定する。
    なお、クラスにグリッドレイアウトを複数指定した場合、画面のサイズから自動的に適用される。
    例えば"col-xs-12 col-sm-6"と指定した場合は、画面サイズがスマートフォン(xtra-small)の場合は領域一杯、タブレット端末(small)以上の場合は領域の半分で割り当てられる。

    グリッドレイアウトでのプラグインの外観制御方法は以下の通り。

    画面サイズ 制御方法
    スマートフォン(xtra-small) 横幅が狭いためxs-12で領域全体をラベルやテキストボックスに割当
    タブレッド以上(small) ラベルとテキストボックスを半々などにして表示させる。
    文字列長さが必要な場合は領域全体を使用

    グリッドレイアウトを指定した場合、その div タグの左右余白は 15pixel 確保されるので、グリッドレイアウトの親タグでform-groupを指定しておくとパーツの縦並びを揃えることが可能になる。

  • 7.form.html(その4)

    (4)ラベル

    <label class="control-label col-sm-4">support PNG</label>
    

    テキストボックス等の設定項目の表題を表示する場合、<label>タグを使用する。
    labelタグのクラスにはcontrol-labelとグリッドレイアウトのサイズを指定する。
    つまりラベルとテキストボックスを組み合わせることで1行にラベルでの説明+テキストボックスでの入力域を構成可能である。
    (上記の例だとsmallの場合はラベルが 4/12 のサイズで表示され、その後ろに テキストボックスを定義すると最大 8/12 で構成可能となる。)

    (5)テキストボックス

    <input type="text" class="run" />
    

    テキストボックスの指定にはinputタグを使用する。
    typeでtextを指定すると文字列、numberを指定すると数字のみ指定(かつ右横に上下の矢印が表示され、これのクリックで値の増減が可能)
    テキストボックのクラスにはrunとconfigの2つが指定出来、以下の通り処理が変更される。

    クラス名 詳細
    run run.shにパラメータとして指定することが可能
    config smpd サーバ上のファイルを後述の方法で直接書換可能
    ※configで値を取得する/ファイルの値を書き換えする場合は以下のとおり実施

     

    • classにconfigを指定(class="config")
    • uriに読込/書換対象のファイル名を指定(url="/etc/config")
    • keyに書換対象行のキー文字列を指定(key="hogehoge")

    つまりuriに指定した設定ファイル名内に key=value という記述を見つけた場合、valueを当該 input タグの値として表示し、
    プラグインの処理終了時にinput タグの値で設定ファイル内の当該キーを書き換えする。

    テキストボックスに対してグリッドレイアウトを適用する場合は適用したいボックスの上位にグリッド用のdivタグを配置し、
    当該テキストボックスを入れ込む。

    <div class="col-sm-6">
      <input type="text" class="run" id="hogehoge">
    </div>
    
  • 8.form.html(その5)

    (6)select タグ

     <div class="select-wrap">
       <select id="mpdnasno2" class="form-control">
         <option value="">NAS0</option>
         <option value="1">NAS1</option>
         <option value="2">NAS2</option>
        *** 中略 ***
       </select>
     </div>
    

    予め指定された選択肢から選択するような場合、select タグとoptionタグを使用する。
    bootstrap の標準設定では select タグのスタイルは無指定だが、webui では他のフォーム要素と同等のスタイルを設定済みである。これを有効にするため、selectタグの親に <div class="select-wrap"> が必要となる。

    selectタグの代わりに datalist タグ+inputタグによるコンボボックスも最新のブラウザでは使用可能ではあるが、
    スマートフォン向けのブラウザではdatalistは解析されず通常の input タグとしてしか機能しないため select の使用をお勧めする。

    (7)チェックボックス・ラジオボタン

    <div class="checkbox-inline">
     <input type="checkbox" id="rescan-flg"/> rescan this directory
    </div>
    

    グリッドレイアウトで配置した他のフォームと同一行内にチェックボックス/ラジオボタンを配置する場合、
    チェックボックス/ラジオボタンの親にdiv要素などでcheckbox-inline や radio-inline をクラス指定しておくと当該要素の高さがグリッドレイアウトのパーツと揃って表示される。
    また、複数のチェックボックス/ラジオボタンをグリッドレイアウトを使用せずに1行に並べて表示したい場合もcheckbox-inline・radio-inline は有効である。

  • 9.form.html(その6)

    (7)JavaScript

    <script>
      $(function(){
        if (config.browsepath.trim() == "") {
          $("input.run").val("/");
        } else {
          $("input.run").val(config.browsepath.trim());
        }
      });
    
      $('#plugin-form').on('shown.bs.modal', function() {
        socket.send('YMPD_GET_MPDHOST');
      });
    
      $("#mpd_database_form button").on('click', function() {
        $(this).off('click');
        var v = 'cmd:' + $(this).attr('id') + (($('#mpdnasno2').val() === '') ? '' : $('#mpdnasno2').val());
        $('input.run').val(v);
        plugin_form_ok();
        $('#plugin-form').modal('hide');
      });
      $('#rescan-flg').on('click', function() {
        var v = $('input.run').val().replace('rescan-', '');
        if ($('#rescan-flg').prop('checked')) v = 'rescan-' + v;
        $('input.run').val(v);
      });
    </script>
    

    script タグで囲まれた、form.html の最後部部分に JavaScript を記載する。
    まず、

      $(function(){
      }
    

    で囲まれた領域にプログラムを記載すると、form.htmlの読み込み終了直後(プラグイン起動直後)に何らかの動作を1度だけ行うことが可能である。
    本領域に記載する内容としては主に「プラグイン起動時にパラメータを読み込み」したりplugin-form-footer 領域にある標準のプラグイン制御ボタン(Cancel・OK)を非表示にする場合などに使用する。

    サンプルで表示している JavaScript では $(function(){ 部分に続き、$('#plugin-form').on('shown.bs.modal', function() { と $("#mpd_database_form button").on('click', function() { の2つの関数定義があるが、
    それぞれ pluguin-form の表示時と mpd_database_form 内のボタンが押された場合の処理を記載している。

    なお、前項で説明したテキストボックスの run と config クラスの実行については原則 plugin-form-footer の 「OK」ボタンを押した際のみ実行される(webui内部のmpd.js スクリプト内で plugin-form-ok()関数が呼ばれて実行される)。

  • 10.標準外のボタンを設置する場合
    03_searchの場合など、標準の「Cancel」・「OK」ボタンを使わずに別なボタンをプラグインの最下部に表示したい場合、
    <div class="form-horizontal"> とは別に <div class="modal-footer"> を作成して、この中にボタンを配置する。
    また、標準のボタンを JavaScript で以下の通り非表示とする。

    $('#plugin-form-footer').addClass('hidden');
    

    (標準ボタンはプラグイン領域が閉じた際に自動的に表示状態に戻るようにメインのスクリプトで記載済みのため、新たに再表示のためのコマンドは記載する必要は不要)

    また、追加で設置したボタンでの処理終了後、以下の通り当該プラグインフォームを閉じるように記述が必要です。

    $('#plugin-form').modal('hide');
    
  • 11.結果だけを表示するプラグインを配置する場合

    <div class="form-horizontal pd-20">
      <div class="panel panel-default">
        <table class="table"><tbody id="mpdstats"></tbody></table>
      </div>
    </div>
    <script>
      $(function() {
        $('#plugin-form-footer').addClass('hidden');
        $('#plugin-form').on('shown.bs.modal', function() {
          $(this).off('shown.bs.modal');
          socket.send('MPD_API_GET_STATS');
        });
      });
    </script>
    

    これはmpdの内部状況を返す 05-stats の form.html ですが、通常のform.htmlの先頭で定義されている form-horizontal が定義されたdivタグに pd-20 というクラスが追加で定義されています。

    このクラスは内部的に上下左右の余白(padding)を20pixelとして設定されているため、このクラスを定義すると上下左右に20pixelの余白を生成します。

    なお、form-horizontalの下層のdivタグにはpanel panel-default と定義されていますが、これはtableの外枠を描くために使用しています。

  • 12.結果だけを表示するプラグインを配置する場合(その2)

    <div class="pd-20" id="plugin-form-body">
      <pre></pre>
    </div>
    

    これはDisk-Usage プラグインの場合です(一部分)。
    当該プラグインの場合、実際にはform.html は存在せず、run.shの動作結果が pre タグとその内部の記述で返されています。

    また、プラグインを起動する際にplugin-form-bodyをIDに持つdivタグに pd-20 のクラスを追加して、上下余白を20pixelに設定しています。

    ※form.html を使用しない結果だけ出力するプラグインは現状 Disk-Usage のみであるため、上記 pd−20 クラスの追加は決め打ちで実行していますが、
    今後そのような事例が追加になった場合は別途対応を考えたいと思います。

  • 13.run.sh

    form.html の OK ボタンを押した際に、run クラスを持つ input タグ(テキストボックス)が1つでも存在する場合、当該タグの値を引数として run.sh が実行されます。
    ちなみに複数値がある場合、form.html 内での出現順に引数が渡されます。

    #!/bin/bash
    # Copyright © 2020 symphonic-mpd Research and Development Club
    
    #target="/var/lib/mpd/music/NAS"
    
    case "$1" in
      cmd:plugin_run_backup*)
        target="/var/lib/mpd/music/NAS${1#cmd:plugin_run_backup}"
        mpc stop > /dev/null 2>&1
        systemctl stop mpd > /dev/null 2>&1
        mount $target > /dev/null 2>&1
        mount -o rw,remount $target  > /dev/null 2>&1
        if [ -e $target/ ]; then
          cd /var/lib/mpd
          tar czf $target/playlists.tar.gz playlists
          cp -p tag_cache $target/
          mount -o ro,remount $target > /dev/null 2>&1
        fi
        systemctl start mpd
        sleep 0.2
        echo "<h2>Backup complete.</h2><pre>"
        ls $target/playlists.tar.gz $target/tag_cache -1
        echo "</pre>"
        echo "<script>setTimeout(function(){$.notifyClose();}, 1500);</script>"
        ;;
    
      cmd:plugin_run_restore*)
        target="/var/lib/mpd/music/NAS${1#cmd:plugin_run_restore}"
        if [ ! -e $target/tag_cache ]; then
          echo "<h3>There is no tag_cache on the NAS.</h3>"
          exit 0
        fi
        mpc stop > /dev/null 2>&1
        systemctl stop mpd > /dev/null 2>&1
        mount $target > /dev/null 2>&1
        cd /var/lib/mpd
        if [ -e $target/tag_cache ]; then
          cp -p $target/tag_cache .
        fi
        if [ -e $target/playlists.tar.gz ]; then
          tar xf $target/playlists.tar.gz
        fi
        systemctl start mpd
        sleep 0.2
        echo "<h2>Restore complete.</h2>"
        echo "<script>setTimeout(function(){$.notifyClose();}, 1500);</script>"
        ;;
      rescan-*)
        mpc rescan "${1#rescan-}" > /dev/null
        ;;
      *)
        mpc update "$1" > /dev/null
        ;;
    esac
    
    exit $?
    
    

    11-update_library の run.sh で説明します。

    本例では引数1個だけの場合で、cmd:plugin_run_backup* ・ cmd:plugin_run_restore* ・ rescan-* *のどれかに該当すると処理を行っています。
    ("*" の部分にはさらに追加指定されたパラメータが付与されている)

    run.sh の実行はもう一パターンあり、当該プラグインのディレクトリ内にform.htmlが存在しない場合(正確にはプラグイン呼び出し時にform.htmlをload関数で呼び出ししたがエラーが返された場合)は引数なしで実行されます。

    #!/bin/bash
    # Copyright © 2020 symphonic-mpd Research and Development Club
    
    echo "<pre>"
    pstree -Uclntp
    echo "</pre><pre>"
    df -BM -T -xdevtmpfs -xtmpfs -l |sed -z 's/\n/<br>/g'
    echo "</pre>"
    
    used_root=` df -h | grep "/dev/root" | awk '{ print $5 }' | sed -e '$s/.$//' `
    if [ $used_root -gt 80 ];then
        echo "<div class='alert alert-danger' role='alert'>Disk utilization has exceeded 80%.</div>"
    fi
    
    exit 0
    

    本例として 81-disk_usage の run.sh を掲載します。
    pstreeとdfの実行結果を pre タグで閉じて文字列として標準出力に返しているのがおわかりいただけるかと思います。

  • 14.run.sh(その2)
    プラグインのボタンクリック時に対し、RaspberryPi側に何らかのアクションを起こさせたい場合にも run.sh を使用することができます。
    実は、先に説明した「run クラスを持つ input タグ」での実行時、webui の内部では以下の処理を行っています。
    ① ブラウザの JavaScript がympdのサーバに向かって 'ACTIVATE_PLUGIN,/opt/plugins/' + プラグインのパス + ’,’ + パラメータ
    という命令を送る。
    ②ympd サーバが①の命令を受信すると、プラグインのパスにある run.sh を指定されたパラメータを与えて実行する。

    つまり、上記と同じ仕組みをプラグインのJavaScriptで構築し、run.sh に直接渡せばよいのです。

    function some_button_click(param) {
      var p = $('#plugin-form').data('path') + ',' + param;
      socket.send('ACTIVATE_PLUGIN,/opt/plugins/' + p);
    }
    

    上記の関数例(プラグインのJavaScriptソースコード)は ①の動きを直接記載したもので、
    関数の引数 param は プラグインの run.sh に渡すパラメータです。
    また、$('#plugin-form').data('path')が当該プラグインのパスが記載されている場所になります。
    この場合は単一の引数ですが、何らかの区切りで文字列を繋げば複数の引数をrun.shに渡し、run.shで区切り文字で配列を分割すれば複数パラメータを渡すことも可能です。

    次に、run.sh の実行結果をどうやってプラグインが受領するかですが、一例として run.sh に以下のように記載すると、
    /var/lib/mpd/RAM/ 上に隠しファイルで実行結果を書き込みできます。

    LOG=/var/lib/mpd/RAM/.bbb.out
    echo "aaa" > $LOG
    echo "---" >> $LOG
    

    さらにこの.bbb.outをプラグインで受信する場合、 JavaScript でこのようにすると良いでしょう。

    var checktimer = setInterval(function () {
        $('#out')
          .load('/RAM/.bbb.out?t=' + Date.now(), '', function (data) {
            if (data.endsWith('---\n')) {
              clearInterval(checktimer);
              var data2 = data.replace('---\n', '');
              //ここに受信したデータの処理を書き込み
            }
          });
      }, 300);
    

    これは、idにoutというタグを予め用意しておき、/RAM/.bbb.out の出力を300ミリ秒間隔で 文字列の最後が "---\n" で終わるまで受信し続けます。
    "---\n"が文字列の最後にあった場合、これを除去して受信したデータを処理すれば良い訳です。

  • 15.任意プログラムの実行方法
    実は webui (で使用しているympd)では、httpで /x/ に続いてコマンドを指定して実行することができます。
    この場合、一般的な cgi と同じように http://(ympdのIPアドレス)/x/(実行コマンド)?(パラメータ)で渡します。
    一連の処理のサンプル(JavaScript)はこちら

        $.ajax({
          url: location.protocol + '//' + location.host + '/x/vsound-setup.sh?interface',
          dataType: 'text',
        }).done(function (data) {
          const v2 = data.split(',');
          //ここに実行処理
        }).fail(function (data) {
          //ここにエラー処理
        });
    

    これは、AoEプラグインでの使用例で、vsound-setup.shにinterface というパラメータを渡した場合です。
    実行は JQuery の$.ajax を使って与えるコマンドを指定し、その実行結果の標準出力をdone関数のdataで受け取るという形になります。
    とりあえず、「/x/を使った実行は RaspberryPi側のPATH が有効な場所にあるアプリしか実行できない」ため、
    新しいプログラムやシェルスクリプトを置く場合は /usr/local/bin になると思います。