OSDN Git Service

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