/* * 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 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); } }); } } } }