313 lines
9.8 KiB
Java
313 lines
9.8 KiB
Java
package us.shandian.giga.get;
|
|
|
|
import android.util.Log;
|
|
|
|
import org.schabi.newpipe.extractor.NewPipe;
|
|
import org.schabi.newpipe.extractor.StreamingService;
|
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
|
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InterruptedIOException;
|
|
import java.net.HttpURLConnection;
|
|
import java.nio.channels.ClosedByInterruptException;
|
|
import java.util.List;
|
|
|
|
import us.shandian.giga.get.DownloadMission.HttpError;
|
|
|
|
import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
|
|
|
|
public class DownloadMissionRecover extends Thread {
|
|
private static final String TAG = "DownloadMissionRecover";
|
|
static final int mID = -3;
|
|
|
|
private final DownloadMission mMission;
|
|
private final boolean mNotInitialized;
|
|
|
|
private final int mErrCode;
|
|
|
|
private HttpURLConnection mConn;
|
|
private MissionRecoveryInfo mRecovery;
|
|
private StreamExtractor mExtractor;
|
|
|
|
DownloadMissionRecover(DownloadMission mission, int errCode) {
|
|
mMission = mission;
|
|
mNotInitialized = mission.blocks == null && mission.current == 0;
|
|
mErrCode = errCode;
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
if (mMission.source == null) {
|
|
mMission.notifyError(mErrCode, null);
|
|
return;
|
|
}
|
|
|
|
Exception err = null;
|
|
int attempt = 0;
|
|
|
|
while (attempt++ < mMission.maxRetry) {
|
|
try {
|
|
tryRecover();
|
|
return;
|
|
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
|
return;
|
|
} catch (Exception e) {
|
|
if (!mMission.running || super.isInterrupted()) return;
|
|
err = e;
|
|
}
|
|
}
|
|
|
|
// give up
|
|
mMission.notifyError(mErrCode, err);
|
|
}
|
|
|
|
private void tryRecover() throws ExtractionException, IOException, HttpError {
|
|
if (mExtractor == null) {
|
|
try {
|
|
StreamingService svr = NewPipe.getServiceByUrl(mMission.source);
|
|
mExtractor = svr.getStreamExtractor(mMission.source);
|
|
mExtractor.fetchPage();
|
|
} catch (ExtractionException e) {
|
|
mExtractor = null;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// maybe the following check is redundant
|
|
if (!mMission.running || super.isInterrupted()) return;
|
|
|
|
if (!mNotInitialized) {
|
|
// set the current download url to null in case if the recovery
|
|
// process is canceled. Next time start() method is called the
|
|
// recovery will be executed, saving time
|
|
mMission.urls[mMission.current] = null;
|
|
|
|
mRecovery = mMission.recoveryInfo[mMission.current];
|
|
resolveStream();
|
|
return;
|
|
}
|
|
|
|
Log.w(TAG, "mission is not fully initialized, this will take a while");
|
|
|
|
try {
|
|
for (; mMission.current < mMission.urls.length; mMission.current++) {
|
|
mRecovery = mMission.recoveryInfo[mMission.current];
|
|
|
|
if (test()) continue;
|
|
if (!mMission.running) return;
|
|
|
|
resolveStream();
|
|
if (!mMission.running) return;
|
|
|
|
// before continue, check if the current stream was resolved
|
|
if (mMission.urls[mMission.current] == null) {
|
|
break;
|
|
}
|
|
}
|
|
} finally {
|
|
mMission.current = 0;
|
|
}
|
|
|
|
mMission.writeThisToFile();
|
|
|
|
if (!mMission.running || super.isInterrupted()) return;
|
|
|
|
mMission.running = false;
|
|
mMission.start();
|
|
}
|
|
|
|
private void resolveStream() throws IOException, ExtractionException, HttpError {
|
|
// FIXME: this getErrorMessage() always returns "video is unavailable"
|
|
/*if (mExtractor.getErrorMessage() != null) {
|
|
mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage()));
|
|
return;
|
|
}*/
|
|
|
|
String url = null;
|
|
|
|
switch (mRecovery.getKind()) {
|
|
case 'a':
|
|
for (AudioStream audio : mExtractor.getAudioStreams()) {
|
|
if (audio.average_bitrate == mRecovery.getDesiredBitrate() && audio.getFormat() == mRecovery.getFormat()) {
|
|
url = audio.getUrl();
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case 'v':
|
|
List<VideoStream> videoStreams;
|
|
if (mRecovery.isDesired2())
|
|
videoStreams = mExtractor.getVideoOnlyStreams();
|
|
else
|
|
videoStreams = mExtractor.getVideoStreams();
|
|
for (VideoStream video : videoStreams) {
|
|
if (video.resolution.equals(mRecovery.getDesired()) && video.getFormat() == mRecovery.getFormat()) {
|
|
url = video.getUrl();
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case 's':
|
|
for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.getFormat())) {
|
|
String tag = subtitles.getLanguageTag();
|
|
if (tag.equals(mRecovery.getDesired()) && subtitles.isAutoGenerated() == mRecovery.isDesired2()) {
|
|
url = subtitles.getUrl();
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
throw new RuntimeException("Unknown stream type");
|
|
}
|
|
|
|
resolve(url);
|
|
}
|
|
|
|
private void resolve(String url) throws IOException, HttpError {
|
|
if (mRecovery.getValidateCondition() == null) {
|
|
Log.w(TAG, "validation condition not defined, the resource can be stale");
|
|
}
|
|
|
|
if (mMission.unknownLength || mRecovery.getValidateCondition() == null) {
|
|
recover(url, false);
|
|
return;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
////// Validate the http resource doing a range request
|
|
/////////////////////
|
|
try {
|
|
mConn = mMission.openConnection(url, true, mMission.length - 10, mMission.length);
|
|
mConn.setRequestProperty("If-Range", mRecovery.getValidateCondition());
|
|
mMission.establishConnection(mID, mConn);
|
|
|
|
int code = mConn.getResponseCode();
|
|
|
|
switch (code) {
|
|
case 200:
|
|
case 413:
|
|
// stale
|
|
recover(url, true);
|
|
return;
|
|
case 206:
|
|
// in case of validation using the Last-Modified date, check the resource length
|
|
long[] contentRange = parseContentRange(mConn.getHeaderField("Content-Range"));
|
|
boolean lengthMismatch = contentRange[2] != -1 && contentRange[2] != mMission.length;
|
|
|
|
recover(url, lengthMismatch);
|
|
return;
|
|
}
|
|
|
|
throw new HttpError(code);
|
|
} finally {
|
|
disconnect();
|
|
}
|
|
}
|
|
|
|
private void recover(String url, boolean stale) {
|
|
Log.i(TAG,
|
|
String.format("recover() name=%s isStale=%s url=%s", mMission.storage.getName(), stale, url)
|
|
);
|
|
|
|
mMission.urls[mMission.current] = url;
|
|
|
|
if (url == null) {
|
|
mMission.urls = new String[0];
|
|
mMission.notifyError(ERROR_RESOURCE_GONE, null);
|
|
return;
|
|
}
|
|
|
|
if (mNotInitialized) return;
|
|
|
|
if (stale) {
|
|
mMission.resetState(false, false, DownloadMission.ERROR_NOTHING);
|
|
}
|
|
|
|
mMission.writeThisToFile();
|
|
|
|
if (!mMission.running || super.isInterrupted()) return;
|
|
|
|
mMission.running = false;
|
|
mMission.start();
|
|
}
|
|
|
|
private long[] parseContentRange(String value) {
|
|
long[] range = new long[3];
|
|
|
|
if (value == null) {
|
|
// this never should happen
|
|
return range;
|
|
}
|
|
|
|
try {
|
|
value = value.trim();
|
|
|
|
if (!value.startsWith("bytes")) {
|
|
return range;// unknown range type
|
|
}
|
|
|
|
int space = value.lastIndexOf(' ') + 1;
|
|
int dash = value.indexOf('-', space) + 1;
|
|
int bar = value.indexOf('/', dash);
|
|
|
|
// start
|
|
range[0] = Long.parseLong(value.substring(space, dash - 1));
|
|
|
|
// end
|
|
range[1] = Long.parseLong(value.substring(dash, bar));
|
|
|
|
// resource length
|
|
value = value.substring(bar + 1);
|
|
if (value.equals("*")) {
|
|
range[2] = -1;// unknown length received from the server but should be valid
|
|
} else {
|
|
range[2] = Long.parseLong(value);
|
|
}
|
|
} catch (Exception e) {
|
|
// nothing to do
|
|
}
|
|
|
|
return range;
|
|
}
|
|
|
|
private boolean test() {
|
|
if (mMission.urls[mMission.current] == null) return false;
|
|
|
|
try {
|
|
mConn = mMission.openConnection(mMission.urls[mMission.current], true, -1, -1);
|
|
mMission.establishConnection(mID, mConn);
|
|
|
|
if (mConn.getResponseCode() == 200) return true;
|
|
} catch (Exception e) {
|
|
// nothing to do
|
|
} finally {
|
|
disconnect();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void disconnect() {
|
|
try {
|
|
try {
|
|
mConn.getInputStream().close();
|
|
} finally {
|
|
mConn.disconnect();
|
|
}
|
|
} catch (Exception e) {
|
|
// nothing to do
|
|
} finally {
|
|
mConn = null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void interrupt() {
|
|
super.interrupt();
|
|
if (mConn != null) disconnect();
|
|
}
|
|
}
|