Twitterの新機能である動画投稿のツイートからの動画URLをJSONから抽出し、動画をじゃぶじゃぶダウンロードする

  • 28
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Twitterの公式アプリから動画(30秒まで)がアップロードできるようになりました。

スクリーンショット 2015-02-03 1.09.17.png
https://twitter.com/n428dev/status/561814427150741505

その他サンプル
https://twitter.com/n428dev/status/564344850112192512

このツイートをJSONで取得すると以下のようになります

動画投稿情報を含むツイートのJSON

video_tweet.json
{
  "contributors": null,
  "text": "ツイッター動画投稿テスト http://t.co/y9hjEfABw1",
  "geo": null,
  "retweeted": false,
  "in_reply_to_screen_name": null,
  "possibly_sensitive": false,
  "truncated": false,
  "lang": "ja",
  "entities": {
    "symbols": [],
    "urls": [],
    "hashtags": [],
    "media": [
      {
        "sizes": {
          "thumb": {
            "w": 150,
            "resize": "crop",
            "h": 150
          },
          "small": {
            "w": 340,
            "resize": "fit",
            "h": 340
          },
          "large": {
            "w": 720,
            "resize": "fit",
            "h": 720
          },
          "medium": {
            "w": 600,
            "resize": "fit",
            "h": 600
          }
        },
        "id": 561814337585569800,
        "media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/561814337585569793/pu/img/ZyW1taT-3_AkDB-4.jpg",
        "media_url": "http://pbs.twimg.com/ext_tw_video_thumb/561814337585569793/pu/img/ZyW1taT-3_AkDB-4.jpg",
        "expanded_url": "http://twitter.com/n428dev/status/561814427150741505/video/1",
        "indices": [
          13,
          35
        ],
        "id_str": "561814337585569793",
        "type": "photo",
        "display_url": "pic.twitter.com/y9hjEfABw1",
        "url": "http://t.co/y9hjEfABw1"
      }
    ],
    "user_mentions": []
  },
  "in_reply_to_status_id_str": null,
  "id": 561814427150741500,
  "extended_entities": {
    "media": [
      {
        "sizes": {
          "thumb": {
            "w": 150,
            "resize": "crop",
            "h": 150
          },
          "small": {
            "w": 340,
            "resize": "fit",
            "h": 340
          },
          "large": {
            "w": 720,
            "resize": "fit",
            "h": 720
          },
          "medium": {
            "w": 600,
            "resize": "fit",
            "h": 600
          }
        },
        "id": 561814337585569800,
        "media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/561814337585569793/pu/img/ZyW1taT-3_AkDB-4.jpg",
        "video_info": {
          "duration_millis": 11965,
          "variants": [
            {
              "bitrate": 832000,
              "content_type": "video/webm",
              "url": "https://video.twimg.com/ext_tw_video/561814337585569793/pu/vid/480x480/QKhWzHdEK0QDPtg8.webm"
            },
            {
              "bitrate": 1280000,
              "content_type": "video/mp4",
              "url": "https://video.twimg.com/ext_tw_video/561814337585569793/pu/vid/720x720/IdBKWbELa8D8cqLD.mp4"
            },
            {
              "bitrate": 320000,
              "content_type": "video/mp4",
              "url": "https://video.twimg.com/ext_tw_video/561814337585569793/pu/vid/240x240/7EK_gSEEPFBATzXI.mp4"
            },
            {
              "bitrate": 832000,
              "content_type": "video/mp4",
              "url": "https://video.twimg.com/ext_tw_video/561814337585569793/pu/vid/480x480/QKhWzHdEK0QDPtg8.mp4"
            },
            {
              "content_type": "application/x-mpegURL",
              "url": "https://video.twimg.com/ext_tw_video/561814337585569793/pu/pl/6G6kgS5lRXAOiCcg.m3u8"
            }
          ],
          "aspect_ratio": [
            1,
            1
          ]
        },
        "media_url": "http://pbs.twimg.com/ext_tw_video_thumb/561814337585569793/pu/img/ZyW1taT-3_AkDB-4.jpg",
        "expanded_url": "http://twitter.com/n428dev/status/561814427150741505/video/1",
        "indices": [
          13,
          35
        ],
        "id_str": "561814337585569793",
        "type": "video",
        "display_url": "pic.twitter.com/y9hjEfABw1",
        "url": "http://t.co/y9hjEfABw1"
      }
    ]
  },
  "source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",
  "in_reply_to_user_id_str": null,
  "favorited": false,
  "in_reply_to_status_id": null,
  "retweet_count": 0,
  "created_at": "Sun Feb 01 09:12:52 +0000 2015",
  "in_reply_to_user_id": null,
  "favorite_count": 0,
  "id_str": "561814427150741505",
  "place": null,
  "user": {
    "location": "",
    "default_profile": true,
    "profile_background_tile": false,
    "statuses_count": 66,
    "lang": "ja",
    "profile_link_color": "0084B4",
    "id": 826661216,
    "following": false,
    "protected": false,
    "profile_location": null,
    "favourites_count": 5,
    "profile_text_color": "333333",
    "description": "428で働くプログラマー",
    "verified": false,
    "contributors_enabled": false,
    "profile_sidebar_border_color": "C0DEED",
    "name": "AKB428",
    "profile_background_color": "C0DEED",
    "created_at": "Sun Sep 16 07:06:52 +0000 2012",
    "is_translation_enabled": false,
    "default_profile_image": false,
    "followers_count": 9,
    "profile_image_url_https": "https://pbs.twimg.com/profile_images/507934202185011200/HcQGt2_r_normal.jpeg",
    "geo_enabled": false,
    "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
    "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
    "follow_request_sent": false,
    "entities": {
      "description": {
        "urls": []
      }
    },
    "url": null,
    "utc_offset": 32400,
    "time_zone": "Irkutsk",
    "notifications": false,
    "profile_use_background_image": true,
    "friends_count": 40,
    "profile_sidebar_fill_color": "DDEEF6",
    "screen_name": "n428dev",
    "id_str": "826661216",
    "profile_image_url": "http://pbs.twimg.com/profile_images/507934202185011200/HcQGt2_r_normal.jpeg",
    "listed_count": 0,
    "is_translator": false
  },
  "coordinates": null
}

JSONを見るとextended_entities:プロパティの、mediaプロパティから読んでいけば動画の情報が取得できることがわかります。

extended_entitiesプロパティの構成図

extended_entities.mediaには投稿された写真の情報も格納されているようになっています。
JSONの構造を図におこすと以下になります。

video2.png

ただし、現状はiPhoneアプリ側の仕様で1つのツイートに複数の動画を投稿できません。
また写真と動画の混合の投稿もできないため(おそらくサーバー負荷のため)しばらくは動画投稿の場合は取得すべき動画情報はextended_entities.media[0]のみになると思います。

mediaプロパティにはtypeプロパティがあり、typeがphotoの場合は写真、typeがvideoの場合はビデオ動画情報が格納されていることになります。

media.type == 'video' の時のmediaプロパティのJSON構成図

video.png

video_infoプロパティ詳細

media.type が 'video' の場合video_infoプロパティがmediaプロパティに存在します。
動画URLや動画形式はここから取得する形になります。
各プロパティの意味は以下のようなものです。

video_info プロパティ 意味
duration_millis 動画の長さをミリセカンドで表したもの
aspect_ratio 動画のアスペクト比率
variants 投稿された動画を各種フォーマットに変換した情報配列
variantsプロパティの配列の各要素 意味
bitrate 動画のビットレート
content_type 動画フォーマット
url 動画URL

(設計) video_info プロパティを言語の型に変換する

LL言語(Ruby/Python/Perlなど)であればこの情報を元にJSONをハッシュに変換するだけで話は終了だと思います。
ただし型がある言語はJSONをゴリゴリ型に変換するパーサーを書く必要があるのでマッピング表を作ります。

Javaの場合

VideoInfo Class

VideoInfo Class property type 備考
durationMillis Long
aspectEatio List <Long> IntegerのListでもいいかも
variants List <Variant> 独自で作成するクラス、後述

Variant Class

Variant Class property type 備考
bitrate Long
contentType String
url String url系のクラスにラップしてもいいかも

VideoInfo Class. Method

なくてもいいですが、便利系メソッドをVideoInfoクラスに追加します

VideoInfo Class. Method List static or class 引数 備考
fromRawJson static method String rawJsonString ツイートの生JSON文字列からVideoInfo情報をListにして返却します
fromVideoJsonNode static method JsonNode videoNode videoInfo情報をJsonNodeクラスに変換したもの引数に私VideoInfoクラスに変換します
printFormatVideoInfo class method なし VideoInfoクラスをJSON構造体っぽく整形して返します。

(実装) video_info プロパティを言語の型に変換する

Javaでパーサーを実装

Variant.java

Variant.java
package akb428.twitter.model.videoinfo;

public class Variant {

    private Long bitrate = null;
    private String contentType = null;
    private String url = null;

        //TODO publicなgetter setter メソッドは記載省略
}

VideoInfo.java

Jsonのパースにjacksonライブラリを使用しています。

VideoInfo.java
package akb428.twitter.model;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.ObjectMapper;

import akb428.twitter.model.videoinfo.Variant;

public class VideoInfo {

    private Long durationMillis = 0L;
    private List<Variant> variants = null;
    private List<Long> aspectRatio = null;

    /**
     * ツイートの生JSON文字列からVideoInfo情報をListにして返却します VideoInfoが見つからない場合はnullを返却します
     * ツイートの生JSONはTwitter4Jであれば
     * 
     * ----------------------------------------------------------------
     * ConfigurationBuilder cb = new ConfigurationBuilder();
     * cb.setJSONStoreEnabled(true); String rawJsonString =
     * DataObjectFactory.getRawJSON(Status status);
     * ----------------------------------------------------------------
     * 
     * で取得できます。
     * 
     * @param rawJsonString
     *            JSON文字列
     * @return VideoInfoのリスト。ビデオ情報がない場合はnull
     * @throws JsonProcessingException
     * @throws IOException
     */
    public static List<VideoInfo> fromRawJson(String rawJsonString) throws JsonProcessingException, IOException {
        List<VideoInfo> videoInfoList = new ArrayList<>();
        ObjectMapper mapper = new ObjectMapper();
        JsonNode rootNode = mapper.readTree(rawJsonString);

        JsonNode extentdedEntitiesNode = rootNode.get("extended_entities");
        if (extentdedEntitiesNode == null) {
            return null;
        }
        JsonNode mediaNodo = extentdedEntitiesNode.get("media");
        if (mediaNodo == null) {
            return null;
        }

        Iterator<JsonNode> mediaNodeList = mediaNodo.getElements();
        // mediaノードにはビデオ以外もあるので、動画情報のみ取得するようにする
        // video_infoがあるかどうか
        // media.type="video" かどうか
        // のいずれかで判定する
        // media: [ {type: video, video_info:{}}, {type: video, video_info:{}},
        // {}, ]
        while (mediaNodeList.hasNext()) {
            JsonNode videoNode = mediaNodeList.next().get("video_info");
            if (videoNode != null) {
                VideoInfo videoInfo = fromVideoJsonNode(videoNode);
                videoInfoList.add(videoInfo);
            }
        }

        return videoInfoList;
    }

    /**
     * videoInfo(JsonNodeクラス)をVideoInfoクラスに変換します
     * VideoInfoクラスの各フィールドに対応するJSONプロパティが存在しないときはフィールドには何も設定しません。(nullのまま)
     * 
     * @param videoNode
     * @return
     */
    public static VideoInfo fromVideoJsonNode(JsonNode videoNode) {
        VideoInfo videoInfo = new VideoInfo();
        JsonNode durationMillisNode = videoNode.get("duration_millis");

        if (durationMillisNode != null) {
            videoInfo.setDurationMillis(durationMillisNode.getLongValue());
        }

        JsonNode aspectRatioNode = videoNode.get("aspect_ratio");
        if (aspectRatioNode != null) {
            List<Long> aspectRatioList = new ArrayList<>();
            Iterator<JsonNode> aspectRatioChildNodes = aspectRatioNode.getElements();
            while (aspectRatioChildNodes.hasNext()) {
                aspectRatioList.add(aspectRatioChildNodes.next().getLongValue());
            }
            videoInfo.setAspectRatio(aspectRatioList);
        }

        JsonNode variantsNode = videoNode.get("variants");

        if (variantsNode != null) {
            List<Variant> variantList = new ArrayList<>();
            Iterator<JsonNode> variantChildNodes = variantsNode.getElements();
            while (variantChildNodes.hasNext()) {
                Variant variant = new Variant();
                JsonNode child = variantChildNodes.next();
                if (child.get("bitrate") != null) {
                    variant.setBitrate(child.get("bitrate").getLongValue());
                }
                variant.setContentType(child.get("content_type").getTextValue());
                variant.setUrl(child.get("url").getTextValue());
                variantList.add(variant);
            }
            videoInfo.setVariants(variantList);
        }

        return videoInfo;
    }

    /**
     * VideoInfoリストを整形してprintします
     */
    public void printFormatVideoInfo() {
        System.out.println("video_info");
        System.out.println("|-- duration_millis : " + this.getDurationMillis());
        System.out.println("|-- aspect_ratio : " + this.getAspectRatio());
        System.out.println("|-- variants : [ ");
        for (Variant variant : this.getVariants()) {
            System.out.println("  {");
            System.out.println("  |-- bitrate : " + variant.getBitrate());
            System.out.println("  |-- content_type : " + variant.getContentType());
            System.out.println("  |-- url : " + variant.getUrl());
            System.out.println("  }");
        }
        System.out.println("  ]");
    }

        //TODO publicなgetter setter メソッドは記載省略
}

特に難しいことはしてないですが、ジェネリクスの型省略を使っているためJava7以降でコンパイルできます。Java6以下で使用する場合はジェネリクスの型を指定してください。

VideoInfo.fromRawJsonメソッドに渡す生JSON(rawJson)はvideoInfo以外のプロパティが入っていても問題なく動作します。
コメントにもあるようにTwitter4JでDataObjectFactory.getRawJSON(Status status)を使い取得したrawJSON文字列を渡せます。

実装したパーサーを利用する例1

実装したVideoInfoクラスを簡単なmainクラスで試してみます

TwitterVideoInfoParserSample.java

TwitterVideoInfoParserSample.java
package akb428.tkws;

import java.io.IOException;
import java.util.List;

import org.codehaus.jackson.JsonProcessingException;
import akb428.twitter.model.VideoInfo;

//arg[0] = ツイッターの生JSON
public class TwitterVideoInfoParserSample {

    public static void main(String[] args) throws JsonProcessingException, IOException {

        String rawJsonString = args[0];

        List<VideoInfo> videoInfoList = VideoInfo.fromRawJson(rawJsonString);

        for (VideoInfo videoInfo : videoInfoList) {
            videoInfo.printFormatVideoInfo();
        }
    }

}

これを

java TwitterVideoInfoParserSample '{Twitterの生JSON}'

で実行すると以下のような出力がされます

outSample.txt
video_info
|-- duration_millis : 11965
|-- aspect_ratio : [1, 1]
|-- variants : [ 
  {
  |-- bitrate : 832000
  |-- content_type : video/webm
  |-- url : https://video.twimg.com/ext_tw_video/561814337585569793/pu/vid/480x480/QKhWzHdEK0QDPtg8.webm
  }
  {
  |-- bitrate : 1280000
  |-- content_type : video/mp4
  |-- url : https://video.twimg.com/ext_tw_video/561814337585569793/pu/vid/720x720/IdBKWbELa8D8cqLD.mp4
  }
  {
  |-- bitrate : 320000
  |-- content_type : video/mp4
  |-- url : https://video.twimg.com/ext_tw_video/561814337585569793/pu/vid/240x240/7EK_gSEEPFBATzXI.mp4
  }
  {
  |-- bitrate : 832000
  |-- content_type : video/mp4
  |-- url : https://video.twimg.com/ext_tw_video/561814337585569793/pu/vid/480x480/QKhWzHdEK0QDPtg8.mp4
  }
  {
  |-- bitrate : null
  |-- content_type : application/x-mpegURL
  |-- url : https://video.twimg.com/ext_tw_video/561814337585569793/pu/pl/6G6kgS5lRXAOiCcg.m3u8
  }
  ]

実装したパーサーを利用する例2 (Twitter API[twitter4j]の後続で呼び出し)

長いのでコア部分だけ掲載

GetUserTimeLineTweetSample.java
        Twitter twitter = new TwitterFactory(cb.build()).getInstance();
        twitter.setOAuthConsumer(twitterModel.getConsumerKey(), twitterModel.getConsumerSecret());
        twitter.setOAuthAccessToken(new AccessToken(twitterModel.getAccessToken(), twitterModel.getAccessToken_secret()));

        List<Status> statuses = twitter.getUserTimeline(args[0]);
        System.out.println("Showing home timeline.");
        for (Status status : statuses) {
            System.out.println(status.getUser().getName() + ":" + status.getText());
            String rawJSON = DataObjectFactory.getRawJSON(status);
            System.out.println(rawJSON);


// Twitter4Jを使用し画像URL(1枚のみ)を取得
            MediaEntity[] arrMedia = status.getMediaEntities();
            for (MediaEntity media : arrMedia) {
                System.out.println("MediaEntity= " + media.getMediaURL());
            }

// Twitter4Jを使用し画像URL(複数)を取得
            MediaEntity[] arrMediaExt = status.getExtendedMediaEntities();
            for (MediaEntity media : arrMediaExt) {
                System.out.println("ExtendedMediaEntities= " + media.getMediaURL());
            }

            URLEntity[] entity = status.getURLEntities();
            for (URLEntity urlEntity : entity) {
                System.out.println("URLEntity= " + urlEntity.getExpandedURL());
            }

// VideoInfoクラスを使用し動画URLを取得
            List<VideoInfo> videoInfoList = VideoInfo.fromRawJson(rawJSON);
            if (videoInfoList != null) {
                for (VideoInfo videoInfo : videoInfoList) {
                    videoInfo.printFormatVideoInfo();
                }
            }
        }

全体のコードは以下
https://github.com/AKB428/nico/blob/master/src/akb428/tkws/GetUserTimeLineTweetSample.java
https://github.com/AKB428/nico

独自でパーサーを用意するにせよ、Twitterライブラリで実装するにせよこれで動画URLがじゃぶじゃぶダウンロードできるようになります。

Let's Twitter Video!
https://twitter.com/n428dev/status/564332206961614848