みなさん、こんにちは。
私は普段、動画ファイルをサムネイルで一覧・タグ管理できる SceneExplorer というQtアプリを、Haiku対応とサムネイル枚数の拡張を加えたフォークとして愛用しています。
リポジトリ:https://github.com/taoman26/SceneExplorer
ただ、使っていてひとつ困っていたのが「デスクトップPCの前に座らないと動画を確認できない」ことでした。スマホでちょっと見返したいときや、リビングのPCから再生したい場面でも、わざわざSceneExplorer本体を起動できる端末の前まで移動する必要があるのが地味に不便なんですよね。
そこで、以下の2つの選択肢を考えました。
- 動画管理アプリをゼロから作り直す
- 既存のSceneExplorerに、ブラウザから使えるWebUIを後付けする
SceneExplorerはすでに「動画フォルダをスキャンしてサムネイルを10枚まで自動生成し、SQLiteデータベースに蓄積し、タグ付けもできる」というところまでしっかり作り込まれています。動画管理アプリを自作する場合、一番面倒で時間がかかるのはまさにこの部分(動画の走査・サムネイル生成・メタデータのDB化)です。
そのため、すでに完成しているアプリにWebUIだけを付け足す方が圧倒的に投資対効果が高いと判断し、今回の開発をスタートしました。
最終的な成果はこちらです。Qtアプリと直接関係しない全くの新機能なので、ブランチとして残しています。場合によっては今後、新規リポジトリを作成するかもしれません。
なお、実装作業自体はClaude Codeと対話しながら進めました。設計方針や後述の開発ルールも含め、セッションを通して一貫した方針で開発を行うことができました。
開発環境
開発環境は以下の通りです。
- OS
- Ubuntu 22.04(WSL2)
- Runtime
- Node.js 22.13+(
node:sqlite組み込みモジュールを使うため、これが最低ライン)
- Node.js 22.13+(
- Tools:
- Python 3 + Pillow(画面モックアップ生成用)
- Playwright(ヘッドレスChromiumでの実ブラウザ検証用)
- ffmpeg / cvlc(検証用のダミー動画生成と、実際の外部プレイヤー再生確認用)
プログラム自体はNode.js 22以降がインストールできるLinux環境なら動作するはずです。
なぜ「WebUIを足す」設計にしたのか
SceneExplorer本体(Qtアプリ)が管理しているデータは主に以下の3つです。
db.sqlite3― スキャンした動画のメタデータ(パス、サイズ、解像度、コーデックなど)とサムネイル画像default.scexd― フォルダ登録・タグ・再生回数などを持つドキュメントファイルthumbs/― サムネイルのJPEGファイル本体
WebUIサーバーは、この3つのデータをそのまま読みに行く別プロセスとして構築することにしました。ポイントは以下の2点です。
- Qtアプリを起動していなくても動く
WebUIサーバーは独立したNode.jsプロセスなので、SceneExplorer本体を閉じていてもブラウザから閲覧・検索・再生・タグ編集が可能です。 - 書き込みは最小限に絞る
db.sqlite3(動画のスキャン結果)は読み取り専用で開きます。書き込むのはdefault.scexdのTag/Tagged/Access(タグ情報と再生回数)だけに絞り、重い処理であるフォルダのスキャンやサムネイル生成は、引き続きQtアプリ側の仕事として残しました。
技術選定
| レイヤー | 選定技術 | 選定理由 |
| サーバー | Node.js 22 (ESM) + Express 5 | LTS世代で枯れていて安定しているため |
| DB接続 | node:sqlite(Node組み込み) | ネイティブアドオンのビルドが不要。better-sqlite3 等と違いOS依存のコンパイル問題を踏まずに済むため |
| 認証 | scryptによるパスワードハッシュ + Cookieセッション | 追加ライブラリなしでNode標準の crypto だけで完結できるため |
| クライアント | React + TypeScript + Vite + react-router | シンプルにするためプレーンCSSを採用(UIフレームワークは不使用) |
「ネイティブアドオン不要」というのは地味ながら大きなメリットで、node:sqlite のおかげで npm install だけでサクッと動くサーバーになりました。C++拡張のビルドで環境ごとに詰まる心配がないのはとても快適です。
画面を先に作ってから実装する開発フロー
いきなりコードを書き始めると手戻りが大きくなってしまうため、次の順序で進めるルールを最初に決めました。これは以前一人ハッカソンをやった際に、バイブコーディングを効率的に行う知見として得たものです。
- Python(Pillow)でダミーデータを使った画面モックアップをPNG画像として生成する
- モックアップをもとに
design.md(配色・レイアウト・操作ルール)を執筆する - データとAPIの仕様を
schema.mdにまとめる - それらに沿って実装を進める
モックアップは「ログイン」「ライブラリのグリッド表示」「動画詳細(プレイヤー+サムネイル帯)」「一覧表示」「ユーザー管理」「モバイル」の6画面を作成しました。実装後にブラウザで撮影したスクリーンショットとモックアップを見比べることで、見た目のズレをその都度スムーズに修正できました。
小さく作って、その都度実際に動かす
機能はF1〜F10の10ステップに細分化し、1ステップごとに以下の運用を徹底しました。
- 1機能だけ実装する(まとめて作らない)
- 実装したら必ず実際に動かして確認する(curlでAPIを叩く、Playwrightでヘッドレスブラウザを操作するの両方を実施)
- 「実装しました」だけで完了にしない(コマンドと実際の出力を確認してから次のステップに進む)
| ステップ | 内容 |
| F1 | サーバーの土台構築、/api/health |
| F2 | 認証機能(管理者作成/ログイン/ログアウト、レート制限、CSRF対策) |
| F3 | 読み取りAPI(フォルダ・タグ・動画一覧の検索/並び替え/ページング) |
| F4 | サムネイル配信・動画ストリーミング(Range対応)・再生回数更新 |
| F5 | クライアントの土台作成とログイン画面 |
| F6 | ライブラリ画面(グリッド・サイドバー・検索・並び替え) |
| F7 | 動画詳細画面(プレイヤー・サムネイル帯) |
| F8 | タグ編集機能 |
| F9 | 管理者用ユーザー管理機能 |
| F10 | レスポンシブ対応・README整備 |
なお、開発や検証には自分の動画ライブラリは一切使わず、Pythonスクリプトでダミーの動画ライブラリ(動画120本)を自動生成する仕組みを別途作成しました。サムネイルはPillowで描いた着色画像、動画本体はffmpegの testsrc で生成した実際に再生可能なmp4ファイルです。
これにより、UIの見た目確認から実際の再生テストまで、本物の動画データに一切触れずに完結できるようになりました。最終的に作成した72件の自動テストも、すべてこのダミーデータ上で無事に完走しています。
$ python3 webui/dev/make_dummy_library.py /tmp/dummy
dummy library: 120 videos in /tmp/dummy
$ webui/dev/run_dummy_server.sh /tmp/dummy /tmp/dummy-data
ちなみに run_dummy_server.sh は、指定されたディレクトリに生成スクリプトが作成した「目印ファイル」があるかを確認し、それがない場合(=本物のライブラリパスが渡された場合)は起動を拒否するようになっています。検証のたびに環境変数を手動で組み立てる手間を省きつつ、誤って本物のライブラリをテスト用サーバーに読み込ませない安全設計です。
実装した機能のまとめ
| 分野 | 実装内容 |
| 認証 | 初回アクセス時に管理者アカウントを作成。ロールは admin(全操作+ユーザー管理)、user(タグ編集・再生)、viewer(閲覧のみ)の3種類 |
| 閲覧 | フォルダ/タグ/タグなし/欠損ファイルによる絞り込み、ファイル名検索、並び替え、ページング |
| 再生 | HTML5 <video> によるブラウザ内再生(HTTP Range対応)、サムネイルクリックによるシーク、ダウンロード機能 |
| タグ | タグの付け外し・作成・削除(Qtアプリの Tag/Tagged テーブルと相互共有) |
| 管理 | 管理者によるユーザー追加・パスワード変更・削除・ロール変更(※最後の管理者は削除/降格できないよう保護) |
| レスポンシブ | 360px〜デスクトップまで幅広く対応(モバイル表示時はサイドバーがドロワー化) |

URLに検索条件・並び順・ページ番号が保持される設計にしているため、ページをリロードしても元の状態がしっかりと復元されます。また、LAN内で「このタグの一覧画面」をURLリンクとして手軽に共有できるのも便利です。
つまずいた点1. ドキュメントフォルダの場所がロケールで変わる問題
WebUIの初期設定では、SceneExplorerのドキュメントファイル(default.scexd)を ~/Documents/SceneExplorer/default.scexd から探すように固定していました。
しかし、日本語Linuxのデスクトップ環境では xdg-user-dirs によって ~/ドキュメント のようにフォルダ名がローカライズされるケースがあり、パスの決め打ちでは見つからないことが判明しました。
そこでSceneExplorer本体のソース(mainwindow_document.cpp)を確認してみたところ、Qtの QStandardPaths::DocumentsLocation を使用していました。これはLinux環境においては ~/.config/user-dirs.dirs 内の XDG_DOCUMENTS_DIR を参照しに行く仕様です。
そのため、WebUI(Node.js)側でも全く同じ手順でパスを解決するようロジックを修正しました。
// 1. 環境変数 XDG_DOCUMENTS_DIR を確認
// 2. なければ $XDG_CONFIG_HOME/user-dirs.dirs の XDG_DOCUMENTS_DIR("$HOME/…" 形式・引用符・エスケープに対応)を解析
// 3. どちらもなければ ~/Documents にフォールバック
実際に日本語ロケールの user-dirs.dirs(XDG_DOCUMENTS_DIR="$HOME/ドキュメント")を用意したテスト環境で検証したところ、修正前は503エラー(ライブラリが見つからない)になっていたものが、修正後は正常に動画一覧を返せるようになりました。Qtアプリと同じ挙動をさせるには、Node側でも同じ探索ロジックを丁寧に再現する必要があるという、当たり前ながらも見落としやすい学びでした。
つまずいた点2. ブラウザで再生できない動画フォーマットへの対応
SceneExplorerで扱う動画はコーデックが多岐にわたるため、hevc や特定の vp9 設定など、ブラウザの標準HTML5プレイヤーではデコードできない形式が普通に混ざってしまいます。
対策としてダウンロードボタンを用意したものの、再生のたびにファイルをダウンロードして別アプリで開くのはさすがに面倒です。「ローカルのVLCメディアプレイヤーで直接開くボタンがあれば最高では?」と考え、追加で実装することにしました。
ただ、ここで問題になるのが「セキュリティ上、ブラウザのJavaScriptからローカルのアプリを直接起動することはできない」という制約です。調査した結果、以下のような現実的な運用方法にたどり着きました。
- ボタンを押すと、その動画専用のストリームURLが1行だけ書かれた
.m3uプレイリストファイルをブラウザにダウンロードさせる - OS側で
.m3uがVLCに関連付けられていれば(Windows/macOSのVLCインストーラでは標準的です)、ダウンロード後そのまま自動でVLCが起動して再生が始まる
ここでさらに課題となったのが認証です。WebUIの全APIはログインセッション(Cookie)が必須ですが、当然ながら外部アプリであるVLCはCookieを送ってくれません。だからといって認証を完全に無効化するわけにもいかないため、動画1本・数時間限定の「再生専用ワンタイムトークン」を新設することにしました。
- トークンの発行には、ブラウザの通常ログインセッション(CSRFヘッダ付き)が必要
- 発行されたトークンは「該当動画のストリーム配信」にのみ利用可能(サムネイル取得や他の動画・APIへのアクセスは不可)
- 有効期限はデフォルトで6時間。VLC側でシークバーを動かす際に何度もリクエストが飛ぶため、期限内であれば繰り返し使用可能
$ cat downloaded.m3u
#EXTM3U
#EXTINF:-1,Birthday_Party 062.webm
http://172.30.119.83:18686/api/videos/62/stream?token=F-nbLmnmG4vvmbRzvMzHv0hwiH4gjm_************
実装後、本当に外部プレイヤーとして問題なく機能するかどうかを、ヘッドレス環境の cvlc にこのプレイリストを渡して動作確認を行いました。
$ cvlc --intf dummy --vout dummy --aout dummy --play-and-exit downloaded.m3u
[…] avcodec decoder debug: codec (h264) started
[…] main input debug: Buffering 100%
しっかりh264デコーダが起動し、バッファリング100%を経て最後まで正常に再生・終了することを確認できました!
また、発行されたトークンを使ってサムネイルAPIや別の動画ストリームにアクセスしようとすると、期待通り401エラーで弾かれることもcurlで確認済みです。

遭遇した課題と解決策
今回の開発で遭遇した課題と、それに対するアプローチをまとめました。
| No | 発生した課題 | 実施した対応 |
| 1 | 複数端末から動画を見たいが、アプリをゼロから作るのは非効率 | SceneExplorer本体には手を加えず、既存DBを参照するWebUIを後付け。Qtアプリが起動していなくても動く独立プロセスとして構築 |
| 2 | 見た目や仕様の手戻りが発生しやすい | モックアップ画像作成 → 設計書執筆 → 実装 の順序を徹底。機能は1つずつ実装してその都度実機検証 |
| 3 | テストのたびに実際の動画データを扱うのはリスクが高い | ダミー動画ライブラリを自動生成するスクリプトを作成し、UI確認から動画の再生テストまで全てダミー環境で完結できるようにした |
| 4 | OSのロケールによってドキュメントフォルダのパスが変わる | Qtアプリと同様に、XDGのユーザーディレクトリ設定(user-dirs.dirs)を読み込んで解決するロジックを実装 |
| 5 | ブラウザで再生できないコーデックの動画がある | 動画1本・数時間限定の再生専用トークン+.m3uファイルのダウンロード機能を組み合わせ、VLC等の外部プレイヤー連携を実現 |
今後について
今回の改修によって、LAN内にあるWindows PC、スマートフォン、タブレットから快適に動画を閲覧できるようになりました!
開発後、Raspberry Pi 3B+で動作を確認しました。家庭内の小規模運用ならRaspberry Pi をメディアサーバーにする運用も可能です。ただし、Raspberry Pi 3B+ で SceneExplorer のQtアプリを起動して大量の動画をスキャンすることは現実的ではないかもしれません。物凄く遅いです。
なお、以下の項目については今後の検討課題(積み残し)としています。
- HTTPS対応(現在はLAN内限定のHTTP運用)
- 一覧表示(サムネイル帯付き)のデザイン(画面モックアップのみ作成し、実装は未対応)
HTTPS対応はLAN内利用であれば現状不要ですし、動画を探す作業自体もそこまで不便を感じていないため、一覧表示の実装も一旦はこのままで十分かなと考えています。
まずはこの状態でしばらく日常的に使ってみて、また不便なところが出てきたら機能を追加して続報を書いてみたいと思います。
本日も最後までお読みいただきありがとうございました。
それでは、よい動画管理ライフを!
「自分も何か作ってみたい」と思われた方、そのアイデア、頭の中で寝かせておくのはもったいないかもしれません。
ビューローみかみでは、構想段階の壁打ちからPoC・実装・現場導入まで、現場で「使い続けられる」ものづくりを支援しています。
アイデアを短期間で「動くもの」にし、PoCで終わらせず、実運用まで伴走します。まずは「これ、作る価値ありますか?」という壁打ちからでも大歓迎です。
▶ PoC・MVP開発サービスの詳細はこちら
気になることがあればお気軽にご相談ください。



