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