gogakuondemand.rbをv1804_1に更新

すみません、今回はデバッグのための更新です。NHKのせいではありません…。

それから、基礎英語0のストリーミングサイトが他の番組と若干異なっております。そのため、もしかすると5月に再度更新するかもしれません。

と書いておりましたが、5月に変わったら予期していた仕掛けが上手く行かず暴発しましたorz

ということで、またいつものところ
NHK語学講座のラジオ番組ストリーミングを取得するRubyスクリプトgogakuondemand.rb(v1804_1 2018/5/5更新版) - 別館 子子子子子子(ねこのここねこ)をご覧ください。
ダウンロードもそこから行えます。
よろしくお願いします。

gogakuondemand.rbをv1804に更新

NHK大好きな[twitter:@riocampos]です。
毎度おなじみ gogakuondemand.rb を更新しました。
今回は年度変更に伴う番組変更への対応を行いました。具体的には

  • 短期集中!3か月英会話 削除
  • 基礎英語0 追加
  • 遠山顕の英会話楽習 追加

以上の3番組への対応です。
また、ダウンロード範囲を同一年度に限定しました。特に

の為の変更です。ボキャブライダーは未だに2017年度のファイルもアップされているのでダウンロード出来ますが、いちいち全ファイルの確認を行う必要も無いと思いましたのでそのように変更しました。
なお、pref.rb ファイルでの "school_year" 設定変更で2017年度分もダウンロード可能ですが、ボキャブライダー以外の番組に対してもダウンロード対象年度が限定されます。必要なときにだけ変更してください。
それから、基礎英語0のストリーミングサイトが他の番組と若干異なっております。そのため、もしかすると5月に再度更新するかもしれません。

またいつものところ
NHK語学講座のラジオ番組ストリーミングを取得するRubyスクリプトgogakuondemand.rb(v1804 2018/4/9更新版) - 別館 子子子子子子(ねこのここねこ)
をご覧ください。
ダウンロードもそこから行えます。
よろしくお願いします。

WatirでHeadless Chromeを使うまでのメモ

サーバ的環境から JavaScript が必要なブラウザアクセスする場合はいままでずっと PhantomJS を使ってきたわけですが、残念ながら PhantomJS は2017年6月ごろで開発を終了したそうです*1
ということで遅まきながら Headless Chrome を使ってみることにしましたのでメモ書きします。

更新すべきもの

  • chromedriver

これが一番のネックでした。Chrome に内蔵されていないと思ってなかったので、古いバージョンのまま使っていました。その際のエラーはこんな感じ。

browser = Watir::Browser.new(:chrome, headless: true)                                                       
Selenium::WebDriver::Error::UnknownError: unknown error: unrecognized Chrome version: HeadlessChrome/64.0.3282.186
  (Driver info: chromedriver=2.27.440174 (e97a722caafc2d3a8b807ee115bfb307f7d2cfd9),platform=Mac OS X 10.12.6 x86_64)
from /Users/riocampos/.anyenv/envs/rbenv/versions/2.3.3/lib/ruby/gems/2.3.0/gems/selenium-webdriver-3.9.0/lib/selenium/webdriver/remote/response.rb:69:in `assert_ok'

chromedriver=2.27 ってちゃんと出てますよね。気付けよ>俺。
で。chromedriver の更新を今後も継続的に行うため、今回は Homebrew でインストールすることにしました。現時点のバージョンは2.35。

$ brew info chromedriver
chromedriver: stable 2.35
Tool for automated testing of webapps across many browsers
https://sites.google.com/a/chromium.org/chromedriver/
Not installed
From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/chromedriver.rb
==> Caveats
To have launchd start chromedriver now and restart at login:
  brew services start chromedriver
Or, if you don't want/need a background service you can just run:
  chromedriver
$ brew install chromedriver
==> Downloading https://chromedriver.storage.googleapis.com/2.35/chromedriver_mac64.zip
######################################################################## 100.0%
Error: The `brew link` step did not complete successfully <==ここでエラー出ていることに気付こう…
The formula built, but is not symlinked into /usr/local
Could not symlink bin/chromedriver
Target /usr/local/bin/chromedriver
already exists. You may want to remove it:
  rm '/usr/local/bin/chromedriver'

To force the link and overwrite all conflicting files:
  brew link --overwrite chromedriver

To list all files that would be deleted:
  brew link --overwrite --dry-run chromedriver

Possible conflicting files are:
/usr/local/bin/chromedriver
==> Caveats
To have launchd start chromedriver now and restart at login:
  brew services start chromedriver
Or, if you don't want/need a background service you can just run:
  chromedriver
==> Summary
&#127866;  /usr/local/Cellar/chromedriver/2.35: 4 files, 11.3MB, built in 16 seconds

インストールは終わったので確認。

$ which chromedriver
/usr/local/bin/chromedriver
$ chromedriver
Starting ChromeDriver 2.27.440174 (e97a722caafc2d3a8b807ee115bfb307f7d2cfd9) on port 9515 <==バージョンが以前のものになっていることに気付こうよ…
Only local connections are allowed.
^C

ということで古いものを削除。

$ rm '/usr/local/bin/chromedriver'
$ brew link chromedriver
Linking /usr/local/Cellar/chromedriver/2.35... 1 symlinks created

現時点のバージョンは3.9.0。ちなみに3.6.0から「PhantomJS は deprecated」と出るようになりました。

$ gem update selenium-webdriver
Updating installed gems
Updating selenium-webdriver
Fetching: selenium-webdriver-3.9.0.gem (100%)
Successfully installed selenium-webdriver-3.9.0
Gems updated: selenium-webdriver

現時点のバージョンは6.10.3。Headless Chrome が使えるようになったのは6.6からだそうです。
インストールログを消しちゃいましたが、まあ selenium-webdriver と同様に gem update しちゃえばいいわけで。

Headless Chrome実行時の注意

Headless Chrome を先に起動した状態から Chrome も起動しようとしたのですが、起動しませんでした。どうやら先に画面ありのふつうの Chrome を起動しておかないとダメみたいですね。まあ普段は Mac の起動と同時に Chrome も立ち上げますから問題無いでしょう。

実行してみる

$ pry
[1] pry(main)> require 'watir'                                                                                             
=> true
[2] pry(main)> browser = Watir::Browser.new(:chrome, headless: true)                                                       
=> #<Watir::Browser:0x..fbfcb7aab4783eb70 url="data:," title="">
[3] pry(main)> browser.goto "www.yahoo.co.jp"                                                                              
=> "http://www.yahoo.co.jp"
[4] pry(main)> browser.screenshot.save("yahoo.jpg")                                                                       
2018-03-01 16:07:06 WARN Selenium name used for saved screenshot does not match file type. It should end with .png extension
=> #<File:yahoo.jpg (closed)>

スクリーンショットpng じゃないと怒られるようです(以前はそんなことなかったのに。
ではこの記事のサイトを撮影しましょう。

[5] pry(main)> browser.goto('http://d.hatena.ne.jp/riocampos+tech/20180301/watir_with_headless_chrome')                    
=> "http://d.hatena.ne.jp/riocampos+tech/20180301/watir_with_headless_chrome"
[6] pry(main)> browser.screenshot.save('htd.png')                                                                          
=> #<File:htd.png (closed)>

こんなスクリーンショットが撮れました。

*1:とはいえ最新の PhantomJS は http://phantomjs.org/release-2.1.3.html を見ると2018年1月リリースなんだよね。

Google検索で日付範囲指定したい場合(2016年以降)

2016年より前だと、Google検索窓の検索文字列に「daterange:YYYY-MM-DD..YYYY-MM-DD」を追記すれば良かったようなのですが、最近は使えなくなってしまいました。
いまは、例えば2016/1/1から2017/2/21までを検索したい場合だと検索URLに

&tbs=cdr%3A1%2Ccd_min%3A1%2F1%2F2016%2Ccd_max%3A2%2F21%2F2018

を追記するようにします。
で、cdr以降は何かというと

encodeURIComponent("cdr:1,cd_min:1/1/2016,cd_max:2/21/2018")

の結果なのです。日付は米国式にMM/DD/YYYYと書かなきゃいけない、ので面倒です。

これを追記するための bookmarklet を書きたいのですが、面倒になって書いてません。
だれか書いてくださいw

2019/2/19追記:書いてる人が居た。しかもこのブログ記事よりもずーっと前の記事でしたw

でもこの記事のころはまだ YYYY/MM/DD だったようです。
この bookmarklet を更新した方がいらっしゃいます。

ということで後者を使いましょう。

tbsって何だ

検索結果の日付範囲指定やソート順などを指定する要素みたいです。

  • tbs=sbd:1 →(0:古い順、1:新しい順)
  • tbs=qdr:h →(1時間以内
  • tbs=qdr:h24 →(24時間以内
  • tbs=qdr:d →(1日以内
  • tbs=qdr:w →(1週間以内
  • tbs=qdr:m →(1ヶ月以内
  • tbs=qdr:m6 →(6ヶ月以内
  • tbs=qdr:y →(1年以内更新
  • tbs=qdr:y2 →(2年以内

google検索を最新順にソートする方法 - そこに落ちてた帳面

heroku-buildpack-imagemagickの使い方

Twitter bot に画像処理を加えたいと思って Ruby MiniMagick gem を使いました。
で、デプロイしたあとに気付きました。
あ、ImageMagick を使えるように設定してない!
大急ぎでやり方を探して処理しました。

heroku-buildpack-imagemagick

とはいえ簡単な処理なのです。 heroku に buildpack を追加するだけ。

ImageMagick

ImageMagickのビルドパックは公式からは提供されていませんが、カスタムビルドパックを作ってる人がいるのでこれを利用します。

$ heroku buildpacks:add https://github.com/ello/heroku-buildpack-imagemagick

これでImageMagickのカスタムビルドパックの追加が完了です。
HerokuでImageMagickのconvertができるまで - Qiita

これですね。
ello/heroku-buildpack-imagemagick

実行

$ heroku buildpacks:add https://github.com/ello/heroku-buildpack-imagemagick
Buildpack added. Next release on twitter-bot will use:
  1. heroku/ruby
  2. https://github.com/ello/heroku-buildpack-imagemagick
Run git push heroku master to create a new release using these buildpacks.

空コミットする必要があります。

$ git commit -m "add heroku-buildpack-imagemagick" --allow-empty
[master 69186ad] add heroku-buildpack-imagemagick

ではデプロイしましょう。

$ git push heroku master
Counting objects: 1, done.
Writing objects: 100% (1/1), 196 bytes | 196.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote: 
remote: -----> Ruby app detected
remote: -----> Compiling Ruby
 :
remote: -----> Detecting rake tasks
remote: 
remote: -----> ImageMagick app detected
remote: -----> Install ImageMagick
remote: -----> Downloading ImageMagick from http://www.imagemagick.org/download/releases/ImageMagick-6.9.5-10.tar.xz
remote: --2018-02-15 13:47:21--  http://www.imagemagick.org/download/releases/ImageMagick-6.9.5-10.tar.xz
remote: Resolving www.imagemagick.org (www.imagemagick.org)... 198.72.81.86
remote: Connecting to www.imagemagick.org (www.imagemagick.org)|198.72.81.86|:80... connected.
remote: HTTP request sent, awaiting response... 200 OK
remote: Length: 8794440 (8.4M) [application/x-xz]
remote: Saving to: ‘/tmp/build_e7e5db58f15dc697460ae578ff813215/ImageMagick-6.9.5-10.tar.xz’
 :
remote: -----> Extracting ImageMagick from /tmp/build_e7e5db58f15dc697460ae578ff813215/ImageMagick-6.9.5-10.tar.xz
 :
remote: -----> Building ImageMagick
 :
remote: -----> Caching ImageMagick installation
remote: -----> Writing policy file
remote: -----> Updating environment variables
remote: -----> Discovering process types
remote:        Procfile declares types -> bot
remote: 
remote: -----> Compressing...
remote:        Done: 56.2M
remote: -----> Launching...
remote:        Released v31
remote:        https://twitter-bot.herokuapp.com/ deployed to Heroku
remote: 
remote: Verifying deploy... done.
To https://git.heroku.com/twitter-bot.git
   75f8ab8..69186ad  master -> master
[https://gist.github.com/riocampos/2e758f3d359796b93ff19356d6f76f08:title=heroku log after `heroku buildpacks:add https://github.com/ello/heroku-buildpack-imagemagick`]

ImageMagick のビルドに6分ぐらいかかった。

動くか確認

先ほどの Qiita 記事に従って、動くかどうかを確認してみましょう。

$ heroku run magick -version
Running magick -version on ⬢ colbase-bot... up, run.2716 (Free)
bash: magick: command not found

あれ?
でも ImageMagick ってだいたいは convert コマンドを使うよね?

$ heroku run convert -version
Running convert -version on &#11042; colbase-bot... up, run.2202 (Free)
Version: ImageMagick 6.9.5-10 Q16 x86_64 2018-02-15 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2016 ImageMagick Studio LLC
License: http://www.imagemagick.org/script/license.php
Features: Cipher DPC OpenMP 
Delegates (built-in): bzlib djvu fontconfig freetype jbig jng jpeg lcms lqr lzma openexr png tiff wmf x xml zlib

動いた動いた^_^

NHKラジオニュースサイトの音声ファイルのありか

最近 Amazon Echo Dot を入手して遊んでいる @riocampos です。

Amazon Echoでニュースを聞く

Echo Dot へ向かって「Alexa、ニュース」と言うと NHK ラジオニュースの最新ニュースを流し、そのあとに天気予報を喋ってくれます。便利です。
さて、そのニュース元である NHK ラジオニュースの音声ファイルはどこにあるのかを確認してみました。

音声ファイルの場所を示すファイル

https://api.nhk.or.jp/r-news/v1/newslist.js
です。中身を見てみましょう。

radionews(
  {"lastBuildDate":"Fri, 02 Feb 2018 15:00:07 +09:00",
    "news":[
      {
        "startdate":"Thu, 01 Feb 2018 19:00:03 +09:00",
        "enddate":"Thu, 01 Feb 2018 19:30:00 +09:00",
        "title":"夜7時NHKきょうのニュース",
        "soundlist":{
          "sound_normal":{"size":"14376717","type":"mp3","duration":"1797","filename":"20180201190003_16134_1_1_1"},
          "sound_fast":{"size":"8631693","type":"mp3","duration":"1078","filename":"20180201190003_16134_2_1_1"},
          "sound_slow":{"size":"17263053","type":"mp3","duration":"2157","filename":"20180201190003_16134_3_1_2"}
        }
      },
      {
        "startdate":"Thu, 01 Feb 2018 22:00:03 +09:00",
        "enddate":"Thu, 01 Feb 2018 23:10:00 +09:00",
        "title":"夜10時NHKジャーナル",
        "soundlist":{
          "sound_normal":{"size":"16788717","type":"mp3","duration":"4197","filename":"20180201220003_16379_1_1_2"},
          "sound_fast":{"size":"20152269","type":"mp3","duration":"2518","filename":"20180201220003_16379_2_1_1"},
          "sound_slow":{"size":"20152125","type":"mp3","duration":"5037","filename":"20180201220003_16379_3_1_3"}
        }
      },
      {
        "startdate":"Fri, 02 Feb 2018 07:00:03 +09:00",
        "enddate":"Fri, 02 Feb 2018 07:20:00 +09:00",
        "title":"朝7時NHKけさのニュース",
        "soundlist":{
          "sound_normal":{"size":"9577485","type":"mp3","duration":"1197","filename":"20180202070003_15388_1_1_1"},
          "sound_fast":{"size":"5752269","type":"mp3","duration":"718","filename":"20180202070003_15388_2_1_2"},
          "sound_slow":{"size":"11504205","type":"mp3","duration":"1437","filename":"20180202070003_15388_3_1_3"}
        }
      },
      {
        "startdate":"Fri, 02 Feb 2018 08:00:03 +09:00",
        "enddate":"Fri, 02 Feb 2018 08:05:00 +09:00",
        "title":"午前8時のNHKニュース",
        "soundlist":{
          "sound_normal":{"size":"2376621","type":"mp3","duration":"297","filename":"20180202080003_13346_1_1_1"},
          "sound_fast":{"size":"1431693","type":"mp3","duration":"178","filename":"20180202080003_13346_2_1_2"},
          "sound_slow":{"size":"2863341","type":"mp3","duration":"357","filename":"20180202080003_13346_3_1_3"}
        }
      },
      {
        "startdate":"Fri, 02 Feb 2018 12:00:03 +09:00",
        "enddate":"Fri, 02 Feb 2018 12:15:00 +09:00",
        "title":"正午のNHKニュース",
        "soundlist":{
          "sound_normal":{"size":"7177293","type":"mp3","duration":"897","filename":"20180202120003_15633_1_1_1"},
          "sound_fast":{"size":"4312269","type":"mp3","duration":"538","filename":"20180202120003_15633_2_1_1"},
          "sound_slow":{"size":"8623917","type":"mp3","duration":"1077","filename":"20180202120003_15633_3_1_2"}
        }
      }
    ]
  }
);

ニュースのスピードは「ふつう・ゆっくり・はやい」の三種類から選べるので、音声ファイルもそれぞれ3種類あります。
例えば「正午のNHKニュース」のふつうスピードの音声ファイルへのリンクは
http://www.nhk.or.jp/r-news/ondemand/mp3/20180202120003_15633_1_1_1.mp3?201802025155441
のような形式になっています。"正午のNHKニュース"のところの"filename"要素は"20180202120003_15633_1_1_1"になっているので、つまり

http://www.nhk.or.jp/r-news/ondemand/mp3/

に目的の"filename"要素+".mp3"としてしまえば、音声ファイルの URL が得られますね。
なお、この URL の"?"以降ですが、どうやらアクセスした日時 YYYY/MM/DD HH:MM:SS に対して

?YYYYMMDD5HHMMSS

となっているようです。他の日にセパレータ"5"が変わっていないかどうかをチェックしたいと思います*1

Rubyで音声ファイルURLを取り出す

中身はほぼ JSON なのですが、JSON じゃなく JavaScript ファイルになっています。っていうか JSONP って言うんでしたっけ、よく分かりません。
このため、そのままでは JavaScript 以外だと取り扱いづらくなっています。JSON にしてしまいましょう。
いつものように Ruby で取り扱います。
JavaScript 的には radionews 関数として読めるようになっているので、その内側の部分を取得するようにしてしまえば JSONになりますね。

> require 'json'
=> true
> require 'open-uri'
=> true
> js = open('https://api.nhk.or.jp/r-news/v1/newslist.js').read;

> json = js[/radionews\(([^)]+)\)/, 1];

> hash = JSON.parse(json)
=> {"lastBuildDate"=>"Fri, 02 Feb 2018 15:00:07 +09:00",
 "news"=>
  [{"startdate"=>"Thu, 01 Feb 2018 19:00:03 +09:00",
    "enddate"=>"Thu, 01 Feb 2018 19:30:00 +09:00",
    "title"=>"夜7時NHKきょうのニュース",
    "soundlist"=>
     {"sound_normal"=>{"size"=>"14376717", "type"=>"mp3", "duration"=>"1797", "filename"=>"20180201190003_16134_1_1_1"},
      "sound_fast"=>{"size"=>"8631693", "type"=>"mp3", "duration"=>"1078", "filename"=>"20180201190003_16134_2_1_1"},
      "sound_slow"=>{"size"=>"17263053", "type"=>"mp3", "duration"=>"2157", "filename"=>"20180201190003_16134_3_1_2"}}},
   {"startdate"=>"Thu, 01 Feb 2018 22:00:03 +09:00",
    "enddate"=>"Thu, 01 Feb 2018 23:10:00 +09:00",
    "title"=>"夜10時NHKジャーナル",
    "soundlist"=>
     {"sound_normal"=>{"size"=>"16788717", "type"=>"mp3", "duration"=>"4197", "filename"=>"20180201220003_16379_1_1_2"},
      "sound_fast"=>{"size"=>"20152269", "type"=>"mp3", "duration"=>"2518", "filename"=>"20180201220003_16379_2_1_1"},
      "sound_slow"=>{"size"=>"20152125", "type"=>"mp3", "duration"=>"5037", "filename"=>"20180201220003_16379_3_1_3"}}},
   {"startdate"=>"Fri, 02 Feb 2018 07:00:03 +09:00",
    "enddate"=>"Fri, 02 Feb 2018 07:20:00 +09:00",
    "title"=>"朝7時NHKけさのニュース",
    "soundlist"=>
     {"sound_normal"=>{"size"=>"9577485", "type"=>"mp3", "duration"=>"1197", "filename"=>"20180202070003_15388_1_1_1"},
      "sound_fast"=>{"size"=>"5752269", "type"=>"mp3", "duration"=>"718", "filename"=>"20180202070003_15388_2_1_2"},
      "sound_slow"=>{"size"=>"11504205", "type"=>"mp3", "duration"=>"1437", "filename"=>"20180202070003_15388_3_1_3"}}},
   {"startdate"=>"Fri, 02 Feb 2018 08:00:03 +09:00",
    "enddate"=>"Fri, 02 Feb 2018 08:05:00 +09:00",
    "title"=>"午前8時のNHKニュース",
    "soundlist"=>
     {"sound_normal"=>{"size"=>"2376621", "type"=>"mp3", "duration"=>"297", "filename"=>"20180202080003_13346_1_1_1"},
      "sound_fast"=>{"size"=>"1431693", "type"=>"mp3", "duration"=>"178", "filename"=>"20180202080003_13346_2_1_2"},
      "sound_slow"=>{"size"=>"2863341", "type"=>"mp3", "duration"=>"357", "filename"=>"20180202080003_13346_3_1_3"}}},
   {"startdate"=>"Fri, 02 Feb 2018 12:00:03 +09:00",
    "enddate"=>"Fri, 02 Feb 2018 12:15:00 +09:00",
    "title"=>"正午のNHKニュース",
    "soundlist"=>
     {"sound_normal"=>{"size"=>"7177293", "type"=>"mp3", "duration"=>"897", "filename"=>"20180202120003_15633_1_1_1"},
      "sound_fast"=>{"size"=>"4312269", "type"=>"mp3", "duration"=>"538", "filename"=>"20180202120003_15633_2_1_1"},
      "sound_slow"=>{"size"=>"8623917", "type"=>"mp3", "duration"=>"1077", "filename"=>"20180202120003_15633_3_1_2"}}}]}

目的の hash が得られました。

正午のニュースの音声ファイルURLを取得

まずは正午のニュースのハッシュ要素 noon を得ましょう。

> noon = hash["news"].find { |item| item["title"]["正午"] }                                                          
=> {"startdate"=>"Fri, 02 Feb 2018 12:00:03 +09:00",
 "enddate"=>"Fri, 02 Feb 2018 12:15:00 +09:00",
 "title"=>"正午のNHKニュース",
 "soundlist"=>
  {"sound_normal"=>{"size"=>"7177293", "type"=>"mp3", "duration"=>"897", "filename"=>"20180202120003_15633_1_1_1"},
   "sound_fast"=>{"size"=>"4312269", "type"=>"mp3", "duration"=>"538", "filename"=>"20180202120003_15633_2_1_1"},
   "sound_slow"=>{"size"=>"8623917", "type"=>"mp3", "duration"=>"1077", "filename"=>"20180202120003_15633_3_1_2"}}}

定時ニュースは5分間ですが、けさのニュースは20分、正午のニュースは15分、(夜7時の)きょうのニュースは30分、NHK ジャーナルは10分間なので、これらのニュースを聞くことが多いかと思います。それぞれ"けさ"、"正午"、"きょう"、"ジャーナル"を Array#find のピックアップ要素にしてやれば抜き出せますね。

続いて通常スピードの音声ファイルの"filename"要素を得ましょう。

> normal_speed_file = noon["soundlist"]["sound_normal"]["filename"]                                          
=> "20180202120003_15633_1_1_1"

では音声ファイルのURLを出してみましょう。

> base_url = "http://www.nhk.or.jp/r-news/ondemand/mp3/";
> mp3 = ".mp3";
> url = base_url + normal_speed_file + mp3                                                                   
=> "http://www.nhk.or.jp/r-news/ondemand/mp3/20180202120003_15633_1_1_1.mp3"

これで目的の URL が得られました。めでたしめでたし。

*1:2/2は5、2/4は0

Web上の画像を付けてツイート(1つ・複数)Twitter gemバージョン6.2.0以降の場合

Twitter gem バージョン6.2.0は去る2017年の11/8リリースなのですが、昨日までこの変更に気付いてませんでした(ずっと6.1.0以前を使ってたのさ…)。
さて、以前の記事(Web上の画像を付けてツイート(1つ・複数) - 別館 子子子子子子(ねこのここねこ))で

Twitter::REST::Tweets#update_with_media が楽だと思ったのだが、 API 側が deprecated になった

と書いたのですが、バージョン6.2.0で Twitter::REST::Tweets#uploadが private に変更された(正確には Twitter::REST::Media#upload から Twitter::REST::Tweets#uploadupload メソッドを引っ越して更に private に変更した)ため以前の書き方は使えなくなり*1Twitter gem を使って画像などのメディアをアップするには、API 側に関係なく Twitter::REST::Tweets#update_with_media を使わねばならぬようになりました…。ですので、バージョン6.2.0向けに書き換える必要があります。ぷんすか。

書き方

画像が一つの場合

この場合は img_url 先が画像ではなく動画であってもいけますね、多分。

gem 'twitter', '>= 6.2.0'
require 'twitter'
require 'open-uri'

img = open(img_url)
client.update_with_media(text, img)
画像が複数の場合(例は4画像)
gem 'twitter', '>= 6.2.0'
require 'twitter'
require 'open-uri'

img_urls = [img_url1, img_url2, img_url3, img_url4]
imgs = img_urls.map { |img_url| open(img_url) }
client.update_with_media(text, imgs)
実用例

以前の記事と同様に url_exist? メソッドを使って、画像 URL 先にファイルが本当に存在するか確認しています。また img_urls 配列の中身が5つ以上であった場合には冒頭4つに限定しておきます。

gem 'twitter', '>= 6.2.0'
require 'twitter'
require 'open-uri'

img_urls = [img_url1, img_url2, img_url3,...]
imgs = img_urls[0, 4].map { |img_url| url_exist?(img_url) ? open(img_url) : nil }.compact
client.update_with_media(text, imgs)

twitter_rescue do ブロックを作ってあげれば、より安心です。そして以前の記事の如くメソッドにしてやればラクになりますね。

Twitter::REST::Tweets#update_with_mediaの引数mediaについて

バージョン6.1.0まで
  • media (File, Hash) — A File object with your picture (PNG, JPEG or GIF)

Method: Twitter::REST::Tweets#update_with_media — Documentation for twitter (6.1.0)

画像が1つしか添付出来ない時代の API 向け。なので配列を渡すと

The IO object for media must respond to to_io (Twitter::Error::UnacceptableIO)

と怒られます。

バージョン6.2.0から
  • media (File, Array) — An image file or array of image files (PNG, JPEG or GIF).

Method: Twitter::REST::Tweets#update_with_media — Documentation for twitter (6.2.0)

こちらは配列を渡しても、ちゃんとその中身が IO オブジェクトであるかどうか確認してくれます。

[DEPRECATED] :mime_type option deprecated, use :content_typeと出るのは?

Twitter gem はバージョン6.2.0から http-form_data gem を使っているのですが、この gem のバージョン2.0.0から :mime_type キーが :content_type キーへと変更されました。Twitter gem の private メソッドである Twitter::REST::Request#merge_multipart_file! メソッドで、 HTTP::FormData::File.new のオプションに :mime_type キーが利用されているために
http-form_data gem が [DEPRECATED] を出力しています。
Twitter::REST::Request#merge_multipart_file! メソッドに関する修正ブランチはまだメインブランチに取り込まれていませんが、いずれ取り込まれるのではないでしょうか。

Looks like this option was deprecated here in v1.0.2 of https://github.com/httprb/form_data. The changelog entry is https://github.com/httprb/form_data/commit/5b902fab8d5b6493a400b2f82ac748d8ec0f25d3:title=here].
The changes to this gem would need to be made here.
DEPRECATED :mime_type option deprecated, use :content_type · Issue #881 · sferik/twitter

それでもエラーが出る場合は…もしかして画像ファイルサイズが小さいのかも

画像ファイルサイズが小さいと、Ruby は Tempfile にせずにオンメモリで StringIO のまま処理しようとし、そのために Twitter gem との衝突が生じるようです(バージョン6.1.0以前のときに「Tempfile が保存出来ない」とのエラーが出ていたのはこれが原因だったのかもしれないけど、よくわからない)。

media_url の画像ファイルのサイズが10kb以下と小さい場合に、表題の例外エラーが発生します。
この理由は、ruby の open-uri の open メソッドが、対象ファイルが10kb以下の場合は Tempfile ではなくて StringIO のオブジェクトを返し、gem twitterの update_with_media が to_io メソッドを持たない StringIO オブジェクトを受け付けないため。
twitter gemのupdate_with_mediaで”The IO object for media must respond to to_io”エラー | EasyRamble

対策としては、上に引用した blog や以下の blog のように、StringIO オブジェクトから Tempfile オブジェクトを作ってやることが必要になります。ちょっと面倒ですね。(以下では一旦 open-uri を使ってるけど、上記 blog に類するように Tempfile.open([File.basename(img_url), File.extname(img_url)]) としてに全ての画像 URL 先ファイルを Tempfile にしてしまうやり方でもいいんじゃないかと思ったり。)

  • Solution

... This will handle both the normal and StringIO cases for you by converting StringIO’s to a File.

# lib/twitter/image.rb
module Twitter::Image
  # The Twitter gem is particular about the type of IO object it
  #   recieves when tweeting an image. If an image is < 10kb, Ruby opens it as a
  #   StringIO object. Which is not supported by the Twitter gem/api.
  #
  #   This method ensures we always have a valid IO object for Twitter.
  def self.open_from_url(image_url)
    image_file = open(image_url)
    return image_file unless image_file.is_a?(StringIO)

    file_name = File.basename(image_url)

    temp_file = Tempfile.new(file_name)
    temp_file.binmode
    temp_file.write(image_file.read)
    temp_file.close

    open(temp_file.path)
  end
end

Now, when you’re tweeting an image. You can do this.

image = Twitter::Image.open_from_url(image_url)
twitter_client.update_with_media("Tweet tweet", image)

The IO object for media must respond to to_io

上に引用したblogでは、もう一つの対策として、定数 OpenURI::Buffer::StringMax を強引に書き換える方法も示してあります。open-uri を多用しない*2のであれば、これもアリじゃないかと思います。

OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax')
OpenURI::Buffer.const_set 'StringMax', 0

twitter gemのupdate_with_mediaで”The IO object for media must respond to to_io”エラー | EasyRamble

*1:なぜ後方互換性を潰した…

*2:全て Tempfile を作ることになるので、画像ファイルが小さく且つ open-uri を多用している場合には、速度が落ちるおそれがある。