mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-22 07:45:20 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ffdf39f13 | |||
| 8bb1ff87c0 |
@@ -0,0 +1,15 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
|
||||
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||
fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||
|
||||
|
||||
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());
|
||||
@@ -4,7 +4,6 @@ import android.content.ContentResolver
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
@@ -18,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
|
||||
@@ -29,7 +29,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
class UISlideOverlays {
|
||||
companion object {
|
||||
@@ -45,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<View>();
|
||||
var menu: SlideUpMenuOverlay? = null;
|
||||
|
||||
@@ -68,6 +68,12 @@ class UISlideOverlays {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!VideoHelper.isDownloadable(video)) {
|
||||
Logger.i(TAG, "Attempted to open downloads without valid sources for [${video.name}]: ${video.url}");
|
||||
UIDialogs.toast( "No downloadable sources (yet)");
|
||||
return null;
|
||||
}
|
||||
|
||||
items.add(SlideUpMenuGroup(container.context, "Video", videoSources,
|
||||
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, "None", "Audio Only", "none", {
|
||||
selectedVideo = null;
|
||||
@@ -76,7 +82,7 @@ class UISlideOverlays {
|
||||
menu?.setOk("Download");
|
||||
}, false)) +
|
||||
videoSources
|
||||
.filter { it is IVideoUrlSource }
|
||||
.filter { it.isDownloadable() }
|
||||
.map {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||
selectedVideo = it as IVideoUrlSource;
|
||||
@@ -88,14 +94,14 @@ class UISlideOverlays {
|
||||
));
|
||||
|
||||
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0)
|
||||
selectedVideo = VideoHelper.selectBestVideoSource(videoSources.filter { it is IVideoUrlSource }.asIterable(),
|
||||
selectedVideo = VideoHelper.selectBestVideoSource(videoSources.filter { it.isDownloadable() }.asIterable(),
|
||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) as IVideoUrlSource;
|
||||
|
||||
|
||||
audioSources?.let { audioSources ->
|
||||
items.add(SlideUpMenuGroup(container.context, "Audio", audioSources, audioSources
|
||||
.filter { it is IAudioUrlSource }
|
||||
.filter { VideoHelper.isDownloadable(it) }
|
||||
.map {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
||||
selectedAudio = it as IAudioUrlSource;
|
||||
@@ -111,24 +117,27 @@ class UISlideOverlays {
|
||||
menu?.selectOption(asources, preferredAudioSource);
|
||||
|
||||
|
||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource }.asIterable(),
|
||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it.isDownloadable() }.asIterable(),
|
||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
||||
Settings.instance.playback.getPrimaryLanguage(container.context),
|
||||
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);
|
||||
|
||||
@@ -153,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) {
|
||||
@@ -191,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 ->
|
||||
@@ -269,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<View>();
|
||||
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
||||
@@ -291,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",
|
||||
@@ -344,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<SlideUpMenuItem>();
|
||||
|
||||
+1
-4
@@ -9,7 +9,7 @@ import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.orNull
|
||||
|
||||
class JSHLSManifestAudioSource : IAudioUrlSource, IHLSManifestAudioSource, JSSource {
|
||||
class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||
override val container : String get() = "application/vnd.apple.mpegurl";
|
||||
override val codec: String = "HLS";
|
||||
override val name : String;
|
||||
@@ -31,9 +31,6 @@ class JSHLSManifestAudioSource : IAudioUrlSource, IHLSManifestAudioSource, JSSou
|
||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||
}
|
||||
|
||||
override fun getAudioUrl(): String {
|
||||
return url;
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) };
|
||||
|
||||
+1
-5
@@ -7,7 +7,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class JSHLSManifestSource : IVideoUrlSource, IHLSManifestSource, JSSource {
|
||||
class JSHLSManifestSource : IHLSManifestSource, JSSource {
|
||||
override val width : Int = 0;
|
||||
override val height : Int = 0;
|
||||
override val container : String get() = "application/vnd.apple.mpegurl";
|
||||
@@ -28,8 +28,4 @@ class JSHLSManifestSource : IVideoUrlSource, IHLSManifestSource, JSSource {
|
||||
|
||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||
}
|
||||
|
||||
override fun getVideoUrl(): String {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,19 @@
|
||||
package com.futo.platformplayer.downloads
|
||||
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class PlaylistDownloadDescriptor(
|
||||
val id: String,
|
||||
val targetPxCount: Long?,
|
||||
val targetBitrate: Long?
|
||||
);
|
||||
) {
|
||||
var preventDownload: MutableList<String> = arrayListOf();
|
||||
|
||||
fun getPreventDownloadList(): List<String> = synchronized(preventDownload){ preventDownload };
|
||||
fun shouldDownload(video: IPlatformVideo): Boolean {
|
||||
synchronized(preventDownload) {
|
||||
return !preventDownload.contains(video.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,16 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.exceptions.DownloadException
|
||||
import com.futo.platformplayer.hasAnySource
|
||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.isDownloadable
|
||||
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
|
||||
@@ -27,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
|
||||
@@ -147,27 +150,37 @@ class VideoDownload {
|
||||
if(original !is IPlatformVideoDetails)
|
||||
throw IllegalStateException("Original content is not media?");
|
||||
|
||||
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
||||
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
||||
throw DownloadException("Unsupported video for downloading", false);
|
||||
}
|
||||
|
||||
videoDetails = SerializedPlatformVideoDetails.fromVideo(original, if (subtitleSource != null) listOf(subtitleSource!!) else listOf());
|
||||
if(videoSource == null && targetPixelCount != null) {
|
||||
val vsource = VideoHelper.selectBestVideoSource(videoDetails!!.video, targetPixelCount!!.toInt(), arrayOf())
|
||||
?: throw IllegalStateException("Could not find a valid video source for video");
|
||||
if(vsource is IVideoUrlSource)
|
||||
videoSource = VideoUrlSource.fromUrlSource(vsource);
|
||||
else
|
||||
throw IllegalStateException("Download video source is not a url source");
|
||||
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||
if(vsource != null) {
|
||||
if (vsource is IVideoUrlSource)
|
||||
videoSource = VideoUrlSource.fromUrlSource(vsource);
|
||||
else
|
||||
throw DownloadException("Video source is not supported for downloading (yet)", false);
|
||||
}
|
||||
}
|
||||
|
||||
if(audioSource == null && targetBitrate != null) {
|
||||
val asource = VideoHelper.selectBestAudioSource(videoDetails!!.video, arrayOf(), null, targetPixelCount)
|
||||
?: if(videoSource != null ) null
|
||||
else throw IllegalStateException("Could not find a valid audio source for video");
|
||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||
if(asource == null)
|
||||
audioSource = null;
|
||||
else if(asource is IAudioUrlSource)
|
||||
audioSource = AudioUrlSource.fromUrlSource(asource);
|
||||
else
|
||||
throw IllegalStateException("Download audio source is not a url source");
|
||||
throw DownloadException("Audio source is not supported for downloading (yet)", false);
|
||||
}
|
||||
|
||||
if(videoSource == null && audioSource == null)
|
||||
throw DownloadException("No valid sources found for video/audio");
|
||||
}
|
||||
}
|
||||
suspend fun download(client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
||||
@@ -358,7 +371,7 @@ class VideoDownload {
|
||||
}
|
||||
|
||||
if (isCancelled)
|
||||
throw IllegalStateException("Cancelled");
|
||||
throw CancellationException("Cancelled");
|
||||
} while (read > 0);
|
||||
|
||||
lastSpeed = 0;
|
||||
@@ -410,7 +423,7 @@ class VideoDownload {
|
||||
}
|
||||
|
||||
if(isCancelled)
|
||||
throw IllegalStateException("Cancelled");
|
||||
throw CancellationException("Cancelled", null);
|
||||
}
|
||||
onProgress(sourceLength, totalRead, 0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.futo.platformplayer.exceptions
|
||||
|
||||
class DownloadException : Throwable {
|
||||
val isRetryable: Boolean;
|
||||
|
||||
constructor(innerException: Throwable, retryable: Boolean = true): super(innerException) {
|
||||
isRetryable = retryable;
|
||||
}
|
||||
constructor(msg: String, retryable: Boolean = true): super(msg) {
|
||||
isRetryable = retryable;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
@@ -4,7 +4,10 @@ import android.net.Uri
|
||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -17,6 +20,12 @@ import com.google.android.exoplayer2.upstream.ResolvingDataSource
|
||||
class VideoHelper {
|
||||
companion object {
|
||||
|
||||
fun isDownloadable(detail: IPlatformVideoDetails) =
|
||||
(detail.video.videoSources.any { isDownloadable(it) }) ||
|
||||
(if (detail is VideoUnMuxedSourceDescriptor) detail.audioSources.any { isDownloadable(it) } else false);
|
||||
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource;
|
||||
fun isDownloadable(source: IAudioSource) = source is IAudioUrlSource;
|
||||
|
||||
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
|
||||
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
|
||||
val targetVideo = if(desiredPixelCount > 0)
|
||||
|
||||
@@ -12,12 +12,14 @@ import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.downloads.VideoDownload
|
||||
import com.futo.platformplayer.exceptions.DownloadException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.Announcement
|
||||
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
|
||||
@@ -134,15 +136,21 @@ class DownloadService : Service() {
|
||||
Logger.w(TAG, "Video had no video or videodetail, removing download");
|
||||
StateDownloads.instance.removeDownload(currentVideo);
|
||||
}
|
||||
else if(ex is DownloadException && !ex.isRetryable) {
|
||||
Logger.w(TAG, "Video had exception that should not be retried");
|
||||
StateDownloads.instance.removeDownload(currentVideo);
|
||||
StateDownloads.instance.preventPlaylistDownload(currentVideo);
|
||||
}
|
||||
else
|
||||
Logger.e(TAG, "Failed download [${currentVideo.name}]: ${ex.message}", ex);
|
||||
currentVideo.error = ex.message;
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
@@ -108,6 +112,11 @@ class StateDownloads {
|
||||
fun getPlaylistDownload(playlistId: String): PlaylistDownloadDescriptor? {
|
||||
return _downloadPlaylists.findItem { it.id == playlistId };
|
||||
}
|
||||
fun savePlaylistDownload(playlistDownload: PlaylistDownloadDescriptor) {
|
||||
synchronized(playlistDownload.preventDownload) {
|
||||
_downloadPlaylists.save(playlistDownload);
|
||||
}
|
||||
}
|
||||
fun deleteCachedPlaylist(id: String) {
|
||||
val pdl = getPlaylistDownload(id);
|
||||
if(pdl != null)
|
||||
@@ -142,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();
|
||||
@@ -157,12 +179,15 @@ class StateDownloads {
|
||||
val playlistsDownloaded = getCachedPlaylists();
|
||||
for(playlist in playlistsDownloaded) {
|
||||
val playlistDownload = getPlaylistDownload(playlist.playlist.id) ?: continue;
|
||||
|
||||
if(playlist.playlist.videos.any{ getCachedVideo(it.id) == null }) {
|
||||
Logger.i(TAG, "Found new videos on playlist [${playlist.playlist.name}]");
|
||||
val toIgnore = playlistDownload.getPreventDownloadList();
|
||||
val missingVideoCount = playlist.playlist.videos.count { !toIgnore.contains(it.url) && getCachedVideo(it.id) == null };
|
||||
if(missingVideoCount > 0) {
|
||||
Logger.i(TAG, "Found new videos (${missingVideoCount}) on playlist [${playlist.playlist.name}] to download");
|
||||
continueDownload(playlistDownload, playlist.playlist);
|
||||
hasChanged = true;
|
||||
}
|
||||
else
|
||||
Logger.v(TAG, "Offline playlist [${playlist.playlist.name}] is up to date");
|
||||
}
|
||||
return hasChanged;
|
||||
}
|
||||
@@ -171,6 +196,11 @@ class StateDownloads {
|
||||
var hasNew = false;
|
||||
for(item in playlist.videos) {
|
||||
val existing = getCachedVideo(item.id);
|
||||
|
||||
if(!playlistDownload.shouldDownload(item)) {
|
||||
Logger.i(TAG, "Not downloading for playlist [${playlistDownload.id}] Video [${item.name}]:${item.url}")
|
||||
continue;
|
||||
}
|
||||
if(existing == null) {
|
||||
val ongoingDownload = getDownloading().find { it.id.value == item.id.value && it.id.value != null };
|
||||
if(ongoingDownload != null) {
|
||||
@@ -291,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<Int, Long> {
|
||||
val expected = getDownloadedVideos();
|
||||
val validFiles = HashSet(expected.flatMap { it.videoSource.map { it.filePath } + it.audioSource.map { it.filePath } });
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -68,6 +68,7 @@ class ActiveDownloadItem: LinearLayout {
|
||||
|
||||
_videoCancel.setOnClickListener {
|
||||
StateDownloads.instance.removeDownload(_download);
|
||||
StateDownloads.instance.preventPlaylistDownload(_download);
|
||||
};
|
||||
|
||||
_download.onProgressChanged.subscribe(this) {
|
||||
|
||||
+7
-1
@@ -40,7 +40,7 @@ class SlideUpMenuOverlay : RelativeLayout {
|
||||
_groupItems = listOf();
|
||||
}
|
||||
|
||||
constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>): super(context){
|
||||
constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>, 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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user