diff --git a/app/src/main/java/com/futo/platformplayer/Utility.kt b/app/src/main/java/com/futo/platformplayer/Utility.kt index 0875aadb..5b00bbb9 100644 --- a/app/src/main/java/com/futo/platformplayer/Utility.kt +++ b/app/src/main/java/com/futo/platformplayer/Utility.kt @@ -101,7 +101,7 @@ fun String.isHexColor(): Boolean { fun IPlatformClient.fromPool(pool: PlatformMultiClientPool) = pool.getClientPooled(this); -fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec); +fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec); fun DocumentFile.getInputStream(context: Context) = context.contentResolver.openInputStream(this.uri); fun DocumentFile.getOutputStream(context: Context) = context.contentResolver.openOutputStream(this.uri); diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 2679d338..e613e94c 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -33,7 +33,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.withStateAtLeast import androidx.media3.common.util.UnstableApi -import com.curlbind.Libcurl import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.R import com.futo.platformplayer.RootInsetsController diff --git a/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpProxyHandler.kt b/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpProxyHandler.kt index 74dcbb52..b78925a1 100644 --- a/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpProxyHandler.kt +++ b/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpProxyHandler.kt @@ -5,6 +5,7 @@ import android.util.Log import com.futo.platformplayer.api.http.server.HttpContext import com.futo.platformplayer.api.http.server.HttpHeaders import com.futo.platformplayer.api.http.ManagedHttpClient +import com.futo.platformplayer.api.media.models.modifier.IRequest import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.parsers.HttpResponseParser import com.futo.platformplayer.readLine @@ -27,6 +28,7 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv private var _injectReferer = false; private val _client = ManagedHttpClient(); + private var _requestModifier: ((String, Map) -> IRequest)? = null; override fun handle(context: HttpContext) { if (useTcp) { @@ -43,21 +45,33 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv for (injectHeader in _injectRequestHeader) proxyHeaders[injectHeader.first] = injectHeader.second; - val parsed = Uri.parse(targetUrl); + val req = _requestModifier?.invoke(targetUrl, proxyHeaders) + var url = targetUrl + if (req != null) { + req.url?.let { + url = it + } + req.headers.let { + proxyHeaders.clear() + proxyHeaders.putAll(it) + } + } + + val parsed = Uri.parse(url); if(_injectHost) proxyHeaders.put("Host", parsed.host!!); if(_injectReferer) - proxyHeaders.put("Referer", targetUrl); + proxyHeaders.put("Referer", url); val useMethod = if (method == "inherit") context.method else method; - Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${targetUrl}"); + Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${url}"); Logger.i(TAG, "handleWithOkHttp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n")); val resp = when (useMethod) { - "GET" -> _client.get(targetUrl, proxyHeaders); - "POST" -> _client.post(targetUrl, content ?: "", proxyHeaders); - "HEAD" -> _client.head(targetUrl, proxyHeaders) - else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders); + "GET" -> _client.get(url, proxyHeaders); + "POST" -> _client.post(url, content ?: "", proxyHeaders); + "HEAD" -> _client.head(url, proxyHeaders) + else -> _client.requestMethod(useMethod, url, proxyHeaders); }; Logger.i(TAG, "Proxied Response [${resp.code}]"); @@ -91,11 +105,23 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv for (injectHeader in _injectRequestHeader) proxyHeaders[injectHeader.first] = injectHeader.second; - val parsed = Uri.parse(targetUrl); + val req = _requestModifier?.invoke(targetUrl, proxyHeaders) + var url = targetUrl + if (req != null) { + req.url?.let { + url = it + } + req.headers.let { + proxyHeaders.clear() + proxyHeaders.putAll(it) + } + } + + val parsed = Uri.parse(url); if(_injectHost) proxyHeaders.put("Host", parsed.host!!); if(_injectReferer) - proxyHeaders.put("Referer", targetUrl); + proxyHeaders.put("Referer", url); val useMethod = if (method == "inherit") context.method else method; Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}"); @@ -242,6 +268,10 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv _ignoreRequestHeaders.add("referer"); return this; } + fun withRequestModifier(modifier: (String, Map) -> IRequest) : HttpProxyHandler { + _requestModifier = modifier; + return this; + } companion object { private const val TAG = "HttpProxyHandler" diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index dc66d6b2..269a73b8 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -17,6 +17,7 @@ import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler +import com.futo.platformplayer.api.media.models.modifier.IRequestModifier 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.IHLSManifestAudioSource @@ -295,20 +296,63 @@ abstract class StateCasting { val url = getLocalUrl(ad); val id = UUID.randomUUID(); + if (videoSource is IVideoUrlSource) { - val videoPath = "/video-${id}" - val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl(); - Logger.i(TAG, "Casting as singular video"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video)); + val videoPath = "/video-$id" + val upstreamUrl = videoSource.getVideoUrl() + val videoUrl = if (proxyStreams) url + videoPath else upstreamUrl + val jsReqMod = (videoSource as? JSSource)?.getRequestModifier() + + if (proxyStreams) { + _castServer.addHandlerWithAllowAllOptions( + HttpProxyHandler("GET", videoPath, upstreamUrl, true) + .withIRequestModifier(jsReqMod) + .withInjectedHost() + .withHeader("Access-Control-Allow-Origin", "*"), + true + ).withTag("castSingular") + } + + Logger.i(TAG, "Casting as singular video (proxy=$proxyStreams, url=$videoUrl)") + ad.loadVideo( + if (video.isLive) "LIVE" else "BUFFERED", + videoSource.container, + videoUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ) } else if (audioSource is IAudioUrlSource) { - val audioPath = "/audio-${id}" - val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl(); - Logger.i(TAG, "Casting as singular audio"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video)); + val audioPath = "/audio-$id" + val upstreamUrl = audioSource.getAudioUrl() + val audioUrl = if (proxyStreams) url + audioPath else upstreamUrl + val jsReqMod = (audioSource as? JSSource)?.getRequestModifier() + + if (proxyStreams) { + _castServer.addHandlerWithAllowAllOptions( + HttpProxyHandler("GET", audioPath, upstreamUrl, true) + .withIRequestModifier(jsReqMod) + .withInjectedHost() + .withHeader("Access-Control-Allow-Origin", "*"), + true + ).withTag("castSingular") + } + + Logger.i(TAG, "Casting as singular audio (proxy=$proxyStreams, url=$audioUrl)") + ad.loadVideo( + if (video.isLive) "LIVE" else "BUFFERED", + audioSource.container, + audioUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ) } else if (videoSource is IHLSManifestSource) { if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) { Logger.i(TAG, "Casting as proxied HLS"); - castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed); + castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed, (videoSource as JSSource?)?.getRequestModifier()); } else { Logger.i(TAG, "Casting as non-proxied HLS"); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video)); @@ -316,7 +360,7 @@ abstract class StateCasting { } else if (audioSource is IHLSManifestAudioSource) { if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) { Logger.i(TAG, "Casting as proxied audio HLS"); - castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed); + castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed, (audioSource as JSSource?)?.getRequestModifier()); } else { Logger.i(TAG, "Casting as non-proxied audio HLS"); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video)); @@ -347,6 +391,11 @@ abstract class StateCasting { } } + private fun HttpProxyHandler.withIRequestModifier(requestModifier: IRequestModifier?): HttpProxyHandler { + if (requestModifier == null) return this + return withRequestModifier { url, headers -> requestModifier.modifyRequest(url, headers) } + } + fun resumeVideo(): Boolean { val ad = activeDevice ?: return false; try { @@ -665,7 +714,8 @@ abstract class StateCasting { sourceUrl: String, codec: String?, resumePosition: Double, - speed: Double? + speed: Double?, + requestModifier: IRequestModifier? ): List { _castServer.removeAllHandlers("castProxiedHlsMaster") @@ -686,7 +736,9 @@ abstract class StateCasting { val headers = masterContext.headers.clone() headers["Content-Type"] = "application/vnd.apple.mpegurl"; - val masterPlaylistResponse = _client.get(sourceUrl) + val req = requestModifier?.modifyRequest(sourceUrl, mapOf()) + val masterPlaylistResponse = _client.get(req?.url ?: sourceUrl, (req?.headers ?: mapOf()).toMutableMap()) + check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" } val masterPlaylistContent = masterPlaylistResponse.body?.string() @@ -706,7 +758,7 @@ abstract class StateCasting { val variantPlaylist = HLS.parseVariantPlaylist(masterPlaylistContent, sourceUrl) val proxiedVariantPlaylist = - proxyVariantPlaylist(url, id, variantPlaylist, video.isLive) + proxyVariantPlaylist(url, id, variantPlaylist, video.isLive, requestModifier) val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); return@HttpFunctionHandler @@ -747,7 +799,7 @@ abstract class StateCasting { val variantPlaylist = HLS.parseVariantPlaylist(vpContent, variantPlaylistRef.url) val proxiedVariantPlaylist = - proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive) + proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive, requestModifier) val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); }.withHeader("Access-Control-Allow-Origin", "*"), true @@ -784,7 +836,7 @@ abstract class StateCasting { val variantPlaylist = HLS.parseVariantPlaylist(vpContent, mediaRendition.uri) val proxiedVariantPlaylist = proxyVariantPlaylist( - url, playlistId, variantPlaylist, video.isLive + url, playlistId, variantPlaylist, video.isLive, requestModifier ) val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); @@ -826,13 +878,13 @@ abstract class StateCasting { return listOf(hlsUrl); } - private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist, isLive: Boolean, proxySegments: Boolean = true): HLS.VariantPlaylist { + private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist, isLive: Boolean, requestModifier: IRequestModifier?, proxySegments: Boolean = true): HLS.VariantPlaylist { val newSegments = arrayListOf() if (proxySegments) { variantPlaylist.segments.forEachIndexed { index, segment -> val sequenceNumber = (variantPlaylist.mediaSequence ?: 0) + index.toLong() - newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber)) + newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber, requestModifier)) } } else { newSegments.addAll(variantPlaylist.segments) @@ -850,7 +902,7 @@ abstract class StateCasting { ) } - private fun proxySegment(url: String, playlistId: UUID, segment: HLS.Segment, index: Long): HLS.Segment { + private fun proxySegment(url: String, playlistId: UUID, segment: HLS.Segment, index: Long, requestModifier: IRequestModifier?): HLS.Segment { if (segment is HLS.MediaSegment) { val newSegmentPath = "/hls-playlist-${playlistId}-segment-${index}" val newSegmentUrl = url + newSegmentPath; @@ -858,6 +910,7 @@ abstract class StateCasting { if (_castServer.getHandler("GET", newSegmentPath) == null) { _castServer.addHandlerWithAllowAllOptions( HttpProxyHandler("GET", newSegmentPath, segment.uri, true) + .withIRequestModifier(requestModifier) .withInjectedHost() .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castProxiedHlsVariant") diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt index 80bd08cf..8bb2d0e1 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt @@ -26,6 +26,7 @@ import com.futo.platformplayer.models.HistoryVideo import com.futo.platformplayer.states.StateHistory import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlayer +import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.views.ToggleBar import com.futo.platformplayer.views.adapters.HistoryListViewHolder @@ -243,12 +244,23 @@ class HistoryFragment : MainFragment() { return; } - val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager; val diff = v.video.duration - v.position; val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video }; - StatePlayer.instance.clearQueue(); - _fragment.navigate(vid).maximizeVideoDetail(); + + val playlistId = v.playlistId + val playlist = playlistId?.let { StatePlaylists.instance.getPlaylist(it) } + val playlistIndex = playlist?.videos?.indexOfFirst { it.url == v.video.url } + if (playlist != null && playlistIndex != null && playlistIndex >= 0) { + _fragment.navigate(vid).maximizeVideoDetail(); + StatePlayer.instance.setPlaylist(playlist, playlistIndex) + + } else { + StatePlayer.instance.clearQueue(); + _fragment.navigate(vid).maximizeVideoDetail(); + } + _editSearch.clearFocus(); + val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager; inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0); _fragment.lifecycleScope.launch(Dispatchers.Main) { 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 d01586af..c7d012bf 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 @@ -1777,7 +1777,8 @@ class VideoDetailView : ConstraintLayout { false, (toResume.toFloat() / 1000.0f).toLong(), null, - true + true, + StatePlayer.instance.playlistId ); Logger.i( TAG, @@ -3095,7 +3096,7 @@ class VideoDetailView : ConstraintLayout { if (v !is TutorialFragment.TutorialVideo) { fragment.lifecycleScope.launch(Dispatchers.IO) { val history = getHistoryIndex(v) ?: return@launch; - StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong(), null, true); + StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong(), null, true, StatePlayer.instance.playlistId); } } _lastPositionSaveTime = currentTime; diff --git a/app/src/main/java/com/futo/platformplayer/models/HistoryVideo.kt b/app/src/main/java/com/futo/platformplayer/models/HistoryVideo.kt index 80574968..fee3f7e0 100644 --- a/app/src/main/java/com/futo/platformplayer/models/HistoryVideo.kt +++ b/app/src/main/java/com/futo/platformplayer/models/HistoryVideo.kt @@ -14,15 +14,17 @@ import java.time.ZoneOffset class HistoryVideo { var video: SerializedPlatformVideo; var position: Long; + var playlistId: String? = null @kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class) var date: OffsetDateTime; - constructor(video: SerializedPlatformVideo, position: Long, date: OffsetDateTime) { + constructor(video: SerializedPlatformVideo, position: Long, date: OffsetDateTime, playlistId: String?) { this.video = video; this.position = position; this.date = date; + this.playlistId = playlistId } @@ -59,7 +61,7 @@ class HistoryVideo { viewCount = -1 ); - return HistoryVideo(video, position, OffsetDateTime.of(LocalDateTime.ofEpochSecond(dateSec, 0, ZoneOffset.UTC), ZoneOffset.UTC)); + return HistoryVideo(video, position, OffsetDateTime.of(LocalDateTime.ofEpochSecond(dateSec, 0, ZoneOffset.UTC), ZoneOffset.UTC), null); } } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt b/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt index 11aae89e..9a368152 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt @@ -365,7 +365,7 @@ class StateBackup { } val hist = StateHistory.instance.getHistoryByVideo(histObj.video, true, histObj.date); if(hist != null) - StateHistory.instance.updateHistoryPosition(histObj.video, hist, true, histObj.position, histObj.date, false); + StateHistory.instance.updateHistoryPosition(histObj.video, hist, true, histObj.position, histObj.date, false, histObj.playlistId); } catch(ex: Throwable) { Logger.e(TAG, "Failed to import subscription group", ex); 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 32cc835d..927d90ba 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt @@ -543,7 +543,9 @@ class StateDownloads { val file = export.export(context, { progress -> val now = System.currentTimeMillis(); if (lastNotifyTime == -1L || now - lastNotifyTime > 100) { - it.setProgress(progress); + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + it.setProgress(progress); + } lastNotifyTime = now; } }, null); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt index 26fc3170..57b8d39c 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt @@ -65,7 +65,7 @@ class StateHistory { } private var _lastHistoryBroadcast = ""; - fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L, date: OffsetDateTime? = null, isUserAction: Boolean = false): Long { + fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L, date: OffsetDateTime? = null, isUserAction: Boolean = false, playlistId: String? = null): Long { val pos = if(position < 0) 0 else position; val historyVideo = index.obj; @@ -86,6 +86,7 @@ class StateHistory { historyVideo.position = pos; historyVideo.date = date ?: OffsetDateTime.now(); + historyVideo.playlistId = playlistId _historyDBStore.update(index.id!!, historyVideo); onHistoricVideoChanged.emit(liveObj, pos); @@ -157,7 +158,7 @@ class StateHistory { UIDialogs.toast("History item null?\nNo history tracking.."); } else if(create) { - val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now()); + val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now(), StatePlayer.instance.playlistId); val id = _historyDBStore.insert(newHistItem); result = _historyDBStore.getOrNull(id); if(result == null) diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt index fc4239ae..1ccf4497 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt @@ -114,6 +114,15 @@ class StatePlayer { var currentVideo: IPlatformVideo? = null private set; + private var _currentPlaylistId: String? = null + val playlistId: String? get() = if (_queueType == TYPE_PLAYLIST) _currentPlaylistId else null + + init { + onQueueChanged.subscribe { + updateLastQueue() + } + } + fun setCurrentlyPlaying(video: IPlatformVideo?) { currentVideo = video; } @@ -269,23 +278,6 @@ class StatePlayer { } onQueueChanged.emit(true); } - fun setPlaylist(playlist: IPlatformPlaylistDetails, toPlayIndex: Int = 0, focus: Boolean = false, shuffle: Boolean = false) { - synchronized(_queue) { - _queue.clear(); - setQueueType(TYPE_PLAYLIST); - _queueName = playlist.name; - _queue.addAll(playlist.contents.getResults()); - queueFocused = focus; - queueShuffle = shuffle; - if (shuffle) { - createShuffledQueue(); - } - _queuePosition = toPlayIndex; - } - playlist.id.value?.let { StatePlaylists.instance.didPlay(it); }; - - onQueueChanged.emit(true); - } fun setPlaylist(playlist: Playlist, toPlayIndex: Int = 0, focus: Boolean = false, shuffle: Boolean = false) { synchronized(_queue) { _queue.clear(); @@ -299,6 +291,7 @@ class StatePlayer { } _queuePosition = toPlayIndex; } + _currentPlaylistId = playlist.id StatePlaylists.instance.didPlay(playlist.id); onQueueChanged.emit(true); @@ -384,6 +377,23 @@ class StatePlayer { setQueuePosition(video); } } + + fun updateLastQueue() { + val queueVideos = synchronized(_queue) { + if (!_queue.isEmpty()) { + return@synchronized _queue.map { SerializedPlatformVideo.fromVideo(it) }.toList() + } + + return@synchronized null + } + + if (queueVideos != null) { + val playlist = StatePlaylists.instance.getPlaylist(StatePlaylists.LAST_QUEUE_PLAYLIST_ID) ?: Playlist("Last Queue", queueVideos).apply { + id = StatePlaylists.LAST_QUEUE_PLAYLIST_ID + } + StatePlaylists.instance.createOrUpdatePlaylist(playlist) + } + } fun setQueuePosition(video: IPlatformVideo) { synchronized(_queue) { if (getCurrentQueueItem() == video) { diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt index cbe1c518..8eeb2342 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt @@ -200,10 +200,10 @@ class StatePlaylists { } fun getLastPlayedPlaylist() : Playlist? { - return playlistStore.queryItem { it.maxByOrNull { x -> x.datePlayed } }; + return playlistStore.queryItem { it.filter { x -> x.id != StatePlaylists.LAST_QUEUE_PLAYLIST_ID }.maxByOrNull { x -> x.datePlayed } }; } fun getLastUpdatedPlaylist() : Playlist? { - return playlistStore.queryItem { it.maxByOrNull { x -> x.dateUpdate } }; + return playlistStore.queryItem { it.filter { x -> x.id != StatePlaylists.LAST_QUEUE_PLAYLIST_ID }.maxByOrNull { x -> x.dateUpdate } }; } fun getPlaylists() : List { @@ -394,6 +394,7 @@ class StatePlaylists { companion object { val TAG = "StatePlaylists"; + val LAST_QUEUE_PLAYLIST_ID = "a70a3287-45dd-4227-832c-6ecde7fb1bf6" private var _instance : StatePlaylists? = null; private var _lockObject = Object() val instance : StatePlaylists diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt index 8b620239..2aee32c2 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt @@ -463,7 +463,7 @@ class StateSync { for(video in history){ val hist = StateHistory.instance.getHistoryByVideo(video.video, true, video.date); if(hist != null) - StateHistory.instance.updateHistoryPosition(video.video, hist, true, video.position, video.date) + StateHistory.instance.updateHistoryPosition(video.video, hist, true, video.position, video.date, false, video.playlistId) if(lastHistory < video.date) lastHistory = video.date; }