5 import java.net.URISyntaxException;
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;
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;
27 import java.net.URLEncoder;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.EnumSet;
31 import java.util.Enumeration;
32 import java.util.HashMap;
33 import java.util.LinkedHashMap;
34 import java.util.List;
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;
82 public class NicoHttpClient {
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://ext.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=";
100 public NicoHttpClient() {
101 http = new DefaultHttpClient();
102 http.getParams().setParameter(
103 ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
107 * プロキシサーバを経由してアクセスする場合のコンストラクタ.
108 * @param host プロキシサーバのホスト名.
109 * @param port プロキシサーバで利用するポート番号.
111 public NicoHttpClient(String host, int port) {
113 HttpHost proxy = new HttpHost(host, port);
114 http.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
119 * @param mail ログイン識別子(登録メールアドレス).
120 * @param password パスワード.
121 * @return 認証がOKであればtrue.
123 public boolean login(String mail, String password) throws InterruptedException {
124 boolean auth = false;
125 HttpPost post = new HttpPost(LOGIN_PAGE);
128 NameValuePair[] nvps = new NameValuePair[]{
129 new BasicNameValuePair("mail", mail),
130 new BasicNameValuePair("password", password),
131 new BasicNameValuePair("next_url", "")
133 post.setEntity(new UrlEncodedFormEntity(Arrays.asList(nvps), "UTF-8"));
135 //post.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
136 HttpResponse response = http.execute(post);
137 logger.debug("ログインステータスコード: " + response.getStatusLine().getStatusCode());
140 HttpEntity entity = response.getEntity();
141 EntityUtils.consume(entity);
142 List<Cookie> cookies = http.getCookieStore().getCookies();
143 if (!cookies.isEmpty()) {
146 } catch (IOException ex) {
147 logger.error("ログイン時に問題が発生", ex);
154 * @return ログアウトに成功すればtrue.
156 public boolean logout() throws URISyntaxException, HttpException, InterruptedException {
157 boolean result = false;
158 HttpGet method = new HttpGet(LOGOUT_PAGE);
160 HttpResponse response = http.execute(method);
161 logger.debug("ログアウトステータスコード: " + response.getStatusLine().getStatusCode());
163 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
166 EntityUtils.consume(response.getEntity());
167 } catch (IOException ex) {
168 logger.error("ログアウト時に問題が発生", ex);
175 * @param word 検索キーワード
178 * @page 検索結果ページのうち, 結果を返すページ.
181 public SearchResult search(SearchKind kind, String word, SortKind sort, SortOrder order, int page) throws
183 logger.debug("検索:" + word);
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();
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());
205 TreeMap<Integer, String> otherPages = Util.getOtherPages(is);
206 return new SearchResult(conts, otherPages);
207 } catch (IOException ex) {
208 logger.error("検索結果処理時に例外発生", ex);
214 } catch (IOException ex) {
221 * 「マイリスト登録数ランキング(本日)」の動画一覧を取得する。
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);
229 HttpGet get = new HttpGet(url);
231 BufferedReader reader = null;
233 HttpResponse response = http.execute(get);
234 reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
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);
245 if (reader != null) {
248 } catch (IOException ex) {
249 logger.error("", ex);
257 * ニコニコ動画のRSSからコンテンツリストを取得する.
258 * @param url 取得するrssのurl.
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);
272 * @param vi {@link #getVideoInfo(java.lang.String) }で取得したオブジェクト.
274 * @throws IOException 取得に失敗した場合.
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);
282 final int statusCode = response.getStatusLine().getStatusCode();
283 if (statusCode != HttpStatus.SC_OK) {
284 throw new IOException("waybackkey get error " + statusCode);
287 final BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
289 logger.debug("wayback get result text: " + res);
291 EntityUtils.consume(response.getEntity());
294 final String keyWayBackKey = "waybackkey";
295 final String[] keyValues = res.split("&");
296 for (String s : keyValues) {
297 final String[] kv = s.split("=");
298 if (kv.length == 2) {
299 if (keyWayBackKey.equals(kv[0])) {
305 throw new IOException("pick up no waybackkey: " + res);
309 * rankingの場合、本当のタイトルの前に"第XX位:"の文字列が
311 * @param list 対象のリスト.
313 private void deleteRankString(List<NicoContent> list) {
314 for (NicoContent c : list) {
315 String title = c.getTitle();
316 int offset = title.indexOf(":") + 1;
317 c.setTitle(title.substring(offset));
323 * 「公開」設定にしていないリストからは取得できない.
325 * @param listNo マイリストNo.
328 public List<NicoContent> loadMyList(String listNo) {
329 String url = MY_LIST_PAGE_HEADER + listNo + "?rss=atom";
330 logger.debug("マイリストURL: " + url);
331 return accessRssUrl(url);
335 * コンテンツ概略のストリームを取得する.
337 * @return コンテンツ概略. 取得元でcloseすること.
338 * @throws IOException
340 public InputStream getThumbInfo(String movieNo) throws IOException {
341 String url = MOVIE_THUMBNAIL_PAGE_HEADER + movieNo;
342 logger.debug("動画サムネイルURL: " + url);
344 HttpGet get = new HttpGet(url);
345 HttpResponse response = http.execute(get);
346 return response.getEntity().getContent();
351 * 動画番号を指定したコンテンツ情報の取得.
352 * @param movieNo 動画番号.
355 public NicoContent loadMyMovie(String movieNo) {
356 NicoContent cont = null;
357 InputStream re = null;
360 re = getThumbInfo(movieNo);
361 // ドキュメントビルダーファクトリを生成
362 DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
364 DocumentBuilder builder = dbfactory.newDocumentBuilder();
365 // パースを実行してDocumentオブジェクトを取得
366 Document doc = builder.parse(re);
367 // ルート要素を取得(タグ名:site)
368 Element root = doc.getDocumentElement();
370 if ("fail".equals(root.getAttribute("status"))) {
371 logger.warn("情報取得できません: " + movieNo);
375 NodeList list2 = root.getElementsByTagName("thumb");
376 cont = new NicoContent();
377 Element element = (Element) list2.item(0);
379 String watch_url = ((Element) element.getElementsByTagName("watch_url").item(0)).getFirstChild().
381 cont.setPageLink(watch_url);
383 String title = ((Element) element.getElementsByTagName("title").item(0)).getFirstChild().getNodeValue();
384 cont.setTitle(title);
387 // String first_retrieve = ((Element) element.getElementsByTagName("first_retrieve").item(0)).getFirstChild().getNodeValue();
388 // cont.setPublishedDate(DateFormat.getInstance().parse(first_retrieve));
390 // } catch (ParseException ex) {
391 // Logger.getLogger(NicoHttpClient.class.getName()).log(Level.SEVERE, null, ex);
392 } catch (SAXException ex) {
393 logger.error("", ex);
394 } catch (IOException ex) {
395 logger.error("", ex);
396 } catch (ParserConfigurationException ex) {
397 logger.error("", ex);
403 } catch (IOException ex) {
404 logger.error("", ex);
410 private List<NicoContent> accessRssUrl(String url) {
411 List<NicoContent> contList = new ArrayList<NicoContent>();
412 HttpGet get = new HttpGet(url);
413 BufferedReader reader = null;
415 HttpResponse response = http.execute(get);
416 reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
417 if (logger.isTraceEnabled()) {
418 reader.mark(1024 * 1024);
420 String str = reader.readLine();
428 contList = getNicoContents(reader);
429 } catch (FeedException ex) {
430 logger.warn("アクセスできません: " + url);
431 logger.debug("", ex);
432 } catch (IOException ex) {
433 logger.error("", ex);
435 if (reader != null) {
438 } catch (IOException ex) {
439 logger.error("", ex);
446 private List<NicoContent> getNicoContents(Reader reader) throws FeedException {
447 SyndFeedInput input = new SyndFeedInput();
448 SyndFeed feed = input.build(reader);
450 @SuppressWarnings("unchecked")
451 final List<SyndEntryImpl> list = (List<SyndEntryImpl>) feed.getEntries();
453 List<NicoContent> contList;
455 contList = new ArrayList<NicoContent>();
457 contList = createContentsList(list);
462 @SuppressWarnings("unchecked")
463 private List<NicoContent> createContentsList(List<SyndEntryImpl> list) {
464 class CallBack extends HTMLEditorKit.ParserCallback {
466 private boolean descFlag;
467 private String imageLink = new String();
468 private StringBuilder description = new StringBuilder();
471 public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
472 logger.debug("--------<" + t.toString() + ">--------");
474 if (HTML.Tag.IMG.equals(t)) {
475 imageLink = a.getAttribute(HTML.Attribute.SRC).toString();
480 public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
481 if (HTML.Tag.P.equals(t)) {
482 if ("nico-description".equals(
483 a.getAttribute(HTML.Attribute.CLASS).toString())) {
487 logger.debug("--------<" + t.toString() + ">--------");
492 public void handleEndTag(HTML.Tag t, int pos) {
493 if (HTML.Tag.P.equals(t)) {
496 logger.debug("--------</" + t.toString() + ">--------");
500 public void handleText(char[] data, int pos) {
502 description.append(data);
504 logger.debug("--------TEXT--------");
508 private void printAttributes(MutableAttributeSet a) {
509 final Enumeration<?> e = a.getAttributeNames();
510 while (e.hasMoreElements()) {
511 Object key = e.nextElement();
512 logger.debug("---- " + key.toString() + " : " + a.getAttribute(key));
516 public String getImageLink() {
520 public String getDescription() {
521 return description.toString();
525 List<NicoContent> contList = new ArrayList<NicoContent>();
527 for (SyndEntryImpl entry : list) {
528 NicoContent content = new NicoContent();
530 String title = entry.getTitle();
531 content.setTitle(title);
532 content.setPageLink(entry.getLink());
535 CallBack callBack = new CallBack();
536 for (SyndContentImpl sc : (List<SyndContentImpl>) entry.getContents()) {
538 Reader reader = new StringReader(sc.getValue());
539 new ParserDelegator().parse(reader, callBack, true);
540 } catch (IOException ex) {
541 logger.error("RSSの読み込み失敗: " + content.getTitle());
546 contList.add(content);
552 * FLVファイルのURLを取得する. ログインが必要.
553 * また, 実際にFLVファイルの実態をダウンロードするには
554 * 一度http://www.nicovideo.jp/watch/ビデオID に一度アクセスする必要があることに
556 * (参考: http://yusukebe.com/tech/archives/20070803/124356.html)
557 * @param videoId ニコニコ動画のビデオID.
558 * @return FLVファイル実体があるURL.
559 * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
561 public VideoInfo getVideoInfo(String videoId) throws IOException {
562 final GetRealVideoIdResult res = accessWatchPage(videoId);
563 final String realVideoId = res.videoId;
565 String accessUrl = GET_FLV_INFO + realVideoId;
566 if (realVideoId.startsWith("nm")) {
567 accessUrl += "?as3=1";
569 Map<String, String> map = getParameterMap(accessUrl);
571 LinkedHashMap<String, String> keyMap = new LinkedHashMap<String, String>();
572 if ("1".equals(map.get("needs_key"))) {
573 // 公式動画投稿者コメント取得用パラメータ.
574 keyMap = getParameterMap(GET_THREAD_KEY_PAGE + map.get(VideoInfo.KEY_THREAD_ID));
576 return new VideoInfo(realVideoId, res.title, map, keyMap);
579 private LinkedHashMap<String, String> getParameterMap(String accessUrl) throws IOException, IllegalStateException {
580 logger.debug("アクセス: " + accessUrl);
581 HttpGet get = new HttpGet(accessUrl);
583 BufferedReader reader = null;
585 HttpResponse response = http.execute(get);
586 reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
588 StringBuilder strBuilder = new StringBuilder();
589 while ((str = reader.readLine()) != null) {
590 strBuilder.append(str);
592 resultString = strBuilder.toString();
593 EntityUtils.consume(response.getEntity());
594 logger.debug(resultString);
596 if (reader != null) {
600 String[] params = resultString.split("&");
601 LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
602 for (String param : params) {
603 String[] elm = param.split("=");
604 map.put(elm[0], elm[1]);
610 * watchページコンテンツからタイトルを抽出する.
611 * @param content watchページコンテンツのストリーム.
613 private String getTitleInWatchPage(InputStream content) throws IOException {
614 final String TITLE_PARSE_STR_START = "<title>";
615 BufferedReader br = new BufferedReader(new InputStreamReader(content, "UTF-8"));
617 while ((ret = br.readLine()) != null) {
618 final int index = ret.indexOf(TITLE_PARSE_STR_START);
620 String videoTitle = ret.substring(index + TITLE_PARSE_STR_START.length(), ret.indexOf(" ‐", index));
628 private static class GetRealVideoIdResult {
630 private final String videoId;
631 private final String title;
633 private GetRealVideoIdResult(String videoId, String title) {
634 this.videoId = videoId;
640 * WATCHページへアクセスする. getflvを行うためには, 必ず事前にWATCHページへアクセスしておく必要があるため.
641 * WATCHページ参照時にリダイレクトが発生する(so動画ではスレッドIDのWATCHページにリダイレクトされる)場合には
642 * そちらのページにアクセスし、そのスレッドIDをrealIdとして返します.
643 * @param videoId 取得したいビデオのビデオID.
644 * @return 実際のアクセスに必要なIDと、タイトル. タイトルはいんきゅばす互換用です.
645 * @throws IOException アクセスに失敗した場合. 有料動画などがこれに含まれます.
647 private GetRealVideoIdResult accessWatchPage(String videoId) throws IOException {
648 String realId = videoId;
650 String watchUrl = WATCH_PAGE + videoId;
651 logger.debug("アクセス: " + watchUrl);
652 final HttpGet get = new HttpGet(watchUrl);
653 final HttpContext context = new BasicHttpContext();
654 final HttpResponse response = http.execute(get, context);
656 final RedirectLocations rl = (RedirectLocations) context.getAttribute(
657 "http.protocol.redirect-locations");
658 // 通常の動画(sm動画など)はリダイレクトが発生しないためnullになる
660 final List<URI> locations = rl.getAll();
661 logger.debug("リダイレクト数: " + locations.size());
663 // so動画はスレッドIDのページへリダイレクトされる
664 if (locations.size() == 1) {
665 realId = locations.get(0).toString().replace(WATCH_PAGE, "");
666 } else if (locations.size() > 1) {
667 throw new IOException("有料動画と思われるため処理を中断しました: " + ArrayUtils.toString(locations));
671 title = getTitleInWatchPage(response.getEntity().getContent());
673 EntityUtils.consume(response.getEntity());
675 return new GetRealVideoIdResult(realId, title);
679 * ニコニコ動画から動画ファイルをダウンロードする.
680 * @param vi getVideoInfoメソッドで取得したオブジェクト.
681 * @param saveDir ダウンロードしたファイルを保存するディレクトリ.
682 * @param np 保存するファイル名の命名規則. 拡張子は別途付与されるため不要.
683 * @param nowStatus ダウンロードしようとしている動画ファイルの, 現在のステータス.
684 * @param needLowFile エコノミー動画をダウンロードするのであればtrue.
685 * @return この処理を行った後の, 対象ファイルのステータス.
686 * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
688 public GetFlvResult getFlvFile(VideoInfo vi, File saveDir, NamePattern np, Status nowStatus, boolean needLowFile,
689 ProgressListener listener) throws IOException, URISyntaxException, HttpException, InterruptedException {
691 final URL notifierUrl = vi.getSmileUrl();
693 String userName = null;
694 if (notifierUrl != null) {
695 HttpGet get = new HttpGet(notifierUrl.toString());
696 HttpResponse response = http.execute(get);
697 userName = Util.getUserName(response.getEntity().getContent());
698 EntityUtils.consume(response.getEntity());
701 final URL url = vi.getVideoUrl();
702 if (nowStatus == Status.GET_LOW || !needLowFile) {
703 if (url.toString().contains("low")) {
704 logger.info("エコノミー動画のためスキップ: " + vi.getRealVideoId());
705 return new GetFlvResult(null, nowStatus, userName);
708 final boolean isNotLow = !url.toString().contains("low");
710 final File downloadFile = new File(saveDir, np.createFileName(vi.getRealVideoId(), isNotLow));
712 HttpGet get = new HttpGet(url.toURI());
713 HttpResponse response = http.execute(get);
714 String contentType = response.getEntity().getContentType().getValue();
715 logger.debug(contentType);
716 logger.debug(downloadFile.toString());
717 if ("text/plain".equals(contentType) || "text/html".equals(contentType)) {
718 logger.error("取得できませんでした. サーバが混みあっている可能性があります: " + vi.getRealVideoId());
719 EntityUtils.consume(response.getEntity());
720 return new GetFlvResult(null, Status.GET_INFO, userName);
722 String ext = Util.getExtention(contentType);
723 final long fileSize = response.getEntity().getContentLength();
725 final int BUF_SIZE = 1024 * 512;
726 BufferedInputStream in = null;
727 BufferedOutputStream out = null;
728 File file = new File(downloadFile.toString() + "." + ext);
730 in = new BufferedInputStream(response.getEntity().getContent(), BUF_SIZE);
732 logger.info("保存します(" + fileSize / 1024 + "KB): " + file.getPath());
733 FileOutputStream fos = new FileOutputStream(file);
734 out = new BufferedOutputStream(fos, BUF_SIZE);
736 long downloadSize = 0;
738 byte[] buffer = new byte[BUF_SIZE];
739 while ((i = in.read(buffer)) != -1) {
740 out.write(buffer, 0, i);
742 listener.progress(fileSize, downloadSize);
743 if (Thread.interrupted()) {
744 logger.info("中断します");
745 throw new InterruptedException("中断しました");
752 EntityUtils.consume(response.getEntity());
758 if (url.toString().contains("low")) {
759 return new GetFlvResult(file, Status.GET_LOW, userName);
761 return new GetFlvResult(file, Status.GET_FILE, userName);
765 * ニコニコ動画から動画ファイルをダウンロードする.
766 * @param vi getVideoInfoメソッドで取得したオブジェクト.
767 * @param fileName ダウンロード後のファイル名. 拡張子は別途付与されるため不要.
768 * @param nowStatus ダウンロードしようとしている動画ファイルの, 現在のステータス.
769 * @param needLowFile エコノミー動画をダウンロードするのであればtrue.
770 * @return この処理を行った後の, 対象ファイルのステータス.
771 * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
773 public GetFlvResult getFlvFile(VideoInfo vi, String fileName, Status nowStatus, boolean needLowFile,
774 ProgressListener listener) throws IOException, URISyntaxException, HttpException, InterruptedException {
775 String file = FilenameUtils.getName(fileName);
776 String dir = fileName.substring(0, fileName.length() - file.length());
777 NamePattern np = new NamePattern(file, "", "", "");
778 return getFlvFile(vi, new File(dir), np, nowStatus, needLowFile, listener);
782 * ニコニコ動画から動画ファイルをダウンロードする.
783 * @param vi getVideoInfoメソッドで取得したオブジェクト.
784 * @param fileName ダウンロード後のファイル名. 拡張子は別途付与されるため不要.
785 * @return この処理を行った後の, 対象ファイルのステータス.
786 * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
788 public GetFlvResult getFlvFile(VideoInfo vi, String fileName, ProgressListener listener) throws IOException,
790 HttpException, InterruptedException {
791 return getFlvFile(vi, fileName, Status.GET_INFO, true, listener);
795 * ニコニコ動画から動画ファイルをダウンロードする.
797 * @param vi getVideoInfoメソッドで取得したオブジェクト.
798 * @return この処理を行った後の, 対象ファイルのステータス.
799 * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
801 public GetFlvResult getFlvFile(VideoInfo vi) throws IOException, URISyntaxException, HttpException,
802 InterruptedException {
803 return getFlvFile(vi, vi.getRealVideoId(), Status.GET_INFO, true, ProgressListener.EMPTY_LISTENER);
807 * ニコニコ動画サービスからコメントファイルを取得します.
808 * @param vi {@link #getVideoInfo(java.lang.String)}で取得したオブジェクト.
809 * @param fileName 保存するファイル名.
810 * @param wayback 過去ログ情報. 過去ログ取得でなければnull.
811 * @param commentNum 取得するコメント数. 再生時間に応じて取得数を自動決定する場合は非正値.
812 * @param oldVersion 2010/12/22 以前のコメント表示仕様に基づいて取得する場合はtrue.
814 * @throws Exception コメント取得失敗.
816 public File getCommentFile(VideoInfo vi, String fileName, WayBackInfo wayback, int commentNum, boolean oldVersion)
818 final EnumSet<DownloadCommentType> set = EnumSet.noneOf(DownloadCommentType.class);
820 set.add(DownloadCommentType.COMMENT_OLD);
822 set.add(DownloadCommentType.COMMENT);
824 return getCommentFile(vi, fileName, set, wayback, commentNum);
828 * ニコニコ動画サービスからコメントファイルを取得します.
829 * {@link #getCommentFile(nicobrowser.VideoInfo, java.lang.String, nicobrowser.WayBackInfo, int, boolean)}
831 * @param vi {@link #getVideoInfo(java.lang.String)}で取得したオブジェクト.
832 * @param fileName 保存するファイル名.
834 * @throws Exception コメント取得失敗.
836 public File getCommentFile(VideoInfo vi, String fileName) throws Exception {
837 return getCommentFile(vi, fileName, EnumSet.of(DownloadCommentType.COMMENT), null, -1);
841 * ニコニコ動画サービスから投稿者コメントファイルを取得します.
842 * @param vi {@link #getVideoInfo(java.lang.String)}で取得したオブジェクト.
843 * @param fileName 保存するファイル名.
844 * @return 投稿者コメントファイル.
845 * @throws Exception 投稿者コメント取得失敗.
847 public File getTCommentFile(VideoInfo vi, String fileName) throws Exception {
848 return getCommentFile(vi, fileName, EnumSet.of(DownloadCommentType.OWNER), null, -1);
852 * ニコニコ動画サービスからコメントファイルを取得します.
853 * @param vi {@link #getVideoInfo(java.lang.String)}で取得したオブジェクト.
854 * @param fileName 保存するファイル名.
855 * @param types ダウンロード対象とするコメントの種類.
856 * @param wayback 過去ログ情報. 過去ログ取得でなければnull.
857 * @param commentNum 取得するコメント数. 再生時間に応じて取得数を自動決定する場合は非正値.
859 * @throws Exception コメント取得失敗.
861 public File getCommentFile(VideoInfo vi, String fileName, EnumSet<DownloadCommentType> types,
862 WayBackInfo wayback, int commentNum) throws Exception {
863 HttpResponse response = null;
864 BufferedOutputStream bos = null;
867 final HttpPost post = new HttpPost(vi.getMessageUrl().toString());
868 final StringBuilder paramBuilder = new StringBuilder("<packet>");
870 // COMMENTとCOMMENT_OLDは二者択一
871 if(types.contains(DownloadCommentType.COMMENT)) {
872 final String param = createCommentDownloadParameter(vi, wayback, commentNum);
873 paramBuilder.append(param);
874 } else if (types.contains(DownloadCommentType.COMMENT_OLD)) {
875 final String param = createCommentDownloadParameter20101222(vi, false, wayback, commentNum);
876 paramBuilder.append(param);
879 if(types.contains(DownloadCommentType.OWNER)) {
880 final String param = createCommentDownloadParameter20101222(vi, true, wayback, 1000);
881 paramBuilder.append(param);
884 paramBuilder.append("</packet>");
886 final StringEntity se = new StringEntity(paramBuilder.toString());
888 response = http.execute(post);
889 final InputStream is = response.getEntity().getContent();
890 final BufferedInputStream bis = new BufferedInputStream(is);
892 final String outputFileName = (fileName.endsWith(".xml")) ? fileName : fileName + ".xml";
893 bos = new BufferedOutputStream(new FileOutputStream(outputFileName));
895 final byte[] buf = new byte[1024 * 1024];
897 while ((read = bis.read(buf, 0, buf.length)) > 0) {
898 bos.write(buf, 0, read);
901 return new File(outputFileName);
902 } catch (Exception e) {
903 throw new Exception("コメントダウンロードに失敗しました。", e);
905 if (response != null) {
906 EntityUtils.consume(response.getEntity());
915 * 2011/2/3 以降のコメント表示仕様に基づいた取得パラメータ生成.
917 * @param wayback 過去ログ情報. 過去ログ取得でないバイはnull.
918 * @param commentNum 取得するコメント数. 再生時間に応じて取得数を自動決定する場合は非正値.
919 * @return 生成されたパラメータ.
921 private String createCommentDownloadParameter(VideoInfo vi, WayBackInfo wayback, int commentNum) {
922 final Map<String, String> threadKey = vi.getKeyMap();
923 final Map<String, String> th = new HashMap<String, String>();
924 th.put("thread", vi.getThreadId());
925 th.put("version", "20090904");
926 th.put("user_id", vi.getUserId());
927 if (wayback != null) {
928 th.put("waybackkey", wayback.getKey());
929 th.put("when", Long.toString(wayback.getTime()));
932 final Map<String, String> leaf = new HashMap<String, String>();
933 leaf.put("thread", vi.getThreadId());
934 leaf.put("user_id", vi.getUserId());
935 if (wayback != null) {
936 leaf.put("waybackkey", wayback.getKey());
937 leaf.put("when", Long.toString(wayback.getTime()));
940 final int minutes = (int) Math.ceil(vi.getVideoLength() / 60.0);
941 // 1分当たり100件のコメントを表示するのは720分未満の動画だけで, それ以上は調整が入るらしい
942 // (どんなに長くても1動画当たり720*100件が最大。それを超える場合には1分当たりの件数を削減する)
943 final int max100perMin = 720;
944 final int perMin = (minutes < max100perMin) ? 100 : (max100perMin * 100) / minutes;
946 final int resFrom = (commentNum > 0) ? commentNum : vi.getResFrom();
947 final String element = "0-" + minutes + ":" + perMin + "," + resFrom;
949 final StringBuilder str = new StringBuilder();
951 str.append("<thread");
952 addMapToAttr(str, th);
953 addMapToAttr(str, threadKey);
956 str.append("<thread_leaves");
957 addMapToAttr(str, leaf);
958 addMapToAttr(str, threadKey);
961 str.append("</thread_leaves>");
963 return str.toString();
967 * 2010/12/22 までのコメント表示仕様に基づいた取得パラメータ生成.
968 * 「コメントの量を減らす」にチェックを入れた場合は現在でもこれが用いられているはず.
970 * @param isTcomm 投稿者コメント取得パラメータを生成する場合にはtrue.
971 * @param wayback 過去ログ情報. 過去ログ取得でないバイはnull.
972 * @param commentNum 取得するコメント数. 再生時間に応じて取得数を自動決定する場合は非正値.
973 * @return 生成されたパラメータ.
975 private String createCommentDownloadParameter20101222(VideoInfo vi, boolean isTcomm, WayBackInfo wayback,
977 final Map<String, String> params = new HashMap<String, String>();
979 params.put(VideoInfo.KEY_USER_ID, vi.getUserId());
980 params.put("thread", vi.getThreadId());
981 params.put("version", "20061206");
983 final int resFrom = (commentNum > 0) ? commentNum : vi.getResFrom();
984 params.put("res_from", "-" + resFrom);
987 params.put("fork", "1");
990 if (wayback != null) {
991 params.put("waybackkey", wayback.getKey());
992 params.put("when", Long.toString(wayback.getTime()));
995 final StringBuilder str = new StringBuilder();
996 str.append("<thread");
998 addMapToAttr(str, vi.getKeyMap());
999 addMapToAttr(str, params);
1003 return str.toString();
1006 private static void addMapToAttr(final StringBuilder str, final Map<String, String> map) {
1007 final String quote = "\"";
1008 for (String k : map.keySet()) {
1009 final String v = map.get(k);
1020 * 動画をマイリストへ登録する. ログインが必要.
1021 * @param myListId 登録するマイリストのID.
1022 * @param videoId 登録する動画ID.
1023 * @throws IOException 登録に失敗した.
1025 public void addMyList(String myListId, String videoId) throws IOException {
1026 String itemType = null;
1027 String itemId = null;
1028 String token = null;
1029 HttpGet get = new HttpGet(ADD_MYLIST_PAGE + videoId);
1030 HttpResponse response = http.execute(get);
1031 HttpEntity entity = response.getEntity();
1033 InputStream is = entity.getContent();
1034 BufferedReader reader = new BufferedReader(new InputStreamReader(is));
1037 Pattern pattern = Pattern.compile("input type=\"hidden\" name=\"item_type\" value=\"(.+)\"");
1038 while ((line = reader.readLine()) != null) {
1039 Matcher m = pattern.matcher(line);
1041 itemType = m.group(1);
1046 pattern = Pattern.compile("input type=\"hidden\" name=\"item_id\" value=\"(.+)\"");
1047 while ((line = reader.readLine()) != null) {
1048 Matcher m = pattern.matcher(line);
1050 itemId = m.group(1);
1055 pattern = Pattern.compile("NicoAPI\\.token = \"(.*)\";");
1056 while ((line = reader.readLine()) != null) {
1057 Matcher m = pattern.matcher(line);
1064 EntityUtils.consume(entity);
1067 if (itemType == null || itemId == null || token == null) {
1068 throw new IOException("マイリスト登録に必要な情報が取得できませんでした。 "
1069 + "マイリスト:" + myListId + ", 動画ID:" + videoId + ", item_type:" + itemType + ", item_id:" + itemId
1070 + ", token:" + token);
1073 StringEntity se = new StringEntity(
1074 "group_id=" + myListId
1075 + "&item_type=" + itemType
1076 + "&item_id=" + itemId
1077 + "&description=" + ""
1078 + "&token=" + token);
1080 HttpPost post = new HttpPost("http://www.nicovideo.jp/api/mylist/add");
1081 post.setHeader("Content-Type", "application/x-www-form-urlencoded");
1083 response = http.execute(post);
1084 int statusCode = response.getStatusLine().getStatusCode();
1085 EntityUtils.consume(response.getEntity());
1086 if (statusCode != HttpStatus.SC_OK) {
1087 throw new IOException("マイリスト登録に失敗" + "マイリスト:" + myListId + ", 動画ID:" + videoId);