diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index 56e13659..fea36ed5 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -17,6 +17,7 @@ import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.states.* +import com.futo.platformplayer.views.Loader import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay @@ -28,6 +29,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.lang.IllegalStateException class UISlideOverlays { companion object { @@ -43,7 +45,7 @@ class UISlideOverlays { menu.show(); } - fun showDownloadVideoOverlay(contentResolver: ContentResolver, video: IPlatformVideoDetails, container: ViewGroup): SlideUpMenuOverlay? { + fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? { val items = arrayListOf(); var menu: SlideUpMenuOverlay? = null; @@ -121,18 +123,21 @@ class UISlideOverlays { if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?; } - items.add(SlideUpMenuGroup(container.context, "Subtitles", subtitleSources, subtitleSources - .map { - SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, { - if (selectedSubtitle == it) { - selectedSubtitle = null; - menu?.selectOption(subtitleSources, null); - } else { - selectedSubtitle = it; - menu?.selectOption(subtitleSources, it); - } - }, false); - })); + //ContentResolver is required for subtitles.. + if(contentResolver != null) { + items.add(SlideUpMenuGroup(container.context, "Subtitles", subtitleSources, subtitleSources + .map { + SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, { + if (selectedSubtitle == it) { + selectedSubtitle = null; + menu?.selectOption(subtitleSources, null); + } else { + selectedSubtitle = it; + menu?.selectOption(subtitleSources, it); + } + }, false); + })); + } menu = SlideUpMenuOverlay(container.context, container, "Download Video", null, true, items); @@ -157,29 +162,12 @@ class UISlideOverlays { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { try { val subtitleUri = subtitleToDownload.getSubtitlesURI(); - if (subtitleUri != null) { - var subtitles: String? = null; - if ("file" == subtitleUri.scheme) { - val inputStream = contentResolver.openInputStream(subtitleUri); - inputStream?.use { stream -> - val reader = stream.bufferedReader(); - subtitles = reader.use { it.readText() }; - } - } else if ("http" == subtitleUri.scheme || "https" == subtitleUri.scheme) { - val client = ManagedHttpClient(); - val subtitleResponse = client.get(subtitleUri.toString()); - if (!subtitleResponse.isOk) { - throw Exception("Cannot fetch subtitles from source '${subtitleUri}': ${subtitleResponse.code}"); - } - - subtitles = subtitleResponse.body?.toString() - ?: throw Exception("Subtitles are invalid '${subtitleUri}': ${subtitleResponse.code}"); - } else { - throw Exception("Unsuported scheme"); - } + //TODO: Remove uri dependency, should be able to work with raw aswell? + if (subtitleUri != null && contentResolver != null) { + val subtitlesRaw = StateDownloads.instance.downloadSubtitles(subtitleToDownload, contentResolver); withContext(Dispatchers.Main) { - StateDownloads.instance.download(video, selectedVideo, selectedAudio, if (subtitles != null) SubtitleRawSource(subtitleToDownload.name, subtitleToDownload.format, subtitles!!) else null); + StateDownloads.instance.download(video, selectedVideo, selectedAudio, subtitlesRaw); } } else { withContext(Dispatchers.Main) { @@ -195,10 +183,41 @@ class UISlideOverlays { }; return menu.apply { show() }; } - fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup) { - showUnknownVideoDownload("Video", container) { px, bitrate -> - StateDownloads.instance.download(video, px, bitrate) + fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup, useDetails: Boolean = false) { + val handleUnknownDownload: ()->Unit = { + showUnknownVideoDownload("Video", container) { px, bitrate -> + StateDownloads.instance.download(video, px, bitrate) + }; }; + if(!useDetails) + handleUnknownDownload(); + else { + val scope = StateApp.instance.scopeOrNull; + + if(scope != null) { + val loader = showLoaderOverlay("Fetching video details", container); + scope.launch(Dispatchers.IO) { + try { + val videoDetails = StatePlatform.instance.getContentDetails(video.url, false).await(); + if(videoDetails !is IPlatformVideoDetails) + throw IllegalStateException("Not a video details"); + + withContext(Dispatchers.Main) { + if(showDownloadVideoOverlay(videoDetails, container, StateApp.instance.contextOrNull?.contentResolver) == null) + loader.hide(true); + } + } + catch(ex: Throwable) { + withContext(Dispatchers.Main) { + UIDialogs.toast("Failed to fetch details for download"); + handleUnknownDownload(); + loader.hide(true); + } + } + } + } + else handleUnknownDownload(); + } } fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) { showUnknownVideoDownload("Video", container) { px, bitrate -> @@ -273,6 +292,18 @@ class UISlideOverlays { menu.show(); } + fun showLoaderOverlay(text: String, container: ViewGroup): SlideUpMenuOverlay { + val dp70 = 70.dp(container.context.resources); + val dp15 = 15.dp(container.context.resources); + val overlay = SlideUpMenuOverlay(container.context, container, text, null, true, listOf( + Loader(container.context, true, dp70).apply { + this.setPadding(0, dp15, 0, dp15); + } + ), true); + overlay.show(); + return overlay; + } + fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, onVideoHidden: (()->Unit)? = null): SlideUpMenuOverlay { val items = arrayListOf(); val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist(); @@ -295,7 +326,7 @@ class UISlideOverlays { SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, "Hide", "Hide from Home", "hide", { StateMeta.instance.addHiddenVideo(video.url); onVideoHidden?.invoke() }), SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download", - { showDownloadVideoOverlay(video, container); }, false) + { showDownloadVideoOverlay(video, container, true); }, false) )) items.add( SlideUpMenuGroup(container.context, "Add To", "addto", @@ -348,7 +379,7 @@ class UISlideOverlays { SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} videos", "watch later", { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }), SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download", - { showDownloadVideoOverlay(video, container); }, false)) + { showDownloadVideoOverlay(video, container, true); }, false)) ); val playlistItems = arrayListOf(); diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index 8543574b..7f082407 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -22,6 +22,7 @@ import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer import com.futo.platformplayer.serializers.OffsetDateTimeSerializer import com.futo.platformplayer.toHumanBitrate import com.futo.platformplayer.toHumanBytesSpeed +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -30,7 +31,6 @@ import java.io.File import java.io.FileOutputStream import java.io.IOException import java.time.OffsetDateTime -import java.util.concurrent.CancellationException import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinTask import java.util.concurrent.ThreadLocalRandom @@ -371,7 +371,7 @@ class VideoDownload { } if (isCancelled) - throw IllegalStateException("Cancelled"); + throw CancellationException("Cancelled"); } while (read > 0); lastSpeed = 0; @@ -423,7 +423,7 @@ class VideoDownload { } if(isCancelled) - throw IllegalStateException("Cancelled"); + throw CancellationException("Cancelled", null); } onProgress(sourceLength, totalRead, 0); } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 4aba9b3f..20d3da69 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -608,7 +608,7 @@ class VideoDetailView : ConstraintLayout { }, RoundButton(context, R.drawable.ic_download, "Download", TAG_DOWNLOAD) { video?.let { - _slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(context.contentResolver, it, _overlayContainer); + _slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver); }; }, RoundButton(context, R.drawable.ic_share, "Share", TAG_SHARE) { diff --git a/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt b/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt index 00ea5fd6..a58a9b29 100644 --- a/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt +++ b/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt @@ -19,6 +19,7 @@ import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.stores.FragmentedStorage +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -138,18 +139,7 @@ class DownloadService : Service() { else if(ex is DownloadException && !ex.isRetryable) { Logger.w(TAG, "Video had exception that should not be retried"); StateDownloads.instance.removeDownload(currentVideo); - //Ensure impossible downloads are not retried for playlists - if(currentVideo.video != null && currentVideo.groupID != null && currentVideo.groupType == VideoDownload.GROUP_PLAYLIST) { - StateDownloads.instance.getPlaylistDownload(currentVideo.groupID!!)?.let { - synchronized(it.preventDownload) { - if(currentVideo?.video?.url != null && !it.preventDownload.contains(currentVideo!!.video!!.url)) { - it.preventDownload.add(currentVideo!!.video!!.url); - StateDownloads.instance.savePlaylistDownload(it); - Logger.w(TAG, "Preventing further download attempts in playlist [${it.id}] for [${currentVideo?.name}]:${currentVideo?.video?.url}"); - } - } - } - } + StateDownloads.instance.preventPlaylistDownload(currentVideo); } else Logger.e(TAG, "Failed download [${currentVideo.name}]: ${ex.message}", ex); @@ -157,9 +147,10 @@ class DownloadService : Service() { currentVideo.changeState(VideoDownload.State.ERROR); ignore.add(currentVideo); - StateAnnouncement.instance.registerAnnouncement(currentVideo?.id?.value?:"" + currentVideo?.id?.pluginId?:"" + "_FailDownload", - "Download failed", - "Download for [${currentVideo.name}] failed.\nDownloads are automatically retried.\nReason: ${ex.message}", AnnouncementType.SESSION, null, "download"); + if(ex !is CancellationException) + StateAnnouncement.instance.registerAnnouncement(currentVideo?.id?.value?:"" + currentVideo?.id?.pluginId?:"" + "_FailDownload", + "Download failed", + "Download for [${currentVideo.name}] failed.\nDownloads are automatically retried.\nReason: ${ex.message}", AnnouncementType.SESSION, null, "download"); //Give it a sec Thread.sleep(500); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt index 70799da2..fa017213 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt @@ -1,12 +1,16 @@ package com.futo.platformplayer.states +import android.content.ContentResolver +import android.net.Uri import android.os.StatFs import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.exceptions.AlreadyQueuedException import com.futo.platformplayer.api.media.models.streams.sources.* +import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.constructs.Event0 @@ -147,6 +151,19 @@ class StateDownloads { _downloading.delete(download); onDownloadsChanged.emit(); } + fun preventPlaylistDownload(download: VideoDownload) { + if(download.video != null && download.groupID != null && download.groupType == VideoDownload.GROUP_PLAYLIST) { + getPlaylistDownload(download.groupID!!)?.let { + synchronized(it.preventDownload) { + if(download.video?.url != null && !it.preventDownload.contains(download.video!!.url)) { + it.preventDownload.add(download.video!!.url); + savePlaylistDownload(it); + Logger.w(TAG, "Preventing further download attempts in playlist [${it.id}] for [${download.name}]:${download.video?.url}"); + } + } + } + } + } fun checkForDownloadsTodos() { val hasPlaylistChanged = checkForOutdatedPlaylists(); @@ -304,6 +321,32 @@ class StateDownloads { } } + suspend fun downloadSubtitles(subtitle: ISubtitleSource, contentResolver: ContentResolver): SubtitleRawSource? { + val subtitleUri = subtitle.getSubtitlesURI(); + if(subtitleUri == null) + return null; + var subtitles: String? = null; + if ("file" == subtitleUri.scheme) { + val inputStream = contentResolver.openInputStream(subtitleUri); + inputStream?.use { stream -> + val reader = stream.bufferedReader(); + subtitles = reader.use { it.readText() }; + } + } else if ("http" == subtitleUri.scheme || "https" == subtitleUri.scheme) { + val client = ManagedHttpClient(); + val subtitleResponse = client.get(subtitleUri.toString()); + if (!subtitleResponse.isOk) { + throw Exception("Cannot fetch subtitles from source '${subtitleUri}': ${subtitleResponse.code}"); + } + + subtitles = subtitleResponse.body?.toString() + ?: throw Exception("Subtitles are invalid '${subtitleUri}': ${subtitleResponse.code}"); + } else { + throw NotImplementedError("Unsuported scheme"); + } + return if (subtitles != null) SubtitleRawSource(subtitle.name, subtitle.format, subtitles!!) else null; + } + fun cleanupDownloads(): Pair { val expected = getDownloadedVideos(); val validFiles = HashSet(expected.flatMap { it.videoSource.map { it.filePath } + it.audioSource.map { it.filePath } }); diff --git a/app/src/main/java/com/futo/platformplayer/views/Loader.kt b/app/src/main/java/com/futo/platformplayer/views/Loader.kt index 644d6047..8e4a64d3 100644 --- a/app/src/main/java/com/futo/platformplayer/views/Loader.kt +++ b/app/src/main/java/com/futo/platformplayer/views/Loader.kt @@ -5,8 +5,10 @@ import android.graphics.drawable.Animatable import android.util.AttributeSet import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.LinearLayout +import androidx.core.view.updateLayoutParams import com.futo.platformplayer.R class Loader : LinearLayout { @@ -15,7 +17,7 @@ class Loader : LinearLayout { private val _animatable: Animatable; constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { - LayoutInflater.from(context).inflate(R.layout.view_loader, this, true); + inflate(context, R.layout.view_loader, this); _imageLoader = findViewById(R.id.image_loader); _animatable = _imageLoader.drawable as Animatable; @@ -29,6 +31,18 @@ class Loader : LinearLayout { visibility = View.GONE; } + constructor(context: Context, automatic: Boolean, height: Int = -1) : super(context) { + inflate(context, R.layout.view_loader, this); + _imageLoader = findViewById(R.id.image_loader); + _animatable = _imageLoader.drawable as Animatable; + _automatic = automatic; + + if(height > 0) { + layoutParams = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, height); + } + + visibility = View.GONE; + } override fun onAttachedToWindow() { super.onAttachedToWindow() diff --git a/app/src/main/java/com/futo/platformplayer/views/items/ActiveDownloadItem.kt b/app/src/main/java/com/futo/platformplayer/views/items/ActiveDownloadItem.kt index f4e27d33..5b93bffc 100644 --- a/app/src/main/java/com/futo/platformplayer/views/items/ActiveDownloadItem.kt +++ b/app/src/main/java/com/futo/platformplayer/views/items/ActiveDownloadItem.kt @@ -68,6 +68,7 @@ class ActiveDownloadItem: LinearLayout { _videoCancel.setOnClickListener { StateDownloads.instance.removeDownload(_download); + StateDownloads.instance.preventPlaylistDownload(_download); }; _download.onProgressChanged.subscribe(this) { diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt index ad94006f..fcac9eed 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt @@ -40,7 +40,7 @@ class SlideUpMenuOverlay : RelativeLayout { _groupItems = listOf(); } - constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List): super(context){ + constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List, hideButtons: Boolean = false): super(context){ init(animated, okText); _container = parent; if(!_container!!.children.contains(this)) { @@ -50,6 +50,12 @@ class SlideUpMenuOverlay : RelativeLayout { _textTitle.text = titleText; _groupItems = items; + if(hideButtons) { + _textCancel.visibility = GONE; + _textOK.visibility = GONE; + _textTitle.textAlignment = TextView.TEXT_ALIGNMENT_CENTER; + } + setItems(items); }