com.frostwire.bittorrent.BTDownload.java Source code

Java tutorial

Introduction

Here is the source code for com.frostwire.bittorrent.BTDownload.java

Source

/*
 * Created by Angel Leon (@gubatron), Alden Torres (aldenml)
 * Copyright (c) 2011-2017, FrostWire(R). All rights reserved.
 *
 * 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.frostwire.bittorrent;

import com.frostwire.jlibtorrent.AlertListener;
import com.frostwire.jlibtorrent.AnnounceEntry;
import com.frostwire.jlibtorrent.Entry;
import com.frostwire.jlibtorrent.FileStorage;
import com.frostwire.jlibtorrent.PiecesTracker;
import com.frostwire.jlibtorrent.Priority;
import com.frostwire.jlibtorrent.SessionHandle;
import com.frostwire.jlibtorrent.TorrentFlags;
import com.frostwire.jlibtorrent.TorrentHandle;
import com.frostwire.jlibtorrent.TorrentInfo;
import com.frostwire.jlibtorrent.TorrentStatus;
import com.frostwire.jlibtorrent.Vectors;
import com.frostwire.jlibtorrent.alerts.Alert;
import com.frostwire.jlibtorrent.alerts.AlertType;
import com.frostwire.jlibtorrent.alerts.PieceFinishedAlert;
import com.frostwire.jlibtorrent.alerts.SaveResumeDataAlert;
import com.frostwire.jlibtorrent.alerts.TorrentAlert;
import com.frostwire.jlibtorrent.swig.add_torrent_params;
import com.frostwire.jlibtorrent.swig.entry;
import com.frostwire.jlibtorrent.swig.string_entry_map;
import com.frostwire.jlibtorrent.swig.string_vector;
import com.frostwire.jlibtorrent.swig.torrent_flags_t;
import com.frostwire.platform.Platforms;
import com.frostwire.transfers.BittorrentDownload;
import com.frostwire.transfers.TransferItem;
import com.frostwire.transfers.TransferState;
import com.frostwire.util.Logger;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;

import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author gubatron
 * @author aldenml
 */
public final class BTDownload 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_FINISHED.swig(), AlertType.TORRENT_REMOVED.swig(),
            AlertType.TORRENT_CHECKED.swig(), AlertType.SAVE_RESUME_DATA.swig(), AlertType.PIECE_FINISHED.swig(),
            AlertType.STORAGE_MOVED.swig() };

    private static final String EXTRA_DATA_KEY = "extra_data";
    private 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 PiecesTracker piecesTracker;
    private final File parts;

    private final Map<String, String> extra;

    private BTDownloadListener listener;

    private Set<File> incompleteFilesToRemove;

    private long lastSaveResumeTime;
    private final PaymentOptions paymentOptions;

    private final InnerListener innerListener;

    private String predominantFileExtension;

    public BTDownload(BTEngine engine, TorrentHandle th) {
        this.engine = engine;
        this.th = th;
        this.savePath = new File(th.savePath());
        this.created = new Date(th.status().addedTime());
        TorrentInfo ti = th.torrentFile();
        this.piecesTracker = ti != null ? new PiecesTracker(ti) : null;
        this.parts = ti != null ? new File(savePath, "." + ti.infoHash() + ".parts") : null;
        this.extra = createExtra();
        this.paymentOptions = loadPaymentOptions(ti);
        this.innerListener = new InnerListener();
        engine.addListener(innerListener);
    }

    public Map<String, String> getExtra() {
        return extra;
    }

    @Override
    public String getName() {
        if (th == null) {
            return null;
        }
        return th.name();
    }

    @Override
    public String getDisplayName() {
        Priority[] priorities = th.filePriorities();

        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.name() : FilenameUtils.getName(th.torrentFile().files().filePath(index));
    }

    public long getSize() {
        TorrentInfo ti = th.torrentFile();
        return ti != null ? ti.totalSize() : 0;
    }

    public PaymentOptions getPaymentOptions() {
        return paymentOptions;
    }

    public boolean isPaused() {
        return th.isValid() && (isPaused(th.status()) || engine.isPaused() || !engine.isRunning());
    }

    public boolean isSeeding() {
        return th.isValid() && th.status().isSeeding();
    }

    public boolean isFinished() {
        return isFinished(false);
    }

    public boolean isFinished(boolean force) {
        return th.isValid() && th.status(force).isFinished();
    }

    public TransferState getState() {
        if (!engine.isRunning()) {
            return TransferState.STOPPED;
        }

        if (engine.isPaused()) {
            return TransferState.PAUSED;
        }

        if (!th.isValid()) {
            return TransferState.ERROR;
        }

        final TorrentStatus status = th.status();
        final boolean isPaused = isPaused(status);

        if (isPaused && status.isFinished()) {
            return TransferState.FINISHED;
        }

        if (isPaused && !status.isFinished()) {
            return TransferState.PAUSED;
        }

        if (!isPaused && status.isFinished()) { // see the docs of isFinished
            return TransferState.SEEDING;
        }

        final TorrentStatus.State state = status.state();

        switch (state) {
        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;
        }
    }

    /**
     * If we have a torrent which is downloaded as a folder, this will return the parent of that folder.
     * (e.g. default save location for torrents)
     * <p/>
     * If you want to have the folder where the torrent's data files are located you
     * want to use getContentSavePath().
     */
    @Override
    public File getSavePath() {
        return savePath;
    }

    @Override
    public File previewFile() {
        return null;
    }

    @Override
    public int getProgress() {
        if (th == null || !th.isValid()) {
            return 0;
        }

        TorrentStatus ts = th.status();

        if (ts == null) { // this can't never happens
            return 0;
        }

        float fp = ts.progress();
        TorrentStatus.State state = ts.state();

        if (Float.compare(fp, 1f) == 0 && state != TorrentStatus.State.CHECKING_FILES) {
            return 100;
        }

        int p = (int) (fp * 100);
        if (p > 0 && state != TorrentStatus.State.CHECKING_FILES) {
            return Math.min(p, 100);
        }

        return 0;
    }

    @Override
    public boolean isComplete() {
        return getProgress() == 100;
    }

    public long getBytesReceived() {
        return th.isValid() ? th.status().totalDone() : 0;
    }

    public long getTotalBytesReceived() {
        return th.isValid() ? th.status().allTimeDownload() : 0;
    }

    public long getBytesSent() {
        return th.isValid() ? th.status().totalUpload() : 0;
    }

    public long getTotalBytesSent() {
        return th.isValid() ? th.status().allTimeUpload() : 0;
    }

    public long getDownloadSpeed() {
        return (!th.isValid() || isFinished() || isPaused() || isSeeding()) ? 0 : th.status().downloadPayloadRate();
    }

    public long getUploadSpeed() {
        return (!th.isValid() || (isFinished() && !isSeeding()) || isPaused()) ? 0
                : th.status().uploadPayloadRate();
    }

    @Override
    public boolean isDownloading() {
        return getDownloadSpeed() > 0;
    }

    public int getConnectedPeers() {
        return th.isValid() ? th.status().numPeers() : 0;
    }

    public TorrentHandle getTorrentHandle() {
        return th;
    }

    public int getTotalPeers() {
        return th.isValid() ? th.status().listPeers() : 0;
    }

    public int getConnectedSeeds() {
        return th.isValid() ? th.status().numSeeds() : 0;
    }

    public int getTotalSeeds() {
        return th.isValid() ? th.status().listSeeds() : 0;
    }

    @Override
    public File getContentSavePath() {
        try {
            if (!th.isValid()) {
                return null;
            }

            TorrentInfo ti = th.torrentFile();
            if (ti != null && ti.swig() != null) {
                return new File(savePath.getAbsolutePath(), ti.numFiles() > 1 ? th.name() : ti.files().filePath(0));
            }
        } catch (Throwable e) {
            LOG.warn("Could not retrieve download content save path", e);
        }

        return null;
    }

    public String getInfoHash() {
        return th.infoHash().toString();
    }

    @Override
    public Date getCreated() {
        return created;
    }

    public long getETA() {
        if (!th.isValid()) {
            return 0;
        }
        TorrentInfo ti = th.torrentFile();
        if (ti == null) {
            return 0;
        }
        TorrentStatus status = th.status();
        long left = ti.totalSize() - status.totalDone();
        long rate = status.downloadPayloadRate();
        if (left <= 0) {
            return 0;
        }
        if (rate <= 0) {
            return -1;
        }
        return left / rate;
    }

    public void pause() {
        if (!th.isValid()) {
            return;
        }

        extra.put(WAS_PAUSED_EXTRA_KEY, Boolean.TRUE.toString());

        th.unsetFlags(TorrentFlags.AUTO_MANAGED);
        th.pause();

        doResumeData(true);
    }

    public void resume() {
        if (!th.isValid()) {
            return;
        }

        extra.put(WAS_PAUSED_EXTRA_KEY, Boolean.FALSE.toString());

        th.setFlags(TorrentFlags.AUTO_MANAGED);
        th.resume();

        doResumeData(true);
    }

    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();

        incompleteFilesToRemove = getIncompleteFiles();

        if (th.isValid()) {
            if (deleteData) {
                engine.remove(th, SessionHandle.DELETE_FILES);
            } else {
                engine.remove(th);
            }
        }

        if (deleteTorrent) {
            File torrent = engine.readTorrentPath(infoHash);
            if (torrent != null) {
                Platforms.get().fileSystem().delete(torrent);
            }
        }

        engine.resumeDataFile(infoHash).delete();
        engine.resumeTorrentFile(infoHash).delete();
    }

    @Override
    public String getPredominantFileExtension() {
        if (predominantFileExtension == null && th != null) {
            TorrentInfo torrentInfo = th.torrentFile();
            if (torrentInfo != null) {
                FileStorage files = torrentInfo.files();
                Map<String, Long> extensionByteSums = new HashMap<>();
                int numFiles = files.numFiles();
                if (files.paths() != null) {
                    for (int i = 0; i < numFiles; i++) {
                        String path = files.filePath(i);
                        String extension = FilenameUtils.getExtension(path);
                        if ("".equals(extension)) {
                            // skip folders
                            continue;
                        }
                        if (extensionByteSums.containsKey(extension)) {
                            Long bytes = extensionByteSums.get(extension);
                            extensionByteSums.put(extension, bytes + files.fileSize(i));
                        } else {
                            extensionByteSums.put(extension, files.fileSize(i));
                        }
                    }
                    String extensionCandidate = null;
                    Set<String> exts = extensionByteSums.keySet();
                    for (String ext : exts) {
                        if (extensionCandidate == null) {
                            extensionCandidate = ext;
                        } else {
                            if (extensionByteSums.get(ext) > extensionByteSums.get(extensionCandidate)) {
                                extensionCandidate = ext;
                            }
                        }
                    }
                    predominantFileExtension = extensionCandidate;
                }
            }
        }
        return predominantFileExtension;
    }

    public BTDownloadListener getListener() {
        return listener;
    }

    public void setListener(BTDownloadListener listener) {
        this.listener = listener;
    }

    private void torrentFinished() {
        if (listener != null) {
            try {
                listener.finished(this);
            } catch (Throwable e) {
                LOG.error("Error calling listener (finished)", e);
            }
        }
        doResumeData(true);
    }

    private void torrentRemoved() {
        engine.removeListener(innerListener);

        if (parts != null) {
            parts.delete();
        }

        if (listener != null) {
            try {
                listener.removed(this, incompleteFilesToRemove);
            } catch (Throwable e) {
                LOG.error("Error calling listener (removed)", e);
            }
        }
    }

    private void torrentChecked() {
        try {
            if (th.isValid()) {
                // trigger items calculation
                getItems();
            }
        } catch (Throwable e) {
            LOG.warn("Error handling torrent checked logic", e);
        }
    }

    private void pieceFinished(PieceFinishedAlert alert) {
        try {
            if (piecesTracker != null) {
                piecesTracker.setComplete(alert.pieceIndex(), true);
            }
        } catch (Throwable e) {
            LOG.warn("Error handling piece finished logic", e);
        }
    }

    public boolean isPartial() {
        if (th.isValid()) {
            Priority[] priorities = th.filePriorities();
            for (Priority p : priorities) {
                if (Priority.IGNORE.equals(p)) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public String magnetUri() {
        return th.makeMagnetUri();
    }

    public int getDownloadRateLimit() {
        return th.getDownloadLimit();
    }

    public void setDownloadRateLimit(int limit) {
        th.setDownloadLimit(limit);
    }

    public int getUploadRateLimit() {
        return th.getUploadLimit();
    }

    public void setUploadRateLimit(int limit) {
        th.setUploadLimit(limit);
    }

    public void requestTrackerAnnounce() {
        th.forceReannounce();
    }

    public void requestTrackerScrape() {
        th.scrapeTracker();
    }

    public Set<String> trackers() {
        if (!th.isValid()) {
            return new HashSet<>();
        }
        List<AnnounceEntry> trackers = th.trackers();
        Set<String> urls = new HashSet<>(trackers.size());
        for (AnnounceEntry e : trackers) {
            urls.add(e.url());
        }
        return urls;
    }

    public void trackers(Set<String> trackers) {
        List<AnnounceEntry> list = new ArrayList<>(trackers.size());
        for (String url : trackers) {
            list.add(new AnnounceEntry(url));
        }
        th.replaceTrackers(list);
        doResumeData(true);
    }

    @Override
    public List<TransferItem> getItems() {
        ArrayList<TransferItem> items = new ArrayList<>();
        if (th.isValid()) {
            TorrentInfo ti = th.torrentFile();
            if (ti != null && ti.isValid()) {
                FileStorage fs = ti.files();
                int numFiles = ti.numFiles();
                for (int i = 0; i < numFiles; i++) {
                    BTDownloadItem item = new BTDownloadItem(th, i, fs.filePath(i), fs.fileSize(i), piecesTracker);
                    items.add(item);
                }
                if (piecesTracker != null) {
                    int numPieces = ti.numPieces();
                    // perform piece complete check
                    for (int i = 0; i < numPieces; i++) {
                        if (th.havePiece(i)) {
                            piecesTracker.setComplete(i, true);
                        }
                    }
                }
            }
        }
        return items;
    }

    public File getTorrentFile() {
        return engine.readTorrentPath(this.getInfoHash());
    }

    public Set<File> getIncompleteFiles() {
        Set<File> s = new HashSet<>();
        try {
            if (!th.isValid()) {
                return s;
            }
            long[] progress = th.fileProgress(TorrentHandle.FileProgressFlags.PIECE_GRANULARITY);
            TorrentInfo ti = th.torrentFile();
            if (ti == null) {
                // still downloading the info (from magnet)
                return s;
            }
            FileStorage fs = ti.files();
            String prefix = savePath.getAbsolutePath();
            long createdTime = created.getTime();
            for (int i = 0; i < progress.length; i++) {
                String fePath = fs.filePath(i);
                long feSize = fs.fileSize(i);
                if (progress[i] < feSize) {
                    // lets see if indeed the file is incomplete
                    File f = new File(prefix, fePath);
                    if (!f.exists()) {
                        continue; // nothing to do here
                    }
                    if (f.lastModified() >= createdTime) {
                        // we have a file modified (supposedly) by this transfer
                        s.add(f);
                    }
                }
            }
        } catch (Throwable e) {
            LOG.error("Error calculating the incomplete files set", e);
        }
        return s;
    }

    public boolean isSequentialDownload() {
        if (!th.isValid()) {
            return false;
        }

        torrent_flags_t flags = th.status().flags();
        return flags.and_(TorrentFlags.SEQUENTIAL_DOWNLOAD).nonZero();
    }

    public void setSequentialDownload(boolean sequential) {
        if (!th.isValid()) {
            return;
        }

        if (sequential) {
            th.setFlags(TorrentFlags.SEQUENTIAL_DOWNLOAD);
        } else {
            th.unsetFlags(TorrentFlags.SEQUENTIAL_DOWNLOAD);
        }
    }

    public File partsFile() {
        return parts;
    }

    private PaymentOptions loadPaymentOptions(TorrentInfo ti) {
        try {
            BTInfoAdditionalMetadataHolder holder = new BTInfoAdditionalMetadataHolder(ti, getDisplayName());
            return holder.getPaymentOptions();
        } catch (Throwable e) {
            return null;
        }
    }

    private void serializeResumeData(SaveResumeDataAlert alert) {
        try {
            if (th.isValid()) {
                String infoHash = th.infoHash().toString();
                File file = engine.resumeDataFile(infoHash);
                entry e = add_torrent_params.write_resume_data(alert.swig().getParams());
                e.dict().set(EXTRA_DATA_KEY, Entry.fromMap(extra).swig());
                FileUtils.writeByteArrayToFile(file, Vectors.byte_vector2bytes(e.bencode()));
            }
        } catch (Throwable e) {
            LOG.warn("Error saving resume data", e);
        }
    }

    private void doResumeData(boolean force) {
        long now = System.currentTimeMillis();
        if (force || (now - lastSaveResumeTime) >= SAVE_RESUME_RESOLUTION_MILLIS) {
            lastSaveResumeTime = now;
        } else {
            // skip, too fast, see SAVE_RESUME_RESOLUTION_MILLIS
            return;
        }
        try {
            if (th != null && th.isValid()) {
                th.saveResumeData(TorrentHandle.SAVE_INFO_DICT);
            }
        } catch (Throwable e) {
            LOG.warn("Error triggering resume data", e);
        }
    }

    private Map<String, String> createExtra() {
        Map<String, String> map = new HashMap<>();
        try {
            String infoHash = getInfoHash();
            File file = engine.resumeDataFile(infoHash);
            if (file.exists()) {
                byte[] arr = FileUtils.readFileToByteArray(file);
                entry e = entry.bdecode(Vectors.bytes2byte_vector(arr));
                string_entry_map d = e.dict();
                if (d.has_key(EXTRA_DATA_KEY)) {
                    readExtra(d.get(EXTRA_DATA_KEY).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 static boolean isPaused(TorrentStatus s) {
        return s.flags().and_(TorrentFlags.PAUSED).nonZero();
    }

    private final class InnerListener implements AlertListener {

        @Override
        public int[] types() {
            return ALERT_TYPES;
        }

        @Override
        public void alert(Alert<?> alert) {
            if (!(alert instanceof TorrentAlert<?>)) {
                return;
            }
            if (!((TorrentAlert<?>) alert).handle().swig().op_eq(th.swig())) {
                return;
            }
            AlertType type = alert.type();
            switch (type) {
            case TORRENT_FINISHED:
                torrentFinished();
                break;
            case TORRENT_REMOVED:
                torrentRemoved();
                break;
            case TORRENT_CHECKED:
                torrentChecked();
                break;
            case SAVE_RESUME_DATA:
                serializeResumeData((SaveResumeDataAlert) alert);
                break;
            case PIECE_FINISHED:
                pieceFinished((PieceFinishedAlert) alert);
                doResumeData(false);
                break;
            case STORAGE_MOVED:
                doResumeData(true);
                break;
            }
        }
    }
}