OSDN Git Service

ニコニコ動画サービスへの連続アクセス間隔をあける
[coroid/inqubus.git] / frontend / src / saccubus / worker / impl / download / Download.java
1 package saccubus.worker.impl.download;
2
3 import static saccubus.worker.impl.download.DownloadStatus.*;
4
5 import java.io.File;
6 import java.io.IOException;
7 import java.net.URISyntaxException;
8 import java.util.Date;
9 import java.util.logging.Logger;
10 import nicobrowser.GetFlvResult;
11 import nicobrowser.NamePattern;
12 import nicobrowser.NicoHttpClient;
13 import nicobrowser.ProgressListener;
14 import nicobrowser.WayBackInfo;
15 import nicobrowser.entity.NicoContent.Status;
16 import org.apache.http.HttpException;
17 import saccubus.worker.Worker;
18 import saccubus.worker.WorkerListener;
19 import saccubus.worker.profile.CommentProfile;
20 import saccubus.worker.profile.GeneralProfile;
21 import saccubus.worker.profile.DownloadProfile;
22 import saccubus.worker.profile.ProxyProfile;
23
24 /**
25  * <p>タイトル: さきゅばす</p>
26  *
27  * <p>説明: ニコニコ動画の動画をコメントつきで保存</p>
28  *
29  * <p>著作権: Copyright (c) 2007 PSI</p>
30  *
31  * <p>会社名: </p>
32  *
33  * @author 未入力
34  * @version 1.0
35  */
36 public class Download extends Worker<DownloadResult, DownloadProgress> {
37
38     private static final Logger logger = Logger.getLogger(Download.class.getName());
39     private static final Object timeLockObj = new Object();
40     private static volatile long lastStartTime = 0L;
41     private final DownloadProfile profile;
42     private final String videoId;
43
44     public Download(DownloadProfile profile, String videoId) {
45         this(profile, videoId, null);
46     }
47
48     /**
49      * コンバータを構築します.
50      * @param videoId 対象となる動画のID.
51      * @param time
52      * @param profile
53      * @param listener
54      * @param flag
55      */
56     public Download(DownloadProfile profile, String videoId, WorkerListener<DownloadResult, DownloadProgress> listener) {
57         // TODO listener登録
58         super(listener);
59         this.videoId = videoId;
60         this.profile = profile;
61     }
62
63 //    @Override
64 //    public Boolean call() throws Exception {
65 //        try {
66 //            final DownloadResult result = doInBackground();
67 //            return Boolean.valueOf(result.getResultValue());
68 //        } finally {
69 //            // TODO 何か処理が必要?
70 ////            getStopFlag().finished();
71 //        }
72 //    }
73 //    // TODO Runnableを実装しなくなったので削除する
74 //    public void run() {
75 //        try {
76 //            call();
77 //        } catch (Exception ex) {
78 //            String text = (ex.getMessage() != null) ? ex.getMessage() : "予期しないエラー発生のため中断しました。";
79 //            sendText(text);
80 //            logger.log(Level.SEVERE, null, ex);
81 //        }
82 //    }
83     @Override
84     public DownloadResult work() throws Exception {
85         waitAndGo();
86
87         publish(new DownloadProgress(PROCESS, 0.0, "ログイン中"));
88
89         NicoHttpClient client = null;
90         nicobrowser.VideoInfo vi = null;
91         NamePattern videoNamePattern = null;
92         WayBackInfo wbi = null;
93         if (needsLogin()) {
94             client = createClientAndLogin();
95             vi = client.getVideoInfo(videoId);
96
97             final String name = profile.getVideoProfile().getFileName();
98             final String replaceFrom = profile.getGeneralProfile().getReplaceFrom();
99             final String replaceTo = profile.getGeneralProfile().getReplaceTo();
100             videoNamePattern = new NamePattern(name, replaceFrom, replaceTo, vi.getTitleInWatchPage());
101
102             if (needsBackLog()) {
103                 final String key = client.getWayBackKey(vi);
104                 wbi = new WayBackInfo(key, profile.getCommentProfile().getBackLogPoint());
105             }
106         }
107
108         checkStop();
109
110         File commentFile;
111         if (profile.getCommentProfile().isDownload()) {
112             final CommentProfile prof = profile.getCommentProfile();
113             final GeneralProfile gene = profile.getGeneralProfile();
114
115             final NamePattern pattern = new NamePattern(prof.getFileName(), gene.getReplaceFrom(), gene.getReplaceTo(),
116                     vi.getTitleInWatchPage());
117             // TODO コメントファイルに{low}は使えないことをどこかに書くべきか
118             final String name = pattern.createFileName(videoId, true);
119             final File file = new File(profile.getCommentProfile().getDir(), name);
120
121             commentFile = client.getCommentFile(vi, file.getPath(), wbi, profile.getCommentProfile().
122                     getLengthRelatedCommentSize(),
123                     profile.getCommentProfile().isDisablePerMinComment());
124         } else {
125             commentFile = profile.getCommentProfile().getLocalFile();
126         }
127
128         checkStop();
129
130         File videoFile;
131         GetFlvResult vf;
132         if (profile.getVideoProfile().isDownload()) {
133             vf = client.getFlvFile(vi, profile.getVideoProfile().getDir(), videoNamePattern,
134                     Status.GET_INFO, true, new ProgressListener() {
135
136                 @Override
137                 public void progress(long fileSize, long downloadSize) {
138                     final double vol = (double) downloadSize / (double) fileSize * 100.0;
139                     publish(new DownloadProgress(PROCESS, vol, String.format("ダウンロード%.2f%%", vol)));
140                 }
141             });
142
143             videoFile = vf.getFile();
144         } else {
145             videoFile = profile.getVideoProfile().getLocalFile();
146         }
147         return new DownloadResult(true, videoFile, commentFile);
148
149
150         // TODO FFMPEG 実行開始は別タスクとして実装する.
151 //        if (!profile.getOutputFileSetting().isConvert()) {
152 //            publish(new DownloadProgress("動画・コメントを保存し、変換は行いませんでした。"));
153 //            return new DownloadResult(true);
154 //        }
155 //
156 //        if (!videoFile.isFile()) {
157 //            throw new IOException("入力動画ファイルが存在しません:" + videoFile.getPath());
158 //        }
159 //
160 //        if (profile.getOutputFileSetting().isAddComment()) {
161 //            if (!commentFile.isFile()) {
162 //                throw new IOException("入力コメントファイルが存在しません:" + commentFile.getPath());
163 //            }
164 //        } else {
165 //            commentFile = null;
166 //        }
167 //
168 //        /*ビデオ名の確定*/
169 //        final boolean isNotLow = (vf == null) ? true : (vf.getStatus() != Status.GET_LOW);
170 //        File convertedVideoFile = getOutputFileName(vi.getTitleInWatchPage(), isNotLow);
171 //
172
173 //        boolean res = new FfmpegCommand(getListener(), getStopFlag(), commentFile, videoFile,
174 //                convertedVideoFile, profile.getFfmpeg(), profile.getGeneralSetting()).execute();
175 //        return res;
176     }
177
178     /** @return 何かダウンロードするものがあればtrue. */
179     private static boolean needsDownload(DownloadProfile profile) {
180         return (profile.getVideoProfile().isDownload() || profile.getCommentProfile().isDownload());
181     }
182
183     // TODO どこかに処理を移す必要がある.
184     /**
185      * (ネットワーク設定以外の)設定を検証する.
186      * @throws IllegalArgumentException 設定に不備がある場合.
187      */
188 //    private void validSetting() {
189 //        if (profile.getOutputFileSetting().isConvert()) {
190 //            File a = profile.getFfmpeg().getFfmpeg();
191 //            if (!a.canRead()) {
192 //                throw new IllegalArgumentException("FFmpegが見つかりません。");
193 //            }
194 //            if (profile.getFfmpeg().getVhook().getPath().indexOf(' ') >= 0) {
195 //                throw new IllegalArgumentException("すいません。現在vhookライブラリには半角空白は使えません。");
196 //            }
197 //            a = profile.getFfmpeg().getVhook();
198 //            if (!a.canRead()) {
199 //                throw new IllegalArgumentException("Vhookライブラリが見つかりません。");
200 //            }
201 //            a = profile.getFfmpeg().getFont();
202 //            if (!a.canRead()) {
203 //                throw new IllegalArgumentException("フォントが見つかりません。");
204 //            }
205 //        }
206 //    }
207     /**
208      * HttpClientを生成し, ニコニコ動画サーバへログインします.
209      * @return 生成したHttpClientインスタンス.
210      * @throws IOException ログイン失敗.
211      * @throws InterruptedException ログイン失敗.
212      */
213     // TODO HttpException を投げるのをやめたい. コンパイル時にHttpComponentが必要になるので.
214     private NicoHttpClient createClientAndLogin() throws IOException, InterruptedException, HttpException {
215         final NicoHttpClient client = createClient(profile.getProxyProfile());
216         final boolean hasLogin;
217         try {
218             hasLogin = client.login(profile.getLoginInfo().getMail(), profile.getLoginInfo().getPassword());
219             if (!hasLogin) {
220                 throw new IOException("login fail");
221             }
222         } catch (URISyntaxException ex) {
223             throw new IOException("login fail", ex);
224         }
225         return client;
226     }
227
228     private NicoHttpClient createClient(ProxyProfile proxy) {
229         if (proxy.use()) {
230             return new NicoHttpClient(proxy.getHost(), proxy.getPort());
231         } else {
232             return new NicoHttpClient();
233         }
234     }
235
236     /** @return ログインする必要があればtrue. */
237     private boolean needsLogin() {
238         return profile.getVideoProfile().isDownload() || profile.getCommentProfile().isDownload();
239     }
240
241     /** @return 過去ログ取得の必要があればtrue. */
242     private boolean needsBackLog() {
243         return profile.getCommentProfile().getBackLogPoint() >= 0L;
244     }
245
246     private void checkStop() throws InterruptedException {
247         if (Thread.interrupted()) {
248             throw new InterruptedException("中止要求を受け付けました");
249         }
250     }
251
252     /**
253      * ニコニコ動画サービスに連続アクセスするとはじかれるため, 前回のアクセス開始時刻から一定期間は
254      * 次のアクセスに行かないよう待機する必要があります.
255      * @throws InterruptedException 中断されました.
256      */
257     private void waitAndGo() throws InterruptedException {
258         synchronized (timeLockObj) {
259             final long now = new Date().getTime();
260             // TODO 30秒間隔はコンフィグ設定できた方が良い
261             final long needSleep = (30 * 1000L) - (now - lastStartTime);
262             if(needSleep > 0L) {
263                 publish(new DownloadProgress(DownloadStatus.PROCESS, 0.0, "過剰アクセス抑制待機 " + needSleep / 1000));
264                 Thread.sleep(needSleep);
265             }
266             lastStartTime = new Date().getTime();
267         }
268     }
269 }