package com.frostwire.bittorrent;
import com.frostwire.jlibtorrent.*;
import com.frostwire.jlibtorrent.alerts.Alert;
import com.frostwire.jlibtorrent.alerts.AlertType;
import com.frostwire.jlibtorrent.alerts.TorrentAlert;
import com.frostwire.jlibtorrent.swig.entry;
import com.frostwire.logging.Logger;
import com.frostwire.search.torrent.TorrentCrawledSearchResult;
import com.frostwire.util.OSUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import static com.frostwire.jlibtorrent.alerts.AlertType.*;
/**
* @author gubatron
* @author aldenml
*/
public final class BTEngine {
private static final int[] INNER_LISTENER_TYPES = new int[]{TORRENT_ADDED.getSwig(),
PIECE_FINISHED.getSwig(),
PORTMAP.getSwig(),
PORTMAP_ERROR.getSwig()};
private static final Logger LOG = Logger.getLogger(BTEngine.class);
private static final String TORRENT_ORIG_PATH_KEY = "torrent_orig_path";
public static BTContext ctx;
private final ReentrantLock sync;
private final InnerListener innerListener;
private Session session;
private Downloader downloader;
private SessionSettings defaultSettings;
private boolean firewalled;
private BTEngineListener listener;
private BTEngine() {
this.sync = new ReentrantLock();
this.innerListener = new InnerListener();
}
private static class Loader {
static BTEngine INSTANCE = new BTEngine();
}
public static BTEngine getInstance() {
if (ctx == null) {
throw new IllegalStateException("Context can't be null");
}
return Loader.INSTANCE;
}
public Session getSession() {
return session;
}
private SessionSettings getSettings() {
if (session == null) {
return null;
}
return session.getSettings();
}
public BTEngineListener getListener() {
return listener;
}
public void setListener(BTEngineListener listener) {
this.listener = listener;
}
public boolean isFirewalled() {
return firewalled;
}
public long getDownloadRate() {
if (session == null) {
return 0;
}
return session.getStatus().getPayloadDownloadRate();
}
public long getUploadRate() {
if (session == null) {
return 0;
}
return session.getStatus().getPayloadUploadRate();
}
public long getTotalDownload() {
if (session == null) {
return 0;
}
return session.getStatus().getTotalDownload();
}
public long getTotalUpload() {
if (session == null) {
return 0;
}
return session.getStatus().getTotalUpload();
}
public int getDownloadRateLimit() {
if (session == null) {
return 0;
}
return session.getSettings().getDownloadRateLimit();
}
public int getUploadRateLimit() {
if (session == null) {
return 0;
}
return session.getSettings().getDownloadRateLimit();
}
public boolean isStarted() {
return session != null;
}
public boolean isPaused() {
return session != null && session.isPaused();
}
public void start() {
sync.lock();
try {
if (session != null) {
return;
}
Pair<Integer, Integer> prange = new Pair<Integer, Integer>(ctx.port0, ctx.port1);
session = new Session(prange, ctx.iface);
downloader = new Downloader(session);
defaultSettings = session.getSettings();
loadSettings();
session.addListener(innerListener);
fireStarted();
} finally {
sync.unlock();
}
}
/**
* Abort and destroy the internal libtorrent session.
*/
public void stop() {
sync.lock();
try {
if (session == null) {
return;
}
session.removeListener(innerListener);
saveSettings();
downloader = null;
defaultSettings = null;
session.abort();
session = null;
fireStopped();
} finally {
sync.unlock();
}
}
public void restart() {
sync.lock();
try {
stop();
Thread.sleep(1000); // allow some time to release native resources
start();
} catch (InterruptedException e) {
// ignore
} finally {
sync.unlock();
}
}
public void pause() {
if (session != null && !session.isPaused()) {
session.pause();
}
}
public void resume() {
if (session != null) {
session.resume();
}
}
public void loadSettings() {
if (session == null) {
return;
}
try {
File f = settingsFile();
if (f.exists()) {
byte[] data = FileUtils.readFileToByteArray(f);
session.loadState(data);
} else {
revertToDefaultConfiguration();
}
} catch (Throwable e) {
LOG.error("Error loading session state", e);
}
}
public void saveSettings() {
if (session == null) {
return;
}
try {
byte[] data = session.saveState();
FileUtils.writeByteArrayToFile(settingsFile(), data);
} catch (Throwable e) {
LOG.error("Error saving session state", e);
}
}
private void saveSettings(SessionSettings s) {
if (session == null) {
return;
}
session.setSettings(s);
saveSettings();
}
public void revertToDefaultConfiguration() {
if (session == null) {
return;
}
defaultSettings.broadcastLSD(true);
session.setSettings(defaultSettings);
SessionSettings s = session.getSettings(); // working with a copy?
if (ctx.optimizeMemory) {
int maxQueuedDiskBytes = s.getMaxQueuedDiskBytes();
s.setMaxQueuedDiskBytes(maxQueuedDiskBytes / 2);
int sendBufferWatermark = s.getSendBufferWatermark();
s.setSendBufferWatermark(sendBufferWatermark / 2);
s.setCacheSize(256);
s.setActiveDownloads(4);
s.setActiveSeeds(4);
s.setMaxPeerlistSize(200);
s.setUtpDynamicSockBuf(false);
s.setGuidedReadCache(true);
s.setTickInterval(1000);
s.setInactivityTimeout(60);
s.optimizeHashingForSpeed(false);
s.setSeedingOutgoingConnections(false);
s.setConnectionsLimit(200);
} else {
s.setActiveDownloads(10);
s.setActiveSeeds(10);
}
session.setSettings(s);
saveSettings();
}
public void download(File torrent, File saveDir) {
download(torrent, saveDir, null);
}
public void download(File torrent, File saveDir, boolean[] selection) {
if (session == null) {
return;
}
saveDir = setupSaveDir(saveDir);
if (saveDir == null) {
return;
}
TorrentInfo ti = new TorrentInfo(torrent);
Priority[] priorities = null;
TorrentHandle th = downloader.find(ti.getInfoHash());
boolean exists = th != null;
if (selection != null) {
if (th != null) {
priorities = th.getFilePriorities();
} else {
priorities = Priority.array(Priority.IGNORE, ti.getNumFiles());
}
for (int i = 0; i < selection.length; i++) {
if (selection[i]) {
priorities[i] = Priority.NORMAL;
}
}
}
downloader.download(ti, saveDir, priorities, null);
if (!exists) {
saveResumeTorrent(torrent);
}
}
public void download(TorrentInfo ti, File saveDir, boolean[] selection) {
if (session == null) {
return;
}
saveDir = setupSaveDir(saveDir);
if (saveDir == null) {
return;
}
Priority[] priorities = null;
TorrentHandle th = downloader.find(ti.getInfoHash());
boolean exists = th != null;
if (selection != null) {
if (th != null) {
priorities = th.getFilePriorities();
} else {
priorities = Priority.array(Priority.IGNORE, ti.getNumFiles());
}
for (int i = 0; i < selection.length; i++) {
if (selection[i]) {
priorities[i] = Priority.NORMAL;
}
}
}
downloader.download(ti, saveDir, priorities, null);
if (!exists) {
File torrent = saveTorrent(ti);
saveResumeTorrent(torrent);
}
}
public void download(TorrentCrawledSearchResult sr, File saveDir) {
if (session == null) {
return;
}
saveDir = setupSaveDir(saveDir);
if (saveDir == null) {
return;
}
TorrentInfo ti = sr.getTorrentInfo();
int fileIndex = sr.getFileIndex();
TorrentHandle th = downloader.find(ti.getInfoHash());
boolean exists = th != null;
if (th != null) {
Priority[] priorities = th.getFilePriorities();
if (priorities[fileIndex] == Priority.IGNORE) {
priorities[fileIndex] = Priority.NORMAL;
downloader.download(ti, saveDir, priorities, null);
}
} else {
Priority[] priorities = Priority.array(Priority.IGNORE, ti.getNumFiles());
priorities[fileIndex] = Priority.NORMAL;
downloader.download(ti, saveDir, priorities, null);
}
if (!exists) {
File torrent = saveTorrent(ti);
saveResumeTorrent(torrent);
}
}
public byte[] fetchMagnet(String uri, long timeout) {
if (session == null) {
return null;
}
return downloader.fetchMagnet(uri, timeout);
}
public void restoreDownloads() {
if (session == null) {
return;
}
if (ctx.homeDir == null || !ctx.homeDir.exists()) {
LOG.warn("Wrong setup with BTEngine home dir");
return;
}
File[] torrents = ctx.homeDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return FilenameUtils.getExtension(name).equals("torrent");
}
});
for (File t : torrents) {
try {
String infoHash = FilenameUtils.getBaseName(t.getName());
File resumeFile = resumeDataFile(infoHash);
session.asyncAddTorrent(t, null, resumeFile);
} catch (Throwable e) {
LOG.error("Error restoring torrent download: " + t, e);
}
}
migrateVuzeDownloads();
}
File settingsFile() {
return new File(ctx.homeDir, "settings.dat");
}
File resumeTorrentFile(String infoHash) {
return new File(ctx.homeDir, infoHash + ".torrent");
}
File resumeDataFile(String infoHash) {
return new File(ctx.homeDir, infoHash + ".resume");
}
File readTorrentPath(String infoHash) {
File torrent = null;
try {
byte[] arr = FileUtils.readFileToByteArray(resumeTorrentFile(infoHash));
entry e = entry.bdecode(Vectors.bytes2char_vector(arr));
torrent = new File(e.dict().get(TORRENT_ORIG_PATH_KEY).string());
} catch (Throwable e) {
// can't recover original torrent path
}
return torrent;
}
private File saveTorrent(TorrentInfo ti) {
File torrentFile;
try {
String name = ti.getName();
if (name == null || name.length() == 0) {
name = ti.getInfoHash().toString();
}
name = OSUtils.escapeFilename(name);
torrentFile = new File(ctx.torrentsDir, name + ".torrent");
byte[] arr = ti.toEntry().bencode();
FileUtils.writeByteArrayToFile(torrentFile, arr);
} catch (Throwable e) {
torrentFile = null;
LOG.warn("Error saving torrent info to file", e);
}
return torrentFile;
}
private void saveResumeTorrent(File torrent) {
try {
TorrentInfo ti = new TorrentInfo(torrent);
entry e = ti.toEntry().getSwig();
e.dict().set(TORRENT_ORIG_PATH_KEY, new entry(torrent.getAbsolutePath()));
byte[] arr = Vectors.char_vector2bytes(e.bencode());
FileUtils.writeByteArrayToFile(resumeTorrentFile(ti.getInfoHash().toString()), arr);
} catch (Throwable e) {
LOG.warn("Error saving resume torrent", e);
}
}
private void doResumeData(TorrentAlert<?> alert) {
TorrentHandle th = alert.getHandle();
if (th.isValid() && th.needSaveResumeData()) {
th.saveResumeData();
}
}
private void fireStarted() {
if (listener != null) {
listener.started(this);
}
}
private void fireStopped() {
if (listener != null) {
listener.stopped(this);
}
}
private void fireDownloadAdded(BTDownload dl) {
if (listener != null) {
listener.downloadAdded(this, dl);
}
}
private void migrateVuzeDownloads() {
try {
File dir = new File(ctx.homeDir.getParent(), "azureus");
File file = new File(dir, "downloads.config");
if (file.exists()) {
Entry configEntry = Entry.bdecode(file);
List<Entry> downloads = configEntry.dictionary().get("downloads").list();
for (Entry d : downloads) {
try {
Map<String, Entry> map = d.dictionary();
File saveDir = new File(map.get("save_dir").string());
File torrent = new File(map.get("torrent").string());
ArrayList<Entry> filePriorities = map.get("file_priorities").list();
Priority[] priorities = Priority.array(Priority.IGNORE, filePriorities.size());
for (int i = 0; i < filePriorities.size(); i++) {
long p = filePriorities.get(i).integer();
if (p != 0) {
priorities[i] = Priority.NORMAL;
}
}
if (torrent.exists() && saveDir.exists()) {
LOG.info("Restored old vuze download: " + torrent);
downloader.download(new TorrentInfo(torrent), saveDir, priorities, null);
saveResumeTorrent(torrent);
}
} catch (Throwable e) {
LOG.error("Error restoring vuze torrent download", e);
}
}
file.delete();
}
} catch (Throwable e) {
LOG.error("Error migrating old vuze downloads", e);
}
}
private File setupSaveDir(File saveDir) {
File result = null;
if (saveDir == null) {
if (ctx.dataDir != null) {
result = ctx.dataDir;
} else {
LOG.warn("Unable to setup save dir path, review your logic, both saveDir and ctx.dataDir are null.");
}
} else {
result = saveDir;
}
if (result != null && !result.isDirectory() && !result.mkdirs()) {
result = null;
LOG.warn("Failed to create save dir to download");
}
if (result != null && !result.canWrite()) {
result = null;
LOG.warn("Failed to setup save dir with write access");
}
return result;
}
private final class InnerListener implements AlertListener {
@Override
public int[] types() {
return INNER_LISTENER_TYPES;
}
@Override
public void alert(Alert<?> alert) {
//LOG.info(a.message());
AlertType type = alert.getType();
switch (type) {
case TORRENT_ADDED:
fireDownloadAdded(new BTDownload(BTEngine.this, ((TorrentAlert<?>) alert).getHandle()));
doResumeData((TorrentAlert<?>) alert);
break;
case PIECE_FINISHED:
doResumeData((TorrentAlert<?>) alert);
break;
case PORTMAP:
firewalled = false;
break;
case PORTMAP_ERROR:
firewalled = true;
break;
}
}
}
//--------------------------------------------------
// Settings methods
//--------------------------------------------------
public int getDownloadSpeedLimit() {
if (session == null) {
return 0;
}
return getSettings().getDownloadRateLimit();
}
public void setDownloadSpeedLimit(int limit) {
if (session == null) {
return;
}
SessionSettings s = getSettings();
s.setDownloadRateLimit(limit);
saveSettings(s);
}
public int getUploadSpeedLimit() {
if (session == null) {
return 0;
}
return getSettings().getUploadRateLimit();
}
public void setUploadSpeedLimit(int limit) {
if (session == null) {
return;
}
SessionSettings s = getSettings();
s.setUploadRateLimit(limit);
saveSettings(s);
}
public int getMaxActiveDownloads() {
if (session == null) {
return 0;
}
return getSettings().getActiveDownloads();
}
public void setMaxActiveDownloads(int limit) {
if (session == null) {
return;
}
SessionSettings s = getSettings();
s.setActiveDownloads(limit);
saveSettings(s);
}
public int getMaxActiveSeeds() {
if (session == null) {
return 0;
}
return getSettings().getActiveSeeds();
}
public void setMaxActiveSeeds(int limit) {
if (session == null) {
return;
}
SessionSettings s = getSettings();
s.setActiveSeeds(limit);
saveSettings(s);
}
public int getMaxConnections() {
if (session == null) {
return 0;
}
return getSettings().getConnectionsLimit();
}
public void setMaxConnections(int limit) {
if (session == null) {
return;
}
SessionSettings s = getSettings();
s.setConnectionsLimit(limit);
saveSettings(s);
}
public int getMaxPeers() {
if (session == null) {
return 0;
}
return getSettings().getMaxPeerlistSize();
}
public void setMaxPeers(int limit) {
if (session == null) {
return;
}
SessionSettings s = getSettings();
s.setMaxPeerlistSize(limit);
saveSettings(s);
}
}