/*$Id$*/
package nicobrowser;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import nicobrowser.entity.NicoContent;
+import nicobrowser.search.SortKind;
+import nicobrowser.search.SortOrder;
import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.synd.SyndEntryImpl;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
+import java.net.URL;
+import java.net.URLEncoder;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import java.util.Map;
+import java.util.regex.Pattern;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
-import org.apache.commons.httpclient.HttpClient;
-import org.apache.commons.httpclient.HttpStatus;
-import org.apache.commons.httpclient.cookie.CookiePolicy;
-import org.apache.commons.httpclient.methods.GetMethod;
-import org.apache.commons.httpclient.methods.PostMethod;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import nicobrowser.entity.NicoContent.Status;
+import nicobrowser.search.SearchKind;
+import nicobrowser.search.SearchResult;
+import nicobrowser.util.Result;
+import nicobrowser.util.Util;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.params.ClientPNames;
+import org.apache.http.client.params.CookiePolicy;
+import org.apache.http.conn.params.ConnRoutePNames;
+import org.apache.http.cookie.Cookie;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.RedirectLocations;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.util.EntityUtils;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
/**
*
* @author yuki
*/
-public class NicoHttpClient extends HttpClient {
+public class NicoHttpClient {
- static NicoHttpClient instance;
+ private static Log logger = LogFactory.getLog(NicoHttpClient.class);
+ private final DefaultHttpClient http;
private static final String LOGIN_PAGE =
"https://secure.nicovideo.jp/secure/login?site=niconico";
private static final String LOGOUT_PAGE =
"https://secure.nicovideo.jp/secure/logout";
+ private static final String WATCH_PAGE = "http://www.nicovideo.jp/watch/";
private static final String MY_LIST_PAGE_HEADER =
"http://www.nicovideo.jp/mylist/";
+ private static final String MOVIE_THUMBNAIL_PAGE_HEADER =
+ "http://www.nicovideo.jp/api/getthumbinfo/";
+ private static final String GET_FLV_INFO = "http://www.nicovideo.jp/api/getflv/";
+ private static final String SEARCH_HEAD = "http://www.nicovideo.jp/";
+ private static final String ADD_MYLIST_PAGE = "http://www.nicovideo.jp/mylist_add/video/";
+ private static final String GET_THREAD_KEY_PAGE = "http://www.nicovideo.jp/api/getthreadkey?thread=";
- private NicoHttpClient() {
- super();
- instance = this;
+ public NicoHttpClient() {
+ http = new DefaultHttpClient();
+ http.getParams().setParameter(
+ ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
}
- public static NicoHttpClient getInstance() {
- if (instance == null) {
- return new NicoHttpClient();
- }
- return instance;
+ /**
+ * プロキシサーバを経由してアクセスする場合のコンストラクタ.
+ * @param host プロキシサーバのホスト名.
+ * @param port プロキシサーバで利用するポート番号.
+ */
+ public NicoHttpClient(String host, int port) {
+ this();
+ HttpHost proxy = new HttpHost(host, port);
+ http.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
}
/**
* @param password パスワード.
* @return 認証がOKであればtrue.
*/
- public boolean login(String mail, String password) {
+ public boolean login(String mail, String password) throws URISyntaxException, HttpException, InterruptedException {
boolean auth = false;
- PostMethod post = new PostMethod(LOGIN_PAGE);
+ HttpPost post = new HttpPost(LOGIN_PAGE);
try {
- post.addParameter("mail", mail);
- post.addParameter("password", password);
- post.addParameter("next_url", "");
- post.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
- int statusCode = executeMethod(post);
- Logger.getLogger(NicoHttpClient.class.getName()).
- log(Level.INFO, "ログインステータスコード: " + statusCode);
+ NameValuePair[] nvps = new NameValuePair[]{
+ new BasicNameValuePair("mail", mail),
+ new BasicNameValuePair("password", password),
+ new BasicNameValuePair("next_url", "")
+ };
+ post.setEntity(new UrlEncodedFormEntity(Arrays.asList(nvps), "UTF-8"));
+
+ //post.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
+ HttpResponse response = http.execute(post);
+ logger.debug("ログインステータスコード: " + response.getStatusLine().getStatusCode());
// ログイン可否の判定.
- // x-niconico-authflagで判定できそうだったが必ず0になる...
- // Set-Cookieがあればログインできたとみなしているが,あまりよろしくないかも.
- auth = (null != post.getResponseHeader("Set-Cookie")) ? true : false;
+ HttpEntity entity = response.getEntity();
+ EntityUtils.consume(entity);
+ List<Cookie> cookies = http.getCookieStore().getCookies();
+ if (!cookies.isEmpty()) {
+ auth = true;
+ }
} catch (IOException ex) {
- Logger.getLogger(NicoHttpClient.class.getName()).log(Level.SEVERE, null, ex);
- } finally {
- post.releaseConnection();
+ logger.error("ログイン時に問題が発生", ex);
}
return auth;
}
- public boolean logout() {
+ /**
+ * ニコニコ動画からログアウトする.
+ * @return ログアウトに成功すればtrue.
+ */
+ public boolean logout() throws URISyntaxException, HttpException, InterruptedException {
boolean result = false;
- GetMethod method = new GetMethod(LOGOUT_PAGE);
+ HttpGet method = new HttpGet(LOGOUT_PAGE);
try {
- int statusCode = executeMethod(method);
- Logger.getLogger(NicoHttpClient.class.getName()).
- log(Level.INFO, "ログアウトステータスコード: " + statusCode);
+ HttpResponse response = http.execute(method);
+ logger.debug("ログアウトステータスコード: " + response.getStatusLine().getStatusCode());
- if (statusCode == HttpStatus.SC_OK) {
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
result = true;
}
+ EntityUtils.consume(response.getEntity());
} catch (IOException ex) {
- Logger.getLogger(NicoHttpClient.class.getName()).log(Level.SEVERE, null, ex);
- } finally {
- method.releaseConnection();
+ logger.error("ログアウト時に問題が発生", ex);
}
return result;
}
/**
+ * キーワード検索を行う.
+ * @param word 検索キーワード
+ * @param sort ソート種別
+ * @param order ソート順
+ * @page 検索結果ページのうち, 結果を返すページ.
+ * @return 検索結果.
+ */
+ public SearchResult search(SearchKind kind, String word, SortKind sort, SortOrder order, int page) throws
+ IOException {
+ logger.debug("検索:" + word);
+
+ InputStream is = null;
+ ArrayList<NicoContent> conts = new ArrayList<NicoContent>();
+ String url = SEARCH_HEAD + kind.getKey() + "/" + URLEncoder.encode(word, "UTF-8") + "?page=" + Integer.toString(
+ page) + "&sort=" + sort.getKey() + "&order=" + order.getKey();
+
+ try {
+ HttpGet get = new HttpGet(url);
+ HttpResponse response;
+ response = http.execute(get);
+ is = new BufferedInputStream(response.getEntity().getContent());
+ assert is.markSupported();
+ is.mark(1024 * 1024);
+ List<Result> results = Util.parseSearchResult(is);
+ for (Result r : results) {
+ NicoContent c = loadMyMovie(r.getId());
+ if (c != null) {
+ conts.add(c);
+ }
+ }
+ is.reset();
+ TreeMap<Integer, String> otherPages = Util.getOtherPages(is);
+ return new SearchResult(conts, otherPages);
+ } catch (IOException ex) {
+ logger.error("検索結果処理時に例外発生", ex);
+ throw ex;
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException ex) {
+ }
+ }
+ }
+ }
+
+ /**
+ * 「マイリスト登録数ランキング(本日)」の動画一覧を取得する。
+ * @return 動画一覧.
+ */
+ public List<NicoContent> loadMyListDaily() throws URISyntaxException, HttpException, InterruptedException {
+ List<NicoContent> list = new ArrayList<NicoContent>();
+ String url = "http://www.nicovideo.jp/ranking/mylist/daily/all?rss=atom";
+ logger.debug("全動画サイトのマイリスト登録数ランキング(本日)[全体] : " + url);
+
+ HttpGet get = new HttpGet(url);
+
+ BufferedReader reader = null;
+ try {
+ HttpResponse response = http.execute(get);
+ reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
+ // BOMを読み捨て
+ // reader.skip(1);
+ list = getNicoContents(reader);
+ deleteRankString(list);
+ EntityUtils.consume(response.getEntity());
+ } catch (FeedException ex) {
+ logger.error("", ex);
+ } catch (IOException ex) {
+ logger.error("", ex);
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException ex) {
+ logger.error("", ex);
+ }
+ }
+ }
+ return list;
+ }
+
+ /**
+ * ニコニコ動画のRSSからコンテンツリストを取得する.
+ * @param url 取得するrssのurl.
+ * @return コンテンツリスト.
+ */
+ public List<NicoContent> getContentsFromRss(String url) {
+ logger.debug("アクセスURL: " + url);
+ List<NicoContent> list = accessRssUrl(url);
+ if (url.contains("ranking")) {
+ deleteRankString(list);
+ }
+ return list;
+ }
+
+ /**
+ * 過去ログ取得用のキーを取得します.
+ * @param vi {@link #getVideoInfo(java.lang.String) }で取得したオブジェクト.
+ * @return 過去ログ取得用キー
+ * @throws IOException 取得に失敗した場合.
+ */
+ public String getWayBackKey(VideoInfo vi) throws IOException {
+ final String url = "http://flapi.nicovideo.jp/api/getwaybackkey?thread=" + vi.getThreadId();
+ final HttpGet get = new HttpGet(url);
+ HttpResponse response = http.execute(get);
+ String res;
+ try {
+ final int statusCode = response.getStatusLine().getStatusCode();
+ if (statusCode != HttpStatus.SC_OK) {
+ throw new IOException("waybackkey get error " + statusCode);
+ }
+
+ final BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
+ res = br.readLine();
+ logger.debug("wayback get result text: " + res);
+ } finally {
+ EntityUtils.consume(response.getEntity());
+ }
+
+ final String keyWayBackKey = "waybackkey";
+ final String[] keyValues = res.split("&");
+ for (String s : keyValues) {
+ final String[] kv = s.split("=");
+ if (keyWayBackKey.equals(kv[0])) {
+ return kv[1];
+ }
+ }
+
+ throw new IOException("wayback key get fail: " + res);
+ }
+
+ /**
+ * rankingの場合、本当のタイトルの前に"第XX位:"の文字列が
+ * 挿入されているため, それを削る.
+ * @param list 対象のリスト.
+ */
+ private void deleteRankString(List<NicoContent> list) {
+ for (NicoContent c : list) {
+ String title = c.getTitle();
+ int offset = title.indexOf(":") + 1;
+ c.setTitle(title.substring(offset));
+ }
+ }
+
+ /**
* マイリストに登録した動画一覧の取得.
* 「公開」設定にしていないリストからは取得できない.
* ログインしていなくても取得可能.
* @return 動画一覧.
*/
public List<NicoContent> loadMyList(String listNo) {
- List<SyndEntryImpl> list = null;
- String url = new String(MY_LIST_PAGE_HEADER + listNo + "?rss=atom");
- Logger.getLogger(NicoHttpClient.class.getName()).
- log(Level.INFO, "マイリストURL: " + url);
+ String url = MY_LIST_PAGE_HEADER + listNo + "?rss=atom";
+ logger.debug("マイリストURL: " + url);
+ return accessRssUrl(url);
+ }
+
+ /**
+ * コンテンツ概略のストリームを取得する.
+ * @param movieNo
+ * @return コンテンツ概略. 取得元でcloseすること.
+ * @throws IOException
+ */
+ public InputStream getThumbInfo(String movieNo) throws IOException {
+ String url = MOVIE_THUMBNAIL_PAGE_HEADER + movieNo;
+ logger.debug("動画サムネイルURL: " + url);
- GetMethod get = new GetMethod(url);
+ HttpGet get = new HttpGet(url);
+ HttpResponse response = http.execute(get);
+ return response.getEntity().getContent();
+
+ }
+
+ /**
+ * 動画番号を指定したコンテンツ情報の取得.
+ * @param movieNo 動画番号.
+ * @return コンテンツ情報.
+ */
+ public NicoContent loadMyMovie(String movieNo) {
+ NicoContent cont = null;
+ InputStream re = null;
try {
- int statusCode = executeMethod(get);
- Reader reader = new BufferedReader(new InputStreamReader(get.getResponseBodyAsStream(), "UTF-8"));
- SyndFeedInput input = new SyndFeedInput();
- SyndFeed feed = input.build(reader);
+ re = getThumbInfo(movieNo);
+ // ドキュメントビルダーファクトリを生成
+ DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
+ // ドキュメントビルダーを生成
+ DocumentBuilder builder = dbfactory.newDocumentBuilder();
+ // パースを実行してDocumentオブジェクトを取得
+ Document doc = builder.parse(re);
+ // ルート要素を取得(タグ名:site)
+ Element root = doc.getDocumentElement();
+
+ if ("fail".equals(root.getAttribute("status"))) {
+ logger.warn("情報取得できません: " + movieNo);
+ return null;
+ }
+
+ NodeList list2 = root.getElementsByTagName("thumb");
+ cont = new NicoContent();
+ Element element = (Element) list2.item(0);
- list = (List<SyndEntryImpl>) feed.getEntries();
+ String watch_url = ((Element) element.getElementsByTagName("watch_url").item(0)).getFirstChild().
+ getNodeValue();
+ cont.setPageLink(watch_url);
+
+ String title = ((Element) element.getElementsByTagName("title").item(0)).getFirstChild().getNodeValue();
+ cont.setTitle(title);
+
+ // TODO 投稿日の設定
+// String first_retrieve = ((Element) element.getElementsByTagName("first_retrieve").item(0)).getFirstChild().getNodeValue();
+// cont.setPublishedDate(DateFormat.getInstance().parse(first_retrieve));
+//
+// } catch (ParseException ex) {
+// Logger.getLogger(NicoHttpClient.class.getName()).log(Level.SEVERE, null, ex);
+ } catch (SAXException ex) {
+ logger.error("", ex);
+ } catch (IOException ex) {
+ logger.error("", ex);
+ } catch (ParserConfigurationException ex) {
+ logger.error("", ex);
+ } finally {
+ try {
+ if (re != null) {
+ re.close();
+ }
+ } catch (IOException ex) {
+ logger.error("", ex);
+ }
+ }
+ return cont;
+ }
- } catch (IllegalArgumentException ex) {
- Logger.getLogger(NicoHttpClient.class.getName()).log(Level.SEVERE, null, ex);
+ private List<NicoContent> accessRssUrl(String url) {
+ List<NicoContent> contList = new ArrayList<NicoContent>();
+ HttpGet get = new HttpGet(url);
+ BufferedReader reader = null;
+ try {
+ HttpResponse response = http.execute(get);
+ reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
+ if (logger.isTraceEnabled()) {
+ reader.mark(1024 * 1024);
+ while (true) {
+ String str = reader.readLine();
+ if (str == null) {
+ break;
+ }
+ logger.trace(str);
+ }
+ reader.reset();
+ }
+ contList = getNicoContents(reader);
} catch (FeedException ex) {
- Logger.getLogger(NicoHttpClient.class.getName()).log(Level.SEVERE, null, ex);
+ logger.warn("アクセスできません: " + url);
+ logger.debug("", ex);
} catch (IOException ex) {
- Logger.getLogger(NicoHttpClient.class.getName()).log(Level.SEVERE, null, ex);
+ logger.error("", ex);
} finally {
- get.releaseConnection();
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException ex) {
+ logger.error("", ex);
+ }
+ }
}
+ return contList;
+ }
+
+ private List<NicoContent> getNicoContents(Reader reader) throws FeedException {
+ SyndFeedInput input = new SyndFeedInput();
+ SyndFeed feed = input.build(reader);
+
+ @SuppressWarnings("unchecked")
+ final List<SyndEntryImpl> list = (List<SyndEntryImpl>) feed.getEntries();
+
List<NicoContent> contList;
if (list == null) {
contList = new ArrayList<NicoContent>();
return contList;
}
+ @SuppressWarnings("unchecked")
private List<NicoContent> createContentsList(List<SyndEntryImpl> list) {
class CallBack extends HTMLEditorKit.ParserCallback {
- private String imageLink;
+ private boolean descFlag;
+ private String imageLink = new String();
+ private StringBuilder description = new StringBuilder();
@Override
public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
-// System.out.println("--------<" + t.toString() + ">--------");
-// printAttributes(a);
+ logger.debug("--------<" + t.toString() + ">--------");
+ logger.debug(a);
if (HTML.Tag.IMG.equals(t)) {
imageLink = a.getAttribute(HTML.Attribute.SRC).toString();
}
}
- // 属性を表示
-// private void printAttributes(MutableAttributeSet a) {
-// Enumeration e = a.getAttributeNames();
-// while (e.hasMoreElements()) {
-// Object key = e.nextElement();
-// System.out.println("---- " + key.toString() + " : " + a.getAttribute(key));
-// }
-// }
+ @Override
+ public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
+ if (HTML.Tag.P.equals(t)) {
+ if ("nico-description".equals(
+ a.getAttribute(HTML.Attribute.CLASS).toString())) {
+ descFlag = true;
+ }
+ }
+ logger.debug("--------<" + t.toString() + ">--------");
+ logger.debug(a);
+ }
+
+ @Override
+ public void handleEndTag(HTML.Tag t, int pos) {
+ if (HTML.Tag.P.equals(t)) {
+ descFlag = false;
+ }
+ logger.debug("--------</" + t.toString() + ">--------");
+ }
+
+ @Override
+ public void handleText(char[] data, int pos) {
+ if (descFlag) {
+ description.append(data);
+ }
+ logger.debug("--------TEXT--------");
+ logger.debug(data);
+ }
+
+ private void printAttributes(MutableAttributeSet a) {
+ final Enumeration<?> e = a.getAttributeNames();
+ while (e.hasMoreElements()) {
+ Object key = e.nextElement();
+ logger.debug("---- " + key.toString() + " : " + a.getAttribute(key));
+ }
+ }
+
public String getImageLink() {
return imageLink;
}
+
+ public String getDescription() {
+ return description.toString();
+ }
}
- List contList = new ArrayList<NicoContent>();
+ List<NicoContent> contList = new ArrayList<NicoContent>();
for (SyndEntryImpl entry : list) {
NicoContent content = new NicoContent();
- content.setTitle(entry.getTitle());
+ String title = entry.getTitle();
+ content.setTitle(title);
content.setPageLink(entry.getLink());
- content.setPublishedDate(entry.getPublishedDate());
- // サムネイル画像リンクの取得
+ // ã\82µã\83 ã\83\8dã\82¤ã\83«ç\94»å\83\8fã\83ªã\83³ã\82¯ã\81¨èª¬æ\98\8eæ\96\87ã\81®å\8f\96å¾\97
CallBack callBack = new CallBack();
for (SyndContentImpl sc : (List<SyndContentImpl>) entry.getContents()) {
try {
Reader reader = new StringReader(sc.getValue());
new ParserDelegator().parse(reader, callBack, true);
} catch (IOException ex) {
- Logger.getLogger(NicoHttpClient.class.getName()).log(Level.SEVERE, null, ex);
+ logger.error("RSSの読み込み失敗: " + content.getTitle());
}
}
- content.setImageLink(callBack.getImageLink());
- // リストへ追加.
+// リストへ追加.
contList.add(content);
}
return contList;
}
+
+ /**
+ * FLVファイルのURLを取得する. ログインが必要.
+ * また, 実際にFLVファイルの実態をダウンロードするには
+ * 一度http://www.nicovideo.jp/watch/ビデオID に一度アクセスする必要があることに
+ * 注意.
+ * (参考: http://yusukebe.com/tech/archives/20070803/124356.html)
+ * @param videoId ニコニコ動画のビデオID.
+ * @return FLVファイル実体があるURL.
+ * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
+ */
+ public VideoInfo getVideoInfo(String videoId) throws IOException {
+ final GetRealVideoIdResult res = accessWatchPage(videoId);
+ final String realVideoId = res.videoId;
+
+ String accessUrl = GET_FLV_INFO + realVideoId;
+ if (realVideoId.startsWith("nm")) {
+ accessUrl += "?as3=1";
+ }
+ Map<String, String> map = getParameterMap(accessUrl);
+
+ LinkedHashMap<String, String> keyMap = new LinkedHashMap<String, String>();
+ if ("1".equals(map.get("needs_key"))) {
+ // 公式動画投稿者コメント取得用パラメータ.
+ keyMap = getParameterMap(GET_THREAD_KEY_PAGE + map.get(VideoInfo.KEY_THREAD_ID));
+ }
+ return new VideoInfo(realVideoId, res.title, map, keyMap);
+ }
+
+ private LinkedHashMap<String, String> getParameterMap(String accessUrl) throws IOException, IllegalStateException {
+ logger.debug("アクセス: " + accessUrl);
+ HttpGet get = new HttpGet(accessUrl);
+ String resultString;
+ BufferedReader reader = null;
+ try {
+ HttpResponse response = http.execute(get);
+ reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
+ String str;
+ StringBuilder strBuilder = new StringBuilder();
+ while ((str = reader.readLine()) != null) {
+ strBuilder.append(str);
+ }
+ resultString = strBuilder.toString();
+ EntityUtils.consume(response.getEntity());
+ logger.debug(resultString);
+ } finally {
+ if (reader != null) {
+ reader.close();
+ }
+ }
+ String[] params = resultString.split("&");
+ LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
+ for (String param : params) {
+ String[] elm = param.split("=");
+ map.put(elm[0], elm[1]);
+ }
+ return map;
+ }
+
+ /**
+ * watchページコンテンツからタイトルを抽出する.
+ * @param content watchページコンテンツのストリーム.
+ */
+ private String getTitleInWatchPage(InputStream content) throws IOException {
+ final String TITLE_PARSE_STR_START = "<title>";
+ BufferedReader br = new BufferedReader(new InputStreamReader(content, "UTF-8"));
+ String ret;
+ while ((ret = br.readLine()) != null) {
+ final int index = ret.indexOf(TITLE_PARSE_STR_START);
+ if (index >= 0) {
+ String videoTitle = ret.substring(index + TITLE_PARSE_STR_START.length(), ret.indexOf(" ‐", index));
+ return videoTitle;
+ }
+ }
+ return "";
+
+ }
+
+ private static class GetRealVideoIdResult {
+
+ private final String videoId;
+ private final String title;
+
+ private GetRealVideoIdResult(String videoId, String title) {
+ this.videoId = videoId;
+ this.title = title;
+ }
+ }
+
+ /**
+ * WATCHページへアクセスする. getflvを行うためには, 必ず事前にWATCHページへアクセスしておく必要があるため.
+ * WATCHページ参照時にリダイレクトが発生する(so動画ではスレッドIDのWATCHページにリダイレクトされる)場合には
+ * そちらのページにアクセスし、そのスレッドIDをrealIdとして返します.
+ * @param videoId 取得したいビデオのビデオID.
+ * @return 実際のアクセスに必要なIDと、タイトル. タイトルはいんきゅばす互換用です.
+ * @throws IOException アクセスに失敗した場合. 有料動画などがこれに含まれます.
+ */
+ private GetRealVideoIdResult accessWatchPage(String videoId) throws IOException {
+ String realId = videoId;
+ String title;
+ String watchUrl = WATCH_PAGE + videoId;
+ logger.debug("アクセス: " + watchUrl);
+ final HttpGet get = new HttpGet(watchUrl);
+ final HttpContext context = new BasicHttpContext();
+ final HttpResponse response = http.execute(get, context);
+ try {
+ final RedirectLocations rl = (RedirectLocations) context.getAttribute(
+ "http.protocol.redirect-locations");
+ // 通常の動画(sm動画など)はリダイレクトが発生しないためnullになる
+ if (rl != null) {
+ final List<URI> locations = rl.getAll();
+ logger.debug("リダイレクト数: " + locations.size());
+
+ // so動画はスレッドIDのページへリダイレクトされる
+ if (locations.size() == 1) {
+ realId = locations.get(0).toString().replace(WATCH_PAGE, "");
+ } else if (locations.size() > 1) {
+ throw new IOException("有料動画と思われるため処理を中断しました: " + ArrayUtils.toString(locations));
+ }
+ }
+
+ title = getTitleInWatchPage(response.getEntity().getContent());
+ } finally {
+ EntityUtils.consume(response.getEntity());
+ }
+ return new GetRealVideoIdResult(realId, title);
+ }
+
+ /**
+ * ニコニコ動画から動画ファイルをダウンロードする.
+ * @param vi getVideoInfoメソッドで取得したオブジェクト.
+ * @param saveDir ダウンロードしたファイルを保存するディレクトリ.
+ * @param np 保存するファイル名の命名規則. 拡張子は別途付与されるため不要.
+ * @param nowStatus ダウンロードしようとしている動画ファイルの, 現在のステータス.
+ * @param needLowFile エコノミー動画をダウンロードするのであればtrue.
+ * @return この処理を行った後の, 対象ファイルのステータス.
+ * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
+ */
+ public GetFlvResult getFlvFile(VideoInfo vi, File saveDir, NamePattern np, Status nowStatus, boolean needLowFile,
+ ProgressListener listener) throws IOException, URISyntaxException, HttpException, InterruptedException {
+
+ final URL notifierUrl = vi.getSmileUrl();
+
+ String userName = null;
+ if (notifierUrl != null) {
+ HttpGet get = new HttpGet(notifierUrl.toString());
+ HttpResponse response = http.execute(get);
+ userName = Util.getUserName(response.getEntity().getContent());
+ EntityUtils.consume(response.getEntity());
+ }
+
+ final URL url = vi.getVideoUrl();
+ if (nowStatus == Status.GET_LOW || !needLowFile) {
+ if (url.toString().contains("low")) {
+ logger.info("エコノミー動画のためスキップ: " + vi.getRealVideoId());
+ return new GetFlvResult(null, nowStatus, userName);
+ }
+ }
+ final boolean isNotLow = !url.toString().contains("low");
+
+ final File downloadFile = new File(saveDir, np.createFileName(vi.getRealVideoId(), isNotLow));
+
+ HttpGet get = new HttpGet(url.toURI());
+ HttpResponse response = http.execute(get);
+ String contentType = response.getEntity().getContentType().getValue();
+ logger.debug(contentType);
+ logger.debug(downloadFile.toString());
+ if ("text/plain".equals(contentType) || "text/html".equals(contentType)) {
+ logger.error("取得できませんでした. サーバが混みあっている可能性があります: " + vi.getRealVideoId());
+ EntityUtils.consume(response.getEntity());
+ return new GetFlvResult(null, Status.GET_INFO, userName);
+ }
+ String ext = Util.getExtention(contentType);
+ final long fileSize = response.getEntity().getContentLength();
+
+ final int BUF_SIZE = 1024 * 32;
+ BufferedInputStream in = new BufferedInputStream(response.getEntity().getContent());
+
+ File file = new File(downloadFile.toString() + "." + ext);
+ logger.info("保存します(" + fileSize / 1024 + "KB): " + file.getPath());
+ FileOutputStream fos = new FileOutputStream(file);
+ BufferedOutputStream out = new BufferedOutputStream(fos);
+
+ long downloadSize = 0;
+ int i;
+ byte[] buffer = new byte[BUF_SIZE];
+ while ((i = in.read(buffer)) != -1) {
+ out.write(buffer, 0, i);
+ downloadSize += i;
+ listener.progress(fileSize, downloadSize);
+ if (listener.getCancel()) {
+ return new GetFlvResult(null, Status.GET_INFO, userName);
+ }
+ }
+
+ EntityUtils.consume(response.getEntity());
+ out.close();
+ in.close();
+ if (url.toString().contains("low")) {
+ return new GetFlvResult(file, Status.GET_LOW, userName);
+ }
+ return new GetFlvResult(file, Status.GET_FILE, userName);
+ }
+
+ /**
+ * ニコニコ動画から動画ファイルをダウンロードする.
+ * @param vi getVideoInfoメソッドで取得したオブジェクト.
+ * @param fileName ダウンロード後のファイル名. 拡張子は別途付与されるため不要.
+ * @param nowStatus ダウンロードしようとしている動画ファイルの, 現在のステータス.
+ * @param needLowFile エコノミー動画をダウンロードするのであればtrue.
+ * @return この処理を行った後の, 対象ファイルのステータス.
+ * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
+ */
+ public GetFlvResult getFlvFile(VideoInfo vi, String fileName, Status nowStatus, boolean needLowFile,
+ ProgressListener listener) throws IOException, URISyntaxException, HttpException, InterruptedException {
+ String file = FilenameUtils.getName(fileName);
+ String dir = fileName.substring(0, fileName.length() - file.length());
+ NamePattern np = new NamePattern(file, "", "", "");
+ return getFlvFile(vi, new File(dir), np, nowStatus, needLowFile, listener);
+ }
+
+ /**
+ * ニコニコ動画から動画ファイルをダウンロードする.
+ * @param vi getVideoInfoメソッドで取得したオブジェクト.
+ * @param fileName ダウンロード後のファイル名. 拡張子は別途付与されるため不要.
+ * @return この処理を行った後の, 対象ファイルのステータス.
+ * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
+ */
+ public GetFlvResult getFlvFile(VideoInfo vi, String fileName, ProgressListener listener) throws IOException,
+ URISyntaxException,
+ HttpException, InterruptedException {
+ return getFlvFile(vi, fileName, Status.GET_INFO, true, listener);
+ }
+
+ /**
+ * ニコニコ動画から動画ファイルをダウンロードする.
+ * ファイル名はビデオID名となる.
+ * @param vi getVideoInfoメソッドで取得したオブジェクト.
+ * @return この処理を行った後の, 対象ファイルのステータス.
+ * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
+ */
+ public GetFlvResult getFlvFile(VideoInfo vi) throws IOException, URISyntaxException, HttpException,
+ InterruptedException {
+ return getFlvFile(vi, vi.getRealVideoId(), Status.GET_INFO, true, ProgressListener.EMPTY_LISTENER);
+ }
+
+ public File getCommentFile(VideoInfo vi, String fileName, WayBackInfo wayback, int commentNum, boolean oldVersion)
+ throws Exception {
+ return downloadComment(vi, fileName, false, wayback, Integer.valueOf(commentNum), oldVersion);
+ }
+
+ public File getCommentFile(VideoInfo vi, String fileName) throws Exception {
+ return downloadComment(vi, fileName, false, null, null, false);
+ }
+
+ public File getTCommentFile(VideoInfo vi, String fileName) throws Exception {
+ return downloadComment(vi, fileName, true, null, Integer.valueOf(1000), true);
+ }
+
+ private File downloadComment(VideoInfo vi, String fileName, boolean isTcomm, WayBackInfo wayback, Integer commentNum,
+ boolean oldVersion)
+ throws Exception {
+ HttpResponse response = null;
+ BufferedOutputStream bos = null;
+
+ try {
+ final HttpPost post = new HttpPost(vi.getMessageUrl().toString());
+ final String param;
+ if (oldVersion || isTcomm) {
+ param = createCommentDownloadParameter20101222(vi, isTcomm, wayback, commentNum);
+ } else {
+ param = createCommentDownloadParameter(vi, wayback, commentNum);
+ }
+ final StringEntity se = new StringEntity(param);
+ post.setEntity(se);
+ response = http.execute(post);
+ final InputStream is = response.getEntity().getContent();
+ final BufferedInputStream bis = new BufferedInputStream(is);
+
+ final String outputFileName = (fileName.endsWith(".xml")) ? fileName : fileName + ".xml";
+ bos = new BufferedOutputStream(new FileOutputStream(outputFileName));
+
+ final byte[] buf = new byte[1024 * 1024];
+ int read;
+ while ((read = bis.read(buf, 0, buf.length)) > 0) {
+ bos.write(buf, 0, read);
+ }
+
+ return new File(outputFileName);
+ } catch (Exception e) {
+ throw new Exception("コメントダウンロードに失敗しました。", e);
+ } finally {
+ if (response != null) {
+ EntityUtils.consume(response.getEntity());
+ }
+ if (bos != null) {
+ bos.close();
+ }
+ }
+ }
+
+ /**
+ * 2011/2/3 以降のコメント表示仕様に基づいた取得パラメータ生成.
+ * @param vi ビデオ情報.
+ * @param wayback 過去ログ情報. 過去ログ取得でないバイはnull.
+ * @param commentNum コメント取得数. ビデオ再生時間に応じたデフォルト値を用いる場合にはnull.
+ * @return 生成されたパラメータ.
+ */
+ private String createCommentDownloadParameter(VideoInfo vi, WayBackInfo wayback, Integer commentNum) {
+ final Map<String, String> threadKey = vi.getKeyMap();
+ final Map<String, String> th = new HashMap<String, String>();
+ th.put("thread", vi.getThreadId());
+ th.put("version", "20090904");
+ th.put("user_id", vi.getUserId());
+ if (wayback != null) {
+ th.put("waybackkey", wayback.getKey());
+ th.put("when", Long.toString(wayback.getTime()));
+ }
+
+ final Map<String, String> leaf = new HashMap<String, String>();
+ leaf.put("thread", vi.getThreadId());
+ leaf.put("user_id", vi.getUserId());
+ if (wayback != null) {
+ leaf.put("waybackkey", wayback.getKey());
+ leaf.put("when", Long.toString(wayback.getTime()));
+ }
+
+ final int minutes = (int) Math.ceil(vi.getVideoLength() / 60.0);
+ // 1分当たり100件のコメントを表示するのは720分未満の動画だけで, それ以上は調整が入るらしい
+ // (どんなに長くても1動画当たり720*100件が最大。それを超える場合には1分当たりの件数を削減する)
+ final int max100perMin = 720;
+ final int perMin = (minutes < max100perMin) ? 100 : (max100perMin * 100) / minutes;
+
+ final int resFrom = (commentNum != null) ? commentNum.intValue() : vi.getResFrom();
+ final String element = "0-" + minutes + ":" + perMin + "," + resFrom;
+
+ final StringBuilder str = new StringBuilder();
+ str.append("<packet>");
+
+ str.append("<thread");
+ addMapToAttr(str, th);
+ addMapToAttr(str, threadKey);
+ str.append(" />");
+
+ str.append("<thread_leaves");
+ addMapToAttr(str, leaf);
+ addMapToAttr(str, threadKey);
+ str.append(">");
+ str.append(element);
+ str.append("</thread_leaves>");
+
+ str.append("</packet>");
+
+ return str.toString();
+ }
+
+ private static void addMapToAttr(final StringBuilder str, final Map<String, String> map) {
+ final String quote = "\"";
+ for (String k : map.keySet()) {
+ final String v = map.get(k);
+ str.append(" ");
+ str.append(k);
+ str.append("=");
+ str.append(quote);
+ str.append(v);
+ str.append(quote);
+ }
+ }
+
+ /**
+ * 2010/12/22 までのコメント表示仕様に基づいた取得パラメータ生成.
+ * 「コメントの量を減らす」にチェックを入れた場合は現在でもこれが用いられているはず.
+ */
+ private String createCommentDownloadParameter20101222(VideoInfo vi, boolean isTcomm, WayBackInfo wayback,
+ Integer commentNum) {
+ final Map<String, String> params = new HashMap<String, String>();
+
+ params.put(VideoInfo.KEY_USER_ID, vi.getUserId());
+ params.put("thread", vi.getThreadId());
+ params.put("version", "20061206");
+
+ final int resFrom = (commentNum == null || commentNum <= 0) ? vi.getResFrom() : commentNum.intValue();
+ params.put("res_from", "-" + Integer.toString(resFrom));
+
+ if (isTcomm) {
+ params.put("fork", "1");
+ }
+
+ if (wayback != null) {
+ params.put("waybackkey", wayback.getKey());
+ params.put("when", Long.toString(wayback.getTime()));
+ }
+
+ final StringBuilder str = new StringBuilder();
+ str.append("<thread");
+
+ addMapToAttr(str, vi.getKeyMap());
+ addMapToAttr(str, params);
+
+ str.append("/>");
+
+ return str.toString();
+ }
+
+ /**
+ * 動画をマイリストへ登録する. ログインが必要.
+ * @param myListId 登録するマイリストのID.
+ * @param videoId 登録する動画ID.
+ * @throws IOException 登録に失敗した.
+ */
+ public void addMyList(String myListId, String videoId) throws IOException {
+ String itemType = null;
+ String itemId = null;
+ String token = null;
+ HttpGet get = new HttpGet(ADD_MYLIST_PAGE + videoId);
+ HttpResponse response = http.execute(get);
+ HttpEntity entity = response.getEntity();
+ try {
+ InputStream is = entity.getContent();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+ String line;
+
+ Pattern pattern = Pattern.compile("input type=\"hidden\" name=\"item_type\" value=\"(.+)\"");
+ while ((line = reader.readLine()) != null) {
+ Matcher m = pattern.matcher(line);
+ if (m.find()) {
+ itemType = m.group(1);
+ break;
+ }
+ }
+
+ pattern = Pattern.compile("input type=\"hidden\" name=\"item_id\" value=\"(.+)\"");
+ while ((line = reader.readLine()) != null) {
+ Matcher m = pattern.matcher(line);
+ if (m.find()) {
+ itemId = m.group(1);
+ break;
+ }
+ }
+
+ pattern = Pattern.compile("NicoAPI\\.token = \"(.*)\";");
+ while ((line = reader.readLine()) != null) {
+ Matcher m = pattern.matcher(line);
+ if (m.find()) {
+ token = m.group(1);
+ break;
+ }
+ }
+ } finally {
+ EntityUtils.consume(entity);
+ }
+
+ if (itemType == null || itemId == null || token == null) {
+ throw new IOException("マイリスト登録に必要な情報が取得できませんでした。 "
+ + "マイリスト:" + myListId + ", 動画ID:" + videoId + ", item_type:" + itemType + ", item_id:" + itemId
+ + ", token:" + token);
+ }
+
+ StringEntity se = new StringEntity(
+ "group_id=" + myListId
+ + "&item_type=" + itemType
+ + "&item_id=" + itemId
+ + "&description=" + ""
+ + "&token=" + token);
+
+ HttpPost post = new HttpPost("http://www.nicovideo.jp/api/mylist/add");
+ post.setHeader("Content-Type", "application/x-www-form-urlencoded");
+ post.setEntity(se);
+ response = http.execute(post);
+ int statusCode = response.getStatusLine().getStatusCode();
+ EntityUtils.consume(response.getEntity());
+ if (statusCode != HttpStatus.SC_OK) {
+ throw new IOException("マイリスト登録に失敗" + "マイリスト:" + myListId + ", 動画ID:" + videoId);
+ }
+ }
}