#!/cygdrive/c/Perl/bin/perl
#
# nicoStat.pl - ニコニコ動画に投稿した動画の再生数、コメント数、
#               マイリスト数、タイトルをcsv形式で出力する。
#
# [実行方法]
#    (Windowsでの実行方法)
#
#    NICOSTAT+DIFF.BATまたはNICOSTAT.BATをダブルクリックして下さい。
#    するとログインがまだならメールアドレスやパスワード入力を行います。
#    その後投稿動画を解析し、Statディレクトリの下にcsv形式で
#    再生数やコメント数などのデータを保存します。
#    NICOSTAT.BATを起動した場合はここで終了です。
#    NICOSTAT+DIFF.BATを起動した場合は前回実行した時点の再生回数等との
#    差を表示します。
#
#    (cygwinなどコマンドプロンプトでの実行方法)
#
#    事前に以下のモジュールを準備して下さい。
#      LWP::UserAgent, HTTP::Cookies, HTTP::Request, HTTP::Headers, 
#      CGI, YAML::Syck, Unicode::Japanese, Time::Piece
#
#    インストール方法は以下のような具合で行えます。
#      > perl -MCPAN -e 'install YAML::Syck'
#
#    もしどうしてもモジュールインストールがうまくいかない場合は
#    以下をお試し下さい。
#      > ./nicoStat.csh
#    （作者のPC(WinXP+cygwin)でインストールしたモジュールを読み込んで実行します)
#
#    実行の際は以下のようにして下さい。
#
#      > ./nicoStat.pl
#
#    するとログインがまだならメールアドレスやパスワード入力を行います。
#    その後投稿動画を解析し、Statディレクトリの下にcsv形式で
#    再生数やコメント数などのデータを保存します。
#
#    データを保存した後で、前回実行時点の再生回数等との差分を表示させたい場合は
#    以下のように実行して下さい。
#
#      > ./nicoStat.pl -d
#
#    -d を付けると nicoStat.pl 実行後に diff.pl を実行するようになります。
#    diff.pl とは指定した２つのcsvファイルの差を表示するスクリプトです。
#
# [主な動きなど]
#    - 実行すると「ニコニコ動画(9)」の投稿動画ページを取得し、
#      csv形式にしてStatディレクトリ下にファイル保存します。
#
#    - 結果ファイル名には「stat_年-月-日_時刻.csv」のように実行日時が付きます。
#      同時刻に複数回実行した場合は時刻の後に通し番号が追記されます。
#
#    - ログインしたらクッキーが .cookies.txtに保存され、
#      次回実行時に利用されます。
#
#    - .nicorcというファイルをテキストエディタで作成し、以下のように
#      記述すればメールアドレスやパスワードを自動入力できます。
#
#      mail: foo@example.com
#      password: hogehoge
#
#      ※メールアドレスの行のみ記述してもOKです。
#        その場合はメールアドレス入力のみ省略されます。
#
# [使用上の注意]
#    本スクリプトは2010/6月17日のニコニコ動画(9)での投稿動画ページ、
#    ログインページに対応しています。これらのページ構成が変更された場合
#    うまく実行できなくなる可能性があります。
#
#    本スクリプトはutf-8で保存して下さい。
#    また文字は sjisで出力されます。sjis以外にする場合は
#    本ソースをsjisで検索して適宜書き換えて下さい。
#
# [ライセンス]
#    cygwin1.dllなどを再配布していますので、本スクリプトはcygwinのライセンスと
#    同じGPLライセンスとしてお使い下さい。
#    cygwinのライセンスに関する説明は以下にあります。
#
#      http://cygwin.com/licensing.html
#
# [参考]
#    以下のページのPerlスクリプトを参考にしました。ありがとうございます。
#    404 Blog Not Found - 小飼 弾さん
#    http://blog.livedoor.jp/dankogai/archives/50885358.html
#

# 変更履歴
# ver.0.19
#  - (追加) csvのF 列に音DL数を保存するようにした。
#  - (修正) -iオプションを指定したときのバグを修正した。
# ver.0.18
#  - (変更) ニコニコ動画のマイページHTML変更('10/10/29)追加対応した。
#  - (変更) データ抽出する規則（正規表現）を.nicorcから読み込むようにした。
#  - (追加) 以下のオプションを追加した（主にデバッグ用）。
#           -verbose ... 途中経過を表示
#           -i file  ... 入力ファイルを指定
#           -o file  ... 出力ファイルを指定(stdoutとすると標準出力へ)
# ver.0.17
#  - (変更) ニコニコ動画のマイページHTML変更(多分'10/06/17)追加対応した。
#           今回の対応で、マイページが複数ページの場合に対応した。
# ver.0.16
#  - (変更) ニコニコ動画のマイページHTML変更('10/06/17)に対応した。
# ver.0.15
#  - (変更) ニコニコ動画のマイページHTML変更('10/06/01)に対応した。
#  - (追加) 最後に出力ファイル名を表示するようにした。
# ver.0.14
#  - (変更) activeperlのexe化向きにした。
#  - (変更) 結果ファイルや設定ファイル等一連のファイルが$0から生成されるようにした
#
# ver.0.11
#  - (変更) 保存csvファイルの改行コードを\r\nにした。
#  - (追加) csvのE 列に動画IDを保存するようにした。
#  - (変更) 動画タイトルに「,」が含まれている場合は全角"，"に変換するようにした((動作未確認)。

# exe化メモ
#   1. pathの先頭にactiveperlが来るようにする。念のためcygwinへのpathを削除する。
#   2. コマンドプロンプトで nicoStatのフォルダへ。
#   3. pp -o nicoStat.exe nicoStat.pl
#      このときカレントディレクトリにcygwinのperl.exeがあると実行時エラーを起こす。
#      ノートPCでは pp -M URI.pm -o nicoStat.exe nicoStat.pl とした。
#   4. .\nicoStat.exeで実行確認。
#      「can't locate ***/***.pm ～」というエラーが出た場合は
#      pp -M ***/***.pm -o nicoStat.exe nicoStat.pl とやってpmを取り込ませる
#   5. pathを以前の状態に戻す。

# use lib './lib';
use strict;
use warnings;
use LWP::UserAgent;
use HTTP::Cookies;
use HTTP::Request;
use HTTP::Headers;
use CGI;
use YAML::Syck;
use Unicode::Japanese;
use Time::Piece;

# コマンドのあるディレクトリを求める
our $appDir = '.';
if ($0 =~ /\//){
  if ($0 =~ /\.(exe|EXE)$/) {
    $appDir = $0;
    $appDir =~ s/^(.+)\/.+\.(exe|EXE)$/$1/;
  } elsif ($0 =~ /\.(pl|PL)$/) {
    $appDir = $0;
    $appDir =~ s/^(.+)\/.+\.pl$/$1/;
  }
}

# ログインに使用するアドレスやパスワードを以下のように $rcFile に
# 書いておくと自動ログインに使われる。メールアドレスのみ書いてもOK。
# mail: foo@example.com
# password: hogehoge
my $rcFile   = "$appDir/.nicorc";

# $rcFileから読んだデータを入れるハッシュ
our $conf;

# 結果ファイルをおくディレクトリを指定
my $resultDir = "$appDir/Stat";

# クッキー設定
my $cookieFile = "$appDir/.cookies.txt";
my $cookie_jar = HTTP::Cookies->new(
    file => $cookieFile,
    autosave => 1,
);
my $ua = LWP::UserAgent->new( keep_alive => 4 );
$ua->cookie_jar( $cookie_jar );

# showDiff=1...csvファイル保存後にdiff.plを実行し
# 直近の２つのcsvファイルの差を表示させる
my $showDiff = 0;              

# 冗長表示モード（デバッグ用）
our $verbose = 0;

# ローカルにダウンロードしたマイページHTMLファイルを解析
# する場合はこの変数にファイル名を代入する。通常は空文字列にする
our $localHTML = '';

# 結果ファイル名$outFileの指定(空文字列の場合は標準出力へ)
# ./Stat/stat_年-月-日_時-分[_通し番号].csv
my $t = localtime;
my $time = sprintf "%s_%s", $t->ymd, $t->hms("-");
$time =~ s/-\d\d$//;  # 最後の秒データを削除
my $outFile = sprintf "%s/stat_%s.csv", $resultDir, $time;
# 同名のファイルが既にあった場合
my $outSeq = 2;
while (-e $outFile){
  $outFile = sprintf "%s/stat_%s_%02d.csv", $resultDir, $time, $outSeq;
  $outSeq++;
}
# 結果ディレクトリが無かった場合
unless (-e $resultDir){
  my $ret = mkdir $resultDir;
  if ($ret == 0){
    warn "error: can not make directory $resultDir\n";
    exit 1;
  }
}

# 設定ファイルを読む
$conf = YAML::Syck::LoadFile($rcFile) or die "error: $rcFile: $!";
# コードをsjisからutf-8にする
$conf->{playNum} = Unicode::Japanese->new($conf->{playNum}, 'sjis')->get;
$conf->{commNum} = Unicode::Japanese->new($conf->{commNum}, 'sjis')->get;
$conf->{mylistNum} = Unicode::Japanese->new($conf->{mylistNum}, 'sjis')->get;
$conf->{next} = Unicode::Japanese->new($conf->{next}, 'sjis')->get;
$conf->{dlNum} = Unicode::Japanese->new($conf->{dlNum}, 'sjis')->get;

if ($conf->{playNum} eq '' || $conf->{playNum}   eq '' ||
    $conf->{commNum} eq '' || $conf->{mylistNum} eq '' ||
    $conf->{title}   eq '' || $conf->{id}        eq '' ||
    $conf->{dlNum}   eq '' || $conf->{next}      eq ''){
  warn Unicode::Japanese->new("$rcFileからパラメータ抽出用の設定を読み込めませんでした。\n")->sjis;
  exit 1;
}

# 引数処理
while (@ARGV > 0 && $ARGV[0] =~ /^-/){
  if ($ARGV[0] eq '-d'){
    $showDiff = 1;
    shift @ARGV;
  } elsif ($ARGV[0] eq '-verbose'){
    $verbose = 1;
    shift @ARGV;
  } elsif ($ARGV[0] eq '-i' && @ARGV >= 2){
    $localHTML = $ARGV[1];
    shift @ARGV;
    shift @ARGV;
  } elsif ($ARGV[0] eq '-o' && @ARGV >= 2){
    $outFile = $ARGV[1];
    $outFile = '' if ($outFile eq 'stdout' || $outFile eq 'STDOUT');
    shift @ARGV;
    shift @ARGV;
  } else {
    print STDERR "usage: $0 [-d|-verbose|-i localHTMLfile|-o saveFile(or 'stdout')]\n";
    exit 1;
  }
}

my $retry = 0;
my $page = 1;
my $movieNum = 0;
my $outData = '';
while (1){
  # 投稿動画ページを取得
  my $res;
  if ($localHTML) {
    # ローカルHTMLを開くなら
    unless (-r $localHTML) {
      warn Unicode::Japanese->new("入力ファイル$localHTMLがありません。\n")->sjis;
      exit 1;
    }
    # $res = $ua->get(sprintf "file://localhost%s", `cygpath -a $localHTML`);
    $res = $ua->get(sprintf "file:///%s", `cygpath -am $localHTML`);
  } else {
    # webからHTMLを取得するなら
    $res = $ua->get("http://www.nicovideo.jp/my/video?page=$page");

    # ログインしておらずログインページが表示された場合の処理
    my $ret = loginPage ($res->content);
    if ($ret == 1){
      if ($retry >= 2){
        # 再ログインでもここにきた場合
        warn Unicode::Japanese->new("ログインに失敗しました。\n")->sjis;
        exit 1;
      }
      $retry++;
      login($ua, $rcFile);
      redo;
    }
    elsif ($ret == -1){
      exit 1;
    }
  }

  # 投稿動画のデータを得て出力データに追記
  my ($playNum, $commNum, $mylistNum, $dlNum, $title, $id);
  $playNum = $commNum = $mylistNum = $dlNum = -1;
  $title = $id = '';
  foreach my $line (split /\n/, $res->content){
    $line =~ s/(\r|\n|,)//g;

    if ($line =~ /$conf->{playNum}/){
      $playNum = $1;
      print Unicode::Japanese->new("playNum: $line -> $playNum\n")->sjis if $verbose;
    }
    if ($line =~ /$conf->{commNum}/){
      $commNum = $1;
      print Unicode::Japanese->new("commNum: $line -> $commNum\n")->sjis if $verbose;
    }
    if ($line =~ /$conf->{mylistNum}/){
      $mylistNum = $1;
      print Unicode::Japanese->new("mylistNum: $line -> $mylistNum\n")->sjis if $verbose;
    }
    if ($line =~ /$conf->{dlNum}/){
      $dlNum = $1;
      print Unicode::Japanese->new("dlNum: $line -> $dlNum\n")->sjis if $verbose;
    }
    if ($line =~ /$conf->{title}/){
      $title = $1;
      $title =~ s/,/、/g;
      print Unicode::Japanese->new("title: $line -> $title\n")->sjis if $verbose;
    }
    if ($line =~ /$conf->{id}/){
      $id = $1;
      print Unicode::Japanese->new("id: $line -> $id\n")->sjis if $verbose;
    }

    # データがそろったら出力文を作成
    if ($playNum >= 0 && $commNum >= 0 && $mylistNum >= 0 && $dlNum >= 0 &&
        $title ne ''  && $id ne ''){
      my $mesg = "$playNum,$commNum,$mylistNum,$title,$id,$dlNum\n";
      $outData .= Unicode::Japanese->new($mesg)->sjis;
      $playNum = $commNum = $mylistNum = $dlNum = -1;
      $title = $id = '';
      $movieNum++;
    }
  }

  last if ($localHTML || lastPage($res->content));
  $page++;
}

# 結果保存
if ($outData ne ''){
  if ($outFile ne ''){
    open (FID, "> $outFile") or die "error: $outFile: $!";
    print FID $outData;
    close FID;
    print Unicode::Japanese->new("再生状況(動画数=$movieNum)を $outFile に記録しました。\n")->sjis;
  } else {
    print $outData;
  }
} else {
  print Unicode::Japanese->new("error: マイページから動画データを抽出できませんでした。\n")->sjis;
}

# 差分表示
if ($showDiff){
  if (defined $ENV{USEBAT}){
    # system ("$appDir/perl.exe -I./lib ./diff.pl");
    # system ("$appDir/diff.exe");
    # system ("$appDir/DIFF.BAT");
    # ↑だとエラーが出るので、BATファイル側で呼び出すようにした
  } else {
    system ("$appDir/diff.pl");
  }
}

#---------------------------------------------
# ログインページであるかどうかを判定する
# ログインページだったら1を返す
# ログインページでなければ0を返す
# 解析できなかったら-1を返す
sub loginPage {
  my ($content) = @_;
  foreach (split /\n/, $content){
    if ($_ =~ /<title>/){
      if ($_ =~ /ログインフォーム<\/title>/){
        return 1;
      } else {
        return 0;
      }
    }
  }
  warn "error: something error has occured...\n\n$content";
  return -1;
}

#---------------------------------------------
# 投稿動画の最終ページであれば1を返す
sub lastPage {
  my ($content) = @_;
  foreach my $line (split /\n/, $content){
    $line =~ s/(\r|\n)//g;
    if ($line =~ /$conf->{next}/){
      print "lastPage(): next page is exist\n" if $verbose;
      return 0;
    }
  }
  print "lastPage(): this page is the last\n" if $verbose;
  return 1;
}

#---------------------------------------------
# ログインする
sub login {
  my ($ua) = @_;

  # 設定に抜けがある場合は入力を求める
  unless (defined $conf->{mail} && $conf->{mail} ne ''){
    print STDERR "Email: ";   my $addr = <STDIN>;
    $addr =~ s/(\r|\n)//g;
    $conf->{mail} = $addr;
  }
  unless (defined $conf->{password} && $conf->{password} ne ''){
    my $pass = '';
    if (defined $ENV{USEBAT} || ! -e '$appDir/stty'){
      # batからの起動なら
      print STDERR "Password: ";
      $pass = <STDIN>;
    } else {
      # cygwin(perlでの直接起動)なら stty でパスワード入力中の文字を非表示にする
      system("$appDir/stty -echo");
      print STDERR "Password: ";
      $pass = <STDIN>;
      print STDERR "\n";
      system("$appDir/stty echo");
    }
    $pass =~ s/(\r|\n)//g;
    $conf->{password} = $pass;
  }
  # print YAML::Syck::Dump($conf),"\n";exit;
  # ログイン
  $ua->post( "https://secure.nicovideo.jp/secure/login?site=niconico" => $conf );
}

