俺がわかるように書いたAlexaスキル入門４（AudioPlayer）


## Alexaの基本的なスキル作成４（オーディオ）
###### 2021/6

### ■ 前回
とりあえずプログラムをオフラインでやれるようにして、多少複雑なプログラムを書いても発狂しないようにしました。

で、Alexaでやりたいことの一つとして、個人所有のmp3とかを流せないかなというのがあります。
DLNAとかのを流せればいいなと思ってましたが、どうもそういうのは難しそうです。

Alexaで音楽を流す場合、そのデータは正式な署名があるサーバーにhttpsアクセスして取得できないとだめらしいんすよね。
（https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/audioplayer-interface-reference.html#audio-stream-requirements）

米amazonにはその辺を、PCにソフトと連携してそれっぽくできてるように見せるスキルがいくつかありましたが、日本では見つからなかった。

乗り掛かった舟なので、とりあえずAudioPlayerインターフェイスでなんか鳴らします。
<br>

---
### ■AudioPlayerインターフェイス
まっさらなカスタムスキルがあるところから開始です。
DeveloperConsoleのビルドのところにインテントとかに並んで「インターフェイス」ってところがあると思います。

この中に「AudioPlayer」ってのがあるので、それをONにします。

リファレンスにリンクがありますね。
https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/audioplayer-interface-reference.html
これ読んで、するする入る人ならここ読む必要ないです。
<br>


---
### ■処理するIntent、Request
ONにするとまず「AMAZON.PauseIntent」「AMAZON.ResumeIntent」ってのがビルトインインテントのところに増えます。

なぜ、このふたつのIntentが追加されて、ほかに飛んでくるIntentは追加されないのかはちょっと意味がわかりません。
この二つもエディットできるわけでもないし・・・。

ほかに飛んでくるのも含めてまとめます。

|Intent|どんな時に|どうする|必須？|
|--|--|--|--|
|AMAZON.PauseIntent|「止めて」とか|再生を止める|必須、ないと再生を意図的に止められない|
|AMAZON.ResumeIntent|「再開して」とか|再生を再開する|必須だけど、なくてもエラーになるだけ。使わないなら無反応のHundlerを作っておけばいい。|
|AMAZON.CancelIntent|「キャンセル」とか|終了かな|とにかく止まればいいので面倒ならPauseと一緒でもいいのでは？|
|AMAZON.RepeatIntent|「もう一度」とか|今の曲をはじめから|機能が欲しければ。無反応でもいいと思う。|
|AMAZON.StartOverIntent|「再起動」とか|今の曲をはじめから？|機能が欲しければ。無反応でもいいと思う。|
|AMAZON.NextIntent|「次」とか|次の曲に移動|これはあった方がいいと思う。|
|AMAZON.PreviousIntent|「前」とか|前の曲に移動|これはあった方がいいと思う。|
|AMAZON.ShuffleOnIntent|「シャッフル再生」とか？|シャッフル再生を開始|機能が欲しければ。無反応でもいいと思う。|
|AMAZON.ShuffleOffIntent|「シャッフル再生解除」とか？|シャッフル再生を解除|機能が欲しければ。無反応でもいいと思う。|
|AMAZON.LoopOnIntent|「ループ再生」とか？|ループ再生を開始|機能が欲しければ。無反応でもいいと思う。|
|AMAZON.LoopOffIntent|「ループ再生解除」とか？|ループ再生を解除|機能が欲しければ。無反応でもいいと思う。|

いくつか、実際は来なさそうなやつも混じってるんですが、一応仕様的にはこれらが来る可能性があるってことみたいです。

<br>

さらに、タッチ操作のデバイス（EchoShowとか）がある関係で、以下のRequestが飛んでくるみたいです。
まあ、これらは同様のIntent処理が上のにあるので、そこのcan_handleに追加するだけで大丈夫です。

|Request|どんな時に|どうする|何と同じ|
|--|--|--|--|
|PlaybackController.PreviousCommandIssued|前ボタン押された|前の曲再生？|AMAZON.PreviousIntent|
|PlaybackController.NextCommandIssued|次ボタン押された|次の曲再生|AMAZON.NextIntent|
|PlaybackController.PlayCommandIssued|再生ボタン押された|再生開始|再生開始のIntentとか、RepeatIntent、StartOverIntentあたりと同じでもいいのではないかと。|
|PlaybackController.PauseCommandIssued|再生中にポーズボタン押された|再生を止める|AMAZON.PauseIntentとか|

おっと、ここでRequestってなに？ですね。
IntentもRequestの一種で、Intent情報が含まれたRequestをIntentと総称しているポイです。

最初のほうでIntentを辞書みたいなものと評したのと剥離してる感じになってますが、Intentの辞書で認識された結果情報もまたIntentと呼ばれてるみたいです。

<br>

さらにさらに、AudioPlayerはスキルから指示が別のところに飛んで、そこからAlexaに直接 曲とかを流す感じで稼働するようなんですが、そこから実行時に信号的に飛んでくるRequestがこちらです。

|Request|どんな時に|どうする|必須？|
|--|--|--|--|
|AudioPlayer.PlaybackStarted|再生したぜ！|わかった。|無反応でOK|
|AudioPlayer.PlaybackStopped|止めたぜ！|わかった。|無反応でOK|
|AudioPlayer.PlaybackNearlyFinished|もうすぐ今の曲終わるぜ！|次の曲はこれだ。|次の曲をキューに追加してあげると連続再生になって良い。|
|AudioPlayer.PlaybackFailed|なんか失敗したぜ！|わかった。|無反応でOKだが、再生をやり直してもよい。|

これらで、おそらくすべてだと思いますが、まあ違ったら違ったで補完してください。
<br>
<br>

#### 最低限の実装
上の一覧だとかなりの量だと思います。
めまいがするってなもんです。
どんだけ減らせるかなと考えると、こんな感じでまとめてしまえるんじゃないかなという表。

|Intent|どのIntentHundlerでまとめて一緒の処理をするか|
|--|--|
|AMAZON.PauseIntent|StopIntentHundler|
|AMAZON.ResumeIntent|PlayerIntentHundler|
|AMAZON.CancelIntent|StopIntentHundler|
|AMAZON.RepeatIntent|PlayerIntentHundler|
|AMAZON.StartOverIntent|PlayerIntentHundler|
|AMAZON.NextIntent|NextIntentHundler|
|AMAZON.PreviousIntent|PreviousIntentHundler|
|AMAZON.ShuffleOnIntent|IgnoreIntentHundler|
|AMAZON.ShuffleOffIntent|IgnoreIntentHundler|
|AMAZON.LoopOnIntent|IgnoreIntentHundler|
|AMAZON.LoopOffIntent|IgnoreIntentHundler|
|PlaybackController.PreviousCommandIssued|PreviousIntentHundler|
|PlaybackController.NextCommandIssued|NextIntentHundler|
|PlaybackController.PlayCommandIssued|PlayerIntentHundler|
|PlaybackController.PauseCommandIssued|StopIntentHundler|
|AudioPlayer.PlaybackStarted|IgnoreIntentHundler|
|AudioPlayer.PlaybackStopped|IgnoreIntentHundler|
|AudioPlayer.PlaybackFailed|IgnoreIntentHundler|
|AudioPlayer.PlaybackNearlyFinished|NextIntentHundler亜種|

ということで、Player（HalloWorldIntent）、Stop、Next、NearlyFinish（Nextの亜種）、Previous、Ignoreの6種類ぐらいのIntentHundlerで最低限の実装になりそうです。
Resumeする場合はResumeを追加で。
<br>

---
### ■Directive
スキルが指示してAudioPlayerを動かすみたいなことを上で書きました。
Hundlerのレスポンスにその指示を入れ込んでおくとAudioPlayerに送ってくれます。

で、その指示書のことをDirectiveと呼んでるみたいです。
音を再生する指示（PlayDirective）、止める指示（StopDirective）ぐらいしか使いません。

```
handler_input
    .response_builder
    .add_directive( DIRECTIVE )
```

こんな感じで登録します。
<br>


#### PlayDirective

```python:import
from ask_sdk_model.interfaces.audioplayer import (
    PlayDirective, StopDirective, PlayBehavior, AudioItem, Stream)
```
```python:PlayDirective
PlayDirective(
    play_behavior=mode,
    audio_item=AudioItem(
        stream=Stream(
            token=tag,
            url=audio_url,
            offset_in_milliseconds=ofs,
            expected_previous_token=None
        )
    )
)
```
|名前|何？|
|--|--|
|play_behavior|すぐ再生するか、次に再生するかの指定|
|token|自由に持たせられる文字列。短すぎてもエラーになるっぽいけど、どこにも明記がない。8文字ぐらいは入れておいた方がよさげ。|
|url|データのURL（httpsでアクセスする署名サイト）|
|offset_in_milliseconds|曲のどこから再生するかの指定（ミリ秒）最低限実装ならResume無しにして0固定でもいい|
|expected_previous_token|None（前のトークンがこれならば実行するみたいな証明に使うもので最低限実装ではNone固定で問題ない）|

play_behaviorは基本的にはすぐ実行してもらうのでPlayBehavior.REPLACE_ALLです。
PlaybackNearlyFinishedの時には再生中のは止めずに次のを登録したいのでPlayBehavior.REPLACE_ENQUEUEDを指定します。

もう一つENQUEUEというのがありますが、次の次の曲をいろいろコントロールしたいということがほぼないので使わなくていいと思います。

tokenには再生中の曲の情報などを入れておくと、この情報を介して次の曲を探したりなどできるのでそういった情報を入れておくのが一般的な使い方みたいです。
<br>

#### StopDirective

引数ないので、特に説明いらないですね。
レスポンスに入れておけば止まります。
<br>

---
### ■再生中の情報は
NextInitentだとか、PauseIntentだとかに来た時にその時再生している曲の情報とかはどうやって取るんじゃい？

PlayDirectiveで指定したtoken、曲の現在時間はこんな感じでrequest_envelopeに入ってきます。

```python
tag = handler_input.request_envelope.context.audio_player.token
ofs = handler_input.request_envelope.context.audio_player.offset_in_milliseconds
```
PlayDirectiveで登録しておいた再生情報をここで取得し、解析して次の曲を割り出して再生すれば次の曲の再生ができるわけです。

ofsはResumeの時ぐらいしか使わないです。
Resumeから再度PlayDirectiveを入れるときに、ofsの値を渡すと止めたところから再生できます。
<br>


---
### ■データどこに置くか
サーバー用意できる人は好きにやってもらうとして、とにかく何も考えずに済ますならDeveloperConsoleのコードエディタのところにある「S3 Storage」ですね。

AWSの中にスキルに対応して使える領域が用意されてるようで、5Gほど自由に使えるようです。

スキル作るとlambda_function.pyのところにutils.pyってのが作られてると思いますが、この中のcreate_presigned_urlって関数がS3内のパスからURLを作ってくれます。

そのURLをPlayDirectiveに渡してやるとS3にアップロードした曲が使えます。

### ■サンプル
最低限実装でたぶん動くのをサンプルで置いておきます。
未来永劫動くわけじゃないので、参考程度で。

```python
# -*- coding: utf-8 -*-

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# import
# -------------------------------------------------------------------
import logging
import traceback
import random

from ask_sdk_core.utils import is_intent_name, is_request_type, request_util

from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_core.dispatch_components import AbstractExceptionHandler
from ask_sdk_core.handler_input import HandlerInput
from ask_sdk_model.slu.entityresolution.status_code import StatusCode

from ask_sdk_model import Response

from ask_sdk_model.interfaces.audioplayer import (PlayDirective, PlayBehavior, AudioItem, Stream, StopDirective)
from utils import create_presigned_url

# 001-004というファイル名でMedia直下にあるというサンプル
album={
    "files":["001", "002", "003", "004"]
}

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# audio関連処理する基底
# -------------------------------------------------------------------
class AudioRequestHandler(AbstractRequestHandler):

    TAG_MAGIC = "token_tag"

    def get_slot_value(self, handler_input, name, def_value):
        try:
            s = request_util.get_slot(handler_input, name)
            if s is not None and s.value is not None:
                return str(s.value)
        except:
            return def_value
        return def_value

    def get_slot_id(self, handler_input, name, def_value):
        try:
            s = request_util.get_slot(handler_input, name)
            if s is not None and s.resolutions is not None:
                r = s.resolutions.resolutions_per_authority[0]
                if r.status.code == StatusCode.ER_SUCCESS_MATCH:
                    return r.values[0].value.id
        except:
            return def_value
        return def_value
    
    # ---------------------------------------------------------------
    def play_s3(self, handler_input, s3_path, tag, mode=PlayBehavior.REPLACE_ALL, ofs=0):
        audio_url = create_presigned_url("Media/"+s3_path)
        handler_input.response_builder.add_directive(
            PlayDirective(
                play_behavior=mode,
                audio_item=AudioItem(
                    stream=Stream(
                        token=tag,
                        url=audio_url,
                        offset_in_milliseconds=ofs,
                        expected_previous_token=None
                    )
                )
            )
        ).set_should_end_session(True)

    # ---------------------------------------------------------------
    def tag_to_info(self, tag):
        # tagかチェック
        ws = tag.split(':')
        if ws[0] != self.TAG_MAGIC or len(ws) != 2:
            return None
        info = {}
        info["fileidx"] = int(ws[1])
        return info

    def info_to_tag(self, info):
        return self.create_tag(info["fileidx"])

    def create_tag(self, fileidx):
        return self.TAG_MAGIC + ":{}".format(fileidx)

    # ---------------------------------------------------------------
    def play_list(self, handler_input, tag, shift=0, mode=PlayBehavior.REPLACE_ALL, ofs=0):
        info = self.tag_to_info(tag)
        if info is None:
            return "再生情報がありませんでした。"

        # tagから現在の状態を得る
        if "count" in album.keys():
            listsnd = None
            listlen = album["count"]
        elif "files" in album.keys():
            listsnd = album["files"]
            listlen = len(listsnd)
        else:
            return "曲目情報がありませんでした。"

        # 次のidxを出す
        nextidx = info["fileidx"] + shift
        # 強制的に最初と最後はつながってる感じで
        if nextidx >= listlen:
            nextidx = 0
        elif nextidx < 0:
            nextidx = listlen - 1
        info["fileidx"] = nextidx

        # プレイ(通し番号のフォルダなら番号をそのまま変換)
        if listsnd is None:
            path_s3 = str(nextidx+1).zfill(3) + ".mp3"
        else:
            path_s3 = str(listsnd[nextidx]) + ".mp3"
        self.play_s3(handler_input, path_s3, self.info_to_tag(info), mode, ofs)
        return path_s3 + "を再生します"

    # ---------------------------------------------------------------
    def play_next(self, handler_input, mode=PlayBehavior.REPLACE_ALL):
        tag = handler_input.request_envelope.context.audio_player.token
        return self.play_list(handler_input, tag, 1, mode)

    def play_prev(self, handler_input):
        tag = handler_input.request_envelope.context.audio_player.token
        return self.play_list(handler_input, tag, -1)

    def play_first(self, handler_input, idx):
        return self.play_list(handler_input, self.create_tag(idx))

    def stop(self, handler_input):
        handler_input.response_builder.add_directive(StopDirective())


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# 再生
# -------------------------------------------------------------------
# 呼ばれました
# 認識する単語が聞こえました
class PlayerIntentHandler(AudioRequestHandler):
    def can_handle(self, handler_input):
        return(
            is_request_type("LaunchRequest")(handler_input) or
            is_intent_name("PlayerIntent")(handler_input) or
            is_intent_name("AMAZON.StartOverIntent")(handler_input) or
            is_request_type("AudioPlayer.PlaybackFailed")(handler_input)
        )

    def handle(self, handler_input):
        idx = self.get_slot_value(handler_input, "number", "0")
        msg = self.play_first(handler_input, idx)
        return handler_input.response_builder.speak(msg).response


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# ヘルプ
# -------------------------------------------------------------------
# ヘルプとかどうやって使うのみたいなこと言われました
class HelpIntentHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return is_intent_name("AMAZON.HelpIntent")(handler_input)

    def handle(self, handler_input):
        msg = "登録されている曲を流すだけです。"
        return handler_input.response_builder.speak(msg).response


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# 停止
# -------------------------------------------------------------------
# 止めてとか、終わってとか言われました
class CancelOrStopIntentHandler(AudioRequestHandler):
    def can_handle(self, handler_input):
        return (
            is_intent_name("AMAZON.CancelIntent")(handler_input) or
            is_intent_name("AMAZON.StopIntent")(handler_input) or
            is_intent_name("AMAZON.PauseIntent")(handler_input) or
            is_request_type("PlaybackController.PauseCommandIssued")(handler_input)
        )

    def handle(self, handler_input):
        msg = "終了します。"
        self.stop(handler_input)
        return handler_input.response_builder.speak(msg).response


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# わからない
# -------------------------------------------------------------------
# 何言ってるかわかりませんでした。
class FallbackIntentHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return is_intent_name("AMAZON.FallbackIntent")(handler_input)

    def handle(self, handler_input):
        msg = "すいません。わかりませんでした。"
        return handler_input.response_builder.speak(msg).response


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# 完了しました
# -------------------------------------------------------------------
class SessionEndedRequestHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return is_request_type("SessionEndedRequest")(handler_input)

    def handle(self, handler_input):
        return handler_input.response_builder.response


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# 再開してとか言われました、画面で押されました
# -------------------------------------------------------------------
class ResumeIntentHandler(AudioRequestHandler):
    def can_handle(self, handler_input):
        return (
            is_intent_name("AMAZON.ResumeIntent")(handler_input) or
            is_request_type("PlaybackController.PlayCommandIssued")(handler_input)
        )

    def handle(self, handler_input):
        tag = handler_input.request_envelope.context.audio_player.token
        ofs = handler_input.request_envelope.context.audio_player.offset_in_milliseconds
        msg = self.play_list(handler_input, tag, 0, PlayBehavior.REPLACE_ALL, ofs)
        return handler_input.response_builder.speak(msg).response


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# 次とか言われました、画面で押されました
# -------------------------------------------------------------------
class NextIntentHandler(AudioRequestHandler):
    def can_handle(self, handler_input):
        return (
            is_intent_name("AMAZON.NextIntent")(handler_input) or
            is_request_type("PlaybackController.NextCommandIssued")(handler_input)
        )

    def handle(self, handler_input):
        msg = self.play_next(handler_input)
        return handler_input.response_builder.speak(msg).response


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# 前とか言われました、画面で押されました
# -------------------------------------------------------------------
class PreviousIntentHandler(AudioRequestHandler):
    def can_handle(self, handler_input):
        return (
            is_intent_name("AMAZON.PreviousIntent")(handler_input) or
            is_request_type("PlaybackController.PreviousCommandIssued")(handler_input)
        )

    def handle(self, handler_input):
        msg = self.play_prev(handler_input)
        return handler_input.response_builder.speak(msg).response


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# 今の再生がそろそろ終わります(speakしてはいけない)
# -------------------------------------------------------------------
class PlaybackNearlyFinishedHandler(AudioRequestHandler):
    def can_handle(self, handler_input):
        return is_request_type("AudioPlayer.PlaybackNearlyFinished")(handler_input)

    def handle(self, handler_input):
        msg = self.play_next(handler_input, PlayBehavior.REPLACE_ENQUEUED)
        return handler_input.response_builder.response


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# 無視
# -------------------------------------------------------------------
class IgnoreIntentHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return (
            is_intent_name("AMAZON.ShuffleOnIntent")(handler_input) or
            is_intent_name("AMAZON.ShuffleOffIntent")(handler_input) or
            is_intent_name("AMAZON.LoopOnIntent")(handler_input) or
            is_intent_name("AMAZON.LoopOffIntent")(handler_input) or
            is_request_type("AudioPlayer.PlaybackStarted")(handler_input) or
            is_request_type("AudioPlayer.PlaybackFinished")(handler_input) or
            is_request_type("AudioPlayer.PlaybackStopped")(handler_input)
        )

    def handle(self, handler_input):
        return handler_input.response_builder.response


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# 処理するHandlerがありませんでした
# -------------------------------------------------------------------
class IntentReflectorHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return is_request_type("IntentRequest")(handler_input)

    def handle(self, handler_input):
        msg = "unknown Intent: " + get_intent_name(handler_input)
        return handler_input.response_builder.speak(msg).response


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# 何か例外が起こりました (すべて受ける)
class CatchAllExceptionHandler(AbstractExceptionHandler):
    def can_handle(self, handler_input, exception):
        return True

    def handle(self, handler_input, exception):
        logger.error(exception, exc_info=True)
        msg = "exception: {}:{}".format(exception, traceback.format_exc())

        return handler_input.response_builder.speak(msg).response




# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# ハンドラーの登録
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
sb = SkillBuilder()

# -------------------------------------------------------------------
# Intent,Requestのハンドラ
sb.add_request_handler(PlayerIntentHandler())

sb.add_request_handler(HelpIntentHandler())
sb.add_request_handler(CancelOrStopIntentHandler())
sb.add_request_handler(FallbackIntentHandler())
sb.add_request_handler(SessionEndedRequestHandler())

# audio playback
sb.add_request_handler(PlaybackNearlyFinishedHandler())
sb.add_request_handler(IgnoreIntentHandler())

# audio player
sb.add_request_handler(ResumeIntentHandler())
sb.add_request_handler(NextIntentHandler())
sb.add_request_handler(PreviousIntentHandler())


# 何も見つからなかった用なので、最後に登録する
sb.add_request_handler(IntentReflectorHandler())

# -------------------------------------------------------------------
# 例外ハンドラ（後ろの方が後に来る）
sb.add_exception_handler(CatchAllExceptionHandler())

# -------------------------------------------------------------------
lambda_handler = sb.lambda_handler()


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
```
モデルのほうはどうなってるか推理してみたらいいかもですねー。
ここまで読めるほど頑張ってたら、わかると思います。
<br>

---
### ■終わり

ほんと、もっと簡単に作れるようにできると思うわ。
Amazonも知能が高いんだろうから、そういうやつらの簡単は簡単じゃないってことが理解できないんだろうな～。

とりあえず、ここまででスキルは終わり。

Node-REDめんどくさそうだなぁ・・・。
askをpythonから直接使う感じでしゃべらせられないかなぁ・・・。
<br>

---
### ■おまけ
シャッフルとかループとかのあたりのヒント。

```python
    def set_repeat(self, handler_input, repeat):
        info = self.tag_to_info(handler_input.request_envelope.context.audio_player.token)
        info["repeat"] = repeat
        return self.play_list(handler_input, self.info_to_tag(info))

    def set_shuffle(self, handler_input, is_shuffle):
        info = self.tag_to_info(handler_input.request_envelope.context.audio_player.token)
        if is_shuffle:
            info["shuffle"] = self.create_shuffle()
        else:
            info["shuffle"] = None
        return self.play_list(handler_input, self.info_to_tag(info))

    def create_tag(self, listidx, listall, fileidx, filemax, repeat=1, shuffle=None):
        tag = "tag:{}:{}:{}:{}:{}".format(listidx, listall, fileidx, filemax, repeat)
        if shuffle is not None:
            tag += ":{}".format(','.join(map(str, shuffle)))
        return tag
    
    def create_shuffle(self, max):
        l = list(range(5))
        random.shuffle(l)
        return l

```