OSDN Git Service

3ed4017376a89e990756dc6f11ef2585b783b716
[coroid/NicoBrowser.git] / src / nicobrowser / NicoHttpClient.java
1 /*$Id$*/
2 package nicobrowser;
3
4 import java.net.URI;
5 import java.net.URISyntaxException;
6 import java.util.Set;
7 import java.util.TreeMap;
8 import java.util.regex.Matcher;
9 import nicobrowser.entity.NicoContent;
10 import nicobrowser.search.SortKind;
11 import nicobrowser.search.SortOrder;
12 import com.sun.syndication.feed.synd.SyndContentImpl;
13 import com.sun.syndication.feed.synd.SyndEntryImpl;
14 import com.sun.syndication.feed.synd.SyndFeed;
15 import com.sun.syndication.io.FeedException;
16 import com.sun.syndication.io.SyndFeedInput;
17 import java.io.BufferedInputStream;
18 import java.io.BufferedOutputStream;
19 import java.io.BufferedReader;
20 import java.io.File;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25 import java.io.Reader;
26 import java.io.StringReader;
27 import java.net.URL;
28 import java.net.URLEncoder;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.Enumeration;
32 import java.util.HashMap;
33 import java.util.LinkedHashMap;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.regex.Pattern;
37 import javax.swing.text.MutableAttributeSet;
38 import javax.swing.text.html.HTML;
39 import javax.swing.text.html.HTMLEditorKit;
40 import javax.swing.text.html.parser.ParserDelegator;
41 import javax.xml.parsers.DocumentBuilder;
42 import javax.xml.parsers.DocumentBuilderFactory;
43 import javax.xml.parsers.ParserConfigurationException;
44 import nicobrowser.entity.NicoContent.Status;
45 import nicobrowser.search.SearchKind;
46 import nicobrowser.search.SearchResult;
47 import nicobrowser.util.Result;
48 import nicobrowser.util.Util;
49 import org.apache.commons.io.FilenameUtils;
50 import org.apache.commons.lang.ArrayUtils;
51 import org.apache.commons.logging.Log;
52 import org.apache.commons.logging.LogFactory;
53 import org.apache.http.HttpEntity;
54 import org.apache.http.HttpException;
55 import org.apache.http.HttpHost;
56 import org.apache.http.HttpResponse;
57 import org.apache.http.HttpStatus;
58 import org.apache.http.NameValuePair;
59 import org.apache.http.client.entity.UrlEncodedFormEntity;
60 import org.apache.http.client.methods.HttpGet;
61 import org.apache.http.client.methods.HttpPost;
62 import org.apache.http.client.params.ClientPNames;
63 import org.apache.http.client.params.CookiePolicy;
64 import org.apache.http.conn.params.ConnRoutePNames;
65 import org.apache.http.cookie.Cookie;
66 import org.apache.http.entity.StringEntity;
67 import org.apache.http.impl.client.DefaultHttpClient;
68 import org.apache.http.impl.client.RedirectLocations;
69 import org.apache.http.message.BasicNameValuePair;
70 import org.apache.http.protocol.BasicHttpContext;
71 import org.apache.http.protocol.HttpContext;
72 import org.apache.http.util.EntityUtils;
73 import org.w3c.dom.Document;
74 import org.w3c.dom.Element;
75 import org.w3c.dom.NodeList;
76 import org.xml.sax.SAXException;
77
78 /**
79  *
80  * @author yuki
81  */
82 public class NicoHttpClient {
83
84     private static Log logger = LogFactory.getLog(NicoHttpClient.class);
85     private final DefaultHttpClient http;
86     private static final String LOGIN_PAGE =
87             "https://secure.nicovideo.jp/secure/login?site=niconico";
88     private static final String LOGOUT_PAGE =
89             "https://secure.nicovideo.jp/secure/logout";
90     private static final String WATCH_PAGE = "http://www.nicovideo.jp/watch/";
91     private static final String MY_LIST_PAGE_HEADER =
92             "http://www.nicovideo.jp/mylist/";
93     private static final String MOVIE_THUMBNAIL_PAGE_HEADER =
94             "http://www.nicovideo.jp/api/getthumbinfo/";
95     private static final String GET_FLV_INFO = "http://www.nicovideo.jp/api/getflv/";
96     private static final String SEARCH_HEAD = "http://www.nicovideo.jp/";
97     private static final String ADD_MYLIST_PAGE = "http://www.nicovideo.jp/mylist_add/video/";
98     private static final String GET_THREAD_KEY_PAGE = "http://www.nicovideo.jp/api/getthreadkey?thread=";
99
100     public NicoHttpClient() {
101         http = new DefaultHttpClient();
102         http.getParams().setParameter(
103                 ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
104     }
105
106     /**
107      * プロキシサーバを経由してアクセスする場合のコンストラクタ.
108      * @param host プロキシサーバのホスト名.
109      * @param port プロキシサーバで利用するポート番号.
110      */
111     public NicoHttpClient(String host, int port) {
112         this();
113         HttpHost proxy = new HttpHost(host, port);
114         http.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
115     }
116
117     /**
118      * ニコニコ動画へログインする.
119      * @param mail ログイン識別子(登録メールアドレス).
120      * @param password パスワード.
121      * @return 認証がOKであればtrue.
122      */
123     public boolean login(String mail, String password) throws URISyntaxException, HttpException, InterruptedException {
124         boolean auth = false;
125         HttpPost post = new HttpPost(LOGIN_PAGE);
126
127         try {
128             NameValuePair[] nvps = new NameValuePair[]{
129                 new BasicNameValuePair("mail", mail),
130                 new BasicNameValuePair("password", password),
131                 new BasicNameValuePair("next_url", "")
132             };
133             post.setEntity(new UrlEncodedFormEntity(Arrays.asList(nvps), "UTF-8"));
134
135             //post.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
136             HttpResponse response = http.execute(post);
137             logger.debug("ログインステータスコード: " + response.getStatusLine().getStatusCode());
138
139             // ログイン可否の判定.
140             HttpEntity entity = response.getEntity();
141             EntityUtils.consume(entity);
142             List<Cookie> cookies = http.getCookieStore().getCookies();
143             if (!cookies.isEmpty()) {
144                 auth = true;
145             }
146         } catch (IOException ex) {
147             logger.error("ログイン時に問題が発生", ex);
148         }
149         return auth;
150     }
151
152     /**
153      * ニコニコ動画からログアウトする.
154      * @return ログアウトに成功すればtrue.
155      */
156     public boolean logout() throws URISyntaxException, HttpException, InterruptedException {
157         boolean result = false;
158         HttpGet method = new HttpGet(LOGOUT_PAGE);
159         try {
160             HttpResponse response = http.execute(method);
161             logger.debug("ログアウトステータスコード: " + response.getStatusLine().getStatusCode());
162
163             if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
164                 result = true;
165             }
166             EntityUtils.consume(response.getEntity());
167         } catch (IOException ex) {
168             logger.error("ログアウト時に問題が発生", ex);
169         }
170         return result;
171     }
172
173     /**
174      * キーワード検索を行う.
175      * @param word 検索キーワード
176      * @param sort ソート種別
177      * @param order ソート順
178      * @page 検索結果ページのうち, 結果を返すページ.
179      * @return 検索結果.
180      */
181     public SearchResult search(SearchKind kind, String word, SortKind sort, SortOrder order, int page) throws
182             IOException {
183         logger.debug("検索:" + word);
184
185         InputStream is = null;
186         ArrayList<NicoContent> conts = new ArrayList<NicoContent>();
187         String url = SEARCH_HEAD + kind.getKey() + "/" + URLEncoder.encode(word, "UTF-8") + "?page=" + Integer.toString(
188                 page) + "&sort=" + sort.getKey() + "&order=" + order.getKey();
189
190         try {
191             HttpGet get = new HttpGet(url);
192             HttpResponse response;
193             response = http.execute(get);
194             is = new BufferedInputStream(response.getEntity().getContent());
195             assert is.markSupported();
196             is.mark(1024 * 1024);
197             List<Result> results = Util.parseSearchResult(is);
198             for (Result r : results) {
199                 NicoContent c = loadMyMovie(r.getId());
200                 if (c != null) {
201                     conts.add(c);
202                 }
203             }
204             is.reset();
205             TreeMap<Integer, String> otherPages = Util.getOtherPages(is);
206             return new SearchResult(conts, otherPages);
207         } catch (IOException ex) {
208             logger.error("検索結果処理時に例外発生", ex);
209             throw ex;
210         } finally {
211             if (is != null) {
212                 try {
213                     is.close();
214                 } catch (IOException ex) {
215                 }
216             }
217         }
218     }
219
220     /**
221      * 「マイリスト登録数ランキング(本日)」の動画一覧を取得する。
222      * @return 動画一覧.
223      */
224     public List<NicoContent> loadMyListDaily() throws URISyntaxException, HttpException, InterruptedException {
225         List<NicoContent> list = new ArrayList<NicoContent>();
226         String url = "http://www.nicovideo.jp/ranking/mylist/daily/all?rss=atom";
227         logger.debug("全動画サイトのマイリスト登録数ランキング(本日)[全体] : " + url);
228
229         HttpGet get = new HttpGet(url);
230
231         BufferedReader reader = null;
232         try {
233             HttpResponse response = http.execute(get);
234             reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
235             // BOMを読み捨て
236             // reader.skip(1);
237             list = getNicoContents(reader);
238             deleteRankString(list);
239             EntityUtils.consume(response.getEntity());
240         } catch (FeedException ex) {
241             logger.error("", ex);
242         } catch (IOException ex) {
243             logger.error("", ex);
244         } finally {
245             if (reader != null) {
246                 try {
247                     reader.close();
248                 } catch (IOException ex) {
249                     logger.error("", ex);
250                 }
251             }
252         }
253         return list;
254     }
255
256     /**
257      * ニコニコ動画のRSSからコンテンツリストを取得する.
258      * @param url 取得するrssのurl.
259      * @return コンテンツリスト.
260      */
261     public List<NicoContent> getContentsFromRss(String url) {
262         logger.debug("アクセスURL: " + url);
263         List<NicoContent> list = accessRssUrl(url);
264         if (url.contains("ranking")) {
265             deleteRankString(list);
266         }
267         return list;
268     }
269
270     /**
271      * 過去ログ取得用のキーを取得します.
272      * @param vi {@link #getVideoInfo(java.lang.String) }で取得したオブジェクト.
273      * @return 過去ログ取得用キー
274      * @throws IOException 取得に失敗した場合.
275      */
276     public String getWayBackKey(VideoInfo vi) throws IOException {
277         final String url = "http://flapi.nicovideo.jp/api/getwaybackkey?thread=" + vi.getThreadId();
278         final HttpGet get = new HttpGet(url);
279         HttpResponse response = http.execute(get);
280         String res;
281         try {
282             final int statusCode = response.getStatusLine().getStatusCode();
283             if (statusCode != HttpStatus.SC_OK) {
284                 throw new IOException("waybackkey get error " + statusCode);
285             }
286
287             final BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
288             res = br.readLine();
289             logger.debug("wayback get result text: " + res);
290         } finally {
291             EntityUtils.consume(response.getEntity());
292         }
293
294         final String keyWayBackKey = "waybackkey=";
295         final String[] keyValues = res.split("&");
296         for (String s : keyValues) {
297             final String[] kv = s.split("=");
298             if (keyWayBackKey.equals(kv[0])) {
299                 return kv[1];
300             }
301         }
302
303         throw new IOException("wayback key get fail: " + res);
304     }
305
306     /**
307      * rankingの場合、本当のタイトルの前に"第XX位:"の文字列が
308      * 挿入されているため, それを削る.
309      * @param list 対象のリスト.
310      */
311     private void deleteRankString(List<NicoContent> list) {
312         for (NicoContent c : list) {
313             String title = c.getTitle();
314             int offset = title.indexOf(":") + 1;
315             c.setTitle(title.substring(offset));
316         }
317     }
318
319     /**
320      * マイリストに登録した動画一覧の取得.
321      * 「公開」設定にしていないリストからは取得できない.
322      * ログインしていなくても取得可能.
323      * @param listNo マイリストNo.
324      * @return 動画一覧.
325      */
326     public List<NicoContent> loadMyList(String listNo) {
327         String url = MY_LIST_PAGE_HEADER + listNo + "?rss=atom";
328         logger.debug("マイリストURL: " + url);
329         return accessRssUrl(url);
330     }
331
332     /**
333      * コンテンツ概略のストリームを取得する.
334      * @param movieNo
335      * @return コンテンツ概略. 取得元でcloseすること.
336      * @throws IOException
337      */
338     public InputStream getThumbInfo(String movieNo) throws IOException {
339         String url = MOVIE_THUMBNAIL_PAGE_HEADER + movieNo;
340         logger.debug("動画サムネイルURL: " + url);
341
342         HttpGet get = new HttpGet(url);
343         HttpResponse response = http.execute(get);
344         return response.getEntity().getContent();
345
346     }
347
348     /**
349      * 動画番号を指定したコンテンツ情報の取得.
350      * @param movieNo 動画番号.
351      * @return コンテンツ情報.
352      */
353     public NicoContent loadMyMovie(String movieNo) {
354         NicoContent cont = null;
355         InputStream re = null;
356
357         try {
358             re = getThumbInfo(movieNo);
359             // ドキュメントビルダーファクトリを生成
360             DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
361             // ドキュメントビルダーを生成
362             DocumentBuilder builder = dbfactory.newDocumentBuilder();
363             // パースを実行してDocumentオブジェクトを取得
364             Document doc = builder.parse(re);
365             // ルート要素を取得(タグ名:site)
366             Element root = doc.getDocumentElement();
367
368             if ("fail".equals(root.getAttribute("status"))) {
369                 logger.warn("情報取得できません: " + movieNo);
370                 return null;
371             }
372
373             NodeList list2 = root.getElementsByTagName("thumb");
374             cont = new NicoContent();
375             Element element = (Element) list2.item(0);
376
377             String watch_url = ((Element) element.getElementsByTagName("watch_url").item(0)).getFirstChild().
378                     getNodeValue();
379             cont.setPageLink(watch_url);
380
381             String title = ((Element) element.getElementsByTagName("title").item(0)).getFirstChild().getNodeValue();
382             cont.setTitle(title);
383
384             // TODO 投稿日の設定
385 //            String first_retrieve = ((Element) element.getElementsByTagName("first_retrieve").item(0)).getFirstChild().getNodeValue();
386 //            cont.setPublishedDate(DateFormat.getInstance().parse(first_retrieve));
387 //
388 //        } catch (ParseException ex) {
389 //            Logger.getLogger(NicoHttpClient.class.getName()).log(Level.SEVERE, null, ex);
390         } catch (SAXException ex) {
391             logger.error("", ex);
392         } catch (IOException ex) {
393             logger.error("", ex);
394         } catch (ParserConfigurationException ex) {
395             logger.error("", ex);
396         } finally {
397             try {
398                 if (re != null) {
399                     re.close();
400                 }
401             } catch (IOException ex) {
402                 logger.error("", ex);
403             }
404         }
405         return cont;
406     }
407
408     private List<NicoContent> accessRssUrl(String url) {
409         List<NicoContent> contList = new ArrayList<NicoContent>();
410         HttpGet get = new HttpGet(url);
411         BufferedReader reader = null;
412         try {
413             HttpResponse response = http.execute(get);
414             reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
415             if (logger.isTraceEnabled()) {
416                 reader.mark(1024 * 1024);
417                 while (true) {
418                     String str = reader.readLine();
419                     if (str == null) {
420                         break;
421                     }
422                     logger.trace(str);
423                 }
424                 reader.reset();
425             }
426             contList = getNicoContents(reader);
427         } catch (FeedException ex) {
428             logger.warn("アクセスできません: " + url);
429             logger.debug("", ex);
430         } catch (IOException ex) {
431             logger.error("", ex);
432         } finally {
433             if (reader != null) {
434                 try {
435                     reader.close();
436                 } catch (IOException ex) {
437                     logger.error("", ex);
438                 }
439             }
440         }
441         return contList;
442     }
443
444     private List<NicoContent> getNicoContents(Reader reader) throws FeedException {
445         List<SyndEntryImpl> list = null;
446         SyndFeedInput input = new SyndFeedInput();
447         SyndFeed feed = input.build(reader);
448
449         list = (List<SyndEntryImpl>) feed.getEntries();
450
451         List<NicoContent> contList;
452         if (list == null) {
453             contList = new ArrayList<NicoContent>();
454         } else {
455             contList = createContentsList(list);
456         }
457         return contList;
458     }
459
460     private List<NicoContent> createContentsList(List<SyndEntryImpl> list) {
461         class CallBack extends HTMLEditorKit.ParserCallback {
462
463             private boolean descFlag;
464             private String imageLink = new String();
465             private StringBuilder description = new StringBuilder();
466
467             @Override
468             public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
469                 logger.debug("--------<" + t.toString() + ">--------");
470                 logger.debug(a);
471                 if (HTML.Tag.IMG.equals(t)) {
472                     imageLink = a.getAttribute(HTML.Attribute.SRC).toString();
473                 }
474             }
475
476             @Override
477             public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
478                 if (HTML.Tag.P.equals(t)) {
479                     if ("nico-description".equals(
480                             a.getAttribute(HTML.Attribute.CLASS).toString())) {
481                         descFlag = true;
482                     }
483                 }
484                 logger.debug("--------<" + t.toString() + ">--------");
485                 logger.debug(a);
486             }
487
488             @Override
489             public void handleEndTag(HTML.Tag t, int pos) {
490                 if (HTML.Tag.P.equals(t)) {
491                     descFlag = false;
492                 }
493                 logger.debug("--------</" + t.toString() + ">--------");
494             }
495
496             @Override
497             public void handleText(char[] data, int pos) {
498                 if (descFlag) {
499                     description.append(data);
500                 }
501                 logger.debug("--------TEXT--------");
502                 logger.debug(data);
503             }
504
505             private void printAttributes(MutableAttributeSet a) {
506                 Enumeration e = a.getAttributeNames();
507                 while (e.hasMoreElements()) {
508                     Object key = e.nextElement();
509                     logger.debug("---- " + key.toString() + " : " + a.getAttribute(key));
510                 }
511             }
512
513             public String getImageLink() {
514                 return imageLink;
515             }
516
517             public String getDescription() {
518                 return description.toString();
519             }
520         }
521
522         List<NicoContent> contList = new ArrayList<NicoContent>();
523
524         for (SyndEntryImpl entry : list) {
525             NicoContent content = new NicoContent();
526
527             String title = entry.getTitle();
528             content.setTitle(title);
529             content.setPageLink(entry.getLink());
530
531             // サムネイル画像リンクと説明文の取得
532             CallBack callBack = new CallBack();
533             for (SyndContentImpl sc : (List<SyndContentImpl>) entry.getContents()) {
534                 try {
535                     Reader reader = new StringReader(sc.getValue());
536                     new ParserDelegator().parse(reader, callBack, true);
537                 } catch (IOException ex) {
538                     logger.error("RSSの読み込み失敗: " + content.getTitle());
539                 }
540             }
541
542 // リストへ追加.
543             contList.add(content);
544         }
545         return contList;
546     }
547
548     /**
549      * FLVファイルのURLを取得する. ログインが必要.
550      * また, 実際にFLVファイルの実態をダウンロードするには
551      * 一度http://www.nicovideo.jp/watch/ビデオID に一度アクセスする必要があることに
552      * 注意.
553      * (参考: http://yusukebe.com/tech/archives/20070803/124356.html)
554      * @param videoId ニコニコ動画のビデオID.
555      * @return FLVファイル実体があるURL.
556      * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
557      */
558     public VideoInfo getVideoInfo(String videoId) throws IOException {
559         final GetRealVideoIdResult res = accessWatchPage(videoId);
560         final String realVideoId = res.videoId;
561
562         String accessUrl = GET_FLV_INFO + realVideoId;
563         if (realVideoId.startsWith("nm")) {
564             accessUrl += "?as3=1";
565         }
566         Map<String, String> map = getParameterMap(accessUrl);
567
568         LinkedHashMap<String, String> keyMap = new LinkedHashMap<String, String>();
569         if ("1".equals(map.get("needs_key"))) {
570             // 公式動画投稿者コメント取得用パラメータ.
571             keyMap = getParameterMap(GET_THREAD_KEY_PAGE + map.get(VideoInfo.KEY_THREAD_ID));
572         }
573         return new VideoInfo(realVideoId, res.title, map, keyMap);
574     }
575
576     private LinkedHashMap<String, String> getParameterMap(String accessUrl) throws IOException, IllegalStateException {
577         logger.debug("アクセス: " + accessUrl);
578         HttpGet get = new HttpGet(accessUrl);
579         String resultString;
580         BufferedReader reader = null;
581         try {
582             HttpResponse response = http.execute(get);
583             reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
584             String str;
585             StringBuilder strBuilder = new StringBuilder();
586             while ((str = reader.readLine()) != null) {
587                 strBuilder.append(str);
588             }
589             resultString = strBuilder.toString();
590             EntityUtils.consume(response.getEntity());
591             logger.debug(resultString);
592         } finally {
593             if (reader != null) {
594                 reader.close();
595             }
596         }
597         String[] params = resultString.split("&");
598         LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
599         for (String param : params) {
600             String[] elm = param.split("=");
601             map.put(elm[0], elm[1]);
602         }
603         return map;
604     }
605
606     /**
607      * watchページコンテンツからタイトルを抽出する.
608      * @param content watchページコンテンツのストリーム.
609      */
610     private String getTitleInWatchPage(InputStream content) throws IOException {
611         final String TITLE_PARSE_STR_START = "<title>";
612         BufferedReader br = new BufferedReader(new InputStreamReader(content, "UTF-8"));
613         String ret;
614         while ((ret = br.readLine()) != null) {
615             final int index = ret.indexOf(TITLE_PARSE_STR_START);
616             if (index >= 0) {
617                 String videoTitle = ret.substring(index + TITLE_PARSE_STR_START.length(), ret.indexOf(" ‐", index));
618                 return videoTitle;
619             }
620         }
621         return "";
622
623     }
624
625     private static class GetRealVideoIdResult {
626
627         private final String videoId;
628         private final String title;
629
630         private GetRealVideoIdResult(String videoId, String title) {
631             this.videoId = videoId;
632             this.title = title;
633         }
634     }
635
636     /**
637      * WATCHページへアクセスする. getflvを行うためには, 必ず事前にWATCHページへアクセスしておく必要があるため.
638      * WATCHページ参照時にリダイレクトが発生する(so動画ではスレッドIDのWATCHページにリダイレクトされる)場合には
639      * そちらのページにアクセスし、そのスレッドIDをrealIdとして返します.
640      * @param videoId 取得したいビデオのビデオID.
641      * @return 実際のアクセスに必要なIDと、タイトル. タイトルはいんきゅばす互換用です.
642      * @throws IOException アクセスに失敗した場合. 有料動画などがこれに含まれます.
643      */
644     private GetRealVideoIdResult accessWatchPage(String videoId) throws IOException {
645         String realId = videoId;
646         String title;
647         String watchUrl = WATCH_PAGE + videoId;
648         logger.debug("アクセス: " + watchUrl);
649         final HttpGet get = new HttpGet(watchUrl);
650         final HttpContext context = new BasicHttpContext();
651         final HttpResponse response = http.execute(get, context);
652         try {
653             final RedirectLocations rl = (RedirectLocations) context.getAttribute(
654                     "http.protocol.redirect-locations");
655             // 通常の動画(sm動画など)はリダイレクトが発生しないためnullになる
656             if (rl != null) {
657                 final List<URI> locations = rl.getAll();
658                 logger.debug("リダイレクト数: " + locations.size());
659
660                 // so動画はスレッドIDのページへリダイレクトされる
661                 if (locations.size() == 1) {
662                     realId = locations.get(0).toString().replace(WATCH_PAGE, "");
663                 } else if (locations.size() > 1) {
664                     throw new IOException("有料動画と思われるため処理を中断しました: " + ArrayUtils.toString(locations));
665                 }
666             }
667
668             title = getTitleInWatchPage(response.getEntity().getContent());
669         } finally {
670             EntityUtils.consume(response.getEntity());
671         }
672         return new GetRealVideoIdResult(realId, title);
673     }
674
675     /**
676      * ニコニコ動画から動画ファイルをダウンロードする.
677      * @param vi getVideoInfoメソッドで取得したオブジェクト.
678      * @param saveDir ダウンロードしたファイルを保存するディレクトリ.
679      * @param np 保存するファイル名の命名規則. 拡張子は別途付与されるため不要.
680      * @param nowStatus ダウンロードしようとしている動画ファイルの, 現在のステータス.
681      * @param needLowFile エコノミー動画をダウンロードするのであればtrue.
682      * @return この処理を行った後の, 対象ファイルのステータス.
683      * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
684      */
685     public GetFlvResult getFlvFile(VideoInfo vi, File saveDir, NamePattern np, Status nowStatus, boolean needLowFile,
686             ProgressListener listener) throws IOException, URISyntaxException, HttpException, InterruptedException {
687
688         final URL notifierUrl = vi.getSmileUrl();
689
690         String userName = null;
691         if (notifierUrl != null) {
692             HttpGet get = new HttpGet(notifierUrl.toString());
693             HttpResponse response = http.execute(get);
694             userName = Util.getUserName(response.getEntity().getContent());
695             EntityUtils.consume(response.getEntity());
696         }
697
698         final URL url = vi.getVideoUrl();
699         if (nowStatus == Status.GET_LOW || !needLowFile) {
700             if (url.toString().contains("low")) {
701                 logger.info("エコノミー動画のためスキップ: " + vi.getRealVideoId());
702                 return new GetFlvResult(null, nowStatus, userName);
703             }
704         }
705         final boolean isNotLow = !url.toString().contains("low");
706
707         final File downloadFile = new File(saveDir, np.createFileName(vi.getRealVideoId(), isNotLow));
708
709         HttpGet get = new HttpGet(url.toURI());
710         HttpResponse response = http.execute(get);
711         String contentType = response.getEntity().getContentType().getValue();
712         logger.debug(contentType);
713         logger.debug(downloadFile.toString());
714         if ("text/plain".equals(contentType) || "text/html".equals(contentType)) {
715             logger.error("取得できませんでした. サーバが混みあっている可能性があります: " + vi.getRealVideoId());
716             EntityUtils.consume(response.getEntity());
717             return new GetFlvResult(null, Status.GET_INFO, userName);
718         }
719         String ext = Util.getExtention(contentType);
720         final long fileSize = response.getEntity().getContentLength();
721
722         final int BUF_SIZE = 1024 * 32;
723         BufferedInputStream in = new BufferedInputStream(response.getEntity().getContent());
724
725         File file = new File(downloadFile.toString() + "." + ext);
726         logger.info("保存します(" + fileSize / 1024 + "KB): " + file.getPath());
727         FileOutputStream fos = new FileOutputStream(file);
728         BufferedOutputStream out = new BufferedOutputStream(fos);
729
730         long downloadSize = 0;
731         int i;
732         byte[] buffer = new byte[BUF_SIZE];
733         while ((i = in.read(buffer)) != -1) {
734             out.write(buffer, 0, i);
735             downloadSize += i;
736             listener.progress(fileSize, downloadSize);
737             if (listener.getCancel()) {
738                 return new GetFlvResult(null, Status.GET_INFO, userName);
739             }
740         }
741
742         EntityUtils.consume(response.getEntity());
743         out.close();
744         in.close();
745         if (url.toString().contains("low")) {
746             return new GetFlvResult(file, Status.GET_LOW, userName);
747         }
748         return new GetFlvResult(file, Status.GET_FILE, userName);
749     }
750
751     /**
752      * ニコニコ動画から動画ファイルをダウンロードする.
753      * @param vi getVideoInfoメソッドで取得したオブジェクト.
754      * @param fileName ダウンロード後のファイル名. 拡張子は別途付与されるため不要.
755      * @param nowStatus ダウンロードしようとしている動画ファイルの, 現在のステータス.
756      * @param needLowFile エコノミー動画をダウンロードするのであればtrue.
757      * @return この処理を行った後の, 対象ファイルのステータス.
758      * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
759      */
760     public GetFlvResult getFlvFile(VideoInfo vi, String fileName, Status nowStatus, boolean needLowFile,
761             ProgressListener listener) throws IOException, URISyntaxException, HttpException, InterruptedException {
762         String file = FilenameUtils.getName(fileName);
763         String dir = fileName.substring(0, fileName.length() - file.length());
764         NamePattern np = new NamePattern(file, "", "", "");
765         return getFlvFile(vi, new File(dir), np, nowStatus, needLowFile, listener);
766     }
767
768     /**
769      * ニコニコ動画から動画ファイルをダウンロードする.
770      * @param vi getVideoInfoメソッドで取得したオブジェクト.
771      * @param fileName ダウンロード後のファイル名. 拡張子は別途付与されるため不要.
772      * @return この処理を行った後の, 対象ファイルのステータス.
773      * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
774      */
775     public GetFlvResult getFlvFile(VideoInfo vi, String fileName, ProgressListener listener) throws IOException,
776             URISyntaxException,
777             HttpException, InterruptedException {
778         return getFlvFile(vi, fileName, Status.GET_INFO, true, listener);
779     }
780
781     /**
782      * ニコニコ動画から動画ファイルをダウンロードする.
783      * ファイル名はビデオID名となる.
784      * @param vi getVideoInfoメソッドで取得したオブジェクト.
785      * @return この処理を行った後の, 対象ファイルのステータス.
786      * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
787      */
788     public GetFlvResult getFlvFile(VideoInfo vi) throws IOException, URISyntaxException, HttpException,
789             InterruptedException {
790         return getFlvFile(vi, vi.getRealVideoId(), Status.GET_INFO, true, ProgressListener.EMPTY_LISTENER);
791     }
792
793     public File getCommentFile(VideoInfo vi, String fileName, WayBackInfo wayback) throws Exception {
794         return downloadComment(vi, fileName, false, wayback);
795     }
796
797     public File getTCommentFile(VideoInfo vi, String fileName) throws Exception {
798         return downloadComment(vi, fileName, true, null, true);
799     }
800
801     private File downloadComment(VideoInfo vi, String fileName, boolean isTcomm, WayBackInfo wayback) throws Exception {
802         return downloadComment(vi, fileName, isTcomm, wayback, false);
803     }
804
805     private File downloadComment(VideoInfo vi, String fileName, boolean isTcomm, WayBackInfo wayback, boolean oldVersion)
806             throws Exception {
807         HttpResponse response = null;
808         BufferedOutputStream bos = null;
809
810         try {
811             final HttpPost post = new HttpPost(vi.getMessageUrl().toString());
812             final String param;
813             if (oldVersion || isTcomm) {
814                 param = createCommendDownloadParameter20101222(vi, isTcomm, wayback);
815             } else {
816                 param = createCommendDownloadParameter(vi, wayback);
817             }
818             final StringEntity se = new StringEntity(param);
819             post.setEntity(se);
820             response = http.execute(post);
821             final InputStream is = response.getEntity().getContent();
822             final BufferedInputStream bis = new BufferedInputStream(is);
823
824             final String outputFileName = (fileName.endsWith(".xml")) ? fileName : fileName + ".xml";
825             bos = new BufferedOutputStream(new FileOutputStream(outputFileName));
826
827             final byte[] buf = new byte[1024 * 1024];
828             int read;
829             while ((read = bis.read(buf, 0, buf.length)) > 0) {
830                 bos.write(buf, 0, read);
831             }
832
833             return new File(outputFileName);
834         } catch (Exception e) {
835             throw new Exception("コメントダウンロードに失敗しました。", e);
836         } finally {
837             if (response != null) {
838                 EntityUtils.consume(response.getEntity());
839             }
840             if (bos != null) {
841                 bos.close();
842             }
843         }
844     }
845
846     private String createCommendDownloadParameter(VideoInfo vi, WayBackInfo wayback) {
847         final String quote = "\"";
848         final Map<String, String> th = new HashMap<String, String>();
849         th.put("thread", vi.getThreadId());
850         th.put("version", "20090904");
851         th.put("user_id", vi.getUserId());
852
853         final Map<String, String> leaf = new HashMap<String, String>();
854         leaf.put("thread", vi.getThreadId());
855         leaf.put("user_id", vi.getUserId());
856
857         // TODO videoLengh は秒数が入っているんだっけ?
858         final int length = (int) Math.ceil(vi.getVideoLength() / 60.0);
859         final String element = "0-" + length + ":100," + vi.getResFrom();
860
861         final StringBuilder str = new StringBuilder();
862         str.append("<packet>");
863
864         str.append("<thread");
865         for (String k : th.keySet()) {
866             final String v = th.get(k);
867             str.append(" ");
868             str.append(k);
869             str.append("=");
870             str.append(quote);
871             str.append(v);
872             str.append(quote);
873         }
874         str.append(" />");
875
876         str.append("<thread_leaves");
877         for (String k : leaf.keySet()) {
878             final String v = th.get(k);
879             str.append(" ");
880             str.append(k);
881             str.append("=");
882             str.append(quote);
883             str.append(v);
884             str.append(quote);
885         }
886         str.append(">");
887         str.append(element);
888         str.append("</thread_leaves>");
889
890         str.append("</packet>");
891
892         return str.toString();
893     }
894
895     /**
896      * 2010/12/22 までのコメント表示仕様に基づいた取得パラメータ生成.
897      * 「コメントの量を減らす」にチェックを入れた場合は現在でもこれが用いられているはず.
898      */
899     private String createCommendDownloadParameter20101222(VideoInfo vi, boolean isTcomm, WayBackInfo wayback) {
900         final String tcommStr = (isTcomm) ? "fork=\"1\" " : "";
901         // TODO wayBackStr 使用するのを忘れている
902         final String wayBackStr = (wayback != null) ? "when=" + "\"" + wayback.getTime() + "\"" + " waybackkey=" + "\""
903                 + wayback.getKey() + " " : "";
904         StringBuilder builder = new StringBuilder();
905         Set<String> keySet = vi.getKeyMap().keySet();
906         for (String key : keySet) {
907             builder.append(key).append("=\"").append(vi.getKeyMap().get(key)).append("\" ");
908         }
909         final String officialOption = builder.toString();
910
911         return "<thread " + VideoInfo.KEY_USER_ID + "=\"" + vi.getUserId() + "\" res_from=\"" + (-1 * vi.getResFrom())
912                 + "\" version=\"20061206\" thread=\"" + vi.getThreadId() + "\" " + tcommStr + officialOption + "/>";
913     }
914
915     /**
916      * 動画をマイリストへ登録する. ログインが必要.
917      * @param myListId 登録するマイリストのID.
918      * @param videoId 登録する動画ID.
919      * @throws IOException 登録に失敗した.
920      */
921     public void addMyList(String myListId, String videoId) throws IOException {
922         String itemType = null;
923         String itemId = null;
924         String token = null;
925         HttpGet get = new HttpGet(ADD_MYLIST_PAGE + videoId);
926         HttpResponse response = http.execute(get);
927         HttpEntity entity = response.getEntity();
928         try {
929             InputStream is = entity.getContent();
930             BufferedReader reader = new BufferedReader(new InputStreamReader(is));
931             String line;
932
933             Pattern pattern = Pattern.compile("input type=\"hidden\" name=\"item_type\" value=\"(.+)\"");
934             while ((line = reader.readLine()) != null) {
935                 Matcher m = pattern.matcher(line);
936                 if (m.find()) {
937                     itemType = m.group(1);
938                     break;
939                 }
940             }
941
942             pattern = Pattern.compile("input type=\"hidden\" name=\"item_id\" value=\"(.+)\"");
943             while ((line = reader.readLine()) != null) {
944                 Matcher m = pattern.matcher(line);
945                 if (m.find()) {
946                     itemId = m.group(1);
947                     break;
948                 }
949             }
950
951             pattern = Pattern.compile("NicoAPI\\.token = \"(.*)\";");
952             while ((line = reader.readLine()) != null) {
953                 Matcher m = pattern.matcher(line);
954                 if (m.find()) {
955                     token = m.group(1);
956                     break;
957                 }
958             }
959         } finally {
960             EntityUtils.consume(entity);
961         }
962
963         if (itemType == null || itemId == null || token == null) {
964             throw new IOException("マイリスト登録に必要な情報が取得できませんでした。 "
965                     + "マイリスト:" + myListId + ", 動画ID:" + videoId + ", item_type:" + itemType + ", item_id:" + itemId
966                     + ", token:" + token);
967         }
968
969         StringEntity se = new StringEntity(
970                 "group_id=" + myListId
971                 + "&item_type=" + itemType
972                 + "&item_id=" + itemId
973                 + "&description=" + ""
974                 + "&token=" + token);
975
976         HttpPost post = new HttpPost("http://www.nicovideo.jp/api/mylist/add");
977         post.setHeader("Content-Type", "application/x-www-form-urlencoded");
978         post.setEntity(se);
979         response = http.execute(post);
980         int statusCode = response.getStatusLine().getStatusCode();
981         EntityUtils.consume(response.getEntity());
982         if (statusCode != HttpStatus.SC_OK) {
983             throw new IOException("マイリスト登録に失敗" + "マイリスト:" + myListId + ", 動画ID:" + videoId);
984         }
985     }
986 }