Mac の FrontRowからリモコン操作で YouTubeの動画を観れないか考えてみました。

YouTube動画を表示するような FrontRowプラグインを作れればてっとりばやいんだけど、プラグインの作り方がわからないので他の方法を考えます。

~/Movieディレクトリ以下に配置した動画は FrontRowからも観られるので、YouTubeから観たい動画を自動的にダウンロードできるようにしてみましょう。

欲しい機能はこんな感じ。

  1. 検索条件に合う動画を一括ダウンロードする
  2. はてブの注目動画を定期的にダウンロードする
  3. 新着動画がすぐにわかるようにする

これらの機能はスクリプトで実現します。以下のように書けるようにします。


#(1) Baseball関連の動画を /movies/Baseball/ディレクトリに保存する
WebVideo.to '/movies/Baseball' do
search 'baseball' #baseballというキーワードにマッチする動画をダウンロードします
end

#(2) Music関連の動画を /movies/Music/ディレクトリに保存する
WebVideo.to '/movies/Music' do
search 'jpop', 10, "published" #10件まで、新着順に探してダウンロードします
search 'jazz', 10, "published" #10件まで、新着順に探してダウンロードします
end

#(3) はてブ注目動画を /movies/注目の動画/{今日の日付}ディレクトリに保存する
WebVideo.to_on_date '/movies/注目の動画' do
from 'http://b.hatena.ne.jp/video?src=youtube' #指定URLページにリンクとして含まれる動画をダウンロードします
end

(1)と(2)は検索ダウンロードの例です。(3)をcrontabなどで1日おきに実行すれば、欲しい機能の 2.と 3.を実現できます。

利用イメージ


ダウンロードした動画を FrontRowから観てみます。



注目動画は日付ごとに保存されます。



その日にダウンロードした動画が一覧表示されます。

インストール

Rubyが必要です。Macの場合、オプションパッケージから XCodeをインストールしてください。
その他に以下の gemパッケージを gem install でインストールする必要があります。

web_video.rbとサンプルをそれぞれファイルに保存します。サンプルの'ディレクトリ'と'キーワード'を書き換えて実行してください。


注意:

  • Mac OS 10.5 でのみ動作確認しています。
  • 対応する YouTubeの動画形式は mp4です。


参考にしたサイト


web_video.rb


$KCODE = 'u'

require 'rubygems'
require 'jcode'
require 'unicode'
require 'tmpdir'
require 'tempfile'
require 'open-uri'
require 'mechanize'
require 'progressbar'
require 'active_youtube'


module WebVideo
# ディレクトリ base_dir に動画をダウンロードする
def self.to base_dir, &block
VideoBox.new base_dir, base_dir, &block
end

# ディレクトリ base_dir/[今日の日付] に動画をダウンロードする
def self.to_on_date base_dir, &block
download_dir = base_dir + "/" + DOWNLOAD_DATE
VideoBox.new base_dir, download_dir, &block
end

private
TMPDIR = Dir.tmpdir
DOWNLOAD_DATE = Date.today.strftime "%Y %m %d"

module Util
def self.mkdir dir
Dir.mkdir dir unless FileTest.exist?(dir)

end

# ファイル名をエスケープする
def self.escape_filename(filename)
filename.gsub(/./) {|c| FILENAME_MAPPER[c] || c}
end

private
FILENAME_MAPPER = {"/" => "/"} #for Mac
=begin # for Windows
FILENAME_MAPPER = {}
'\\/:?"<>|'.split(//).each do |c|
FILENAME_MAPPER[c] = "_"
end
puts FILENAME_MAPPER
=end
end

class NotFound < StandardError
def initialize(message)
super(message)
end
end

class VideoBox
attr_reader :download_dir

def path video_filename
@download_dir + "/" + video_filename
end

def exist? video_filename
@download_video_list.member? Unicode.normalize_D("#{video_filename}")
end

private
def local_files file_ext
Dir[@base_dir + "/**/*." + file_ext].map do |f|
File.basename f
end.uniq
end

def initialize basedir, download_dir, &block
Util.mkdir basedir
@base_dir = basedir #重複ファイルを探すディレクト
@download_dir = download_dir #ダウンロード先ディレクト
@download_video_list = local_files("flv") + local_files("mp4")

Collector.new(self).instance_eval &block
end
end


class Collector
def from page_uri
video_uris = YoutubeVideo::search_from page_uri
download video_uris
rescue
output_failed_reason $!
end

def search *args
video_uris = YoutubeVideo::search *args
download video_uris
rescue
output_failed_reason $!
end

private
def initialize vbox
@video_box = vbox
end

def output_failed_reason exception
case exception
when OpenURI::HTTPError
puts "Network error: " + $!
when Timeout::Error
puts "Timeout."
when WebVideo::NotFound
puts "Not found: " + $!
else
if $!.kind_of? Net::HTTPResponse
STDERR.puts $!.to_s + ": " + $!.response.body
else
STDERR.puts $!
STDERR.puts $@.join("\n")
end
end
puts
end

def download_video_content video
temp = Tempfile::new('webvideo', TMPDIR)
video.download temp
puts
Util.mkdir @video_box.download_dir
temp.close false
File.rename temp.path, @video_box.path(video.filename)
rescue
temp.close true
fail
end


def download_video video_watch_uri
puts "opening #{video_watch_uri}"
video = YoutubeVideo.new video_watch_uri

if @video_box.exist? video.filename
puts "#{video.filename} already exists. Skip."
puts
return
end

puts "download movie file: #{@video_box.path(video.filename)}"
download_video_content video
puts
rescue
output_failed_reason $!
end

def download video_uris
video_uris.uniq!
for video_uri in video_uris do
download_video video_uri
end
end
end


class YoutubeVideo
private
TIMEOUT_SEC = 30
DEFAULT_MAX_TOTAL_RESULTS = 25
DEFAULT_MAX_FEED_RESULTS = 25

public
def self.search_from uri
agent = WWW::Mechanize.new
page = timeout(TIMEOUT_SEC) do
page = agent.get uri
end
video_uris = page.links.with.href(%r{^http://.+\.youtube.com/watch\?}).map {|link| link.href} +
page.links.with.href(%r{^/watch\?}).map {|link| 'http://www.youtube.com/' + link.href}
end

def self.search query, max_total_results = DEFAULT_MAX_TOTAL_RESULTS, orderby = "relevance_lang_ja"
params = {}
params[:vq] = query
params[:orderby] = orderby
params["max-results"] = DEFAULT_MAX_FEED_RESULTS

results = []
1.step(max_total_results, DEFAULT_MAX_FEED_RESULTS) do |start_index|
params["start-index"] = start_index
feed = Youtube::Video.find :first, :params => params

urls = feed.entry.map do |entry|
entry.group.player.url
end
results.concat urls
end
results
end

def initialize uri
@watch_uri = uri
@download_uri, @filename = YoutubeVideo.video_info(uri)
end
attr_reader :watch_uri, :download_uri, :filename

def download file
progress_bar = nil
open(@download_uri,
:content_length_proc => proc {|content_size|
if content_size and (0 < content_size)
progress_bar = ProgressBar.new file.path, content_size
end
},
:progress_proc => proc {|size|
progress_bar.set size if progress_bar
}) do |remote_file|
file.write remote_file.read
end
end

private
def self.video_info uri_str
agent = WWW::Mechanize.new
watch_page = timeout(TIMEOUT_SEC) do
agent.get(uri_str)
end
video_title = watch_page.title.toutf8.gsub(/\n/, '').sub(/^YouTube - ?/, '').sub(/^\s+/, '')
# filename = video_title + '.flv'
filename = video_title + '.mp4'

uri = URI.parse(uri_str)
raise NotFound.new(uri_str + ' is not found') unless /v=([^&]+)&?/ =~ uri.query
video_id = $1

agent = WWW::Mechanize.new
redirect_uri = timeout(TIMEOUT_SEC) do
agent.get("http://" + uri.host + "/watch_video?v=#{video_id}&fmt=18").uri
end
params = redirect_uri.query.scan(/([^&]+)=([^&]*)/).inject({}){|h, v| h[v[0]] = v[1]; h}
download_uri = "#{uri.scheme}://#{uri.host}/get_video?video_id=#{params['video_id']}&t=#{params['t']}&fmt=18"

return download_uri, Util.escape_filename(filename)
end
end
end

サンプル


require 'web_video'

WebVideo.to 'ディレクトリ' do
search 'キーワード'
end