X (Twitter) の動画・GIFアニメを保存するmacOS用Safari拡張機能
Xに投稿された動画はMP4、GIFアニメはgifskiでGIFへ変換してMacのダウンロードフォルダへ保存するSafari拡張です。GIFはXが配信するMP4から再生成するもので、アップロード時の原本GIFを復元するものではありません。
mediaDetails[].video_info.variants から最高ビットレートのMP4を選択します。同値・未指定の場合はURL中の解像度を比較します。~/Downloads/投稿者-投稿ID-メディア番号.mp4 または .gif。同名ファイルは上書きせず、拡張子の前に半角スペースと数字を付け、投稿者-投稿ID-メディア番号 2.mp4、投稿者-投稿ID-メディア番号 3.mp4 のように保存します。非公開・削除済み・閲覧制限のある投稿、Syndicationで公開されないメディア、ライブ配信/HLS、引用先の自動ダウンロード、静止画は対象外です。引用動画は引用元の投稿を開いてください。MP4の画質選択・保存先変更・履歴・再開機能も今回の対象外です。
macOS 14以降のApple Silicon搭載Mac(arm64)のみ対応しています。Intel搭載Mac(x86_64)には対応していません。生成したXcodeプロジェクトはXcode 27形式です。今回の開発・動作確認環境はmacOS 27 / Xcode 27 / Safari 27です。
Rust 1.98.1(rustup)が必要です。Xcodeのビルドフェーズとスクリプトの両方で、同じ固定版gifskiの静的ライブラリを生成します。初回はCargo依存のダウンロードが必要です。FFmpegや外部Gifski.appは不要です。
署名のTeam IDはローカル設定で管理します。初回はリポジトリのルートで次を実行し、作成した Signing.local.xcconfig の YOUR_TEAM_ID を自分のTeam IDへ置き換えてください。
cp "X Media Assist/Config/Signing.local.xcconfig.example" "X Media Assist/Config/Signing.local.xcconfig"
共有の Signing.xcconfig がこのファイルを任意読み込みし、本体・ExtensionのDebug/Releaseへ同じTeam IDを適用します。Signing.local.xcconfig はGit管理対象外です。ファイルがない場合はTeam IDが空になるため、署名付きビルドにはローカル設定またはビルド時の DEVELOPMENT_TEAM 指定が必要です。Team IDの変更はこのファイルで行ってください。XcodeのSigning & CapabilitiesでTeamを選び直すと、project.pbxproj に値が書き戻されます。
バージョン番号(MARKETING_VERSION)とbuild番号(CURRENT_PROJECT_VERSION)は共有の Signing.xcconfig で管理し、本体・ExtensionのDebug/Releaseへ共通で適用します。Signing.local.xcconfig にはTeam IDだけを記述してください。バージョン番号を変更するときは、Extension/manifest.json の version も同じ値へ更新します。
gifskiは固定コミットを参照するGit submoduleです。新規取得時は git clone --recurse-submodules https://github.com/jemielttf/X-MediaAssist-safari.git を使用するか、既存のcloneで以下の初期化コマンドを実行してください。
git submodule update --init --recursive
rustup toolchain install 1.98.1 --profile minimal
# Apple Silicon向けのターゲットを用意
rustup target add --toolchain 1.98.1 aarch64-apple-darwin
./script/build_and_run.sh
CodexのRunボタンも同じコマンドを実行します。アプリは build/DerivedData/Build/Products/Debug/X Media Assist.app に生成されます。
com.jemielttf.XMediaAssist、Extensionは com.jemielttf.XMediaAssist.Extension です。cdn.syndication.twimg.com へのアクセスを許可してください。初期MVPの検証ではアドホック署名とSafariの「未署名の機能拡張を許可」を使用しましたが、現在のスクリプトはXcodeの開発用署名設定に従います。配布用の署名・公証・App Store対応は含めていません。
Bundle Identifierを変更した後は、Safariを通常終了して起動し直し、Xのページも再読み込みしてください。Safariが旧Identifierのネイティブ接続先を保持していると、拡張が有効でも保存できないことがあります。保存開始前に接続を確認し、15秒以内に応答しない場合は再起動を案内します。
./script/build_and_run.sh --build-only # ビルドのみ
./script/build_and_run.sh --verify # ビルド・起動・プロセス確認
./script/build_and_run.sh --logs # 起動してネイティブログを表示
./script/test.sh # Node + Swiftの隔離テスト
起動中はメニューバーにX Media Assistのアイコンが表示されます。ウインドウを閉じても常駐し、アイコンのメニューから再表示・終了できます。
SMAppService.mainApp で登録・解除します。初期状態は未登録で、システム設定側での変更もメニューを開くたびに反映します。承認が必要な場合は「承認待ち」と表示し、ログイン項目のシステム設定へのメニューを表示します。ログイン時起動を使う場合は、アプリを継続利用する場所(例:/Applications)に置いてから有効にしてください。
| 場所 | 責務 |
|---|---|
Extension/content.js |
投稿の検出、保存ボタン、結果表示 |
Extension/core.js |
URL検証、Syndication解析、MP4選択、保存の順序制御 |
Extension/background.js |
メッセージ送信元確認、同時実行制御、nativeMessaging |
Extension/popup.* |
URL指定の保存UI |
AppSupport/AppPreferences.swift |
Dock表示の設定保存、ログイン項目の登録・解除 |
Native/GIFConverter.swift |
AVFoundationでフレーム抽出、libgifski C APIでGIF生成 |
Native/MediaSave.swift |
形式の分岐、GIF失敗時のMP4保持 |
Native/MediaDownload.swift |
URLSessionでストリーム保存、応答検証、上書きしない公開処理 |
X Media Assist/ |
macOS案内アプリ、Safari Web ExtensionのXcodeプロジェクト |
Tests/ |
JavaScript/Swiftの回帰テスト、ブラウザ用UI fixture |
共通CSSの正本は Docs/layout/style.css です。画面のレイアウトは Tests/localization-fixture.html で実際のHTMLとCSSを使って確認します。Extension/popup.css とアプリの Resources/Style.css はこのCSSへの相対シンボリックリンクです。管理画面の Resources/X-Media-Assist.svg も X-Media-Assist.icon/Assets/X-Media-Assist.svg への相対リンクです。リンク先はリポジトリ内で完結し、Xcodeの Copy Bundle Resources がリンクを解決して通常ファイルとしてバンドルします。CSSはリンク先の正本を編集してください。Gitのチェックアウト時はシンボリックリンクを保持してください。
SafariのnativeMessagingは同梱されたApp Extensionが受信し、そのプロセスがダウンロード・変換・保存を行います。GIF基本設定のみ、両ターゲットの App Group $(DEVELOPMENT_TEAM).com.jemielttf.XMediaAssist.shared の UserDefaults に共有します。別の常駐ヘルパーやデータベースは導入していません。案内アプリ自体にダウンロード処理を中継する構成ではありません。
Extension/_locales/{ja,en}/messages.json と browser.i18n.getMessage を使います。manifest.json の default_locale は en です。i18n.js はcore/content/backgroundより前に読み込みます。画面への挿入は textContent を使用します。Resources/Localizable.xcstrings を使い、AppLocalization.swift でメニュー文字列とWKWebView用の辞書を取得します。選択言語の翻訳テーブル全体をメインフレームのdocument開始時に渡し、単一の Base.lproj/Main.html に適用します。文言追加時は Localizable.xcstrings に日英の翻訳を追加してください。webPayload の個別キー更新は不要です。標準メニューの日本語訳は ja.lproj/Main.strings です。errorCode(HTTPエラーのみ httpStatus)、GIFフォールバック時に warningCode を追加します。従来の error / warning も残し、コードを知らない組み合わせではその文言を表示します。既知コードは拡張側の辞書で翻訳し、ファイル名やGIF設定値は変更しません。README.md / README.en.md、ライセンスの案内ページは Licenses/index.html / Licenses/index-en.html です。ライセンス本文・第三者著作権表示は原文を維持します。node --test Tests/*.test.mjs は辞書のキー・置換引数、日英の保存結果・Nativeエラー・GIFフォールバック、旧応答との互換性を確認します。Tests/localization-fixture.html と Tests/content-fixture.html はローカルHTTPサーバーで開き、?lang=ja / ?lang=en で実通信・保存なしの表示確認ができます。video.twimg.com のMP4へ再検証し、リダイレクトも同じ条件・最大3回に制限します。ftyp ヘッダーを検査してからファイル名を確定します。完全なMP4デコード検証を毎回行うわけではありません。link(2) で公開することで既存ファイルの上書きを防ぎます。通常の成功・失敗時は一時ファイルを削除します。プロセスの強制終了で残った .xma-* は、最終更新から1時間以上経過したものを接続確認(ping)時に15分以上の間隔で掃除します。保存処理中は掃除を延期し、掃除と新規保存の受付は同じロックで排他制御します。1 / maximumFrameRate 秒の固定区間ごとに、最初の採用可能なフレームを最大1枚選びます。30fps→上限20fpsでは約20fpsとなります。空区間は埋めず、補間・複製・固定fpsへのリサンプリングはしません。設定値は区間あたりの採用枚数の上限で、隣接PTSの最小間隔を示すものではありません。Shared/GIFConversionOptions.swift が許可値と既定値を管理します。基本設定は辞書 gifConversionOptions として共有UserDefaultsへ保存し、Dock設定は従来の標準UserDefaultsを維持します。SwiftPMでは共有ターゲット、Xcodeでは同じファイルを両ターゲットへ組み込みます。gifOptions をまとめて渡します。個別設定→基本設定→組み込み既定値の順で解決し、Nativeでも全項目の許可値を再検証します。ポップアップには永続設定の書き込み経路はありません。Copyright 2026 X Media Assist contributors. プロジェクト全体の配布ライセンスは AGPL-3.0-or-later です。本文は LICENSE を参照してください。第三者コードはそれぞれの著作権表示・ライセンスを維持し、Licenses/THIRD_PARTY_NOTICES.txt 、Rust標準ライブラリの表示 と Vendor/README.md に記録しています。案内アプリの「ライセンス・著作権表示」からもライセンス本文を開けます。
配布はビルド済みアプリを対象とし、自分でビルドする場合はこのリポジトリをcloneしてください。配布ページには、アプリをビルドしたコミットのリリースタグと、そのタグのソース・ビルド手順へのリンクを記載します。
リポジトリ: https://github.com/jemielttf/X-MediaAssist-safari
git clone --recurse-submodules https://github.com/jemielttf/X-MediaAssist-safari.git
cd X-MediaAssist-safari
git switch --detach <配布ページに記載されたリリースタグ>
git submodule update --init --recursive
その後、上記「起動」の手順に従ってRustツールチェーンとXcodeを用意し、Signing.local.xcconfig に自分のTeam IDを設定してビルドします。配布者の秘密鍵や証明書はリポジトリに含めません。
親リポジトリのタグがgifskiのコミットを固定し、gifski内の Cargo.lock がRust依存を固定します。ビルドには cargo build --locked を使用し、Rust自体は rust-toolchain.toml で固定します。初回は依存ソースを取得するネットワーク接続が必要です。GitHubが自動生成する「Source code」ZIP/tar.gzにはsubmoduleの実ソースが含まれないため、上記のclone手順を使用してください。
専用のソースアーカイブ作成は通常の配布手順に含めません。ライブラリ更新時は python3 script/generate_notices.py で著作権表示を再生成します。配布用署名・公証は別工程です。