Raspberry Pi でYouTube目覚ましを作る

　初投稿です。頑張っていきます。改善点などあればコメントにて指摘いただけると助かります。

　youtube-dlを使ってyoutube上の公開プレイリストをwavファイル群としてローカルに同期し、cronで任意の時間にローカルに保存した曲をランダムに１曲再生します。

　ソースだけ見たい人
＝＞https://github.com/Nshu/wakeup_youtube

# 0. 環境
- Raspberry Pi 3
- Raspbian 9.8
- 3.5mmステレオミニジャック端子のスピーカー(Logicool Z120BW)
- USB-DAC(ASIN : B00NMXY2MO) 
    - ※最初は、raspiの内臓のDACから出力しようとしたが、音が出ない(出力不足？)ので急遽USB-DACを使うことに。

# 1. いろいろ動作確認
　まとめた実装をする前に個々の要素をテストしておきます。
## 1.1. youtube-dlの動作確認
　[公式ドキュメント](https://github.com/ytdl-org/youtube-dl/blob/master/README.md)を参考に進めていきます。
### 1.1.1. インストール
>
```
sudo curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl
sudo chmod a+rx /usr/local/bin/youtube-dl
```

### 1.1.2. youtube動画をwav形式でダウンロード
[公式リポジトリのIssue](https://github.com/ytdl-org/youtube-dl/issues/6724)より
>
```
youtube-dl --extract-audio --audio-format wav -o "output.%(ext)s" https://www.youtube.com/watch?v=[動画ID]
```

※ -oオプションでファイル名を指定する際、%(ext)sを使わず、直接拡張子(ここではwav)を指定してしまうと、ffmpegがスキップされてしまう。

### 1.1.3 youtubeの公開プレイリストに含まれる全動画のIDを取得
>
```
youtube-dl --get-id "https://youtube.com/playlist?list=[プレイリストID]"
```

標準出力に動画IDが一行ごとに表示されていきます。

### 1.2. wavファイルの再生
　aplayコマンドを使います。
　まず、`aplay -l`で使うオーディオデバイスのカード番号とデバイス番号を確認します。
　私の環境では、以下のようになりました。

```
**** ハードウェアデバイス PLAYBACK のリスト ****
カード 0: ALSA [bcm2835 ALSA], デバイス 0: bcm2835 ALSA [bcm2835 ALSA]
  サブデバイス: 7/7
  サブデバイス #0: subdevice #0
  サブデバイス #1: subdevice #1
  サブデバイス #2: subdevice #2
  サブデバイス #3: subdevice #3
  サブデバイス #4: subdevice #4
  サブデバイス #5: subdevice #5
  サブデバイス #6: subdevice #6
カード 0: ALSA [bcm2835 ALSA], デバイス 1: bcm2835 ALSA [bcm2835 IEC958/HDMI]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0
カード 1: Device [USB Audio Device], デバイス 0: USB Audio [USB Audio]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0
```
　今回使いたい、デバイスは

```
カード 1: Device [USB Audio Device], デバイス 0: USB Audio [USB Audio]
```
　なので、カード番号：１、デバイス番号：０となります。
　それから、

```
aplay -D hw:[カード番号],[デバイス番号] [ファイル名].wav
```
で再生できます。

# 2. 本実装
## 2.1. youtube上の公開プレイリストをローカルに同期させる
　まず、公開プレイリスト中の動画のIDリスト(online video IDs : oIDs)と
ローカルに保存した動画のIDリスト(saved video IDs : sIDs)を用意し、
　oIDsにはあってsIDsにはない動画は保存し、
　sIDsにはあってoIDsにはない動画は削除する。
という方式で同期を実装しようと思います

### 2.1.1. bashで実装

```bash:sync_youtube.sh

#!/usr/bin/env bash

readonly WORKING_DIR="${HOME}/wakeup_youtube"
cd ${WORKING_DIR}
export PATH=${PATH}:/usr/local/bin

readonly PLAYLIST_URL="https://www.youtube.com/playlist?list=PLl_bkR2Yd2EF0TShPL2tl7mYb98hvt8OV"
readonly CACHE_DIR="local_cache"

# online video IDs
oIDs=$(youtube-dl --get-id ${PLAYLIST_URL}) || exit 0 # if youtube-dl failed, exit.s
#oIDs=$(cat oIDs_cache.txt)
echo ${oIDs} > oIDs_cache.txt

function all_download () {
    for oID in ${oIDs}
    do
        youtube-dl -x --audio-format wav -o "${CACHE_DIR}/%(id)s.%(ext)s" https://www.youtube.com/watch?v=${oID}
    done
}

function download_missing_video () {
    # saved video IDs
    sIDs=$(ls ${CACHE_DIR} | xargs -i basename {} .wav)

    # if sIDs is empty
    if [[ -z ${sIDs} ]]
    then
        all_download
        return 0
    fi

    for oID in ${oIDs}
    do
        is_oID_in_sID="false"
        for sID in ${sIDs}
        do
            echo "oID : ${oID}"
            echo "sID : ${sID}"
            if [[ ${oID} = ${sID} ]]
            then
                is_oID_in_sID="true"
                break
                #echo "oID : ${oID}, sID : ${sID}"
            else
                :
                #echo "oID : ${oID}, sID : ${sID}"
            fi
        done
        if [[ ${is_oID_in_sID} = "false" ]]
        then
            youtube-dl -x --audio-format wav -o "${CACHE_DIR}/%(id)s.%(ext)s" https://www.youtube.com/watch?v=${oID}
        fi
    done
}

function del_excess_video () {
    # saved video IDs
    sIDs=$(ls ${CACHE_DIR} | xargs -i basename {} .wav)

    # Delete excess video
    for sID in ${sIDs}
    do
        is_sID_in_oID="false"
        for oID in ${oIDs}
        do
            if [[ ${sID} = ${oID} ]]
            then
                is_sID_in_oID="true"
                break
            fi
        done
        if [[ ${is_sID_in_oID} = "false" ]]
        then
             rm ${CACHE_DIR}/${sID}.wav
        fi
    done
}

download_missing_video
del_excess_video

```

## 2.2 ローカルに同期したプレイリストをランダムに一曲再生する


```bash:aplay_random.sh
#!/usr/bin/env bash

readonly WORKING_DIR="${HOME}/[WORKING_DIR_NAME]"
cd ${WORKING_DIR}
readonly WAV_DIR="[CACHE_DIR_NAME]"

wavs=($(ls ${WAV_DIR}))
random=$RANDOM
wavs_l=${#wavs[@]}
play_index=$(( ${random} % ${wavs_l} ))

aplay -N -D hw:1,0 ${WAV_DIR}/${wavs[${play_index}]}

```

## 2.3 「Device or resource busy」
　原因はよくわからないのですが、このエラーが出るので対処します。
　複数回実行を試みると実行されるようです。
　実行が成功するまで試行を続けるようにします。

```bash:drive_wakeup.sh
#!/usr/bin/env bash

readonly W_DIR="${HOME}/wakeup_youtube"

# if W_DIR is empty, exit
if [[ -z "$(ls ${W_DIR})" ]]; then
    exit 0
fi

bash ${W_DIR}/aplay_random.sh
while [ $? -gt 0 ]
do
  sleep 1
  bash ${W_DIR}/aplay_random.sh
done 
echo "finish"

```

## 2.4 cronで自動化
　後はcronで同期スクリプト、再生スクリプトを任意の時間に起動するように設定するだけです。

#### 主な留意点
- スクリプトの冒頭でワーキングディレクトリまで移動する
- 必要なPATHを通す
- 環境変数SHELLにBashを指定する

　以下設定例。

```
SHELL=/bin/bash

0 4 * * * bash [WORKING_DIR_PATH]/sync_youtube.sh
0 5 * * * bash [WORKING_DIR_PATH]/drive_wakeup.sh
```

# 3. まとめ、所感
　文法簡潔で、高機能なデバッガもあるPythonで書いた方が楽だと思ったのですがBashの勉強にとBashで実装してみました。

　条件評価式のなかで、変数の参照の際に$をつけ忘れると変数名がそのまま文字列として評価されるバグ(仕様)は、bashの`-x`オプションつけてトレースしないでいたら見つけるの本当に苦労したと思います。

　いつもはbashスクリプトを書くときraspiとのsshセッション上のvimで書いていたのですが、
　今回はPycharmにBash_Supportを乗っけて、SFTPでraspi上のファイルをリモートから直接編集する形にしてみました。
　リッチなIDEは初学者にやさしくて大変嬉しいです。欲をいえばBash_Supportプラグインにデバッグ機能も欲しいですね。