/*
* Created by Angel Leon (@gubatron), Alden Torres (aldenml)
* Copyright (c) 2011-2014, FrostWire(R). All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.frostwire.bittorrent;
import com.frostwire.jlibtorrent.*;
import com.frostwire.jlibtorrent.alerts.*;
import com.frostwire.jlibtorrent.swig.entry;
import com.frostwire.jlibtorrent.swig.string_entry_map;
import com.frostwire.jlibtorrent.swig.string_vector;
import com.frostwire.logging.Logger;
import com.frostwire.transfers.BittorrentDownload;
import com.frostwire.transfers.TransferItem;
import com.frostwire.transfers.TransferState;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import java.io.File;
import java.util.*;
/**
* @author gubatron
* @author aldenml
*/
public final class BTDownload extends TorrentAlertAdapter implements BittorrentDownload {
private static final Logger LOG = Logger.getLogger(BTDownload.class);
private static final long SAVE_RESUME_RESOLUTION_MILLIS = 10000;
private static final int[] ALERT_TYPES = {AlertType.TORRENT_PRIORITIZE.getSwig(),
AlertType.TORRENT_FINISHED.getSwig(),
AlertType.TORRENT_REMOVED.getSwig(), AlertType.SAVE_RESUME_DATA.getSwig()};
public static final String WAS_PAUSED_EXTRA_KEY = "was_paused";
private final BTEngine engine;
private final TorrentHandle th;
private final File savePath;
private final Date created;
private final Map<String, String> extra;
private BTDownloadListener listener;
private Set<File> incompleteFilesToRemove;
private long lastSaveResumeTime;
public BTDownload(BTEngine engine, TorrentHandle th) {
super(th);
this.engine = engine;
this.th = th;
this.savePath = new File(th.getSavePath());
this.created = new Date(th.getStatus().getAddedTime());
this.extra = createExtra();
engine.getSession().addListener(this);
}
public Map<String, String> getExtra() {
return extra;
}
@Override
public String getName() {
return th.getName();
}
@Override
public String getDisplayName() {
Priority[] priorities = th.getFilePriorities();
int count = 0;
int index = 0;
for (int i = 0; i < priorities.length; i++) {
if (!Priority.IGNORE.equals(priorities[i])) {
count++;
index = i;
}
}
return count != 1 ? th.getName() : FilenameUtils.getName(th.getTorrentInfo().getFileAt(index).getPath());
}
public long getSize() {
TorrentInfo ti = th.getTorrentInfo();
return ti != null ? ti.getTotalSize() : 0;
}
public boolean isPaused() {
return th.getStatus().isPaused();
}
public boolean isSeeding() {
return th.getStatus().isSeeding();
}
public boolean isFinished() {
return th.getStatus().isFinished();
}
@Override
public boolean isDownloading() {
return getDownloadSpeed() > 0;
}
@Override
public boolean isUploading() {
return getUploadSpeed() > 0;
}
public TransferState getState() {
if (!engine.isStarted()) {
return TransferState.STOPPED;
}
if (engine.isPaused()) {
return TransferState.PAUSED;
}
if (!th.isValid()) {
return TransferState.ERROR;
}
final TorrentStatus status = th.getStatus();
if (status.isPaused()) {
return TransferState.PAUSED;
}
if (status.isFinished()) { // see the docs of isFinished
return TransferState.SEEDING;
}
final TorrentStatus.State state = status.getState();
switch (state) {
case QUEUED_FOR_CHECKING:
return TransferState.QUEUED_FOR_CHECKING;
case CHECKING_FILES:
return TransferState.CHECKING;
case DOWNLOADING_METADATA:
return TransferState.DOWNLOADING_METADATA;
case DOWNLOADING:
return TransferState.DOWNLOADING;
case FINISHED:
return TransferState.FINISHED;
case SEEDING:
return TransferState.SEEDING;
case ALLOCATING:
return TransferState.ALLOCATING;
case CHECKING_RESUME_DATA:
return TransferState.CHECKING;
case UNKNOWN:
return TransferState.UNKNOWN;
default:
return TransferState.UNKNOWN;
}
}
@Override
public File getSavePath() {
return savePath;
}
@Override
public int getProgress() {
float fp = th.getStatus().getProgress();
if (Float.compare(fp, 1f) == 0) {
return 100;
}
int p = (int) (th.getStatus().getProgress() * 100);
return Math.min(p, 100);
}
@Override
public boolean isComplete() {
return getProgress() == 100;
}
public long getBytesReceived() {
return th.getStatus().getTotalDownload();
}
public long getTotalBytesReceived() {
return th.getStatus().getAllTimeDownload();
}
public long getBytesSent() {
return th.getStatus().getTotalUpload();
}
public long getTotalBytesSent() {
return th.getStatus().getAllTimeUpload();
}
public long getDownloadSpeed() {
return (isFinished() || isPaused() || isSeeding()) ? 0 : th.getStatus().getDownloadPayloadRate();
}
public long getUploadSpeed() {
return th.getStatus().getUploadPayloadRate();
}
public int getConnectedPeers() {
return th.getStatus().getNumPeers();
}
public int getTotalPeers() {
return th.getStatus().getListPeers();
}
public int getConnectedSeeds() {
return th.getStatus().getNumSeeds();
}
public int getTotalSeeds() {
return th.getStatus().getListSeeds();
}
public String getInfoHash() {
return th.getInfoHash().toString();
}
@Override
public Date getCreated() {
return created;
}
public long getETA() {
TorrentInfo ti = th.getTorrentInfo();
if (ti == null) {
return 0;
}
TorrentStatus status = th.getStatus();
long left = ti.getTotalSize() - status.getTotalDone();
long rate = status.getDownloadPayloadRate();
if (left <= 0) {
return 0;
}
if (rate <= 0) {
return -1;
}
return left / rate;
}
public void pause() {
extra.put(WAS_PAUSED_EXTRA_KEY, Boolean.TRUE.toString());
th.setAutoManaged(false);
th.pause();
th.saveResumeData();
}
public void resume() {
extra.put(WAS_PAUSED_EXTRA_KEY, Boolean.FALSE.toString());
th.setAutoManaged(true);
th.resume();
th.saveResumeData();
}
public void remove() {
remove(false, false);
}
@Override
public void remove(boolean deleteData) {
remove(false, deleteData);
}
public void remove(boolean deleteTorrent, boolean deleteData) {
String infoHash = this.getInfoHash();
Session s = engine.getSession();
incompleteFilesToRemove = getIncompleteFiles(true);
if (th.isValid()) {
if (deleteData) {
s.removeTorrent(th, Session.Options.DELETE_FILES);
} else {
s.removeTorrent(th);
}
}
if (deleteTorrent) {
File torrent = engine.readTorrentPath(infoHash);
if (torrent != null && torrent.exists()) {
torrent.delete();
}
}
engine.resumeDataFile(infoHash).delete();
engine.resumeTorrentFile(infoHash).delete();
}
public BTDownloadListener getListener() {
return listener;
}
public void setListener(BTDownloadListener listener) {
this.listener = listener;
}
@Override
public int[] types() {
return ALERT_TYPES;
}
@Override
public void torrentPrioritize(TorrentPrioritizeAlert alert) {
if (listener != null) {
try {
listener.update(this);
} catch (Throwable e) {
LOG.error("Error calling listener", e);
}
}
resume();
}
@Override
public void torrentFinished(TorrentFinishedAlert alert) {
if (listener != null) {
try {
listener.finished(this);
} catch (Throwable e) {
LOG.error("Error calling listener", e);
}
}
}
@Override
public void torrentRemoved(TorrentRemovedAlert alert) {
engine.getSession().removeListener(this);
fireRemoved(incompleteFilesToRemove);
}
@Override
public void saveResumeData(SaveResumeDataAlert alert) {
long now = System.currentTimeMillis();
if ((now - lastSaveResumeTime) >= SAVE_RESUME_RESOLUTION_MILLIS) {
lastSaveResumeTime = now;
} else {
// skip, too fast, see SAVE_RESUME_RESOLUTION_MILLIS
return;
}
try {
TorrentHandle th = alert.getHandle();
if (th.isValid()) {
String infoHash = th.getInfoHash().toString();
File file = engine.resumeDataFile(infoHash);
Entry e = alert.getResumeData();
e.getSwig().dict().set("extra_data", Entry.fromMap(extra).getSwig());
FileUtils.writeByteArrayToFile(file, e.bencode());
}
} catch (Throwable e) {
LOG.warn("Error saving resume data", e);
}
}
public boolean isPartial() {
Priority[] priorities = th.getFilePriorities();
for (Priority p : priorities) {
if (Priority.IGNORE.equals(p)) {
return true;
}
}
return false;
}
public String makeMagnetUri() {
return th.makeMagnetUri();
}
public int getDownloadRateLimit() {
return th.getDownloadLimit();
}
public void setDownloadRateLimit(int limit) {
th.setDownloadLimit(limit);
th.saveResumeData();
}
public int getUploadRateLimit() {
return th.getUploadLimit();
}
public void setUploadRateLimit(int limit) {
th.setUploadLimit(limit);
th.saveResumeData();
}
public void requestTrackerAnnounce() {
th.forceReannounce();
}
public void requestTrackerScrape() {
th.scrapeTracker();
}
public Set<String> getTrackers() {
List<AnnounceEntry> trackers = th.getTrackers();
Set<String> urls = new HashSet<String>(trackers.size());
for (AnnounceEntry e : trackers) {
urls.add(e.getUrl());
}
return urls;
}
public void setTrackers(Set<String> trackers) {
List<AnnounceEntry> list = new ArrayList<AnnounceEntry>(trackers.size());
for (String url : trackers) {
list.add(new AnnounceEntry(url));
}
th.replaceTrackers(list);
th.saveResumeData();
}
@Override
public List<TransferItem> getItems() {
List<TransferItem> items = Collections.emptyList();
if (th.isValid()) {
TorrentInfo ti = th.getTorrentInfo();
if (ti != null && ti.isValid()) {
int numFiles = ti.getNumFiles();
items = new ArrayList<TransferItem>(numFiles);
for (int i = 0; i < numFiles; i++) {
FileEntry fe = ti.getFileAt(i);
items.add(new BTDownloadItem(th, i, fe));
}
}
}
return items;
}
public File getTorrentFile() {
return engine.readTorrentPath(this.getInfoHash());
}
public Set<File> getIncompleteFiles() {
return getIncompleteFiles(false);
}
private Set<File> getIncompleteFiles(boolean accurate) {
Set<File> s = new HashSet<File>();
try {
if (!th.isValid()) {
return s;
}
long[] progress = accurate ? th.getFileProgress() : th.getFileProgress(TorrentHandle.FileProgressFlags.PIECE_GRANULARITY);
TorrentInfo ti = th.getTorrentInfo();
String prefix = savePath.getAbsolutePath();
for (int i = 0; i < progress.length; i++) {
FileEntry fe = ti.getFileAt(i);
if (progress[i] < fe.getSize()) {
s.add(new File(prefix, fe.getPath()));
}
}
} catch (Throwable e) {
LOG.error("Error calculating the incomplete files set", e);
}
return s;
}
private Map<String, String> createExtra() {
Map<String, String> map = new HashMap<String, String>();
try {
String infoHash = getInfoHash();
File file = engine.resumeDataFile(infoHash);
if (file.exists()) {
byte[] arr = FileUtils.readFileToByteArray(file);
entry e = entry.bdecode(Vectors.bytes2char_vector(arr));
string_entry_map d = e.dict();
if (d.has_key("extra_data")) {
readExtra(d.get("extra_data").dict(), map);
}
}
} catch (Throwable e) {
LOG.error("Error reading extra data from resume file", e);
}
return map;
}
private void readExtra(string_entry_map dict, Map<String, String> map) {
string_vector keys = dict.keys();
int size = (int) keys.size();
for (int i = 0; i < size; i++) {
String k = keys.get(i);
entry e = dict.get(k);
if (e.type() == entry.data_type.string_t) {
map.put(k, e.string());
}
}
}
public boolean wasPaused() {
boolean flag = false;
if (extra.containsKey(WAS_PAUSED_EXTRA_KEY)) {
try {
flag = Boolean.parseBoolean(extra.get(WAS_PAUSED_EXTRA_KEY));
} catch (Throwable e) {
// ignore
}
}
return flag;
}
private void fireRemoved(Set<File> incompleteFiles) {
if (listener != null) {
try {
listener.removed(this, incompleteFiles);
} catch (Throwable e) {
LOG.error("Error calling listener", e);
}
}
}
}