WSL/Bash + CMD.EXE = やめてくれ!

Windows10 Anniversary Update、スタートメニューやその他いろいろ更新されましたが、開発者としての関心事はやっぱり “Windows Subsystem for Linux”(以下WSL)じゃないでしょうか。。。さっそく開発者モードにしてこの機能を有効にしてみました(^^)

僕はてっきりBash用のターミナルで動かすんでしょう??(これでコマンドプロンプトからおさらばさ!) と思ってましたが・・・まさかのコマンドプロンプト(CMD.EXE)立ち上げ、からの “bash”タイプ・・・。ここでかなりのガッカリ感が・・・。

Linuxは Redhat系のCentOSしか知らないので、apt-get とかよくわかんねーよ。とりあえず、sudo apt-get yumとかでパッケージ管理してしまおうかな(笑)
WSL自体よく仕組みが分かんないけど、ubuntuの他にも選択肢があればいいのになー、と素人考えはダメですか(^^;;;

当面 Cygwin で問題無い。っていうか、ホンモノが欲しかったらVMで用意すれば・・・同じことができるんじゃ・・・。そういえば、DockerのWindows版が出たってニュース記事があったし・・・。

なんにせよ、選択肢が多いとどれ使えば正解なのかよくわかりません・・・。使いやすい環境を自分で構築していく労力が無駄だなぁーと思うときはあります・・・。本来の仕事が効率的に前に進まない・・・。

Tweet image download agent.

【追記】2017/09/19 現在「いいね」画像のTL取得に失敗しているみたい。原因究明中(デバッグ中)使用しているアカウントがロックされたので、ちょっとお手上げ。ログインするととたんにロックされるのでビルド済みのファイルは公開中止。アカウントを永久凍結されると困るので、これにて終了(^^;; チキンハートw

ちょうどGUIをWinFomsからWPFに移行中でそろそろ完了しつつあったのでちょい残念。でもまぁ、WPFのバインディングやら非同期やら勉強できたので、いい教材だったかなと。


【追記】 2017/08/27 二段階認証に対応。画面をちょっと変更。ファイル拡張子の判別がちゃんと機能してなかった・・・他色々修正しまくり。


【追記】 2017/08/20 set-cookieヘッダの取得方法を若干変更。 現在2段階認証には対応しておりません。今度対応させます。


【追記】 2017/08/19 ログイン後の画像取得が失敗する不具合を修正。


【追記】2017/08/18 ↓の原因をほぼ特定。暫定的に修正したものを更新予定。


【追記】2017/08/17 ログインのプロセスが変更になった関係で、現在、ログインが成功しても画像の取得に失敗します。ログイン自体が失敗している模様です。また調査次第修正できるようであれば、修正します m(__)m


【追記】2017/07/09 取得するファイルタイプ(拡張子)、ファイルサイズの上下限を設定できるようにした。コードのリファクタリング実施。


【追記】2016/09/06 テキストボックスのところをコンボボックスに張り替え。新しいものから10個まで入力履歴を記憶できるように修正。他、諸々修正。


【追記】2016/04/27 GW前に、ちまちま修正他、検索ワードに対応。これで画像収集が捗るわい(^^;


ちょっと前に書いたツイッターの画像を一括ダウンロードするやつ、のGUI版を 非同期タスクのお勉強も兼ねて日曜プログラミング(^^;

コンソールでコマンドを打つ方が慣れているのでわざわざGUIみたいなクソめんどくさいもん書く必要もなかったんですが・・・。

とくに最近、C#でアシンクやらアウェイトやらウェイタブルとか今まで見たこともないキーワードがガンガン出てくるようなコードを見る機会が多くなったので、勉強がてら練習で作ってみました。・・・・というレベルです(m_m)
とはいいつつ、awaitとかasyncとかまだよく分からないので、Task.Run()をふんだんにちりばめてみました(^_^;;; ま、自分で使うツールだから動けばいいや。
検索とかはまだ実装していません、というか、自分には必要ないので、やるつもり無し。時間があればまたブラウザで解析して・・・時間があれば・・・。
そもそもこの手のツールは他にもっと便利なソフトが窓の杜とかVectorとか探せばあると思うので。。。
動画とかも落とせればいいかなー、とか思って昼休みにchromeの開発者ツールでちょっと調べてみたけど、結構めんどくさそうですね・・・。

twitterimageagent-1 twitterAgent2

Twitter APIじゃないので、ブラウザでの表示方法が変更されると誤動作するでしょう。

ビルド済みのファイルとあまり綺麗では無いソースファイルは、OSDN.JPの作業部屋 ↓↓↓から。(使い方よく分かってません・・・)
https://osdn.jp/users/earlgreyx/pf/TwitterImageDownloadAgent/wiki/FrontPage

CSSで文字回転

HTMLで表組みを組んでいくと、カラム数が多くなるとどうしても↓の左端のように不格好になりますよねぇ。。。。
damedame
イラレとかで文字を縦書きにして描画してSVGで書き出して配置すれば一応解決しますが・・・できるならそんなメンドッチーこと抜きに、画像を使わずCSSだけで済ませたい・・・。

はじめは、writing-mode プロパティを使えば縦書きにできるやろ???? と思ってth要素にwriting-mode: vertical-lrと設定してみたけど、だめだった。それならば・・・と思って、<th>タグ内のテキストを<span>タグで囲ってやり、そのspan要素に writing-mode: vertical-lr;を設定すれば一応縦書きになってくれた。
でも、望んでいるものと違う。縦書きだとアルファベットが90度回転してしまうので、縦書きではなく、横書きのまま90度回転した状態になってほしいんだな。。。

で、CSS3ならtransformプロパティでテキスト回転できる!ってのをグーグル先生に教えてもらって早速やってみたんですが・・・これがなかなか、圧倒的な知識不足でスマートな指定が分からない。試行錯誤して下記のような感じにできました。
kaiten
しかし・・・やり方がスマートじゃない・・・・。

<!-- HTML 抜粋 -->
<tr>
  <th><span style="margin-top:3em;">ほにゃらら</span></th>
  <td>
     ナンチャラ<br>
     かんちゃら
  </td>
</tr>

で、CSSは

/* ベンダープレフィックスは省略 */
th {
  vertical-align: middle;
  text-align: left;
  width: 1em !important;
}
th > span {
  transform: rotate(-90deg);
  transform-origin: left top;
  display: inline-block;
  white-space: nowrap;
  position: absolute;
  line-height: 1;
}

ようするに、セル内のspan要素を反時計回りに90度回転。デフォルトでspan要素の中心で回転するので不揃いになるので、回転軸は 左端上に設定。これだけではダメで、左端で回転するのでせっかくth要素でvertical-align:middleにして縦中央表示させているのにずれてしまう。これを補正するため、span要素にわざわざstyle属性で、span要素内の文字数の半分の長さだけマージンを設定。

span要素を補正するのはスクリプトを使って、文字数を割り出して・・・って自動でやればいいかもしれない。

/* サンプル */
(function($)
{
  $('th > span').each( function(){ $(this).css('margin-top', $(this).width() / 2); } );
}(jQuery);

一応画像は使わずできたけど、正直やり方が強引過ぎて納得できない。たぶんもっとCSS3のプロパティとか探せばパシッとスマートな設定(プロパティ・値)があるのかもしれないけど・・・ザラッと調べてみた感じ見つけられなかった。

グーグル先生にどういうワードで検索すればヒットするんだろ・・・T(;_;)T 教えてエロい人!

CentOS6でシステムのHDDを入れ替える手順

(*)その後を書き足し。(2016/5/2)
(*)若干書き足し。(2016/3/2)
(*)ストレージのどちらかがMBRではなくGPTで初期化している場合おそらくこのエントリは役に立たないでしょう。(2019/5/20)

例によって、記録(備忘録)記事です。前置きがかなり長いです(m_m)

社内で使用しているテストサーバーは、Webアプリケーションの開発及び稼働チェック、社内マシンへのDNSサービス、ダミーのMTA、ソース管理(Subversion)サーバー、Sambaによるファイルサーバー等、僕が仕事する上で必要な機能がたった1つのマシンに集約されてます。というか、一台しか使わせて貰えない(^_^;;;

さらに、このマシンは10年前に買ってもらったCore2 Duo搭載のWindowsXPマシンだったものを社内のOSをXPからWindows7に一斉に変更するタイミングでWindows7へのアップグレードの対象外にされたもの。これを半ば強引に奪い取り(笑)、Linux(CentOS)を入れて今まで特に問題もなく、使用してきましたが、なんせ10年もHDD交換せずに稼働してきたので、そろそろHDDも寿命が尽きる頃。っていうか、もう十分寿命過ぎてる感があるのですが・・・。

HDDだけ新品に交換すれば十分ということで、内蔵HDDを経費として認めてもらえそうなので、仕事が一段落したらシステム移行することにした。

Windowsなら、ドライブごとコピー・・・とかすればいいんでしょうけど、Linuxではやったことないし、2つのHDDをLVMパーティションで作成して/,swap,/usr,/homeという風に分けて使っているのでそう単純にいかないことに気付く。かといって、クリーンインストールはできるだけ避けたい(やってる暇が無い・・・)

ってことで、前置きが長くなりましたが、実作業前に手順を記録して、VMWareで仮想マシンを一度作成しCentOSをインストール後、新しい仮想HDDを追加してHDD移行をシミュレートしてみました。

結果的に上手くいったので実作業前に移行手順を記録しておくことにした。でも正直これでいいのかどうか分からない。参考程度に見て頂くといいかもしれない。

一応分からないことはドキュメントを読んだり、ググったりしたけど、rsyncとかddとかfdiskとか、LVM関係とかの基本的なコマンドは普段殆ど使わないからオプションの指定の仕方とか合ってるかどうかわからん。

素人なりに考えた方法なので、たぶんもっとスマートな方法があるんだと思う。。。

※ パーティションの作成とかブートフラグをつける、等のfdiskやpartedコマンドの操作は省いています。それもできない、分からない、という人は大人しくクリーンインストールした方がいいと思う(逃げ)

(1)環境のチェック

現在のHDD構成 。使用率は20%ぐらい。1TBあるけどほとんど活用されていない。CentOSインストールするときRAID1構成にしようと思ったけど、はじめて仕事で使うサーバーだったので、知識ないのと、なんかあったら怖い、という理由でRAID1構成を諦めた経緯がある(ーー;

  • /dev/sda (500GB)
  • /dev/sdb (500GB)

新しいHDDは、500GB。このHDDに現在のHDDをシュリンクする。

  • /dev/sdc (500GB)

moving

現在のパーティション構成 (*bootable flag)
-------------------------------------------
/dev/sda1(*) Linux        => /boot

/dev/sda2    Linux LVM -+
                        | =>  /,/home,/usr,swap
/dev/sdb1    Linux LVM -+

現在のPV
-------------------------------------------
/dev/sda2  VolGroup lvm2 465.27G
/dev/sdb1  VolGroup lvm2 465.76G

現在のVG
-------------------------------------------
name: VolGroup

現在のLV
-------------------------------------------
lv_home VolGroup ext4 465.76G
lv_root VolGroup ext4  50.00G
lv_swap VolGroup swap   8.00G
lv_usr  VolGroup ext4 407.27G

(2)新しいHDDを現在のHDDと同じ構成にする。

 /dev/sdc1 => /boot 用
 /dev/sdc2 => LVMパーティション(/,/usr,/home,swap用)

作成した/boot用パーティション(/dev/sdc1)へ現在の/bootパーティション(/dev/sda1)をコピー。

 >> dd if=/dev/sda1 of=/dev/sdc1 bs=64MB

LVMパーティション/dev/sdc2にボリュームグループを作成する。
この際VolumeGroup名を現在のものとは別名(VolGroup01など)にする。
(現在のHDDを取り外した後、現在の名前にリネームするので)

 >> vgcreate VolGroup01 /dev/sdc2

この新しいVolumeGroup01から論理ボリューム(LV)を切り出していく(サイズは適当)

 >> lvcreate VolGroup01 -n lv_root -L 22GB 
 >> lvcreate VolGroup01 -n lv_swap -L 8GB 
 >> lvcreate VolGroup01 -n lv_usr -L 70GB 
 >> lvcreate VolGroup01 -n lv_home -l 100%Free
 各論理ボリューム上にファイルシステムを作成
 >> mkfs.ext4 /dev/VolumeGroup01/lv_xxxx (xxxxはroot,usr,home)

 スワップ用には
 >> mkswap /dev/VolumeGroup01/lv_swap

ここまでで、一旦シャットダウンする。

(3)インストールメディアから起動し、レスキューモードで立ち上げる。

(VolumeGroupがinactiveの場合はアクティベートする)
>> vgchange -a y 

(マウントポイント作成)
>> mkdir /mnt/src
>> mkdir /mnt/src/root
>> mkdir /mnt/src/home
>> mkdir /mnt/src/usr
>> mkdir /mnt/dest
>> mkdir /mnt/dest/root
>> mkdir /mnt/dest/home
>> mkdir /mnt/dest/usr

(論理ボリュームをマウントする)
>> mount /dev/VolGroup/lv_root /mnt/src/root
>> mount /dev/VolGroup/lv_home /mnt/src/home
>> mount /dev/VolGroup/lv_usr /mnt/src/usr
>> mount /dev/VolGroup01/lv_root /mnt/dest/root
>> mount /dev/VolGroup01/lv_home /mnt/dest/home
>> mount /dev/VolGroup01/lv_usr /mnt/dest/usr

(パーティションごとコピーする)
rsync -aHxv /mnt/src/root/ /mnt/dest/root
rsync -aHxv /mnt/src/home/ /mnt/dest/home
rsync -aHxv /mnt/src/usr/ /mnt/dest/usr

(コピーが終わったらマウントを解除して、後片付け)
>> umount /mnt/src/root
>> umount /mnt/src/home
>> umount /mnt/src/usr
>> rm -fr /mnt/src
>> umount /mnt/dest/root
>> umount /mnt/dest/home
>> umount /mnt/dest/usr
>> rm -fr /mnt/dest

(fdisk or parted コマンドで/dev/sdc1 にブートフラグを付ける)

ここで、poweroff (shutdown -h now)する。

(4) 一旦電源断して古いHDDを取り外し、再度レスキューモードで起動する

レスキューモードで起動する際の最終段階で、既存のLinuxインストールの探索をスキップしないように。スキップしてしまうと、せっかく上でコピーしたパーティションが/mnt/sysimageにマウントされないので)

ここまで来たらあとは、ボリュームグループのリネームとMBRのインストールするだけ。現在のHDDを取り外した後は、新しいHDDが /dev/sda と認識されるはずなので、ボリュームグループ名を元に戻す。.autorelabelファイルの作成は別にしなくてもいいかも。たぶんリブートしたら強制的にリラベルされるはず(僕が試したときはそうだった)

(ボリュームグループ名を旧HDDにあったものと同じにする)
>> vgrename VolGroup01 VolGroup

(grubインストール: MBRの復旧?)
>> chroot /mnt/sysimage
>> grub-install /dev/sda

(selinux relabel)
>> touch /.autorelabel

(5) インストールメディアを取り外し、リブートして起動を確認する。

ここで、起動に失敗したら、なんかおかしい事になってるので、もう一度HDDを古いものに接続しなおしてやりなおし(^^;;;;
とにかく一度仮想環境で手順を実際にやっておく方がいいと思う。
僕はWindows上のVMWareでHDD容量を1/20サイズでシミュレーションをしたらどうにか上手くいってシステム移行ができた。

(6)でも、ホントにこれで移行できるかどうか分からん

実機で、しかもLinuxで、のシステム環境移行ってやったことないので、デーモンがいろいろ動いていて実稼働しているシステムが本当に移行後上手く動いてくれるのかすんげぇ心配。

また、実際に実機でやってみて結果を追記する予定。でも・・・まだHDD買えてないんですけどね(^_^;;そもそも移行前にHDDが限界で壊れたらどうしよう・・・オロオロ (・_・ )( ・_・) オロオロ

(7) その後 2016年5月2日 追記

飛び石連休の最中、1日だけ出ても仕事は中途半端になるので、システム移行を決行。
HDDは、東芝製のバルクHDD(DT01ACA050)の500GBを買ってきて、PCに接続・・・ブートしない・・・orz
いろいろ試行錯誤した結果、元からあるHDDと新しく買ってきたHDD全部SATAポートに繋ぐと、ブートしないと判明。BIOS画面では一瞬認識するけど、すぐフリーズする。元からあるHDDとマザーボードはかなり古い機種なので混在するとダメなのかもしれないが、原因は不明。

仕方ないので、S-ATA ⇔ USB3 の変換コネクタを使用して新しいHDDをUSBストレージとして認識させた。コピーが遅くなるけど、仕方ない。

移行そのものは、スムーズに進行・・・と思いきや、(3)のrsyncでコピーする際、スラッシュを付け忘れているのに気付かず、(4)のレスキューモードで再起動した際、既存のLinuxシステムの検索に失敗したorz 。結局mvコマンドで1つ上のディレクトリに移動(^^;;;

最終的には上手くいきました。手順と理屈がある程度分かると、ツールを使ったHDD丸ごとコピーよりマニュアルでやる方が早いかも(^^)

twitterの「いいね」画像をダウンロード C#版

2017年7月15日
こっちに移動 → https://ptsv.jp/2016/04/20/tweet-image-download-agent/

2016年4月29日 Windows フォームを利用したGUI版追加
検索ワード対応とスレッドプールにダウンロード処理を投げるように、ソース本体も改編した版

2015年11月27日・29日 コード修正・他
コード修正 画像ファイルの正規表現の間違い修正及びcommandを追加。
コード修正 URL を favorites => likes に変更。文章をちょろっと変更。

2015年11月23日 コード修正
2回目以降のtimeline URLの取得方法が間違ってました。ごめんなさい。(m_m)


perl で組んだものを C# に書き直したコードです。perlで組んだもので僕がやりたかったことは全部できた訳ですが・・・ま、今仕事暇なんで(^^;

エラーチェックとか全然してません。このコードは参考テスト程度のものなので、あしからず。
間違ったtargetとか入力してサーバーが404エラー返すと例外吐いて死にます(^_^;; エラー処理をコードに盛り込むとコードの要点が見えずらくなるので・・・いないとは思いますが、コードを使う時は、エラー処理(例外捕捉とか)を入れてね。

また、Twitter API使った方法ではないので、twitter側がブラウザ表示の方法を変更してしまうと、たぶんエラーで落ちると思う。まぁ、いつまでこの方法で使えんのかな、という感じです。

コンソールプログラムで良ければ、コンパイルしたものを置いときます。[ getTweetImage.exe ]
※使用方法は下記コードのコメントを参照してください。
(EXEファイルをダウンロードすると、たぶんセキュリティーチェックで引っかかると思いますが、信用できない方は、下記コードをご自身でコンパイルしてください。 )

たぶん Windows以外のOSでも、monoが入っていたら 上記 exe ファイルがそのまま動くんじゃないかなー。
[追記]
CentOS6 + mono で実行してみましたが、https通信でセキュリティ?かなんかの例外が発生してしまいました。
デフォルトでインストールしたまんまのmonoの場合はルート証明書をインポートする等しないといけないみたいですね。mozroots.exeをインストールして解決しました。下記ページを参考にさせていただきましたm(__)m。
~Monoで、Secure Socket Layer (SSL) / Transport Layer Security (TLS)通信を行うには~
http://qiita.com/takanemu/items/129acc13d8b7ce088b92

[追記ここまで]

これにて終了。

/*******************************************************************************

  saving image file on twitter.(getTweetImage.cs)

   build: Visual Studio 2015 Community.( also Express Edition) or Windows SDK
     IDE使うほどでもないので、コマンドラインツールでcsc.exeでバイトコンパイルしてください。
     >> csc.exe getTweetImage.cs

  howto: Console command
     >> getTweetImage.exe command target [username password]
     
     command  : ターゲットが"いいね"した画像 => 'favo' 
                ターゲットがアップした画像   => 'profile'
                ターゲットのTLの画像        => 'timeline'
     target   : ターゲットとなる取得したい画像のアカウント名 
     username : 自分のログインアカウント
     password : 自分のパスワード

     usernameとpasswordは、「いいね」画像の取得及び、
     ターゲットとなるアカウントが非公開アカの場合に必要かと思います。

*******************************************************************************/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Net;
using System.Linq;

namespace ptsv.jp
{
  public class ConsoleApplication1
    {
      private static Dictionary<string,string> TIMELINEURL = new Dictionary<string,string>
        {
          { "profile","https://twitter.com/i/profiles/show/{0}/media_timeline"},
          { "favo","https://twitter.com/{0}/likes/timeline"},
          { "timeline","https://twitter.com/i/profiles/show/{0}/timeline"}
        };

      /// <summary>
      /// スタートアップ
      /// </summary>
      /// <param name="args">コマンドライン引数</param>
      public static void Main(string [] args)
        {
          if(args.Length < 2)
            {
              Console.WriteLine("USAGE: getTweetImage.exe command target [username password]");
              Console.WriteLine("USAGE: command is 'favo' or 'profile'");
              Console.WriteLine("require username and password\n when command is 'favo',or target is private account.");
              return;
            }
          string command  = args[0];
          string target = args[1];
          string username_or_email = null;
          string password = null;

          if(args.Length == 4)
            {
               username_or_email = args[2];
               password = args[3];
            }

          if(command == "favo" && (string.IsNullOrEmpty(username_or_email) || string.IsNullOrEmpty(password)))
            {
               Console.WriteLine("when command is 'favo', username and password is required.");
               Console.WriteLine("USAGE: getTweetImage.exe command target [username password]");
               return;
            }

          string timeline_url = TIMELINEURL.ContainsKey(command) ? string.Format(TIMELINEURL[command],target) : null;

          var twitter = new TwitterDownload((type,v) =>
                                            {
                                              switch(type)
                                                {
                                                case "get-timeline":
                                                  Console.WriteLine("---- getting and parsing\n{0} ...\n----",v);
                                                  break;
                                                case "getting-image":
                                                  Console.WriteLine("fetching: {0}",v);
                                                  break;
                                                case "saved-image":
                                                  Console.WriteLine("saved {0}",v);
                                                  break;
                                                case "file-exists":
                                                  Console.WriteLine("{0} is already exists.",v);
                                                  break;
                                                case "fail-login":
                                                  Console.WriteLine("Auth code not found.","");
                                                  break;
                                                case "fail-auth":
                                                  Console.WriteLine("username or password were refused.","");
                                                  break;
                                                case "try-login":
                                                  Console.WriteLine("Trying to login by '{0}' for twitter.com", v);
                                                  break;
                                                }
                                            });

          if(!string.IsNullOrEmpty(timeline_url))
            {
              twitter.Agent(timeline_url,username_or_email,password);
            }
          else
            {
              Console.WriteLine("[INVALID COMMAND] command should be 'favo' or 'profile'");
            }
        }
    }

  /// <summary>
  /// 画像ダウンロード本体クラス
  /// </summary>
  public class TwitterDownload
    {
      /*------------------------------------------------------------------------------

        Const

      ------------------------------------------------------------------------------*/
      public const string URL_LOGIN    = "https://twitter.com/login";
      public const string URL_SESSIONS = "https://twitter.com/sessions";
      public const string USER_AGENT   = "Mozilla/5.0 (compatible; MSIE 11.0; Windows NT 6.0; Trident/5.0)"; //適当
      public const string REFERER      = "https://twitter.com/";
      private const string RE_MEDIA_URL = @"https://pbs\.twimg\.com/media/([\w\-]+\.\w{3,4})(:large)?";
      private const string RE_TWEET_ID  = @"data-tweet-id=\\""(\d+)\\""";
      private const string RE_AUTH_CODE = @"<input type=""hidden"" value=""([\d\w]+?)"" name=""authenticity_token""(?:\s*/)?>";

      /*------------------------------------------------------------------------------

        Instance

      ------------------------------------------------------------------------------*/
      private string OutputDirectory = Path.Combine(".", "tmp");
      private TwitterWebClient webClient;
      private Action<string,string> Callback;

      public TwitterDownload(Action<string,string> callback, string cookieFile)
        {
         this.Callback = callback;
         this.webClient =  new TwitterWebClient(cookieFile);
        }

      public TwitterDownload(Action<string,string> callback)
        {
          this.Callback = callback;
          this.webClient = new TwitterWebClient();
         }

      public string directory
        {
          set
            {
              this.OutputDirectory = value;
            }
          get
            {
              return this.OutputDirectory;
            }
        }

      public void Agent(string timelineURL,string username,string password)
        {
          if(String.IsNullOrEmpty(timelineURL))
            return;

          if(!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
            {
                if (!GetLogin(username, password))
                 {
                    Callback("fail-auth", "");
                    return;
                 }
            }

          if (!Directory.Exists(OutputDirectory))
            Directory.CreateDirectory(OutputDirectory);

          string param = "?include_available_features=1&include_entities=1";
          string url;
          do
            {
              url = timelineURL + param;
              Callback("get-timeline",url);

              string result = webClient.DownloadString(url);
              param = GetImage(result);

            } while(!string.IsNullOrEmpty(param));
        }

      private bool GetLogin(string username,string password)
        {
          Callback("try-login", username);
          webClient.Headers[HttpRequestHeader.UserAgent] = USER_AGENT;
          webClient.Headers[HttpRequestHeader.Referer] = REFERER;

          string loginResult = webClient.DownloadString(URL_LOGIN);
          Match authMatch = Regex.Match(loginResult,RE_AUTH_CODE);
          if(!authMatch.Success)
            return false;

          var param = new NameValueCollection();
          param.Add("session[username_or_email]",username);
          param.Add("session[password]",password);
          param.Add("authenticity_token",authMatch.Groups[1].Value);
          param.Add("remember_me","1");
          param.Add("redirect_after_login","/");

          return Encoding.UTF8.GetString(webClient.UploadValues(URL_SESSIONS,param)).IndexOf("error") < 0;
        }

      private string GetImage(string json)
        {
          json = json.Replace(@"\/", "/");

          foreach(Match match in Regex.Matches(json,RE_MEDIA_URL))
            {
              string url = match.Value;
              string basename = match.Groups[1].Value;
              string filename = Path.Combine(OutputDirectory,basename);

              url = string.IsNullOrEmpty(match.Groups[2].Value) ? url + ":orig" : url.Replace(":large",":orig");

              if(File.Exists(filename))
                {
                  Callback("file-exists",filename);
                }
              else
                {
                  Callback("getting-image",url);
                  webClient.DownloadFile(url,filename);
                  Callback("saved-image",filename);
                }
            }

          var ids = new List<string>();
          foreach(Match match in Regex.Matches(json,RE_TWEET_ID))
            {
              ids.Add(match.Groups[1].Value);
            }

          return ids.Count > 0 ? string.Format("?max_position={0}&include_available_features=1&include_entities=1",ids.Last()) : null;
        }
    }

  /// <summary>
  /// クッキーの読書に対応するためだけのWebClientクラス。
  /// </summary>
  internal class TwitterWebClient : WebClient
    {
      private CookieContainer cookieContainer = new CookieContainer();
      private bool isAutoRedirectEnabled = false;

      public TwitterWebClient() : this("") {}

      /// <summary>
      /// もしクッキーファイルが存在したらクッキーファイルを読み込んでCookieContainerに追加する。
      /// </summary>
      /// <param name="cookieFile">ブラウザからエクスポートしたクッキーファイル</param>
      public TwitterWebClient(string cookieFile)
        {
          if(File.Exists(cookieFile))
            {
              using(var sr = new StreamReader(cookieFile))
              {
                string line;
                var reCRLF = new Regex(@"[\r\n]$");
                var reEmpty = new Regex("^$");
                var reComment = new Regex("^#");
                var reSpace = new Regex(@"\s+");

                while ((line = sr.ReadLine()) != null) 
                  {
                    line = reCRLF.Replace(line,"");
                    if(reEmpty.Match(line).Success || reComment.Match(line).Success)
                      continue;
                    string[] ar =  reSpace.Split(line);

                    cookieContainer.Add(new Cookie(ar[5],ar[6],ar[2],ar[0]));
                  }
              }
            }
        }

      /// <summary>
      /// WebClient.GetWebRequestメソッドをオーバーライド
      /// </summary>
      protected override WebRequest GetWebRequest(Uri address)
        {
          WebRequest request = base.GetWebRequest(address);

          if (request is HttpWebRequest)
            {
              var httpWebRequest = request as HttpWebRequest;
              if(httpWebRequest != null)
                {
                  httpWebRequest.CookieContainer = this.cookieContainer;
                  httpWebRequest.AllowAutoRedirect = this.isAutoRedirectEnabled;
                }
            }

          return request;
        }
      /// <summary>
      /// 自動リダイレクトするか?
      /// </summary>
      public bool AutoRedirect
         {
            set { isAutoRedirectEnabled = value; }
            get { return isAutoRedirectEnabled; }
         }
    }

}