543 lines
19 KiB
Java
543 lines
19 KiB
Java
/*
|
|
* Copyright (C) 2015-2018 Sébastiaan (github.com/se-bastiaan)
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.github.se_bastiaan.torrentstream;
|
|
|
|
import android.net.Uri;
|
|
import android.os.Handler;
|
|
import android.os.HandlerThread;
|
|
|
|
import com.frostwire.jlibtorrent.Priority;
|
|
import com.frostwire.jlibtorrent.SessionManager;
|
|
import com.frostwire.jlibtorrent.SessionParams;
|
|
import com.frostwire.jlibtorrent.SettingsPack;
|
|
import com.frostwire.jlibtorrent.TorrentHandle;
|
|
import com.frostwire.jlibtorrent.TorrentInfo;
|
|
import com.frostwire.jlibtorrent.alerts.AddTorrentAlert;
|
|
import com.frostwire.jlibtorrent.swig.settings_pack;
|
|
import com.github.se_bastiaan.torrentstream.exceptions.DirectoryModifyException;
|
|
import com.github.se_bastiaan.torrentstream.exceptions.NotInitializedException;
|
|
import com.github.se_bastiaan.torrentstream.exceptions.TorrentInfoException;
|
|
import com.github.se_bastiaan.torrentstream.listeners.DHTStatsAlertListener;
|
|
import com.github.se_bastiaan.torrentstream.listeners.TorrentAddedAlertListener;
|
|
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener;
|
|
import com.github.se_bastiaan.torrentstream.utils.FileUtils;
|
|
import com.github.se_bastiaan.torrentstream.utils.ThreadUtils;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.URL;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.concurrent.CountDownLatch;
|
|
|
|
public final class TorrentStream {
|
|
|
|
private static final String LIBTORRENT_THREAD_NAME = "TORRENTSTREAM_LIBTORRENT", STREAMING_THREAD_NAME = "TORRENTSTREAMER_STREAMING";
|
|
private static TorrentStream sThis;
|
|
private final List<TorrentListener> listeners = new ArrayList<>();
|
|
private CountDownLatch initialisingLatch;
|
|
private SessionManager torrentSession;
|
|
private Boolean initialising = false, initialised = false, isStreaming = false, isCanceled = false;
|
|
private TorrentOptions torrentOptions;
|
|
private Torrent currentTorrent;
|
|
private final TorrentAddedAlertListener torrentAddedAlertListener = new TorrentAddedAlertListener() {
|
|
@Override
|
|
public void torrentAdded(AddTorrentAlert alert) {
|
|
InternalTorrentListener listener = new InternalTorrentListener();
|
|
TorrentHandle th = torrentSession.find(alert.handle().infoHash());
|
|
currentTorrent = new Torrent(th, listener, torrentOptions.prepareSize);
|
|
|
|
torrentSession.addListener(currentTorrent);
|
|
}
|
|
};
|
|
private String currentTorrentUrl;
|
|
private Integer dhtNodes = 0;
|
|
private final DHTStatsAlertListener dhtStatsAlertListener = new DHTStatsAlertListener() {
|
|
@Override
|
|
public void stats(int totalDhtNodes) {
|
|
dhtNodes = totalDhtNodes;
|
|
}
|
|
};
|
|
private HandlerThread libTorrentThread, streamingThread;
|
|
private Handler libTorrentHandler, streamingHandler;
|
|
|
|
private TorrentStream(TorrentOptions options) {
|
|
torrentOptions = options;
|
|
initialise();
|
|
}
|
|
|
|
public static TorrentStream init(TorrentOptions options) {
|
|
sThis = new TorrentStream(options);
|
|
return sThis;
|
|
}
|
|
|
|
public static TorrentStream getInstance() throws NotInitializedException {
|
|
if (sThis == null)
|
|
throw new NotInitializedException();
|
|
|
|
return sThis;
|
|
}
|
|
|
|
/**
|
|
* Obtain internal session manager
|
|
*
|
|
* @return {@link SessionManager}
|
|
*/
|
|
public SessionManager getSessionManager() {
|
|
return torrentSession;
|
|
}
|
|
|
|
private void initialise() {
|
|
if (libTorrentThread != null && torrentSession != null) {
|
|
resumeSession();
|
|
} else {
|
|
if ((initialising || initialised) && libTorrentThread != null) {
|
|
libTorrentThread.interrupt();
|
|
}
|
|
|
|
initialising = true;
|
|
initialised = false;
|
|
initialisingLatch = new CountDownLatch(1);
|
|
|
|
libTorrentThread = new HandlerThread(LIBTORRENT_THREAD_NAME);
|
|
libTorrentThread.start();
|
|
libTorrentHandler = new Handler(libTorrentThread.getLooper());
|
|
libTorrentHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
torrentSession = new SessionManager();
|
|
setOptions(torrentOptions);
|
|
|
|
torrentSession.addListener(dhtStatsAlertListener);
|
|
torrentSession.startDht();
|
|
|
|
initialising = false;
|
|
initialised = true;
|
|
initialisingLatch.countDown();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resume TorrentSession
|
|
*/
|
|
public void resumeSession() {
|
|
if (libTorrentThread != null && torrentSession != null) {
|
|
libTorrentHandler.removeCallbacksAndMessages(null);
|
|
|
|
//resume torrent session if needed
|
|
if (torrentSession.isPaused()) {
|
|
libTorrentHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
torrentSession.resume();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (!torrentSession.isDhtRunning()) {
|
|
libTorrentHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
torrentSession.startDht();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pause TorrentSession
|
|
*/
|
|
public void pauseSession() {
|
|
if (!isStreaming)
|
|
libTorrentHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
torrentSession.pause();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get torrent metadata, either by downloading the .torrent or fetching the magnet
|
|
*
|
|
* @param torrentUrl {@link String} URL to .torrent or magnet link
|
|
* @return {@link TorrentInfo}
|
|
*/
|
|
private TorrentInfo getTorrentInfo(String torrentUrl) throws TorrentInfoException {
|
|
if (torrentUrl.startsWith("magnet")) {
|
|
byte[] data = torrentSession.fetchMagnet(torrentUrl, 30);
|
|
if (data != null)
|
|
try {
|
|
return TorrentInfo.bdecode(data);
|
|
} catch (IllegalArgumentException e) {
|
|
throw new TorrentInfoException(e);
|
|
}
|
|
|
|
} else if (torrentUrl.startsWith("http") || torrentUrl.startsWith("https")) {
|
|
try {
|
|
URL url = new URL(torrentUrl);
|
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
|
|
connection.setRequestMethod("GET");
|
|
connection.setInstanceFollowRedirects(true);
|
|
connection.connect();
|
|
|
|
InputStream inputStream = connection.getInputStream();
|
|
|
|
byte[] responseByteArray = new byte[0];
|
|
|
|
if (connection.getResponseCode() == 200) {
|
|
responseByteArray = getBytesFromInputStream(inputStream);
|
|
}
|
|
|
|
inputStream.close();
|
|
connection.disconnect();
|
|
|
|
if (responseByteArray.length > 0) {
|
|
return TorrentInfo.bdecode(responseByteArray);
|
|
}
|
|
} catch (IOException | IllegalArgumentException e) {
|
|
throw new TorrentInfoException(e);
|
|
}
|
|
} else if (torrentUrl.startsWith("file")) {
|
|
Uri path = Uri.parse(torrentUrl);
|
|
File file = new File(path.getPath());
|
|
|
|
try {
|
|
FileInputStream fileInputStream = new FileInputStream(file);
|
|
byte[] responseByteArray = getBytesFromInputStream(fileInputStream);
|
|
fileInputStream.close();
|
|
|
|
if (responseByteArray.length > 0) {
|
|
return TorrentInfo.bdecode(responseByteArray);
|
|
}
|
|
} catch (IOException | IllegalArgumentException e) {
|
|
throw new TorrentInfoException(e);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private byte[] getBytesFromInputStream(InputStream inputStream) throws IOException {
|
|
ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
|
|
|
|
int bufferSize = 1024;
|
|
byte[] buffer = new byte[bufferSize];
|
|
|
|
int len = 0;
|
|
while ((len = inputStream.read(buffer)) != -1) {
|
|
byteBuffer.write(buffer, 0, len);
|
|
}
|
|
|
|
return byteBuffer.toByteArray();
|
|
}
|
|
|
|
/**
|
|
* Start stream download for specified torrent
|
|
*
|
|
* @param torrentUrl {@link String} .torrent or magnet link
|
|
*/
|
|
public void startStream(final String torrentUrl) {
|
|
if (!initialising && !initialised)
|
|
initialise();
|
|
|
|
if (libTorrentHandler == null || isStreaming) return;
|
|
|
|
isCanceled = false;
|
|
|
|
streamingThread = new HandlerThread(STREAMING_THREAD_NAME);
|
|
streamingThread.start();
|
|
streamingHandler = new Handler(streamingThread.getLooper());
|
|
|
|
streamingHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
isStreaming = true;
|
|
|
|
if (initialisingLatch != null) {
|
|
try {
|
|
initialisingLatch.await();
|
|
initialisingLatch = null;
|
|
} catch (InterruptedException e) {
|
|
isStreaming = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
currentTorrentUrl = torrentUrl;
|
|
|
|
File saveDirectory = new File(torrentOptions.saveLocation);
|
|
if (!saveDirectory.isDirectory() && !saveDirectory.mkdirs()) {
|
|
for (final TorrentListener listener : listeners) {
|
|
ThreadUtils.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
listener.onStreamError(null, new DirectoryModifyException());
|
|
}
|
|
});
|
|
}
|
|
isStreaming = false;
|
|
return;
|
|
}
|
|
|
|
torrentSession.removeListener(torrentAddedAlertListener);
|
|
TorrentInfo torrentInfo = null;
|
|
try {
|
|
torrentInfo = getTorrentInfo(torrentUrl);
|
|
} catch (final TorrentInfoException e) {
|
|
for (final TorrentListener listener : listeners) {
|
|
ThreadUtils.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
listener.onStreamError(null, e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
torrentSession.addListener(torrentAddedAlertListener);
|
|
|
|
if (torrentInfo == null) {
|
|
for (final TorrentListener listener : listeners) {
|
|
ThreadUtils.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
listener.onStreamError(null, new TorrentInfoException(null));
|
|
}
|
|
});
|
|
}
|
|
isStreaming = false;
|
|
return;
|
|
}
|
|
|
|
Priority[] priorities = new Priority[torrentInfo.numFiles()];
|
|
for (int i = 0; i < priorities.length; i++) {
|
|
priorities[i] = Priority.IGNORE;
|
|
}
|
|
|
|
if (!currentTorrentUrl.equals(torrentUrl) || isCanceled) {
|
|
return;
|
|
}
|
|
|
|
torrentSession.download(torrentInfo, saveDirectory, null, priorities, null);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stop current torrent stream
|
|
*/
|
|
public void stopStream() {
|
|
//remove all callbacks from handler
|
|
if (libTorrentHandler != null)
|
|
libTorrentHandler.removeCallbacksAndMessages(null);
|
|
if (streamingHandler != null)
|
|
streamingHandler.removeCallbacksAndMessages(null);
|
|
|
|
isCanceled = true;
|
|
isStreaming = false;
|
|
if (currentTorrent != null) {
|
|
final File saveLocation = currentTorrent.getSaveLocation();
|
|
|
|
currentTorrent.pause();
|
|
torrentSession.removeListener(currentTorrent);
|
|
torrentSession.remove(currentTorrent.getTorrentHandle());
|
|
currentTorrent = null;
|
|
|
|
if (torrentOptions.removeFiles) {
|
|
new Thread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
int tries = 0;
|
|
while (!FileUtils.recursiveDelete(saveLocation) && tries < 5) {
|
|
tries++;
|
|
try {
|
|
Thread.sleep(1000); // If deleted failed then something is still using the file, wait and then retry
|
|
} catch (InterruptedException e) {
|
|
for (final TorrentListener listener : listeners) {
|
|
ThreadUtils.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
listener.onStreamError(currentTorrent, new DirectoryModifyException());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}).start();
|
|
}
|
|
}
|
|
|
|
if (streamingThread != null)
|
|
streamingThread.interrupt();
|
|
|
|
for (final TorrentListener listener : listeners) {
|
|
ThreadUtils.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
listener.onStreamStopped();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public TorrentOptions getOptions() {
|
|
return torrentOptions;
|
|
}
|
|
|
|
public void setOptions(TorrentOptions options) {
|
|
torrentOptions = options;
|
|
|
|
SettingsPack settingsPack = new SettingsPack()
|
|
.anonymousMode(torrentOptions.anonymousMode)
|
|
.connectionsLimit(torrentOptions.maxConnections)
|
|
.downloadRateLimit(torrentOptions.maxDownloadSpeed)
|
|
.uploadRateLimit(torrentOptions.maxUploadSpeed)
|
|
.activeDhtLimit(torrentOptions.maxDht);
|
|
|
|
if (torrentOptions.listeningPort != -1) {
|
|
String ifStr = String.format(Locale.ENGLISH, "%s:%d", "0.0.0.0", torrentOptions.listeningPort);
|
|
settingsPack.setString(settings_pack.string_types.listen_interfaces.swigValue(), ifStr);
|
|
}
|
|
|
|
if (torrentOptions.proxyHost != null) {
|
|
settingsPack.setString(settings_pack.string_types.proxy_hostname.swigValue(), torrentOptions.proxyHost);
|
|
if (torrentOptions.proxyUsername != null) {
|
|
settingsPack.setString(settings_pack.string_types.proxy_username.swigValue(), torrentOptions.proxyUsername);
|
|
if (torrentOptions.proxyPassword != null) {
|
|
settingsPack.setString(settings_pack.string_types.proxy_password.swigValue(), torrentOptions.proxyPassword);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (torrentOptions.peerFingerprint != null) {
|
|
settingsPack.setString(settings_pack.string_types.peer_fingerprint.swigValue(), torrentOptions.peerFingerprint);
|
|
}
|
|
|
|
if (!torrentSession.isRunning()) {
|
|
SessionParams sessionParams = new SessionParams(settingsPack);
|
|
torrentSession.start(sessionParams);
|
|
} else {
|
|
torrentSession.applySettings(settingsPack);
|
|
}
|
|
}
|
|
|
|
public boolean isStreaming() {
|
|
return isStreaming;
|
|
}
|
|
|
|
public String getCurrentTorrentUrl() {
|
|
return currentTorrentUrl;
|
|
}
|
|
|
|
public Integer getTotalDhtNodes() {
|
|
return dhtNodes;
|
|
}
|
|
|
|
public Torrent getCurrentTorrent() {
|
|
return currentTorrent;
|
|
}
|
|
|
|
public void addListener(TorrentListener listener) {
|
|
if (listener != null)
|
|
listeners.add(listener);
|
|
}
|
|
|
|
public void removeListener(TorrentListener listener) {
|
|
if (listener != null)
|
|
listeners.remove(listener);
|
|
}
|
|
|
|
protected class InternalTorrentListener implements TorrentListener {
|
|
|
|
public void onStreamStarted(final Torrent torrent) {
|
|
for (final TorrentListener listener : listeners) {
|
|
ThreadUtils.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
listener.onStreamStarted(torrent);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public void onStreamError(final Torrent torrent, final Exception e) {
|
|
for (final TorrentListener listener : listeners) {
|
|
ThreadUtils.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
listener.onStreamError(torrent, e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public void onStreamReady(final Torrent torrent) {
|
|
for (final TorrentListener listener : listeners) {
|
|
ThreadUtils.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
listener.onStreamReady(torrent);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public void onStreamProgress(final Torrent torrent, final StreamStatus status) {
|
|
for (final TorrentListener listener : listeners) {
|
|
ThreadUtils.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
listener.onStreamProgress(torrent, status);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onStreamStopped() {
|
|
// Not used
|
|
}
|
|
|
|
@Override
|
|
public void onStreamPrepared(final Torrent torrent) {
|
|
if (torrentOptions.autoDownload) {
|
|
torrent.startDownload();
|
|
}
|
|
|
|
for (final TorrentListener listener : listeners) {
|
|
ThreadUtils.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
listener.onStreamPrepared(torrent);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|