はじめに
映像制作をしていると、MotionElementsやAudiioから1プロジェクトで数十〜数百のストック素材をダウンロードします。BGM、効果音、動画素材、テンプレートがすべて「ダウンロード」フォルダに混在し、ファイル名は ME_12345678_preview.mp4 のような意味不明な文字列。後から「あの曲なんだっけ」と探すのが本当に大変でした。
この問題を解決するために、ダウンロードをサイト別・カテゴリ別に自動振り分けするChrome拡張機能 Stockpile を作りました。
何を作ったか
Stockpileは、ストック素材サイトからのダウンロードを自動的に整理してくれる拡張機能です。
主な機能は以下の通りです。
- ダウンロードを
Stockpile/[サイト名]/[カテゴリ]/に自動振り分け - ページからタイトル、タグ、再生時間などのメタデータを自動取得
- 検索・フィルタリング可能なダウンロード履歴
- JSON/CSV形式でのエクスポート
対応サイトは以下の通りです。
- MotionElements(動画、BGM、効果音、テンプレート等)
- Audiio(BGM、効果音)
なぜ作ったか
課題
映像編集の仕事をしていると、1つのプロジェクトで数十〜数百のストック素材をダウンロードすることがあります。
- 動画素材
- BGM
- 効果音
- モーショングラフィックステンプレート
- LUTファイル
これらが全て「ダウンロード」フォルダへ一緒くたに放り込まれる。ファイル名も ME_12345678_preview.mp4 のような意味不明な名前。後から「あの曲なんだっけ」と探すのが本当に大変でした。
既存のソリューション
フォルダを手動で作って整理する方法もありますが、ダウンロードのたびに手作業でフォルダに移動するのは面倒すぎる。ダウンロードマネージャー系の拡張機能も試しましたが、ストック素材サイト特有の「カテゴリ情報」や「メタデータ」を活用した整理には対応していませんでした。
「じゃあ作るか」と。
開発で苦労した点
1. Manifest V3への対応
Chrome拡張機能のManifest V3は、V2と比べて制約が多くなっています。特にバックグラウンドページが廃止され、Service Workerに移行したことで、いくつかの設計変更が必要でした。
// manifest.json
{
"manifest_version": 3,
"background": {
"service_worker": "background/service-worker.js",
"type": "module"
}
}
Service Workerは必要なときだけ起動し、アイドル状態になると停止します。そのため、ダウンロード待ちのメタデータを一時的に保持する方法として、変数ではなく chrome.storage.local を使う必要がありました。
2. ダウンロードURLのマッチング問題
これが一番苦労した部分です。
ストック素材サイトでは、ダウンロードボタンのクリックから実際のファイル取得までに、複数のリダイレクトが発生します。
クリック → API呼び出し → 認証 → CDNリダイレクト → 実際のダウンロード
Content Scriptで取得した「クリック時のURL」と、chrome.downloads.onDeterminingFilenameの「実際のダウンロードURL」の不一致が頻発しました。
最終的に、複数のマッチング戦略をフォールバックで実装しました。
// 1. 完全一致
let metadata = pendingDownloads[downloadUrl];
// 2. 部分一致(URLの一部でマッチ)
if (!metadata) {
metadata = Object.values(pendingDownloads).find(m =>
downloadUrl.includes(m.urlPart) || m.originalUrl?.includes(downloadUrl)
);
}
// 3. ドメインマッチ + タイムスタンプ(最終手段)
if (!metadata) {
metadata = findByDomainAndTimestamp(downloadUrl);
}
3. 動的コンテンツへの対応
MotionElementsやAudiioはSPAライクな構造で、ページ遷移なしにコンテンツが切り替わります。また、ダウンロードボタンが動的に生成されることも。
MutationObserverを使って、DOMの変更を監視し、新しく追加されたダウンロードリンクを検出するようにしました。
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
const downloadLinks = node.querySelectorAll('a[href*="download"]');
downloadLinks.forEach(link => attachDownloadListener(link));
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
4. XHR/Fetchのインターセプト
一部のダウンロードは、通常のリンククリックではなく、JavaScriptで動的に発火されます。これに対応するため、XHRとFetchをラップして監視しています。
// Fetchのインターセプト
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const url = args[0]?.url || args[0];
if (isDownloadUrl(url)) {
registerPendingDownload(url);
}
return originalFetch.apply(this, args);
};
技術選定
フレームワークを使わない選択
PopupやOptionsページのUIには、ReactやVueなどのフレームワークを使わず、Vanilla JavaScriptで実装しました。
拡張機能のサイズを最小限に抑えたかったこと、UIがそこまで複雑ではないこと、ビルドプロセスなしでデバッグしたかったことが理由です。
結果的に、全体で数十KBに収まり、インストール後の動作も軽快です。
ES Modulesの活用
Service Workerでは "type": "module" を指定することで、ES Modulesが使えます。これにより、storageやdatabaseの処理を別ファイルに分離し、コードの見通しが良くなりました。
// service-worker.js
import { getSettings, savePendingDownload } from '../lib/storage.js';
import { addDownloadRecord, searchDownloads } from '../lib/database.js';
学びと気づき
Chrome拡張機能は「3つの世界」を跨ぐ
Chrome拡張機能には3つの実行コンテキストがあります。Webページ上で動くContent Script、バックグラウンドで動くService Worker、独立したページとして動くPopup/Optionsです。
これらの間のデータのやり取りは chrome.runtime.sendMessage や chrome.storage を介して行います。最初は戸惑いましたが、一度理解すると責務の分離が明確で設計しやすいと感じました。
ユーザーの行動は予測できない
「ダウンロードボタンをクリックする」という単純な行動にも、様々なパターンがあります。
- 普通にクリック
- 右クリック→「名前を付けてリンク先を保存」
- ミドルクリック
- ダウンロードマネージャー拡張機能との併用
全てのケースに対応するのは難しいですが、主要なユースケースをカバーしつつ、エッジケースでもクラッシュしないよう防御的なコードを心がけました。
i18nは最初から入れておくべき
日本語と英語の両対応を後から追加したのですが、最初からi18nを意識して実装しておけばよかったと反省。Chromeの chrome.i18n APIは使いやすいので、最初から __MSG_xxx__ 形式でテキストを定義しておくことをおすすめします。
おわりに
自分が欲しいものを自分で作る。プログラマーの特権ですね。
Stockpileを使い始めてから、ダウンロードフォルダのカオスが解消され、素材を探す手間がなくなりました。過去にダウンロードした素材も、拡張機能のポップアップから検索できるので「あの曲なんだっけ」問題も解決。
Chrome拡張機能の開発は、Web技術の知識がそのまま活かせるので、Webエンジニアには取り組みやすい領域です。Manifest V3の制約へ慣れる必要はありますが、一度コツをつかめば、ブラウザ体験を自分好みへカスタマイズできる強力なツールになります。
同じような課題を抱えている方はぜひ使ってみてください。
関連記事
Chrome拡張の開発記事をほかにも書いています。

