diff --git a/app/build.gradle b/app/build.gradle
index 623a220..79b3770 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -160,6 +160,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.browser:browser:1.3.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
+ implementation project(path: ':torrentStream')
testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
@@ -181,7 +182,6 @@ dependencies {
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.github.mancj:MaterialSearchBar:0.8.5'
- implementation "com.github.TorrentStream:TorrentStream-Android:2.7.0"
implementation "io.github.kobakei:ratethisapp:1.2.0"
implementation 'com.github.vkay94:DoubleTapPlayerView:1.0.0'
diff --git a/app/src/main/java/app/fedilab/fedilabtube/PeertubeActivity.java b/app/src/main/java/app/fedilab/fedilabtube/PeertubeActivity.java
index c88164e..7dee14c 100644
--- a/app/src/main/java/app/fedilab/fedilabtube/PeertubeActivity.java
+++ b/app/src/main/java/app/fedilab/fedilabtube/PeertubeActivity.java
@@ -75,6 +75,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
+import com.frostwire.jlibtorrent.SessionManager;
import com.github.se_bastiaan.torrentstream.StreamStatus;
import com.github.se_bastiaan.torrentstream.Torrent;
import com.github.se_bastiaan.torrentstream.TorrentOptions;
@@ -119,6 +120,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Timer;
+import java.util.TimerTask;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -239,7 +242,8 @@ public class PeertubeActivity extends AppCompatActivity implements CommentListAd
@Override
public void onStreamStarted(Torrent torrent) {
- startStream(peertube, torrent.getVideoFile().getAbsolutePath(), null, autoPlay, -1, null, null, true);
+
+
}
@Override
@@ -249,11 +253,44 @@ public class PeertubeActivity extends AppCompatActivity implements CommentListAd
@Override
public void onStreamReady(Torrent torrent) {
+ if (torrent.getVideoFile() != null) {
+ for (int i = 0; i < torrent.getFileNames().length; i++) {
+ torrent.getTorrentHandle().renameFile(i, torrent.getFileNames()[0].replaceAll("[^a-zA-Z0-9/.-]", "_"));
+ }
+ }
+ startStream(peertube, torrent.getVideoFile().getAbsolutePath().replaceAll("[^a-zA-Z0-9/.-]", "_"), null, autoPlay, -1, null, null, true);
+ PlayerControlView controlView = binding.doubleTapPlayerView.findViewById(R.id.exo_controller);
+ ConstraintLayout torrent_info = controlView.findViewById(R.id.torrent_info);
+ TextView dowload_rate = controlView.findViewById(R.id.dowload_rate);
+ TextView upload_rate = controlView.findViewById(R.id.upload_rate);
+ torrent_info.setVisibility(View.VISIBLE);
+
+ new Timer().scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ SessionManager sessionManager = torrentStream.getSessionManager();
+ if (sessionManager != null) {
+ long upload = sessionManager.uploadRate();
+ long download = sessionManager.downloadRate();
+ int seeds = sessionManager.maxActiveSeeds();
+ runOnUiThread(() -> {
+ dowload_rate.setText(String.format("▼ %s", Helper.rateSize(PeertubeActivity.this, download)));
+ upload_rate.setText(String.format("▲ %s", Helper.rateSize(PeertubeActivity.this, upload)));
+ });
+ }
+ }
+ }, 0, 1000);
+
}
+
@Override
public void onStreamProgress(Torrent torrent, StreamStatus status) {
-
+ if (binding != null) {
+ PlayerControlView controlView = binding.doubleTapPlayerView.findViewById(R.id.exo_controller);
+ TextView peers_number = controlView.findViewById(R.id.peers_number);
+ peers_number.setText(getString(R.string.peers, status.peers));
+ }
}
@Override
@@ -278,7 +315,7 @@ public class PeertubeActivity extends AppCompatActivity implements CommentListAd
}
isRemote = false;
TorrentOptions torrentOptions = new TorrentOptions.Builder()
- .saveLocation(getCacheDir())
+ .saveLocation(getCacheDir() + "/torrent/")
.autoDownload(true)
.removeFilesAfterStop(true)
.build();
@@ -1344,14 +1381,16 @@ public class PeertubeActivity extends AppCompatActivity implements CommentListAd
SingleSampleMediaSource subtitleSource = null;
DataSource.Factory dataSourceFactory = null;
if (localTorrentUrl != null) {
+
+ java.io.File localFile = new java.io.File(localTorrentUrl);
DataSpec dataSpec = new DataSpec(Uri.fromFile(new java.io.File(localTorrentUrl)));
FileDataSource fileDataSource = new FileDataSource();
try {
fileDataSource.open(dataSpec);
+ dataSourceFactory = () -> fileDataSource;
} catch (FileDataSource.FileDataSourceException e) {
e.printStackTrace();
}
- dataSourceFactory = () -> fileDataSource;
}
if (video_cache == 0 || dataSourceFactory != null) {
if (dataSourceFactory == null) {
diff --git a/app/src/main/java/app/fedilab/fedilabtube/helper/Helper.java b/app/src/main/java/app/fedilab/fedilabtube/helper/Helper.java
index c3ba4ac..1dddc73 100644
--- a/app/src/main/java/app/fedilab/fedilabtube/helper/Helper.java
+++ b/app/src/main/java/app/fedilab/fedilabtube/helper/Helper.java
@@ -628,4 +628,25 @@ public class Helper {
return String.format(Locale.getDefault(), "%s%s", df.format(rounded), context.getString(R.string.mb));
}
}
+
+
+ public static String rateSize(Context context, long size) {
+ if (size > 1000000000) {
+ float rounded = (float) size / 1000000000;
+ DecimalFormat df = new DecimalFormat("#.#");
+ return String.format(Locale.getDefault(), "%s%s", df.format(rounded), context.getString(R.string.gb));
+ } else if (size > 1000000) {
+ float rounded = (float) size / 1000000;
+ DecimalFormat df = new DecimalFormat("#.#");
+ return String.format(Locale.getDefault(), "%s%s", df.format(rounded), context.getString(R.string.mb));
+ } else if (size > 1000) {
+ float rounded = (float) size / 1000000;
+ DecimalFormat df = new DecimalFormat("#.#");
+ return String.format(Locale.getDefault(), "%s%s", df.format(rounded), context.getString(R.string.kb));
+ } else {
+ float rounded = (float) size / 1000000;
+ DecimalFormat df = new DecimalFormat("#.#");
+ return String.format(Locale.getDefault(), "%s%s", df.format(rounded), context.getString(R.string.b));
+ }
+ }
}
diff --git a/app/src/main/res/layout/exo_player_control_view.xml b/app/src/main/res/layout/exo_player_control_view.xml
index 79cf0f2..5b8281f 100644
--- a/app/src/main/res/layout/exo_player_control_view.xml
+++ b/app/src/main/res/layout/exo_player_control_view.xml
@@ -55,6 +55,44 @@
+
+
+
+
+
+
+
+
+
Mute
Unlimited
+
+ %1$d Peers
+ B
+ KB
MB
GB
Total video quota
@@ -449,4 +453,5 @@
Instance is not available!
The video should not have more than 5 tags!
Watermark
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index c4d03dc..e84ac5a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -9,6 +9,8 @@ buildscript {
classpath 'com.android.tools.build:gradle:4.1.1'
def nav_version = "2.3.0"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
+ classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
+ classpath 'de.undercouch:gradle-download-task:4.0.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
diff --git a/settings.gradle b/settings.gradle
index 1f87172..a8343ec 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,3 @@
+include ':torrentStream'
include ':app'
rootProject.name = "Fedilab Tube"
\ No newline at end of file
diff --git a/torrentStream/.gitignore b/torrentStream/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/torrentStream/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/torrentStream/build.gradle b/torrentStream/build.gradle
new file mode 100644
index 0000000..4f14510
--- /dev/null
+++ b/torrentStream/build.gradle
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+apply plugin: 'com.android.library'
+apply plugin: 'com.github.dcendents.android-maven'
+apply plugin: 'de.undercouch.download'
+
+group='com.github.TorrentStream'
+
+android {
+ compileSdkVersion 29
+
+ defaultConfig {
+ minSdkVersion 15
+ targetSdkVersion 29
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ lintOptions {
+ abortOnError true
+ }
+}
+
+ext {
+ libtorrentVersion = '1.2.11.0'
+}
+
+// Custom task which downloads the appropriate version of JAR files for jlibtorrent
+task downloadDependencies(type: Download) {
+ def baseUrl = "https://github.com/frostwire/frostwire-jlibtorrent" +
+ "/releases/download/release%2F$libtorrentVersion"
+
+ def platforms = ['arm', 'arm64', 'x86', 'x86_64']
+ def urls = platforms.collect { "$baseUrl/jlibtorrent-android-$it-${libtorrentVersion}.jar" }
+ urls.add("$baseUrl/jlibtorrent-${libtorrentVersion}.jar")
+
+ src urls
+ dest 'libs'
+ overwrite false
+}
+
+// Add our custom task as a dependency to the build
+// You may need to run gradle sync for IDE warnings to disappear
+preBuild.dependsOn(downloadDependencies)
+
+// Add deletion of libs folder to clean task
+clean {
+ delete 'libs'
+}
+
+dependencies {
+ api fileTree(dir: 'libs', include: ['*.jar'])
+ implementation fileTree(dir: "libs", include: ["*.jar"])
+}
\ No newline at end of file
diff --git a/torrentStream/libs/jlibtorrent-1.2.11.0.jar b/torrentStream/libs/jlibtorrent-1.2.11.0.jar
new file mode 100644
index 0000000..72f3073
Binary files /dev/null and b/torrentStream/libs/jlibtorrent-1.2.11.0.jar differ
diff --git a/torrentStream/libs/jlibtorrent-android-arm-1.2.11.0.jar b/torrentStream/libs/jlibtorrent-android-arm-1.2.11.0.jar
new file mode 100644
index 0000000..bdbdb89
Binary files /dev/null and b/torrentStream/libs/jlibtorrent-android-arm-1.2.11.0.jar differ
diff --git a/torrentStream/libs/jlibtorrent-android-arm64-1.2.11.0.jar b/torrentStream/libs/jlibtorrent-android-arm64-1.2.11.0.jar
new file mode 100644
index 0000000..1f962e6
Binary files /dev/null and b/torrentStream/libs/jlibtorrent-android-arm64-1.2.11.0.jar differ
diff --git a/torrentStream/libs/jlibtorrent-android-x86-1.2.11.0.jar b/torrentStream/libs/jlibtorrent-android-x86-1.2.11.0.jar
new file mode 100644
index 0000000..6c97276
Binary files /dev/null and b/torrentStream/libs/jlibtorrent-android-x86-1.2.11.0.jar differ
diff --git a/torrentStream/libs/jlibtorrent-android-x86_64-1.2.11.0.jar b/torrentStream/libs/jlibtorrent-android-x86_64-1.2.11.0.jar
new file mode 100644
index 0000000..6f6bf87
Binary files /dev/null and b/torrentStream/libs/jlibtorrent-android-x86_64-1.2.11.0.jar differ
diff --git a/torrentStream/proguard-rules.pro b/torrentStream/proguard-rules.pro
new file mode 100644
index 0000000..43926b7
--- /dev/null
+++ b/torrentStream/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/Sebastiaan/Development/Android/SDK/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/torrentStream/src/main/AndroidManifest.xml b/torrentStream/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..72d1c51
--- /dev/null
+++ b/torrentStream/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/StreamStatus.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/StreamStatus.java
new file mode 100644
index 0000000..f9d6760
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/StreamStatus.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+public class StreamStatus {
+ public final float progress;
+ public final int bufferProgress;
+ public final int seeds;
+ public final int peers;
+ public final int downloadSpeed;
+
+ StreamStatus(float progress, int bufferProgress, int seeds, int peers, int downloadSpeed) {
+ this.progress = progress;
+ this.bufferProgress = bufferProgress;
+ this.seeds = seeds;
+ this.peers = peers;
+ this.downloadSpeed = downloadSpeed;
+ }
+}
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/Torrent.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/Torrent.java
new file mode 100644
index 0000000..b8d4491
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/Torrent.java
@@ -0,0 +1,527 @@
+/*
+ * 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 com.frostwire.jlibtorrent.AlertListener;
+import com.frostwire.jlibtorrent.FileStorage;
+import com.frostwire.jlibtorrent.Priority;
+import com.frostwire.jlibtorrent.TorrentFlags;
+import com.frostwire.jlibtorrent.TorrentHandle;
+import com.frostwire.jlibtorrent.TorrentInfo;
+import com.frostwire.jlibtorrent.TorrentStatus;
+import com.frostwire.jlibtorrent.alerts.Alert;
+import com.frostwire.jlibtorrent.alerts.AlertType;
+import com.frostwire.jlibtorrent.alerts.BlockFinishedAlert;
+import com.frostwire.jlibtorrent.alerts.PieceFinishedAlert;
+import com.github.se_bastiaan.torrentstream.listeners.TorrentListener;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+public class Torrent implements AlertListener {
+
+ private final static Integer MAX_PREPARE_COUNT = 20;
+ private final static Integer MIN_PREPARE_COUNT = 2;
+ private final static Integer DEFAULT_PREPARE_COUNT = 5;
+ private final static Integer SEQUENTIAL_CONCURRENT_PIECES_COUNT = 5;
+ private final List> torrentStreamReferences;
+ private final TorrentHandle torrentHandle;
+ private final TorrentListener listener;
+ private final Long prepareSize;
+ private Integer piecesToPrepare;
+ private Integer lastPieceIndex;
+ private Integer firstPieceIndex;
+ private Integer selectedFileIndex = -1;
+ private Integer interestedPieceIndex = 0;
+ private Double prepareProgress = 0d;
+ private Double progressStep = 0d;
+ private List preparePieces;
+ private Boolean[] hasPieces;
+ private State state = State.RETRIEVING_META;
+
+ /**
+ * The constructor for a new Torrent
+ *
+ * First the largest file in the download is selected as the file for playback
+ *
+ * After setting this priority, the first and last index of the pieces that make up this file are determined.
+ * And last: amount of pieces that are needed for playback are calculated (needed for playback means: make up 10 megabyte of the file)
+ *
+ * @param torrentHandle jlibtorrent TorrentHandle
+ */
+ public Torrent(TorrentHandle torrentHandle, TorrentListener listener, Long prepareSize) {
+ this.torrentHandle = torrentHandle;
+ this.listener = listener;
+
+ this.prepareSize = prepareSize;
+
+ torrentStreamReferences = new ArrayList<>();
+
+ if (selectedFileIndex == -1) {
+ setLargestFile();
+ }
+
+ if (this.listener != null) {
+ this.listener.onStreamPrepared(this);
+ }
+ }
+
+ /**
+ * Reset piece priorities of selected file to normal
+ */
+ private void resetPriorities() {
+ Priority[] priorities = torrentHandle.piecePriorities();
+ for (int i = 0; i < priorities.length; i++) {
+ if (i >= firstPieceIndex && i <= lastPieceIndex) {
+ torrentHandle.piecePriority(i, Priority.NORMAL);
+ } else {
+ torrentHandle.piecePriority(i, Priority.IGNORE);
+ }
+ }
+ }
+
+ /**
+ * Get LibTorrent torrent handle of this torrent
+ *
+ * @return {@link TorrentHandle}
+ */
+ public TorrentHandle getTorrentHandle() {
+ return torrentHandle;
+ }
+
+ public File getVideoFile() {
+
+ return new File(torrentHandle.savePath() + "/" + torrentHandle.torrentFile().files().filePath(selectedFileIndex));
+ }
+
+ /**
+ * Get an InputStream for the video file.
+ * Read is be blocked until the requested piece(s) is downloaded.
+ *
+ * @return {@link InputStream}
+ */
+ public InputStream getVideoStream() throws FileNotFoundException {
+ File file = getVideoFile();
+ TorrentInputStream inputStream = new TorrentInputStream(this, new FileInputStream(file));
+ torrentStreamReferences.add(new WeakReference<>(inputStream));
+
+ return inputStream;
+ }
+
+ /**
+ * Get the location of the file that is being downloaded
+ *
+ * @return {@link File} The file location
+ */
+ public File getSaveLocation() {
+ return new File(torrentHandle.savePath() + "/" + torrentHandle.name());
+ }
+
+ /**
+ * Resume the torrent download
+ */
+ public void resume() {
+ torrentHandle.resume();
+ }
+
+ /**
+ * Pause the torrent download
+ */
+ public void pause() {
+ torrentHandle.pause();
+ }
+
+ /**
+ * Set the selected file index to the largest file in the torrent
+ */
+ public void setLargestFile() {
+ setSelectedFileIndex(-1);
+ }
+
+ /**
+ * Set the index of the file that should be downloaded
+ * If the given index is -1, then the largest file is chosen
+ *
+ * @param selectedFileIndex {@link Integer} Index of the file
+ */
+ public void setSelectedFileIndex(Integer selectedFileIndex) {
+ TorrentInfo torrentInfo = torrentHandle.torrentFile();
+ FileStorage fileStorage = torrentInfo.files();
+
+ if (selectedFileIndex == -1) {
+ long highestFileSize = 0;
+ int selectedFile = -1;
+ for (int i = 0; i < fileStorage.numFiles(); i++) {
+ long fileSize = fileStorage.fileSize(i);
+ if (highestFileSize < fileSize) {
+ highestFileSize = fileSize;
+ torrentHandle.filePriority(selectedFile, Priority.IGNORE);
+ selectedFile = i;
+ torrentHandle.filePriority(i, Priority.NORMAL);
+ } else {
+ torrentHandle.filePriority(i, Priority.IGNORE);
+ }
+ }
+ selectedFileIndex = selectedFile;
+ } else {
+ for (int i = 0; i < fileStorage.numFiles(); i++) {
+ if (i == selectedFileIndex) {
+ torrentHandle.filePriority(i, Priority.NORMAL);
+ } else {
+ torrentHandle.filePriority(i, Priority.IGNORE);
+ }
+ }
+ }
+ this.selectedFileIndex = selectedFileIndex;
+
+ Priority[] piecePriorities = torrentHandle.piecePriorities();
+ int firstPieceIndexLocal = -1;
+ int lastPieceIndexLocal = -1;
+ for (int i = 0; i < piecePriorities.length; i++) {
+ if (piecePriorities[i] != Priority.IGNORE) {
+ if (firstPieceIndexLocal == -1) {
+ firstPieceIndexLocal = i;
+ }
+ piecePriorities[i] = Priority.IGNORE;
+ } else {
+ if (firstPieceIndexLocal != -1 && lastPieceIndexLocal == -1) {
+ lastPieceIndexLocal = i - 1;
+ }
+ }
+ }
+
+ if (lastPieceIndexLocal == -1) {
+ lastPieceIndexLocal = piecePriorities.length - 1;
+ }
+ int pieceCount = lastPieceIndexLocal - firstPieceIndexLocal + 1;
+ int pieceLength = torrentHandle.torrentFile().pieceLength();
+ int activePieceCount;
+ if (pieceLength > 0) {
+ activePieceCount = (int) (prepareSize / pieceLength);
+ if (activePieceCount < MIN_PREPARE_COUNT) {
+ activePieceCount = MIN_PREPARE_COUNT;
+ } else if (activePieceCount > MAX_PREPARE_COUNT) {
+ activePieceCount = MAX_PREPARE_COUNT;
+ }
+ } else {
+ activePieceCount = DEFAULT_PREPARE_COUNT;
+ }
+
+ if (pieceCount < activePieceCount) {
+ activePieceCount = pieceCount / 2;
+ }
+
+ this.firstPieceIndex = firstPieceIndexLocal;
+ this.interestedPieceIndex = this.firstPieceIndex;
+ this.lastPieceIndex = lastPieceIndexLocal;
+ piecesToPrepare = activePieceCount;
+ }
+
+ /**
+ * Get the filenames of the files in the torrent
+ *
+ * @return {@link String[]}
+ */
+ public String[] getFileNames() {
+ FileStorage fileStorage = torrentHandle.torrentFile().files();
+ String[] fileNames = new String[fileStorage.numFiles()];
+ for (int i = 0; i < fileStorage.numFiles(); i++) {
+ fileNames[i] = fileStorage.fileName(i);
+ }
+ return fileNames;
+ }
+
+ /**
+ * Prepare torrent for playback. Prioritize the first {@code piecesToPrepare} pieces and the last {@code piecesToPrepare} pieces
+ * from {@code firstPieceIndex} and {@code lastPieceIndex}. Ignore all other pieces.
+ */
+ public void startDownload() {
+ if (state == State.STREAMING || state == State.STARTING) return;
+ state = State.STARTING;
+
+ List indices = new ArrayList<>();
+
+ Priority[] priorities = torrentHandle.piecePriorities();
+ for (int i = 0; i < priorities.length; i++) {
+ if (priorities[i] != Priority.IGNORE) {
+ torrentHandle.piecePriority(i, Priority.NORMAL);
+ }
+ }
+
+ for (int i = 0; i < piecesToPrepare; i++) {
+ indices.add(lastPieceIndex - i);
+ torrentHandle.piecePriority(lastPieceIndex - i, Priority.SEVEN);
+ torrentHandle.setPieceDeadline(lastPieceIndex - i, 1000);
+ }
+
+ for (int i = 0; i < piecesToPrepare; i++) {
+ indices.add(firstPieceIndex + i);
+ torrentHandle.piecePriority(firstPieceIndex + i, Priority.SEVEN);
+ torrentHandle.setPieceDeadline(firstPieceIndex + i, 1000);
+ }
+
+ preparePieces = indices;
+
+ hasPieces = new Boolean[lastPieceIndex - firstPieceIndex + 1];
+ Arrays.fill(hasPieces, false);
+
+ TorrentInfo torrentInfo = torrentHandle.torrentFile();
+ TorrentStatus status = torrentHandle.status();
+
+ double blockCount = indices.size() * torrentInfo.pieceLength() / status.blockSize();
+
+ progressStep = 100 / blockCount;
+
+ torrentStreamReferences.clear();
+
+ torrentHandle.resume();
+
+ listener.onStreamStarted(this);
+ }
+
+ /**
+ * Check if the piece that contains the specified bytes were downloaded already
+ *
+ * @param bytes The bytes you're interested in
+ * @return {@code true} if downloaded, {@code false} if not
+ */
+ public boolean hasBytes(long bytes) {
+ if (hasPieces == null) {
+ return false;
+ }
+
+ int pieceIndex = (int) (bytes / torrentHandle.torrentFile().pieceLength());
+ return hasPieces[pieceIndex];
+ }
+
+ /**
+ * Set the bytes of the selected file that you're interested in
+ * The piece of that specific offset is selected and that piece plus the 1 preceding and the 3 after it.
+ * These pieces will then be prioritised, which results in continuing the sequential download after that piece
+ *
+ * @param bytes The bytes you're interested in
+ */
+ public void setInterestedBytes(long bytes) {
+ if (hasPieces == null && bytes >= 0) {
+ return;
+ }
+
+ int pieceIndex = (int) (bytes / torrentHandle.torrentFile().pieceLength());
+ interestedPieceIndex = pieceIndex;
+ if (!hasPieces[pieceIndex] && torrentHandle.piecePriority(pieceIndex + firstPieceIndex) != Priority.SEVEN) {
+ interestedPieceIndex = pieceIndex;
+ int pieces = 5;
+ for (int i = pieceIndex; i < hasPieces.length; i++) {
+ // Set full priority to first found piece that is not confirmed finished
+ if (!hasPieces[i]) {
+ torrentHandle.piecePriority(i + firstPieceIndex, Priority.SEVEN);
+ torrentHandle.setPieceDeadline(i + firstPieceIndex, 1000);
+ pieces--;
+ if (pieces == 0) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks if the interesting pieces are downloaded already
+ *
+ * @return {@code true} if the 5 pieces that were selected using `setInterestedBytes` are all reported complete including the `nextPieces`, {@code false} if not
+ */
+ public boolean hasInterestedBytes(int nextPieces) {
+ for (int i = 0; i < 5 + nextPieces; i++) {
+ int index = interestedPieceIndex + i;
+ if (hasPieces.length <= index || index < 0) {
+ continue;
+ }
+
+ if (!hasPieces[interestedPieceIndex + i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks if the interesting pieces are downloaded already
+ *
+ * @return {@code true} if the 5 pieces that were selected using `setInterestedBytes` are all reported complete, {@code false} if not
+ */
+ public boolean hasInterestedBytes() {
+ return hasInterestedBytes(5);
+ }
+
+ /**
+ * Get the index of the piece we're currently interested in
+ *
+ * @return Interested piece index
+ */
+ public int getInterestedPieceIndex() {
+ return interestedPieceIndex;
+ }
+
+ /**
+ * Get amount of pieces to prepare
+ *
+ * @return Amount of pieces to prepare
+ */
+ public Integer getPiecesToPrepare() {
+ return piecesToPrepare;
+ }
+
+ /**
+ * Start sequential mode downloading
+ */
+ private void startSequentialMode() {
+ resetPriorities();
+
+ if (hasPieces == null) {
+ torrentHandle.setFlags(torrentHandle.flags().and_(TorrentFlags.SEQUENTIAL_DOWNLOAD));
+ } else {
+ for (int i = firstPieceIndex + piecesToPrepare; i < firstPieceIndex + piecesToPrepare + SEQUENTIAL_CONCURRENT_PIECES_COUNT; i++) {
+ torrentHandle.piecePriority(i, Priority.SEVEN);
+ torrentHandle.setPieceDeadline(i, 1000);
+ }
+ }
+ }
+
+ /**
+ * Get current torrent state
+ *
+ * @return {@link State}
+ */
+ public State getState() {
+ return state;
+ }
+
+ /**
+ * Piece finished
+ *
+ * @param alert
+ */
+ private void pieceFinished(PieceFinishedAlert alert) {
+ if (state == State.STREAMING && hasPieces != null) {
+ int pieceIndex = alert.pieceIndex() - firstPieceIndex;
+ hasPieces[pieceIndex] = true;
+
+ if (pieceIndex >= interestedPieceIndex) {
+ for (int i = pieceIndex; i < hasPieces.length; i++) {
+ // Set full priority to first found piece that is not confirmed finished
+ if (!hasPieces[i]) {
+ torrentHandle.piecePriority(i + firstPieceIndex, Priority.SEVEN);
+ torrentHandle.setPieceDeadline(i + firstPieceIndex, 1000);
+ break;
+ }
+ }
+ }
+ } else {
+ Iterator piecesIterator = preparePieces.iterator();
+ while (piecesIterator.hasNext()) {
+ int index = piecesIterator.next();
+ if (index == alert.pieceIndex()) {
+ piecesIterator.remove();
+ }
+ }
+
+ if (hasPieces != null) {
+ hasPieces[alert.pieceIndex() - firstPieceIndex] = true;
+ }
+
+ if (preparePieces.size() == 0) {
+ startSequentialMode();
+
+ prepareProgress = 100d;
+ sendStreamProgress();
+ state = State.STREAMING;
+
+ if (listener != null) {
+ listener.onStreamReady(this);
+ }
+ }
+ }
+ }
+
+ private void blockFinished(BlockFinishedAlert alert) {
+ for (Integer index : preparePieces) {
+ if (index == alert.pieceIndex()) {
+ prepareProgress += progressStep;
+ break;
+ }
+ }
+
+ sendStreamProgress();
+ }
+
+ private void sendStreamProgress() {
+ TorrentStatus status = torrentHandle.status();
+ float progress = status.progress() * 100;
+ int seeds = status.numSeeds();
+ int peers = status.numPeers();
+ int downloadSpeed = status.downloadPayloadRate();
+
+ if (listener != null && prepareProgress >= 1) {
+ listener.onStreamProgress(this, new StreamStatus(progress, prepareProgress.intValue(), seeds, peers, downloadSpeed));
+ }
+ }
+
+ @Override
+ public int[] types() {
+ return new int[]{
+ AlertType.PIECE_FINISHED.swig(),
+ AlertType.BLOCK_FINISHED.swig()
+ };
+ }
+
+ @Override
+ public void alert(Alert> alert) {
+ switch (alert.type()) {
+ case PIECE_FINISHED:
+ pieceFinished((PieceFinishedAlert) alert);
+ break;
+ case BLOCK_FINISHED:
+ blockFinished((BlockFinishedAlert) alert);
+ break;
+ default:
+ break;
+ }
+
+ Iterator> i = torrentStreamReferences.iterator();
+
+ while (i.hasNext()) {
+ WeakReference reference = i.next();
+ TorrentInputStream inputStream = reference.get();
+
+ if (inputStream == null) {
+ i.remove();
+ continue;
+ }
+
+ inputStream.alert(alert);
+ }
+ }
+
+ public enum State {UNKNOWN, RETRIEVING_META, STARTING, STREAMING}
+}
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/TorrentInputStream.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/TorrentInputStream.java
new file mode 100644
index 0000000..6ce04ed
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/TorrentInputStream.java
@@ -0,0 +1,116 @@
+package com.github.se_bastiaan.torrentstream;
+
+import com.frostwire.jlibtorrent.AlertListener;
+import com.frostwire.jlibtorrent.alerts.Alert;
+import com.frostwire.jlibtorrent.alerts.AlertType;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+class TorrentInputStream extends FilterInputStream implements AlertListener {
+ private final Torrent torrent;
+ private boolean stopped;
+ private long location;
+
+ TorrentInputStream(Torrent torrent, InputStream inputStream) {
+ super(inputStream);
+
+ this.torrent = torrent;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ synchronized (this) {
+ stopped = true;
+ notifyAll();
+ }
+
+ super.finalize();
+ }
+
+ private synchronized boolean waitForPiece(long offset) {
+ while (!Thread.currentThread().isInterrupted() && !stopped) {
+ try {
+ if (torrent.hasBytes(offset)) {
+ return true;
+ }
+
+ wait();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public synchronized int read() throws IOException {
+ if (!waitForPiece(location)) {
+ return -1;
+ }
+
+ location++;
+
+ return super.read();
+ }
+
+ @Override
+ public synchronized int read(byte[] buffer, int offset, int length) throws IOException {
+ int pieceLength = torrent.getTorrentHandle().torrentFile().pieceLength();
+
+ for (int i = 0; i < length; i += pieceLength) {
+ if (!waitForPiece(location + i)) {
+ return -1;
+ }
+ }
+
+ location += length;
+
+ return super.read(buffer, offset, length);
+ }
+
+ @Override
+ public void close() throws IOException {
+ synchronized (this) {
+ stopped = true;
+ notifyAll();
+ }
+
+ super.close();
+ }
+
+ @Override
+ public synchronized long skip(long n) throws IOException {
+ location += n;
+ return super.skip(n);
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ private synchronized void pieceFinished() {
+ notifyAll();
+ }
+
+ @Override
+ public int[] types() {
+ return new int[]{
+ AlertType.PIECE_FINISHED.swig(),
+ };
+ }
+
+ @Override
+ public void alert(Alert> alert) {
+ switch (alert.type()) {
+ case PIECE_FINISHED:
+ pieceFinished();
+ break;
+ default:
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/TorrentOptions.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/TorrentOptions.java
new file mode 100644
index 0000000..adf9c16
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/TorrentOptions.java
@@ -0,0 +1,151 @@
+/*
+ * 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 java.io.File;
+
+public final class TorrentOptions {
+
+ String saveLocation = "/";
+ String proxyHost;
+ String proxyUsername;
+ String proxyPassword;
+ String peerFingerprint;
+ Integer maxDownloadSpeed = 0;
+ Integer maxUploadSpeed = 0;
+ Integer maxConnections = 200;
+ Integer maxDht = 88;
+ Integer listeningPort = -1;
+ Boolean removeFiles = false;
+ Boolean anonymousMode = false;
+ Boolean autoDownload = true;
+ Long prepareSize = 15 * 1024L * 1024L;
+
+ private TorrentOptions() {
+ // Unused
+ }
+
+ private TorrentOptions(TorrentOptions torrentOptions) {
+ this.saveLocation = torrentOptions.saveLocation;
+ this.proxyHost = torrentOptions.proxyHost;
+ this.proxyUsername = torrentOptions.proxyUsername;
+ this.proxyPassword = torrentOptions.proxyPassword;
+ this.peerFingerprint = torrentOptions.peerFingerprint;
+ this.maxDownloadSpeed = torrentOptions.maxDownloadSpeed;
+ this.maxUploadSpeed = torrentOptions.maxUploadSpeed;
+ this.maxConnections = torrentOptions.maxConnections;
+ this.maxDht = torrentOptions.maxDht;
+ this.listeningPort = torrentOptions.listeningPort;
+ this.removeFiles = torrentOptions.removeFiles;
+ this.anonymousMode = torrentOptions.anonymousMode;
+ this.autoDownload = torrentOptions.autoDownload;
+ this.prepareSize = torrentOptions.prepareSize;
+ }
+
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ public static class Builder {
+
+ private TorrentOptions torrentOptions;
+
+ public Builder() {
+ torrentOptions = new TorrentOptions();
+ }
+
+ private Builder(TorrentOptions torrentOptions) {
+ torrentOptions = new TorrentOptions(torrentOptions);
+ }
+
+ public Builder saveLocation(String saveLocation) {
+ torrentOptions.saveLocation = saveLocation;
+ return this;
+ }
+
+ public Builder saveLocation(File saveLocation) {
+ torrentOptions.saveLocation = saveLocation.getAbsolutePath();
+ return this;
+ }
+
+ public Builder maxUploadSpeed(Integer maxUploadSpeed) {
+ torrentOptions.maxUploadSpeed = maxUploadSpeed;
+ return this;
+ }
+
+ public Builder maxDownloadSpeed(Integer maxDownloadSpeed) {
+ torrentOptions.maxDownloadSpeed = maxDownloadSpeed;
+ return this;
+ }
+
+ public Builder maxConnections(Integer maxConnections) {
+ torrentOptions.maxConnections = maxConnections;
+ return this;
+ }
+
+ public Builder maxActiveDHT(Integer maxActiveDHT) {
+ torrentOptions.maxDht = maxActiveDHT;
+ return this;
+ }
+
+ public Builder removeFilesAfterStop(Boolean b) {
+ torrentOptions.removeFiles = b;
+ return this;
+ }
+
+ public Builder prepareSize(Long prepareSize) {
+ torrentOptions.prepareSize = prepareSize;
+ return this;
+ }
+
+ public Builder listeningPort(Integer port) {
+ torrentOptions.listeningPort = port;
+ return this;
+ }
+
+ public Builder proxy(String host, String username, String password) {
+ torrentOptions.proxyHost = host;
+ torrentOptions.proxyUsername = username;
+ torrentOptions.proxyPassword = password;
+ return this;
+ }
+
+ public Builder peerFingerprint(String peerId) {
+ torrentOptions.peerFingerprint = peerId;
+ torrentOptions.anonymousMode = false;
+ return this;
+ }
+
+ public Builder anonymousMode(Boolean enable) {
+ torrentOptions.anonymousMode = enable;
+ if (torrentOptions.anonymousMode)
+ torrentOptions.peerFingerprint = null;
+ return this;
+ }
+
+ public Builder autoDownload(Boolean enable) {
+ torrentOptions.autoDownload = enable;
+ return this;
+ }
+
+ public TorrentOptions build() {
+ return torrentOptions;
+ }
+
+ }
+
+}
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/TorrentStream.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/TorrentStream.java
new file mode 100644
index 0000000..9d3f3a4
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/TorrentStream.java
@@ -0,0 +1,542 @@
+/*
+ * 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, 30000);
+ 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);
+ }
+ });
+ }
+ }
+ }
+
+}
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/exceptions/DirectoryModifyException.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/exceptions/DirectoryModifyException.java
new file mode 100644
index 0000000..00803f8
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/exceptions/DirectoryModifyException.java
@@ -0,0 +1,25 @@
+/*
+ * 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.exceptions;
+
+public class DirectoryModifyException extends Exception {
+
+ public DirectoryModifyException() {
+ super("Could not create or delete save directory");
+ }
+
+}
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/exceptions/NotInitializedException.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/exceptions/NotInitializedException.java
new file mode 100644
index 0000000..da3c5e3
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/exceptions/NotInitializedException.java
@@ -0,0 +1,25 @@
+/*
+ * 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.exceptions;
+
+public class NotInitializedException extends Exception {
+
+ public NotInitializedException() {
+ super("TorrentStreamer is not initialized. Call init() first before getting an instance.");
+ }
+
+}
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/exceptions/TorrentInfoException.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/exceptions/TorrentInfoException.java
new file mode 100644
index 0000000..4d9431d
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/exceptions/TorrentInfoException.java
@@ -0,0 +1,25 @@
+/*
+ * 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.exceptions;
+
+public class TorrentInfoException extends Exception {
+
+ public TorrentInfoException(Throwable cause) {
+ super("No torrent info could be found or read", cause);
+ }
+
+}
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/listeners/DHTStatsAlertListener.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/listeners/DHTStatsAlertListener.java
new file mode 100644
index 0000000..4e0e04a
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/listeners/DHTStatsAlertListener.java
@@ -0,0 +1,57 @@
+/*
+ *
+ * * This file is part of TorrentStreamer-Android.
+ * *
+ * * TorrentStreamer-Android is free software: you can redistribute it and/or modify
+ * * it under the terms of the GNU Lesser General Public License as published by
+ * * the Free Software Foundation, either version 3 of the License, or
+ * * (at your option) any later version.
+ * *
+ * * TorrentStreamer-Android 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 Lesser General Public License for more details.
+ * *
+ * * You should have received a copy of the GNU Lesser General Public License
+ * * along with TorrentStreamer-Android. If not, see .
+ *
+ */
+
+package com.github.se_bastiaan.torrentstream.listeners;
+
+import com.frostwire.jlibtorrent.AlertListener;
+import com.frostwire.jlibtorrent.DhtRoutingBucket;
+import com.frostwire.jlibtorrent.alerts.Alert;
+import com.frostwire.jlibtorrent.alerts.AlertType;
+import com.frostwire.jlibtorrent.alerts.DhtStatsAlert;
+
+import java.util.ArrayList;
+
+public abstract class DHTStatsAlertListener implements AlertListener {
+ @Override
+ public int[] types() {
+ return new int[]{AlertType.DHT_STATS.swig()};
+ }
+
+ public void alert(Alert> alert) {
+ if (alert instanceof DhtStatsAlert) {
+ DhtStatsAlert dhtAlert = (DhtStatsAlert) alert;
+ stats(countTotalDHTNodes(dhtAlert));
+ }
+ }
+
+ public abstract void stats(int totalDhtNodes);
+
+ private int countTotalDHTNodes(DhtStatsAlert alert) {
+ final ArrayList routingTable = alert.routingTable();
+
+ int totalNodes = 0;
+ if (routingTable != null) {
+ for (DhtRoutingBucket bucket : routingTable) {
+ totalNodes += bucket.numNodes();
+ }
+ }
+
+ return totalNodes;
+ }
+}
\ No newline at end of file
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/listeners/TorrentAddedAlertListener.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/listeners/TorrentAddedAlertListener.java
new file mode 100644
index 0000000..19e0773
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/listeners/TorrentAddedAlertListener.java
@@ -0,0 +1,42 @@
+/*
+ * 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.listeners;
+
+import com.frostwire.jlibtorrent.AlertListener;
+import com.frostwire.jlibtorrent.alerts.AddTorrentAlert;
+import com.frostwire.jlibtorrent.alerts.Alert;
+import com.frostwire.jlibtorrent.alerts.AlertType;
+
+public abstract class TorrentAddedAlertListener implements AlertListener {
+ @Override
+ public int[] types() {
+ return new int[]{AlertType.ADD_TORRENT.swig()};
+ }
+
+ @Override
+ public void alert(Alert> alert) {
+ switch (alert.type()) {
+ case ADD_TORRENT:
+ torrentAdded((AddTorrentAlert) alert);
+ break;
+ default:
+ break;
+ }
+ }
+
+ public abstract void torrentAdded(AddTorrentAlert alert);
+}
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/listeners/TorrentListener.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/listeners/TorrentListener.java
new file mode 100644
index 0000000..e099daa
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/listeners/TorrentListener.java
@@ -0,0 +1,34 @@
+/*
+ * 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.listeners;
+
+import com.github.se_bastiaan.torrentstream.StreamStatus;
+import com.github.se_bastiaan.torrentstream.Torrent;
+
+public interface TorrentListener {
+ void onStreamPrepared(Torrent torrent);
+
+ void onStreamStarted(Torrent torrent);
+
+ void onStreamError(Torrent torrent, Exception e);
+
+ void onStreamReady(Torrent torrent);
+
+ void onStreamProgress(Torrent torrent, StreamStatus status);
+
+ void onStreamStopped();
+}
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/utils/FileUtils.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/utils/FileUtils.java
new file mode 100644
index 0000000..7158239
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/utils/FileUtils.java
@@ -0,0 +1,48 @@
+/*
+ *
+ * * This file is part of TorrentStreamer-Android.
+ * *
+ * * TorrentStreamer-Android is free software: you can redistribute it and/or modify
+ * * it under the terms of the GNU Lesser General Public License as published by
+ * * the Free Software Foundation, either version 3 of the License, or
+ * * (at your option) any later version.
+ * *
+ * * TorrentStreamer-Android 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 Lesser General Public License for more details.
+ * *
+ * * You should have received a copy of the GNU Lesser General Public License
+ * * along with TorrentStreamer-Android. If not, see .
+ *
+ */
+
+package com.github.se_bastiaan.torrentstream.utils;
+
+import java.io.File;
+
+public final class FileUtils {
+
+ private FileUtils() throws InstantiationException {
+ throw new InstantiationException("This class is not created for instantiation");
+ }
+
+ /**
+ * Delete every item below the File location
+ *
+ * @param file Location
+ * @return {@code true} when successful delete
+ */
+ public static boolean recursiveDelete(File file) {
+ if (file.isDirectory()) {
+ String[] children = file.list();
+ if (children == null) return false;
+ for (String child : children) {
+ recursiveDelete(new File(file, child));
+ }
+ }
+
+ return file.delete();
+ }
+
+}
diff --git a/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/utils/ThreadUtils.java b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/utils/ThreadUtils.java
new file mode 100644
index 0000000..6c27bb1
--- /dev/null
+++ b/torrentStream/src/main/java/com/github/se_bastiaan/torrentstream/utils/ThreadUtils.java
@@ -0,0 +1,42 @@
+/*
+ *
+ * * This file is part of TorrentStreamer-Android.
+ * *
+ * * TorrentStreamer-Android is free software: you can redistribute it and/or modify
+ * * it under the terms of the GNU Lesser General Public License as published by
+ * * the Free Software Foundation, either version 3 of the License, or
+ * * (at your option) any later version.
+ * *
+ * * TorrentStreamer-Android 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 Lesser General Public License for more details.
+ * *
+ * * You should have received a copy of the GNU Lesser General Public License
+ * * along with TorrentStreamer-Android. If not, see .
+ *
+ */
+
+package com.github.se_bastiaan.torrentstream.utils;
+
+import android.os.Handler;
+import android.os.Looper;
+
+public final class ThreadUtils {
+
+ private ThreadUtils() throws InstantiationException {
+ throw new InstantiationException("This class is not created for instantiation");
+ }
+
+ /**
+ * Execute the given {@link Runnable} on the ui thread.
+ *
+ * @param runnable The runnable to execute.
+ */
+ public static void runOnUiThread(Runnable runnable) {
+ Thread uiThread = Looper.getMainLooper().getThread();
+ if (Thread.currentThread() != uiThread) new Handler(Looper.getMainLooper()).post(runnable);
+ else runnable.run();
+ }
+
+}
\ No newline at end of file