OSDN Git Service

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