OSDN Git Service

2010年年末コメント方式でダウンロードするインタフェースを追加. 過去ログ取得処理を再実装.
[coroid/NicoBrowser.git] / src / nicobrowser / NicoHttpClient.java
index f009f34..f4e2664 100644 (file)
 /*$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);
     }
 
     /**
@@ -58,51 +120,203 @@ public class NicoHttpClient extends HttpClient {
      * @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));
+        }
+    }
+
+    /**
      * マイリストに登録した動画一覧の取得.
      * 「公開」設定にしていないリストからは取得できない.
      * ログインしていなくても取得可能.
@@ -110,30 +324,130 @@ public class NicoHttpClient extends HttpClient {
      * @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>();
@@ -143,57 +457,567 @@ public class NicoHttpClient extends HttpClient {
         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);
+        }
+    }
 }