2008/10/29


追記 最新版はgithubで作ってます。
mattn's nicodown at master — GitHub
http://github.com/mattn/nicodown/tree/master


適当だけど書いてみた。
タイトル取って来る所はlibxml使うの面倒臭かったのでXMLパーサ使わずベタで(実態参照文字あると変になるので気を付けて)。Windowsの場合だけWin32 APIでシフトJISにファイル名を変換しています。
荒いコードなので色々直し所がありますが、サンプルって事で。
//#define CURL_STATICLIB
#include <curl/curl.h>

#define HEX_DIGITS "0123456789ABCDEF"
#define IS_QUOTED(x) (*x == '%' && strchr(HEX_DIGITS, *(x+1)) && strchr(HEX_DIGITS, *(x+2)))

static char* response_data = NULL;  /* response data from server. */
static size_t response_size = 0;    /* response size of data */

static void
curl_handle_init() {
    response_data = NULL;
    response_size = 0;
}

static void
curl_handle_term() {
    if (response_data) free(response_data);
}

static size_t
curl_handle_returned_data(char* ptr, size_t size, size_t nmemb, void* stream) {
    if (!response_data)
        response_data = (char*)malloc(size*nmemb);
    else
        response_data = (char*)realloc(response_data, response_size+size*nmemb);
    if (response_data) {
        memcpy(response_data+response_size, ptr, size*nmemb);
        response_size += size*nmemb;
    }
    return size*nmemb;
}

int
main(int argc, char* argv[]) {
    CURLcode res;
    CURL* curl;
    char error[256];
    char name[256];
    char data[1024];
    char* buf = NULL;
    char* ptr = NULL;
    char* tmp = NULL;
    FILE* fp = NULL;
    int status = 0;

    // usage
    if (argc != 4) {
        fputs("usage: nicodown [usermail] [password] [video_id]", stderr);
        goto leave;
    }

    // default filename
    sprintf(name, "%s.flv", argv[3]);

    curl = curl_easy_init();
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, &error);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_handle_returned_data);
    curl_easy_setopt(curl, CURLOPT_COOKIEJAR, "cookies.jar");
    curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookies.txt");

    // login
    sprintf(data, "mail=%s&password=%s&next_url=/watch/%s", argv[1], argv[2], argv[3]);
    curl_handle_init();
    curl_easy_setopt(curl, CURLOPT_URL, "https://secure.nicovideo.jp/secure/login?site=niconico");
    curl_easy_setopt(curl, CURLOPT_POST, 1);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data);
    res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, error);
        goto leave;
    }
    buf = malloc(response_size + 1);
    strcpy(buf, response_data);
    if (strstr(buf, "id=\"login_bar\"")) {
        printf("%s\n", buf);
        free(buf);
        fprintf(stderr, "failed to login\n");
        goto leave;
    }
    free(buf);
    curl_handle_term();

    // get video url, and get filename
    sprintf(data, "http://www.nicovideo.jp/api/getthumbinfo?v=%s", argv[3]);
    curl_handle_init();
    curl_easy_setopt(curl, CURLOPT_URL, data);
    curl_easy_setopt(curl, CURLOPT_POST, 0);
    res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, error);
        goto leave;
    }
    buf = malloc(response_size + 1);
    strcpy(buf, response_data);
    ptr = strstr(buf, "<title>");
    if (ptr) {
        ptr += 7;
        tmp = strstr(ptr, "</title>");
        if (*tmp) {
            *tmp = 0;
            strcpy(name, ptr);
        }
#ifdef _WIN32
        {
            UINT codePage;
            size_t wcssize;
            wchar_t* wcsstr;
            size_t mbssize;
            char* mbsstr;

            codePage = CP_UTF8;
            wcssize = MultiByteToWideChar(codePage, 0, name, -1,  NULL, 0);
            wcsstr = (wchar_t*)malloc(sizeof(wchar_t) * (wcssize + 1));
            wcssize = MultiByteToWideChar(codePage, 0, ptr, -1, wcsstr, wcssize + 1);
            wcsstr[wcssize] = 0;
            codePage = GetACP();
            mbssize = WideCharToMultiByte(codePage, 0, (LPCWSTR)wcsstr,-1,NULL,0,NULL,NULL);
            mbsstr = (char*)malloc(mbssize+1);
            mbssize = WideCharToMultiByte(codePage, 0, (LPCWSTR)wcsstr, -1, mbsstr, mbssize, NULL, NULL);
            mbsstr[mbssize] = 0;
            sprintf(name, "%s.flv", mbsstr);
            free(mbsstr);
            free(wcsstr);
        }
#endif
    }
    free(buf);
    printf("downloading %s\n", name);
    curl_handle_term();

    // get video url
    sprintf(data, "http://www.nicovideo.jp/api/getflv?v=%s", argv[3]);
    curl_handle_init();
    curl_easy_setopt(curl, CURLOPT_URL, data);
    curl_easy_setopt(curl, CURLOPT_POST, 0);
    res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, error);
        goto leave;
    }
    buf = malloc(response_size + 1);
    strcpy(buf, response_data);
    ptr = strstr(response_data, "url=");
    if (!ptr) {
        free(buf);
        fprintf(stderr, "failed to get video info\n");
        goto leave;
    }
    tmp = strstr(ptr, "&");
    if (tmp) *tmp = 0;
    tmp = ptr;
    while(*tmp) {
        if (IS_QUOTED(tmp)) {
            char num = 0;
            sscanf(tmp+1, "%02x", &num);
            *tmp = num;
            strcpy(tmp + 1, tmp + 3);
        }
        tmp++;
    }
    strcpy(data, ptr + 4);
    printf("URL: %s\n", data);
    free(buf);
    curl_handle_term();

    // download video
    fp = fopen(name, "wb");
    if (!fp) {
        fprintf(stderr, "failed to open file\n");
        goto leave;
    }
    curl_handle_init();
    curl_easy_setopt(curl, CURLOPT_URL, data);
    curl_easy_setopt(curl, CURLOPT_POST, 0);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
    res = curl_easy_perform(curl);
    fclose(fp);
    if (res != CURLE_OK) {
        fprintf(stderr, error);
        goto leave;
    }

leave:
    curl_handle_term();
    curl_easy_cleanup(curl);

    return 0;
}
確認はWindowsでしかしてませんが、きっとUN*Xでも動くはず。perlやpythonやruby使わない派の方は雛形として持ってって下さい。

UNIXプログラミングの道具箱: プロフェッショナルが明かす研ぎ澄まされたツール群の使いこなし UNIXプログラミングの道具箱: プロフェッショナルが明かす研ぎ澄まされたツール群の使いこなし
工藤 智行
技術評論社 単行本 / ¥60 (2004年06月01日)
 
発送可能時間:

Posted at by



2008/10/28


miyagawaさんがPlaggerのFetchNicoVideoからダウンローダとして抜き出してくれました。
Tatsuhiko Miyagawa / WWW-NicoVideo-Download-0.01 - search.cpan.org
http://search.cpan.org/~miyagawa/WWW-NicoVideo-Download-0.01/
中身はMooseを使ったモダンなコード。eg/fetch-video.plにそのまま使えそうなサンプルまで入っています。
サンプルではTerm::ProgressBarで進捗表示までされて至れり尽くせり。今日はちょっとだけ修正してファイル名をタイトルから名付ける様にしてみました。
といってもWWW::NicoVideo::Downloadではloginが単体で呼び出せられる様になっていますし、user_agentが得られるようになっているのでそれを同じくmiyagawaさん作のWeb::Scraperに渡しただけです。
#!/usr/bin/perl
use strict;
use warnings;
use Encode qw(encode decode_utf8);
use URI;
use Web::Scraper;
use WWW::NicoVideo::Download;
use Term::ProgressBar;

my($email, $password, $video_id) = @ARGV;

my($term, $fh);

my $client = WWW::NicoVideo::Download->new( email => $email, password => $password );
$client->login( $video_id );

my $helper = scraper { process '#subinfo img', title => '@alt' };
$helper->user_agent( $client->user_agent );

my $title = $helper->scrape(
    URI->new("http://www.nicovideo.jp/watch/$video_id")
)->{title} || $video_id;
$title = encode("cp932", decode_utf8($title)) if $^O eq 'MSWin32';

$client->download($video_id, \&cb);

sub cb {
    my($data, $res, $proto) = @_;

    unless ($term && $fh) {
        my $ext = (split '/', $res->header('Content-Type'))[-1] || "flv";
        my $filename = "$title.$ext";
        $filename =~ s![\\/|:<>"?*]!_!g;
        open $fh, ">", $filename or die $!;
        $term = Term::ProgressBar->new( $res->header('Content-Length') );
    }

    $term->update( $term->last_update + length $data );
    print $fh $data;
}
うむ。便利便利。

そういえばWEB+DB PRESS vol47買った。codereposの記事も面白かった。

WEB+DB PRESS Vol.47 WEB+DB PRESS Vol.47
高橋 徹, 遠藤 康裕, はまちや2, 正野 勇嗣, 前坂 徹, やまだ あきら, 笠谷 真也, 大沢 和宏, 縣 俊貴, ミック, 田中 洋一郎, 有賀 一輝, 石黒 尚久, 石室 元典, 長野 雅広, 池邉 智洋, 和田 正則, 下岡 秀幸, 伊藤 直也, nanto_vi, 武者 晶紀, 大塚 知洋, 山本 陽平, 高林 哲, 小飼 弾, WEB+DB PRESS編集部
技術評論社 大型本 / ¥93 (2008年10月23日)
 
発送可能時間:


追記
otsuneさんから、API叩いた方が仕様変更に強いとコメント頂きました。XML::Simple使ってタイトル取る様に修正しました。
#!/usr/bin/perl
use strict;
use warnings;
use Encode qw(encode decode_utf8);
use XML::Simple;
use WWW::NicoVideo::Download;
use Term::ProgressBar;

my($email, $password, $video_id) = @ARGV;

my($term, $fh);

my $client = WWW::NicoVideo::Download->new( email => $email, password => $password );
$client->login( $video_id );

my $res = $client->user_agent->get("http://www.nicovideo.jp/api/getthumbinfo?v=$video_id");
my $title = $video_id;
if ($res->is_success) {
  my $xs = XML::Simple->new;
  my $ref = $xs->XMLin($res->decoded_content);
  $title = $ref->{thumb}->{title} || $video_id;
  $title = encode("cp932", decode_utf8($title)) if $^O eq 'MSWin32';
}

$client->download($video_id, \&cb);

sub cb {
    my($data, $res, $proto) = @_;

    unless ($term && $fh) {
        my $ext = (split '/', $res->header('Content-Type'))[-1] || "flv";
        my $filename = "$title.$ext";
        $filename =~ s![\\/|:<>"?*]!_!g;
        open $fh, ">", $filename or die $!;
        $term = Term::ProgressBar->new( $res->header('Content-Length') );
    }

    $term->update( $term->last_update + length $data );
    print $fh $data;
}
Posted at by




今のところ使い道見つからないけど、面白い。
Chris Grau / Export-Lexical - search.cpan.org

Export::Lexical - Lexically scoped subroutine imports

http://search.cpan.org/dist/Export-Lexical/
SYNOPSISをちょと変えて use strict;
use warnings;
package Foo;

use Export::Lexical;
sub foo :ExportLexical {
    print "foo@_\n" or 1;
}
sub bar :ExportLexical {
    print "bar@_\n" or 1;
}
1;
を使う以下のスクリプト use strict;
use warnings;
use Foo;

no Foo 'foo';

eval { foo(1); } or warn "foo1 is disabled.";
eval { bar(1); } or warn "bar1 is disabled.";

{
    use Foo 'foo';
    no Foo 'bar';

    eval { foo(2); } or warn "foo2 is disabled.";
    eval { bar(2); } or warn "bar2 is disabled.";
}

eval { foo(3); } or warn "foo3 is disabled.";
eval { bar(3); } or warn "bar3 is disabled.";
実行すると foo1 is disabled. at hoge.pl line 7.
bar1
foo2
bar2 is disabled. at hoge.pl line 15.
foo3 is disabled. at hoge.pl line 18.
bar3
こんな動きをする。レキシカルスコープでnoが使える。ただnoとは言えどメソッド自体は定義されてるからstrictでも通るし、eval無しで実行してもエラーにはならない。

面白い。でも使い道が見つからない。
Posted at by