本稿は、Pythonで実装したデスクトップ向け動画ダウンローダーアプリケーションを題材に、その設計・実装に使用した技術スタックとプログラミング手法を体系的に解説する技術記事です。UI設計、非同期処理、ブラウザ自動化、動画ストリーミングの取得、そしてWindows向けのパッケージング技術まで、実装の各層を掘り下げます。

1. 技術スタック概要
本アプリケーションは、以下の技術レイヤーで構成されています。
| レイヤー | 技術・ライブラリ | 役割 |
|---|---|---|
| UI フレームワーク | CustomTkinter / Tkinter | ウィンドウ・ウィジェット描画 |
| ブラウザ自動化 | undetected-chromedriver | ヘッドレスChrome操作・ネットワーク監視 |
| HTTP 通信 | requests | セッション管理・セグメントダウンロード |
| 動画処理 | ffmpeg(外部バイナリ) | TSセグメント結合・MP4出力 |
| 画像処理 | Pillow (PIL) | アイコン変換・ロゴ表示 |
| 並列処理 | threading(標準ライブラリ) | UI ブロッキング回避 |
| パッケージング | PyInstaller | 単一 exe 化・リソース同梱 |
2. GUIフレームワーク:CustomTkinter
2-1. CustomTkinterとは
CustomTkinter(ctk)は、Tkinterをベースにモダンなルック&フィールを提供するサードパーティのUIライブラリです。標準Tkinterの古いウィジェット外観を刷新しつつ、Python標準ライブラリに近い操作感で使えるのが特徴です。
2-2. カラーシステムの設計
本アプリはダークテーマ固定・ピンクアクセントというデザイン方針を採用しています。UIカラーはすべてグローバル定数として一元管理されており、変更に強い構造です。
カラーパレット定義例:
C_PINK = "#FF3D7F" # メインアクセント C_SURFACE = "#1E1E2E" # 背景ベース C_SURFACE2 = "#2A2A3E" # カード背景 C_TEXT = "#F0E6EA" # 本文テキスト C_TEXT_SUB = "#9E8090" # サブテキスト
カラーを変数化することで、テーマ切り替えや将来的なカスタマイズに対応しやすくなります。フォント定義も同様に定数化(FONT_TITLE, FONT_LABEL 等)されており、表記ゆれを防いでいます。
2-3. ウィジェット構造
レイアウトはpackジオメトリマネージャを採用し、フレームを縦に積み上げる構成です。各UIブロックを独立したCTkFrame(カード)にまとめることで、視覚的なグルーピングとコードの保守性を両立しています。
- ヘッダー部: ロゴ画像またはテキストタイトル+バージョン表示
- 入力部: URL入力フィールド、ファイル名フィールド、保存フォルダ選択
- 状態表示部: ステータスラベル+プログレスバー
- ログ部:
CTkTextboxによる読み取り専用ログ - ボタン部: 開始・一時停止・停止の3ボタン
2-4. スレッドセーフなUI更新
TkinterはシングルスレッドのイベントループモデルをとっているためUIの更新はメインスレッドからのみ行う必要があります。本アプリではself.after(0, callback)パターンを使い、バックグラウンドスレッドからメインスレッドのイベントキューにUI更新をポストするアーキテクチャを採用しています。
def safe_append_log(self, text):
self.after(0, self._append_log_ui, text)
def _append_log_ui(self, text):
self.textbox.configure(state="normal")
self.textbox.insert("end", text)
self.textbox.see("end")
self.textbox.configure(state="disabled")
このパターンにより、ダウンロードスレッドが直接ウィジェットを操作することなく、競合状態(race condition)を回避しています。
3. ブラウザ自動化:undetected-chromedriver
3-1. 通常のSeleniumとの違い
undetected-chromedriver(uc)は、Seleniumが検出されるのを防ぐために開発されたChromeDriverのラッパーライブラリです。多くのWebサービスはSeleniumによる自動化を検知してアクセスをブロックするため、より人間のブラウザ操作に近い挙動を再現することを目的としています。
3-2. ヘッドレスモードの設定
本アプリはGUIアプリでありながら、ブラウザ操作部分はバックグラウンドで不可視のまま処理します。以下のオプションを組み合わせることで、ヘッドレスモードでも通常ブラウザとほぼ同じ挙動を実現しています。
| オプション | 目的 |
|---|---|
--headless |
画面に表示しないバックグラウンド実行 |
--window-size=1920,1080 |
フルHD相当のビューポートを持つ通常ブラウザに偽装 |
--user-agent=... |
通常のChrome UAを使用 |
page_load_strategy = 'eager' |
DOM生成完了時点で制御を受け取り、高速化 |
goog:loggingPrefs performance ALL |
ネットワーク通信ログの全捕捉を有効化 |
3-3. ネットワーク通信ログの解析
ページを読み込んだあと、動画プレイヤーが発行するHLS(HLS = HTTP Live Streaming)のプレイリストファイル(playlist.m3u8)のURLをChromeのパフォーマンスログから検出します。これはブラウザに実際にページを再生させ、内部通信をスニッフィングする手法です。
logs = self.driver.get_log("performance")
for entry in logs:
log_data = json.loads(entry["message"])["message"]
if "params" in log_data and "request" in log_data["params"]:
url = log_data["params"]["request"].get("url", "")
if "playlist.m3u8" in url:
m3u8_url = url
最大25秒間、1秒間隔でポーリングを行い、m3u8 URLが検出できなかった場合はフォールバック処理(480p固定URLへのアクセス)に移行します。
3-4. セッション情報の引き継ぎ
m3u8 URLが特定されたらブラウザの役割は終わりです。その後のセグメントダウンロードは軽量なrequestsセッションで行うため、ブラウザのクッキーとUser-Agentをrequests.Sessionに移植します。これにより認証済みセッション状態を維持したままブラウザを閉じることができます。
4. HLSストリーミング解析とセグメントダウンロード
4-1. HLSの仕組み
HLS(HTTP Live Streaming)はAppleが策定した動画配信プロトコルで、動画を短いセグメントに分割して配信します。プレイリストファイル(.m3u8)には配信URLやセグメントのリストが記述されており、2階層構造(マスタープレイリスト→メディアプレイリスト)を持つことが多いです。
M3U8の2階層構造
マスターM3U8 → 解像度ごとのメディアM3U8 → 各TSセグメント
4-2. 解像度選択
本アプリはマスタープレイリストの末尾エントリを取得することで、最高画質のストリームを自動選択します。
lines = [l.strip() for l in r.text.splitlines() if l.strip() and not l.startswith("#")]
target_resolution = lines[-1] # 末尾 = 最高解像度
4-3. セグメントの逐次ダウンロード
セグメントを1つずつダウンロードし、一時フォルダに連番ファイルとして保存します。重要な実装ポイントは以下の通りです。
- 拡張子の自動判別: URLに
.jpegが含まれる場合(拡張子偽装への対応)、保存ファイルの拡張子を正しく切り替える - スキップ処理: 既にダウンロード済みで100バイト以上のファイルは再取得せず、再開機能の基盤となる
- リトライ処理: 失敗時は最大3回リトライを行い、1秒のウェイトを挟む
- 一時停止対応:
is_pausedフラグのポーリングにより、ループの途中から一時停止・再開が可能
4-4. ffmpegによるセグメント結合
全セグメントのダウンロード完了後、ffmpegのconcatデマクサーを使ってTS→MP4変換を行います。
ffmpeg -y -f concat -safe 0 -i file_list.txt -c copy output.mp4
-c copyはエンコードをスキップして単純なコンテナ変換を行うため、高速かつ無劣化での出力が可能です。セグメントのファイルパスはダウンロード中にfile_list.txtへ逐次書き込まれます。ffmpegはsubprocess.Popenで起動し、プロセス参照をself.ffmpeg_procに保持することで停止ボタンによるkill()を可能にしています。
5. 並列処理と状態管理
5-1. マルチスレッド設計
ダウンロード処理はthreading.Threadでメインスレッドとは独立して実行されます。daemon=Trueを設定することで、メインウィンドウが閉じられたときに自動でスレッドが終了します。
5-2. 制御フラグによる状態管理
一時停止・停止・再開のコントロールは2つのブールフラグで実現しています。
| フラグ | 型 | 役割 |
|---|---|---|
is_paused |
bool | ダウンロードループの一時停止 |
is_stopped |
bool | 処理全体の中断フラグ |
ダウンロードループは各セグメント処理の先頭で両フラグをチェックし、is_paused中はtime.sleep(0.3)を繰り返すビジーウェイト方式で待機します。is_stoppedが立った時点で即座にループを抜け、クリーンアップ処理に移ります。
5-3. 停止時のリソース強制解放
停止ボタンを押した際には専用の_force_kill関数が別スレッドで実行されます。
ffmpeg_proc.kill()でffmpegプロセスを即時終了driver.quit()でChromeを終了_cleanup_workspace()で一時ファイルを全削除self.after()でUIをリセット状態に復帰
6. PyInstallerによるパッケージング
6-1. 単一exeへのバンドル
PyInstallerはPythonスクリプトと依存ライブラリをすべて1つの実行ファイルにまとめるツールです。配布先にPython環境がなくても動作するWindowsアプリを生成できます。
6-2. リソースパスの解決
PyInstallerでパックした場合、アイコンや画像等のリソースファイルは一時展開ディレクトリ(sys._MEIPASS)に配置されます。通常実行時とパック時の両方でパスを正しく解決するため、以下のヘルパー関数が実装されています。
def resource_path(relative_path):
base = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base, relative_path)
getattr(sys, '_MEIPASS', ...)は、PyInstaller実行時は展開先パスを、通常実行時はスクリプトのディレクトリを返します。これにより開発時・配布時の双方で同じコードが動作します。
6-3. アイコン処理
Windowsのウィンドウアイコン(.ico)がない場合、Pillowを使ってPNGから動的にICOファイルを生成し一時ファイルとして保存・適用します。ICOは複数サイズのビットマップを内包する形式であるため、256px・64px・48px・32px・16pxの5サイズを同梱しています。
6-4. ffmpegの同梱
resource_path("ffmpeg.exe")でバンドル内のffmpegを参照し、存在しない場合はシステムPATHにフォールバックします。これにより、ffmpeg同梱版と非同梱版の両方のビルド形態に対応できます。
7. セキュリティ・品質に関する実装
7-1. ファイル名サニタイズ
Windowsのファイル名として禁止されている文字(\ / * ? : " < > |)を正規表現で一括置換します。
def sanitize_filename(filename):
return re.sub(r'[\\/*?:"<>|]', "_", filename).strip()
7-2. ウィンドウ非表示フラグ
サブプロセス起動時にcreationflags=_CREATE_NO_WINDOW(0x08000000)を指定することで、ffmpegのコンソールウィンドウがユーザーに見えないよう制御しています。これはWindows専用のフラグです。
7-3. 例外の網羅的な処理
ダウンロードロジック全体をtry...finallyで囲み、いかなる例外発生時もブラウザドライバが確実に閉じられるよう保証しています。各所のexcept Exception: passはUI・ドライバ関連の副次的な失敗を許容するもので、メインロジックの堅牢性を維持する設計です。
8. 技術的なポイントまとめ
本プロジェクトで採用した技術と設計方針を改めて整理します。
| テーマ | 採用技術・手法 |
|---|---|
| モダンGUI | CustomTkinter + カラー定数管理によるテーマ設計 |
| スレッドセーフUI | self.after()によるメインスレッドへのUI更新委譲 |
| ブラウザ偽装 | undetected-chromedriver + UA・Cookie引き継ぎ |
| HLS解析 | Chromeパフォーマンスログのポーリングによるm3u8 URL検出 |
| セグメント処理 | 逐次DL・リトライ・拡張子偽装対応・一時停止対応 |
| 動画結合 | ffmpeg concatデマクサー(無劣化・高速) |
| 配布 | PyInstaller単一exe化 + _MEIPASSリソースパス解決 |
GUIアプリケーション・ブラウザ自動化・ストリーミング処理・プロセス管理という複数の領域を1つのPythonスクリプトとして統合した点が本プロジェクトの技術的な特徴です。各レイヤーを疎結合に設計し、フラグベースの状態管理とスレッドセーフなUI設計を組み合わせることで、実用的なデスクトップアプリとして完成しています。



コメント