OSDN Git Service

ccb364829adbeacd014ead07f7ee3fa8f135adc0
[coroid/NicoBrowser.git] / src / nicobrowser / NicoHttpClient.java
1 /*$Id$*/
2 package nicobrowser;
3
4 import java.net.URISyntaxException;
5 import nicobrowser.entity.NicoContent;
6 import com.sun.syndication.feed.synd.SyndContentImpl;
7 import com.sun.syndication.feed.synd.SyndEntryImpl;
8 import com.sun.syndication.feed.synd.SyndFeed;
9 import com.sun.syndication.io.FeedException;
10 import com.sun.syndication.io.SyndFeedInput;
11 import java.io.BufferedInputStream;
12 import java.io.BufferedOutputStream;
13 import java.io.BufferedReader;
14 import java.io.File;
15 import java.io.FileOutputStream;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.io.InputStreamReader;
19 import java.io.Reader;
20 import java.io.StringReader;
21 import java.net.URL;
22 import java.net.URLDecoder;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Enumeration;
26 import java.util.List;
27 import javax.swing.text.MutableAttributeSet;
28 import javax.swing.text.html.HTML;
29 import javax.swing.text.html.HTMLEditorKit;
30 import javax.swing.text.html.parser.ParserDelegator;
31 import javax.xml.parsers.DocumentBuilder;
32 import javax.xml.parsers.DocumentBuilderFactory;
33 import javax.xml.parsers.ParserConfigurationException;
34 import nicobrowser.entity.NicoContent.Status;
35 import nicobrowser.util.Result;
36 import nicobrowser.util.Util;
37 import org.apache.commons.logging.Log;
38 import org.apache.commons.logging.LogFactory;
39 import org.apache.http.HttpEntity;
40 import org.apache.http.HttpException;
41 import org.apache.http.HttpResponse;
42 import org.apache.http.HttpStatus;
43 import org.apache.http.NameValuePair;
44 import org.apache.http.client.entity.UrlEncodedFormEntity;
45 import org.apache.http.client.methods.HttpGet;
46 import org.apache.http.client.methods.HttpPost;
47 import org.apache.http.client.params.ClientPNames;
48 import org.apache.http.client.params.CookiePolicy;
49 import org.apache.http.cookie.Cookie;
50 import org.apache.http.impl.client.DefaultHttpClient;
51 import org.apache.http.message.BasicNameValuePair;
52 import org.w3c.dom.Document;
53 import org.w3c.dom.Element;
54 import org.w3c.dom.NodeList;
55 import org.xml.sax.SAXException;
56
57 /**
58  *
59  * @author yuki
60  */
61 public class NicoHttpClient extends DefaultHttpClient {
62
63     private static Log log = LogFactory.getLog(NicoHttpClient.class);
64     static NicoHttpClient instance;
65     private static final String LOGIN_PAGE =
66             "https://secure.nicovideo.jp/secure/login?site=niconico";
67     private static final String LOGOUT_PAGE =
68             "https://secure.nicovideo.jp/secure/logout";
69     private static final String MY_LIST_PAGE_HEADER =
70             "http://www.nicovideo.jp/mylist/";
71     private static final String MOVIE_THUMBNAIL_PAGE_HEADER =
72             "http://www.nicovideo.jp/api/getthumbinfo/";
73     private static final String GET_FLV_INFO = "http://www.nicovideo.jp/api/getflv/";
74     private static final String SEARCH_HEAD = "http://www.nicovideo.jp/search/";
75     private static final String SEARCH_TAIL = "?sort=v";
76
77     private NicoHttpClient() {
78         super();
79         getParams().setParameter(
80                 ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
81         instance = this;
82     }
83
84     public static NicoHttpClient getInstance() {
85         if (instance == null) {
86             return new NicoHttpClient();
87         }
88         return instance;
89     }
90
91     /**
92      * ニコニコ動画へログインする.
93      * @param mail ログイン識別子(登録メールアドレス).
94      * @param password パスワード.
95      * @return 認証がOKであればtrue.
96      */
97     public boolean login(String mail, String password) throws URISyntaxException, HttpException, InterruptedException {
98         boolean auth = false;
99         HttpPost post = new HttpPost(LOGIN_PAGE);
100
101         try {
102             NameValuePair[] nvps = new NameValuePair[]{
103                 new BasicNameValuePair("mail", mail),
104                 new BasicNameValuePair("password", password),
105                 new BasicNameValuePair("next_url", "")
106             };
107             post.setEntity(new UrlEncodedFormEntity(Arrays.asList(nvps), "UTF-8"));
108
109             //post.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
110             HttpResponse response = execute(post);
111             log.debug("ログインステータスコード: " + response.getStatusLine().getStatusCode());
112
113             // ログイン可否の判定.
114             HttpEntity entity = response.getEntity();
115             entity.consumeContent();
116             List<Cookie> cookies = getCookieStore().getCookies();
117             if (!cookies.isEmpty()) {
118                 auth = true;
119             }
120         } catch (IOException ex) {
121             log.error("ログイン時に問題が発生", ex);
122         }
123         return auth;
124     }
125
126     /**
127      * ニコニコ動画からログアウトする.
128      * @return ログアウトに成功すればtrue.
129      */
130     public boolean logout() throws URISyntaxException, HttpException, InterruptedException {
131         boolean result = false;
132         HttpGet method = new HttpGet(LOGOUT_PAGE);
133         try {
134             HttpResponse response = execute(method);
135             log.debug("ログアウトステータスコード: " + response.getStatusLine().getStatusCode());
136
137             if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
138                 result = true;
139             }
140             response.getEntity().consumeContent();
141         } catch (IOException ex) {
142             log.error("ログアウト時に問題が発生", ex);
143         }
144         return result;
145     }
146
147     /**
148      * キーワード検索を行う.
149      * @param word 検索キーワード
150      * @return 検索結果.
151      */
152     public List<NicoContent> search(String word) {
153         log.debug("検索:" + word);
154
155         InputStream is = null;
156         List<NicoContent> conts = new ArrayList<NicoContent>();
157         String url = new String(SEARCH_HEAD + word + SEARCH_TAIL);
158
159         try {
160             while (url != null) {
161                 HttpGet get = new HttpGet(url);
162                 HttpResponse response;
163                 response = execute(get);
164                 is = new BufferedInputStream(response.getEntity().getContent());
165                 assert is.markSupported();
166                 is.mark(1024 * 1024);
167                 List<Result> results = Util.parseSerchResult(is);
168                 for (Result r : results) {
169                     NicoContent c = loadMyMovie(r.getId());
170                     if (c != null) {
171                         conts.add(c);
172                     }
173                 }
174                 is.reset();
175                 url = Util.getNextPage(is);
176                 is.close();
177             }
178         } catch (IOException ex) {
179             log.error("検索結果処理時に例外発生", ex);
180         }
181         return conts;
182     }
183
184     /**
185      * 「マイリスト登録数ランキング(本日)」の動画一覧を取得する。
186      * @return 動画一覧.
187      */
188     public List<NicoContent> loadMyListDaily() throws URISyntaxException, HttpException, InterruptedException {
189         List<NicoContent> list = new ArrayList<NicoContent>();
190         String url = new String("http://www.nicovideo.jp/ranking/mylist/daily/all?rss=atom");
191         log.debug("全動画サイトのマイリスト登録数ランキング(本日)[全体] : " + url);
192
193         HttpGet get = new HttpGet(url);
194
195         BufferedReader reader = null;
196         try {
197             HttpResponse response = execute(get);
198             reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
199             // BOMを読み捨て
200             // reader.skip(1);
201             list = getNicoContents(reader);
202             deleteRankString(list);
203             response.getEntity().consumeContent();
204         } catch (FeedException ex) {
205             log.error("", ex);
206         } catch (IOException ex) {
207             log.error("", ex);
208         } finally {
209             if (reader != null) {
210                 try {
211                     reader.close();
212                 } catch (IOException ex) {
213                     log.error("", ex);
214                 }
215             }
216         }
217         return list;
218     }
219
220     /**
221      * ニコニコ動画のRSSからコンテンツリストを取得する.
222      * @param url 取得するrssのurl.
223      * @return コンテンツリスト.
224      */
225     public List<NicoContent> getContentsFromRss(String url) {
226         log.debug("アクセスURL: " + url);
227         List<NicoContent> list = accessRssUrl(url);
228         if (url.contains("ranking")) {
229             deleteRankString(list);
230         }
231         return list;
232     }
233
234     /**
235      * rankingの場合、本当のタイトルの前に"第XX位:"の文字列が
236      * 挿入されているため, それを削る.
237      * @param list 対象のリスト.
238      */
239     private void deleteRankString(List<NicoContent> list) {
240         for (NicoContent c : list) {
241             String title = c.getTitle();
242             int offset = title.indexOf(":") + 1;
243             c.setTitle(title.substring(offset));
244         }
245     }
246
247     /**
248      * マイリストに登録した動画一覧の取得.
249      * 「公開」設定にしていないリストからは取得できない.
250      * ログインしていなくても取得可能.
251      * @param listNo マイリストNo.
252      * @return 動画一覧.
253      */
254     public List<NicoContent> loadMyList(String listNo) {
255         String url = new String(MY_LIST_PAGE_HEADER + listNo + "?rss=atom");
256         log.debug("マイリストURL: " + url);
257         return accessRssUrl(url);
258     }
259
260     /**
261      * 動画番号を指定したコンテンツ情報の取得.
262      * @param movieNo 動画番号.
263      * @return コンテンツ情報.
264      */
265     public NicoContent loadMyMovie(String movieNo) {
266         NicoContent cont = null;
267         InputStream re = null;
268         List<SyndEntryImpl> list = null;
269         String url = new String(MOVIE_THUMBNAIL_PAGE_HEADER + movieNo);
270         log.debug("動画サムネイルURL: " + url);
271
272         HttpGet get;
273
274         try {
275             get = new HttpGet(url);
276             HttpResponse response = execute(get);
277             re = response.getEntity().getContent();
278             // ドキュメントビルダーファクトリを生成
279             DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
280             // ドキュメントビルダーを生成
281             DocumentBuilder builder = dbfactory.newDocumentBuilder();
282             // パースを実行してDocumentオブジェクトを取得
283             Document doc = builder.parse(re);
284             // ルート要素を取得(タグ名:site)
285             Element root = doc.getDocumentElement();
286
287             if ("fail".equals(root.getAttribute("status"))) {
288                 log.warn("情報取得できません: " + movieNo);
289                 return null;
290             }
291
292             NodeList list2 = root.getElementsByTagName("thumb");
293             cont = new NicoContent();
294             Element element = (Element) list2.item(0);
295
296             String watch_url = ((Element) element.getElementsByTagName("watch_url").item(0)).getFirstChild().
297                     getNodeValue();
298             cont.setPageLink(watch_url);
299
300             String title = ((Element) element.getElementsByTagName("title").item(0)).getFirstChild().getNodeValue();
301             cont.setTitle(title);
302
303             // TODO 投稿日の設定
304 //            String first_retrieve = ((Element) element.getElementsByTagName("first_retrieve").item(0)).getFirstChild().getNodeValue();
305 //            cont.setPublishedDate(DateFormat.getInstance().parse(first_retrieve));
306 //
307 //        } catch (ParseException ex) {
308 //            Logger.getLogger(NicoHttpClient.class.getName()).log(Level.SEVERE, null, ex);
309         } catch (SAXException ex) {
310             log.error("", ex);
311         } catch (IOException ex) {
312             log.error("", ex);
313         } catch (ParserConfigurationException ex) {
314             log.error("", ex);
315         } finally {
316             try {
317                 if (re != null) {
318                     re.close();
319                 }
320             } catch (IOException ex) {
321                 log.error("", ex);
322             }
323         }
324         return cont;
325     }
326
327     private List<NicoContent> accessRssUrl(String url) {
328         List<NicoContent> contList = new ArrayList<NicoContent>();
329         HttpGet get = new HttpGet(url);
330         BufferedReader reader = null;
331         try {
332             HttpResponse response = execute(get);
333             reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
334             if (log.isTraceEnabled()) {
335                 reader.mark(1024 * 1024);
336                 while (true) {
337                     String str = reader.readLine();
338                     if (str == null) {
339                         break;
340                     }
341                     log.trace(str);
342                 }
343                 reader.reset();
344             }
345             contList = getNicoContents(reader);
346         } catch (FeedException ex) {
347             log.warn("アクセスできません: " + url);
348             log.debug("", ex);
349         } catch (IOException ex) {
350             log.error("", ex);
351         } finally {
352             if (reader != null) {
353                 try {
354                     reader.close();
355                 } catch (IOException ex) {
356                     log.error("", ex);
357                 }
358             }
359         }
360         return contList;
361     }
362
363     private List<NicoContent> getNicoContents(Reader reader) throws FeedException {
364         List<SyndEntryImpl> list = null;
365         SyndFeedInput input = new SyndFeedInput();
366         SyndFeed feed = input.build(reader);
367
368         list = (List<SyndEntryImpl>) feed.getEntries();
369
370         List<NicoContent> contList;
371         if (list == null) {
372             contList = new ArrayList<NicoContent>();
373         } else {
374             contList = createContentsList(list);
375         }
376         return contList;
377     }
378
379     private List<NicoContent> createContentsList(List<SyndEntryImpl> list) {
380         class CallBack extends HTMLEditorKit.ParserCallback {
381
382             private boolean descFlag;
383             private String imageLink = new String();
384             private StringBuilder description = new StringBuilder();
385
386             @Override
387             public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
388                 log.debug("--------<" + t.toString() + ">--------");
389                 log.debug(a);
390                 if (HTML.Tag.IMG.equals(t)) {
391                     imageLink = a.getAttribute(HTML.Attribute.SRC).toString();
392                 }
393             }
394
395             @Override
396             public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
397                 if (HTML.Tag.P.equals(t)) {
398                     if ("nico-description".equals(
399                             a.getAttribute(HTML.Attribute.CLASS).toString())) {
400                         descFlag = true;
401                     }
402                 }
403                 log.debug("--------<" + t.toString() + ">--------");
404                 log.debug(a);
405             }
406
407             @Override
408             public void handleEndTag(HTML.Tag t, int pos) {
409                 if (HTML.Tag.P.equals(t)) {
410                     descFlag = false;
411                 }
412                 log.debug("--------</" + t.toString() + ">--------");
413             }
414
415             @Override
416             public void handleText(char[] data, int pos) {
417                 if (descFlag) {
418                     description.append(data);
419                 }
420                 log.debug("--------TEXT--------");
421                 log.debug(data);
422             }
423
424             private void printAttributes(MutableAttributeSet a) {
425                 Enumeration e = a.getAttributeNames();
426                 while (e.hasMoreElements()) {
427                     Object key = e.nextElement();
428                     log.debug("---- " + key.toString() + " : " + a.getAttribute(key));
429                 }
430             }
431
432             public String getImageLink() {
433                 return imageLink;
434             }
435
436             public String getDescription() {
437                 return description.toString();
438             }
439         }
440
441         List<NicoContent> contList = new ArrayList<NicoContent>();
442
443         for (SyndEntryImpl entry : list) {
444             NicoContent content = new NicoContent();
445
446             String title = entry.getTitle();
447             content.setTitle(title);
448             content.setPageLink(entry.getLink());
449
450             // サムネイル画像リンクと説明文の取得
451             CallBack callBack = new CallBack();
452             for (SyndContentImpl sc : (List<SyndContentImpl>) entry.getContents()) {
453                 try {
454                     Reader reader = new StringReader(sc.getValue());
455                     new ParserDelegator().parse(reader, callBack, true);
456                 } catch (IOException ex) {
457                     log.error("RSSの読み込み失敗: " + content.getTitle());
458                 }
459             }
460
461 // リストへ追加.
462             contList.add(content);
463         }
464         return contList;
465     }
466
467     /**
468      * FLVファイルのURLを取得する. ログインが必要.
469      * また, 実際にFLVファイルの実態をダウンロードするには
470      * 一度http://www.nicovideo.jp/watch/ビデオIDに一度アクセスする必要があることに
471      * 注意.
472      * (参考: http://yusukebe.com/tech/archives/20070803/124356.html)
473      * @param videoID ニコニコ動画のビデオID.
474      * @return FLVファイル実体があるURL.
475      * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
476      */
477     public URL getFlvUrl(String videoID) throws IOException {
478         String accessUrl = GET_FLV_INFO + videoID;
479         if (videoID.startsWith("nm")) {
480             accessUrl += "?as3=1";
481         }
482         log.debug("アクセス: " + accessUrl);
483         HttpGet get = new HttpGet(accessUrl);
484         String resultString;
485         BufferedReader reader = null;
486         try {
487             HttpResponse response = execute(get);
488             reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
489
490             String str;
491             StringBuilder strBuilder = new StringBuilder();
492             while ((str = reader.readLine()) != null) {
493                 strBuilder.append(str);
494             }
495             resultString = strBuilder.toString();
496             response.getEntity().consumeContent();
497             log.debug(resultString);
498         } finally {
499             if (reader != null) {
500                 reader.close();
501             }
502         }
503
504         String[] urls = resultString.split("&");
505         final String marker = "url=";
506         for (String url : urls) {
507             if (url.contains(marker)) {
508                 String result = url.substring(marker.length());
509                 result = URLDecoder.decode(result, "UTF-8");
510
511                 return new URL(result);
512             }
513         }
514         throw new IOException("フォーマット仕様変更? ID: " + videoID + ", パラメータ:" + resultString);
515     }
516
517     /**
518      * ニコニコ動画から動画ファイルをダウンロードする.
519      * @param videoID smxxxx形式のビデオID.
520      * @param fileName ダウンロード後のファイル名. 拡張子は別途付与されるため不要.
521      * @param nowStatus ダウンロードしようとしている動画ファイルの, 現在のステータス.
522      * @param mp4ExtIsMp4 mp4ファイルの拡張子に.mp4を用いるか. falseの場合は.flvを付与する(過去のCraving Explorer互換用).
523      * @return この処理を行った後の, 対象ファイルのステータス.
524      * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
525      */
526     public GetFlvResult getFlvFile(String videoID, String fileName, Status nowStatus, boolean mp4ExtIsMp4) throws
527             IOException,
528             URISyntaxException, HttpException, InterruptedException {
529         byte[] buffer = new byte[1024 * 32];
530         final String watchUrl = "http://www.nicovideo.jp/watch/" + videoID;
531         log.debug("アクセス: " + watchUrl);
532         HttpGet get = new HttpGet(watchUrl);
533         HttpResponse response = execute(get);
534         final String userId = Util.getUserId(response.getEntity().getContent());
535         log.debug("userId: " + userId);
536         response.getEntity().consumeContent();
537
538         String userName = null;
539         if (userId != null) {
540             final String userUrl = "http://www.nicovideo.jp/user/" + userId;
541             log.debug("アクセス: " + watchUrl);
542             get = new HttpGet(userUrl);
543             response = execute(get);
544             userName = Util.getUserName(response.getEntity().getContent());
545             response.getEntity().consumeContent();
546         }
547
548         URL url = getFlvUrl(videoID);
549         if (nowStatus == Status.GET_LOW && url.toString().contains("low")) {
550             log.info("lowファイル取得済みのためスキップ" + videoID + ":" + fileName);
551             return new GetFlvResult(nowStatus, userName);
552         }
553
554         get = new HttpGet(url.toURI());
555         response = execute(get);
556         String contentType = response.getEntity().getContentType().getValue();
557         log.debug(contentType);
558         log.debug(fileName);
559         if ("text/plain".equals(contentType) || "text/html".equals(contentType)) {
560             log.error("取得できませんでした. サーバが混みあっている可能性があります: " + videoID + ":" + fileName);
561             response.getEntity().consumeContent();
562             return new GetFlvResult(Status.GET_INFO, userName);
563         }
564         String ext = Util.getExtention(contentType);
565         if (!mp4ExtIsMp4) {
566             if (ext.equals("mp4")) {
567                 ext = "flv";
568             }
569         }
570
571         BufferedInputStream in = new BufferedInputStream(response.getEntity().getContent());
572
573         File file = new File(fileName + "." + ext);
574 //        int postfix = 0;
575 //        while (file.isFile()) {
576 //            postfix++;
577 //            file = new File(fileName + "(" + postfix + ")" + "." + ext);
578 //        }
579         log.info("保存します: " + file.getPath());
580         FileOutputStream fos = new FileOutputStream(file);
581         BufferedOutputStream out = new BufferedOutputStream(fos);
582
583         int i;
584         while ((i = in.read(buffer)) != -1) {
585             out.write(buffer, 0, i);
586         }
587
588         response.getEntity().consumeContent();
589         out.close();
590         in.close();
591         if (url.toString().contains("low")) {
592             return new GetFlvResult(Status.GET_LOW, userName);
593         }
594         return new GetFlvResult(Status.GET_FILE, userName);
595     }
596
597     /**
598      * ニコニコ動画から動画ファイルをダウンロードする.
599      * @param videoID smxxxx形式のビデオID.
600      * @param fileName ダウンロード後のファイル名. 拡張子は別途付与されるため不要.
601      * @return この処理を行った後の, 対象ファイルのステータス.
602      * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
603      */
604     public GetFlvResult getFlvFile(String videoID, String fileName) throws IOException, URISyntaxException,
605             HttpException,
606             InterruptedException {
607         return getFlvFile(videoID, fileName, Status.GET_INFO, true);
608     }
609
610     /**
611      * ニコニコ動画から動画ファイルをダウンロードする.
612      * ファイル名はビデオID名となる.
613      * @param videoID smxxxx形式のビデオID.
614      * @return この処理を行った後の, 対象ファイルのステータス.
615      * @throws java.io.IOException ファイル取得失敗. 権限の無いファイルを取得しようとした場合も.
616      */
617     public GetFlvResult getFlvFile(String videoID) throws IOException, URISyntaxException, HttpException,
618             InterruptedException {
619         return getFlvFile(videoID, videoID, Status.GET_INFO, true);
620     }
621 }