LINEからラズパイ経由でGoogle Home(Nest Hub)のYouTube再生リストのキューに動画をどんどん追加する [Raspberry Pi]
やりたいこと
LINEのMessaging APIを通して、YouTubeのURLを送信してラズベリーパイのみを
経由して、Google Home(Nest Hub)のYouTubeプレイリストのキューに
再生する動画を順次追加する。
意地でも声入力で再生させたくない人向け
システムイメージ
LINEのMessaging APIを通して、YouTubeのURLを送信してラズベリーパイのみを
経由して、Google Home(Nest Hub)のYouTubeプレイリストのキューに
再生する動画を順次追加する。
意地でも声入力で再生させたくない人向け
システムイメージ
動作の条件:Google Home(Nest Hub)でYouTubeの動画が再生中であること
※下記のスクリプトでは1個目を再生させられるので、まずこれを使うなどして
立ち上げる必要がある。
一応出来る限り1から説明していく。
前提
・Messaging APIのアカウントを作成していること(このあたりを参考に作成してください)
・Raspbian OSがインストールされていること
・ルーター側でIPのポートマッピングが出来ること
方針
・出来る限りフリーのサービスを用いる
・サーバーはすべてラズパイで完結させる(Herokuなどを使わない)
①https://developers.line.biz/console/にアクセスし、Providersを新規作成
②適当な名前をつけて作成する
③Create a Messaging API channelを選択
④適宜必要事項を入力して、利用規約に同意しCreate
⑤作成したAPIのページに移動し、Messaging APIのタブに移動する
QRコードが表示されているので、スマホのLINEからQRで友だち追加しておく
⑥独自ドメインを取得する
既に取得している場合は、ドメイン関係の手順は飛ばして良い
LINEのwebhookサーバを設定するために、独自ドメインを取得する必要がある。
今回はフリーのmydns.jpを用いた手順で説明する
https://www.mydns.jp/?MENU=010にアクセスし、登録情報を入力してアカウント作成
IDとパスワードが登録したメールアドレスに送られてくるので、メモしておく
⑦https://www.mydns.jp/にアクセスし、上記アカウントでログインする
⑧DOMEIN INFO(https://www.mydns.jp/?MENU=300)を開き、
自分の好きなドメイン名を入力する
Hostnameの欄は*を入力しておく
⑨ラズパイを起動し、ラズパイのIPを固定にする
/etc/dhcpcd.confを開く
古いラズパイの場合
sudo leafpad /etc/dhcpcd.conf
leafpadがない場合
⑩有線の場合
interface eth0の部分のコメントアウトを外し、以下のように入力
⑪無線の場合
wlan0を追記する
ラズパイを一旦再起動し、上記IPになっているかを確認する
⑫mydnsに自分のリモートIPを通知する
ターミナルを開き、以下を入力
上記のように、200 OKが出ていれば成功
⑬mydnsにIPが通知されていることを確認する
上記の登録状況に0以外が入っていれば成功している
⑭mydnsにIPを定期通知するスクリプトを作成する
mydnsの場合、一定期間IP通知がないと使用されていないとみなされ、
アカウントが削除されるため定期的に通知するようにする必要がある。
これに関しては色々やり方があるが、今回はcrontabを用いることとする
/etc/cron.d/にshファイルを作成する(sudo mousepad /etc/cron.d/mydns.sh等)
内容は以下
⑮crontabにIP定期通知するスクリプトを設定する
以下を実行
初回時は上記のメッセージが出るが、どのエディタを使うかという問いなので、1が推奨
青い文字の画面が出るはずなので、↓キーでスクロールして以下の行を追記
書いたらCtl+X→y→Enterで終了
⑯SSL証明書の取得
既に持っているなら飛ばして良い
LINEのwebhookサーバーはhttps化していないと動作しないので、
SSL証明書を導入する必要がある(自己証明書では×)
今回はフリーのLet's Encryptを用いる
以下コマンドを入力し、certbot-autoを取得する
以下の画面が出るので、yを押して完了まで待つ
⑰certbot-autoでSSL証明書ファイルを取得する
まず、ルーター側でラズパイのIPに対してポート80をマッピングする
(これをやってないと取得失敗する)
以下のコマンドを入力する
上記のような画面が出るが、今回はラズパイ上で取得したいので1を入力
⑱初回入力時はサービスに同意するかを聞かれる
aを入力する
⑲ドメイン名を入力
mydnsで設定したドメイン名を入力する
⑳証明書が保存される
上記のような画面が出ていれば成功
㉑証明書をファイルの確認
/etc/letsencrypt/live/設定したドメイン.mydns.jp以下にfullchain.pemや
privkey.pemがあることを確認する
この場所にあると扱いにくいので、ホームディレクトリ等にコピーし、アクセス権を変更しておく
crontabに登録しておくと良い
以下のような内容を/etc/cron.d/certbot.shに作成する
crontab -eで以下を追記する
まずターミナルから以下を実行してインストールする
※この後導入するcasttubeの仕様上、スクリーンIDを取得する必要がある
以下を実行し、IDを取得する
ランダムのような文字列が出るはずなので、メモしておく
㉓LINEのアクセストークンを取得する
LINEのMessaging APIのページを再び開き、下の方にあるChannel access tokenの
Issueをクリックする
画像ではReissueになっているが、一度実行するとReissueとなる
表示された文字列(Token)をメモしておく
㉔ルーター側でラズパイのIPに対してwebhookサーバに用いるポート番号をマッピングする
1000番台以下のポートを用いる場合は、この後の手順をすべてsudoで
やる必要があるので、できれば10000番台以上65535以下の適当なポート番号が望ましい
㉕WebhookのURLを設定する
LINEのMessaging APIのページを再び開き、Webhook URLのEditを選択する
アドレスの部分に以下を入力
https://設定したドメイン.mydns.jp:上記で設定したポート番号/
Use webhookを有効にする
㉖webhookサーバーを立ち上げる
以下のpythonスクリプトを保存し、次の部分を自分の環境に合わせて変更する
fullchain_pathにfullchain.pemのフルパスを記載
prvkey_pathにprivkey.pemのパスを記載
server_portに設定したポート番号を記載
line_tokenにLINEのトークンを記載
device_nameにGoogle Home(Nest Hub)のデバイス名を記載
device_screen_idにGoogle Home(Nest Hub)のスクリーンIDを記載
LINEのMessaging APIのページを再び開き、WebhookのURLのVerifyを選択する
上記のようなメッセージが出れば成功
出ない場合はサーバーが立ち上がっていない、ルーターのポートマッピングが失敗、
IPが固定できてないなどしている可能性が高いので、それぞれ設定できているかを確認する
㉘自動応答メッセージをオフにしておく
デフォルトのままだとメッセージ送信の度に固定メッセージが返って鬱陶しいので、
こないようにしておく
Auto-reply messagesのEditを選択する
基本設定>あいさつメッセージと、詳細設定>応答メッセージをオフにする
㉙LINEで追加したAPIのチャットでYouTubeのURLを送信し、Google Home(Nest Hub)が
立ち上がることを確認する
上記スクリプトでは、URLのみを入力することで再生が始まる
cast 飛ばしたURLのメッセージが返ってこれば成功しているはず
㉚再生キューに動画を追加する
↑でYouTubeが再生されている間に入力することが条件
add
追加したいURL(複数行入力可)を入力することで、次再生する動画に追加される
add 送ったURLのメッセージが返ってこれば成功しているはず
㉛Google Home(Nest Hub)を確認し、次に再生する動画が入っていることを確認する
Nest Hub miniなど画面がない端末は見れないと思うが、動きは同じになるはず
ちなみにnext 改行 URL(複数行入力可)で、割り込みして次に入れることも可能
remove 改行 URL(複数行入力可)で、キューから削除もできる
clearでリストを一括削除できる
連続してaddやnextを送ることも可能
※removeやclearで全部の動画がキューから削除されると、
一定時間後にYouTubeが立ち上がってしまう模様
その場合はアプリなどから強制的に終了させる必要がある
※このあたりはcattやcasttubeの動作仕様を見れば色々やれるはず
また、addなどが打ちにくい場合、自分の好きなように変えてもらえば良い
ここからは蛇足
ラズパイ再起動で自動でwebhookサーバを立ち上げる
㉜再起動時に立ち上げるターミナルgnome-terminalをインストールする
デフォルトだと背景白で気持ち悪いので、デフォのターミナルと同じ色にする
以下をターミナルから入力
㉞色変更
"システムのテーマ色を使用する"のチェックを外し、組み込みのスキームを
黒字に灰色文字にし、パレット>組み込みのスキームをLinux コンソールに変更
㉟フォントサイズ変更
文字>Custom fontのチェックを入れ、フォントサイズを10にする
これでデフォのターミナルっぽい色になる
㊱再起動時に走らせるスクリプト作成
適当なところに以下のようなshファイルを作成
起動時に走らせる仕組みとして、今回はautostartを用いる
これは隠しフォルダ以下に配置する必要があるので、まず隠しフォルダを表示させる
エクスプローラを開き、表示>隠しファイルを表示する を選択する
㊳ホームディレクトリ/.config/autostartに移動する
ない場合は新規ディレクトリ作成する
適当な名前.desktop というファイルを作成する
㊴autostartの内容を記述する
上記.desktopを開き、以下の内容を記載する
ただし、この手順ではSSL更新後の証明書再読み込みのための定期再起動が出来ないので、
何ヶ月も再起動させず、立ち上がりっぱなしにしておくと認証エラーになると思われる。
crontabなどで再起動出来ると思われるが、色々試したが出来なかったので
いつか出来るようになったら追記する
2020/12/05 0:40追記
最初のYouTube再生のcatt要求で、「YouTube said: Unable to extract video data」
といったようなエラーが出る場合がある。
cattの仕組みとして、youtube-dlを用いるようにしているようで、
YouTubeの仕様が変更された場合、こちらのモジュールが対応していないと
このようなエラーが出る模様。
youtube-dl側に対応してもらう必要があるが、最新版で解消したので記載しておく。
以下コマンドを実行し、webhookサーバを再起動
参考にした記事
CATTを使ってラズパイからChromecastやNest Hubに動画やWebサイトをキャストしてみる - Qiita
【Ubuntu16.04.5】PythonのFlaskをHTTPS化 - Qiita
casttube · PyPI
【Python】YouTubeの動画URLから動画IDを取り出す【メモ書き】 - Qiita
【python】os.systemはもう古い!?subprocessでコマンドを実行しよう! | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイト
sudo mousepad /etc/dhcpcd.conf
⑩有線の場合
interface eth0の部分のコメントアウトを外し、以下のように入力
interface eth0
static ip_address=192.168.(環境に合わせる).(他端末にかぶらない適当な数字)/24
static routers=192.168.(環境に合わせる).1
static domain_name_servers=192.168.(環境に合わせる).1
⑪無線の場合
wlan0を追記する
interface wlan0
static ip_address=192.168.(環境に合わせる).(他端末にかぶらない適当な数字)/24
static routers=192.168.(環境に合わせる).1
static domain_name_servers=192.168.(環境に合わせる).1
ラズパイを一旦再起動し、上記IPになっているかを確認する
⑫mydnsに自分のリモートIPを通知する
ターミナルを開き、以下を入力
wget -O /dev/null http://mydnsの自分のID:mydnsのパスワード@www.mydns.jp/login.html
上記のように、200 OKが出ていれば成功
⑬mydnsにIPが通知されていることを確認する
上記の登録状況に0以外が入っていれば成功している
⑭mydnsにIPを定期通知するスクリプトを作成する
mydnsの場合、一定期間IP通知がないと使用されていないとみなされ、
アカウントが削除されるため定期的に通知するようにする必要がある。
これに関しては色々やり方があるが、今回はcrontabを用いることとする
/etc/cron.d/にshファイルを作成する(sudo mousepad /etc/cron.d/mydns.sh等)
内容は以下
#!/bin/sh保存したら実行権限をつける
/usr/bin/wget -O /dev/null http://mydnsの自分のID:mydnsのパスワード@www.mydns.jp/login.html
sudo chmod 777 /etc/cron.d/mydns.sh
⑮crontabにIP定期通知するスクリプトを設定する
以下を実行
sudo crontab -e
初回時は上記のメッセージが出るが、どのエディタを使うかという問いなので、1が推奨
青い文字の画面が出るはずなので、↓キーでスクロールして以下の行を追記
0 */1 * * * /etc/cron.d/mydns.sh簡単に言うと、1日毎に通知する(詳細はcrontabの使い方を調べてください)。
書いたらCtl+X→y→Enterで終了
⑯SSL証明書の取得
既に持っているなら飛ばして良い
LINEのwebhookサーバーはhttps化していないと動作しないので、
SSL証明書を導入する必要がある(自己証明書では×)
今回はフリーのLet's Encryptを用いる
以下コマンドを入力し、certbot-autoを取得する
wget https://dl.eff.org/certbot-auto続けて以下のコマンドを入力する
sudo mv certbot-auto /usr/local/bin/certbot-auto
sudo chown root /usr/local/bin/certbot-auto
sudo chmod 0755 /usr/local/bin/certbot-auto
sudo /usr/local/bin/certbot-auto
以下の画面が出るので、yを押して完了まで待つ
⑰certbot-autoでSSL証明書ファイルを取得する
まず、ルーター側でラズパイのIPに対してポート80をマッピングする
(これをやってないと取得失敗する)
以下のコマンドを入力する
sudo /usr/local/bin/certbot-auto certonly --register-unsafely-without-email
上記のような画面が出るが、今回はラズパイ上で取得したいので1を入力
⑱初回入力時はサービスに同意するかを聞かれる
aを入力する
⑲ドメイン名を入力
mydnsで設定したドメイン名を入力する
⑳証明書が保存される
上記のような画面が出ていれば成功
㉑証明書をファイルの確認
/etc/letsencrypt/live/設定したドメイン.mydns.jp以下にfullchain.pemや
privkey.pemがあることを確認する
この場所にあると扱いにくいので、ホームディレクトリ等にコピーし、アクセス権を変更しておく
cp fullchain.pem /home/自分のアカウントこれに関しても、mydns同様一定期間更新しないと無効な証明書となるので、
cp privkey.pem /home/自分のアカウント
chown -R 自分のアカウント:自分のアカウント /home/自分のアカウント/*.pem
chmod 777 /home/自分のアカウント/*.pem
crontabに登録しておくと良い
以下のような内容を/etc/cron.d/certbot.shに作成する
#!/bin/sh
sudo /usr/local/certbot/certbot-auto renew
cd /etc/letsencrypt/live/設定したドメイン.mydns.jp
cp fullchain.pem /home/自分のアカウント
cp privkey.pem /home/自分のアカウント
chown -R 自分のアカウント:自分のアカウント /home/自分のアカウント/*.pem
chmod 777 /home/自分のアカウント/*.pem
sudo chmod 777 /etc/cron.d/certbot.sh
crontab -eで以下を追記する
0 0 1 * * /etc/cron.d/certbot.sh㉒必要なモジュールをインストールする
まずターミナルから以下を実行してインストールする
pip3 install flask更に、Google Home(Nest Hub)のスクリーンIDを取得する
sudo pip3 install catt
pip3 install casttube
※この後導入するcasttubeの仕様上、スクリーンIDを取得する必要がある
以下を実行し、IDを取得する
git clone https://github.com/ur1katz/CastTube-Scripts.git
cd CastTube-Scripts/Scripts/
python3 ./get_screen_id.py "自分のデバイス名(アプリなどから確認してください)"
ランダムのような文字列が出るはずなので、メモしておく
㉓LINEのアクセストークンを取得する
LINEのMessaging APIのページを再び開き、下の方にあるChannel access tokenの
Issueをクリックする
画像ではReissueになっているが、一度実行するとReissueとなる
表示された文字列(Token)をメモしておく
㉔ルーター側でラズパイのIPに対してwebhookサーバに用いるポート番号をマッピングする
1000番台以下のポートを用いる場合は、この後の手順をすべてsudoで
やる必要があるので、できれば10000番台以上65535以下の適当なポート番号が望ましい
㉕WebhookのURLを設定する
LINEのMessaging APIのページを再び開き、Webhook URLのEditを選択する
アドレスの部分に以下を入力
https://設定したドメイン.mydns.jp:上記で設定したポート番号/
Use webhookを有効にする
㉖webhookサーバーを立ち上げる
以下のpythonスクリプトを保存し、次の部分を自分の環境に合わせて変更する
fullchain_pathにfullchain.pemのフルパスを記載
prvkey_pathにprivkey.pemのパスを記載
server_portに設定したポート番号を記載
line_tokenにLINEのトークンを記載
device_nameにGoogle Home(Nest Hub)のデバイス名を記載
device_screen_idにGoogle Home(Nest Hub)のスクリーンIDを記載
import ssl
import subprocess
import requests
import json
import re
import urllib.parse
from flask import Flask, request, jsonify
from casttube import YouTubeSession
fullchain_path = 'fullchain.pemのフルパスを記載'
prvkey_path = 'privkey.pemのフルパスを記載'
server_port = 設定したポート番号を記載
line_token = 'LINEのトークンを記載'
device_name = 'Google Home(Nest Hub)のデバイス名を記載'
device_screen_id = 'Google Home(Nest Hub)のスクリーンIDを記載'
line_reply_api = 'https://api.line.me/v2/bot/message/reply'
youtube_long_url = 'https://www.youtube.com/watch?'
youtube_short_url = 'https://youtu.be/'
app = Flask(__name__)
session = YouTubeSession(device_screen_id)
@app.route('/', methods=['GET'])
def get_reply():
return jsonify({}), 200
@app.route('/', methods=['POST'])
def post_reply():
req_json = request.json['events'][0]
reply_token = req_json['replyToken']
req_message = req_json['message']['text']
messages = req_message.split('\n')
reply_message = ''
try:
if 'add' == messages[0] or 'next' == messages[0] or 'remove' == messages[0]:
reply_message = messages[0] + '\n'
lines = messages[1:]
if 'next' == messages[0]:
lines = reversed(lines)
for line in lines:
id = extract_id(line)
if '' == id:
reply_message += 'invalid url:'
else:
if 'add' == messages[0]:
session.add_to_queue(id)
elif 'next' == messages[0]:
session.play_next(id)
else:
session.remove_video(id)
reply_message += line + '\n'
reply_message = reply_message.rstrip('\n')
elif 'clear' == messages[0]:
session.clear_playlist()
reply_message = 'clear'
else:
id = extract_id(messages[0])
if '' != id:
cast_cmd = ['catt', '-d', device_name, 'cast', messages[0]]
try:
subprocess.check_output(cast_cmd, stderr=subprocess.STDOUT).decode()
reply_message = 'cast\n' + messages[0]
except subprocess.CalledProcessError as e:
reply_message = 'cast error\n' + messages[0] + '\n' + e.output.decode()
except Exception as e:
reply_message = str(e.args)
if '' != reply_message:
result = reply_text(reply_token, reply_message)
if 200 != result.status_code:
print('reply failed:' + str(result.status_code))
return jsonify({}), 200
def extract_id(url):
if re.match(youtube_long_url, url):
return urllib.parse.parse_qs(urllib.parse.urlparse(url).query)['v'][0]
if re.match(youtube_short_url, url):
return url[len(youtube_short_url):len(youtube_short_url) + 11]
return ''
def reply_text(reply_token, text):
return requests.post(
line_reply_api,
data=json.dumps({
'replyToken': reply_token,
'messages': [
{
'type': 'text',
'text': text
}
]
}).encode('utf-8'),
headers={
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + line_token
}
)
if __name__ == '__main__':
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_cert_chain(fullchain_path, prvkey_path)
print('start:' + str(server_port))
app.run(host='0.0.0.0', port=server_port, ssl_context=context, threaded=True)
以下のようにコマンドを入力してサーバーを立ち上げる
python3 my_srv.py㉗Webhookサーバーが使えるか確認
LINEのMessaging APIのページを再び開き、WebhookのURLのVerifyを選択する
上記のようなメッセージが出れば成功
出ない場合はサーバーが立ち上がっていない、ルーターのポートマッピングが失敗、
IPが固定できてないなどしている可能性が高いので、それぞれ設定できているかを確認する
㉘自動応答メッセージをオフにしておく
デフォルトのままだとメッセージ送信の度に固定メッセージが返って鬱陶しいので、
こないようにしておく
Auto-reply messagesのEditを選択する
基本設定>あいさつメッセージと、詳細設定>応答メッセージをオフにする
㉙LINEで追加したAPIのチャットでYouTubeのURLを送信し、Google Home(Nest Hub)が
立ち上がることを確認する
上記スクリプトでは、URLのみを入力することで再生が始まる
cast 飛ばしたURLのメッセージが返ってこれば成功しているはず
㉚再生キューに動画を追加する
↑でYouTubeが再生されている間に入力することが条件
add
追加したいURL(複数行入力可)を入力することで、次再生する動画に追加される
add 送ったURLのメッセージが返ってこれば成功しているはず
㉛Google Home(Nest Hub)を確認し、次に再生する動画が入っていることを確認する
Nest Hub miniなど画面がない端末は見れないと思うが、動きは同じになるはず
ちなみにnext 改行 URL(複数行入力可)で、割り込みして次に入れることも可能
remove 改行 URL(複数行入力可)で、キューから削除もできる
clearでリストを一括削除できる
連続してaddやnextを送ることも可能
※removeやclearで全部の動画がキューから削除されると、
一定時間後にYouTubeが立ち上がってしまう模様
その場合はアプリなどから強制的に終了させる必要がある
※このあたりはcattやcasttubeの動作仕様を見れば色々やれるはず
また、addなどが打ちにくい場合、自分の好きなように変えてもらえば良い
ここからは蛇足
ラズパイ再起動で自動でwebhookサーバを立ち上げる
㉜再起動時に立ち上げるターミナルgnome-terminalをインストールする
sudo apt-get install gnome-terminal㉝gnome-terminalのデザインを変える
デフォルトだと背景白で気持ち悪いので、デフォのターミナルと同じ色にする
以下をターミナルから入力
gnome-terminal編集>Preferencesを選択
㉞色変更
"システムのテーマ色を使用する"のチェックを外し、組み込みのスキームを
黒字に灰色文字にし、パレット>組み込みのスキームをLinux コンソールに変更
㉟フォントサイズ変更
文字>Custom fontのチェックを入れ、フォントサイズを10にする
これでデフォのターミナルっぽい色になる
㊱再起動時に走らせるスクリプト作成
適当なところに以下のようなshファイルを作成
#!/bin/sh実行権限をつけておく
my_server_path=上記スクリプトのフルパス.py
gnome-terminal -e "bash -c \"sudo kill $(pgrep -f $my_server_path);python3 $my_server_path; exec bash\""
chmod 777 restart_my_serv.sh㊲隠しフォルダを表示する
起動時に走らせる仕組みとして、今回はautostartを用いる
これは隠しフォルダ以下に配置する必要があるので、まず隠しフォルダを表示させる
エクスプローラを開き、表示>隠しファイルを表示する を選択する
㊳ホームディレクトリ/.config/autostartに移動する
ない場合は新規ディレクトリ作成する
適当な名前.desktop というファイルを作成する
㊴autostartの内容を記述する
上記.desktopを開き、以下の内容を記載する
[Desktop Entry]これで、ラズパイを再起動すれば自動でwebhookサーバーが立ち上がっているはず
Exec=再起動用スクリプトのフルパス.sh
Type=Application
Name=my_serv
Terminal=false
ただし、この手順ではSSL更新後の証明書再読み込みのための定期再起動が出来ないので、
何ヶ月も再起動させず、立ち上がりっぱなしにしておくと認証エラーになると思われる。
crontabなどで再起動出来ると思われるが、色々試したが出来なかったので
いつか出来るようになったら追記する
2020/12/05 0:40追記
最初のYouTube再生のcatt要求で、「YouTube said: Unable to extract video data」
といったようなエラーが出る場合がある。
cattの仕組みとして、youtube-dlを用いるようにしているようで、
YouTubeの仕様が変更された場合、こちらのモジュールが対応していないと
このようなエラーが出る模様。
youtube-dl側に対応してもらう必要があるが、最新版で解消したので記載しておく。
以下コマンドを実行し、webhookサーバを再起動
pip3 install --upgrade youtube-dl
参考にした記事
CATTを使ってラズパイからChromecastやNest Hubに動画やWebサイトをキャストしてみる - Qiita
【Ubuntu16.04.5】PythonのFlaskをHTTPS化 - Qiita
casttube · PyPI
【Python】YouTubeの動画URLから動画IDを取り出す【メモ書き】 - Qiita
【python】os.systemはもう古い!?subprocessでコマンドを実行しよう! | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイト
コメント 0