wgetでYouTube等から動画を落とす

Rubyに戻ってみました。selfを書かなくて済むのはいいんですが、Pythonに慣れてきたせいか、endを書くのが少し面倒になってます。後、インスタンスごとにメソッドの振る舞いを変えられるのはいいです。新しくクラスを作らなくて済みますし。

#!ruby

require 'net/http'
require 'cgi'
require 'kconv'

save_dir = 'c:/My Documents'
interval = 3

module Video
  class Error < StandardError; end
  class InvalidUrlError < Error; end

  VideoDetail = Struct.new(:id, :dl_url, :title, :ext, :encoding)

  def get_detail(url)
    @@videos.each do |video|
      begin
        return video.get_detail(url)
      rescue InvalidUrlError
        raise if video === @@videos.last
      end
    end
  end
  module_function :get_detail

  class Video

    def initialize(url_re, dl_url_re, title_re=nil, api_url=nil,
                   ext=".flv", encoding=Kconv::UTF8)
      @url_re = url_re
      @dl_url_re = dl_url_re
      @title_re = title_re
      @api_url = api_url
      @ext = ext
      @encoding = encoding
    end

    def get_detail(url)
      id = extract_video_id(url)
      if @api_url
        video_url = @api_url % id
      else
        video_url = url
      end
      content = get_content(video_url)
      params = extract_dl_url_params(content)
      dl_url = build_dl_url(params)
      title = extract_title(content)
      VideoDetail.new(id, dl_url, title, @ext, @encoding)
    end

    private

    def extract_video_id(url)
      @url_re.match(url).to_a[1] or
        raise InvalidUrlError.new('invalid video url')
    end

    def get_content(url)
      begin
        res = Net::HTTP.get_response(URI.parse(url))
        raise unless Net::HTTPSuccess === res      
        res.body
      rescue
        raise Error.new('unable to download video page')
      end
    end

    def extract_dl_url_params(content)
      @dl_url_re.match(content).to_a[1..-1] or
        raise Error.new('unable to extract download url params')
    end

    def build_dl_url(params)
      params.first
    end

    def extract_title(content)
      @title_re.match(content).to_a[1] if @title_re
    end

  end

  @@videos = []

  youtube = Video.new(
    %r!\Ahttp://(?:\w+\.)youtube\.com/watch\?v=([\w-]+)!,
    %r!watch_fullscreen\?.*?video_id=([\w-]+).*?&t=([\w-]+)!,
    %r!<title>YouTube - ([^<>]*)</title>!
  )
  def youtube.build_dl_url(params)
    "http://www.youtube.com/get_video?video_id=%s&t=%s" % params
  end
  @@videos << youtube

  veoh = Video.new(
    %r!\Ahttp://www\.veoh\.com/videos/(\w+)!,
    %r!fullPreviewHashPath="([^"]+)"!,
    %r!title="([^"]*)"\s+dateAdded=!,
    'http://www.veoh.com/rest/video/%s/details'
  )
  @@videos << veoh

  dailymotion = Video.new(
    %r!http://www.dailymotion\.com/.*?/video/([\w/-]+)!,
    %r!(http%3A%2F%2Fwww\.dailymotion\.com%2Fget%2F\d{2}%2F320x240%2Fflv%2F\d+\.flv%3Fkey%3D\w+)!,
    %r!<h1 class="nav with_uptitle">([^<>]*)</h1>!
  )
  def dailymotion.build_dl_url(params)
    CGI.unescape(params.first)
  end
  @@videos << dailymotion

  amebavision = Video.new(
    %r!http://vision\.ameba\.jp/watch\.do.*?\?movie=(\d+)!,
    %r!<imageUrlLarge>([^<>]+)</imageUrlLarge>!,
    %r!<item>\s*<title>([^<>]*)</title>!,
    "http://vision.ameba.jp/api/get/detailMovie.do?movie=%s"
  )
  def amebavision.build_dl_url(params)
    flv_url = params.first
    flv_url['//vi'] = '//vm'
    flv_url['/jpg/'] = '/flv/'
    flv_url['_4.jpg'] = '.flv'
    flv_url
  end
  @@videos << amebavision

  yourfilehost = Video.new(
    %r!http://(?:www\.)?yourfilehost\.com/media\.php\?cat=video&file=([\w.-]+)\.!,
    %r!videoembed_id=([\w%.-]+)&!
  )
  def yourfilehost.build_dl_url(params)
    CGI.unescape(params.first)
  end
  @@videos << yourfilehost
end

ARGV.each do |url|
  begin
    video = Video.get_detail(url)
  rescue  => e
    puts "Error: #{e} (#{url})"
    next
  end
  if video.title
    filename = video.title.kconv(Kconv::SJIS, video.encoding)
  else
    filename = video.id
  end
  filepath = File.join(save_dir, filename << video.ext)
  system('wget', '-O', filepath, "--referer=#{url}", video.dl_url)
  sleep interval
end

スーパークラスのメソッドを呼び出す

class SuperClass:
    def __init__(self, x):
        self.x = x

    def show(self):
        print "x:%s" % self.x

class SubClass(SuperClass):
    def __init__(self):
        SuperClass.__init__(self, 3)

c = SubClass()
c.show()

サブクラスのメソッド内で、同名のスーパークラスのメソッドを呼び出すには、スーパークラス名.メソッド名(self, 引数...)とします。

wgetでYouTubeから動画を落とす

wgetYouTube、Veoh、Dailymotion、AmebaVision、YourfilehostからFLVファイルを落とします。ただし、ログインしないと見れない動画は落とせません。

#!python
#encoding=utf-8

import os
import sys
import re
import urllib
import urllib2
import time

save_dir = r"C:\My Documents"
interval = 3

class Video:
    video_url_re = None
    api_url_format = None
    flv_url_re = None
    title_re = None
    server_encoding = "utf-8"

    def __init__(self, video_url):
        self.id = self._extract_id(video_url)
        if self.api_url_format:
            api_url = self.api_url_format % self.id
        else:
            api_url = video_url
        content = self._get_content(api_url)
        self.flv_url = self._extract_flv_url(content)
        self.title = self._extract_title(content)

    def _extract_id(self, video_url):
        match = self.video_url_re.match(video_url)
        if match:
            return match.group(1)
        else:
            raise ValueError("invalid video url")
    
    def _get_content(self, url):
        try:
            response = urllib2.urlopen(url)
            return response.read()
        except urllib2.URLError:
            raise RuntimeError("unable to download video page")
 
    def _extract_flv_url(self, content):
        match = self.flv_url_re.search(content)
        if match is None:
            raise RuntimeError("unable to extract flv url")
        return self._build_flv_url(match)

    def _build_flv_url(self, match):
        return match.group(1)

    def _extract_title(self, content):
        if self.title_re:
            match = self.title_re.search(content)
            if match:
                return match.group(1).decode(self.server_encoding, "ignore")

class YouTube(Video):
    video_url_re = re.compile(r"http://(?:\w+\.)?youtube\.com/watch\?v=([\w-]+)")
    flv_url_re = re.compile(r"watch_fullscreen\?.*?video_id=([^&]+)&.*?t=([^&]+)&")
    title_re = re.compile(r"<title>YouTube - ([^<>]*)</title>")

    def _build_flv_url(self, match):
        return "http://www.youtube.com/get_video?video_id=%s&t=%s" % match.group(1,2)

class Veoh(Video):
    video_url_re = re.compile(r"http://www\.veoh\.com/videos/(\w+)")
    api_url_format = "http://www.veoh.com/rest/video/%s/details"
    flv_url_re = re.compile(r'fullPreviewHashPath="([^"]+)"')
    title_re = re.compile(r'title="([^"]*)"\s+dateAdded=')

class Dailymotion(Video):
    video_url_re = re.compile(r"http://www.dailymotion\.com/.*?/video/([\w/-]+)")
    flv_url_re = re.compile("http%3A%2F%2F\w+\.dailymotion\.com%2Fget%2F\d{2}%2F320x240%2Fflv%2F\d+\.flv%3Fkey%3D[a-z0-9]+")
    title_re = re.compile(r'<h1 class="nav with_uptitle">([^<>]*)</h1>')

    def _build_flv_url(self, match):
        return urllib.unquote(match.group(0))

class AmevaVision(Video):
    video_url_re = re.compile(r"http://vision\.ameba\.jp/watch\.do.*?\?movie=(\d+)")
    api_url_format = "http://vision.ameba.jp/api/get/detailMovie.do?movie=%s"
    flv_url_re = re.compile(r"<imageUrlLarge>([^<>]+)</imageUrlLarge>")
    title_re = re.compile(r"<item>\s*<title>([^<>]*)</title>")

    def _build_flv_url(self, match):
        flv_url = match.group(1).replace("//vi", "//vm")
        flv_url = flv_url.replace("/jpg/", "/flv/")
        flv_url = flv_url.replace("_4.jpg", ".flv")
        return flv_url

class Yourfilehost(Video):
    video_url_re = re.compile(r"http://(?:www\.)?yourfilehost\.com/media\.php\?cat=video&file=([\w.-]+)\.")
    flv_url_re =re.compile(r"videoembed_id=([\w%.-]+)&")

    def _build_flv_url(self, match):
        return urllib.unquote(match.group(1))

klass = [YouTube, Veoh, Dailymotion, AmevaVision, Yourfilehost]

def get_video(url):
    for k in klass:
        try:
            return k(url)
        except ValueError:
            if k == klass[-1]: raise

invalid_chr_re = re.compile(u'[\/:*?"<>|]')
for url in sys.argv[1:]:
    try:
        video = get_video(url)
        filename = (video.title or video.id) + ".flv"
        filename = invalid_chr_re.sub(" ", filename)
        filepath = os.path.join(save_dir, filename)
        command = "wget -O '%s' --referer='%s' '%s'" % (filepath, url, video.flv_url)
        os.system(command)
        time.sleep(interval)
    except (ValueError, RuntimeError), e:
        print "Error: %s [%s]" % (e, url)

テキストの編集

文字列の挿入

a カーソル位置の後に文字列を追加
A 現在行の末尾に文字列を追加
i カーソル位置の前に文字列を追加
I 現在行の最初の非空白文字の前に文字列を追加
o 現在行の下に文字列を追加
O 現在行を下に1行ずらし、空いた行に文字列を追加

文字列の削除

N x カーソル位置とその後ろのN文字を削除
N X カーソル位置の前のN文字を削除
N dd N行削除
N D カーソル位置からN-1行下の行末までを削除

文字列のコピーと貼り付け

N yy N行をコピー
N p カーソル位置の後にN回貼り付け
N P カーソル位置の前にN回貼り付け
N ]p インデントを現在行に合わせ、カーソル位置の後に貼り付け
N [p インデントを現在行に合わせ、カーソル位置の前に貼り付け

文字列の変更

N r ? N文字を?に変更
R を押すまで置換モード
N cc N行を変更
N S N行を変更
N s N文字を変更
N ~ N文字を大文字/小文字を入れ替える
N g~~ N行の大文字/小文字を入れ替える
N guu N行を小文字にする
N gUU N行を大文字にする

その他

N . 直前のコマンドをN回繰り返す
N u 最後のN回分の処理を取り消す(アンドゥ)
N Ctrl-R 最後のN回分のアンドゥを取り消す
N >> N行分インデントを追加
N << N行分インデントを解除
N J 現在行を含め、N行を空白文字で連結
N gJ 現在行を含め、N行を空白文字を付けずに連結

演算コマンドと移動コマンド/テキストオブジェクト

d 削除
c 変更
y コピー
g~ 大文字/小文字を入れ替える
gu 小文字にする
gU 大文字にする

これらの演算コマンドは、移動コマンドやテキストオブジェクトと組み合わせて使うことができます。以下のコマンドのdの部分をyで置き換えればコピーに、cで置き換えれば変更になります。

N dl N文字を削除
N dd N行を削除
N d $ カーソル位置からN-1行下の行末までを削除
d 0 カーソル位置から行頭までを削除
d ^ カーソル位置から先頭の空白までを削除
N dgg 現在行からN行目までを削除
N dG 現在行からN行目までを削除(デフォルトは最後の行まで)
N dw dW N個目の単語とその後ろの空白までを削除
N de dE N個目の単語の末尾までを削除
N df? カーソル位置から右側にあるN番目の?のまでを削除
N dt? カーソル位置から右側にあるN番目の?の1文字前までを削除
diw カーソル上にある単語を削除
daw カーソル上にある単語とその後ろの空白を削除
daw カーソル上にある単語とその後ろの空白文字を削除
dib di( ( )の中の文字列削除
dab da( ( )とその中の文字列を削除
diB di{ { }の中の文字列を削除
daB da{ { }とその中の文字列を削除
di[ [ ]の中の文字列を削除
da[ [ ]とその中の文字列を削除
di" di' 引用符の中の文字列を削除
da" di' 引用符とその中の文字列を削除

カーソルの移動

上下左右にN桁移動

N l N桁右に移動
N h N桁左に移動
N k N桁上に移動
N j N桁下に移動
N gk 見た目の行でN桁上に移動
N gj 見た目の行でN桁下に移動


行を折り返す設定にしている場合、長い行は以下のように表示されます。

5 XXX Perhaps there should be a slimmed version that doesn't
  contain all those backwards compatible?

これは本当は1行ですが、見た目では2行です。

行頭・行末への移動

N $ 現在位置からN-1行下の行末に移動
0 行の先頭に移動
^ 行の先頭の非空白文字に移動
N g$ 見た目の行で現在位置からN-1行下の行末に移動
N g0 見た目の行の先頭に移動
N g^ 見た目の行の先頭の非空白文字に移動

行単位での移動

N gg N行目に移動(デフォルトは1行目)
N G N行目に移動(デフォルトは最後の行)
N % ファイルのN%の位置に移動(Nは省略不可)
H 画面上の最初の行に移動
M 画面上の真中の行に移動
L 画面上の最後の行に移動

単語単位での移動

N w N個先の単語の先頭に移動
N b N個前の単語の先頭に移動(カーソル位置が単語の先頭でなければn-1個前)
N e N個先の単語の末尾に移動(カーソル位置が単語の末尾でなければn-1個先)
N ge N個前の単語の末尾に移動
  w        2w                   2b     b     2b
 --->--------------->      <----------<--<--------    
URL encoded queries should be treated as blank strings
       --->------->--------->       <--------<-----
        e     e        2e               2ge     ge

大文字にした場合は、空白で区切られたものを単語として扱います。

       ge                w                              e
       <-               --->                           --->
This is-a line, with special/separated/words (and some more).
   <-----               --------------------->         ----->
      gE                           W                     E

これらのコマンドは行をまたいで移動します。

特定の文字への移動

N f ? 右方向にあるN個目の?に移動
N t ? 右方向にあるN個目の?の左側に移動
N F ? 左方向にあるN個目の?に移動
N T ? 左方向にあるN個目の?の右側に移動
N ; 直前のf,t,F,TをN回繰り返す
N , 直前のf,t,F,Tを逆方向にN回繰り返す
 2fe          ;               3;
----->----------------->----------------->
      3fo                               2Ft
--------------------->            <--------------          
The method may not store state in the Codec instance.
-------------------->              <-------------
     3to                                  2Tt

行をまたいでの移動はできません。また、3foの後の3;は3foを3回繰り返すのではなく、foを3回繰り返します。

対応する括弧への移動

%を使います。

   -------------------------------->
len(tuple([(1, 2), (3, 4), (5, 6)]))
          <-----------------------

スクロール

N Ctrl-E 下にN行スクロール(デフォルトは1)
N Ctrl-D 下にN行スクロール(デフォルトは画面の半分)
N Ctrl-F 下にNページ分スクロール
N Ctrl-Y 上にN行スクロール(デフォルトは1)
N Ctrl-U 上にN行スクロール(デフォルトは画面の半分)
N Ctrl-B 上にNページ文スクロール
zz カーソルが画面の真ん中に来るようにする

ニコニコから動画を落とすスクリプト改

ニコニコ動画ダウンローダ - odz bufferを参考にして、以前作ったニコニコから動画を落とすスクリプトを少しばかり修正しました。


関数を使ってまとめたので、前よりは見やすくなったと思います。複数の動画を落とせるようにしようと思ったのですが、ブラウザからアクセスした時に「短時間での連続アクセスはご遠慮ください」と言われたので止めておきました。後、22時から2時ぐらいまでダウンロードの制限がかかっているようです。FLVファイルを落とす際にhttplib.BadStatusLineという例外が発生するので調べてみたら、それが原因でした。

#!python
# encoding=utf-8

import os
import sys
import re
import urllib
import urllib2
import cookielib
import httplib
import cgi
from optparse import OptionParser
from progressbar import *

# Constants for niconico
VIDEO_URL_FORMAT = "http://www.nicovideo.jp/watch/{video_id}"
LOGIN_URL = "https://secure.nicovideo.jp/secure/login"
API_URL = "http://www.nicovideo.jp/api/getflv?v="
VIDEO_URL_RE = re.compile(r"http://www.nicovideo.jp/watch/(sm\d+)$")
VIDEO_TITLE_RE = re.compile(r'<h1><a class="video" [^<>]*>(.*)</a></h1>')

# Constants for progress bar
PBAR_WIDGETS = [Percentage(), " ", Bar(marker="=",left="[",right="]"),
           " ", ETA(), " ", FileTransferSpeed()]

COOKIE_FILE = "nico-cookie.txt"

class LoginError(Exception):
    pass

def login(mail, password):
    for i in range(2):
        if mail is None: mail = raw_input("mail: ")
        if password is None: password = raw_input("pass: ")
        query = {"mail":mail, "password":password}
        query = urllib.urlencode(query)
        try:
            response = urllib2.urlopen(LOGIN_URL, query)
            if response.headers["x-niconico-authflag"] == "1":
                return True
            else:
                mail = password = None
        except urllib2.URLError:
            pass
    return False

def get_flv_url(video_id):
    response = urllib2.urlopen(API_URL + video_id)
    try:
        content = response.read()
        flv_url = cgi.parse_qs(content)["url"][0]
        return flv_url
    except KeyError:
        if content == "closed=1&done=true":
            raise LoginError
        else:
            raise ValueError

def get_video_title(video_url):
    response = urllib2.urlopen(video_url)
    match = VIDEO_TITLE_RE.search(response.read())
    if match:
        video_title = match.group(1).decode("utf-8", "ignore")
        video_title = re.compile(u'[\/:*?"<>|]').sub(" ", video_title)
        return video_title

def download(flv_url, filepath):
    try:
        file = None
        response = urllib2.urlopen(flv_url)
        try:
            file = open(filepath, "wb")
        except IOError:
            raise RuntimeError("unable to open the file for writing")
        total = response.headers["content-length"]
        downloaded = 0
        pbar = ProgressBar(widgets=PBAR_WIDGETS, maxval=int(total)).start()
        while True:
            data = response.read(4096)
            if not data: break
            file.write(data)
            downloaded += len(data)
            pbar.update(downloaded)
        pbar.finish()
    except (urllib2.URLError, IOError):
        raise RuntimeError("unable to download video data")
    finally:
        if file: file.close()

# Create the command line options parser and parse command line
usage = "usage: %prog [options] http://www.nicovideo.jp/watch/{video_id}"
parser = OptionParser(usage)
parser.add_option("-m", dest="mail",
                  help="specify the email address")
parser.add_option("-p", dest="password",
                  metavar="PASS",
                  help="specify the password")
parser.add_option("-c", dest="cookie_file",
                  metavar="FILE",
                  help="load cookies from FILE of the Mozilla/Netscape 'cookie.txt' format"),
parser.add_option("-d", dest="save_dir",
                  metavar="PREFIX",
                  help="save a file to PREFIX/ (default current directory)")
parser.add_option("-n", dest="no_title",
                  action="store_true",
                  help="save the file under the name {ID}.flv (default {title}.flv)")

(options, args) = parser.parse_args()

# Check arguments
if not args:
    parser.print_help()
    sys.exit(1)
video_url = args[0]
match = VIDEO_URL_RE.match(video_url)
if match is None:
    sys.exit("Error: video url not in this format: %s" % VIDEO_URL_FORMAT)
video_id = match.group(1)

# Check whether the directory specified by -d option exists
if options.save_dir:
    if not os.path.isdir(options.save_dir):
        sys.exit("Error: %s is not a directory" % options.save_dir)

# Configure urllib2 to use cookies
cj = cookielib.MozillaCookieJar()
try:
    cj.load(options.cookie_file or COOKIE_FILE)
except:
    pass
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
urllib2.install_opener(opener)

# Prepare downloading video
try:
    try:
        flv_url = get_flv_url(video_id)
    except ValueError, e:
        sys.exit("Error: invalid video ID")
    except LoginError:
        # Exit if use the browser's cookies file
        if options.cookie_file:
            sys.exit("Error: not logged in")
        # Login and retry
        if login(options.mail, options.password):
            cj.save(COOKIE_FILE)
            flv_url = get_flv_url(video_id)
        else:
            sys.exit("Error: unable to login")
    video_title = get_video_title(video_url)      
except urllib2.URLError, e:
    sys.exit("Error: unable to prepare downloading a video")

# Download video
filename = video_title or video_id
if options.no_title:
    filename = video_id
filepath = os.path.join(options.save_dir or ".", filename + ".flv")
print video_title
try:
    download(flv_url, filepath)
except (IOError, RuntimeError), e:
    sys.exit("Error: %s" % e)
except httplib.BadStatusLine:
    sys.exit("Error: download seems to be limited")