From d12d3eeda0ff68f8442e967c9c2ee7eadea1081f Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Fri, 4 Oct 2024 21:30:19 +0200 Subject: [PATCH 01/16] 457 make server video api and modify url on player vlc --- .../sentinelweb/cuer/remote/server/WebExt.kt | 2 + hub/build.gradle.kts | 1 + .../hub/ui/player/vlc/VlcPlayerSwingWindow.kt | 25 +++++-- .../ui/player/vlc/VlcPlayerUiCoordinator.kt | 3 +- .../cuer/remote/server/JvmRemoteWebServer.kt | 70 +++++++++++++++++++ .../cuer/app/ui/player/PlayerContract.kt | 2 +- .../cuer/app/ui/player/PlayerStoreFactory.kt | 4 +- 7 files changed, 99 insertions(+), 8 deletions(-) diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/WebExt.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/WebExt.kt index 181f16e1b..98a45a912 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/WebExt.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/WebExt.kt @@ -15,7 +15,9 @@ fun LocalNodeDomain.locator() = Locator(ipAddress, port) fun Pair.http() = "http://$first:$second" fun LocalNodeDomain.http() = "http://$ipAddress:$port" fun RemoteNodeDomain.http() = "http://$ipAddress:$port" +fun Locator.http() = "http://$address:$port" fun Pair.https() = "https://$first:$second" fun LocalNodeDomain.https() = "https://$ipAddress:$port" fun RemoteNodeDomain.https() = "https://$ipAddress:$port" +fun Locator.https() = "https://$address:$port" diff --git a/hub/build.gradle.kts b/hub/build.gradle.kts index 3587b1752..23eab567d 100644 --- a/hub/build.gradle.kts +++ b/hub/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(libs.kotlinxCoroutinesJdk8) implementation(libs.ktorClientCore) implementation(libs.ktorClientCio) + implementation(libs.kotlinxDatetime) implementation(libs.batikTranscoder) implementation(libs.multiplatformSettings) implementation(libs.vlcj) diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt index 894f2c7c0..26435a363 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt @@ -15,6 +15,7 @@ import uk.co.caprica.vlcj.player.base.MediaPlayer import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter import uk.co.caprica.vlcj.player.base.State import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.REMOTE import uk.co.sentinelweb.cuer.app.ui.player.PlayerContract import uk.co.sentinelweb.cuer.app.ui.player.PlayerContract.PlayerCommand.* import uk.co.sentinelweb.cuer.app.ui.player.PlayerContract.View.Event.* @@ -23,9 +24,14 @@ import uk.co.sentinelweb.cuer.core.mappers.TimeFormatter import uk.co.sentinelweb.cuer.core.providers.PlayerConfigProvider import uk.co.sentinelweb.cuer.core.providers.TimeProvider import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper +import uk.co.sentinelweb.cuer.domain.MediaDomain.MediaTypeDomain.FILE import uk.co.sentinelweb.cuer.domain.PlayerNodeDomain import uk.co.sentinelweb.cuer.domain.PlayerStateDomain import uk.co.sentinelweb.cuer.domain.PlayerStateDomain.* +import uk.co.sentinelweb.cuer.domain.PlaylistItemDomain +import uk.co.sentinelweb.cuer.remote.server.LocalRepository +import uk.co.sentinelweb.cuer.remote.server.http +import uk.co.sentinelweb.cuer.remote.server.locator import java.awt.BorderLayout import java.awt.BorderLayout.* import java.awt.Color @@ -42,6 +48,7 @@ class VlcPlayerSwingWindow( private val folderListUseCase: GetFolderListUseCase, private val showHideControls: VlcPlayerShowHideControls, private val keyMap: VlcPlayerKeyMap, + private val localRepository: LocalRepository, ) : JFrame(), KoinComponent { lateinit var mediaPlayerComponent: CallbackMediaPlayerComponent @@ -395,11 +402,11 @@ class VlcPlayerSwingWindow( fun playStateChanged(command: PlayerContract.PlayerCommand) = when (command) { is Load -> { - command.platformId - .also { log.d("") } - .let { folderListUseCase.truncatedToFullFolderPath(it) } + command.item + .also { log.d("${it.media.platformId}") } + .let { mapPath(it) } ?.also { playItem(it) } - ?: log.d("Cannot get full path ${command.platformId}") + ?: log.d("Cannot get full path ${command.item.media.platformId}") } is Pause -> mediaPlayerComponent.mediaPlayer().controls().pause() @@ -409,6 +416,16 @@ class VlcPlayerSwingWindow( is SeekTo -> mediaPlayerComponent.mediaPlayer().controls().setTime(command.ms) }.also { log.d("command:${command::class.java.simpleName}") } + private fun mapPath(item: PlaylistItemDomain) = + item + .takeIf { it.id != null && it.id?.source == REMOTE && it.id?.locator != null } + ?.takeIf { localRepository.localNode.locator() != it.id?.locator } + ?.takeIf { it.media.mediaType == FILE } + ?.let { it.copy(media = it.media.copy(platformId = "${it.id?.locator?.http()}/video-stream/${it.media.platformId}")) } + ?.media + ?.platformId + ?: folderListUseCase.truncatedToFullFolderPath(item.media.platformId) + fun updateTexts(texts: PlayerContract.View.Model.Texts) { if (!this@VlcPlayerSwingWindow.isUndecorated) { title = texts.title diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerUiCoordinator.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerUiCoordinator.kt index 0dff62831..12bd3b546 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerUiCoordinator.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerUiCoordinator.kt @@ -222,7 +222,8 @@ class VlcPlayerUiCoordinator( coordinator = get(), folderListUseCase = get(), showHideControls = VlcPlayerShowHideControls(), - keyMap = VlcPlayerKeyMap() + keyMap = VlcPlayerKeyMap(), + localRepository = get() ) } } diff --git a/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt b/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt index 07abaed5c..4a0c72d8e 100644 --- a/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt +++ b/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt @@ -14,6 +14,9 @@ import io.ktor.server.plugins.cors.routing.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +import io.ktor.utils.io.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source @@ -49,6 +52,7 @@ import uk.co.sentinelweb.cuer.remote.server.message.ResponseMessage import uk.co.sentinelweb.cuer.remote.server.player.PlayerSessionContract.PlayerCommandMessage.* import uk.co.sentinelweb.cuer.remote.server.player.PlayerSessionHolder import uk.co.sentinelweb.cuer.remote.server.player.PlayerSessionMessageMapper +import java.io.File import java.io.PrintWriter import java.io.StringWriter @@ -302,6 +306,7 @@ class JvmRemoteWebServer( try { (postData) .let { deserialisePlaylistItem(it) } +// .let {it.copy(media = it.media.copy())} .let { item -> remotePlayerLaunchHost.launchVideo( item, @@ -327,6 +332,48 @@ class JvmRemoteWebServer( call.respond(HttpStatusCode.NotFound, "No folder with path: $path") } } + //http://192.168.1.12:9843/video-stream/torrent:::::farscape-s1:::::Farscape%20S01E03%20Back%20and%20Back%20and%20Back%20to%20the%20Future.mp4 + get("/video-stream/{filePath}") { + val filePath = call.parameters["filePath"] +// ?.substring("/video-stream".length) + ?.replace(":::::","/") + if (filePath == null) { + call.respond(HttpStatusCode.BadRequest, "File filePath is missing") + return@get + } + logWrapper.d("/video-stream/filepath: $filePath") + + val parentPath = filePath.substring(0, filePath.lastIndexOf("/")) + val item = getFolderListUseCase.getFolderList(parentPath) + ?.children?.filter { it.platformId?.endsWith(filePath)?:false } + val fullPath = getFolderListUseCase.truncatedToFullFolderPath(filePath) + logWrapper.d("/video-stream/filepath: $fullPath") + + val file = File(fullPath) + if (!file.exists()) { + call.respond(HttpStatusCode.NotFound, "File not found") + return@get + } + + val byteRange = call.request.header(HttpHeaders.Range) + if (byteRange != null) { + val range = parseRange(byteRange, file.length()) + if (range != null) { + val (start, end) = range + call.response.header(HttpHeaders.ContentRange, "bytes $start-$end/${file.length()}") + call.respondBytesWriter( + status = HttpStatusCode.PartialContent, + contentType = ContentType.defaultForFile(file) + ) { + writeFilePart(file, start, end) + } + } else { + call.respond(HttpStatusCode.RequestedRangeNotSatisfiable) + } + } else { + call.respondFile(file) + } + } static("/") { resources("") } @@ -352,3 +399,26 @@ suspend fun ApplicationCall.error( ContentType.Application.Json ) } + + +fun parseRange(rangeHeader: String, fileLength: Long): Pair? { + val range = rangeHeader.removePrefix("bytes=").split("-") + val start = range[0].toLongOrNull() ?: return null + val end = range.getOrNull(1)?.toLongOrNull() ?: (fileLength - 1) + return if (start <= end) start to end else null +} + +suspend fun ByteWriteChannel.writeFilePart(file: File, start: Long, end: Long) = + withContext(Dispatchers.IO) { + file.inputStream().use { inputStream -> + inputStream.skip(start) + var remaining = end - start + 1 + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + while (remaining > 0) { + val read = inputStream.read(buffer, 0, minOf(buffer.size.toLong(), remaining).toInt()) + if (read == -1) break + writeFully(buffer, 0, read) + remaining -= read + } + } + } diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/player/PlayerContract.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/player/PlayerContract.kt index aae038481..176d8e437 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/player/PlayerContract.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/player/PlayerContract.kt @@ -222,7 +222,7 @@ interface PlayerContract { sealed class PlayerCommand { object Play : PlayerCommand() object Pause : PlayerCommand() - data class Load(val platformId: String, val startPosition: Long) : PlayerCommand() + data class Load(val item: PlaylistItemDomain, val startPosition: Long) : PlayerCommand() data class SkipFwd(val ms: Int) : PlayerCommand() data class SkipBack(val ms: Int) : PlayerCommand() data class SeekTo(val ms: Long) : PlayerCommand() diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/player/PlayerStoreFactory.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/player/PlayerStoreFactory.kt index b668533c9..54379d11f 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/player/PlayerStoreFactory.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/player/PlayerStoreFactory.kt @@ -256,7 +256,7 @@ class PlayerStoreFactory( dispatch(Result.SetVideo(intent.item, queueConsumer.playlist)) publish( Label.Command( - Load(intent.item.media.platformId, intent.item.media.startPosition()) + Load(intent.item, intent.item.media.startPosition()) ) ) } @@ -286,7 +286,7 @@ class PlayerStoreFactory( when (playState) { VIDEO_CUED -> { item?.media?.apply { - publish(Label.Command(Load(platformId, startPosition()))) + publish(Label.Command(Load(item, startPosition()))) } Unit } From 968ca3b7e4ff319365be8c065842ac8606024fb0 Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Mon, 7 Oct 2024 13:42:13 +0100 Subject: [PATCH 02/16] #482 stop app reconnection on launch id already connected --- .../cuer/app/ui/filebrowser/FileBrowserViewModel.kt | 8 +++++++- .../co/sentinelweb/cuer/app/ui/ytplayer/AytViewHolder.kt | 6 +++--- .../app/ui/ytplayer/yt_land/YoutubeFullScreenActivity.kt | 2 +- .../uk/co/sentinelweb/cuer/app/ui/cast/CastController.kt | 3 +++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserViewModel.kt b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserViewModel.kt index 62afa6033..f08994b30 100644 --- a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserViewModel.kt +++ b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserViewModel.kt @@ -78,6 +78,7 @@ class FileBrowserViewModel( viewModelScope.launch { appModelObservable.value = appModelMapper.map(state = state, loading = true) state.sourceNode?.apply { + // fixme when this is possible on both platforms - need a broader check if (cuerCastPlayerWatcher.isWatching()) { launchRemotePlayer( cuerCastPlayerWatcher.remoteNode ?: throw IllegalStateException("No remote"), @@ -101,7 +102,12 @@ class FileBrowserViewModel( state.selectedFile ?: throw IllegalStateException(), screen.index ) - castController.connectCuerCast(state.sourceNode, screen) + // todo check if already connected to remote node + // todo also check if the dialog has connected + if (!castController.isConnected()) { + // assumes here that sourecnode == targetnode + castController.connectCuerCast(state.sourceNode, screen) + } remoteDialogLauncher.hideRemotesDialog() appModelObservable.value = appModelMapper.map(state = state, loading = false) } diff --git a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/ytplayer/AytViewHolder.kt b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/ytplayer/AytViewHolder.kt index 5cf5bedca..ca97bdbe8 100644 --- a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/ytplayer/AytViewHolder.kt +++ b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/ytplayer/AytViewHolder.kt @@ -200,9 +200,9 @@ class AytViewHolder( log.d(command.toString()) when (command) { is PlayerContract.PlayerCommand.Load -> { - log.d("PlayerCommand.Load: $_currentVideoId != ${command.platformId} start:${command.startPosition}") - if (_currentVideoId != command.platformId) { - _player?.loadVideo(command.platformId, command.startPosition / 1000f) + log.d("PlayerCommand.Load: $_currentVideoId != ${command.item.media.platformId} start:${command.startPosition}") + if (_currentVideoId != command.item.media.platformId) { + _player?.loadVideo(command.item.media.platformId, command.startPosition / 1000f) _progressBar?.isVisible = true } else { _player?.play() diff --git a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/ytplayer/yt_land/YoutubeFullScreenActivity.kt b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/ytplayer/yt_land/YoutubeFullScreenActivity.kt index 929e184d6..e1e45beb6 100644 --- a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/ytplayer/yt_land/YoutubeFullScreenActivity.kt +++ b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/ytplayer/yt_land/YoutubeFullScreenActivity.kt @@ -281,7 +281,7 @@ class YoutubeFullScreenActivity : YouTubeBaseActivity(), when (label) { is Command -> label.command.let { command -> when (command) { - is Load -> player.cueVideo(command.platformId, command.startPosition.toInt()) + is Load -> player.cueVideo(command.item.media.platformId, command.startPosition.toInt()) is Play -> player.play() is Pause -> player.pause() is SkipBack -> player.seekToMillis(player.currentTimeMillis - command.ms) diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/cast/CastController.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/cast/CastController.kt index 6f23c5009..99f32b11e 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/cast/CastController.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/cast/CastController.kt @@ -100,6 +100,9 @@ class CastController( } } + fun isConnected(): Boolean = + cuerCastPlayerWatcher.isWatching() || chromeCastHolder.isConnected() + fun killCurrentSession() { chromeCastHolder.destroy() cuerCastPlayerWatcher.cleanup() From a96c697473b658299780e8cb66d55d1fe5ac7ef6 Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Mon, 7 Oct 2024 21:55:00 +0100 Subject: [PATCH 03/16] #482 send to action for remotes --- .../ui/filebrowser/FileBrowserViewModel.kt | 2 +- .../cuer/app/ui/remotes/RemotesComposables.kt | 5 ++++ .../cuer/app/ui/remotes/RemotesFragment.kt | 9 +++++++ .../remotes/selector/RemotesDialogFragment.kt | 24 ++++++++++++------- .../remotes/selector/RemotesDialogLauncher.kt | 9 +++---- app/src/main/res/values/strings.xml | 1 + .../cuer/net/remote/RemoteStatusInteractor.kt | 7 ++++++ .../remote/server/AvailableMessageMapper.kt | 20 ++++++++++++++++ .../net/remote/RemoteStatusKtorInteractor.kt | 19 +++++++++++++++ .../cuer/net/remote/RemoteStatusService.kt | 2 +- .../cuer/app/ui/remotes/RemotesContract.kt | 5 ++++ .../cuer/app/ui/remotes/RemotesController.kt | 2 ++ .../app/ui/remotes/RemotesStoreFactory.kt | 14 ++++++++++- .../remotes/selector/RemotesDialogContract.kt | 8 ++++--- .../selector/RemotesDialogViewModel.kt | 14 ++++++++--- 15 files changed, 119 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserViewModel.kt b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserViewModel.kt index f08994b30..889305f25 100644 --- a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserViewModel.kt +++ b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserViewModel.kt @@ -86,7 +86,7 @@ class FileBrowserViewModel( ) } else { remoteDialogLauncher.launchRemotesDialog({ remoteNode, screen -> - launchRemotePlayer(remoteNode, screen) + launchRemotePlayer(remoteNode, screen ?: throw IllegalStateException("No screen selected")) }) } appModelObservable.value = appModelMapper.map(state = state, loading = false) diff --git a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesComposables.kt b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesComposables.kt index 1c9aba024..3f0251e30 100644 --- a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesComposables.kt +++ b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesComposables.kt @@ -257,6 +257,11 @@ object RemotesComposables { }) { Text(stringResource(R.string.menu_delete)) } + DropdownMenuItem(onClick = { + expanded = dispatchAndClose(view, OnActionSendTo(remote.domain)) + }) { + Text(stringResource(R.string.menu_sendto)) + } // Divider() // DropdownMenuItem(onClick = { // expanded = dispatchAndClose(view, OnActionPlaylists(remote.domain)) diff --git a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesFragment.kt b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesFragment.kt index bc1032dcd..23dee6b7b 100644 --- a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesFragment.kt +++ b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesFragment.kt @@ -169,6 +169,15 @@ class RemotesFragment : Fragment(), AndroidScopeComponent { value.node ) + is CuerSelectSendTo -> + remotesDialogLauncher.launchRemotesDialog( + { remoteNodeDomain, screen -> + remotesMviView.dispatch(Event.OnActionSendToSelected(value.sendNode, remoteNodeDomain)) + remotesDialogLauncher.hideRemotesDialog() + }, + null + ) + None -> Unit } } diff --git a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogFragment.kt b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogFragment.kt index 02af27ab8..732d87e97 100644 --- a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogFragment.kt +++ b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogFragment.kt @@ -21,8 +21,9 @@ import uk.co.sentinelweb.cuer.domain.PlayerNodeDomain import uk.co.sentinelweb.cuer.domain.RemoteNodeDomain class RemotesDialogFragment( - private val selectedListener: (RemoteNodeDomain, PlayerNodeDomain.Screen) -> Unit, - private val selectedNode: RemoteNodeDomain? + private val selectedListener: (RemoteNodeDomain, PlayerNodeDomain.Screen?) -> Unit, + private val selectedNode: RemoteNodeDomain?, + private val isSelectNodeOnly: Boolean, ) : DialogFragment(), AndroidScopeComponent { override val scope: Scope by fragmentScopeWithSource() @@ -31,7 +32,7 @@ class RemotesDialogFragment( private val compactPlayerScroll: CompactPlayerScroll by inject() private var _binding: FragmentComposeBinding? = null - private val binding get() = _binding ?: throw IllegalStateException("BrowseFragment view not bound") + private val binding get() = _binding ?: throw IllegalStateException("RemotesDialogFragment view not bound") init { log.tag(this) @@ -61,7 +62,12 @@ class RemotesDialogFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.listener = selectedListener - selectedNode?.apply { viewModel.onNodeSelected(this) } + if (isSelectNodeOnly) { + viewModel.setSelectNodeOnly() + } else { + selectedNode + ?.apply { viewModel.onNodeSelected(this) } + } binding.composeView.setContent { RemotesDialogComposeables.RemotesDialogUi(viewModel) } @@ -89,10 +95,11 @@ class RemotesDialogFragment( companion object { fun newInstance( - selected: (RemoteNodeDomain, PlayerNodeDomain.Screen) -> Unit, - selectedNode: RemoteNodeDomain? + selected: (RemoteNodeDomain, PlayerNodeDomain.Screen?) -> Unit, + selectedNode: RemoteNodeDomain?, + isSelectNodeOnly: Boolean ): RemotesDialogFragment { - return RemotesDialogFragment(selected, selectedNode) + return RemotesDialogFragment(selected, selectedNode, isSelectNodeOnly) } @JvmStatic @@ -110,5 +117,4 @@ class RemotesDialogFragment( } } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogLauncher.kt b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogLauncher.kt index f4005ac3f..ed5d8bd9c 100644 --- a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogLauncher.kt +++ b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogLauncher.kt @@ -12,10 +12,11 @@ class RemotesDialogLauncher( private var dialogFragment: DialogFragment? = null override fun launchRemotesDialog( - callback: (RemoteNodeDomain, PlayerNodeDomain.Screen) -> Unit, - node: RemoteNodeDomain? + callback: (RemoteNodeDomain, PlayerNodeDomain.Screen?) -> Unit, + node: RemoteNodeDomain?, + isSelectNodeOnly: Boolean ) { - dialogFragment = RemotesDialogFragment.newInstance(callback, node) + dialogFragment = RemotesDialogFragment.newInstance(callback, node, isSelectNodeOnly) dialogFragment?.show(activity.supportFragmentManager, CAST_DIALOG_FRAGMENT_TAG) } @@ -27,4 +28,4 @@ class RemotesDialogLauncher( companion object { val CAST_DIALOG_FRAGMENT_TAG = "RemotesDialogFragment" } -} \ No newline at end of file +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e837980bc..d71719dfa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,6 +24,7 @@ Pin Share Delete + Send to … Paste & add Restart Settings diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/net/remote/RemoteStatusInteractor.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/net/remote/RemoteStatusInteractor.kt index 444f12572..30773ef35 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/net/remote/RemoteStatusInteractor.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/net/remote/RemoteStatusInteractor.kt @@ -12,4 +12,11 @@ interface RemoteStatusInteractor { remote: RemoteNodeDomain, ): NetResult + @Throws(Exception::class) + suspend fun sendTo( + messageType: AvailableMessage.MsgType, + remote: RemoteNodeDomain, + target: RemoteNodeDomain, + ): NetResult + } diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/AvailableMessageMapper.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/AvailableMessageMapper.kt index fd498b570..d7b8f4a8d 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/AvailableMessageMapper.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/AvailableMessageMapper.kt @@ -24,12 +24,32 @@ class AvailableMessageMapper( ) } + fun mapToMulticastMessage(remoteNode: RemoteNodeDomain): AvailableMessage.DeviceInfo { + return AvailableMessage.DeviceInfo( + id = remoteNode.id, + hostname = remoteNode.hostname, + deviceType = remoteNode.deviceType, + version = config.version, + ipAddress = remoteNode.ipAddress, + port = remoteNode.port, + device = remoteNode.device, + authType = mapAuthType(remoteNode.authType), + versionCode = config.versionCode, + ) + } + private fun mapAuthType(authType: LocalNodeDomain.AuthConfig): AvailableMessage.AuthMethod = when (authType) { is LocalNodeDomain.AuthConfig.Open -> AvailableMessage.AuthMethod.Open is LocalNodeDomain.AuthConfig.Username -> AvailableMessage.AuthMethod.Username is LocalNodeDomain.AuthConfig.Confirm -> AvailableMessage.AuthMethod.Confirm } + private fun mapAuthType(authType: RemoteNodeDomain.AuthType): AvailableMessage.AuthMethod = when (authType) { + is RemoteNodeDomain.AuthType.Open -> AvailableMessage.AuthMethod.Open + is RemoteNodeDomain.AuthType.Username -> AvailableMessage.AuthMethod.Username + is RemoteNodeDomain.AuthType.Token -> AvailableMessage.AuthMethod.Confirm + } + fun mapFromMulticastMessage( msg: AvailableMessage.DeviceInfo, wifiState: WifiStateProvider.WifiState diff --git a/net/src/commonMain/kotlin/uk/co/sentinelweb/cuer/net/remote/RemoteStatusKtorInteractor.kt b/net/src/commonMain/kotlin/uk/co/sentinelweb/cuer/net/remote/RemoteStatusKtorInteractor.kt index 1b049465f..57e11a609 100644 --- a/net/src/commonMain/kotlin/uk/co/sentinelweb/cuer/net/remote/RemoteStatusKtorInteractor.kt +++ b/net/src/commonMain/kotlin/uk/co/sentinelweb/cuer/net/remote/RemoteStatusKtorInteractor.kt @@ -36,4 +36,23 @@ internal class RemoteStatusKtorInteractor( NetResult.Error(e) } } + + override suspend fun sendTo( + messageType: AvailableMessage.MsgType, + remote: RemoteNodeDomain, + target: RemoteNodeDomain + ): NetResult = + try { + val availableMessage = AvailableMessage( + messageType, + availableMessageMapper.mapToMulticastMessage(remote) + ) + val dto = + availableService.sendAvailable(target, RequestMessage(availableMessage)) + NetResult.Data(true) + } catch (e: RequestFailureException) { + NetResult.HttpError(e) + } catch (e: Exception) { + NetResult.Error(e) + } } diff --git a/net/src/commonMain/kotlin/uk/co/sentinelweb/cuer/net/remote/RemoteStatusService.kt b/net/src/commonMain/kotlin/uk/co/sentinelweb/cuer/net/remote/RemoteStatusService.kt index 31d630f9e..448b491e0 100644 --- a/net/src/commonMain/kotlin/uk/co/sentinelweb/cuer/net/remote/RemoteStatusService.kt +++ b/net/src/commonMain/kotlin/uk/co/sentinelweb/cuer/net/remote/RemoteStatusService.kt @@ -17,4 +17,4 @@ internal class RemoteStatusService( path = node.ipport() + AVAILABLE_API.PATH, body = msg, ) -} \ No newline at end of file +} diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesContract.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesContract.kt index 343214687..dc8c5a7ac 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesContract.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesContract.kt @@ -28,6 +28,8 @@ class RemotesContract { object ActionConfig : Intent() object ActionObscuredPerm : Intent() data class ActionPingNode(val remote: RemoteNodeDomain) : Intent() + data class ActionSendTo(val sendNode: RemoteNodeDomain) : Intent() + data class ActionSendToSelected(val sendNode: RemoteNodeDomain, val target: RemoteNodeDomain) : Intent() data class WifiStateChange(val wifiState: WifiStateProvider.WifiState) : Intent() data class RemoteUpdate(val remotes: List) : Intent() data class LocalUpdate(val local: LocalNodeDomain) : Intent() @@ -51,6 +53,7 @@ class RemotesContract { data class Message(val msg: String) : Label() data class CuerSelectScreen(val node: RemoteNodeDomain) : Label() data class CuerConnected(val remote: RemoteNodeDomain, val screen: Screen?) : Label() + data class CuerSelectSendTo(val sendNode: RemoteNodeDomain) : Label() } @@ -146,6 +149,8 @@ class RemotesContract { object OnActionConfigClicked : Event() object OnActionObscuredPermClicked : Event() data class OnActionPingNodeClicked(val remote: RemoteNodeDomain) : Event() + data class OnActionSendTo(val sendNode: RemoteNodeDomain) : Event() + data class OnActionSendToSelected(val sendNode: RemoteNodeDomain, val target: RemoteNodeDomain) : Event() data class OnActionDelete(val remote: RemoteNodeDomain) : Event() data class OnActionSync(val remote: RemoteNodeDomain) : Event() data class OnActionPlaylists(val remote: RemoteNodeDomain) : Event() diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesController.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesController.kt index 646870eb2..6905afc74 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesController.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesController.kt @@ -70,6 +70,8 @@ class RemotesController( is Event.OnActionFolders -> Intent.RemoteFolders(remote) is Event.OnActionCuerConnect -> Intent.CuerConnect(remote) is Event.OnActionCuerConnectScreen -> Intent.CuerConnectScreen(remote, screen) + is Event.OnActionSendTo -> Intent.ActionSendTo(sendNode) + is Event.OnActionSendToSelected -> Intent.ActionSendToSelected(sendNode, target) } } diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesStoreFactory.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesStoreFactory.kt index 1ebb6d68d..3aa251362 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesStoreFactory.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/RemotesStoreFactory.kt @@ -27,7 +27,7 @@ import uk.co.sentinelweb.cuer.remote.server.ServerState import uk.co.sentinelweb.cuer.remote.server.http import uk.co.sentinelweb.cuer.remote.server.message.AvailableMessage.MsgType.Ping -class RemotesStoreFactory constructor( +class RemotesStoreFactory( private val storeFactory: StoreFactory = DefaultStoreFactory(), private val log: LogWrapper, private val remoteServerManager: RemoteServerContract.Manager, @@ -106,6 +106,8 @@ class RemotesStoreFactory constructor( is Intent.LocalUpdate -> dispatch(Result.UpdateServerState) is Intent.CuerConnect -> cuerConnect(intent) is Intent.CuerConnectScreen -> cuerConnectScreen(intent) + is Intent.ActionSendTo -> sendTo(intent) + is Intent.ActionSendToSelected -> sendToSelected(intent) } private fun cuerConnect(intent: Intent.CuerConnect) { @@ -149,6 +151,16 @@ class RemotesStoreFactory constructor( } } + private fun sendToSelected(intent: Intent.ActionSendToSelected) { + coroutines.ioScope.launch { + remoteStatusInteractor.sendTo(Ping, intent.sendNode, intent.target).isSuccessful + } + } + + private fun sendTo(intent: Intent.ActionSendTo) { + publish(Label.CuerSelectSendTo(intent.sendNode)) + } + private fun config(intent: Intent) { publish(Label.ActionConfig) } diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogContract.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogContract.kt index 7079384b7..b9012e944 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogContract.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogContract.kt @@ -7,8 +7,9 @@ import uk.co.sentinelweb.cuer.domain.RemoteNodeDomain interface RemotesDialogContract { interface Launcher { fun launchRemotesDialog( - callback: (RemoteNodeDomain, PlayerNodeDomain.Screen) -> Unit, - node: RemoteNodeDomain? = null + callback: (RemoteNodeDomain, PlayerNodeDomain.Screen?) -> Unit, + node: RemoteNodeDomain? = null, + isSelectNodeOnly: Boolean = false, ) fun hideRemotesDialog() @@ -17,6 +18,7 @@ interface RemotesDialogContract { data class State( var selectedNode: RemoteNodeDomain? = null, var selectedNodeConfig: PlayerNodeDomain? = null, + var isSelectNodeOnly: Boolean = false, ) data class Model( @@ -26,4 +28,4 @@ interface RemotesDialogContract { val blank = Model(listOf()) } } -} \ No newline at end of file +} diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogViewModel.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogViewModel.kt index ad4b411da..6920a8d69 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogViewModel.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogViewModel.kt @@ -21,7 +21,7 @@ class RemotesDialogViewModel( private val coroutines: CoroutineContextProvider, ) { - lateinit var listener: (RemoteNodeDomain, PlayerNodeDomain.Screen) -> Unit + lateinit var listener: (RemoteNodeDomain, PlayerNodeDomain.Screen?) -> Unit private val _model = MutableStateFlow(Model.blank) val model: Flow = _model @@ -34,7 +34,11 @@ class RemotesDialogViewModel( fun onNodeSelected(node: RemoteNodeDomain) { coroutines.mainScope.launch { - remoteSelected(node) + if (state.isSelectNodeOnly) { + listener(node, null) + } else { + remoteSelected(node) + } } } @@ -59,7 +63,11 @@ class RemotesDialogViewModel( } } - private suspend fun map() { + private fun map() { _model.value = Model(repo.remoteNodes.map { mapper.mapRemoteNode(it) }) } + + fun setSelectNodeOnly() { + state.isSelectNodeOnly = true + } } From a4de055690fe264e15ae9540852e6df3fe68d29b Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Mon, 7 Oct 2024 22:48:41 +0100 Subject: [PATCH 04/16] #482 show remote dialog selector for hub --- .../uk/co/sentinelweb/cuer/hub/di/Modules.kt | 2 + .../cuer/hub/ui/remotes/RemotesComposables.kt | 9 ++- .../hub/ui/remotes/RemotesUiCoordinator.kt | 29 ++++++++- .../remotes/selector/RemotesDialogLauncher.kt | 60 +++++++++++++++++++ .../RemotesDialogLauncherComposeables.kt | 52 ++++++++++++++++ 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/selector/RemotesDialogLauncher.kt create mode 100644 hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/selector/RemotesDialogLauncherComposeables.kt diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/di/Modules.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/di/Modules.kt index 8462f975b..790d2f33d 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/di/Modules.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/di/Modules.kt @@ -41,6 +41,7 @@ import uk.co.sentinelweb.cuer.hub.ui.player.cast.* import uk.co.sentinelweb.cuer.hub.ui.player.vlc.VlcPlayerUiCoordinator import uk.co.sentinelweb.cuer.hub.ui.preferences.PreferencesUiCoordinator import uk.co.sentinelweb.cuer.hub.ui.remotes.RemotesUiCoordinator +import uk.co.sentinelweb.cuer.hub.ui.remotes.selector.RemotesDialogLauncher import uk.co.sentinelweb.cuer.hub.util.permission.EmptyLocationPermissionLaunch import uk.co.sentinelweb.cuer.hub.util.platform.getNodeDeviceType import uk.co.sentinelweb.cuer.hub.util.platform.getOSData @@ -80,6 +81,7 @@ object Modules { PreferencesUiCoordinator.uiModule, FilesUiCoordinator.uiModule, VlcPlayerUiCoordinator.uiModule, + RemotesDialogLauncher.launcherModule, ) private val resourcesModule = module { diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/RemotesComposables.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/RemotesComposables.kt index ba56b79ce..9a80e5337 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/RemotesComposables.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/RemotesComposables.kt @@ -28,6 +28,7 @@ import uk.co.sentinelweb.cuer.hub.ui.common.image.ImageEnumMapper import uk.co.sentinelweb.cuer.hub.ui.common.image.ImageFromUrl import uk.co.sentinelweb.cuer.hub.ui.common.image.ImageSvg import uk.co.sentinelweb.cuer.hub.ui.local.LocalComposables +import uk.co.sentinelweb.cuer.hub.ui.remotes.selector.RemotesDialogLauncherComposeables.ShowRemotesDialogIfNecessary import uk.co.sentinelweb.cuer.remote.server.ServerState object RemotesComposables { @@ -38,6 +39,7 @@ object RemotesComposables { fun RemotesUi(coordinator: RemotesUiCoordinator) { val state = coordinator.modelObservable.collectAsState(initial = blankModel()) RemotesView(state.value, coordinator) + ShowRemotesDialogIfNecessary(coordinator.remotesDialogLauncher) } @Composable @@ -232,6 +234,11 @@ object RemotesComposables { }) { Text("Delete") } + DropdownMenuItem(onClick = { + expanded = dispatchAndClose(view, Event.OnActionSendTo(remote.domain)) + }) { + Text("Send To ...") + } Divider() DropdownMenuItem(onClick = { expanded = dispatchAndClose(view, Event.OnActionPlaylists(remote.domain)) @@ -250,4 +257,4 @@ object RemotesComposables { view.dispatch(event) return false } -} \ No newline at end of file +} diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/RemotesUiCoordinator.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/RemotesUiCoordinator.kt index ca3014861..bc11a6332 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/RemotesUiCoordinator.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/RemotesUiCoordinator.kt @@ -13,6 +13,7 @@ import org.koin.dsl.module import uk.co.sentinelweb.cuer.app.ui.cast.CastController import uk.co.sentinelweb.cuer.app.ui.cast.EmptyCastDialogLauncher import uk.co.sentinelweb.cuer.app.ui.remotes.RemotesContract +import uk.co.sentinelweb.cuer.app.ui.remotes.RemotesContract.MviStore.Label.CuerSelectSendTo import uk.co.sentinelweb.cuer.app.ui.remotes.RemotesContract.View.Event import uk.co.sentinelweb.cuer.app.ui.remotes.RemotesContract.View.Model import uk.co.sentinelweb.cuer.app.ui.remotes.RemotesController @@ -20,10 +21,15 @@ import uk.co.sentinelweb.cuer.app.ui.remotes.RemotesModelMapper import uk.co.sentinelweb.cuer.app.ui.remotes.RemotesStoreFactory import uk.co.sentinelweb.cuer.app.util.chromecast.listener.EmptyChromecastDialogWrapper import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper +import uk.co.sentinelweb.cuer.domain.PlayerNodeDomain +import uk.co.sentinelweb.cuer.domain.RemoteNodeDomain +import uk.co.sentinelweb.cuer.domain.ext.name import uk.co.sentinelweb.cuer.hub.ui.local.LocalUiCoordinator +import uk.co.sentinelweb.cuer.hub.ui.remotes.selector.RemotesDialogLauncher import uk.co.sentinelweb.cuer.hub.util.extension.DesktopScopeComponent import uk.co.sentinelweb.cuer.hub.util.extension.desktopScopeWithSource import uk.co.sentinelweb.cuer.hub.util.view.UiCoordinator +import uk.co.sentinelweb.cuer.remote.server.locator class RemotesUiCoordinator : UiCoordinator, @@ -37,12 +43,16 @@ class RemotesUiCoordinator : override var modelObservable = MutableStateFlow(Model.blankModel()) private set + val remotesDialogLauncher: RemotesDialogLauncher by scope.inject() + private val controller: RemotesController by scope.inject() private val log: LogWrapper by inject() private val lifecycle: LifecycleRegistry by inject() private var _localCoordinator: LocalUiCoordinator? = null + + init { log.tag(this) } @@ -64,6 +74,20 @@ class RemotesUiCoordinator : override fun processLabel(label: RemotesContract.MviStore.Label) { log.d("label: $label") + when (label) { + is CuerSelectSendTo -> { + remotesDialogLauncher.launchRemotesDialog( + { remoteNodeDomain, screen -> + println("selected node: target: ${remoteNodeDomain.name()}, targets: ${label.sendNode.name()}") + dispatch(Event.OnActionSendToSelected(label.sendNode, remoteNodeDomain)) + remotesDialogLauncher.hideRemotesDialog() + }, + null, + true, + ) + } + else -> Unit + } } override fun render(model: Model) { @@ -135,7 +159,10 @@ class RemotesUiCoordinator : log = get() ) } + scoped { + RemotesDialogLauncher() + } } } } -} \ No newline at end of file +} diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/selector/RemotesDialogLauncher.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/selector/RemotesDialogLauncher.kt new file mode 100644 index 000000000..4558110de --- /dev/null +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/selector/RemotesDialogLauncher.kt @@ -0,0 +1,60 @@ +package uk.co.sentinelweb.cuer.hub.ui.remotes.selector + +import kotlinx.coroutines.flow.MutableStateFlow +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.dsl.module +import uk.co.sentinelweb.cuer.app.ui.remotes.selector.RemotesDialogContract +import uk.co.sentinelweb.cuer.app.ui.remotes.selector.RemotesDialogViewModel +import uk.co.sentinelweb.cuer.domain.PlayerNodeDomain +import uk.co.sentinelweb.cuer.domain.RemoteNodeDomain + +class RemotesDialogLauncher : RemotesDialogContract.Launcher, KoinComponent { + + var modelObservable = MutableStateFlow(DisplayModel.blankModel) + private set + + val viewModel: RemotesDialogViewModel by inject() + + data class DisplayModel( + val isSelectRemotesVisible: Boolean, + ) { + companion object { + val blankModel = DisplayModel(false) + } + } + + override fun launchRemotesDialog( + callback: (RemoteNodeDomain, PlayerNodeDomain.Screen?) -> Unit, + node: RemoteNodeDomain?, + isSelectNodeOnly: Boolean + ) { + viewModel.listener = callback + if (isSelectNodeOnly) { + viewModel.setSelectNodeOnly() + } else { + node?.also { viewModel.onNodeSelected(it)} + } + modelObservable.value = DisplayModel(true) + } + + override fun hideRemotesDialog() { + println("hideRemotesDialog()") + modelObservable.value = DisplayModel(false) + } + + companion object { + @JvmStatic + val launcherModule = module { + factory { + RemotesDialogViewModel( + repo = get(), + mapper = get(), + coroutines = get(), + playerInteractor = get(), + state = RemotesDialogContract.State() + ) + } + } + } +} diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/selector/RemotesDialogLauncherComposeables.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/selector/RemotesDialogLauncherComposeables.kt new file mode 100644 index 000000000..4c74bb9ec --- /dev/null +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/selector/RemotesDialogLauncherComposeables.kt @@ -0,0 +1,52 @@ +package uk.co.sentinelweb.cuer.hub.ui.remotes.selector + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.AlertDialog +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import org.jetbrains.skia.Surface +import uk.co.sentinelweb.cuer.app.ui.remotes.selector.RemotesDialogComposeables +import uk.co.sentinelweb.cuer.hub.ui.remotes.selector.RemotesDialogLauncher.DisplayModel + +object RemotesDialogLauncherComposeables { + + @Composable + fun ShowRemotesDialogIfNecessary(remotesDialogLauncher: RemotesDialogLauncher) { + val displayModel = remotesDialogLauncher.modelObservable.collectAsState(DisplayModel.blankModel) + + if (displayModel.value.isSelectRemotesVisible) { +// AlertDialog( +// modifier = Modifier.height(540.dp), +// onDismissRequest = {remotesDialogLauncher.hideRemotesDialog()}, +// title = {}, +// buttons = {}, +// text = { +// RemotesDialogComposeables.RemotesDialogUi(remotesDialogLauncher.viewModel) +// } +// ) + Dialog(onDismissRequest = { remotesDialogLauncher.hideRemotesDialog() }) { + Surface( + shape = MaterialTheme.shapes.medium, + elevation = 24.dp + ) { + Box( + modifier = Modifier + .wrapContentSize() + .background(Color.White) + ) { + RemotesDialogComposeables.RemotesDialogUi(remotesDialogLauncher.viewModel) + } + } + } + } + } +} From 54e511ed593faac3d1dec0597ad97f050deca28a Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Mon, 7 Oct 2024 23:18:39 +0100 Subject: [PATCH 05/16] #482 disable attemptRestoreConnection to test bug --- .../util/cuercast/CuerCastPlayerWatcher.kt | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/util/cuercast/CuerCastPlayerWatcher.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/util/cuercast/CuerCastPlayerWatcher.kt index d11803860..ff8a87590 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/util/cuercast/CuerCastPlayerWatcher.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/util/cuercast/CuerCastPlayerWatcher.kt @@ -101,22 +101,22 @@ class CuerCastPlayerWatcher( fun isCommunicating(): Boolean = state.isCommunicating suspend fun attemptRestoreConnection(playerControls: PlayerContract.PlayerControls): Boolean { - prefs.curecastRemoteNodeName - ?.also { name -> - remotesRepository.getByName(name) - ?.also { foundNode -> - prefs.cuerCastScreen?.also { screenIndex -> - screen = - remotePlayerInteractor.getPlayerConfig(foundNode.locator()) - .data - ?.screens - ?.getOrNull(screenIndex) - ?.also { remoteNode = foundNode } - ?.also { mainPlayerControls = playerControls } - return true - } - } - } +// prefs.curecastRemoteNodeName +// ?.also { name -> +// remotesRepository.getByName(name) +// ?.also { foundNode -> +// prefs.cuerCastScreen?.also { screenIndex -> +// screen = +// remotePlayerInteractor.getPlayerConfig(foundNode.locator()) +// .data +// ?.screens +// ?.getOrNull(screenIndex) +// ?.also { remoteNode = foundNode } +// ?.also { mainPlayerControls = playerControls } +// return true +// } +// } +// } return false } @@ -151,11 +151,11 @@ class CuerCastPlayerWatcher( ?.apply { mainPlayerControls?.setPlaylistItem(item) } ?.apply { item.media.duration?.let { mainPlayerControls?.setDuration(it / 1000f) } } ?.apply { item.media.positon?.let { mainPlayerControls?.setCurrentSecond(it / 1000f) } } - ?.apply {// fixme enable when we have playlist + ?.apply { // fixme enable when we have playlist mainPlayerControls?.setButtons(Buttons(false, false, true)) } ?.apply { mediaSessionManager.setMedia(this.item.media, null) } // fixme get playlist - ?.apply {// fixme get liveOffset, playlist + ?.apply { // fixme get liveOffset, playlist mediaSessionManager.updatePlaybackState(this.item.media, this.playbackState, null, null) } ?.apply { mainPlayerControls?.setVolume(volume / volumeMax) } From c545ebef11c6a4df762a845aad16b64d35a98cbc Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Fri, 11 Oct 2024 20:32:19 +0200 Subject: [PATCH 06/16] #482 reset state RemotesDialogViewModel.kt on new invocation --- .github/workflows/android.yml | 10 ----- .../remotes/selector/RemotesDialogLauncher.kt | 1 + .../selector/RemotesDialogComposeables.kt | 4 +- .../selector/RemotesDialogViewModel.kt | 6 +++ .../util/cuercast/CuerCastPlayerWatcher.kt | 38 ++++++++++--------- 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 80ee157be..5e4c5dddb 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -3,22 +3,12 @@ name: Android CI on: push: branches: [ develop ] - # paths: - # - app/** - # - remote/** - # - shared/** - # - net/** pull_request: branches: - feature/** - bugfix/** - develop - main -# paths: -# - app/** -# - remote/** -# - shared/** -# - net/** jobs: build: diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/selector/RemotesDialogLauncher.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/selector/RemotesDialogLauncher.kt index 4558110de..d50b63fd7 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/selector/RemotesDialogLauncher.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/remotes/selector/RemotesDialogLauncher.kt @@ -29,6 +29,7 @@ class RemotesDialogLauncher : RemotesDialogContract.Launcher, KoinComponent { node: RemoteNodeDomain?, isSelectNodeOnly: Boolean ) { + viewModel.resetState() viewModel.listener = callback if (isSelectNodeOnly) { viewModel.setSelectNodeOnly() diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogComposeables.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogComposeables.kt index 70c22416a..f94ecc7e7 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogComposeables.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogComposeables.kt @@ -87,12 +87,10 @@ object RemotesDialogComposeables { remote: RemotesContract.View.RemoteNodeModel, viewModel: RemotesDialogViewModel ) { - // fixme why isAvailable false? val contentColor = remote.domain.isAvailable .takeIf { it } ?.let { MaterialTheme.colorScheme.onSurface } ?: MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) -// val contentColor = MaterialTheme.colors.onSurface Column( modifier = Modifier .fillMaxWidth() @@ -108,7 +106,7 @@ object RemotesDialogComposeables { Image( painter = painterResource(RemotesIconMapper.map(remote.deviceType)), contentDescription = "Remote Icon", - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), + colorFilter = ColorFilter.tint(contentColor), modifier = Modifier .size(48.dp) .padding(8.dp) diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogViewModel.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogViewModel.kt index 6920a8d69..7ad4b545a 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogViewModel.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/remotes/selector/RemotesDialogViewModel.kt @@ -70,4 +70,10 @@ class RemotesDialogViewModel( fun setSelectNodeOnly() { state.isSelectNodeOnly = true } + + fun resetState() { + state.selectedNode = null + state.selectedNodeConfig = null + state.isSelectNodeOnly = false + } } diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/util/cuercast/CuerCastPlayerWatcher.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/util/cuercast/CuerCastPlayerWatcher.kt index ff8a87590..910bceb77 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/util/cuercast/CuerCastPlayerWatcher.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/util/cuercast/CuerCastPlayerWatcher.kt @@ -31,6 +31,7 @@ class CuerCastPlayerWatcher( private val remotesRepository: RemotesRepository, private val log: LogWrapper, ) { + data class State( var lastMessage: PlayerSessionContract.PlayerStatusMessage? = null, var isCommunicating: Boolean = false @@ -43,7 +44,8 @@ class CuerCastPlayerWatcher( var remoteNode: RemoteNodeDomain? = null get() = field set(value) { - prefs.curecastRemoteNodeName = value?.hostname + log.e("Remote node set to: ${value?.name()} ${value?.locator()}", Exception()) + prefs.curecastRemoteNodeName = value?.hostname // fixme store guid field = value } @@ -87,7 +89,6 @@ class CuerCastPlayerWatcher( startPolling() } field = value - //currentButtons?.let { value?.setButtons(it) } } fun getConnectionDescription() = @@ -98,25 +99,26 @@ class CuerCastPlayerWatcher( fun isWatching(): Boolean = remoteNode != null fun isPlaying(): Boolean = state.lastMessage?.playbackState == PlayerStateDomain.PLAYING + fun isCommunicating(): Boolean = state.isCommunicating suspend fun attemptRestoreConnection(playerControls: PlayerContract.PlayerControls): Boolean { -// prefs.curecastRemoteNodeName -// ?.also { name -> -// remotesRepository.getByName(name) -// ?.also { foundNode -> -// prefs.cuerCastScreen?.also { screenIndex -> -// screen = -// remotePlayerInteractor.getPlayerConfig(foundNode.locator()) -// .data -// ?.screens -// ?.getOrNull(screenIndex) -// ?.also { remoteNode = foundNode } -// ?.also { mainPlayerControls = playerControls } -// return true -// } -// } -// } + prefs.curecastRemoteNodeName + ?.also { name -> + remotesRepository.getByName(name) + ?.also { foundNode -> + prefs.cuerCastScreen?.also { screenIndex -> + screen = + remotePlayerInteractor.getPlayerConfig(foundNode.locator()) + .data + ?.screens + ?.getOrNull(screenIndex) + ?.also { remoteNode = foundNode } + ?.also { mainPlayerControls = playerControls } + return true + } + } + } return false } From 73f35fa5cc03f3fcffc9b727e838e43f9a7f0fcc Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Fri, 11 Oct 2024 20:52:18 +0200 Subject: [PATCH 07/16] #482 connect to correct target node --- .../cuer/app/ui/filebrowser/FileBrowserViewModel.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserViewModel.kt b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserViewModel.kt index 889305f25..523c425ea 100644 --- a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserViewModel.kt +++ b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserViewModel.kt @@ -85,8 +85,8 @@ class FileBrowserViewModel( cuerCastPlayerWatcher.screen ?: throw IllegalStateException("No remote screen") ) } else { - remoteDialogLauncher.launchRemotesDialog({ remoteNode, screen -> - launchRemotePlayer(remoteNode, screen ?: throw IllegalStateException("No screen selected")) + remoteDialogLauncher.launchRemotesDialog({ targetNode, screen -> + launchRemotePlayer(targetNode, screen ?: throw IllegalStateException("No screen selected")) }) } appModelObservable.value = appModelMapper.map(state = state, loading = false) @@ -94,11 +94,11 @@ class FileBrowserViewModel( } } - private fun launchRemotePlayer(remoteNode: RemoteNodeDomain, screen: PlayerNodeDomain.Screen) { + private fun launchRemotePlayer(targetNode: RemoteNodeDomain, screen: PlayerNodeDomain.Screen) { viewModelScope.launch { appModelObservable.value = appModelMapper.map(state = state, loading = true) playerInteractor.launchPlayerVideo( - remoteNode.locator(), + targetNode.locator(), state.selectedFile ?: throw IllegalStateException(), screen.index ) @@ -106,7 +106,7 @@ class FileBrowserViewModel( // todo also check if the dialog has connected if (!castController.isConnected()) { // assumes here that sourecnode == targetnode - castController.connectCuerCast(state.sourceNode, screen) + castController.connectCuerCast(targetNode, screen) } remoteDialogLauncher.hideRemotesDialog() appModelObservable.value = appModelMapper.map(state = state, loading = false) From 079fc4329e74abab4a830b1a15304a2b180cace6 Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Fri, 11 Oct 2024 21:25:53 +0200 Subject: [PATCH 08/16] #482 rewrite ids -> Remote when serving files out --- .../ui/player/vlc/VlcPlayerUiCoordinator.kt | 4 +- .../cuer/remote/server/JvmRemoteWebServer.kt | 49 +++++++++++++++++-- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerUiCoordinator.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerUiCoordinator.kt index 12bd3b546..c26770ba3 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerUiCoordinator.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerUiCoordinator.kt @@ -37,6 +37,7 @@ import uk.co.sentinelweb.cuer.app.util.mediasession.MediaSessionContract import uk.co.sentinelweb.cuer.core.providers.CoroutineContextProvider import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper import uk.co.sentinelweb.cuer.domain.* +import uk.co.sentinelweb.cuer.domain.ext.serialise import uk.co.sentinelweb.cuer.hub.ui.home.HomeUiCoordinator import uk.co.sentinelweb.cuer.hub.util.extension.DesktopScopeComponent import uk.co.sentinelweb.cuer.hub.util.extension.desktopScopeWithSource @@ -81,7 +82,7 @@ class VlcPlayerUiCoordinator( scope.get(VlcPlayerSwingWindow::class) .apply { assemble(screen) } .also { SleepPreventer.preventSleep() } - } else throw IllegalStateException("Can't find VLC") + } else error("Can't find VLC") } override fun destroy() { @@ -148,6 +149,7 @@ class VlcPlayerUiCoordinator( if (playlist.id?.source == MEMORY) { playlistOrchestrator.save(playlist, playlist.id!!.deepOptions()) } + log.d("showPlayer: play itemId:${item.id?.serialise()}") playerItemLoader.setPlaylistAndItem( PlaylistAndItemDomain( playlistId = playlist.id, diff --git a/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt b/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt index 4a0c72d8e..dff8cc1c9 100644 --- a/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt +++ b/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt @@ -21,11 +21,13 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.LOCAL +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.REMOTE import uk.co.sentinelweb.cuer.app.orchestrator.toGuidIdentifier import uk.co.sentinelweb.cuer.app.service.remote.RemoteServerContract import uk.co.sentinelweb.cuer.app.usecase.GetFolderListUseCase import uk.co.sentinelweb.cuer.core.providers.PlayerConfigProvider import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper +import uk.co.sentinelweb.cuer.domain.PlaylistAndChildrenDomain import uk.co.sentinelweb.cuer.domain.ext.deserialisePlaylistItem import uk.co.sentinelweb.cuer.domain.ext.domainMessageJsonSerializer import uk.co.sentinelweb.cuer.domain.ext.serialise @@ -305,8 +307,9 @@ class JvmRemoteWebServer( val postData = call.receiveText() try { (postData) + .also { logWrapper.d("launchitem: postdata: ${it}") } .let { deserialisePlaylistItem(it) } -// .let {it.copy(media = it.media.copy())} + .also { logWrapper.d("launchitem: ${it.id?.serialise()}") } .let { item -> remotePlayerLaunchHost.launchVideo( item, @@ -326,6 +329,7 @@ class JvmRemoteWebServer( val path = call.parameters[FOLDER_LIST_API.P_PARAM] logWrapper.d("Folder: $path") getFolderListUseCase.getFolderList(path) + ?.let { rewriteIdsToRemote(it) } // todo rewrite platformid -> http ?.let { ResponseDomain(it) } ?.apply { call.respondText(serialise(), ContentType.Application.Json) } ?: apply { @@ -336,7 +340,7 @@ class JvmRemoteWebServer( get("/video-stream/{filePath}") { val filePath = call.parameters["filePath"] // ?.substring("/video-stream".length) - ?.replace(":::::","/") + ?.replace(":::::", "/") if (filePath == null) { call.respond(HttpStatusCode.BadRequest, "File filePath is missing") return@get @@ -345,8 +349,8 @@ class JvmRemoteWebServer( val parentPath = filePath.substring(0, filePath.lastIndexOf("/")) val item = getFolderListUseCase.getFolderList(parentPath) - ?.children?.filter { it.platformId?.endsWith(filePath)?:false } - val fullPath = getFolderListUseCase.truncatedToFullFolderPath(filePath) + ?.children?.filter { it.platformId?.endsWith(filePath) ?: false } + val fullPath = getFolderListUseCase.truncatedToFullFolderPath(filePath) logWrapper.d("/video-stream/filepath: $fullPath") val file = File(fullPath) @@ -379,6 +383,43 @@ class JvmRemoteWebServer( } } } + + private fun rewriteIdsToRemote(it: PlaylistAndChildrenDomain) = it.copy( + playlist = it.playlist.copy( + id = it.playlist.id?.copy( + source = REMOTE, + locator = localRepository.localNode.locator() + ), + items = it.playlist.items.map { + it.copy( + id = it.id?.copy( + source = REMOTE, + locator = localRepository.localNode.locator() + ), + media = it.media.copy( + id = it.media.id?.copy( + source = REMOTE, + locator = localRepository.localNode.locator() + ), + channelData = it.media.channelData.copy( + id = it.media.channelData.id?.copy( + source = REMOTE, + locator = localRepository.localNode.locator() + ) + ) + ) + ) + } + ), + children = it.children.map { child -> + child.copy( + id = child.id?.copy( + source = REMOTE, + locator = localRepository.localNode.locator() + ) + ) + } + ) } From 93fe4b301109762f04a673c46533130d9de0aaed Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Fri, 11 Oct 2024 21:42:36 +0200 Subject: [PATCH 09/16] #482 rewrite ids -> LOCAL_NETWORK when serving files out --- .../cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt | 4 ++-- .../cuer/remote/server/JvmRemoteWebServer.kt | 11 +++++------ .../co/sentinelweb/cuer/app/di/SharedAppModule.kt | 2 +- .../co/sentinelweb/cuer/app/queue/QueueMediator.kt | 4 ++++ .../cuer/app/usecase/PlaylistMediaUpdateUsecase.kt | 14 ++++++++++---- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt index 26435a363..dbc916a40 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt @@ -15,7 +15,7 @@ import uk.co.caprica.vlcj.player.base.MediaPlayer import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter import uk.co.caprica.vlcj.player.base.State import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent -import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.REMOTE +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.LOCAL_NETWORK import uk.co.sentinelweb.cuer.app.ui.player.PlayerContract import uk.co.sentinelweb.cuer.app.ui.player.PlayerContract.PlayerCommand.* import uk.co.sentinelweb.cuer.app.ui.player.PlayerContract.View.Event.* @@ -418,7 +418,7 @@ class VlcPlayerSwingWindow( private fun mapPath(item: PlaylistItemDomain) = item - .takeIf { it.id != null && it.id?.source == REMOTE && it.id?.locator != null } + .takeIf { it.id != null && it.id?.source == LOCAL_NETWORK && it.id?.locator != null } ?.takeIf { localRepository.localNode.locator() != it.id?.locator } ?.takeIf { it.media.mediaType == FILE } ?.let { it.copy(media = it.media.copy(platformId = "${it.id?.locator?.http()}/video-stream/${it.media.platformId}")) } diff --git a/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt b/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt index dff8cc1c9..aaa953f3c 100644 --- a/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt +++ b/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt @@ -20,8 +20,7 @@ import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source -import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.LOCAL -import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.REMOTE +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.* import uk.co.sentinelweb.cuer.app.orchestrator.toGuidIdentifier import uk.co.sentinelweb.cuer.app.service.remote.RemoteServerContract import uk.co.sentinelweb.cuer.app.usecase.GetFolderListUseCase @@ -387,23 +386,23 @@ class JvmRemoteWebServer( private fun rewriteIdsToRemote(it: PlaylistAndChildrenDomain) = it.copy( playlist = it.playlist.copy( id = it.playlist.id?.copy( - source = REMOTE, + source = LOCAL_NETWORK, locator = localRepository.localNode.locator() ), items = it.playlist.items.map { it.copy( id = it.id?.copy( - source = REMOTE, + source = LOCAL_NETWORK, locator = localRepository.localNode.locator() ), media = it.media.copy( id = it.media.id?.copy( - source = REMOTE, + source = LOCAL_NETWORK, locator = localRepository.localNode.locator() ), channelData = it.media.channelData.copy( id = it.media.channelData.id?.copy( - source = REMOTE, + source = LOCAL_NETWORK, locator = localRepository.localNode.locator() ) ) diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/di/SharedAppModule.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/di/SharedAppModule.kt index 03d940853..097fbca4f 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/di/SharedAppModule.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/di/SharedAppModule.kt @@ -157,7 +157,7 @@ object SharedAppModule { coProvider = get() ) } - factory { PlaylistMediaUpdateUsecase(get()) } + factory { PlaylistMediaUpdateUsecase(get(), get()) } factory { PlaylistOrDefaultUsecase( playlistDatabaseRepository = get(), diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/queue/QueueMediator.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/queue/QueueMediator.kt index 994f07393..496720912 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/queue/QueueMediator.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/queue/QueueMediator.kt @@ -9,6 +9,8 @@ import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Companion.NO_PLAYLIST import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Identifier import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Operation.* +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.LOCAL +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.MEMORY import uk.co.sentinelweb.cuer.app.orchestrator.deepOptions import uk.co.sentinelweb.cuer.app.orchestrator.flatOptions import uk.co.sentinelweb.cuer.app.usecase.PlaylistMediaUpdateUsecase @@ -236,6 +238,8 @@ class QueueMediator constructor( private suspend fun updateCurrentItemFromMedia(updatedMedia: MediaDomain) { state.currentItem = state.currentItem + // todo support local netowrk + ?.takeIf { listOf(MEMORY, LOCAL).contains(it.id?.source) } ?.run { media.let { MediaPositionUpdateDomain( diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/usecase/PlaylistMediaUpdateUsecase.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/usecase/PlaylistMediaUpdateUsecase.kt index 344673f1b..e70cabd9a 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/usecase/PlaylistMediaUpdateUsecase.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/usecase/PlaylistMediaUpdateUsecase.kt @@ -2,15 +2,20 @@ package uk.co.sentinelweb.cuer.app.usecase import uk.co.sentinelweb.cuer.app.orchestrator.MediaOrchestrator import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract -import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.LOCAL -import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.MEMORY +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.* +import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper import uk.co.sentinelweb.cuer.domain.MediaDomain import uk.co.sentinelweb.cuer.domain.PlaylistDomain import uk.co.sentinelweb.cuer.domain.update.UpdateDomain class PlaylistMediaUpdateUsecase( - private val mediaOrchestrator: MediaOrchestrator + private val mediaOrchestrator: MediaOrchestrator, + private val log:LogWrapper ) { + init { + log.tag(this) + } + suspend fun updateMedia( playlist: PlaylistDomain, update: UpdateDomain, @@ -18,6 +23,7 @@ class PlaylistMediaUpdateUsecase( ): MediaDomain = when (options.source) { LOCAL, MEMORY -> mediaOrchestrator.update(update, options) + //LOCAL_NETWORK -> // todo update remote else -> throw UnsupportedOperationException("Media update not supported for ${options.source}") } -} \ No newline at end of file +} From 41b20accad3318d607806b3a3d3b4a0c35e73282 Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Fri, 11 Oct 2024 22:07:06 +0200 Subject: [PATCH 10/16] #482 encode file urls for streaming --- .../cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt | 12 +++++++++--- .../cuer/remote/server/JvmRemoteWebServer.kt | 8 +++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt index dbc916a40..a2f9d4151 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt @@ -25,10 +25,12 @@ import uk.co.sentinelweb.cuer.core.providers.PlayerConfigProvider import uk.co.sentinelweb.cuer.core.providers.TimeProvider import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper import uk.co.sentinelweb.cuer.domain.MediaDomain.MediaTypeDomain.FILE +import uk.co.sentinelweb.cuer.domain.MediaDomain.MediaTypeDomain.VIDEO import uk.co.sentinelweb.cuer.domain.PlayerNodeDomain import uk.co.sentinelweb.cuer.domain.PlayerStateDomain import uk.co.sentinelweb.cuer.domain.PlayerStateDomain.* import uk.co.sentinelweb.cuer.domain.PlaylistItemDomain +import uk.co.sentinelweb.cuer.domain.ext.serialise import uk.co.sentinelweb.cuer.remote.server.LocalRepository import uk.co.sentinelweb.cuer.remote.server.http import uk.co.sentinelweb.cuer.remote.server.locator @@ -37,6 +39,7 @@ import java.awt.BorderLayout.* import java.awt.Color import java.awt.GraphicsEnvironment import java.awt.event.* +import java.net.URLEncoder import javax.swing.* import javax.swing.JOptionPane.ERROR_MESSAGE import javax.swing.event.ChangeEvent @@ -403,8 +406,9 @@ class VlcPlayerSwingWindow( fun playStateChanged(command: PlayerContract.PlayerCommand) = when (command) { is Load -> { command.item - .also { log.d("${it.media.platformId}") } + .also { log.d("playStateChanged LOAD: ${it.media.platformId}") } .let { mapPath(it) } + ?.also{ log.d("playStateChanged mappedPth: $it")} ?.also { playItem(it) } ?: log.d("Cannot get full path ${command.item.media.platformId}") } @@ -417,11 +421,13 @@ class VlcPlayerSwingWindow( }.also { log.d("command:${command::class.java.simpleName}") } private fun mapPath(item: PlaylistItemDomain) = + // todo remove when url is rewritten item + .also {log.d("mapPath: id:${item.id?.serialise()} mediaType:${it.media.mediaType}")} .takeIf { it.id != null && it.id?.source == LOCAL_NETWORK && it.id?.locator != null } ?.takeIf { localRepository.localNode.locator() != it.id?.locator } - ?.takeIf { it.media.mediaType == FILE } - ?.let { it.copy(media = it.media.copy(platformId = "${it.id?.locator?.http()}/video-stream/${it.media.platformId}")) } + ?.takeIf { it.media.mediaType == VIDEO } + ?.let { it.copy(media = it.media.copy(platformId = "${it.id?.locator?.http()}/video-stream/${URLEncoder.encode(it.media.platformId, "UTF-8")}")) } ?.media ?.platformId ?: folderListUseCase.truncatedToFullFolderPath(item.media.platformId) diff --git a/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt b/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt index aaa953f3c..c4180c3c7 100644 --- a/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt +++ b/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt @@ -56,6 +56,7 @@ import uk.co.sentinelweb.cuer.remote.server.player.PlayerSessionMessageMapper import java.io.File import java.io.PrintWriter import java.io.StringWriter +import java.net.URLDecoder // todo break this up into use cases class JvmRemoteWebServer( @@ -340,15 +341,16 @@ class JvmRemoteWebServer( val filePath = call.parameters["filePath"] // ?.substring("/video-stream".length) ?.replace(":::::", "/") + ?.let { URLDecoder.decode(it, "UTF-8") } if (filePath == null) { call.respond(HttpStatusCode.BadRequest, "File filePath is missing") return@get } logWrapper.d("/video-stream/filepath: $filePath") - val parentPath = filePath.substring(0, filePath.lastIndexOf("/")) - val item = getFolderListUseCase.getFolderList(parentPath) - ?.children?.filter { it.platformId?.endsWith(filePath) ?: false } +// val parentPath = filePath.substring(0, filePath.lastIndexOf("/")) +// val item = getFolderListUseCase.getFolderList(parentPath) +// ?.children?.filter { it.platformId?.endsWith(filePath) ?: false } val fullPath = getFolderListUseCase.truncatedToFullFolderPath(filePath) logWrapper.d("/video-stream/filepath: $fullPath") From 03781f0cfe108e23d9e0a1579e425f92632f54a3 Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Sat, 12 Oct 2024 00:21:49 +0200 Subject: [PATCH 11/16] #482 testing http url --- .../hub/ui/player/vlc/VlcPlayerSwingWindow.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt index a2f9d4151..6183e9b7b 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt @@ -24,7 +24,6 @@ import uk.co.sentinelweb.cuer.core.mappers.TimeFormatter import uk.co.sentinelweb.cuer.core.providers.PlayerConfigProvider import uk.co.sentinelweb.cuer.core.providers.TimeProvider import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper -import uk.co.sentinelweb.cuer.domain.MediaDomain.MediaTypeDomain.FILE import uk.co.sentinelweb.cuer.domain.MediaDomain.MediaTypeDomain.VIDEO import uk.co.sentinelweb.cuer.domain.PlayerNodeDomain import uk.co.sentinelweb.cuer.domain.PlayerStateDomain @@ -408,7 +407,8 @@ class VlcPlayerSwingWindow( command.item .also { log.d("playStateChanged LOAD: ${it.media.platformId}") } .let { mapPath(it) } - ?.also{ log.d("playStateChanged mappedPth: $it")} + //?.let { "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" } + ?.also { log.d("playStateChanged mappedPth: $it") } ?.also { playItem(it) } ?: log.d("Cannot get full path ${command.item.media.platformId}") } @@ -421,13 +421,23 @@ class VlcPlayerSwingWindow( }.also { log.d("command:${command::class.java.simpleName}") } private fun mapPath(item: PlaylistItemDomain) = - // todo remove when url is rewritten item - .also {log.d("mapPath: id:${item.id?.serialise()} mediaType:${it.media.mediaType}")} + .also { log.d("mapPath: id:${item.id?.serialise()} mediaType:${it.media.mediaType}") } .takeIf { it.id != null && it.id?.source == LOCAL_NETWORK && it.id?.locator != null } ?.takeIf { localRepository.localNode.locator() != it.id?.locator } ?.takeIf { it.media.mediaType == VIDEO } - ?.let { it.copy(media = it.media.copy(platformId = "${it.id?.locator?.http()}/video-stream/${URLEncoder.encode(it.media.platformId, "UTF-8")}")) } + ?.let { + it.copy( + media = it.media.copy( + platformId = "${it.id?.locator?.http()}/video-stream/${ + URLEncoder.encode( + it.media.platformId, + "UTF-8" + ) + }" + ) + ) + } ?.media ?.platformId ?: folderListUseCase.truncatedToFullFolderPath(item.media.platformId) From 5e1196ca0289bd963d436f06bc700b4f80d4f744 Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Sat, 12 Oct 2024 00:28:03 +0200 Subject: [PATCH 12/16] #482 reorganise vlc player file --- .../hub/ui/player/vlc/VlcPlayerSwingWindow.kt | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt index 6183e9b7b..513cdd1e1 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt @@ -245,18 +245,54 @@ class VlcPlayerSwingWindow( ?: log.d("playItem: current is null") mediaPlayerComponent.mediaPlayer().media().prepare(path) mediaPlayerComponent.mediaPlayer().media().parsing().parse() - // play after parse is complete -// mediaPlayerComponent.mediaPlayer().media().play(path) } fun destroy() { -// mediaPlayerComponent.mediaPlayer().media().newMedia() - //mediaPlayerComponent.mediaPlayer().media().prepare(null as String?) mediaPlayerComponent.mediaPlayer().release() mediaPlayerComponent.mediaPlayer().media() this@VlcPlayerSwingWindow.dispose() } + fun playStateChanged(command: PlayerContract.PlayerCommand) = when (command) { + is Load -> { + command.item + .also { log.d("playStateChanged LOAD: ${it.media.platformId}") } + .let { mapPath(it) } + //?.let { "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" } + ?.also { log.d("playStateChanged mappedPth: $it") } + ?.also { playItem(it) } + ?: log.d("Cannot get full path ${command.item.media.platformId}") + } + + is Pause -> mediaPlayerComponent.mediaPlayer().controls().pause() + is Play -> mediaPlayerComponent.mediaPlayer().controls().play() + is SkipFwd -> mediaPlayerComponent.mediaPlayer().controls().skipTime(command.ms.toLong()) + is SkipBack -> mediaPlayerComponent.mediaPlayer().controls().skipTime(-command.ms.toLong()) + is SeekTo -> mediaPlayerComponent.mediaPlayer().controls().setTime(command.ms) + }.also { log.d("command:${command::class.java.simpleName}") } + + private fun mapPath(item: PlaylistItemDomain) = + item + .also { log.d("mapPath: id:${item.id?.serialise()} mediaType:${it.media.mediaType}") } + .takeIf { it.id != null && it.id?.source == LOCAL_NETWORK && it.id?.locator != null } + ?.takeIf { localRepository.localNode.locator() != it.id?.locator } + ?.takeIf { it.media.mediaType == VIDEO } + ?.let { + it.copy( + media = it.media.copy( + platformId = "${it.id?.locator?.http()}/video-stream/${ + URLEncoder.encode( + it.media.platformId, + "UTF-8" + ) + }" + ) + ) + } + ?.media + ?.platformId + ?: folderListUseCase.truncatedToFullFolderPath(item.media.platformId) + private fun createControls() { controlsPane = JPanel() controlsPane.layout = BorderLayout() @@ -402,46 +438,6 @@ class VlcPlayerSwingWindow( durText.text = times.durationText } - fun playStateChanged(command: PlayerContract.PlayerCommand) = when (command) { - is Load -> { - command.item - .also { log.d("playStateChanged LOAD: ${it.media.platformId}") } - .let { mapPath(it) } - //?.let { "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" } - ?.also { log.d("playStateChanged mappedPth: $it") } - ?.also { playItem(it) } - ?: log.d("Cannot get full path ${command.item.media.platformId}") - } - - is Pause -> mediaPlayerComponent.mediaPlayer().controls().pause() - is Play -> mediaPlayerComponent.mediaPlayer().controls().play() - is SkipFwd -> mediaPlayerComponent.mediaPlayer().controls().skipTime(command.ms.toLong()) - is SkipBack -> mediaPlayerComponent.mediaPlayer().controls().skipTime(-command.ms.toLong()) - is SeekTo -> mediaPlayerComponent.mediaPlayer().controls().setTime(command.ms) - }.also { log.d("command:${command::class.java.simpleName}") } - - private fun mapPath(item: PlaylistItemDomain) = - item - .also { log.d("mapPath: id:${item.id?.serialise()} mediaType:${it.media.mediaType}") } - .takeIf { it.id != null && it.id?.source == LOCAL_NETWORK && it.id?.locator != null } - ?.takeIf { localRepository.localNode.locator() != it.id?.locator } - ?.takeIf { it.media.mediaType == VIDEO } - ?.let { - it.copy( - media = it.media.copy( - platformId = "${it.id?.locator?.http()}/video-stream/${ - URLEncoder.encode( - it.media.platformId, - "UTF-8" - ) - }" - ) - ) - } - ?.media - ?.platformId - ?: folderListUseCase.truncatedToFullFolderPath(item.media.platformId) - fun updateTexts(texts: PlayerContract.View.Model.Texts) { if (!this@VlcPlayerSwingWindow.isUndecorated) { title = texts.title From 5ea017f9673ea8fbbb6a86e966161fe1dbd83bff Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Sat, 12 Oct 2024 11:19:14 +0200 Subject: [PATCH 13/16] #482 fix http playback crash --- gradle/libs.versions.toml | 2 +- .../uk/co/sentinelweb/cuer/hub/main/Main.kt | 2 + .../hub/ui/player/vlc/VlcPlayerSwingWindow.kt | 14 +++-- .../ui/player/vlc/VlcPlayerUiCoordinator.kt | 3 +- .../sentinelweb/cuer/hub/util/JarJnaCheck.kt | 62 +++++++++++++++++++ 5 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/util/JarJnaCheck.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 01ecaab14..3f91ba81b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -81,7 +81,7 @@ hamcrest = "2.2" kotlin-react = "17.0.2-pre.289-kotlin-1.6.10" kotlin-styled = "5.3.3-pre.289-kotlin-1.6.10" batik = "1.17" -vlcj = "4.8.2" +vlcj = "4.8.3" logback = "1.2.3" jna = "5.14.0" paletteKtx = "1.0.0" diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/main/Main.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/main/Main.kt index 06a1b231d..2e679d64e 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/main/Main.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/main/Main.kt @@ -8,6 +8,7 @@ import uk.co.sentinelweb.cuer.core.wrapper.WifiStateProvider import uk.co.sentinelweb.cuer.hub.di.Modules import uk.co.sentinelweb.cuer.hub.ui.home.HomeUiCoordinator import uk.co.sentinelweb.cuer.hub.ui.home.home +import uk.co.sentinelweb.cuer.hub.util.JarJnaCheck import uk.co.sentinelweb.cuer.hub.util.remote.KeyStoreManager import uk.co.sentinelweb.cuer.hub.util.remote.RemoteConfigFileInitialiseer @@ -43,5 +44,6 @@ fun main() { // System.setProperty("com.apple.mrj.application.apple.menu.about.name", "My Custom App Name"); // } + JarJnaCheck().check() home(homeUiCoordinator) } diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt index 513cdd1e1..f2a82dd1f 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt @@ -2,6 +2,7 @@ package uk.co.sentinelweb.cuer.hub.ui.player.vlc import androidx.compose.ui.graphics.Color.Companion.Black import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch import loadSVG import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -21,6 +22,7 @@ import uk.co.sentinelweb.cuer.app.ui.player.PlayerContract.PlayerCommand.* import uk.co.sentinelweb.cuer.app.ui.player.PlayerContract.View.Event.* import uk.co.sentinelweb.cuer.app.usecase.GetFolderListUseCase import uk.co.sentinelweb.cuer.core.mappers.TimeFormatter +import uk.co.sentinelweb.cuer.core.providers.CoroutineContextProvider import uk.co.sentinelweb.cuer.core.providers.PlayerConfigProvider import uk.co.sentinelweb.cuer.core.providers.TimeProvider import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper @@ -51,6 +53,7 @@ class VlcPlayerSwingWindow( private val showHideControls: VlcPlayerShowHideControls, private val keyMap: VlcPlayerKeyMap, private val localRepository: LocalRepository, + private val coroutineContextProvider: CoroutineContextProvider ) : JFrame(), KoinComponent { lateinit var mediaPlayerComponent: CallbackMediaPlayerComponent @@ -151,9 +154,10 @@ class VlcPlayerSwingWindow( override fun mediaParsedChanged(media: Media, newStatus: MediaParsedStatus) { val ms = media.info().duration() log.d("duration: $ms") - durationMs = ms - coordinator.dispatch(DurationReceived(ms)) - mediaPlayerComponent.mediaPlayer().media().play(media.newMediaRef()) + if (ms > -1) { + durationMs = ms + coordinator.dispatch(DurationReceived(ms)) + } } } ) @@ -243,13 +247,11 @@ class VlcPlayerSwingWindow( mediaPlayerComponent.mediaPlayer().media().info() ?.apply { log.d("playItem: current ${this.mrl()}") } ?: log.d("playItem: current is null") - mediaPlayerComponent.mediaPlayer().media().prepare(path) - mediaPlayerComponent.mediaPlayer().media().parsing().parse() + mediaPlayerComponent.mediaPlayer().media().play(path) } fun destroy() { mediaPlayerComponent.mediaPlayer().release() - mediaPlayerComponent.mediaPlayer().media() this@VlcPlayerSwingWindow.dispose() } diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerUiCoordinator.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerUiCoordinator.kt index c26770ba3..269b8248b 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerUiCoordinator.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerUiCoordinator.kt @@ -225,7 +225,8 @@ class VlcPlayerUiCoordinator( folderListUseCase = get(), showHideControls = VlcPlayerShowHideControls(), keyMap = VlcPlayerKeyMap(), - localRepository = get() + localRepository = get(), + coroutineContextProvider = get() ) } } diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/util/JarJnaCheck.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/util/JarJnaCheck.kt new file mode 100644 index 000000000..78ad599c8 --- /dev/null +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/util/JarJnaCheck.kt @@ -0,0 +1,62 @@ +package uk.co.sentinelweb.cuer.hub.util + +import java.io.File + +class JarJnaCheck { + fun check() { + // Get system class loader + // Get system classpath from system property + val classpath = System.getProperty("java.class.path") + + // Split the classpath into individual paths + val classpathEntries = classpath.split(File.pathSeparator) + + // Define a map to store JNA jar files and their versions + val jarMap = mutableMapOf() + + // Specify the JAR name pattern to check for (case insensitive) + val jarToCheck = "jna" + + // Iterate through classpath URLs + for (entry in classpathEntries) { + val file = File(entry) + + if (file.name.lowercase().contains(jarToCheck.lowercase())) { + val jarName = file.name + val jarVersion = getJarVersion(jarName) + + if (jarMap.containsKey(jarName)) { + println("Multiple versions found for $jarName:") + println("Existing Version: ${jarMap[jarName]}") + println("New Version: $jarVersion") + } else { + jarMap[jarName] = jarVersion + } + } + } + + // Print all JNA jars found + if (jarMap.isEmpty()) { + println("No JNA jars found.") + } else { + println("JNA jars found:") + for ((jarName, jarVersion) in jarMap) { + println("JAR: $jarName, Version: $jarVersion") + } + } + } + + fun getJarVersion(jarName: String): String { + // Simple method to extract version from a jar file name + // This assumes jar name format: name-version.jar + // e.g., jna-5.10.0.jar will return 5.10.0 + val dashIndex = jarName.lastIndexOf('-') + val dotIndex = jarName.lastIndexOf('.') + + return if (dashIndex != -1 && dotIndex != -1 && dashIndex < dotIndex) { + jarName.substring(dashIndex + 1, dotIndex) + } else { + "Unknown Version" + } + } +} From a2eb002d1a8c0a3d289a32a3621652acadccf506 Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Sat, 12 Oct 2024 12:22:59 +0200 Subject: [PATCH 14/16] #482 refactor rewrite url ids --- .../cuer/domain/ext/ChannelDomainExt.kt | 9 +++- .../cuer/domain/ext/MediaDomainExt.kt | 11 ++++- .../ext/PlaylistAndChildrenDomainExt.kt | 15 +++++++ .../cuer/domain/ext/PlaylistDomainExt.kt | 12 +++++- .../cuer/domain/ext/PlaylistItemDomainExt.kt | 9 ++++ .../cuer/remote/server/JvmRemoteWebServer.kt | 43 ++----------------- .../util/cuercast/CuerCastPlayerWatcher.kt | 18 ++++---- 7 files changed, 66 insertions(+), 51 deletions(-) create mode 100644 domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/PlaylistAndChildrenDomainExt.kt diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/ChannelDomainExt.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/ChannelDomainExt.kt index 4677d2ba4..9d557b945 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/ChannelDomainExt.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/ChannelDomainExt.kt @@ -1,5 +1,12 @@ -import uk.co.sentinelweb.cuer.domain.ChannelDomain +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Identifier.Locator +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.REMOTE +import uk.co.sentinelweb.cuer.domain.* fun ChannelDomain.summarise(): String = """ CHANNEL: id: $id, platform: $platform - $platformId, title: $title """.trimIndent() + +fun ChannelDomain.rewriteIdsToSource(source: Source, locator: Locator?) = this.copy( + id = this.id?.copy(source = source, locator = locator) +) diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/MediaDomainExt.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/MediaDomainExt.kt index fd39df9a6..be158bd66 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/MediaDomainExt.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/MediaDomainExt.kt @@ -1,7 +1,11 @@ package uk.co.sentinelweb.cuer.domain.ext +import rewriteIdsToSource import summarise -import uk.co.sentinelweb.cuer.domain.MediaDomain +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Identifier.Locator +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.REMOTE +import uk.co.sentinelweb.cuer.domain.* fun MediaDomain.stringMedia(): String = "id=$id title=$title platform=$platform platformId=$platformId" @@ -20,3 +24,8 @@ fun MediaDomain.startPosition(): Long { fun MediaDomain.summarise(): String = """ MEDIA: id: $id,platform: $platform - $platformId, title: $title, [channel: ${channelData.summarise()}] """.trimIndent() + +fun MediaDomain.rewriteIdsToSource(source: Source, locator: Locator?) = this.copy( + id = this.id?.copy(source = source, locator = locator), + channelData = this.channelData.rewriteIdsToSource(source, locator) +) diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/PlaylistAndChildrenDomainExt.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/PlaylistAndChildrenDomainExt.kt new file mode 100644 index 000000000..f09dddf6e --- /dev/null +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/PlaylistAndChildrenDomainExt.kt @@ -0,0 +1,15 @@ +package uk.co.sentinelweb.cuer.domain.ext + +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Identifier.Locator +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.REMOTE +import uk.co.sentinelweb.cuer.domain.PlaylistAndChildrenDomain + +fun PlaylistAndChildrenDomain.rewriteIdsToSource(source: Source, locator: Locator?) = copy( + playlist = this.playlist.rewriteIdsToSource(source, locator), + children = this.children.map { childPlaylist -> + childPlaylist.copy( + id = childPlaylist.id?.copy(source = REMOTE, locator = locator) + ) + } +) diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/PlaylistDomainExt.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/PlaylistDomainExt.kt index c79dedd0b..5e3c737c4 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/PlaylistDomainExt.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/PlaylistDomainExt.kt @@ -1,7 +1,10 @@ package uk.co.sentinelweb.cuer.domain.ext +import rewriteIdsToSource import summarise import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Identifier +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Identifier.Locator +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source import uk.co.sentinelweb.cuer.domain.* fun PlaylistDomain.currentItem() = @@ -217,4 +220,11 @@ fun PlaylistDomain.orderIsAscending() = ?: false acc && lastIsBefore } else acc - } \ No newline at end of file + } + + +fun PlaylistDomain.rewriteIdsToSource(source: Source, locator: Locator?) = this.copy( + id = this.id?.copy(source = source, locator = locator), + items = this.items.map { it.rewriteIdsToSource(source, locator) }, + channelData = this.channelData?.rewriteIdsToSource(source, locator) +) diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/PlaylistItemDomainExt.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/PlaylistItemDomainExt.kt index 659a5d59d..748897bd2 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/PlaylistItemDomainExt.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/PlaylistItemDomainExt.kt @@ -1,5 +1,14 @@ +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Identifier.Locator +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source import uk.co.sentinelweb.cuer.domain.PlaylistItemDomain +import uk.co.sentinelweb.cuer.domain.ext.rewriteIdsToSource import uk.co.sentinelweb.cuer.domain.ext.summarise fun PlaylistItemDomain.summarise(): String = "ITEM: id: $id, order: $order, playlistId: $playlistId, media: ${media.summarise()}" + + +fun PlaylistItemDomain.rewriteIdsToSource(source: Source, locator: Locator?) = this.copy( + id = this.id?.copy(source = source, locator = locator), + media = this.media.rewriteIdsToSource(source, locator) +) diff --git a/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt b/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt index c4180c3c7..7815d569d 100644 --- a/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt +++ b/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Identifier.Locator import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Source.* import uk.co.sentinelweb.cuer.app.orchestrator.toGuidIdentifier @@ -26,9 +27,10 @@ import uk.co.sentinelweb.cuer.app.service.remote.RemoteServerContract import uk.co.sentinelweb.cuer.app.usecase.GetFolderListUseCase import uk.co.sentinelweb.cuer.core.providers.PlayerConfigProvider import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper -import uk.co.sentinelweb.cuer.domain.PlaylistAndChildrenDomain +import uk.co.sentinelweb.cuer.domain.* import uk.co.sentinelweb.cuer.domain.ext.deserialisePlaylistItem import uk.co.sentinelweb.cuer.domain.ext.domainMessageJsonSerializer +import uk.co.sentinelweb.cuer.domain.ext.rewriteIdsToSource import uk.co.sentinelweb.cuer.domain.ext.serialise import uk.co.sentinelweb.cuer.domain.system.ErrorDomain import uk.co.sentinelweb.cuer.domain.system.ErrorDomain.Level.ERROR @@ -329,7 +331,7 @@ class JvmRemoteWebServer( val path = call.parameters[FOLDER_LIST_API.P_PARAM] logWrapper.d("Folder: $path") getFolderListUseCase.getFolderList(path) - ?.let { rewriteIdsToRemote(it) } // todo rewrite platformid -> http + ?.rewriteIdsToSource(LOCAL_NETWORK, localRepository.localNode.locator()) ?.let { ResponseDomain(it) } ?.apply { call.respondText(serialise(), ContentType.Application.Json) } ?: apply { @@ -384,43 +386,6 @@ class JvmRemoteWebServer( } } } - - private fun rewriteIdsToRemote(it: PlaylistAndChildrenDomain) = it.copy( - playlist = it.playlist.copy( - id = it.playlist.id?.copy( - source = LOCAL_NETWORK, - locator = localRepository.localNode.locator() - ), - items = it.playlist.items.map { - it.copy( - id = it.id?.copy( - source = LOCAL_NETWORK, - locator = localRepository.localNode.locator() - ), - media = it.media.copy( - id = it.media.id?.copy( - source = LOCAL_NETWORK, - locator = localRepository.localNode.locator() - ), - channelData = it.media.channelData.copy( - id = it.media.channelData.id?.copy( - source = LOCAL_NETWORK, - locator = localRepository.localNode.locator() - ) - ) - ) - ) - } - ), - children = it.children.map { child -> - child.copy( - id = child.id?.copy( - source = REMOTE, - locator = localRepository.localNode.locator() - ) - ) - } - ) } diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/util/cuercast/CuerCastPlayerWatcher.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/util/cuercast/CuerCastPlayerWatcher.kt index 910bceb77..8c61ebfe0 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/util/cuercast/CuerCastPlayerWatcher.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/util/cuercast/CuerCastPlayerWatcher.kt @@ -147,7 +147,7 @@ class CuerCastPlayerWatcher( withContext(coroutines.Main) { result.takeIf { it.isSuccessful } ?.data - ?.let { addBlankRemoteIdToItem(it) } + //?.let { addBlankRemoteIdToItem(it) } ?.let { addThumbnailToMedia(it) } ?.apply { mainPlayerControls?.setPlayerState(playbackState) } ?.apply { mainPlayerControls?.setPlaylistItem(item) } @@ -208,14 +208,14 @@ class CuerCastPlayerWatcher( ) } else it - private fun addBlankRemoteIdToItem(it: PlayerSessionContract.PlayerStatusMessage) = - if (it.item.id == null) { - it.copy( - item = it.item.copy( - id = Identifier(GUID(""), LOCAL_NETWORK, remoteNode?.locator()) - ) - ) - } else it +// private fun addBlankRemoteIdToItem(it: PlayerSessionContract.PlayerStatusMessage) = +// if (it.item.id == null) { +// it.copy( +// item = it.item.copy( +// id = Identifier(GUID(""), LOCAL_NETWORK, remoteNode?.locator()) +// ) +// ) +// } else it private val controlsListener = object : PlayerContract.PlayerControls.Listener { From d01ec5d7d74cb08a52a6d8a2c62e2d7fb50da4b0 Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Sat, 12 Oct 2024 13:03:40 +0200 Subject: [PATCH 15/16] #482 url encode refactor --- .../cuer/core/wrapper/URLEncoder.kt | 12 ++++++++++++ .../cuer/core/wrapper/URLEncoder.kt | 6 ++++++ .../remote/server/RemoteWebServerContract.kt | 7 ++++++- .../sentinelweb/cuer/core/ext/NSStringExt.kt | 8 ++++++++ .../cuer/core/wrapper/URLEncoder.kt | 15 +++++++++++++++ .../cuer/core/wrapper/URLEncoder.kt | 14 ++++++++++++++ .../cuer/core/wrapper/URLEncoder.kt | 12 ++++++++++++ .../hub/ui/player/vlc/VlcPlayerSwingWindow.kt | 18 +++++++----------- .../cuer/remote/server/JvmRemoteWebServer.kt | 10 ++++------ 9 files changed, 84 insertions(+), 18 deletions(-) create mode 100644 domain/src/androidMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt create mode 100644 domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt create mode 100644 domain/src/iosMain/kotlin/uk/co/sentinelweb/cuer/core/ext/NSStringExt.kt create mode 100644 domain/src/iosMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt create mode 100644 domain/src/jsMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt create mode 100644 domain/src/jvmMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt diff --git a/domain/src/androidMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt b/domain/src/androidMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt new file mode 100644 index 000000000..77c461fae --- /dev/null +++ b/domain/src/androidMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt @@ -0,0 +1,12 @@ +package uk.co.sentinelweb.cuer.core.wrapper + + +actual object URLEncoder { + actual fun encode(value: String, encoding: String): String { + return java.net.URLEncoder.encode(value, encoding) + } + + actual fun decode(value: String, encoding: String): String { + return java.net.URLDecoder.decode(value, encoding) + } +} diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt new file mode 100644 index 000000000..dd13f4ce1 --- /dev/null +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt @@ -0,0 +1,6 @@ +package uk.co.sentinelweb.cuer.core.wrapper + +expect object URLEncoder { + fun encode(value: String, encoding: String = "UTF-8"): String + fun decode(value: String, encoding: String = "UTF-8"): String +} diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/RemoteWebServerContract.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/RemoteWebServerContract.kt index 57de99e40..43b683dc5 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/RemoteWebServerContract.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/RemoteWebServerContract.kt @@ -49,5 +49,10 @@ interface RemoteWebServerContract { val PATH = "/folders" val P_PARAM = "p" } + object VIDEO_STREAM_API { + val P_FILEPATH = "filePath" + val ROUTE = "/video-stream" + val PATH = "$ROUTE/{$P_FILEPATH}" + } } -} \ No newline at end of file +} diff --git a/domain/src/iosMain/kotlin/uk/co/sentinelweb/cuer/core/ext/NSStringExt.kt b/domain/src/iosMain/kotlin/uk/co/sentinelweb/cuer/core/ext/NSStringExt.kt new file mode 100644 index 000000000..30be91585 --- /dev/null +++ b/domain/src/iosMain/kotlin/uk/co/sentinelweb/cuer/core/ext/NSStringExt.kt @@ -0,0 +1,8 @@ +package uk.co.sentinelweb.cuer.core.ext + +import platform.Foundation.NSString +import platform.Foundation.create + +fun String.nsString(): NSString { + return NSString.create(string = this) +} diff --git a/domain/src/iosMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt b/domain/src/iosMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt new file mode 100644 index 000000000..90f170f78 --- /dev/null +++ b/domain/src/iosMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt @@ -0,0 +1,15 @@ +package uk.co.sentinelweb.cuer.core.wrapper + +import platform.Foundation.* +import uk.co.sentinelweb.cuer.core.ext.nsString + +actual object URLEncoder { + actual fun encode(value: String, encoding: String): String { + val allowedCharacters = NSCharacterSet.URLQueryAllowedCharacterSet + return value.nsString().stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacters) ?: value + } + + actual fun decode(value: String, encoding: String): String { + return value.nsString().stringByRemovingPercentEncoding() ?: value + } +} diff --git a/domain/src/jsMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt b/domain/src/jsMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt new file mode 100644 index 000000000..4d14201fe --- /dev/null +++ b/domain/src/jsMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt @@ -0,0 +1,14 @@ +package uk.co.sentinelweb.cuer.core.wrapper + +// fixme this my not work +external fun encodeURIComponent(str: String): String +external fun decodeURIComponent(str: String): String + +actual object URLEncoder { + actual fun encode(value: String, encoding: String): String = + encodeURIComponent(value) + + actual fun decode(value: String, encoding: String): String = + decodeURIComponent(value) + +} diff --git a/domain/src/jvmMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt b/domain/src/jvmMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt new file mode 100644 index 000000000..77c461fae --- /dev/null +++ b/domain/src/jvmMain/kotlin/uk/co/sentinelweb/cuer/core/wrapper/URLEncoder.kt @@ -0,0 +1,12 @@ +package uk.co.sentinelweb.cuer.core.wrapper + + +actual object URLEncoder { + actual fun encode(value: String, encoding: String): String { + return java.net.URLEncoder.encode(value, encoding) + } + + actual fun decode(value: String, encoding: String): String { + return java.net.URLDecoder.decode(value, encoding) + } +} diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt index f2a82dd1f..9da934742 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/player/vlc/VlcPlayerSwingWindow.kt @@ -2,7 +2,6 @@ package uk.co.sentinelweb.cuer.hub.ui.player.vlc import androidx.compose.ui.graphics.Color.Companion.Black import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch import loadSVG import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -26,13 +25,14 @@ import uk.co.sentinelweb.cuer.core.providers.CoroutineContextProvider import uk.co.sentinelweb.cuer.core.providers.PlayerConfigProvider import uk.co.sentinelweb.cuer.core.providers.TimeProvider import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper -import uk.co.sentinelweb.cuer.domain.MediaDomain.MediaTypeDomain.VIDEO +import uk.co.sentinelweb.cuer.core.wrapper.URLEncoder +import uk.co.sentinelweb.cuer.domain.PlatformDomain import uk.co.sentinelweb.cuer.domain.PlayerNodeDomain import uk.co.sentinelweb.cuer.domain.PlayerStateDomain import uk.co.sentinelweb.cuer.domain.PlayerStateDomain.* import uk.co.sentinelweb.cuer.domain.PlaylistItemDomain -import uk.co.sentinelweb.cuer.domain.ext.serialise import uk.co.sentinelweb.cuer.remote.server.LocalRepository +import uk.co.sentinelweb.cuer.remote.server.RemoteWebServerContract.Companion.VIDEO_STREAM_API import uk.co.sentinelweb.cuer.remote.server.http import uk.co.sentinelweb.cuer.remote.server.locator import java.awt.BorderLayout @@ -40,7 +40,6 @@ import java.awt.BorderLayout.* import java.awt.Color import java.awt.GraphicsEnvironment import java.awt.event.* -import java.net.URLEncoder import javax.swing.* import javax.swing.JOptionPane.ERROR_MESSAGE import javax.swing.event.ChangeEvent @@ -275,18 +274,15 @@ class VlcPlayerSwingWindow( private fun mapPath(item: PlaylistItemDomain) = item - .also { log.d("mapPath: id:${item.id?.serialise()} mediaType:${it.media.mediaType}") } .takeIf { it.id != null && it.id?.source == LOCAL_NETWORK && it.id?.locator != null } ?.takeIf { localRepository.localNode.locator() != it.id?.locator } - ?.takeIf { it.media.mediaType == VIDEO } + ?.takeIf { it.media.platform == PlatformDomain.FILESYSTEM } ?.let { it.copy( media = it.media.copy( - platformId = "${it.id?.locator?.http()}/video-stream/${ - URLEncoder.encode( - it.media.platformId, - "UTF-8" - ) + platformId = + "${it.id?.locator?.http()}${VIDEO_STREAM_API.ROUTE}/${ + URLEncoder.encode(it.media.platformId, "UTF-8") }" ) ) diff --git a/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt b/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt index 7815d569d..755c5e42c 100644 --- a/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt +++ b/remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt @@ -27,6 +27,7 @@ import uk.co.sentinelweb.cuer.app.service.remote.RemoteServerContract import uk.co.sentinelweb.cuer.app.usecase.GetFolderListUseCase import uk.co.sentinelweb.cuer.core.providers.PlayerConfigProvider import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper +import uk.co.sentinelweb.cuer.core.wrapper.URLEncoder import uk.co.sentinelweb.cuer.domain.* import uk.co.sentinelweb.cuer.domain.ext.deserialisePlaylistItem import uk.co.sentinelweb.cuer.domain.ext.domainMessageJsonSerializer @@ -47,6 +48,7 @@ import uk.co.sentinelweb.cuer.remote.server.RemoteWebServerContract.Companion.PL import uk.co.sentinelweb.cuer.remote.server.RemoteWebServerContract.Companion.PLAYLISTS_API import uk.co.sentinelweb.cuer.remote.server.RemoteWebServerContract.Companion.PLAYLIST_API import uk.co.sentinelweb.cuer.remote.server.RemoteWebServerContract.Companion.PLAYLIST_SOURCE_API +import uk.co.sentinelweb.cuer.remote.server.RemoteWebServerContract.Companion.VIDEO_STREAM_API import uk.co.sentinelweb.cuer.remote.server.database.RemoteDatabaseAdapter import uk.co.sentinelweb.cuer.remote.server.ext.checkNull import uk.co.sentinelweb.cuer.remote.server.message.AvailableMessage @@ -58,7 +60,6 @@ import uk.co.sentinelweb.cuer.remote.server.player.PlayerSessionMessageMapper import java.io.File import java.io.PrintWriter import java.io.StringWriter -import java.net.URLDecoder // todo break this up into use cases class JvmRemoteWebServer( @@ -338,12 +339,9 @@ class JvmRemoteWebServer( call.respond(HttpStatusCode.NotFound, "No folder with path: $path") } } - //http://192.168.1.12:9843/video-stream/torrent:::::farscape-s1:::::Farscape%20S01E03%20Back%20and%20Back%20and%20Back%20to%20the%20Future.mp4 - get("/video-stream/{filePath}") { + get(VIDEO_STREAM_API.PATH) { val filePath = call.parameters["filePath"] -// ?.substring("/video-stream".length) - ?.replace(":::::", "/") - ?.let { URLDecoder.decode(it, "UTF-8") } + ?.let { URLEncoder.decode(it, "UTF-8") } if (filePath == null) { call.respond(HttpStatusCode.BadRequest, "File filePath is missing") return@get From 8d9be094c890ce4a403ace5b5932dfd86956838b Mon Sep 17 00:00:00 2001 From: sentinelweb Date: Sat, 12 Oct 2024 13:16:16 +0200 Subject: [PATCH 16/16] #482 fix detekt --- .../remote/server/RemoteWebServerContract.kt | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/RemoteWebServerContract.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/RemoteWebServerContract.kt index 43b683dc5..14fd0b71e 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/RemoteWebServerContract.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/RemoteWebServerContract.kt @@ -12,47 +12,47 @@ interface RemoteWebServerContract { const val WEB_SERVER_PORT_DEF = 9090 object AVAILABLE_API { - val PATH = "/available" + const val PATH = "/available" } object PLAYLISTS_API { val PATH = "/playlists" } object PLAYLIST_API { - val PATH = "/playlist/{id}" + const val PATH = "/playlist/{id}" } object PLAYLIST_SOURCE_API { - val PATH = "/playlist-src/{src}/{id}" + const val PATH = "/playlist-src/{src}/{id}" } object PLAYER_COMMAND_API { - val P_COMMAND = "command" - val P_ARG0 = "arg0" - val PATH = "/player/command/{$P_COMMAND}/{$P_ARG0}" + const val P_COMMAND = "command" + const val P_ARG0 = "arg0" + const val PATH = "/player/command/{$P_COMMAND}/{$P_ARG0}" } object PLAYER_CONFIG_API { - val PATH = "/player/config" + const val PATH = "/player/config" } object PLAYER_LAUNCH_VIDEO_API { - val P_SCREEN_INDEX = "screenIndex" - val PATH = "/player/launch" + const val P_SCREEN_INDEX = "screenIndex" + const val PATH = "/player/launch" } object PLAYER_STATUS_API { - val PATH = "/player/status" + const val PATH = "/player/status" } object FOLDER_LIST_API { - val PATH = "/folders" - val P_PARAM = "p" + const val PATH = "/folders" + const val P_PARAM = "p" } object VIDEO_STREAM_API { - val P_FILEPATH = "filePath" - val ROUTE = "/video-stream" - val PATH = "$ROUTE/{$P_FILEPATH}" + const val P_FILEPATH = "filePath" + const val ROUTE = "/video-stream" + const val PATH = "$ROUTE/{$P_FILEPATH}" } } }