Skip to content

Commit 9aa3b0e

Browse files
committed
added torrent support
1 parent 7194391 commit 9aa3b0e

File tree

13 files changed

+1003
-113
lines changed

13 files changed

+1003
-113
lines changed

app/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,9 @@ dependencies {
222222
^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API
223223
Level 25 or Less. */
224224

225+
// torrent support
226+
implementation("com.github.recloudstream:Aria2cStream:0.0.3")
227+
225228
// Downloading & Networking
226229
implementation("androidx.work:work-runtime:2.9.0")
227230
implementation("androidx.work:work-runtime-ktx:2.9.0")

app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ import com.lagradost.cloudstream3.actions.VideoClickActionHolder
3535
import com.lagradost.cloudstream3.databinding.ToastBinding
3636
import com.lagradost.cloudstream3.mvvm.logError
3737
import com.lagradost.cloudstream3.ui.player.PlayerEventType
38+
import com.lagradost.cloudstream3.ui.player.Torrent
3839
import com.lagradost.cloudstream3.utils.UiText
3940
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
4041
import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
42+
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
4143
import com.lagradost.cloudstream3.utils.Event
4244
import com.lagradost.cloudstream3.utils.UIHelper
4345
import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
@@ -203,6 +205,7 @@ object CommonActivity {
203205

204206
fun init(act: Activity) {
205207
setActivityInstance(act)
208+
ioSafe { Torrent.deleteAllOldFiles() }
206209

207210
val componentActivity = activity as? ComponentActivity ?: return
208211

app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,12 +384,20 @@ abstract class AbstractPlayerFragment(
384384
// }
385385
//}
386386

387+
open fun onDownload(event : DownloadEvent) = Unit
388+
387389
/** This receives the events from the player, if you want to append functionality you do it here,
388390
* do note that this only receives events for UI changes,
389391
* and returning early WONT stop it from changing in eg the player time or pause status */
390392
open fun mainCallback(event: PlayerEvent) {
391-
Log.i(TAG, "Handle event: $event")
393+
// we don't want to spam DownloadEvent
394+
if(event !is DownloadEvent) {
395+
Log.i(TAG, "Handle event: $event")
396+
}
392397
when (event) {
398+
is DownloadEvent -> {
399+
onDownload(event)
400+
}
393401
is ResizedEvent -> {
394402
playerDimensionsLoaded(event.width, event.height)
395403
}

app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt

Lines changed: 177 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ package com.lagradost.cloudstream3.ui.player
22

33
import android.annotation.SuppressLint
44
import android.content.Context
5+
import android.content.DialogInterface
56
import android.graphics.Bitmap
67
import android.net.Uri
78
import android.os.Handler
89
import android.os.Looper
910
import android.util.Log
1011
import android.util.Rational
1112
import android.widget.FrameLayout
13+
import androidx.annotation.MainThread
1214
import androidx.annotation.OptIn
15+
import androidx.appcompat.app.AlertDialog
1316
import androidx.media3.common.C.TIME_UNSET
1417
import androidx.media3.common.C.TRACK_TYPE_AUDIO
1518
import androidx.media3.common.C.TRACK_TYPE_TEXT
@@ -56,21 +59,30 @@ import androidx.preference.PreferenceManager
5659
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
5760
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
5861
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
62+
import com.lagradost.cloudstream3.ErrorLoadingException
5963
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
64+
import com.lagradost.cloudstream3.R
6065
import com.lagradost.cloudstream3.USER_AGENT
6166
import com.lagradost.cloudstream3.app
6267
import com.lagradost.cloudstream3.mvvm.debugAssert
6368
import com.lagradost.cloudstream3.mvvm.logError
6469
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
6570
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
6671
import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData
72+
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
73+
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
74+
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
6775
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
6876
import com.lagradost.cloudstream3.utils.DrmExtractorLink
6977
import com.lagradost.cloudstream3.utils.EpisodeSkip
7078
import com.lagradost.cloudstream3.utils.ExtractorLink
7179
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
7280
import com.lagradost.cloudstream3.utils.ExtractorLinkType
7381
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
82+
import com.lagradost.fetchbutton.aria2c.Aria2Starter
83+
import com.lagradost.fetchbutton.aria2c.DownloadListener
84+
import com.lagradost.fetchbutton.aria2c.DownloadStatusTell
85+
import kotlinx.coroutines.delay
7486
import java.io.File
7587
import java.util.UUID
7688
import javax.net.ssl.HttpsURLConnection
@@ -244,6 +256,7 @@ class CS3IPlayer : IPlayer {
244256
gen.clear(sameEpisode)
245257
}
246258
}
259+
247260
loadOnlinePlayer(context, link)
248261
} else if (data != null) {
249262
(imageGenerator as? PreviewGenerator)?.let { gen ->
@@ -530,7 +543,7 @@ class CS3IPlayer : IPlayer {
530543

531544
private fun releasePlayer(saveTime: Boolean = true) {
532545
Log.i(TAG, "releasePlayer")
533-
546+
eventLooperIndex += 1
534547
if (saveTime)
535548
updatedTime()
536549

@@ -567,6 +580,8 @@ class CS3IPlayer : IPlayer {
567580

568581
override fun release() {
569582
imageGenerator.release()
583+
Torrent.release()
584+
Torrent.deleteAllOldFiles()
570585
releasePlayer()
571586
}
572587

@@ -950,6 +965,35 @@ class CS3IPlayer : IPlayer {
950965
}
951966
}
952967

968+
// we want to push metadata when loading torrents, so we just set up a looper that loops until
969+
// the index changes, this way only 1 looper is active at a time, and modifying eventLooperIndex
970+
// will kill any active loopers
971+
private var eventLooperIndex = 0
972+
private fun torrentEventLooper() = ioSafe {
973+
eventLooperIndex += 1
974+
val currentIndex = eventLooperIndex
975+
while (currentIndex == eventLooperIndex) {
976+
DownloadListener.sessionIdToGid[activeTorrentRequest?.requestId]?.let { gid ->
977+
val metadata = DownloadListener.getInfo(gid)
978+
event(
979+
DownloadEvent(
980+
downloadedBytes = metadata.downloadedLength,
981+
downloadSpeed = metadata.downloadSpeed,
982+
totalBytes = metadata.totalLength,
983+
connections = metadata.items.sumOf { it.connections }
984+
)
985+
)
986+
when (metadata.status) {
987+
DownloadStatusTell.Waiting -> delay(500)
988+
DownloadStatusTell.Paused -> delay(1000)
989+
DownloadStatusTell.Error, DownloadStatusTell.Removed, DownloadStatusTell.Complete -> return@ioSafe
990+
null, DownloadStatusTell.Active -> Unit
991+
}
992+
}
993+
delay(100)
994+
}
995+
}
996+
953997
private fun loadExo(
954998
context: Context,
955999
mediaSlices: List<MediaItemSlice>,
@@ -993,6 +1037,13 @@ class CS3IPlayer : IPlayer {
9931037
isPlaying = exo.isPlaying
9941038
}
9951039

1040+
// we want to avoid an empty exoplayer from sending events
1041+
// this is because we need PlayerAttachedEvent to be called to render the UI
1042+
// but don't really want the rest like Player.STATE_ENDED calling next episode
1043+
if(mediaSlices.isEmpty() && subSources.isEmpty()) {
1044+
return
1045+
}
1046+
9961047
exoPlayer?.addListener(object : Player.Listener {
9971048
override fun onTracksChanged(tracks: Tracks) {
9981049
normalSafeApiCall {
@@ -1079,6 +1130,50 @@ class CS3IPlayer : IPlayer {
10791130
}
10801131

10811132
override fun onPlayerError(error: PlaybackException) {
1133+
val request = activeTorrentRequest
1134+
1135+
// if we are loading an torrent, then we will get these errors, in that case
1136+
// we just treat it as buffering
1137+
if (request != null &&
1138+
(error.errorCode == PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE
1139+
|| error.errorCode == PlaybackException.ERROR_CODE_IO_UNSPECIFIED
1140+
|| error.errorCode == PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED
1141+
|| error.errorCode == PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED
1142+
|| error.errorCode == PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED
1143+
|| error.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED
1144+
|| error.errorCode == PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND)
1145+
) {
1146+
val gid = DownloadListener.sessionIdToGid[request.requestId]
1147+
if (gid == null) {
1148+
event(ErrorEvent(error))
1149+
super.onPlayerError(error)
1150+
return
1151+
}
1152+
1153+
event(
1154+
StatusEvent(
1155+
wasPlaying = CSPlayerLoading.IsPlaying,
1156+
isPlaying = CSPlayerLoading.IsBuffering
1157+
)
1158+
)
1159+
1160+
// we have manually deleted the file without notifying the Aria2 instance
1161+
if (error.errorCode == PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND) {
1162+
Aria2Starter.delete(gid, request.requestId)
1163+
loadOnlinePlayer(context, request.request)
1164+
return
1165+
}
1166+
1167+
// give information when buffering, and after a set timeout we run again
1168+
Handler(Looper.myLooper() ?: Looper.getMainLooper()).postDelayed({
1169+
// if we have released the player while it is waiting, then do nothing
1170+
if(exoPlayer == null) return@postDelayed
1171+
playbackPosition = exoPlayer?.currentPosition ?: 0L
1172+
loadOnlinePlayer(context, request.request, retry = true)
1173+
}, 3000)
1174+
return
1175+
}
1176+
10821177
// If the Network fails then ignore the exception if the duration is set.
10831178
// This is to switch mirrors automatically if the stream has not been fetched, but
10841179
// allow playing the buffer without internet as then the duration is fetched.
@@ -1308,10 +1403,90 @@ class CS3IPlayer : IPlayer {
13081403
return exoPlayer != null
13091404
}
13101405

1406+
var activeTorrentRequest: TorrentRequest? = null
1407+
@MainThread
1408+
private fun loadTorrent(context: Context, link: ExtractorLink) {
1409+
ioSafe {
1410+
// we check exoPlayer a lot here, and that is because we don't want to load exo after
1411+
// the user has left the player, in the case that the user click back when this is
1412+
// happening
1413+
try {
1414+
if(exoPlayer == null) return@ioSafe
1415+
val request = Torrent.loadTorrent(link, eventHandler)
1416+
if(exoPlayer == null) return@ioSafe
1417+
activeTorrentRequest = request
1418+
runOnMainThread {
1419+
if(exoPlayer == null) return@runOnMainThread
1420+
releasePlayer()
1421+
loadOfflinePlayer(context, request.data)
1422+
torrentEventLooper()
1423+
}
1424+
} catch (t: Throwable) {
1425+
event(ErrorEvent(t))
1426+
}
1427+
}
1428+
}
13111429
@SuppressLint("UnsafeOptInUsageError")
1312-
private fun loadOnlinePlayer(context: Context, link: ExtractorLink) {
1430+
@MainThread
1431+
private fun loadOnlinePlayer(context: Context, link: ExtractorLink, retry : Boolean = false) {
13131432
Log.i(TAG, "loadOnlinePlayer $link")
13141433
try {
1434+
activeTorrentRequest = null
1435+
val mime = when (link.type) {
1436+
ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
1437+
ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
1438+
ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4
1439+
ExtractorLinkType.TORRENT, ExtractorLinkType.MAGNET -> {
1440+
// load the initial UI, we require an exoPlayer to be alive
1441+
if(!retry) {
1442+
// this causes a *bug* that restarts all torrents from 0
1443+
// but I would call this a feature
1444+
releasePlayer()
1445+
loadExo(context, listOf(), listOf(),null)
1446+
}
1447+
event(
1448+
StatusEvent(
1449+
wasPlaying = CSPlayerLoading.IsPlaying,
1450+
isPlaying = CSPlayerLoading.IsBuffering
1451+
)
1452+
)
1453+
1454+
if(Torrent.hasAcceptedTorrentForThisSession == true) {
1455+
loadTorrent(context,link)
1456+
return
1457+
}
1458+
1459+
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
1460+
1461+
val dialogClickListener =
1462+
DialogInterface.OnClickListener { _, which ->
1463+
when (which) {
1464+
DialogInterface.BUTTON_POSITIVE -> {
1465+
Torrent.hasAcceptedTorrentForThisSession = true
1466+
loadTorrent(context, link)
1467+
}
1468+
1469+
DialogInterface.BUTTON_NEGATIVE -> {
1470+
Torrent.hasAcceptedTorrentForThisSession = false
1471+
event(ErrorEvent(ErrorLoadingException("Not accepted torrent")))
1472+
}
1473+
}
1474+
}
1475+
1476+
builder.setTitle(R.string.play_torrent_button)
1477+
.setMessage(R.string.torrent_info)
1478+
// Ensure that the user will not accidentally start a torrent session.
1479+
.setCancelable(false).setOnCancelListener {
1480+
event(ErrorEvent(ErrorLoadingException("Not accepted torrent")))
1481+
}
1482+
.setPositiveButton(R.string.ok, dialogClickListener)
1483+
.setNegativeButton(R.string.go_back, dialogClickListener)
1484+
.show().setDefaultFocus()
1485+
1486+
return
1487+
}
1488+
}
1489+
13151490
currentLink = link
13161491

13171492
if (ignoreSSL) {
@@ -1325,14 +1500,6 @@ class CS3IPlayer : IPlayer {
13251500
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
13261501
}
13271502

1328-
val mime = when (link.type) {
1329-
ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
1330-
ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
1331-
ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4
1332-
ExtractorLinkType.TORRENT -> throw IllegalArgumentException("No torrent support")
1333-
ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support")
1334-
}
1335-
13361503

13371504
val mediaItems = when (link) {
13381505
is ExtractorLinkPlayList -> link.playlist.map {

app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
303303
playerFfwdHolder.startAnimation(fadeAnimation)
304304
playerRewHolder.startAnimation(fadeAnimation)
305305
playerPausePlay.startAnimation(fadeAnimation)
306+
downloadBothHeader.startAnimation(fadeAnimation)
306307

307308
/*if (isBuffering) {
308309
player_pause_play?.isVisible = false
@@ -730,6 +731,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
730731
playerPausePlay.startAnimation(fadeAnimation)
731732
playerFfwdHolder.startAnimation(fadeAnimation)
732733
playerRewHolder.startAnimation(fadeAnimation)
734+
downloadBothHeader.startAnimation(fadeAnimation)
733735

734736
//if (hasEpisodes)
735737
// player_episodes_button?.startAnimation(fadeAnimation)
@@ -1311,7 +1313,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
13111313
// if nothing has loaded these buttons should not be visible
13121314
playerBinding?.apply {
13131315
playerSkipEpisode.isVisible = false
1314-
playerGoForward.isVisible = false
1316+
playerGoForwardRoot.isVisible = false
13151317
playerTracksBtt.isVisible = false
13161318
playerSkipOp.isVisible = false
13171319
shadowOverlay.isVisible = false
@@ -1520,7 +1522,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
15201522
mapOf(
15211523
playerGoBack to playerGoBackText,
15221524
playerRestart to playerRestartText,
1523-
playerGoForward to playerGoForwardText
1525+
playerGoForward to playerGoForwardText,
1526+
downloadHeaderToggle to downloadHeaderToggleText,
15241527
).forEach { (button, text) ->
15251528
button.setOnFocusChangeListener { _, hasFocus ->
15261529
if (!hasFocus) {

0 commit comments

Comments
 (0)