外出先から自宅の Mac で動画をダウンロードしたいのに、ローカルネットワークの外だと Mac に触れない。そこで Telegram Bot を経由して Mac 上の処理を動かす形にしました。スマホから URL を貼るだけで済むので、外出先でも気軽に使えます。
構成
構成はこんな感じです。ハンドラとコマンド実行ロジックを分けて、あとから機能を足しやすくしています。
bot/
├── main.py # エントリポイント・ハンドラ登録
├── config.py # 環境変数・設定
├── handlers/
│ ├── dl_handler.py # /dl コマンド
│ ├── help_handler.py # /help
│ ├── status_handler.py # /status
│ └── ...
└── commands/
├── dl_dispatch.py # URL 判定とプラットフォーム振り分け
├── dl_youtube.py # yt-dlp による YouTube 動画・音声
├── dl_twitter.py # Twitter 動画
├── dl_common.py # シグナルハンドラ・サブプロセス終了
└── dl_state.py # 実行中タスクの状態管理 動画ダウンロード
/dl は -s で音声のみ、-l でライブ配信を指定できます。URL に応じて YouTube か Twitter に振り分ける処理は dl_dispatch.py で行っています。
def download(url, audio_only, live_stream) -> tuple[bool, str]:
if dl_youtube.is_youtube_url(url):
if audio_only:
return dl_youtube.download_audio(url, live_stream=live_stream)
return dl_youtube.download_video(url, live_stream=live_stream)
if dl_twitter.is_twitter_url(url):
return dl_twitter.download_video(url)
return False, "対応していない URL です。"
yt-dlp はブロッキングなので、asyncio.to_thread で別スレッドに任せて、ダウンロード中も他のリクエストを処理できるようにしました。
# ブロッキング処理を別スレッドで実行
download_id = register_download(url, media_type, live_stream, user_id)
try:
success, message = await asyncio.to_thread(
download, url, audio_only, live_stream
)
await update.message.reply_text(message)
finally:
unregister_download(download_id)
動画は最高画質・音声をマージして mp4 で出す設定です。ライブ配信の場合は live_from_start で配信終了まで録画します。
ydl_opts = {
"format": "bestvideo+bestaudio/best",
"merge_output_format": "mp4",
"outtmpl": str(DL_OUTPUT_DIR / "%(id)s.%(ext)s"),
"restrictfilenames": True, # パストラバーサル対策
"quiet": True,
"no_warnings": True,
}
if live_stream:
ydl_opts["live_from_start"] = True
ファイル名は restrictfilenames でサニタイズしてパストラバーサル対策を入れました。Bot 終了時(Ctrl+C など)に実行中の yt-dlp をちゃんと止めるため、シグナルハンドラと atexit を登録しています。登録済みのサブプロセスを一括で terminate し、5 秒待っても終わらなければ kill する形にしました。
# Ctrl+C 等で Bot 終了時にサブプロセスを確実に終了
def _terminate_active_subprocesses():
for proc in _active_ytdlp_processes:
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
signal.signal(signal.SIGINT, _signal_handler)
atexit.register(_terminate_active_subprocesses) セキュリティとコマンド
ユーザー ID のホワイトリスト(ALLOWED_USER_IDS)で許可した人だけがコマンドを打てるようにしています。.env でカンマ区切りで指定し、/whoami で自分の ID を確認してから追加する運用にしました。
# 許可するユーザー ID のリスト(カンマ区切り)
_raw_ids = os.getenv("ALLOWED_USER_IDS", "").strip()
ALLOWED_USER_IDS = [int(uid.strip()) for uid in _raw_ids.split(",") if uid.strip()]
YouTube と Twitter の URL だけ受け付け、他サイトは弾くようにしました。技術スタックは python-telegram-bot、yt-dlp、python-dotenv です。
使えるコマンドは /dl(動画・音声・ライブ)、/status、/help、/whoami です。Phase 1 として動く Bot ができたので、今後は Mac 上だからこそできる処理(ローカル LLM、音声の文字起こし、画像・動画の解析など)を足していく予定です。