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.lang.StringUtils;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54 import org.apache.http.Header;
55 import org.apache.http.HttpEntity;
56 import org.apache.http.HttpException;
57 import org.apache.http.HttpHost;
58 import org.apache.http.HttpResponse;
59 import org.apache.http.HttpStatus;
60 import org.apache.http.NameValuePair;
61 import org.apache.http.client.CookieStore;
62 import org.apache.http.client.entity.UrlEncodedFormEntity;
63 import org.apache.http.client.methods.HttpGet;
64 import org.apache.http.client.methods.HttpHead;
65 import org.apache.http.client.methods.HttpPost;
66 import org.apache.http.client.params.ClientPNames;
67 import org.apache.http.client.params.CookiePolicy;
68 import org.apache.http.conn.params.ConnRoutePNames;
69 import org.apache.http.cookie.Cookie;
70 import org.apache.http.entity.StringEntity;
71 import org.apache.http.impl.client.DefaultHttpClient;
72 import org.apache.http.impl.client.RedirectLocations;
73 import org.apache.http.impl.cookie.BasicClientCookie;
74 import org.apache.http.message.BasicNameValuePair;
75 import org.apache.http.protocol.BasicHttpContext;
76 import org.apache.http.protocol.HttpContext;
77 import org.apache.http.util.EntityUtils;
78 import org.w3c.dom.Document;
79 import org.w3c.dom.Element;
80 import org.w3c.dom.NodeList;
81 import org.xml.sax.SAXException;
87 public class NicoHttpClient {
89 private static Logger logger = LoggerFactory.getLogger(NicoHttpClient.class);
90 private final DefaultHttpClient http;
91 private static final String LOGIN_PAGE =
92 "https://secure.nicovideo.jp/secure/login?site=niconico";
93 private static final String LOGOUT_PAGE =
94 "https://secure.nicovideo.jp/secure/logout";
95 private static final String NICOVIDEO_HOME_URL = "http://www.nicovideo.jp";
96 private static final String WATCH_PAGE = NICOVIDEO_HOME_URL + "/watch/";
97 private static final String MY_LIST_PAGE_HEADER = NICOVIDEO_HOME_URL + "/mylist/";
98 private static final String MOVIE_THUMBNAIL_PAGE_HEADER = "http://ext.nicovideo.jp/api/getthumbinfo/";
99 private static final String GET_FLV_INFO = NICOVIDEO_HOME_URL + "/api/getflv/";
100 private static final String SEARCH_HEAD = NICOVIDEO_HOME_URL + "/";
101 private static final String ADD_MYLIST_PAGE = NICOVIDEO_HOME_URL + "/mylist_add/video/";
102 private static final String GET_THREAD_KEY_PAGE = NICOVIDEO_HOME_URL + "/api/getthreadkey?thread=";
104 public NicoHttpClient() {
105 http = new DefaultHttpClient();
106 http.getParams().setParameter(
107 ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
111 * プロキシサーバを経由してアクセスする場合のコンストラクタ.
112 * @param host プロキシサーバのホスト名.
113 * @param port プロキシサーバで利用するポート番号.
115 public NicoHttpClient(String host, int port) {
117 HttpHost proxy = new HttpHost(host, port);
118 http.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
121 public void addCookie(Map<String, String> cookies) {
122 final CookieStore cookieStore = http.getCookieStore();
123 for (String key : cookies.keySet()) {
124 final String value = cookies.get(key);
125 final BasicClientCookie cookie = new BasicClientCookie(key, value);
126 cookie.setDomain(".nicovideo.jp");
128 cookieStore.addCookie(cookie);
133 * 汎用的なHTTP GETを提供します.
134 * @param url アクセスするURL
135 * @return レスポンス. 呼び出し側でcloseする必要があります.
136 * @throws IOException アクセスエラー.
138 public InputStream get(URL url) throws IOException {
139 final HttpGet httpGet = new HttpGet(url.toString());
140 final HttpResponse response = http.execute(httpGet);
141 return response.getEntity().getContent();
146 * @param mail ログイン識別子(登録メールアドレス).
147 * @param password パスワード.
148 * @return 認証がOKであればtrue.
150 public boolean login(String mail, String password) throws InterruptedException {
151 boolean auth = false;
152 HttpPost post = new HttpPost(LOGIN_PAGE);
155 NameValuePair[] nvps = new NameValuePair[]{
156 new BasicNameValuePair("mail", mail),
157 new BasicNameValuePair("password", password),
158 new BasicNameValuePair("next_url", "")
160 post.setEntity(new UrlEncodedFormEntity(Arrays.asList(nvps), "UTF-8"));
162 //post.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
163 HttpResponse response = http.execute(post);
164 logger.debug("ログインステータスコード: " + response.getStatusLine().getStatusCode());
167 HttpEntity entity = response.getEntity();
168 EntityUtils.consume(entity);
169 List<Cookie> cookies = http.getCookieStore().getCookies();
170 if (!cookies.isEmpty()) {
173 } catch (IOException ex) {
174 logger.error("ログイン時に問題が発生", ex);
180 * ニコニコ動画サービスへアクセスし有効なセッションかどうかを試します.
181 * @return 有効なセッションであればtrue.
183 public boolean challengeAuth() throws IOException {
184 final HttpHead head = new HttpHead(NICOVIDEO_HOME_URL);
185 HttpResponse response = null;
187 response = http.execute(head);
188 final Header authFlag = response.getFirstHeader("x-niconico-authflag");
189 // 0:expired, 1:normal user, 3:premium user
190 if ("0".equals(authFlag.getValue())) {
195 if (response != null) {
196 EntityUtils.consume(response.getEntity());
203 * @return ログアウトに成功すればtrue.
205 public boolean logout() throws URISyntaxException, HttpException, InterruptedException {
206 boolean result = false;
207 HttpGet method = new HttpGet(LOGOUT_PAGE);
209 HttpResponse response = http.execute(method);
210 logger.debug("ログアウトステータスコード: " + response.getStatusLine().getStatusCode());
212 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
215 EntityUtils.consume(response.getEntity());
216 } catch (IOException ex) {
217 logger.error("ログアウト時に問題が発生", ex);
224 * @param word 検索キーワード
227 * @page 検索結果ページのうち, 結果を返すページ.
230 public SearchResult search(SearchKind kind, String word, SortKind sort, SortOrder order, int page) throws
232 logger.debug("検索:" + word);
234 InputStream is = null;
235 ArrayList<NicoContent> conts = new ArrayList<NicoContent>();
236 String url = SEARCH_HEAD + kind.getKey() + "/" + URLEncoder.encode(word, "UTF-8") + "?page=" + Integer.toString(
237 page) + "&sort=" + sort.getKey() + "&order=" + order.getKey();
240 HttpGet get = new HttpGet(url);
241 HttpResponse response;
242 response = http.execute(get);
243 is = new BufferedInputStream(response.getEntity().getContent());
244 assert is.markSupported();
245 is.mark(1024 * 1024);
246 List<Result> results = Util.parseSearchResult(is);
247 for (Result r : results) {
248 NicoContent c = loadMyMovie(r.getId());
254 TreeMap<Integer, String> otherPages = Util.getOtherPages(is);
255 return new SearchResult(conts, otherPages);
256 } catch (IOException ex) {
257 logger.error("検索結果処理時に例外発生", ex);
263 } catch (IOException ex) {
270 * 「マイリスト登録数ランキング(本日)」の動画一覧を取得する。
273 public List<NicoContent> loadMyListDaily() throws URISyntaxException, HttpException, InterruptedException {
274 List<NicoContent> list = new ArrayList<NicoContent>();
275 String url = "http://www.nicovideo.jp/ranking/mylist/daily/all?rss=atom";
276 logger.debug("全動画サイトのマイリスト登録数ランキング(本日)[全体] : " + url);
278 HttpGet get = new HttpGet(url);
280 BufferedReader reader = null;
282 HttpResponse response = http.execute(get);
283 reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
286 list = getNicoContents(reader);
287 deleteRankString(list);
288 EntityUtils.consume(response.getEntity());
289 } catch (FeedException ex) {
290 logger.error("", ex);
291 } catch (IOException ex) {
292 logger.error("", ex);
294 if (reader != null) {
297 } catch (IOException ex) {
298 logger.error("", ex);
306 * ニコニコ動画のRSSからコンテンツリストを取得する.
307 * @param url 取得するrssのurl.
310 public List<NicoContent> getContentsFromRss(String url) {
311 logger.debug("アクセスURL: " + url);
312 List<NicoContent> list = accessRssUrl(url);
313 if (url.contains("ranking")) {
314 deleteRankString(list);
321 * @param vi {@link #getVideoInfo(java.lang.String) }で取得したオブジェクト.
323 * @throws IOException 取得に失敗した場合.
325 public String getWayBackKey(VideoInfo vi) throws IOException {
326 final String url = "http://flapi.nicovideo.jp/api/getwaybackkey?thread=" + vi.getThreadId();
327 final HttpGet get = new HttpGet(url);
328 HttpResponse response = http.execute(get);
331 final int statusCode = response.getStatusLine().getStatusCode();
332 if (statusCode != HttpStatus.SC_OK) {
333 throw new IOException("waybackkey get error " + statusCode);
336 final BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
338 logger.debug("wayback get result text: " + res);
340 EntityUtils.consume(response.getEntity());
343 final String keyWayBackKey = "waybackkey";
344 final String[] keyValues = res.split("&");
345 for (String s : keyValues) {
346 final String[] kv = s.split("=");
347 if (kv.length == 2) {
348 if (keyWayBackKey.equals(kv[0])) {
354 throw new IOException("pick up no waybackkey: " + res);
358 * rankingの場合、本当のタイトルの前に"第XX位:"の文字列が
360 * @param list 対象のリスト.
362 private void deleteRankString(List<NicoContent> list) {
363 for (NicoContent c : list) {
364 String title = c.getTitle();
365 int offset = title.indexOf(":") + 1;
366 c.setTitle(title.substring(offset));
372 * 「公開」設定にしていないリストからは取得できない.
374 * @param listNo マイリストNo.
377 public List<NicoContent> loadMyList(String listNo) {
378 String url = MY_LIST_PAGE_HEADER + listNo + "?rss=atom";
379 logger.debug("マイリストURL: " + url);
380 return accessRssUrl(url);
384 * コンテンツ概略のストリームを取得する.
386 * @return コンテンツ概略. 取得元でcloseすること.
387 * @throws IOException
389 public InputStream getThumbInfo(String movieNo) throws IOException {
390 String url = MOVIE_THUMBNAIL_PAGE_HEADER + movieNo;
391 logger.debug("動画サムネイルURL: " + url);
393 HttpGet get = new HttpGet(url);
394 HttpResponse response = http.execute(get);
395 return response.getEntity().getContent();
400 * 動画番号を指定したコンテンツ情報の取得.
401 * @param movieNo 動画番号.
404 public NicoContent loadMyMovie(String movieNo) {
405 NicoContent cont = null;
406 InputStream re = null;
409 re = getThumbInfo(movieNo);
410 // ドキュメントビルダーファクトリを生成
411 DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
413 DocumentBuilder builder = dbfactory.newDocumentBuilder();
414 // パースを実行してDocumentオブジェクトを取得
415 Document doc = builder.parse(re);
416 // ルート要素を取得(タグ名:site)
417 Element root = doc.getDocumentElement();
419 if ("fail".equals(root.getAttribute("status"))) {
420 logger.warn("情報取得できません: " + movieNo);
424 NodeList list2 = root.getElementsByTagName("thumb");
425 cont = new NicoContent();
426 Element element = (Element) list2.item(0);
428 String watch_url = ((Element) element.getElementsByTagName("watch_url").item(0)).getFirstChild().
430 cont.setPageLink(watch_url);
432 String title = ((Element) element.getElementsByTagName("title").item(0)).getFirstChild().getNodeValue();
433 cont.setTitle(title);
436 // String first_retrieve = ((Element) element.getElementsByTagName("first_retrieve").item(0)).getFirstChild().getNodeValue();
437 // cont.setPublishedDate(DateFormat.getInstance().parse(first_retrieve));
439 // } catch (ParseException ex) {
440 // Logger.getLogger(NicoHttpClient.class.getName()).log(Level.SEVERE, null, ex);
441 } catch (SAXException ex) {
442 logger.error("", ex);
443 } catch (IOException ex) {
444 logger.error("", ex);
445 } catch (ParserConfigurationException ex) {
446 logger.error("", ex);
452 } catch (IOException ex) {
453 logger.error("", ex);
459 private List<NicoContent> accessRssUrl(String url) {
460 List<NicoContent> contList = new ArrayList<NicoContent>();
461 HttpGet get = new HttpGet(url);
462 BufferedReader reader = null;
464 HttpResponse response = http.execute(get);
465 reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
466 if (logger.isTraceEnabled()) {
467 reader.mark(1024 * 1024);
469 String str = reader.readLine();
477 contList = getNicoContents(reader);
478 } catch (FeedException ex) {
479 logger.warn("アクセスできません: " + url);
480 logger.debug("", ex);
481 } catch (IOException ex) {
482 logger.error("", ex);
484 if (reader != null) {
487 } catch (IOException ex) {
488 logger.error("", ex);
495 private List<NicoContent> getNicoContents(Reader reader) throws FeedException {
496 SyndFeedInput input = new SyndFeedInput();
497 SyndFeed feed = input.build(reader);
499 @SuppressWarnings("unchecked")
500 final List<SyndEntryImpl> list = (List<SyndEntryImpl>) feed.getEntries();
502 List<NicoContent> contList;
504 contList = new ArrayList<NicoContent>();
506 contList = createContentsList(list);
511 @SuppressWarnings("unchecked")
512 private List<NicoContent> createContentsList(List<SyndEntryImpl> list) {
513 class CallBack extends HTMLEditorKit.ParserCallback {
515 private boolean descFlag;
516 private String imageLink = new String();
517 private StringBuilder description = new StringBuilder();
520 public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
521 logger.debug("--------<" + t.toString() + ">--------");
522 logger.debug(a.toString());
523 if (HTML.Tag.IMG.equals(t)) {
524 imageLink = a.getAttribute(HTML.Attribute.SRC).toString();
529 public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
530 if (HTML.Tag.P.equals(t)) {
531 if ("nico-description".equals(
532 a.getAttribute(HTML.Attribute.CLASS).toString())) {
536 logger.debug("--------<" + t.toString() + ">--------");
537 logger.debug(a.toString());
541 public void handleEndTag(HTML.Tag t, int pos) {
542 if (HTML.Tag.P.equals(t)) {
545 logger.debug("--------</" + t.toString() + ">--------");
549 public void handleText(char[] data, int pos) {
551 description.append(data);
553 logger.debug("--------TEXT--------");
554 logger.debug(data.toString());
557 private void printAttributes(MutableAttributeSet a) {
558 final Enumeration<?> e = a.getAttributeNames();
559 while (e.hasMoreElements()) {
560 Object key = e.nextElement();
561 logger.debug("---- " + key.toString() + " : " + a.getAttribute(key));
565 public String getImageLink() {
569 public String getDescription() {
570 return description.toString();
574 List<NicoContent> contList = new ArrayList<NicoContent>();
576 for (SyndEntryImpl entry : list) {
577 NicoContent content = new NicoContent();
579 String title = entry.getTitle();
580 content.setTitle(title);
581 content.setPageLink(entry.getLink());
584 CallBack callBack = new CallBack();
585 for (SyndContentImpl sc : (List<SyndContentImpl>) entry.getContents()) {
587 Reader reader = new StringReader(sc.getValue());
588 new ParserDelegator().parse(reader, callBack, true);
589 } catch (IOException ex) {
590 logger.error("RSSの読み込み失敗: " + content.getTitle());
595 contList.add(content);
601 * FLVファイルのURLを取得する. ログインが必要.
602 * また, 実際にFLVファイルの実態をダウンロードするには
603 * 一度http://www.nicovideo.jp/watch/ビデオID に一度アクセスする必要があることに
605 * (参考: http://yusukebe.com/tech/archives/20070803/124356.html)
606 * @param videoId ニコニコ動画のビデオID.
607 * @return FLVファイル実体があるURL.
608 * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
610 public VideoInfo getVideoInfo(String videoId) throws IOException {
611 final GetRealVideoIdResult res = accessWatchPage(videoId);
612 final String realVideoId = res.videoId;
614 String accessUrl = GET_FLV_INFO + realVideoId;
615 if (realVideoId.startsWith("nm")) {
616 accessUrl += "?as3=1";
618 Map<String, String> map = getParameterMap(accessUrl);
620 LinkedHashMap<String, String> keyMap = new LinkedHashMap<String, String>();
621 if ("1".equals(map.get("needs_key"))) {
622 // 公式動画投稿者コメント取得用パラメータ.
623 keyMap = getParameterMap(GET_THREAD_KEY_PAGE + map.get(VideoInfo.KEY_THREAD_ID));
625 return new VideoInfo(realVideoId, res.title, map, keyMap);
628 private LinkedHashMap<String, String> getParameterMap(String accessUrl) throws IOException, IllegalStateException {
629 logger.debug("アクセス: " + accessUrl);
630 HttpGet get = new HttpGet(accessUrl);
632 BufferedReader reader = null;
634 HttpResponse response = http.execute(get);
635 reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
637 StringBuilder strBuilder = new StringBuilder();
638 while ((str = reader.readLine()) != null) {
639 strBuilder.append(str);
641 resultString = strBuilder.toString();
642 EntityUtils.consume(response.getEntity());
643 logger.debug(resultString);
645 if (reader != null) {
649 String[] params = resultString.split("&");
650 LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
651 for (String param : params) {
652 String[] elm = param.split("=");
653 map.put(elm[0], elm[1]);
659 * watchページコンテンツからタイトルを抽出する.
660 * @param content watchページコンテンツのストリーム.
662 private String getTitleInWatchPage(InputStream content) throws IOException {
663 return Util.getTitle(content);
666 private static class GetRealVideoIdResult {
668 private final String videoId;
669 private final String title;
671 private GetRealVideoIdResult(String videoId, String title) {
672 this.videoId = videoId;
678 * WATCHページへアクセスする. getflvを行うためには, 必ず事前にWATCHページへアクセスしておく必要があるため.
679 * WATCHページ参照時にリダイレクトが発生する(so動画ではスレッドIDのWATCHページにリダイレクトされる)場合には
680 * そちらのページにアクセスし、そのスレッドIDをrealIdとして返します.
681 * @param videoId 取得したいビデオのビデオID.
682 * @return 実際のアクセスに必要なIDと、タイトル. タイトルはいんきゅばす互換用です.
683 * @throws IOException アクセスに失敗した場合. 有料動画などがこれに含まれます.
685 private GetRealVideoIdResult accessWatchPage(String videoId) throws IOException {
686 String realId = videoId;
688 String watchUrl = WATCH_PAGE + videoId;
689 logger.debug("アクセス: " + watchUrl);
690 final HttpGet get = new HttpGet(watchUrl);
691 final HttpContext context = new BasicHttpContext();
692 final HttpResponse response = http.execute(get, context);
694 final RedirectLocations rl = (RedirectLocations) context.getAttribute(
695 "http.protocol.redirect-locations");
696 // 通常の動画(sm動画など)はリダイレクトが発生しないためnullになる
698 final List<URI> locations = rl.getAll();
699 logger.debug("リダイレクト数: " + locations.size());
701 // so動画はスレッドIDのページへリダイレクトされる
702 if (locations.size() == 1) {
703 realId = locations.get(0).toString().replace(WATCH_PAGE, "");
704 } else if (locations.size() > 1) {
705 throw new IOException("有料動画と思われるため処理を中断しました: " + ArrayUtils.toString(locations));
709 title = getTitleInWatchPage(response.getEntity().getContent());
711 EntityUtils.consume(response.getEntity());
713 return new GetRealVideoIdResult(realId, title);
717 * ニコニコ動画から動画ファイルをダウンロードする.
718 * @param vi getVideoInfoメソッドで取得したオブジェクト.
719 * @param saveDir ダウンロードしたファイルを保存するディレクトリ.
720 * @param np 保存するファイル名の命名規則. 拡張子は別途付与されるため不要.
721 * @param nowStatus ダウンロードしようとしている動画ファイルの, 現在のステータス.
722 * @param needLowFile エコノミー動画をダウンロードするのであればtrue.
723 * @return この処理を行った後の, 対象ファイルのステータス.
724 * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
726 public GetFlvResult getFlvFile(VideoInfo vi, File saveDir, NamePattern np, Status nowStatus, boolean needLowFile,
727 ProgressListener listener) throws IOException, URISyntaxException, HttpException, InterruptedException {
729 final URL notifierUrl = vi.getSmileUrl();
731 String userName = null;
732 if (notifierUrl != null) {
733 HttpGet get = new HttpGet(notifierUrl.toString());
734 HttpResponse response = http.execute(get);
735 userName = Util.getUserName(response.getEntity().getContent());
736 EntityUtils.consume(response.getEntity());
739 final URL url = vi.getVideoUrl();
740 if (nowStatus == Status.GET_LOW || !needLowFile) {
741 if (url.toString().contains("low")) {
742 logger.info("エコノミー動画のためスキップ: " + vi.getRealVideoId());
743 return new GetFlvResult(null, nowStatus, userName);
746 final boolean isNotLow = !url.toString().contains("low");
748 final File downloadFile = new File(saveDir, np.createFileName(vi.getRealVideoId(), isNotLow));
750 HttpGet get = new HttpGet(url.toURI());
751 HttpResponse response = http.execute(get);
752 String contentType = response.getEntity().getContentType().getValue();
753 logger.debug(contentType);
754 logger.debug(downloadFile.toString());
755 if ("text/plain".equals(contentType) || "text/html".equals(contentType)) {
756 logger.error("取得できませんでした. サーバが混みあっている可能性があります: " + vi.getRealVideoId());
757 EntityUtils.consume(response.getEntity());
758 return new GetFlvResult(null, Status.GET_INFO, userName);
760 String ext = Util.getExtention(contentType);
761 final long fileSize = response.getEntity().getContentLength();
763 final int BUF_SIZE = 1024 * 64;
764 BufferedInputStream in = null;
765 BufferedOutputStream out = null;
766 File file = new File(downloadFile.toString() + "." + ext);
768 in = new BufferedInputStream(response.getEntity().getContent(), BUF_SIZE);
770 logger.info("保存します(" + fileSize / 1024 + "KB): " + file.getPath());
771 FileOutputStream fos = new FileOutputStream(file);
772 out = new BufferedOutputStream(fos, BUF_SIZE);
774 long downloadSize = 0;
776 byte[] buffer = new byte[BUF_SIZE];
777 while ((i = in.read(buffer)) != -1) {
778 out.write(buffer, 0, i);
780 listener.progress(fileSize, downloadSize);
781 if (Thread.interrupted()) {
782 logger.info("中断します");
783 throw new InterruptedException("中断しました");
790 EntityUtils.consume(response.getEntity());
796 if (url.toString().contains("low")) {
797 return new GetFlvResult(file, Status.GET_LOW, userName);
799 return new GetFlvResult(file, Status.GET_FILE, userName);
803 * ニコニコ動画から動画ファイルをダウンロードする.
804 * @param vi getVideoInfoメソッドで取得したオブジェクト.
805 * @param fileName ダウンロード後のファイル名. 拡張子は別途付与されるため不要.
806 * @param nowStatus ダウンロードしようとしている動画ファイルの, 現在のステータス.
807 * @param needLowFile エコノミー動画をダウンロードするのであればtrue.
808 * @return この処理を行った後の, 対象ファイルのステータス.
809 * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
811 public GetFlvResult getFlvFile(VideoInfo vi, String fileName, Status nowStatus, boolean needLowFile,
812 ProgressListener listener) throws IOException, URISyntaxException, HttpException, InterruptedException {
813 String file = FilenameUtils.getName(fileName);
814 String dir = fileName.substring(0, fileName.length() - file.length());
815 NamePattern np = new NamePattern(file, "");
816 return getFlvFile(vi, new File(dir), np, nowStatus, needLowFile, listener);
820 * ニコニコ動画から動画ファイルをダウンロードする.
821 * @param vi getVideoInfoメソッドで取得したオブジェクト.
822 * @param fileName ダウンロード後のファイル名. 拡張子は別途付与されるため不要.
823 * @return この処理を行った後の, 対象ファイルのステータス.
824 * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
826 public GetFlvResult getFlvFile(VideoInfo vi, String fileName, ProgressListener listener) throws IOException,
828 HttpException, InterruptedException {
829 return getFlvFile(vi, fileName, Status.GET_INFO, true, listener);
833 * ニコニコ動画から動画ファイルをダウンロードする.
835 * @param vi getVideoInfoメソッドで取得したオブジェクト.
836 * @return この処理を行った後の, 対象ファイルのステータス.
837 * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
839 public GetFlvResult getFlvFile(VideoInfo vi) throws IOException, URISyntaxException, HttpException,
840 InterruptedException {
841 return getFlvFile(vi, vi.getRealVideoId(), Status.GET_INFO, true, ProgressListener.EMPTY_LISTENER);
845 * ニコニコ動画サービスからコメントファイルを取得します.
846 * @param vi {@link #getVideoInfo(java.lang.String)}で取得したオブジェクト.
847 * @param fileName 保存するファイル名.
848 * @param wayback 過去ログ情報. 過去ログ取得でなければnull.
849 * @param commentNum 取得するコメント数. 再生時間に応じて取得数を自動決定する場合は非正値.
850 * @param oldVersion 2010/12/22 以前のコメント表示仕様に基づいて取得する場合はtrue.
852 * @throws Exception コメント取得失敗.
854 public File getCommentFile(VideoInfo vi, String fileName, WayBackInfo wayback, int commentNum, boolean oldVersion)
856 final EnumSet<DownloadCommentType> set = EnumSet.noneOf(DownloadCommentType.class);
858 set.add(DownloadCommentType.COMMENT_OLD);
860 set.add(DownloadCommentType.COMMENT);
862 return getCommentFile(vi, fileName, set, wayback, commentNum);
866 * ニコニコ動画サービスからコメントファイルを取得します.
867 * {@link #getCommentFile(nicobrowser.VideoInfo, java.lang.String, nicobrowser.WayBackInfo, int, boolean)}
869 * @param vi {@link #getVideoInfo(java.lang.String)}で取得したオブジェクト.
870 * @param fileName 保存するファイル名.
872 * @throws Exception コメント取得失敗.
874 public File getCommentFile(VideoInfo vi, String fileName) throws Exception {
875 return getCommentFile(vi, fileName, EnumSet.of(DownloadCommentType.COMMENT), null, -1);
879 * ニコニコ動画サービスから投稿者コメントファイルを取得します.
880 * @param vi {@link #getVideoInfo(java.lang.String)}で取得したオブジェクト.
881 * @param fileName 保存するファイル名.
882 * @return 投稿者コメントファイル.
883 * @throws Exception 投稿者コメント取得失敗.
885 public File getTCommentFile(VideoInfo vi, String fileName) throws Exception {
886 return getCommentFile(vi, fileName, EnumSet.of(DownloadCommentType.OWNER), null, -1);
890 * ニコニコ動画サービスからコメントファイルを取得します.
891 * @param vi {@link #getVideoInfo(java.lang.String)}で取得したオブジェクト.
892 * @param fileName 保存するファイル名.
893 * @param types ダウンロード対象とするコメントの種類.
894 * @param wayback 過去ログ情報. 過去ログ取得でなければnull.
895 * @param commentNum 取得するコメント数. 再生時間に応じて取得数を自動決定する場合は非正値.
897 * @throws Exception コメント取得失敗.
899 public File getCommentFile(VideoInfo vi, String fileName, EnumSet<DownloadCommentType> types,
900 WayBackInfo wayback, int commentNum) throws Exception {
901 HttpResponse response = null;
902 BufferedInputStream bis = null;
903 BufferedOutputStream bos = null;
906 final HttpPost post = new HttpPost(vi.getMessageUrl().toString());
907 final StringBuilder paramBuilder = new StringBuilder("<packet>");
909 // COMMENTとCOMMENT_OLDは二者択一
910 if (types.contains(DownloadCommentType.COMMENT)) {
911 final String param = createCommentDownloadParameter(vi, wayback, commentNum);
912 paramBuilder.append(param);
913 } else if (types.contains(DownloadCommentType.COMMENT_OLD)) {
914 final String param = createCommentDownloadParameter20101222(vi, false, wayback, commentNum);
915 paramBuilder.append(param);
918 if (types.contains(DownloadCommentType.OWNER)) {
919 final String param = createCommentDownloadParameter20101222(vi, true, wayback, 1000);
920 paramBuilder.append(param);
923 paramBuilder.append("</packet>");
925 final StringEntity se = new StringEntity(paramBuilder.toString());
927 response = http.execute(post);
928 final InputStream is = response.getEntity().getContent();
929 bis = new BufferedInputStream(is);
931 final String outputFileName = (fileName.endsWith(".xml")) ? fileName : fileName + ".xml";
932 bos = new BufferedOutputStream(new FileOutputStream(outputFileName));
934 final byte[] buf = new byte[1024 * 1024];
936 while ((read = bis.read(buf, 0, buf.length)) > 0) {
937 bos.write(buf, 0, read);
940 return new File(outputFileName);
941 } catch (Exception e) {
942 throw new Exception("コメントダウンロードに失敗しました。", e);
944 if (response != null) {
945 EntityUtils.consume(response.getEntity());
956 private enum ThreadType {
962 * threadタグとthread_leavesタグに共通な情報を設定します.
964 private static void putCommonPair(final Map<String, String> map, ThreadType threadType,
965 VideoInfo vi, WayBackInfo wayback) {
966 if (threadType != ThreadType.OPTIONAL) {
967 map.put("thread", vi.getThreadId());
969 map.put("thread", vi.getOptionalThreadId());
971 map.put("user_id", vi.getUserId());
972 if (wayback != null) {
973 map.put("waybackkey", wayback.getKey());
974 map.put("when", Long.toString(wayback.getTime()));
979 * 2011/2/3 以降のコメント表示仕様に基づいた取得パラメータ生成.
981 * @param wayback 過去ログ情報. 過去ログ取得でない場合はnull.
982 * @param commentNum 取得するコメント数. 再生時間に応じて取得数を自動決定する場合は非正値.
983 * @return 生成されたパラメータ.
985 private String createCommentDownloadParameter(VideoInfo vi, WayBackInfo wayback, int commentNum) {
986 final String mainParam = createCommentDownloadParameter(ThreadType.MAIN, vi, wayback, commentNum);
987 final String optionParam;
988 if (StringUtils.isNotEmpty(vi.getOptionalThreadId())) {
989 optionParam = createCommentDownloadParameter(ThreadType.OPTIONAL, vi, wayback, commentNum);
993 return mainParam + optionParam;
996 private String createCommentDownloadParameter(ThreadType threadType, VideoInfo vi, WayBackInfo wayback,
998 final Map<String, String> threadKey = vi.getKeyMap();
999 final Map<String, String> th = new HashMap<String, String>();
1000 putCommonPair(th, threadType, vi, wayback);
1001 th.put("version", "20090904");
1003 final Map<String, String> leaf = new HashMap<String, String>();
1004 putCommonPair(leaf, threadType, vi, wayback);
1006 final int minutes = (int) Math.ceil(vi.getVideoLength() / 60.0);
1007 // 1分当たり100件のコメントを表示するのは720分未満の動画だけで, それ以上は調整が入るらしい
1008 // (どんなに長くても1動画当たり720*100件が最大。それを超える場合には1分当たりの件数を削減する)
1009 final int max100perMin = 720;
1010 final int perMin = (minutes < max100perMin) ? 100 : (max100perMin * 100) / minutes;
1012 final int resFrom = (commentNum > 0) ? commentNum : vi.getResFrom();
1013 final String element = "0-" + minutes + ":" + perMin + "," + resFrom;
1015 final StringBuilder str = new StringBuilder();
1017 str.append("<thread");
1018 addMapToAttr(str, th);
1019 addMapToAttr(str, threadKey);
1022 str.append("<thread_leaves");
1023 addMapToAttr(str, leaf);
1024 addMapToAttr(str, threadKey);
1026 str.append(element);
1027 str.append("</thread_leaves>");
1029 return str.toString();
1033 * 2010/12/22 までのコメント表示仕様に基づいた取得パラメータ生成.
1034 * 「コメントの量を減らす」にチェックを入れた場合は現在でもこれが用いられているはず.
1036 * @param isTcomm 投稿者コメント取得パラメータを生成する場合にはtrue.
1037 * @param wayback 過去ログ情報. 過去ログ取得でない場合はnull.
1038 * @param commentNum 取得するコメント数. 再生時間に応じて取得数を自動決定する場合は非正値.
1039 * @return 生成されたパラメータ.
1041 private String createCommentDownloadParameter20101222(VideoInfo vi, boolean isTcomm, WayBackInfo wayback,
1043 final String mainParam = createCommentDownloadParameter20101222(ThreadType.MAIN, vi, isTcomm, wayback,
1045 final String optionParam;
1046 if (StringUtils.isNotEmpty(vi.getOptionalThreadId())) {
1047 optionParam = createCommentDownloadParameter20101222(ThreadType.OPTIONAL, vi, isTcomm, wayback, commentNum);
1051 return mainParam + optionParam;
1054 private String createCommentDownloadParameter20101222(ThreadType threadType, VideoInfo vi, boolean isTcomm,
1055 WayBackInfo wayback, int commentNum) {
1056 final Map<String, String> params = new HashMap<String, String>();
1058 putCommonPair(params, threadType, vi, wayback);
1059 params.put("version", "20061206");
1061 final int resFrom = (commentNum > 0) ? commentNum : vi.getResFrom();
1062 params.put("res_from", "-" + resFrom);
1065 params.put("fork", "1");
1068 final StringBuilder str = new StringBuilder();
1069 str.append("<thread");
1071 addMapToAttr(str, vi.getKeyMap());
1072 addMapToAttr(str, params);
1076 return str.toString();
1079 private static void addMapToAttr(final StringBuilder str, final Map<String, String> map) {
1080 final String quote = "\"";
1081 for (String k : map.keySet()) {
1082 final String v = map.get(k);
1093 * 動画をマイリストへ登録する. ログインが必要.
1094 * @param myListId 登録するマイリストのID.
1095 * @param videoId 登録する動画ID.
1096 * @throws IOException 登録に失敗した.
1098 public void addMyList(String myListId, String videoId) throws IOException {
1099 String itemType = null;
1100 String itemId = null;
1101 String token = null;
1102 HttpGet get = new HttpGet(ADD_MYLIST_PAGE + videoId);
1103 HttpResponse response = http.execute(get);
1104 HttpEntity entity = response.getEntity();
1106 InputStream is = entity.getContent();
1107 BufferedReader reader = new BufferedReader(new InputStreamReader(is));
1110 Pattern pattern = Pattern.compile("input type=\"hidden\" name=\"item_type\" value=\"(.+)\"");
1111 while ((line = reader.readLine()) != null) {
1112 Matcher m = pattern.matcher(line);
1114 itemType = m.group(1);
1119 pattern = Pattern.compile("input type=\"hidden\" name=\"item_id\" value=\"(.+)\"");
1120 while ((line = reader.readLine()) != null) {
1121 Matcher m = pattern.matcher(line);
1123 itemId = m.group(1);
1128 pattern = Pattern.compile("NicoAPI\\.token = \"(.*)\";");
1129 while ((line = reader.readLine()) != null) {
1130 Matcher m = pattern.matcher(line);
1137 EntityUtils.consume(entity);
1140 if (itemType == null || itemId == null || token == null) {
1141 throw new IOException("マイリスト登録に必要な情報が取得できませんでした。 "
1142 + "マイリスト:" + myListId + ", 動画ID:" + videoId + ", item_type:" + itemType + ", item_id:" + itemId
1143 + ", token:" + token);
1146 StringEntity se = new StringEntity(
1147 "group_id=" + myListId
1148 + "&item_type=" + itemType
1149 + "&item_id=" + itemId
1150 + "&description=" + ""
1151 + "&token=" + token);
1153 HttpPost post = new HttpPost("http://www.nicovideo.jp/api/mylist/add");
1154 post.setHeader("Content-Type", "application/x-www-form-urlencoded");
1156 response = http.execute(post);
1157 int statusCode = response.getStatusLine().getStatusCode();
1158 EntityUtils.consume(response.getEntity());
1159 if (statusCode != HttpStatus.SC_OK) {
1160 throw new IOException("マイリスト登録に失敗" + "マイリスト:" + myListId + ", 動画ID:" + videoId);