🗂

Deno + FFmpeg で Twitter からシャニマスのガシャ・イベントロゴを集めたい!

2022/08/05に公開

まえがき

アイドルマスターシャイニーカラーズ はバンダイナムコエンターテインメントが運営する、「THE IDOLM@STER シリーズ」のブラウザゲームです。

このシャニマス、とにかく「イラストがすごくきれい」なのですが、ロゴもすっごくきれい なんですよね…。

そんなロゴたちを集めていつでも見られるようにしたい!

方法

ガシャ・イベントともに、開催前に Twitter へ以下のような動画が投稿されます。

https://twitter.com/imassc_official/status/1336189011870748673?s=20&t=mo2QUP7K4r4M83BtfapQDw

今回はこの動画を保存し、ロゴが表示されている部分のフレームを画像としてエクスポートしようと思います。

環境

  • macOS Monterey 12.3(Intel)
  • zsh
  • Python / Deno の環境がある
  • Twitter の開発者アカウントがある

手順

1. ツイートを集める

Twitter の Recent search API では直近 7 日間のツイートしか検索できないので、(苦肉の策として)Python 製ツールの Twint を利用しました。

pip3 install twint

Basic-usage を読みつつ、検索していきます。

# 「復刻を除くシナリオイベントかつ、動画があるツイート」を検索
twint -u imassc_official -s "シナリオイベント -復刻" --videos -o sc_event.json --json --limit 1000

# 「ガシャのアップデート情報動画」を検索
twint -u imassc_official -s "ガシャ アップデート情報" --videos -o sc_gasha.json --json --limit 1000

これを実行すると検索結果を含んだ JSON ファイルが出力されます。

が、行末にセミコロンがなく、このままでは配列として読み込めないので手直しする必要があります。

vim
:%s/}/},/g

これで OK です。

sc_gasha.json
[
  {"id": 1505063643624665093, "conversation_id": "1505063643624665093", "created_at": "2022-03-19 15:08:30 JST", "date": "2022-03-19", "time": "15:08:30", "timezone": "+0900", "user_id": 958615648799662080, "username": "imassc_official", "name": "アイドルマスター シャイニーカラーズ公式", "place": "", "tweet": "「期間限定 聞こえていますか? 冬優子・真乃スタンプガシャPlus」のアップデート情報を動画でご紹介いたしますね~  #シャニマス #idolmaster  https://t.co/VvQuag1edY", "language": "ja", "mentions": [], "urls": [], "photos": [], "replies_count": 0, "retweets_count": 2280, "likes_count": 3776, "hashtags": ["シャニマス", "idolmaster"], "cashtags": [], "link": "https://twitter.com/imassc_official/status/1505063643624665093", "retweet": false, "quote_url": "https://twitter.com/imassc_official/status/1505063015292366848", "video": 1, "thumbnail": "https://pbs.twimg.com/ext_tw_video_thumb/1505063202748780546/pu/img/Z7_2g_k9kkNiMvKj.jpg", "near": "", "geo": "", "source": "", "user_rt_id": "", "user_rt": "", "retweet_id": "", "reply_to": [], "retweet_date": "", "translate": "", "trans_src": "", "trans_dest": ""},
  {"id": 1501801978669993986, "conversation_id": "1501801978669993986", "created_at": "2022-03-10 15:07:48 JST", "date": "2022-03-10", "time": "15:07:48", "timezone": "+0900", "user_id": 958615648799662080, "username": "imassc_official", "name": "アイドルマスター シャイニーカラーズ公式", "place": "", "tweet": "「SHEER 円香・愛依スタンプガシャ」のアップデート情報を動画でご紹介いたしますね~  #シャニマス #idolmaster  https://t.co/jS913EfKD2", "language": "ja", "mentions": [], "urls": [], "photos": [], "replies_count": 1, "retweets_count": 1951, "likes_count": 3322, "hashtags": ["シャニマス", "idolmaster"], "cashtags": [], "link": "https://twitter.com/imassc_official/status/1501801978669993986", "retweet": false, "quote_url": "https://twitter.com/imassc_official/status/1501801021454102530", "video": 1, "thumbnail": "https://pbs.twimg.com/ext_tw_video_thumb/1501801566273437698/pu/img/DoqJ66rj1-kgJLyA.jpg", "near": "", "geo": "", "source": "", "user_rt_id": "", "user_rt": "", "retweet_id": "", "reply_to": [], "retweet_date": "", "translate": "", "trans_src": "", "trans_dest": ""}
  // ...
]

2. 動画を保存する

先ほどの JSON ファイルからツイートの ID を読み込み、Twitter API の statuses/lookup を叩いて動画の URL を取得。その URL にアクセスして動画を保存していきます。

今回使用したコードは以下のリポジトリに置いています。

Deno、便利 🦕

https://github.com/arrow2nd/dl-twitter-img

これで out/sc_event 以下に動画が保存されます。

deno run -A main.js sc_event.json --video

引っかかったとこ: Twitter API から動画の URL を取得する

現在提供されている Twitter API v2 では ツイートに添付された動画の URL が取得できない[1] ので、従来の Standard v1.1 を利用しました。

また、v1 ではリクエストに tweet_mode=extended を付加しないと extended_entities がレスポンスに含まれないのでお忘れなく。

以下、該当する処理のコードです。

なるべくきれいな画像が欲しいので、ビットレートが一番高い動画の URL を返すようにしています。

twitter.js
/**
 * 動画のURLを取得
 * @param {string[]} ids ツイートIDの配列
 * @returns 動画URLの配列
 */
export async function fetchVideoUrl(ids) {
  const limit = 100;

  /** @type {string[]} */
  let results = [];

  // `statuses/lookup` に一度にリクエストできるツイート数は 100 件までなので、
  // 100件ずつに分割して投げる

  for (let i = 0; i < Math.ceil(ids.length % limit); i += limit) {
    // NOTE: API v2では動画のURLが取得できないのでv1.1のエンドポイントを使用
    const endpointUrl = new URL(
      "https://api.twitter.com/1.1/statuses/lookup.json"
    );

    endpointUrl.searchParams.append("tweet_mode", "extended");
    endpointUrl.searchParams.append("id", ids.slice(i, i + limit).join(","));

    const res = await fetch(endpointUrl.toString(), {
      headers: {
        Authorization: `Bearer ${Deno.env.get("BEARER_TOKEN")}`,
      },
    });

    /** @type {TweetResponse[]} */
    const json = await res.json();

    const urls = json.map((e) => {
      const variants = e.extended_entities.media[0].video_info.variants.filter(
        (e) => typeof e.bitrate !== "undefined"
      );

      // ビットレートで降順ソート
      variants.sort((a, b) => b.bitrate - a.bitrate);

      return variants[0].url;
    });

    results = results.concat(urls);
  }

  return results;
}

3. 動画からロゴ部分のフレーム画像を抜き出す

FFmpeg を利用します。

ガシャの動画の場合

動画の冒頭 1 秒から 2 秒間程度ロゴが表示されるので、そこを画像としてエクスポートします。

参考: 期間限定 聞こえていますか? 冬優子・真乃スタンプガシャ Plus

for file in *.mp4; do
  ffmpeg -ss 1 -i "$file" -t 2 -r 1 "${file%.*}_%d.png"
done

シナリオイベントの場合

「Star n dew by me」のように最初にロゴが表示されるパターンと、「天塵」のように最後にロゴが表示される、2 パターンがあります。

この 2 つに対応するため、以下のようにしました。

for file in *.mp4; do
  # 冒頭2秒からの2秒間を切り出す
  ffmpeg -ss 2 -i "$file" -t 2 -r 1 "${file%.*}_0_%d.png"
  # 末尾4秒前からの2秒間を切り出す
  ffmpeg -sseof -4 -i "$file" -t 2 -r 1 "${file%.*}_1_%d.png"
done

4. 選別する

以上までの工程を踏むと png 画像が何枚か出力されるので、人力で選別していきます 💪

あとがき

image.png

ガシャについては一部 Twitter に動画が上がっていないものがあるようで、すべては収集できませんでしたが、満足のいく量を集めることができました。

また、シナリオイベントについてはほとんど網羅できたと思います!✌️

image.png

ここまでやった後、最近実装された「P デスク」という機能でシナリオイベントのロゴが見られることに気付いたのは内緒です。

脚注
  1. 記事執筆時点(2022/08/05)では Note that video URLs are not currently available, only static images. との記載がありました。 ↩︎

Discussion