From 35275d8651d364c461a1b3b3c8cfaa64e0464721 Mon Sep 17 00:00:00 2001 From: Talo Halton Date: Sun, 23 Jun 2024 15:13:44 +0100 Subject: [PATCH] Add JVM target (#5) - Use KJna - Add flake.nix - Remove native Windows target --- .github/workflows/build-linux-arm64.yml | 48 +- .github/workflows/build-linux-x86_64.yml | 52 +- .github/workflows/build-windows-x86_64.yml | 89 --- .gitignore | 10 +- app/build.gradle.kts | 125 ++++ .../kotlin/dev/toastbits/spms/AppPlatform.kt | 11 + .../kotlin/dev/toastbits}/spms/Command.kt | 22 +- .../toastbits}/spms/client/ClientOptions.kt | 8 +- .../spms/client/cli/CommandLineClient.kt | 28 +- .../spms/client/cli/CommandLineClientMode.kt | 25 +- .../spms/client/cli/CommandLineModeContext.kt | 15 + .../spms/client/cli/modes/Interactive.kt | 6 +- .../toastbits}/spms/client/cli/modes/Poll.kt | 15 +- .../toastbits}/spms/client/cli/modes/Run.kt | 43 +- .../spms/client/player/PlayerClient.kt | 86 ++- .../spms/indicator/AppIndicatorImpl.kt | 153 +++++ .../spms}/indicator/TrayIndicator.kt | 12 +- .../spms/localisation/AppLocalisation.kt | 36 +- .../toastbits/spms/localisation/Language.kt | 5 + .../localisation/strings/AppLocalisationEn.kt | 104 ++- .../localisation/strings/AppLocalisationJa.kt | 12 +- .../localisation/strings/CliLocalisation.kt | 11 +- .../localisation/strings/CliLocalisationEn.kt | 13 +- .../localisation/strings/CliLocalisationJa.kt | 13 +- .../spms/mediasession/SpMsMediaSession.kt | 28 +- .../dev/toastbits}/spms/server/SpMsCommand.kt | 126 ++-- {src => app/src}/commonMain/kotlin/main.kt | 14 +- .../dev/toastbits/spms/AppPlatform.jvm.kt | 16 + .../spms/indicator/AppIndicatorImpl.jvm.kt | 32 + .../spms/localisation/Language.jvm.kt | 15 + .../dev/toastbits/spms/AppPlatform.linux.kt | 16 + .../dev/toastbits/spms/AppPlatform.mingw.kt | 11 + .../spms/indicator/TrayIndicator.mingw.kt | 16 + .../spms/indicator/AppIndicatorImpl.native.kt | 38 ++ .../spms/localisation/Language.native.kt | 6 +- build.gradle.kts | 611 +----------------- flake.lock | 44 ++ flake.nix | 82 +++ gradle.properties | 12 +- gradle/wrapper/gradle-wrapper.properties | 2 +- library/build-logic/build.gradle.kts | 42 ++ .../plugins/plugin.publishing.gradle.kts | 48 ++ library/build.gradle.kts | 551 ++++++++++++++++ .../kotlin/dev/toastbits/spms/Platform.kt | 46 ++ .../dev/toastbits/spms/ReentrantLock.kt | 5 + .../spms/localisation/SpMsLocalisation.kt | 28 + .../localisation/strings/LocalisationEn.kt | 15 + .../localisation/strings/LocalisationJa.kt | 15 + .../strings/PlayerActionLocalisation.kt | 2 +- .../strings/PlayerActionLocalisationEn.kt | 2 +- .../strings/PlayerActionLocalisationJa.kt | 2 +- .../strings/ServerActionLocalisation.kt | 10 +- .../strings/ServerActionLocalisationEn.kt | 10 +- .../strings/ServerActionLocalisationJa.kt | 10 +- .../strings/ServerLocalisation.kt | 5 +- .../strings/ServerLocalisationEn.kt | 5 +- .../strings/ServerLocalisationJa.kt | 5 +- .../dev/toastbits/spms/mpv/LibMpvClient.kt | 140 ++++ .../toastbits/spms/mpv/MpvClientEventLoop.kt | 57 +- .../dev/toastbits/spms}/mpv/MpvClientImpl.kt | 43 +- .../spms}/mpv/MpvCommandInterface.kt | 6 +- .../dev/toastbits}/spms/player/Player.kt | 12 +- .../spms/player/VideoInfoProvider.kt | 4 +- .../spms/player/headless/HeadlessPlayer.kt | 14 +- .../spms/player/headless/PlaybackState.kt | 2 +- .../kotlin/dev/toastbits}/spms/server/SpMs.kt | 155 ++--- .../dev/toastbits}/spms/server/SpMsClient.kt | 10 +- .../dev/toastbits}/spms/socketapi/Action.kt | 10 +- .../toastbits}/spms/socketapi/ParseMessage.kt | 6 +- .../spms/socketapi/player/PlayerAction.kt | 10 +- .../player/PlayerActionAddLocalFiles.kt | 4 +- .../player/PlayerActionCancelRadio.kt | 4 +- .../player/PlayerActionRemoveLocalFiles.kt | 4 +- .../player/PlayerActionSetAuthInfo.kt | 4 +- .../player/PlayerActionSetLocalFiles.kt | 4 +- .../spms/socketapi/server/ServerAction.kt | 10 +- .../socketapi/server/ServerActionAddItem.kt | 6 +- .../server/ServerActionClearQueue.kt | 6 +- .../server/ServerActionGetClients.kt | 6 +- .../server/ServerActionGetProperty.kt | 6 +- .../socketapi/server/ServerActionGetStatus.kt | 84 +++ .../socketapi/server/ServerActionMoveItem.kt | 6 +- .../socketapi/server/ServerActionPause.kt | 6 +- .../spms/socketapi/server/ServerActionPlay.kt | 6 +- .../socketapi/server/ServerActionPlayPause.kt | 6 +- .../server/ServerActionReadyToPlay.kt | 6 +- .../server/ServerActionRemoveItem.kt | 6 +- .../server/ServerActionSeekToItem.kt | 6 +- .../server/ServerActionSeekToNext.kt | 6 +- .../server/ServerActionSeekToPrevious.kt | 6 +- .../server/ServerActionSeekToTime.kt | 6 +- .../server/ServerActionSetRepeatMode.kt | 8 +- .../spms/socketapi/shared/SpMsActionReply.kt | 2 +- .../spms/socketapi/shared/SpMsClientData.kt | 2 +- .../spms/socketapi/shared/SpMsPlayerEvent.kt | 2 +- .../spms/socketapi/shared/SpMsSocketApi.kt | 2 +- .../dev/toastbits/spms/zmq/ZmqMessage.kt | 6 + .../dev/toastbits/spms}/zmq/ZmqRouter.kt | 27 +- .../dev/toastbits/spms/zmq/ZmqSocket.kt | 22 + library/src/commonMain/kotlin/toInt.kt | 3 + .../kotlin/dev/toastbits/spms/Platform.jvm.kt | 49 ++ .../dev/toastbits/spms/ReentrantLock.jvm.kt | 10 + .../dev/toastbits/spms/zmq/ZmqSocket.jvm.kt | 120 ++++ .../toastbits/spms/NativePlatform.linux.kt | 7 + .../dev/toastbits/spms/Platform.linux.kt | 16 + .../toastbits/spms/NativePlatform.mingw.kt | 7 + .../dev/toastbits/spms/Platform.mingw.kt | 18 + .../src}/nativeInterop/buildscripts/libmpv.sh | 26 +- .../dev/toastbits/spms/NativePlatform.kt | 5 + .../dev/toastbits/spms/Platform.native.kt | 46 ++ .../toastbits/spms/ReentrantLock.native.kt | 9 + .../toastbits/spms/native}/safeToKString.kt | 4 +- .../toastbits/spms/zmq/ZmqSocket.native.kt | 49 +- settings.gradle.kts | 35 +- .../indicator/TrayIndicatorImpl.kt.linux | 151 ----- .../indicator/TrayIndicatorImpl.kt.other | 17 - .../cinterop/mpv/LibMpvClient.kt.disabled | 38 -- .../cinterop/mpv/LibMpvClient.kt.enabled | 158 ----- .../mpv/MpvClientEventLoop.kt.disabled | 5 - src/commonMain/kotlin/spms/Platform.kt.linux | 23 - src/commonMain/kotlin/spms/Platform.kt.other | 21 - .../spms/client/cli/CommandLineModeContext.kt | 18 - .../spms/server/SpMsMediaSession.kt.other | 13 - .../socketapi/server/ServerActionGetStatus.kt | 121 ---- 124 files changed, 2547 insertions(+), 1972 deletions(-) delete mode 100644 .github/workflows/build-windows-x86_64.yml create mode 100644 app/build.gradle.kts create mode 100644 app/src/commonMain/kotlin/dev/toastbits/spms/AppPlatform.kt rename {src/commonMain/kotlin => app/src/commonMain/kotlin/dev/toastbits}/spms/Command.kt (77%) rename {src/commonMain/kotlin => app/src/commonMain/kotlin/dev/toastbits}/spms/client/ClientOptions.kt (77%) rename {src/commonMain/kotlin => app/src/commonMain/kotlin/dev/toastbits}/spms/client/cli/CommandLineClient.kt (64%) rename {src/commonMain/kotlin => app/src/commonMain/kotlin/dev/toastbits}/spms/client/cli/CommandLineClientMode.kt (79%) create mode 100644 app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/CommandLineModeContext.kt rename {src/commonMain/kotlin => app/src/commonMain/kotlin/dev/toastbits}/spms/client/cli/modes/Interactive.kt (61%) rename {src/commonMain/kotlin => app/src/commonMain/kotlin/dev/toastbits}/spms/client/cli/modes/Poll.kt (80%) rename {src/commonMain/kotlin => app/src/commonMain/kotlin/dev/toastbits}/spms/client/cli/modes/Run.kt (85%) rename {src/commonMain/kotlin => app/src/commonMain/kotlin/dev/toastbits}/spms/client/player/PlayerClient.kt (83%) create mode 100644 app/src/commonMain/kotlin/dev/toastbits/spms/indicator/AppIndicatorImpl.kt rename {src/commonMain/kotlin/cinterop => app/src/commonMain/kotlin/dev/toastbits/spms}/indicator/TrayIndicator.kt (86%) rename src/commonMain/kotlin/spms/localisation/SpMsLocalisation.kt => app/src/commonMain/kotlin/dev/toastbits/spms/localisation/AppLocalisation.kt (79%) create mode 100644 app/src/commonMain/kotlin/dev/toastbits/spms/localisation/Language.kt rename src/commonMain/kotlin/spms/localisation/strings/LocalisationEn.kt => app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/AppLocalisationEn.kt (68%) rename src/commonMain/kotlin/spms/localisation/strings/LocalisationJa.kt => app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/AppLocalisationJa.kt (94%) rename {src/commonMain/kotlin => app/src/commonMain/kotlin/dev/toastbits}/spms/localisation/strings/CliLocalisation.kt (71%) rename {src/commonMain/kotlin => app/src/commonMain/kotlin/dev/toastbits}/spms/localisation/strings/CliLocalisationEn.kt (77%) rename {src/commonMain/kotlin => app/src/commonMain/kotlin/dev/toastbits}/spms/localisation/strings/CliLocalisationJa.kt (79%) rename src/commonMain/kotlin/spms/server/SpMsMediaSession.kt.x86_64 => app/src/commonMain/kotlin/dev/toastbits/spms/mediasession/SpMsMediaSession.kt (86%) rename {src/commonMain/kotlin => app/src/commonMain/kotlin/dev/toastbits}/spms/server/SpMsCommand.kt (55%) rename {src => app/src}/commonMain/kotlin/main.kt (57%) create mode 100644 app/src/jvmMain/kotlin/dev/toastbits/spms/AppPlatform.jvm.kt create mode 100644 app/src/jvmMain/kotlin/dev/toastbits/spms/indicator/AppIndicatorImpl.jvm.kt create mode 100644 app/src/jvmMain/kotlin/dev/toastbits/spms/localisation/Language.jvm.kt create mode 100644 app/src/linuxMain/kotlin/dev/toastbits/spms/AppPlatform.linux.kt create mode 100644 app/src/mingwMain/kotlin/dev/toastbits/spms/AppPlatform.mingw.kt create mode 100644 app/src/mingwMain/kotlin/dev/toastbits/spms/indicator/TrayIndicator.mingw.kt create mode 100644 app/src/nativeMain/kotlin/dev/toastbits/spms/indicator/AppIndicatorImpl.native.kt rename src/commonMain/kotlin/spms/localisation/Language.kt => app/src/nativeMain/kotlin/dev/toastbits/spms/localisation/Language.native.kt (73%) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 library/build-logic/build.gradle.kts create mode 100644 library/build-logic/src/main/kotlin/plugins/plugin.publishing.gradle.kts create mode 100644 library/build.gradle.kts create mode 100644 library/src/commonMain/kotlin/dev/toastbits/spms/Platform.kt create mode 100644 library/src/commonMain/kotlin/dev/toastbits/spms/ReentrantLock.kt create mode 100644 library/src/commonMain/kotlin/dev/toastbits/spms/localisation/SpMsLocalisation.kt create mode 100644 library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/LocalisationEn.kt create mode 100644 library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/LocalisationJa.kt rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/localisation/strings/PlayerActionLocalisation.kt (92%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/localisation/strings/PlayerActionLocalisationEn.kt (96%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/localisation/strings/PlayerActionLocalisationJa.kt (97%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/localisation/strings/ServerActionLocalisation.kt (86%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/localisation/strings/ServerActionLocalisationEn.kt (90%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/localisation/strings/ServerActionLocalisationJa.kt (90%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/localisation/strings/ServerLocalisation.kt (74%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/localisation/strings/ServerLocalisationEn.kt (81%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/localisation/strings/ServerLocalisationJa.kt (82%) create mode 100644 library/src/commonMain/kotlin/dev/toastbits/spms/mpv/LibMpvClient.kt rename src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.enabled => library/src/commonMain/kotlin/dev/toastbits/spms/mpv/MpvClientEventLoop.kt (63%) rename {src/commonMain/kotlin/cinterop => library/src/commonMain/kotlin/dev/toastbits/spms}/mpv/MpvClientImpl.kt (89%) rename {src/commonMain/kotlin/cinterop => library/src/commonMain/kotlin/dev/toastbits/spms}/mpv/MpvCommandInterface.kt (68%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/player/Player.kt (82%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/player/VideoInfoProvider.kt (92%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/player/headless/HeadlessPlayer.kt (97%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/player/headless/PlaybackState.kt (96%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/server/SpMs.kt (78%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/server/SpMsClient.kt (66%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/Action.kt (80%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/ParseMessage.kt (92%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/player/PlayerAction.kt (88%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/player/PlayerActionAddLocalFiles.kt (88%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/player/PlayerActionCancelRadio.kt (83%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/player/PlayerActionRemoveLocalFiles.kt (88%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/player/PlayerActionSetAuthInfo.kt (88%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/player/PlayerActionSetLocalFiles.kt (88%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerAction.kt (88%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionAddItem.kt (87%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionClearQueue.kt (75%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionGetClients.kt (79%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionGetProperty.kt (73%) create mode 100644 library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionGetStatus.kt rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionMoveItem.kt (87%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionPause.kt (73%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionPlay.kt (73%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionPlayPause.kt (74%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionReadyToPlay.kt (91%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionRemoveItem.kt (84%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionSeekToItem.kt (87%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionSeekToNext.kt (75%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionSeekToPrevious.kt (76%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionSeekToTime.kt (84%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/server/ServerActionSetRepeatMode.kt (80%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/shared/SpMsActionReply.kt (85%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/shared/SpMsClientData.kt (97%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/shared/SpMsPlayerEvent.kt (98%) rename {src/commonMain/kotlin => library/src/commonMain/kotlin/dev/toastbits}/spms/socketapi/shared/SpMsSocketApi.kt (97%) create mode 100644 library/src/commonMain/kotlin/dev/toastbits/spms/zmq/ZmqMessage.kt rename {src/commonMain/kotlin/cinterop => library/src/commonMain/kotlin/dev/toastbits/spms}/zmq/ZmqRouter.kt (50%) create mode 100644 library/src/commonMain/kotlin/dev/toastbits/spms/zmq/ZmqSocket.kt create mode 100644 library/src/commonMain/kotlin/toInt.kt create mode 100644 library/src/jvmMain/kotlin/dev/toastbits/spms/Platform.jvm.kt create mode 100644 library/src/jvmMain/kotlin/dev/toastbits/spms/ReentrantLock.jvm.kt create mode 100644 library/src/jvmMain/kotlin/dev/toastbits/spms/zmq/ZmqSocket.jvm.kt create mode 100644 library/src/linuxMain/kotlin/dev/toastbits/spms/NativePlatform.linux.kt create mode 100644 library/src/linuxMain/kotlin/dev/toastbits/spms/Platform.linux.kt create mode 100644 library/src/mingwMain/kotlin/dev/toastbits/spms/NativePlatform.mingw.kt create mode 100644 library/src/mingwMain/kotlin/dev/toastbits/spms/Platform.mingw.kt rename {src => library/src}/nativeInterop/buildscripts/libmpv.sh (82%) create mode 100644 library/src/nativeMain/kotlin/dev/toastbits/spms/NativePlatform.kt create mode 100644 library/src/nativeMain/kotlin/dev/toastbits/spms/Platform.native.kt create mode 100644 library/src/nativeMain/kotlin/dev/toastbits/spms/ReentrantLock.native.kt rename {src/commonMain/kotlin/cinterop/utils => library/src/nativeMain/kotlin/dev/toastbits/spms/native}/safeToKString.kt (79%) rename src/commonMain/kotlin/cinterop/zmq/ZmqSocket.kt => library/src/nativeMain/kotlin/dev/toastbits/spms/zmq/ZmqSocket.native.kt (70%) delete mode 100644 src/commonMain/kotlin/cinterop/indicator/TrayIndicatorImpl.kt.linux delete mode 100644 src/commonMain/kotlin/cinterop/indicator/TrayIndicatorImpl.kt.other delete mode 100644 src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.disabled delete mode 100644 src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.enabled delete mode 100644 src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.disabled delete mode 100644 src/commonMain/kotlin/spms/Platform.kt.linux delete mode 100644 src/commonMain/kotlin/spms/Platform.kt.other delete mode 100644 src/commonMain/kotlin/spms/client/cli/CommandLineModeContext.kt delete mode 100644 src/commonMain/kotlin/spms/server/SpMsMediaSession.kt.other delete mode 100644 src/commonMain/kotlin/spms/socketapi/server/ServerActionGetStatus.kt diff --git a/.github/workflows/build-linux-arm64.yml b/.github/workflows/build-linux-arm64.yml index d78c161..0b596c2 100644 --- a/.github/workflows/build-linux-arm64.yml +++ b/.github/workflows/build-linux-arm64.yml @@ -16,20 +16,23 @@ jobs: env: TOOLCHAIN_VERSION: aarch64-unknown-linux-gnu-gcc-8.3.0-glibc-2.25-kernel-4.9-2 - JAVA_HOME: /usr/lib/jvm/java-17-openjdk/ + JAVA_HOME: /usr/lib/jvm/java-21-openjdk/ steps: - run: echo "TOOLCHAIN=$GITHUB_WORKSPACE/toolchain" >> $GITHUB_ENV + - run: echo "SPMS_ARCH=arm64" >> $GITHUB_ENV - run: apt update - run: apt install -y nodejs if: env.ACT - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: '17' distribution: 'temurin' + java-version: | + 22 + 21 - name: Test Java run: $JAVA_HOME/bin/java --version @@ -82,8 +85,8 @@ jobs: working-directory: zeromq-4.3.5 run: make -j$(nproc) && make install - - name: Copy libzmq into src/nativeInterop/linux-arm64 - run: mkdir -p src/nativeInterop/linux-arm64 && cp -r libzmq/* src/nativeInterop/linux-arm64 + - name: Copy libzmq into library/src/nativeInterop/linuxArm64 + run: mkdir -p library/src/nativeInterop/linuxArm64 && cp -r libzmq/* library/src/nativeInterop/linuxArm64 - name: Add arm64 architecture to dpkg run: dpkg --add-architecture arm64 @@ -117,48 +120,45 @@ jobs: - name: Install arm64 development libraries run: | - apt update - apt install -y libmpv-dev:arm64 libappindicator3-dev:arm64 + apt-get update + apt-get install -y libmpv-dev:arm64 libayatana-appindicator3-dev:arm64 - name: Set up Gradle uses: gradle/gradle-build-action@v3 - - name: Grant execute permission for gradlew - run: chmod +x gradle - - - name: Build linux-arm64 binaries - run: ./gradlew linux-arm64Binaries -PLINK_STATIC + - name: Build Linux Arm64 minimal binaries + run: ./gradlew app:linuxArm64Binaries -PMINIMAL --stacktrace - name: Strip release binary - run: $TOOLCHAIN/bin/aarch64-unknown-linux-gnu-strip build/bin/linux-arm64/releaseExecutable/*.kexe + run: $TOOLCHAIN/bin/aarch64-unknown-linux-gnu-strip app/build/bin/linuxArm64/releaseExecutable/*.kexe - name: Upload debug binary uses: actions/upload-artifact@v3 with: - name: spms-linux-arm64-debug - path: build/bin/linux-arm64/debugExecutable/*.kexe + name: spms-minimal-linux-arm64-debug + path: app/build/bin/linuxArm64/debugExecutable/*.kexe - name: Upload release binary uses: actions/upload-artifact@v3 with: - name: spms-linux-arm64-release - path: build/bin/linux-arm64/releaseExecutable/*.kexe + name: spms-minimal-linux-arm64-release + path: app/build/bin/linuxArm64/releaseExecutable/*.kexe - - name: Build linux-arm64 binaries without mpv - run: ./gradlew linux-arm64Binaries -PLINK_STATIC -PDISABLE_MPV + - name: Build Linux Arm64 full binaries + run: ./gradlew app:linuxArm64Binaries --stacktrace - name: Strip release binary - run: $TOOLCHAIN/bin/aarch64-unknown-linux-gnu-strip build/bin/linux-arm64/releaseExecutable/*.kexe + run: $TOOLCHAIN/bin/aarch64-unknown-linux-gnu-strip app/build/bin/linuxArm64/releaseExecutable/*.kexe - name: Upload debug binary uses: actions/upload-artifact@v3 with: - name: spms-nompv-linux-arm64-debug - path: build/bin/linux-arm64/debugExecutable/*.kexe + name: spms-linux-arm64-debug + path: app/build/bin/linuxArm64/debugExecutable/*.kexe - name: Upload release binary uses: actions/upload-artifact@v3 with: - name: spms-nompv-linux-arm64-release - path: build/bin/linux-arm64/releaseExecutable/*.kexe + name: spms-linux-arm64-release + path: app/build/bin/linuxArm64/releaseExecutable/*.kexe diff --git a/.github/workflows/build-linux-x86_64.yml b/.github/workflows/build-linux-x86_64.yml index fa0a590..9ae8f8a 100644 --- a/.github/workflows/build-linux-x86_64.yml +++ b/.github/workflows/build-linux-x86_64.yml @@ -16,25 +16,27 @@ jobs: env: TOOLCHAIN_VERSION: x86_64-unknown-linux-gnu-gcc-8.3.0-glibc-2.19-kernel-4.9-2 - JAVA_HOME: /usr/lib/jvm/java-17-openjdk/ + JAVA_HOME: /usr/lib/jvm/java-21-openjdk/ steps: - run: echo "TOOLCHAIN=$GITHUB_WORKSPACE/toolchain" >> $GITHUB_ENV - - run: apt update + - run: apt-get update - - run: apt install -y nodejs + - run: apt-get install -y nodejs if: env.ACT - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: '17' distribution: 'temurin' + java-version: | + 22 + 21 - name: Test Java run: $JAVA_HOME/bin/java --version - - run: apt install -y --reinstall git make wget pkg-config libmpv-dev libcurl4-openssl-dev libappindicator3-dev + - run: apt-get install -y --reinstall git make wget pkg-config libmpv-dev libcurl4-openssl-dev libayatana-appindicator3-dev - uses: actions/checkout@v3 with: @@ -80,50 +82,44 @@ jobs: working-directory: zeromq-4.3.5 run: make -j$(nproc) && make install - - name: Copy libzmq into src/nativeInterop/linux-x86_64 - run: mkdir -p src/nativeInterop/linux-x86_64 && cp -r libzmq/* src/nativeInterop/linux-x86_64 + - name: Copy libzmq into library/src/nativeInterop/linuxX64 + run: mkdir -p library/src/nativeInterop/linuxX64 && cp -r libzmq/* library/src/nativeInterop/linuxX64 - name: Set up Gradle uses: gradle/gradle-build-action@v3 - - name: Grant execute permission for gradlew - run: chmod +x gradle - - - name: Build linux-x86_64 binaries - run: ./gradlew linux-x86_64Binaries -PLINK_STATIC + - name: Build Linux x86_64 minimal binaries + run: ./gradlew app:linuxX64Binaries -PMINIMAL --stacktrace - name: Strip release binary - run: $TOOLCHAIN/bin/x86_64-unknown-linux-gnu-strip build/bin/linux-x86_64/releaseExecutable/*.kexe + run: $TOOLCHAIN/bin/x86_64-unknown-linux-gnu-strip app/build/bin/linuxX64/releaseExecutable/*.kexe - name: Upload debug binary uses: actions/upload-artifact@v3 with: - name: spms-linux-x86_64-debug - path: build/bin/linux-x86_64/debugExecutable/*.kexe + name: spms-minimal-linux-x86_64-debug + path: app/build/bin/linuxX64/debugExecutable/*.kexe - name: Upload release binary uses: actions/upload-artifact@v3 with: - name: spms-linux-x86_64-release - path: build/bin/linux-x86_64/releaseExecutable/*.kexe + name: spms-minimal-linux-x86_64-release + path: app/build/bin/linuxX64/releaseExecutable/*.kexe - - name: Clean build - run: ./gradlew clean - - - name: Build linux-x86_64 binaries without mpv - run: ./gradlew linux-x86_64Binaries -PLINK_STATIC -PDISABLE_MPV + - name: Build Linux x86_64 full binaries + run: ./gradlew app:linuxX64Binaries --stacktrace - name: Strip release binary - run: $TOOLCHAIN/bin/x86_64-unknown-linux-gnu-strip build/bin/linux-x86_64/releaseExecutable/*.kexe + run: $TOOLCHAIN/bin/x86_64-unknown-linux-gnu-strip app/build/bin/linuxX64/releaseExecutable/*.kexe - name: Upload debug binary uses: actions/upload-artifact@v3 with: - name: spms-nompv-linux-x86_64-debug - path: build/bin/linux-x86_64/debugExecutable/*.kexe + name: spms-linux-x86_64-debug + path: app/build/bin/linuxX64/debugExecutable/*.kexe - name: Upload release binary uses: actions/upload-artifact@v3 with: - name: spms-nompv-linux-x86_64-release - path: build/bin/linux-x86_64/releaseExecutable/*.kexe + name: spms-linux-x86_64-release + path: app/build/bin/linuxX64/releaseExecutable/*.kexe diff --git a/.github/workflows/build-windows-x86_64.yml b/.github/workflows/build-windows-x86_64.yml deleted file mode 100644 index aecec8f..0000000 --- a/.github/workflows/build-windows-x86_64.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Build [Windows x86_64] - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - types: [opened, synchronize, reopened, ready_for_review] - workflow_dispatch: - -jobs: - build: - runs-on: windows-latest - if: ${{ github.event.pull_request.draft == false && (github.event_name == 'workflow_dispatch' || !contains(github.event.head_commit.message, 'noci')) }} - - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - - - name: Build libzmq - uses: johnwason/vcpkg-action@v6 - with: - pkgs: zeromq[draft] - triplet: x64-windows - token: ${{ github.token }} - github-binarycache: true - - - name: Copy built files - run: xcopy /s /i /y "${{ github.workspace }}\vcpkg\installed\x64-windows\*" "${{ github.workspace }}\src\nativeInterop\windows-x86_64" - - - name: Download mpv - run: curl -L https://downloads.sourceforge.net/project/mpv-player-windows/libmpv/mpv-dev-x86_64-20240114-git-bd35dc8.7z --output mpv.7z - - - name: Extract mpv - shell: bash - run: | - "/c/Program Files/7-Zip/7z.exe" x mpv.7z -o"src\nativeInterop\windows-x86_64" - - - name: Move libmpv.dll.a - run: move src\nativeInterop\windows-x86_64\libmpv.dll.a src\nativeInterop\windows-x86_64\lib\libmpv.dll.a - - - name: Move libmpv-2.dll - run: move src\nativeInterop\windows-x86_64\libmpv-2.dll src\nativeInterop\windows-x86_64\bin\libmpv-2.dll - - - name: Set up Gradle - uses: gradle/gradle-build-action@v3 - - - name: Build windows-x86_64 binaries - run: .\gradlew.bat windows-x86_64Binaries -PLINK_STATIC - - - name: Upload debug binary - uses: actions/upload-artifact@v3 - with: - name: spms-windows-x86_64-debug - path: build\bin\windows-x86_64\debugExecutable\*.exe - - - name: Upload release binary - uses: actions/upload-artifact@v3 - with: - name: spms-windows-x86_64-release - path: build\bin\windows-x86_64\releaseExecutable\*.exe - - - name: Build windows-x86_64 binaries without mpv - run: .\gradlew.bat windows-x86_64Binaries -PLINK_STATIC -PDISABLE_MPV - - - name: Upload debug binary - uses: actions/upload-artifact@v3 - with: - name: spms-nompv-windows-x86_64-debug - path: build\bin\windows-x86_64\debugExecutable\*.exe - - - name: Upload release binary - uses: actions/upload-artifact@v3 - with: - name: spms-nompv-windows-x86_64-release - path: build\bin\windows-x86_64\releaseExecutable\*.exe - - - name: Upload DLL dependencies - uses: actions/upload-artifact@v3 - with: - name: dependencies - path: build\bin\windows-x86_64\releaseExecutable\*.dll diff --git a/.gitignore b/.gitignore index a2f500b..3ba8cb3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,11 @@ build/ *.gen.kt .vscode /.kotlin +/.konan +*.log -/src/nativeInterop/* -!/src/nativeInterop/buildscripts/ -/src/nativeInterop/buildscripts/build +/library/src/jvmMain/java + +/library/src/nativeInterop/* +!/library/src/nativeInterop/buildscripts/ +/library/src/nativeInterop/buildscripts/build diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..21eb532 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,125 @@ +import org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmRun +import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink + +val GENERATED_FILE_PREFIX: String = "// Generated on build in build.gradle.kts\n" + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") +} + +kotlin { + jvmToolchain(22) + + jvm { + withJava() + + mainRun { + mainClass.set("dev.toastbits.spms.MainKt") + } + } + + val native_targets = listOf( + linuxX64(), + linuxArm64(), + mingwX64() + ) + + applyDefaultHierarchyTemplate() + + for (target in native_targets) { + target.binaries { + executable { + entryPoint = "dev.toastbits.spms.main" + } + } + } + + sourceSets { + all { + languageSettings.enableLanguageFeature("ExpectActualClasses") + } + + val commonMain by getting { + dependencies { + implementation(project(":library")) + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") + + val clikt_version: String = extra["clikt.version"] as String + implementation("com.github.ajalt.clikt:clikt:$clikt_version") + + val okio_version: String = extra["okio.version"] as String + implementation("com.squareup.okio:okio:$okio_version") + + val mediasession_version: String = extra["mediasession.version"] as String + implementation("dev.toastbits:mediasession:$mediasession_version") + + val kjna_version: String = rootProject.extra["kjna.version"] as String + implementation("dev.toastbits.kjna:runtime:$kjna_version") + } + } + } +} + +tasks.register("bundleIcon") { + val in_file: File = rootProject.file("icon.png") + inputs.file(in_file) + + val out_file: File = project.file("src/commonMain/kotlin/Icon.gen.kt") + outputs.file(out_file) + + doLast { + check(in_file.isFile) + + out_file.writer().use { writer -> + writer.write("${GENERATED_FILE_PREFIX}\n// https://youtrack.jetbrains.com/issue/KT-39194\n") + writer.write("val ICON_BYTES: ByteArray = byteArrayOf(") + + val bytes: ByteArray = in_file.readBytes() + for ((i, byte) in bytes.withIndex()) { + if (byte >= 0) { + writer.write("0x${byte.toString(16)}") + } + else { + writer.write("-0x${byte.toString(16).substring(1)}") + } + + if (i + 1 != bytes.size) { + writer.write(",") + } + } + + writer.write(")\n") + } + } +} + +tasks.matching { it.name.startsWith("compileKotlin") }.all { + dependsOn("bundleIcon") +} + +tasks.withType { + jvmArgs("--enable-native-access=ALL-UNNAMED") +} + +tasks.withType { + val link_task: KotlinNativeLink = this + + finalizedBy( tasks.create("${name}Finalise") { + doFirst { + val patch_command: String = System.getenv("KOTLIN_BINARY_PATCH_COMMAND")?.takeIf { it.isNotBlank() } ?: return@doFirst + + for (dir in link_task.outputs.files) { + for (file in dir.listFiles().orEmpty()) { + if (!file.isFile) { + continue + } + + Runtime.getRuntime().exec(arrayOf(patch_command, file.absolutePath)).waitFor() + } + } + } + } ) +} diff --git a/app/src/commonMain/kotlin/dev/toastbits/spms/AppPlatform.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/AppPlatform.kt new file mode 100644 index 0000000..a3bfabd --- /dev/null +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/AppPlatform.kt @@ -0,0 +1,11 @@ +package dev.toastbits.spms + +import dev.toastbits.spms.indicator.TrayIndicator + +expect fun canOpenProcess(): Boolean +expect fun openProcess(command: String, modes: String) + +expect fun canEndProcess(): Boolean +expect fun endProcess() + +expect fun createTrayIndicator(name: String, icon_path: List): TrayIndicator? diff --git a/src/commonMain/kotlin/spms/Command.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/Command.kt similarity index 77% rename from src/commonMain/kotlin/spms/Command.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/Command.kt index 01a7a28..56dcd71 100644 --- a/src/commonMain/kotlin/spms/Command.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/Command.kt @@ -1,4 +1,4 @@ -package spms +package dev.toastbits.spms import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.Context @@ -7,18 +7,18 @@ import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.help import com.github.ajalt.clikt.parameters.options.option -import spms.localisation.SpMsLocalisation -import spms.localisation.loc -import spms.server.SpMs -import spms.socketapi.shared.SpMsLanguage -import spms.socketapi.shared.SPMS_API_VERSION +import dev.toastbits.spms.localisation.SpMsLocalisation +import dev.toastbits.spms.localisation.loc +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsLanguage +import dev.toastbits.spms.socketapi.shared.SPMS_API_VERSION import kotlin.system.exitProcess - -typealias LocalisedMessageProvider = SpMsLocalisation.() -> String +import dev.toastbits.spms.localisation.AppLocalisedMessageProvider +import dev.toastbits.spms.localisation.AppLocalisation abstract class Command( name: String, - private val help: LocalisedMessageProvider?, + private val help: AppLocalisedMessageProvider?, is_default: Boolean = false, hidden: Boolean = false, help_tags: Map = emptyMap() @@ -45,7 +45,7 @@ abstract class Command( val lang: SpMsLanguage? = SpMsLanguage.fromCode(language) if (lang != null && lang != localisation.language) { - localisation = SpMsLocalisation.get(lang) + localisation = AppLocalisation.get(lang) } context { localization = localisation @@ -64,7 +64,7 @@ abstract class Command( } companion object { - var localisation: SpMsLocalisation = SpMsLocalisation.get() + var localisation: AppLocalisation = AppLocalisation.get() } } diff --git a/src/commonMain/kotlin/spms/client/ClientOptions.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/client/ClientOptions.kt similarity index 77% rename from src/commonMain/kotlin/spms/client/ClientOptions.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/client/ClientOptions.kt index 4b309d1..7533cdc 100644 --- a/src/commonMain/kotlin/spms/client/ClientOptions.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/client/ClientOptions.kt @@ -1,13 +1,13 @@ -package spms.client +package dev.toastbits.spms.client import com.github.ajalt.clikt.parameters.groups.OptionGroup import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.help import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.int -import spms.localisation.loc -import spms.server.DEFAULT_ADDRESS -import spms.socketapi.shared.SPMS_DEFAULT_PORT +import dev.toastbits.spms.localisation.loc +import dev.toastbits.spms.server.DEFAULT_ADDRESS +import dev.toastbits.spms.socketapi.shared.SPMS_DEFAULT_PORT class ClientOptions: OptionGroup() { val ip: String by option("-i", "--ip").default(DEFAULT_ADDRESS).help { context.loc.cli.option_help_server_ip } diff --git a/src/commonMain/kotlin/spms/client/cli/CommandLineClient.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/CommandLineClient.kt similarity index 64% rename from src/commonMain/kotlin/spms/client/cli/CommandLineClient.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/CommandLineClient.kt index eb0aa73..595bdc4 100644 --- a/src/commonMain/kotlin/spms/client/cli/CommandLineClient.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/CommandLineClient.kt @@ -1,18 +1,16 @@ -package spms.client.cli +package dev.toastbits.spms.client.cli -import cinterop.zmq.ZmqSocket +import dev.toastbits.spms.zmq.ZmqSocket +import dev.toastbits.spms.zmq.ZmqSocketType import com.github.ajalt.clikt.core.CliktError import com.github.ajalt.clikt.core.subcommands import com.github.ajalt.clikt.parameters.groups.provideDelegate -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.MemScope -import libzmq.ZMQ_DEALER -import spms.Command -import spms.client.ClientOptions -import spms.client.cli.modes.Interactive -import spms.client.cli.modes.Poll -import spms.client.cli.modes.Run -import toRed +import dev.toastbits.spms.Command +import dev.toastbits.spms.client.ClientOptions +import dev.toastbits.spms.client.cli.modes.Interactive +import dev.toastbits.spms.client.cli.modes.Poll +import dev.toastbits.spms.client.cli.modes.Run +import dev.toastbits.spms.toRed import kotlin.time.Duration val SERVER_REPLY_TIMEOUT: Duration = with (Duration) { 2.seconds } @@ -22,7 +20,6 @@ private fun getClientName(): String = internal class SpMsCommandLineClientError(message: String): CliktError(message.toRed()) -@OptIn(ExperimentalForeignApi::class) class CommandLineClient private constructor(): Command( name = "ctrl", help = { cli.command_help_ctrl }, @@ -33,14 +30,13 @@ class CommandLineClient private constructor(): Command( override fun run() { super.run() - val mem_scope: MemScope = MemScope() - val socket: ZmqSocket = ZmqSocket(mem_scope, ZMQ_DEALER, is_binder = false) + val socket: ZmqSocket = ZmqSocket(ZmqSocketType.DEALER, is_binder = false) - val context: CommandLineModeContext = CommandLineModeContext(socket, mem_scope, getClientName()) + val context: CommandLineModeContext = CommandLineModeContext(socket, getClientName()) currentContext.obj = context if (currentContext.invokedSubcommand == null) { - CommandLineClientMode.getDefault().parse(emptyList(), currentContext) + CommandLineClientMode.getDefault().parse(emptyList()) } } diff --git a/src/commonMain/kotlin/spms/client/cli/CommandLineClientMode.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/CommandLineClientMode.kt similarity index 79% rename from src/commonMain/kotlin/spms/client/cli/CommandLineClientMode.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/CommandLineClientMode.kt index 72aebdb..6527a82 100644 --- a/src/commonMain/kotlin/spms/client/cli/CommandLineClientMode.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/CommandLineClientMode.kt @@ -1,22 +1,23 @@ -package spms.client.cli +package dev.toastbits.spms.client.cli -import cinterop.zmq.ZmqSocket +import dev.toastbits.spms.zmq.ZmqSocket import com.github.ajalt.clikt.core.requireObject import com.github.ajalt.clikt.parameters.groups.provideDelegate import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import spms.Command -import spms.LocalisedMessageProvider -import spms.client.ClientOptions -import spms.client.cli.modes.Interactive -import spms.localisation.loc -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientHandshake -import spms.socketapi.shared.SpMsClientType +import dev.toastbits.spms.Command +import dev.toastbits.spms.client.ClientOptions +import dev.toastbits.spms.client.cli.modes.Interactive +import dev.toastbits.spms.localisation.loc +import dev.toastbits.spms.localisation.AppLocalisedMessageProvider +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientHandshake +import dev.toastbits.spms.socketapi.shared.SpMsClientType +import dev.toastbits.spms.getMachineId abstract class CommandLineClientMode( name: String, - help: LocalisedMessageProvider?, + help: AppLocalisedMessageProvider?, hidden: Boolean = false, help_tags: Map = emptyMap() ): Command(name, help = help, hidden = hidden, help_tags = help_tags) { @@ -42,7 +43,7 @@ abstract class CommandLineClientMode( val handshake: SpMsClientHandshake = SpMsClientHandshake( name = context.client_name, type = type, - machine_id = SpMs.getMachineId(), + machine_id = getMachineId(), language = currentContext.loc.language.name, actions = handshake_actions ) diff --git a/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/CommandLineModeContext.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/CommandLineModeContext.kt new file mode 100644 index 0000000..9acc315 --- /dev/null +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/CommandLineModeContext.kt @@ -0,0 +1,15 @@ +// @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +package dev.toastbits.spms.client.cli + +import dev.toastbits.spms.zmq.ZmqSocket + +data class CommandLineModeContext( + val socket: ZmqSocket, + // val mem_scope: MemScope, + val client_name: String +) { + fun release() { + socket.release() + // mem_scope.clearImpl() + } +} diff --git a/src/commonMain/kotlin/spms/client/cli/modes/Interactive.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/modes/Interactive.kt similarity index 61% rename from src/commonMain/kotlin/spms/client/cli/modes/Interactive.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/modes/Interactive.kt index 76f0f7e..a1a3163 100644 --- a/src/commonMain/kotlin/spms/client/cli/modes/Interactive.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/modes/Interactive.kt @@ -1,6 +1,6 @@ -package spms.client.cli.modes +package dev.toastbits.spms.client.cli.modes -import spms.client.cli.CommandLineClientMode +import dev.toastbits.spms.client.cli.CommandLineClientMode class Interactive: CommandLineClientMode("interactive", { "TODO" }) { override fun run() { @@ -11,6 +11,6 @@ class Interactive: CommandLineClientMode("interactive", { "TODO" }) { println("Running in interactive mode") // TODO - Poll().parse(emptyList(), currentContext) + Poll().parse(emptyList()) } } diff --git a/src/commonMain/kotlin/spms/client/cli/modes/Poll.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/modes/Poll.kt similarity index 80% rename from src/commonMain/kotlin/spms/client/cli/modes/Poll.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/modes/Poll.kt index f046fea..b224607 100644 --- a/src/commonMain/kotlin/spms/client/cli/modes/Poll.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/modes/Poll.kt @@ -1,21 +1,18 @@ -package spms.client.cli.modes +package dev.toastbits.spms.client.cli.modes import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement -import libzmq.ZMQ_NOBLOCK -import spms.client.cli.CommandLineClientMode -import spms.client.cli.SpMsCommandLineClientError -import spms.localisation.loc -import spms.socketapi.shared.SpMsSocketApi -import kotlinx.cinterop.ExperimentalForeignApi +import dev.toastbits.spms.client.cli.CommandLineClientMode +import dev.toastbits.spms.client.cli.SpMsCommandLineClientError +import dev.toastbits.spms.localisation.loc +import dev.toastbits.spms.socketapi.shared.SpMsSocketApi import kotlin.time.* private val SERVER_EVENT_TIMEOUT: Duration = with (Duration) { 10000.milliseconds } private const val POLL_INTERVAL: Long = 100 -@OptIn(ExperimentalForeignApi::class) class Poll: CommandLineClientMode("poll", { "TODO" }) { override fun run() { super.run() @@ -34,7 +31,7 @@ class Poll: CommandLineClientMode("poll", { "TODO" }) { while (events == null && wait_start.elapsedNow() < SERVER_EVENT_TIMEOUT) { val message: List? = with (Duration) { socket.recvStringMultipart( - (SERVER_EVENT_TIMEOUT - wait_start.elapsedNow()).inWholeMilliseconds.coerceAtLeast(ZMQ_NOBLOCK.toLong()).milliseconds + (SERVER_EVENT_TIMEOUT - wait_start.elapsedNow()).inWholeMilliseconds.coerceAtLeast(1L).milliseconds )?.let { SpMsSocketApi.decode(it) } diff --git a/src/commonMain/kotlin/spms/client/cli/modes/Run.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/modes/Run.kt similarity index 85% rename from src/commonMain/kotlin/spms/client/cli/modes/Run.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/modes/Run.kt index e9d2087..743bf70 100644 --- a/src/commonMain/kotlin/spms/client/cli/modes/Run.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/client/cli/modes/Run.kt @@ -1,6 +1,6 @@ -package spms.client.cli.modes +package dev.toastbits.spms.client.cli.modes -import cinterop.zmq.ZmqSocket +import dev.toastbits.spms.zmq.ZmqSocket import com.github.ajalt.clikt.core.CliktError import com.github.ajalt.clikt.core.Context import com.github.ajalt.clikt.core.subcommands @@ -19,21 +19,18 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.serializer import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonPrimitive -import libzmq.ZMQ_NOBLOCK -import spms.socketapi.Action -import spms.socketapi.shared.SpMsSocketApi -import spms.client.cli.CommandLineClientMode -import spms.client.cli.SERVER_REPLY_TIMEOUT -import spms.localisation.loc -import spms.socketapi.shared.SpMsServerHandshake -import spms.socketapi.shared.SpMsActionReply -import spms.socketapi.shared.SPMS_EXPECT_REPLY_CHAR -import spms.socketapi.player.PlayerAction -import spms.socketapi.server.ServerAction -import toRed -import kotlin.system.getTimeMillis -import kotlinx.cinterop.ExperimentalForeignApi +import dev.toastbits.spms.socketapi.Action +import dev.toastbits.spms.socketapi.shared.SpMsSocketApi +import dev.toastbits.spms.client.cli.CommandLineClientMode +import dev.toastbits.spms.client.cli.SERVER_REPLY_TIMEOUT +import dev.toastbits.spms.localisation.loc +import dev.toastbits.spms.socketapi.shared.SpMsServerHandshake +import dev.toastbits.spms.socketapi.shared.SpMsActionReply +import dev.toastbits.spms.socketapi.shared.SPMS_EXPECT_REPLY_CHAR +import dev.toastbits.spms.socketapi.player.PlayerAction +import dev.toastbits.spms.socketapi.server.ServerAction import kotlin.time.* +import dev.toastbits.spms.toRed private fun CommandLineClientMode.jsonModeOption() = option("-j", "--json").flag().help { context.loc.server_actions.option_help_json } @@ -137,8 +134,9 @@ class ActionCommandLineClientMode( if (!silent) { println(currentContext.loc.server_actions.server_completed_request_successfully) } - if (reply.result != null) { - println(action.formatResult(reply.result, currentContext)) + + reply.result?.also { result -> + println(action.formatResult(result, currentContext.loc)) } } } @@ -150,7 +148,6 @@ class ActionCommandLineClientMode( releaseSocket() } - @OptIn(ExperimentalForeignApi::class) private fun executeActionOnSocket( action: Action, parameter_values: List, @@ -179,11 +176,9 @@ class ActionCommandLineClientMode( val decoded: String = SpMsSocketApi.decode(reply).first() try { - val parsed_reply: SpMsServerHandshake = Json.decodeFromString(decoded) - check(!parsed_reply.action_replies.isNullOrEmpty()) { - "Got empty reply from server" - } - return parsed_reply.action_replies.first() + val parsed_replies: List? = Json.decodeFromString(decoded).action_replies + check(!parsed_replies.isNullOrEmpty()) { "Got empty reply from server" } + return parsed_replies.first() } catch (e: Throwable) { throw RuntimeException("JSON decoding server reply failed $decoded", e) diff --git a/src/commonMain/kotlin/spms/client/player/PlayerClient.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/client/player/PlayerClient.kt similarity index 83% rename from src/commonMain/kotlin/spms/client/player/PlayerClient.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/client/player/PlayerClient.kt index 9cb1546..e502694 100644 --- a/src/commonMain/kotlin/spms/client/player/PlayerClient.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/client/player/PlayerClient.kt @@ -1,30 +1,27 @@ -package spms.client.player +package dev.toastbits.spms.client.player -import cinterop.mpv.MpvClientImpl -import cinterop.mpv.getCurrentStateJson -import cinterop.zmq.ZmqSocket +import dev.toastbits.spms.mpv.MpvClientImpl +import dev.toastbits.spms.mpv.getCurrentStateJson +import dev.toastbits.spms.zmq.ZmqSocket +import dev.toastbits.spms.zmq.ZmqSocketType import com.github.ajalt.clikt.parameters.groups.provideDelegate -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.MemScope -import kotlinx.cinterop.memScoped import kotlinx.coroutines.* import kotlinx.serialization.encodeToString import kotlinx.serialization.json.* -import libzmq.ZMQ_DEALER -import libzmq.ZMQ_NOBLOCK -import libzmq.ZMQ_REP -import spms.Command -import spms.client.ClientOptions -import spms.client.cli.SpMsCommandLineClientError -import spms.localisation.loc -import spms.server.PlayerOptions -import spms.server.SpMs -import spms.server.getDeviceName -import spms.socketapi.parseSocketMessage -import spms.socketapi.player.PlayerAction -import spms.socketapi.shared.* -import kotlin.system.getTimeMillis +import dev.toastbits.spms.Command +import dev.toastbits.spms.client.ClientOptions +import dev.toastbits.spms.client.cli.SpMsCommandLineClientError +import dev.toastbits.spms.localisation.loc +import dev.toastbits.spms.server.PlayerOptions +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.parseSocketMessage +import dev.toastbits.spms.socketapi.player.PlayerAction +import dev.toastbits.spms.socketapi.shared.* +import dev.toastbits.spms.getDeviceName +import dev.toastbits.spms.getMachineId +import dev.toastbits.spms.createLibMpv import kotlin.time.* +import gen.libmpv.LibMpv private val SERVER_REPLY_TIMEOUT: Duration = with (Duration) { 2.seconds } private val SERVER_EVENT_TIMEOUT: Duration = with (Duration) { 11.seconds } @@ -34,7 +31,7 @@ private val CLIENT_REPLY_TIMEOUT: Duration = with (Duration) { 1.seconds } private fun getClientName(): String = "SpMs Player Client" -private abstract class PlayerImpl(headless: Boolean = true): MpvClientImpl(headless = headless, playlist_auto_progress = false) { +private abstract class PlayerImpl(libmpv: LibMpv, headless: Boolean = true): MpvClientImpl(libmpv, headless = headless, playlist_auto_progress = false) { private var applied_server_state: SpMsServerState? = null private var server_state_applied_time: TimeMark? = null @@ -136,20 +133,21 @@ private abstract class PlayerImpl(headless: Boolean = true): MpvClientImpl(headl throw RuntimeException("Processing event $event failed", e) } } - - companion object { - fun isAvailable(): Boolean = MpvClientImpl.isAvailable() - } } -@OptIn(ExperimentalForeignApi::class) -class PlayerClient private constructor(): Command( +class PlayerClient private constructor(val libmpv: LibMpv): Command( name = "player", help = { "TODO" }, is_default = true ) { companion object { - fun get(): PlayerClient? = if (PlayerImpl.isAvailable()) PlayerClient() else null + fun get(): PlayerClient? { + if (!LibMpv.isAvailable()) { + return null + } + + return PlayerClient(createLibMpv()) + } } private val client_options by ClientOptions() @@ -166,7 +164,7 @@ class PlayerClient private constructor(): Command( val player_port: Int = client_options.port + 1 - player = object : PlayerImpl(headless = !player_options.enable_gui) { + player = object : PlayerImpl(libmpv, headless = !player_options.enable_gui) { override fun onShutdown() { shutdown = true } @@ -179,24 +177,22 @@ class PlayerClient private constructor(): Command( } runBlocking { - memScoped { - coroutineScope { - launch(Dispatchers.Default) { - runPlayer(this@memScoped, player_port) - } - launch(Dispatchers.Default) { - runServer(this@memScoped, player_port) - } + coroutineScope { + launch(Dispatchers.Default) { + runPlayer(player_port) + } + launch(Dispatchers.Default) { + runServer(player_port) } } } } - private suspend fun runServer(mem_scope: MemScope, player_port: Int) { + private suspend fun runServer(player_port: Int) { val address: String = "tcp://127.0.0.1:$player_port" log("Starting server socket on $address") - val socket: ZmqSocket = ZmqSocket(mem_scope, ZMQ_REP, true) + val socket: ZmqSocket = ZmqSocket(ZmqSocketType.REP, true) socket.connect(address) delay(1000) @@ -214,7 +210,7 @@ class PlayerClient private constructor(): Command( device_name = getDeviceName(), spms_api_version = SPMS_API_VERSION, server_state = player.getCurrentStateJson(), - machine_id = SpMs.getMachineId() + machine_id = getMachineId() ) socket.sendStringMultipart( @@ -242,8 +238,8 @@ class PlayerClient private constructor(): Command( socket.release() } - private suspend fun runPlayer(mem_scope: MemScope, player_port: Int) { - val socket: ZmqSocket = ZmqSocket(mem_scope, ZMQ_DEALER, is_binder = false) + private suspend fun runPlayer(player_port: Int) { + val socket: ZmqSocket = ZmqSocket(ZmqSocketType.DEALER, is_binder = false) val socket_address: String = client_options.getAddress("tcp") log(currentContext.loc.cli.connectingToSocket(socket_address)) @@ -254,7 +250,7 @@ class PlayerClient private constructor(): Command( val handshake: SpMsClientHandshake = SpMsClientHandshake( name = getClientName(), type = SpMsClientType.PLAYER, - machine_id = SpMs.getMachineId(), + machine_id = getMachineId(), language = currentContext.loc.language.name, player_port = player_port ) @@ -310,7 +306,7 @@ class PlayerClient private constructor(): Command( while (events == null && wait_start.elapsedNow() < SERVER_EVENT_TIMEOUT) { val message: List? = with (Duration) { recvStringMultipart( - (SERVER_EVENT_TIMEOUT - wait_start.elapsedNow()).inWholeMilliseconds.coerceAtLeast(ZMQ_NOBLOCK.toLong()).milliseconds + (SERVER_EVENT_TIMEOUT - wait_start.elapsedNow()).inWholeMilliseconds.coerceAtLeast(1L).milliseconds ) } diff --git a/app/src/commonMain/kotlin/dev/toastbits/spms/indicator/AppIndicatorImpl.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/indicator/AppIndicatorImpl.kt new file mode 100644 index 0000000..db9f809 --- /dev/null +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/indicator/AppIndicatorImpl.kt @@ -0,0 +1,153 @@ +package dev.toastbits.spms.indicator + +import dev.toastbits.kjna.runtime.KJnaTypedPointer +import dev.toastbits.kjna.runtime.KJnaFunctionPointer +import dev.toastbits.kjna.runtime.KJnaMemScope +import dev.toastbits.kjna.runtime.KJnaPointer +import dev.toastbits.kjna.runtime.KJnaUtils +import gen.libappindicator.LibAppIndicator +import kjna.struct._AppIndicator +import kjna.struct._GtkMenu +import kjna.struct._GtkMenuShell +import kjna.struct._GMainLoop +import kjna.struct._GtkWidget +import kjna.enum.GConnectFlags +import kjna.enum.AppIndicatorCategory +import kjna.enum.AppIndicatorStatus +import kjna.enum.GLogLevelFlags +import kjna.enum.GLogWriterOutput + +class AppIndicatorImpl(name: String, icon_path: List): TrayIndicator { + private val indicator: KJnaTypedPointer<_AppIndicator> + private val menu: KJnaTypedPointer<_GtkMenu> + private var main_loop: KJnaTypedPointer<_GMainLoop>? = null + + private val library: LibAppIndicator + + init { + try { + KJnaUtils.setLocale( + 1, // LC_NUMERIC + "C" + ) + } + catch (e: Throwable) { + RuntimeException("WARNING: Unable to set LC_NUMERIC locale", e).printStackTrace() + } + + library = LibAppIndicator() + } + + override fun show() { + library.app_indicator_set_menu(indicator, menu) + + main_loop = library.g_main_loop_new(null, 0) + library.g_main_loop_run(main_loop) + main_loop = null + } + + override fun hide() { + main_loop?.also { loop -> + library.g_main_loop_quit(loop) + } + } + + override fun release() { + hide() + } + + override fun addClickCallback(onClick: ClickCallback) { + // TODO + } + + override fun addButton(label: String, onClick: ButtonCallback?) { + val item: KJnaTypedPointer<_GtkWidget> = library.gtk_menu_item_new_with_label(label)!! + + if (onClick != null) { + library.g_signal_connect_data( + item, + "activate", + KJnaFunctionPointer.createDataParamFunction1(), + KJnaFunctionPointer.getDataParam(onClick), + null, + GConnectFlags.G_CONNECT_DEFAULT + ) + } + + library.gtk_menu_shell_append(menu as KJnaTypedPointer<_GtkMenuShell>, item) + library.gtk_widget_show(item) + } + + class AppIndicatorScrollEvent { + fun invoke(p0: KJnaPointer, delta: Int, direction: UInt, function: KJnaFunctionPointer) { + + } + } + + override fun addScrollCallback(onScroll: (delta: Int, direction: Int) -> Unit) { + val (function, data) = getScrollEventFunction(onScroll) + + library.g_signal_connect_data( + indicator, + "scroll-event", + function, + data, + null, + GConnectFlags.G_CONNECT_DEFAULT + ) + } + + init { + getLogWriterFunction( + { level: GLogLevelFlags -> + if (level == GLogLevelFlags.G_LOG_LEVEL_ERROR || level == GLogLevelFlags.G_LOG_LEVEL_CRITICAL) { + GLogWriterOutput.G_LOG_WRITER_UNHANDLED + } + else { + GLogWriterOutput.G_LOG_WRITER_HANDLED + } + } + ).also { (function, data) -> + // Effectively disables GTK warnings + library.g_log_set_writer_func( + function, + data, + null + ) + } + + KJnaMemScope.confined { + val gtk_argc: KJnaTypedPointer = alloc() + if (library.gtk_init_check(gtk_argc, null) == 0) { + throw RuntimeException("Call to gtk_init_check failed") + } + } + + indicator = library.app_indicator_new("spms", "indicator-messages", AppIndicatorCategory.APP_INDICATOR_CATEGORY_APPLICATION_STATUS)!! + + library.app_indicator_set_title(indicator, name) + library.app_indicator_set_status(indicator, AppIndicatorStatus.APP_INDICATOR_STATUS_ACTIVE) + + val path: MutableList = icon_path.toMutableList() + + var filename: String = path.removeLast() + val last_dot: Int = filename.lastIndexOf('.') + if (last_dot != -1) { + filename = filename.substring(0, last_dot) + } + + library.app_indicator_set_icon_theme_path(indicator, '/' + path.joinToString("/")) + library.app_indicator_set_icon_full(indicator, filename, name) + + menu = library.gtk_menu_new() as KJnaTypedPointer<_GtkMenu> + } + + companion object { + fun isAvailable(): Boolean { + return LibAppIndicator.isAvailable() && KJnaUtils.getEnv("XDG_CURRENT_DESKTOP") != null + } + } +} + +internal expect fun getScrollEventFunction(onScroll: (delta: Int, direction: Int) -> Unit): Pair +internal expect fun getLogWriterFunction(handleMessage: (GLogLevelFlags) -> GLogWriterOutput): Pair diff --git a/src/commonMain/kotlin/cinterop/indicator/TrayIndicator.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/indicator/TrayIndicator.kt similarity index 86% rename from src/commonMain/kotlin/cinterop/indicator/TrayIndicator.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/indicator/TrayIndicator.kt index 9c25e4c..a90ecbc 100644 --- a/src/commonMain/kotlin/cinterop/indicator/TrayIndicator.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/indicator/TrayIndicator.kt @@ -1,16 +1,16 @@ -package cinterop.indicator - -typealias ClickCallback = () -> Unit -typealias ButtonCallback = () -> Unit -typealias ScrollCallback = (delta: Int, direction: Int) -> Unit +package dev.toastbits.spms.indicator interface TrayIndicator { fun show() fun hide() - fun release() {} + fun release() fun addClickCallback(onClick: ClickCallback) fun addButton(label: String, onClick: ButtonCallback?) fun addScrollCallback(onScroll: ScrollCallback) } + +typealias ClickCallback = () -> Unit +typealias ButtonCallback = () -> Unit +typealias ScrollCallback = (delta: Int, direction: Int) -> Unit diff --git a/src/commonMain/kotlin/spms/localisation/SpMsLocalisation.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/AppLocalisation.kt similarity index 79% rename from src/commonMain/kotlin/spms/localisation/SpMsLocalisation.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/localisation/AppLocalisation.kt index 3e8b5dc..783be10 100644 --- a/src/commonMain/kotlin/spms/localisation/SpMsLocalisation.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/AppLocalisation.kt @@ -1,35 +1,25 @@ -package spms.localisation +package dev.toastbits.spms.localisation import com.github.ajalt.clikt.core.Context import com.github.ajalt.clikt.output.Localization -import spms.Command -import spms.localisation.strings.CliLocalisation -import spms.localisation.strings.LocalisationEn -import spms.localisation.strings.LocalisationJa -import spms.localisation.strings.ServerActionLocalisation -import spms.localisation.strings.PlayerActionLocalisation -import spms.localisation.strings.ServerLocalisation -import spms.socketapi.shared.SpMsLanguage +import dev.toastbits.spms.Command +import dev.toastbits.spms.localisation.strings.CliLocalisation +import dev.toastbits.spms.localisation.strings.AppLocalisationEn +import dev.toastbits.spms.localisation.strings.AppLocalisationJa +import dev.toastbits.spms.socketapi.shared.SpMsLanguage -val Context.loc: SpMsLocalisation get() = Command.localisation +typealias AppLocalisedMessageProvider = AppLocalisation.() -> String +val Context.loc: AppLocalisation get() = Command.localisation -interface SpMsLocalisation: Localization { - val language: SpMsLanguage - - val server: ServerLocalisation - val server_actions: ServerActionLocalisation - val player_actions: PlayerActionLocalisation +interface AppLocalisation: SpMsLocalisation, Localization { val cli: CliLocalisation - - fun versionInfoText(api_version: Int): String companion object { - fun get(language: SpMsLanguage = SpMsLanguage.current): SpMsLocalisation { - return when (language) { - SpMsLanguage.EN -> LocalisationEn() - SpMsLanguage.JA -> LocalisationJa() + fun get(language: SpMsLanguage = SpMsLanguage.current): AppLocalisation = + when (language) { + SpMsLanguage.EN -> AppLocalisationEn() + SpMsLanguage.JA -> AppLocalisationJa() } - } } override fun usageError(): String diff --git a/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/Language.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/Language.kt new file mode 100644 index 0000000..4533c51 --- /dev/null +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/Language.kt @@ -0,0 +1,5 @@ +package dev.toastbits.spms.localisation + +import dev.toastbits.spms.socketapi.shared.SpMsLanguage + +expect val SpMsLanguage.Companion.current: SpMsLanguage diff --git a/src/commonMain/kotlin/spms/localisation/strings/LocalisationEn.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/AppLocalisationEn.kt similarity index 68% rename from src/commonMain/kotlin/spms/localisation/strings/LocalisationEn.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/AppLocalisationEn.kt index ea9c61b..71f304d 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/LocalisationEn.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/AppLocalisationEn.kt @@ -1,14 +1,12 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings -import spms.socketapi.shared.SpMsLanguage -import spms.localisation.SpMsLocalisation +import dev.toastbits.spms.socketapi.shared.SpMsLanguage +import dev.toastbits.spms.localisation.SpMsLocalisation +import dev.toastbits.spms.localisation.AppLocalisation -class LocalisationEn: SpMsLocalisation { +class AppLocalisationEn: LocalisationEn(), AppLocalisation { override val language: SpMsLanguage = SpMsLanguage.EN - override val server: ServerLocalisation = ServerLocalisationEn() - override val server_actions: ServerActionLocalisation = ServerActionLocalisationEn() - override val player_actions: PlayerActionLocalisation = PlayerActionLocalisationEn() override val cli: CliLocalisation = CliLocalisationEn() override fun versionInfoText(api_version: Int): String = @@ -20,19 +18,19 @@ class LocalisationEn: SpMsLocalisation { override fun badParameter(): String = "invalid value" - override fun badParameterWithMessage(message: String): String = + override fun badParameterWithMessage(message: String): String = "invalid value: $message" - override fun badParameterWithParam(paramName: String): String = + override fun badParameterWithParam(paramName: String): String = "invalid value for $paramName" - override fun badParameterWithMessageAndParam(paramName: String, message: String): String = + override fun badParameterWithMessageAndParam(paramName: String, message: String): String = "invalid value for $paramName: $message" - override fun missingOption(paramName: String): String = + override fun missingOption(paramName: String): String = "missing option $paramName" - override fun missingArgument(paramName: String): String = + override fun missingArgument(paramName: String): String = "missing argument $paramName" override fun noSuchSubcommand(name: String, possibilities: List): String = @@ -63,10 +61,10 @@ class LocalisationEn: SpMsLocalisation { else -> "argument $name requires $count values" } - override fun mutexGroupException(name: String, others: List): String = + override fun mutexGroupException(name: String, others: List): String = "option $name cannot be used with ${others.joinToString(" or ")}" - override fun fileNotFound(filename: String): String = + override fun fileNotFound(filename: String): String = "$filename not found" override fun invalidFileFormat(filename: String, message: String): String = @@ -78,120 +76,120 @@ class LocalisationEn: SpMsLocalisation { override fun unclosedQuote(): String = "unclosed quote" - override fun fileEndsWithSlash(): String = + override fun fileEndsWithSlash(): String = "file ends with \\" - override fun extraArgumentOne(name: String): String = + override fun extraArgumentOne(name: String): String = "got unexpected extra argument $name" - override fun extraArgumentMany(name: String, count: Int): String = + override fun extraArgumentMany(name: String, count: Int): String = "got unexpected extra arguments $name" - override fun invalidFlagValueInFile(name: String): String = + override fun invalidFlagValueInFile(name: String): String = "invalid flag value in file for option $name" - override fun switchOptionEnvvar(): String = + override fun switchOptionEnvvar(): String = "environment variables not supported for switch options" - override fun requiredMutexOption(options: String): String = + override fun requiredMutexOption(options: String): String = "must provide one of $options" - override fun invalidGroupChoice(value: String, choices: List): String = + override fun invalidGroupChoice(value: String, choices: List): String = "invalid choice: $value. (choose from ${choices.joinToString()})" - override fun floatConversionError(value: String): String = + override fun floatConversionError(value: String): String = "$value is not a valid floating point value" - override fun intConversionError(value: String): String = + override fun intConversionError(value: String): String = "$value is not a valid integer" - override fun boolConversionError(value: String): String = + override fun boolConversionError(value: String): String = "$value is not a valid boolean" - override fun rangeExceededMax(value: String, limit: String): String = + override fun rangeExceededMax(value: String, limit: String): String = "$value is larger than the maximum valid value of $limit." - override fun rangeExceededMin(value: String, limit: String): String = + override fun rangeExceededMin(value: String, limit: String): String = "$value is smaller than the minimum valid value of $limit." - override fun rangeExceededBoth(value: String, min: String, max: String): String = + override fun rangeExceededBoth(value: String, min: String, max: String): String = "$value is not in the valid range of $min to $max." - override fun invalidChoice(choice: String, choices: List): String = + override fun invalidChoice(choice: String, choices: List): String = "invalid choice: $choice. (choose from ${choices.joinToString()})" - override fun pathTypeFile(): String = + override fun pathTypeFile(): String = "file" - override fun pathTypeDirectory(): String = + override fun pathTypeDirectory(): String = "directory" - override fun pathTypeOther(): String = + override fun pathTypeOther(): String = "path" - override fun pathDoesNotExist(pathType: String, path: String): String = + override fun pathDoesNotExist(pathType: String, path: String): String = "$pathType \"$path\" does not exist." - override fun pathIsFile(pathType: String, path: String): String = + override fun pathIsFile(pathType: String, path: String): String = "$pathType \"$path\" is a file." - override fun pathIsDirectory(pathType: String, path: String): String = + override fun pathIsDirectory(pathType: String, path: String): String = "$pathType \"$path\" is a directory." - override fun pathIsNotWritable(pathType: String, path: String): String = + override fun pathIsNotWritable(pathType: String, path: String): String = "$pathType \"$path\" is not writable." - override fun pathIsNotReadable(pathType: String, path: String): String = + override fun pathIsNotReadable(pathType: String, path: String): String = "$pathType \"$path\" is not readable." - override fun pathIsSymlink(pathType: String, path: String): String = + override fun pathIsSymlink(pathType: String, path: String): String = "$pathType \"$path\" is a symlink." - override fun defaultMetavar(): String = + override fun defaultMetavar(): String = "value" - override fun stringMetavar(): String = + override fun stringMetavar(): String = "text" - override fun floatMetavar(): String = + override fun floatMetavar(): String = "float" - override fun intMetavar(): String = + override fun intMetavar(): String = "int" - override fun pathMetavar(): String = + override fun pathMetavar(): String = "path" - override fun fileMetavar(): String = + override fun fileMetavar(): String = "file" - override fun usageTitle(): String = + override fun usageTitle(): String = "Usage:" - override fun optionsTitle(): String = + override fun optionsTitle(): String = "Options" - override fun argumentsTitle(): String = + override fun argumentsTitle(): String = "Arguments" - override fun commandsTitle(): String = + override fun commandsTitle(): String = "Commands" - override fun optionsMetavar(): String = + override fun optionsMetavar(): String = "options" - override fun commandMetavar(): String = + override fun commandMetavar(): String = "command" - override fun argumentsMetavar(): String = + override fun argumentsMetavar(): String = "args" - override fun helpTagDefault(): String = + override fun helpTagDefault(): String = "default" - override fun helpTagRequired(): String = + override fun helpTagRequired(): String = "required" - override fun helpOptionMessage(): String = + override fun helpOptionMessage(): String = "Show this message and exit" } diff --git a/src/commonMain/kotlin/spms/localisation/strings/LocalisationJa.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/AppLocalisationJa.kt similarity index 94% rename from src/commonMain/kotlin/spms/localisation/strings/LocalisationJa.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/AppLocalisationJa.kt index 7b8672d..b728fe3 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/LocalisationJa.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/AppLocalisationJa.kt @@ -1,14 +1,12 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings -import spms.socketapi.shared.SpMsLanguage -import spms.localisation.SpMsLocalisation +import dev.toastbits.spms.socketapi.shared.SpMsLanguage +import dev.toastbits.spms.localisation.SpMsLocalisation +import dev.toastbits.spms.localisation.AppLocalisation -class LocalisationJa: SpMsLocalisation { +class AppLocalisationJa: LocalisationJa(), AppLocalisation { override val language: SpMsLanguage = SpMsLanguage.JA - override val server: ServerLocalisation = ServerLocalisationJa() - override val server_actions: ServerActionLocalisation = ServerActionLocalisationJa() - override val player_actions: PlayerActionLocalisation = PlayerActionLocalisationJa() override val cli: CliLocalisation = CliLocalisationJa() override fun versionInfoText(api_version: Int): String = diff --git a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisation.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/CliLocalisation.kt similarity index 71% rename from src/commonMain/kotlin/spms/localisation/strings/CliLocalisation.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/CliLocalisation.kt index f764110..e365bfe 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisation.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/CliLocalisation.kt @@ -1,4 +1,4 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings import kotlin.time.Duration @@ -16,13 +16,8 @@ interface CliLocalisation { val option_help_server_ip: String val option_help_server_port: String - val status_key_queue: String - val status_key_state: String - val status_key_is_playing: String - val status_key_current_item_index: String - val status_key_current_position: String - val status_key_duration: String - val status_key_repeat_mode: String + val indicator_button_open_client: String + val indicator_button_stop_server: String fun connectingToSocket(address: String): String val releasing_socket: String diff --git a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationEn.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/CliLocalisationEn.kt similarity index 77% rename from src/commonMain/kotlin/spms/localisation/strings/CliLocalisationEn.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/CliLocalisationEn.kt index 9cdf40a..9dc5fd3 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationEn.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/CliLocalisationEn.kt @@ -1,6 +1,6 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings -import spms.server.BUG_REPORT_URL +import dev.toastbits.spms.server.BUG_REPORT_URL import kotlin.time.Duration class CliLocalisationEn: CliLocalisation { @@ -22,13 +22,8 @@ class CliLocalisationEn: CliLocalisation { override val option_help_server_ip: String = "The IP address of the server to connect to" override val option_help_server_port: String = "The port of the server to connect to" - override val status_key_queue: String = "Queue" - override val status_key_state: String = "Playback state" - override val status_key_is_playing: String = "Is playing" - override val status_key_current_item_index: String = "Current item index" - override val status_key_current_position: String = "Item position" - override val status_key_duration: String = "Item duration" - override val status_key_repeat_mode: String = "Repeat mode" + override val indicator_button_open_client: String = "Open client" + override val indicator_button_stop_server: String = "Stop" override fun connectingToSocket(address: String): String = "Connecting to socket at $address..." diff --git a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationJa.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/CliLocalisationJa.kt similarity index 79% rename from src/commonMain/kotlin/spms/localisation/strings/CliLocalisationJa.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/CliLocalisationJa.kt index 62e0634..190c373 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/CliLocalisationJa.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/CliLocalisationJa.kt @@ -1,6 +1,6 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings -import spms.server.BUG_REPORT_URL +import dev.toastbits.spms.server.BUG_REPORT_URL import kotlin.time.Duration class CliLocalisationJa: CliLocalisation { @@ -22,13 +22,8 @@ class CliLocalisationJa: CliLocalisation { override val option_help_server_ip: String = "接続先のサーバーのIPアドレス" override val option_help_server_port: String = "接続先のサーバーのポート" - override val status_key_queue: String = "キュー" - override val status_key_state: String = "再生状態" - override val status_key_is_playing: String = "再生中" - override val status_key_current_item_index: String = "再生中のアイテムのインデックス" - override val status_key_current_position: String = "アイテム内の時間" - override val status_key_duration: String = "アイテムの長さ" - override val status_key_repeat_mode: String = "リピートモード" + override val indicator_button_open_client: String = "クライアントを開く" + override val indicator_button_stop_server: String = "終了" override fun connectingToSocket(address: String): String = "${address}のソケットに接続中..." diff --git a/src/commonMain/kotlin/spms/server/SpMsMediaSession.kt.x86_64 b/app/src/commonMain/kotlin/dev/toastbits/spms/mediasession/SpMsMediaSession.kt similarity index 86% rename from src/commonMain/kotlin/spms/server/SpMsMediaSession.kt.x86_64 rename to app/src/commonMain/kotlin/dev/toastbits/spms/mediasession/SpMsMediaSession.kt index 3cf9dc1..04aafb9 100644 --- a/src/commonMain/kotlin/spms/server/SpMsMediaSession.kt.x86_64 +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/mediasession/SpMsMediaSession.kt @@ -1,25 +1,18 @@ -package spms.server +package dev.toastbits.spms.mediasession import dev.toastbits.mediasession.MediaSession import dev.toastbits.mediasession.MediaSessionMetadata import dev.toastbits.mediasession.MediaSessionPlaybackStatus import dev.toastbits.mediasession.MediaSessionLoopMode -import spms.socketapi.shared.SpMsPlayerEvent -import spms.socketapi.shared.SpMsPlayerRepeatMode -import spms.player.Player +import dev.toastbits.spms.socketapi.shared.SpMsPlayerEvent +import dev.toastbits.spms.socketapi.shared.SpMsPlayerRepeatMode +import dev.toastbits.spms.player.Player import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.boolean import kotlinx.serialization.json.long import kotlinx.serialization.json.int -class SpMsMediaSession private constructor(val player: Player) { - val session: MediaSession = - object : MediaSession() { - override fun getPositionMs(): Long { - return player.current_position_ms - } - } - +class SpMsMediaSession private constructor(val player: Player, val session: MediaSession) { init { session.setIdentity("spmp") @@ -117,7 +110,14 @@ class SpMsMediaSession private constructor(val player: Player) { } companion object { - fun create(player: Player): SpMsMediaSession? = - SpMsMediaSession(player) + fun create(player: Player): SpMsMediaSession? { + val session: MediaSession = + MediaSession.create( + getPositionMs = { player.current_position_ms } + ) + ?: return null + + return SpMsMediaSession(player, session) + } } } diff --git a/src/commonMain/kotlin/spms/server/SpMsCommand.kt b/app/src/commonMain/kotlin/dev/toastbits/spms/server/SpMsCommand.kt similarity index 55% rename from src/commonMain/kotlin/spms/server/SpMsCommand.kt rename to app/src/commonMain/kotlin/dev/toastbits/spms/server/SpMsCommand.kt index 2ad1062..54e3e20 100644 --- a/src/commonMain/kotlin/spms/server/SpMsCommand.kt +++ b/app/src/commonMain/kotlin/dev/toastbits/spms/server/SpMsCommand.kt @@ -1,8 +1,8 @@ -package spms.server +package dev.toastbits.spms.server import ICON_BYTES -import cinterop.indicator.createTrayIndicator -import cinterop.indicator.TrayIndicator +import dev.toastbits.spms.indicator.TrayIndicator +import dev.toastbits.spms.createTrayIndicator import com.github.ajalt.clikt.parameters.groups.OptionGroup import com.github.ajalt.clikt.parameters.groups.provideDelegate import com.github.ajalt.clikt.parameters.options.default @@ -10,8 +10,6 @@ import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.help import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.int -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.memScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -21,14 +19,15 @@ import okio.ByteString.Companion.toByteString import okio.FileSystem import okio.Path import okio.Path.Companion.toPath -import spms.localisation.SpMsLocalisation -import spms.localisation.loc +import dev.toastbits.spms.localisation.loc +import dev.toastbits.spms.localisation.AppLocalisation import kotlin.system.exitProcess -import platform.posix.getenv -import kotlinx.cinterop.toKString -import spms.* -import spms.socketapi.shared.SPMS_DEFAULT_PORT -import cinterop.mpv.LibMpvClient +import dev.toastbits.spms.* +import dev.toastbits.spms.socketapi.shared.SPMS_DEFAULT_PORT +import dev.toastbits.spms.socketapi.shared.SpMsPlayerEvent +import dev.toastbits.spms.mpv.LibMpvClient +import dev.toastbits.spms.mediasession.SpMsMediaSession +import gen.libmpv.LibMpv const val PROJECT_URL: String = "https://github.com/toasterofbread/spmp-server" const val BUG_REPORT_URL: String = PROJECT_URL + "/issues" @@ -38,20 +37,14 @@ private const val POLL_INTERVAL: Long = 100 private const val CLIENT_REPLY_ATTEMPTS: Int = 10 @Suppress("OPT_IN_USAGE") -@OptIn(ExperimentalForeignApi::class) fun createIndicator( coroutine_scope: CoroutineScope, - loc: SpMsLocalisation, + loc: AppLocalisation, port: Int, user_icon_path: String?, endProgram: () -> Unit ): TrayIndicator? { - val icon_path: Path = - user_icon_path?.toPath() ?: when (Platform.osFamily) { - OsFamily.LINUX -> "/tmp/ic_spmp.png".toPath() - OsFamily.WINDOWS -> "${getenv("USERPROFILE")!!.toKString()}/AppData/Local/Temp/ic_spmp.png".toPath() - else -> throw NotImplementedError(Platform.osFamily.name) - } + val icon_path: Path = getTempDir().resolve("ic_spmp.png") if (user_icon_path == null && !FileSystem.SYSTEM.exists(icon_path)) { val parent: Path = icon_path.parent!! @@ -77,14 +70,14 @@ fun createIndicator( addButton("Running on port $port", null) if (canOpenProcess()) { - addButton(loc.server.indicator_button_open_client) { + addButton(loc.cli.indicator_button_open_client) { coroutine_scope.launch(Dispatchers.Default) { openProcess("spmp", "r") } } } - addButton(loc.server.indicator_button_stop_server) { + addButton(loc.cli.indicator_button_stop_server) { endProgram() } } @@ -97,10 +90,9 @@ class PlayerOptions: OptionGroup() { val mute_on_start: Boolean by option("-m", "--mute").flag().help { context.loc.server.option_help_mute } } -@OptIn(ExperimentalForeignApi::class) class SpMsCommand: Command( name = "spms", - help = { cli.commandHelpRoot(mpv_enabled = LibMpvClient.isAvailable()) }, + help = { cli.commandHelpRoot(mpv_enabled = LibMpv.isAvailable()) }, is_default = true ) { private val port: Int by option("-p", "--port").int().default(SPMS_DEFAULT_PORT).help { context.loc.server.option_help_port } @@ -120,55 +112,71 @@ class SpMsCommand: Command( return } + var media_session: SpMsMediaSession? = null var stop: Boolean = false - memScoped { - val server: SpMs = - SpMs( - mem_scope = this, - headless = headless, - enable_gui = player_options.enable_gui, - enable_media_session = !no_media_session - ) - server.bind(port) + val server: SpMs = + object : SpMs( + headless = !LibMpv.isAvailable() || headless, + enable_gui = player_options.enable_gui + ) { + override fun onPlayerEvent(event: SpMsPlayerEvent, clientless: Boolean) { + super.onPlayerEvent(event, clientless) + media_session?.onPlayerEvent(event) + } - println(localisation.server.serverBoundToPort(server.toString(), port)) + override fun onPlayerShutdown() { + exitProcess(0) + } + } - if (player_options.mute_on_start) { - server.player.setVolume(0.0) + if (!no_media_session) { + try { + media_session = SpMsMediaSession.create(server.player) } + catch (e: Throwable) { + RuntimeException("Ignoring exception that occurred when creating media session", e).printStackTrace() + } + } - runBlocking { - try { - val indicator: TrayIndicator? = createIndicator(this, localisation, port, icon_path) { - stop = true - } + server.bind(port) - if (indicator != null) { - launch(Dispatchers.Default) { - indicator.show() - } - } + println(localisation.server.serverBoundToPort(server.toString(), port)) - println("--- ${localisation.server.polling_started} ---") - while (!stop) { - server.poll(CLIENT_REPLY_ATTEMPTS) - delay(POLL_INTERVAL) - } - println("--- ${localisation.server.polling_ended} ---") + if (player_options.mute_on_start) { + server.player.setVolume(0.0) + } - server.release() - indicator?.release() + runBlocking { + try { + val indicator: TrayIndicator? = createIndicator(this, localisation, port, icon_path) { + stop = true + } - if (canEndProcess()) { - endProcess() + if (indicator != null) { + launch(Dispatchers.Default) { + indicator.show() } } - catch (e: Throwable) { - RuntimeException("Exception in main command sequence", e).printStackTrace() - throw e + + println("--- ${localisation.server.polling_started} ---") + while (!stop) { + server.poll(CLIENT_REPLY_ATTEMPTS) + delay(POLL_INTERVAL) + } + println("--- ${localisation.server.polling_ended} ---") + + server.release() + indicator?.release() + + if (canEndProcess()) { + endProcess() } } + catch (e: Throwable) { + RuntimeException("Exception in main command sequence", e).printStackTrace() + throw e + } } } } diff --git a/src/commonMain/kotlin/main.kt b/app/src/commonMain/kotlin/main.kt similarity index 57% rename from src/commonMain/kotlin/main.kt rename to app/src/commonMain/kotlin/main.kt index 4557e96..5b5247c 100644 --- a/src/commonMain/kotlin/main.kt +++ b/app/src/commonMain/kotlin/main.kt @@ -1,9 +1,15 @@ +package dev.toastbits.spms + import com.github.ajalt.clikt.core.subcommands -import spms.Command -import spms.client.cli.CommandLineClient -import spms.client.player.PlayerClient -import spms.server.SpMsCommand +import dev.toastbits.spms.Command +import dev.toastbits.spms.client.cli.CommandLineClient +import dev.toastbits.spms.client.player.PlayerClient +import dev.toastbits.spms.server.SpMsCommand +import dev.toastbits.kjna.runtime.KJnaTypedPointer import kotlin.system.exitProcess +import kotlinx.coroutines.* +import gen.libmpv.LibMpv +import kjna.struct.mpv_event fun String.toRed() = "\u001b[31m$this\u001b[0m" diff --git a/app/src/jvmMain/kotlin/dev/toastbits/spms/AppPlatform.jvm.kt b/app/src/jvmMain/kotlin/dev/toastbits/spms/AppPlatform.jvm.kt new file mode 100644 index 0000000..4b808f7 --- /dev/null +++ b/app/src/jvmMain/kotlin/dev/toastbits/spms/AppPlatform.jvm.kt @@ -0,0 +1,16 @@ +package dev.toastbits.spms + +import dev.toastbits.spms.indicator.TrayIndicator +import dev.toastbits.spms.indicator.AppIndicatorImpl + +actual fun canOpenProcess(): Boolean = true +actual fun openProcess(command: String, modes: String) { Runtime.getRuntime().exec(command) } + +actual fun canEndProcess(): Boolean = false +actual fun endProcess() { TODO() } + +actual fun createTrayIndicator(name: String, icon_path: List): TrayIndicator? = + when (OS.current) { + OS.LINUX -> if (AppIndicatorImpl.isAvailable()) AppIndicatorImpl(name, icon_path) else null + OS.WINDOWS -> null + } diff --git a/app/src/jvmMain/kotlin/dev/toastbits/spms/indicator/AppIndicatorImpl.jvm.kt b/app/src/jvmMain/kotlin/dev/toastbits/spms/indicator/AppIndicatorImpl.jvm.kt new file mode 100644 index 0000000..f4cc037 --- /dev/null +++ b/app/src/jvmMain/kotlin/dev/toastbits/spms/indicator/AppIndicatorImpl.jvm.kt @@ -0,0 +1,32 @@ +package dev.toastbits.spms.indicator + +import dev.toastbits.kjna.runtime.KJnaPointer +import dev.toastbits.kjna.runtime.KJnaFunctionPointer +import kjna.enum.GLogWriterOutput +import kjna.enum.GLogLevelFlags +import kjna.enum.fromJvm +import java.lang.foreign.MemorySegment + +class ScrollEventFunction(val onScroll: (delta: Int, direction: Int) -> Unit) { + fun invoke(p0: MemorySegment, delta: Int, direction: Int, function: MemorySegment) { + onScroll(delta, if (direction == 1) 1 else -1) + } +} + +class LogWriterFunction(val handleMessage: (GLogLevelFlags) -> GLogWriterOutput) { + fun invoke(level: Int, p1: MemorySegment, p2: Int, function: MemorySegment): Int { + return handleMessage(GLogLevelFlags.fromJvm(level)).value + } +} + +actual fun getScrollEventFunction(onScroll: (delta: Int, direction: Int) -> Unit): Pair = + Pair( + KJnaFunctionPointer.bindObjectMethod(ScrollEventFunction(onScroll), "invoke", Unit::class, listOf(MemorySegment::class, Int::class, Int::class, MemorySegment::class)), + null + ) + +actual fun getLogWriterFunction(handleMessage: (GLogLevelFlags) -> GLogWriterOutput): Pair = + Pair( + KJnaFunctionPointer.bindObjectMethod(LogWriterFunction(handleMessage), "invoke", Int::class, listOf(Int::class, MemorySegment::class, Int::class, MemorySegment::class)), + null + ) diff --git a/app/src/jvmMain/kotlin/dev/toastbits/spms/localisation/Language.jvm.kt b/app/src/jvmMain/kotlin/dev/toastbits/spms/localisation/Language.jvm.kt new file mode 100644 index 0000000..ea7b36d --- /dev/null +++ b/app/src/jvmMain/kotlin/dev/toastbits/spms/localisation/Language.jvm.kt @@ -0,0 +1,15 @@ +package dev.toastbits.spms.localisation + +import dev.toastbits.spms.socketapi.shared.SpMsLanguage + +actual val SpMsLanguage.Companion.current: SpMsLanguage + get() { + var code: String? = null + for (key in listOf("LANGUAGE", "LANG")) { + code = System.getenv(key) + if (code?.isNotBlank() == true) { + break + } + } + return fromCode(code) ?: default + } diff --git a/app/src/linuxMain/kotlin/dev/toastbits/spms/AppPlatform.linux.kt b/app/src/linuxMain/kotlin/dev/toastbits/spms/AppPlatform.linux.kt new file mode 100644 index 0000000..343b63c --- /dev/null +++ b/app/src/linuxMain/kotlin/dev/toastbits/spms/AppPlatform.linux.kt @@ -0,0 +1,16 @@ +package dev.toastbits.spms + +import platform.posix.* +import kotlinx.cinterop.ExperimentalForeignApi +import dev.toastbits.spms.indicator.AppIndicatorImpl +import dev.toastbits.spms.indicator.TrayIndicator + +actual fun canOpenProcess(): Boolean = true +@OptIn(ExperimentalForeignApi::class) +actual fun openProcess(command: String, modes: String) { popen(command, modes) } + +actual fun canEndProcess(): Boolean = true +actual fun endProcess() { kill(0, SIGTERM) } + +actual fun createTrayIndicator(name: String, icon_path: List): TrayIndicator? = + if (AppIndicatorImpl.isAvailable()) AppIndicatorImpl(name, icon_path) else null diff --git a/app/src/mingwMain/kotlin/dev/toastbits/spms/AppPlatform.mingw.kt b/app/src/mingwMain/kotlin/dev/toastbits/spms/AppPlatform.mingw.kt new file mode 100644 index 0000000..fc432ed --- /dev/null +++ b/app/src/mingwMain/kotlin/dev/toastbits/spms/AppPlatform.mingw.kt @@ -0,0 +1,11 @@ +package dev.toastbits.spms + +import dev.toastbits.spms.indicator.TrayIndicator + +actual fun canOpenProcess(): Boolean = false +actual fun openProcess(command: String, modes: String) { TODO() } + +actual fun canEndProcess(): Boolean = true +actual fun endProcess() { TODO() } + +actual fun createTrayIndicator(name: String, icon_path: List): TrayIndicator? = null diff --git a/app/src/mingwMain/kotlin/dev/toastbits/spms/indicator/TrayIndicator.mingw.kt b/app/src/mingwMain/kotlin/dev/toastbits/spms/indicator/TrayIndicator.mingw.kt new file mode 100644 index 0000000..81cbf9b --- /dev/null +++ b/app/src/mingwMain/kotlin/dev/toastbits/spms/indicator/TrayIndicator.mingw.kt @@ -0,0 +1,16 @@ +package dev.toastbits.spms.indicator + +actual class TrayIndicator actual constructor(name: String, icon_path: List) { + actual fun show() { TODO() } + actual fun hide() { TODO() } + + actual fun release() { TODO() } + + actual fun addClickCallback(onClick: ClickCallback) { TODO() } + actual fun addButton(label: String, onClick: ButtonCallback?) { TODO() } + actual fun addScrollCallback(onScroll: (delta: Int, direction: Int) -> Unit) { TODO() } + + actual companion object { + actual fun isAvailable(): Boolean = false + } +} diff --git a/app/src/nativeMain/kotlin/dev/toastbits/spms/indicator/AppIndicatorImpl.native.kt b/app/src/nativeMain/kotlin/dev/toastbits/spms/indicator/AppIndicatorImpl.native.kt new file mode 100644 index 0000000..581596a --- /dev/null +++ b/app/src/nativeMain/kotlin/dev/toastbits/spms/indicator/AppIndicatorImpl.native.kt @@ -0,0 +1,38 @@ +@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) +package dev.toastbits.spms.indicator + +import dev.toastbits.kjna.runtime.KJnaPointer +import dev.toastbits.kjna.runtime.KJnaFunctionPointer +import kotlinx.cinterop.StableRef +import kotlinx.cinterop.COpaquePointer +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.staticCFunction +import kotlinx.cinterop.asStableRef +import kotlinx.cinterop.invoke +import kotlinx.cinterop.reinterpret +import kjna.enum.GLogWriterOutput +import kjna.enum.GLogLevelFlags +import kjna.enum.fromNative +import gen.libappindicator.cinterop.gint +import gen.libappindicator.cinterop.guint +import gen.libappindicator.cinterop.gsize + +actual fun getScrollEventFunction(onScroll: (delta: Int, direction: Int) -> Unit): Pair = + Pair( + KJnaFunctionPointer( + staticCFunction { _: CPointer<*>, delta: gint, direction: guint, function: COpaquePointer -> + function.asStableRef<(Int, Int) -> Unit>().get().invoke(delta, if (direction == 1U) 1 else -1) + }.reinterpret() + ), + KJnaPointer(StableRef.create(onScroll).asCPointer()), + ) + +actual fun getLogWriterFunction(handleMessage: (GLogLevelFlags) -> GLogWriterOutput): Pair = + Pair( + KJnaFunctionPointer( + staticCFunction { level: UInt, _: CPointer<*>, _: gsize, function: COpaquePointer -> + function.asStableRef<(GLogLevelFlags) -> GLogWriterOutput>().get().invoke(GLogLevelFlags.fromNative(level)).value + }.reinterpret() + ), + KJnaPointer(StableRef.create(handleMessage).asCPointer()), + ) diff --git a/src/commonMain/kotlin/spms/localisation/Language.kt b/app/src/nativeMain/kotlin/dev/toastbits/spms/localisation/Language.native.kt similarity index 73% rename from src/commonMain/kotlin/spms/localisation/Language.kt rename to app/src/nativeMain/kotlin/dev/toastbits/spms/localisation/Language.native.kt index 71d652b..8778850 100644 --- a/src/commonMain/kotlin/spms/localisation/Language.kt +++ b/app/src/nativeMain/kotlin/dev/toastbits/spms/localisation/Language.native.kt @@ -1,12 +1,12 @@ -package spms.localisation +package dev.toastbits.spms.localisation import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.toKString import platform.posix.getenv -import spms.socketapi.shared.SpMsLanguage +import dev.toastbits.spms.socketapi.shared.SpMsLanguage @OptIn(ExperimentalForeignApi::class) -val SpMsLanguage.Companion.current: SpMsLanguage +actual val SpMsLanguage.Companion.current: SpMsLanguage get() { var code: String? = null for (key in listOf("LANGUAGE", "LANG")) { diff --git a/build.gradle.kts b/build.gradle.kts index 9c5bf6e..01a325f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,612 +1,3 @@ -@file:Suppress("UNUSED_VARIABLE") - -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.gradle.plugin.mpp.DefaultCInteropSettings -import org.jetbrains.kotlin.gradle.plugin.mpp.Executable -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation -import org.jetbrains.kotlin.gradle.targets.js.binaryen.BinaryenRootPlugin.Companion.kotlinBinaryenExtension -import org.jetbrains.kotlin.gradle.tasks.CompileUsingKotlinDaemon -import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile - -val GENERATED_FILE_PREFIX: String = "// Generated on build in build.gradle.kts\n" - plugins { - kotlin("multiplatform") - kotlin("plugin.serialization") + kotlin("multiplatform").apply(false) } - -repositories { - mavenLocal() - mavenCentral() - gradlePluginPortal() - maven("https://jitpack.io") -} - -val PLATFORM_SPECIFIC_FILES: List = - listOf( - "cinterop/indicator/TrayIndicatorImpl.kt", - "spms/Platform.kt", - "spms/server/SpMsMediaSession.kt" - ) - -enum class BuildFlag { - DISABLE_MPV, - LINK_STATIC; - - fun isEnabled(project: Project): Boolean = - project.hasProperty(name) -} - -enum class Arch { - X86_64, ARM64; - - val identifier: String get() = - when (this) { - X86_64 -> "x86_64" - ARM64 -> "arm64" - } - - val libdir_name: String get() = - when (this) { - X86_64 -> "x86_64-linux-gnu" - ARM64 -> "aarch64-linux-gnu" - } - - val PKG_CONFIG_PATH: String get() = "/usr/lib/$libdir_name/pkgconfig" - - companion object { - fun byName(name: String): Arch = - when (name.lowercase()) { - "x86_64", "amd64" -> X86_64 - "aarch64", "arm64" -> ARM64 - else -> throw GradleException("Unsupported CPU architecture '$name'") - } - - fun getCurrent(): Arch = - byName(System.getProperty("os.arch")) - - fun getTarget(project: Project): Arch { - val target_override: String? = - project.findProperty("SPMS_ARCH")?.toString() - ?: System.getenv("SPMS_ARCH") - - if (target_override == null) { - return getCurrent() - } - - return byName(target_override) - } - } -} - -enum class Platform { - LINUX_X86, LINUX_ARM64, WINDOWS, OSX_X86, OSX_ARM; - - val identifier: String - get() = when (this) { - LINUX_X86 -> "linux-x86_64" - LINUX_ARM64 -> "linux-arm64" - WINDOWS -> "windows-x86_64" - OSX_X86 -> "osx-x86_64" - OSX_ARM -> "osx-arm64" - } - - val gen_file_extension: String - get() = when (this) { - LINUX_X86, LINUX_ARM64 -> "linux" - WINDOWS -> "windows" - OSX_X86, OSX_ARM -> "osx" - } - - val arch: Arch - get() = when (this) { - LINUX_X86, WINDOWS, OSX_X86 -> Arch.X86_64 - LINUX_ARM64, OSX_ARM -> Arch.ARM64 - } - - val is_linux: Boolean - get() = this == LINUX_X86 || this == LINUX_ARM64 - - fun getNativeDependenciesDir(project: Project) = - project.file("src/nativeInterop/$identifier") - - companion object { - val supported: List = listOf( - LINUX_X86, LINUX_ARM64, WINDOWS - ) - - fun byName(name: String, arch: Arch): Platform = - if (name.lowercase() == "linux") - when (arch) { - Arch.X86_64 -> LINUX_X86 - Arch.ARM64 -> LINUX_ARM64 - } - else if (name.lowercase().startsWith("windows") && arch == Arch.X86_64) WINDOWS - else throw GradleException("Unsupported host OS and architecture '$name' ($arch)") - - fun getCurrent(arch: Arch = Arch.getCurrent()): Platform = - byName(System.getProperty("os.name"), arch) - - fun getTarget(project: Project): Platform { - val arch: Arch = Arch.getTarget(project) - val target_override: String? = - project.findProperty("SPMS_OS")?.toString() - ?: System.getenv("SPMS_OS") - - if (target_override == null) { - return getCurrent(arch) - } - - return byName(target_override, arch) - } - } -} - -enum class CinteropLibraries { - LIBMPV, LIBZMQ, LIBAPPINDICATOR; - - val identifier: String get() = name.lowercase() - - fun includeOnPlatform(platform: Platform): Boolean = - when (this) { - LIBAPPINDICATOR -> platform.is_linux - else -> true - } - - fun includeInProject(project: Project): Boolean = - when (this) { - LIBMPV -> !BuildFlag.DISABLE_MPV.isEnabled(project) - else -> true - } - - fun getDependentFiles(): List = - when (this) { - LIBMPV -> listOf("cinterop/mpv/LibMpvClient.kt", "cinterop/mpv/MpvClientEventLoop.kt") - else -> emptyList() - } - - fun getBinaryDependencies(platform: Platform): List = - when (this) { - LIBMPV -> - if (platform == Platform.WINDOWS) listOf("libmpv-2.dll") - else emptyList() - LIBZMQ -> - if (platform == Platform.WINDOWS) listOf("libzmq-mt-4_3_5.dll") - else emptyList() - else -> emptyList() - } - - private fun getPackageName(): String = - when (this) { - LIBMPV -> "mpv" - LIBZMQ -> "libzmq" - LIBAPPINDICATOR -> "appindicator3-0.1" - } - - fun configure( - platform: Platform, - settings: DefaultCInteropSettings, - deps_directory: File - ) { - val cflags: List = pkgConfig(platform, deps_directory, getPackageName(), cflags = true) - settings.compilerOpts(cflags) - - val default_include_dirs: List = - if (platform.is_linux) listOf("/usr/include", "/usr/include/${platform.arch.libdir_name}").map { File(it) } - else emptyList() - - fun addHeaderFile(path: String) { - var file: File? = null - - for (dir in listOf(deps_directory.resolve("include")) + default_include_dirs) { - if (dir.resolve(path).exists()) { - file = dir.resolve(path) - break - } - } - - if (file == null) { - val first_part: String = path.split("/", limit = 2).first() - - for (flag in cflags) { - if (!flag.startsWith("-I/") || !flag.endsWith("/" + first_part)) { - continue - } - - file = File(flag.drop(2).dropLast(first_part.length)).resolve(path).takeIf { it.exists() } - if (file != null) { - break - } - } - } - - if (file == null) { - // println("Could not find header file '$path' for platform $platform in $cflags") - return - } - - settings.header(file) - } - - when (this) { - LIBMPV -> { - addHeaderFile("mpv/client.h") - } - LIBZMQ -> { - addHeaderFile("zmq.h") - addHeaderFile("zmq_utils.h") - settings.compilerOpts("-DZMQ_BUILD_DRAFT_API=1") - } - LIBAPPINDICATOR -> { - addHeaderFile("libappindicator3-0.1/libappindicator/app-indicator.h") - } - } - } - - fun configureExecutable( - platform: Platform, - static: Boolean, - settings: Executable, - deps_directory: File - ) { - if (platform.is_linux) { - settings.linkerOpts(pkgConfig(platform, deps_directory, getPackageName(), libs = true)) - - if (static) { - when (this) { - LIBZMQ -> { - val zmq: File = deps_directory.resolve("lib/libzmq.a") - settings.linkerOpts("-l:${zmq.absolutePath}") - settings.linkerOpts.remove("-lzmq") - } - else -> {} - } - } - - return - } - - fun addLib(filename: String) { - settings.linkerOpts(deps_directory.resolve("lib").resolve(filename).absolutePath.replace("\\", "/")) - } - - when (this) { - LIBMPV -> { - addLib("libmpv.dll.a") - } - LIBZMQ -> { - addLib("libzmq-mt-4_3_5.lib") - settings.linkerOpts("-lssp", "-static", "-static-libgcc", "-static-libstdc++", "-lgcc", "-lstdc++") - } - LIBAPPINDICATOR -> throw IllegalAccessException() - } - } - - // https://gist.github.com/micolous/c00b14b2dc321fdb0eab8ad796d71b80 - private fun pkgConfig( - platform: Platform, - deps_directory: File, - vararg package_names: String, - cflags: Boolean = false, - libs: Boolean = false - ): List { - if (Platform.getCurrent() == Platform.WINDOWS) { - return emptyList() - } - - require(cflags || libs) - - val process_builder: ProcessBuilder = ProcessBuilder( - listOfNotNull( - "pkg-config", - if (cflags) "--cflags" else null, - if (libs) "--libs" else null - ) + package_names - ) - process_builder.environment()["PKG_CONFIG_PATH"] = - listOfNotNull( - deps_directory.resolve("pkgconfig").takeIf { it.isDirectory }?.absolutePath, - platform.arch.PKG_CONFIG_PATH, - System.getenv("SPMS_LIB")?.plus("/pkgconfig") - ).joinToString(":") - process_builder.environment()["PKG_CONFIG_ALLOW_SYSTEM_LIBS"] = "1" - - val process: Process = process_builder.start() - process.waitFor(10, TimeUnit.SECONDS) - - if (process.exitValue() != 0) { - // println("pkg-config failed for platform $platform with package_names: ${package_names.toList()}\n" + process.errorStream.bufferedReader().use { it.readText() }) - return emptyList() - } - - return process.inputStream.bufferedReader().use { reader -> - reader.readText().split(" ").mapNotNull { it.trim().takeIf { line -> - line.isNotBlank() && line != "-I/usr/include/x86_64-linux-gnu" && line != "-I/usr/include/aarch64-linux-gnu" - } } - } - } -} - -kotlin { - for (os in Platform.supported) { - configureKotlinTarget(os) - } -} - -fun KotlinMultiplatformExtension.configureKotlinTarget(platform: Platform) { - val native_target: KotlinNativeTarget = when (platform) { - Platform.LINUX_X86 -> linuxX64(platform.identifier) - Platform.LINUX_ARM64 -> linuxArm64(platform.identifier) - Platform.WINDOWS -> mingwX64(platform.identifier) - Platform.OSX_X86 -> macosX64(platform.identifier) - Platform.OSX_ARM -> macosArm64(platform.identifier) - } - - val static: Boolean = BuildFlag.LINK_STATIC.isEnabled(project) - val deps_directory: File = platform.getNativeDependenciesDir(project) - - native_target.apply { - compilations.getByName("main") { - cinterops { - for (library in CinteropLibraries.values()) { - if (!library.includeOnPlatform(platform) || !library.includeInProject(project)) { - continue - } - - val library_name: String = library.name.lowercase() - - create(library_name) { - packageName(library_name) - library.configure(platform, this, deps_directory) - } - } - } - } - - binaries { - executable { - baseName = "spms-${platform.identifier}" - entryPoint = "main" - - if (deps_directory.resolve("lib").isDirectory) { - linkerOpts("-L" + deps_directory.resolve("lib").absolutePath) - } - - for (library in CinteropLibraries.values()) { - if (!library.includeOnPlatform(platform) || !library.includeInProject(project)) { - continue - } - library.configureExecutable(platform, static, this, deps_directory) - } - } - } - } - - sourceSets.getByName(platform.identifier + "Main") { - languageSettings.optIn("kotlin.experimental.ExperimentalNativeApi") - languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi") - - dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") - - val okio_version: String = extra["okio.version"] as String - implementation("com.squareup.okio:okio:$okio_version") - - val clikt_version: String = extra["clikt.version"] as String - implementation("com.github.ajalt.clikt:clikt:$clikt_version") - - val mediasession_version: String = extra["mediasession.version"] as String - val ytm_version: String = extra["ytm.version"] as String - - when (platform) { - Platform.LINUX_X86 -> { - implementation("dev.toastbits.mediasession:library-linuxx64:$mediasession_version") - implementation("dev.toastbits.ytmkt:ytmkt-linuxx64:$ytm_version") - } - Platform.LINUX_ARM64 -> { - implementation("dev.toastbits.ytmkt:ytmkt-linuxarm64:$ytm_version") - } - Platform.WINDOWS -> { - implementation("dev.toastbits.mediasession:library-mingwx64:$mediasession_version") - implementation("dev.toastbits.ytmkt:ytmkt-mingwx64:$ytm_version") - } - else -> {} - } - } - } -} - -tasks.withType { - gradleVersion = "7.6" - distributionType = Wrapper.DistributionType.BIN -} - -tasks.register("bundleIcon") { - val in_file: File = project.file("icon.png") - inputs.file(in_file) - - val out_file: File = project.file("src/commonMain/kotlin/Icon.gen.kt") - outputs.file(out_file) - - doLast { - check(in_file.isFile) - - out_file.writer().use { writer -> - writer.write("${GENERATED_FILE_PREFIX}\n// https://youtrack.jetbrains.com/issue/KT-39194\n") - writer.write("val ICON_BYTES: ByteArray = byteArrayOf(") - - val bytes: ByteArray = in_file.readBytes() - for ((i, byte) in bytes.withIndex()) { - if (byte >= 0) { - writer.write("0x${byte.toString(16)}") - } - else { - writer.write("-0x${byte.toString(16).substring(1)}") - } - - if (i + 1 != bytes.size) { - writer.write(",") - } - } - - writer.write(")\n") - } - } -} - - -for (platform in Platform.supported) { - val print_target_task by tasks.register("printTarget${platform.identifier}") { - doLast { - println("Building spmp-server for target $platform") - } - } - - fun String.getFile(suffix: String? = null): File = - project.file("src/commonMain/kotlin/" + if (suffix == null) this.replace(".kt", ".gen.kt") else (this + suffix)) - - val configure_library_dependent_files_task by tasks.register("configureLibraryDependentFiles${platform.identifier}") { - outputs.upToDateWhen { false } - - for (library in CinteropLibraries.values()) { - for (path in library.getDependentFiles()) { - outputs.file(path.getFile()) - inputs.file(path.getFile(".enabled")) - inputs.file(path.getFile(".disabled")) - } - } - - doLast { - for (library in CinteropLibraries.values()) { - val enabled: Boolean = library.includeOnPlatform(platform) && library.includeInProject(project) - - for (path in library.getDependentFiles()) { - val out_file: File = path.getFile() - val in_file: File = path.getFile(if (enabled) ".enabled" else ".disabled") - - out_file.writer().use { writer -> - writer.write(GENERATED_FILE_PREFIX) - - in_file.reader().use { reader -> - reader.transferTo(writer) - } - } - } - } - } - } - val configure_platform_files_task by tasks.register("configurePlatformFiles${platform.identifier}") { - outputs.upToDateWhen { false } - - for (path in PLATFORM_SPECIFIC_FILES) { - outputs.file(path.getFile()) - - val input_files: List = - Platform.supported.map { path.getFile('.' + it.name.lowercase()) } + listOf(path.getFile(".other")) - for (file in input_files) { - if (file.isFile) { - inputs.file(file) - } - } - } - - doLast { - for (path in PLATFORM_SPECIFIC_FILES) { - val out_file: File = path.getFile() - - for (platform_file in listOf( - // OS and arch - path.getFile('.' + platform.gen_file_extension + '.' + platform.arch.identifier), - // OS only - path.getFile('.' + platform.gen_file_extension), - // Arch only - path.getFile('.' + platform.arch.identifier), - // Other - path.getFile(".other") - )) { - if (platform_file.isFile) { - out_file.writer().use { writer -> - writer.write(GENERATED_FILE_PREFIX) - - platform_file.reader().use { reader -> - reader.transferTo(writer) - } - } - break - } - } - } - } - } - - val compile_task = tasks.getByName("compileKotlin" + platform.identifier.capitalised()) { - dependsOn(print_target_task) - dependsOn("bundleIcon") - dependsOn(configure_platform_files_task) - dependsOn(configure_library_dependent_files_task) - } - - val check_dependencies_task by tasks.register("checkDependencies${platform.identifier}") { - doFirst { - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - val compilation = (compile_task.compilation as org.jetbrains.kotlin.gradle.plugin.KotlinCompilationInfo.TCS).compilation as KotlinNativeCompilation - - for (executable in compilation.target.binaries) { - for (opt in executable.linkerOpts) { - if (!opt.startsWith("-l:")) { - continue - } - - val file: File = File(opt.drop(3)) - check(file.isFile) { "Static library doesn't exist '${file.absolutePath}'" } - } - } - } - } - - compile_task.dependsOn(check_dependencies_task) - - val debug_link_task: Task = tasks.getByName("linkDebugExecutable" + platform.identifier.capitalised()) - val debug_finalise_task = tasks.register("finaliseBuildDebug" + platform.identifier.capitalised(), FinaliseBuild::class.java) { - binary_output_directory.set(debug_link_task.outputs.files.single()) - } - debug_link_task.finalizedBy(debug_finalise_task) - - val release_link_task: Task = tasks.getByName("linkReleaseExecutable" + platform.identifier.capitalised()) - val release_finalise_task = tasks.register("finaliseBuildRelease" + platform.identifier.capitalised(), FinaliseBuild::class.java) { - binary_output_directory.set(release_link_task.outputs.files.single()) - } - release_link_task.finalizedBy(release_finalise_task) -} - -open class FinaliseBuild: DefaultTask() { - @get:InputDirectory - val binary_output_directory: DirectoryProperty = project.objects.directoryProperty() - - init { - outputs.upToDateWhen { false } - } - - @TaskAction - fun execute() { - val platform: Platform = Platform.getTarget(project) - - val bin_directory: File = platform.getNativeDependenciesDir(project).resolve("bin") - val target_directory: File = binary_output_directory.get().asFile - - for (library in CinteropLibraries.values()) { - for (filename in library.getBinaryDependencies(platform)) { - val file: File = bin_directory.resolve(filename) - if (!file.isFile) { - continue - } - - file.copyTo(target_directory.resolve(filename), overwrite = true) - } - } - } -} - -fun String.capitalised() = this.first().uppercase() + this.drop(1).lowercase() diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..59cefb6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,44 @@ +{ + "nodes": { + "custom_nixpkgs": { + "locked": { + "lastModified": 1719097418, + "narHash": "sha256-aW6s5uQiZTsHqx1CrY35SK2USbT+p6DKrf60G7DvGbY=", + "owner": "toasterofbread", + "repo": "nixpkgs", + "rev": "4df73973bda897522847e03e0820067c053bccad", + "type": "github" + }, + "original": { + "owner": "toasterofbread", + "repo": "nixpkgs", + "rev": "4df73973bda897522847e03e0820067c053bccad", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1718870667, + "narHash": "sha256-jab3Kpc8O1z3qxwVsCMHL4+18n5Wy/HHKyu1fcsF7gs=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "9b10b8f00cb5494795e5f51b39210fed4d2b0748", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "custom_nixpkgs": "custom_nixpkgs", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..9821daa --- /dev/null +++ b/flake.nix @@ -0,0 +1,82 @@ +{ + description = "SpMp server development environment"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + custom_nixpkgs.url = "github:toasterofbread/nixpkgs/4df73973bda897522847e03e0820067c053bccad"; + }; + + outputs = { self, nixpkgs, custom_nixpkgs, ... }: + let + x86_system = "x86_64-linux"; + arm_system = "aarch64-linux"; + in + { + devShells."${x86_system}".default = + let + pkgs = import nixpkgs { + system = x86_system; + }; + arm_pkgs = import nixpkgs { + system = arm_system; + }; + custom_pkgs = import custom_nixpkgs { + system = x86_system; + }; + in + pkgs.mkShell { + packages = with pkgs; [ + jdk21 + jdk22 + pkg-config + cmake + jextract + mpv + libayatana-appindicator + arm_pkgs.libayatana-appindicator + gtk3 + curl + (custom_pkgs.zeromq-kotlin-native.override { enableDrafts = true; }) + (custom_pkgs.kotlin-native-toolchain-env.override { x86_64 = true; aarch64 = true; }) + + # Runtime + patchelf + glibc + glibc_multi + libgcc.lib + ]; + + JAVA_21_HOME = "${pkgs.jdk21_headless}/lib/openjdk"; + JAVA_22_HOME = "${pkgs.jdk22}/lib/openjdk"; + JAVA_HOME = "${pkgs.jdk21_headless}/lib/openjdk"; + JEXTRACT_PATH = "${pkgs.jextract}/bin/jextract"; + + KOTLIN_BINARY_PATCH_COMMAND = "patchkotlinbinary"; + + shellHook = '' + # Add NIX_LDFLAGS to LD_LIBRARY_PATH + lib_paths=($(echo $NIX_LDFLAGS | grep -oP '(?<=-rpath\s| -L)[^ ]+')) + lib_paths_str=$(IFS=:; echo "''${lib_paths[*]}") + export LD_LIBRARY_PATH="$lib_paths_str:$LD_LIBRARY_PATH" + + # Add glibc and glibc_multi to C_INCLUDE_PATH + export C_INCLUDE_PATH="${pkgs.glibc.dev}/include:${pkgs.glibc_multi.dev}/include:$C_INCLUDE_PATH" + + export KONAN_DATA_DIR=$(pwd)/.konan + + mkdir -p $KONAN_DATA_DIR + cp -asfT ${custom_pkgs.kotlin-native-toolchain-env} $KONAN_DATA_DIR + chmod -R u+w $KONAN_DATA_DIR + + mkdir $KONAN_DATA_DIR/bin + export PATH="$KONAN_DATA_DIR/bin:$PATH" + + PATCH_KOTLIN_BINARY_SCRIPT="patchelf --set-interpreter \$(cat \$NIX_CC/nix-support/dynamic-linker) --set-rpath $KONAN_DATA_DIR/dependencies/x86_64-unknown-linux-gnu-gcc-8.3.0-glibc-2.19-kernel-4.9-2/x86_64-unknown-linux-gnu/sysroot/lib64 \$1" + echo "$PATCH_KOTLIN_BINARY_SCRIPT" > $KONAN_DATA_DIR/bin/$KOTLIN_BINARY_PATCH_COMMAND + chmod +x $KONAN_DATA_DIR/bin/$KOTLIN_BINARY_PATCH_COMMAND + + chmod -R u+w $KONAN_DATA_DIR + ''; + }; + }; +} diff --git a/gradle.properties b/gradle.properties index 4fc30a1..c64fd4f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,12 +1,20 @@ +project.version=0.4.0-alpha2 + kotlin.incremental.native=true org.gradle.jvmargs=-Xmx4g -Xms1g org.gradle.caching=true kotlin.mpp.applyDefaultHierarchyTemplate=false kotlin.mpp.enableCInteropCommonization=true +kotlin.native.cacheKind.linuxX64=none # Dependencies -kotlin.version=2.0.0-RC1 +kotlin.version=2.0.0 okio.version=3.6.0 clikt.version=4.4.0 -mediasession.version=0.0.2 +mediasession.version=0.1.1 ytm.version=0.2.1 +ktor.version=3.0.0-beta-1 +kjna.version=3b0e9cb762 + +# Nix +org.gradle.java.installations.fromEnv=JAVA_21_HOME,JAVA_22_HOME diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e411586..48c0a02 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/library/build-logic/build.gradle.kts b/library/build-logic/build.gradle.kts new file mode 100644 index 0000000..f655440 --- /dev/null +++ b/library/build-logic/build.gradle.kts @@ -0,0 +1,42 @@ +import org.jetbrains.kotlin.config.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + +plugins { + `kotlin-dsl` +} + +repositories { + maven("https://jitpack.io") + gradlePluginPortal() +} + +dependencies { + implementation("com.vanniktech.maven.publish:com.vanniktech.maven.publish.gradle.plugin:0.28.0") +} + +kotlin { + jvmToolchain(21) +} + +// java { +// toolchain { +// languageVersion.set(JavaLanguageVersion.of(22)) +// } +// } + +// kotlin { +// compilerOptions { +// jvmTarget.set("21") + +// } +// } + +// val javaVersion = 22 +// java { +// toolchain { +// languageVersion.set(JavaLanguageVersion.of(javaVersion)) +// } +// } +// tasks.withType(JavaCompile::class) { +// options.release.set(javaVersion) +// } diff --git a/library/build-logic/src/main/kotlin/plugins/plugin.publishing.gradle.kts b/library/build-logic/src/main/kotlin/plugins/plugin.publishing.gradle.kts new file mode 100644 index 0000000..8f11a0b --- /dev/null +++ b/library/build-logic/src/main/kotlin/plugins/plugin.publishing.gradle.kts @@ -0,0 +1,48 @@ +import com.vanniktech.maven.publish.SonatypeHost +import com.vanniktech.maven.publish.KotlinMultiplatform +import org.gradle.api.publish.PublishingExtension + +plugins { + id("com.vanniktech.maven.publish") +} + +mavenPublishing { + coordinates("dev.toastbits", "spms", extra["project.version"] as String) + + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + signAllPublications() + + configure(KotlinMultiplatform( + sourcesJar = true + )) + + pom { + name.set("spmp-server") + description.set("SpMs (short for spmp-server) is the desktop server component for SpMp") + url.set("https://github.com/toasterofbread/spmp-server") + inceptionYear.set("2023") + + licenses { + license { + name.set("GPL-3.0") + url.set("https://www.gnu.org/licenses/gpl-3.0.html") + } + } + developers { + developer { + id.set("toasterofbread") + name.set("Talo Halton") + email.set("talohalton@gmail.com") + url.set("https://github.com/toasterofbread") + } + } + scm { + connection.set("https://github.com/toasterofbread/spmp-server.git") + url.set("https://github.com/toasterofbread/spmp-server") + } + issueManagement { + system.set("Github") + url.set("https://github.com/toasterofbread/spmp-server/issues") + } + } +} diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 0000000..ae866d3 --- /dev/null +++ b/library/build.gradle.kts @@ -0,0 +1,551 @@ +@file:Suppress("UNUSED_VARIABLE") + +import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.DefaultCInteropSettings +import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile +import org.gradle.internal.os.OperatingSystem +import dev.toastbits.kjna.c.CType +import dev.toastbits.kjna.c.CValueType +import dev.toastbits.kjna.c.CFunctionDeclaration +import dev.toastbits.kjna.c.CFunctionParameter +import dev.toastbits.kjna.plugin.KJnaBuildTarget + +val GENERATED_FILE_PREFIX: String = "// Generated on build in library/build.gradle.kts\n" + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") + id("plugin.publishing") + id("dev.toastbits.kjna") +} + +kotlin { + jvmToolchain(22) + + val native_targets: MutableList = mutableListOf() + + for (platform in Platform.supported) { + when (platform) { + Platform.JVM -> { + jvm { + withJava() + } + continue + } + Platform.LINUX_X86 -> native_targets.add(linuxX64().apply { configureNativeTarget(platform) }) + Platform.LINUX_ARM64 -> native_targets.add(linuxArm64().apply { configureNativeTarget(platform) }) + Platform.WINDOWS -> native_targets.add(mingwX64().apply { configureNativeTarget(platform) }) + Platform.OSX_X86 -> native_targets.add(macosX64().apply { configureNativeTarget(platform) }) + Platform.OSX_ARM -> native_targets.add(macosArm64().apply { configureNativeTarget(platform) }) + } + } + + kjna { + generate { + parser_include_dirs += listOf("/usr/include/linux/", "/usr/lib/gcc/x86_64-pc-linux-gnu/14.1.1/include/") + + packages(native_targets) { + add("gen.libmpv") { + enabled = !BuildFlag.DISABLE_MPV.isSet(project) && !BuildFlag.MINIMAL.isSet(project) + + addHeader("mpv/client.h", "LibMpv") + libraries = listOf("mpv") + + jextract { + macros += listOf("size_t=unsigned long") + } + + // if (OperatingSystem.current().isWindows()) { + // overrides.overrideTypedefType("size_t", CType.Primitive.LONG) + // } + } + + add("gen.libappindicator") { + enabled = !BuildFlag.DISABLE_APPINDICATOR.isSet(project) && !BuildFlag.MINIMAL.isSet(project) + disabled_targets = listOf(KJnaBuildTarget.NATIVE_MINGW_X64) + + addHeader("libayatana-appindicator3-0.1/libayatana-appindicator/app-indicator.h", "LibAppIndicator") { + bind_includes += listOf( + "gtk-3.0/gtk/gtkmain.h", + "gtk-3.0/gtk/gtkmenushell.h", + "gtk-3.0/gtk/gtkmenuitem.h", + "gtk-3.0/gtk/gtkwidget.h", + "gtk-3.0/gtk/gtkmenu.h", + "glib-2.0/glib/gmain.h", + "glib-2.0/gobject/gsignal.h", + "glib-2.0/glib/gmessages.h" + ) + exclude_functions += listOf("g_clear_handle_id", "_g_log_fallback_handler", "_g_signals_destroy") + } + + libraries += Static.pkgConfig(Platform.getCurrent(), null, "ayatana-appindicator3-0.1", libs = true).mapNotNull { if (it.startsWith("-l")) it.drop(2) else null } + + for (cflag in Static.pkgConfig(Platform.getCurrent(), null, "ayatana-appindicator3-0.1", cflags = true)) { + if (cflag.startsWith("-I")) { + include_dirs += listOf(cflag.drop(2)) + } + } + + parser_ignore_headers += listOf("glib/gwin32.h", "type_traits") + + overrides.overrideTypedefType( + "GCallback", + CType.Function( + CFunctionDeclaration( + "GCallback", null, listOf(CFunctionParameter(null, CValueType(CType.Primitive.VOID, 1))) + ), + typedef_name = "GCallback" + ), + pointer_depth = 1 + ) + + // _cairo_path_data_t + overrides.overrideAnonymousStructIndex(24, 10) + overrides.overrideAnonymousStructIndex(25, 11) + } + } + } + } + + applyDefaultHierarchyTemplate() + + sourceSets { + all { + languageSettings.apply { + optIn("kotlin.experimental.ExperimentalNativeApi") + optIn("kotlinx.cinterop.ExperimentalForeignApi") + + enableLanguageFeature("ExpectActualClasses") + } + } + + val ytm_version: String = extra["ytm.version"] as String + + val commonMain by getting { + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") + + val okio_version: String = extra["okio.version"] as String + implementation("com.squareup.okio:okio:$okio_version") + + implementation("dev.toastbits.ytmkt:ytmkt:$ytm_version") + + val kjna_version: String = rootProject.extra["kjna.version"] as String + implementation("dev.toastbits.kjna:runtime:$kjna_version") + } + } + + val jvmMain by getting { + dependencies { + implementation("net.java.dev.jna:jna:5.14.0") + implementation("org.zeromq:jeromq:0.6.0") + + val ktor_version: String = extra["ktor.version"] as String + implementation("io.ktor:ktor-client-core:$ktor_version") + implementation("io.ktor:ktor-client-core:$ktor_version") + implementation("io.ktor:ktor-client-cio:$ktor_version") + } + } + } +} + +enum class BuildFlag { + DISABLE_MPV, DISABLE_APPINDICATOR, MINIMAL; + + fun isSet(project: Project): Boolean { + return project.hasProperty(name) + } +} + +enum class Arch { + X86_64, ARM64; + + val identifier: String get() = + when (this) { + X86_64 -> "x86_64" + ARM64 -> "arm64" + } + + val libdir_name: String get() = + when (this) { + X86_64 -> "x86_64-linux-gnu" + ARM64 -> "aarch64-linux-gnu" + } + + val PKG_CONFIG_PATH: String get() = "/usr/lib/$libdir_name/pkgconfig" + + companion object { + fun byName(name: String): Arch = + when (name.lowercase()) { + "x86_64", "amd64" -> X86_64 + "aarch64", "arm64" -> ARM64 + else -> throw GradleException("Unsupported CPU architecture '$name'") + } + + fun getCurrent(): Arch = + byName(System.getenv("SPMS_ARCH") ?: System.getProperty("os.arch")) + } +} + +enum class Platform { + JVM, LINUX_X86, LINUX_ARM64, WINDOWS, OSX_X86, OSX_ARM; + + val identifier: String get() = + when (this) { + JVM -> "jvm" + LINUX_X86 -> "linuxX64" + LINUX_ARM64 -> "linuxArm64" + WINDOWS -> "mingwX64" + OSX_X86 -> "macosX64" + OSX_ARM -> "macosArm64" + } + + val gen_file_extension: String + get() = when (this) { + JVM -> "jvm" + LINUX_X86, LINUX_ARM64 -> "linux" + WINDOWS -> "windows" + OSX_X86, OSX_ARM -> "osx" + } + + val arch: Arch + get() = when (this) { + JVM -> Arch.X86_64 + LINUX_X86, WINDOWS, OSX_X86 -> Arch.X86_64 + LINUX_ARM64, OSX_ARM -> Arch.ARM64 + } + + val is_native: Boolean + get() = this != JVM + + val is_linux: Boolean + get() = this == LINUX_X86 || this == LINUX_ARM64 + + fun getNativeDependenciesDir(project: Project) = + project.file("src/nativeInterop/$identifier") + + companion object { + val supported: List = listOf( + JVM, LINUX_X86, LINUX_ARM64 + ) + + fun byName(name: String, arch: Arch): Platform = + if (name.lowercase() == "linux") + when (arch) { + Arch.X86_64 -> LINUX_X86 + Arch.ARM64 -> LINUX_ARM64 + } + else if (name.lowercase().startsWith("windows") && arch == Arch.X86_64) WINDOWS + else throw GradleException("Unsupported host OS and architecture '$name' ($arch)") + + fun getCurrent(arch: Arch = Arch.getCurrent()): Platform = + byName(System.getenv("SPMS_PLATFORM") ?: System.getProperty("os.name"), arch) + } +} + +enum class CinteropLibraries { + LIBZMQ; + + val identifier: String get() = name.lowercase() + + fun shouldInclude(project: Project, platform: Platform): Boolean = + when (this) { + else -> true + } + + fun getDependentFiles(): List = + when (this) { + else -> emptyList() + } + + fun getBinaryDependencies(platform: Platform): List = + when (this) { + LIBZMQ -> + if (platform == Platform.WINDOWS) listOf("libzmq-mt-4_3_5.dll") + else emptyList() + else -> emptyList() + } + + private fun getPackageName(): String = + when (this) { + LIBZMQ -> "libzmq" + } + + fun configureCinterop( + project: Project, + platform: Platform, + settings: DefaultCInteropSettings, + deps_directory: File + ) { + val cflags: List = Static.pkgConfig(platform, deps_directory, getPackageName(), cflags = true) + settings.compilerOpts(cflags) + + val default_include_dirs: List = ( + if (platform.is_linux) listOf("/usr/include", "/usr/include/${platform.arch.libdir_name}").map { File(it) } + else emptyList() + ) + System.getenv("CMAKE_INCLUDE_PATH").orEmpty().split(":").map { File(it) } + + fun addHeaderFile(path: String) { + var file: File? = null + + for (dir in listOf(deps_directory.resolve("include")) + default_include_dirs) { + if (dir.resolve(path).exists()) { + file = dir.resolve(path) + break + } + } + + if (file == null) { + val first_part: String = path.split("/", limit = 2).first() + + for (flag in cflags) { + if (!flag.startsWith("-I/") || !flag.endsWith("/" + first_part)) { + continue + } + + file = File(flag.drop(2).dropLast(first_part.length)).resolve(path).takeIf { it.exists() } + if (file != null) { + break + } + } + + if (file == null) { + println("WARNING: Could not find header file '$path' for platform $platform in $cflags or ${deps_directory.resolve("include")} or $default_include_dirs") + return + } + } + + settings.header(file) + } + + when (this) { + LIBZMQ -> { + addHeaderFile("zmq.h") + addHeaderFile("zmq_utils.h") + settings.compilerOpts("-DZMQ_BUILD_DRAFT_API=1") + } + } + + val deps_libs_dir: File = deps_directory.resolve("lib") + val lib_dirs: List = listOf(deps_libs_dir) + System.getenv("LD_LIBRARY_PATH").orEmpty().split(":").map { File(it) } + + val lib_filenames: List = + when (this) { + LIBZMQ -> listOf("libzmq.a") + else -> emptyList() + } + + for (filename in lib_filenames) { + if (lib_dirs.none { it.resolve(filename).isFile }) { + println("WARNING: Could not find library file '$filename' in $deps_libs_dir or LD_LIBRARY_PATH") + } + } + + val def_file: File = project.file("build/def/${name}.${platform.identifier}.def") + if (!def_file.exists()) { + def_file.ensureParentDirsCreated() + def_file.createNewFile() + } + + val linker_opts: MutableList = Static.pkgConfig(platform, deps_directory, getPackageName(), libs = true).toMutableList() + + when (this) { + LIBZMQ -> { + if (!platform.is_linux) { + linker_opts.addAll(listOf("-lssp", "-static", "-static-libgcc", "-static-libstdc++", "-lgcc", "-lstdc++")) + } + } + else -> {} + } + + def_file.writeText(""" + staticLibraries = ${lib_filenames.joinToString(" ")} + libraryPaths = ${lib_dirs.map { it.absolutePath }.joinToString(" ")} + linkerOpts = ${linker_opts.joinToString(" ")} + """.trimIndent()) + + settings.defFile(def_file) + } +} + +fun KotlinNativeTarget.configureNativeTarget(platform: Platform) { + val deps_directory: File = platform.getNativeDependenciesDir(project) + + compilations.getByName("main") { + cinterops { + for (library in CinteropLibraries.values()) { + if (!library.shouldInclude(project, platform)) { + continue + } + + val library_name: String = library.name.lowercase() + + create(library_name) { + packageName(library_name) + library.configureCinterop(project, platform, this, deps_directory) + } + } + } + } + + binaries { + executable { + baseName = "spms-${platform.identifier}" + entryPoint = "main" + } + } +} + +for (platform in Platform.supported) { + fun String.getFile(suffix: String? = null): File = + project.file("src/" + if (suffix == null) this.replace(".kt", ".gen.kt") else (this + suffix)) + + val configure_library_dependent_files_task by tasks.register("configureLibraryDependentFiles${platform.identifier}") { + outputs.upToDateWhen { false } + + for (library in CinteropLibraries.values()) { + for (path in library.getDependentFiles()) { + // outputs.file(path.getFile()) + inputs.file(path.getFile(".enabled")) + inputs.file(path.getFile(".disabled")) + } + } + + doLast { + for (library in CinteropLibraries.values()) { + val enabled: Boolean = library.shouldInclude(project, platform) + + for (path in library.getDependentFiles()) { + if (path.getFile("").isFile) { + continue + } + + val out_file: File = path.getFile() + val in_file: File = path.getFile(if (enabled) ".enabled" else ".disabled") + + out_file.writer().use { writer -> + writer.write(GENERATED_FILE_PREFIX) + + in_file.reader().use { reader -> + reader.transferTo(writer) + } + } + } + } + } + } + + val compile_task: Task = + tasks.getByName("compileKotlin" + platform.identifier.capitalised()) { + dependsOn(configure_library_dependent_files_task) + } + + if (compile_task !is KotlinNativeCompile) { + continue + } + + val debug_link_task: Task = tasks.getByName("linkDebugExecutable" + platform.identifier.capitalised()) + val debug_finalise_task = tasks.register("finaliseBuildDebug" + platform.identifier.capitalised(), FinaliseBuild::class.java) { + binary_output_directory.set(debug_link_task.outputs.files.single()) + } + debug_link_task.finalizedBy(debug_finalise_task) + + val release_link_task: Task = tasks.getByName("linkReleaseExecutable" + platform.identifier.capitalised()) + val release_finalise_task = tasks.register("finaliseBuildRelease" + platform.identifier.capitalised(), FinaliseBuild::class.java) { + binary_output_directory.set(release_link_task.outputs.files.single()) + } + release_link_task.finalizedBy(release_finalise_task) +} + +open class FinaliseBuild: DefaultTask() { + @get:InputDirectory + val binary_output_directory: DirectoryProperty = project.objects.directoryProperty() + + init { + outputs.upToDateWhen { false } + } + + @TaskAction + fun execute() { + for (platform in Platform.supported) { + val bin_directory: File = platform.getNativeDependenciesDir(project).resolve("bin") + val target_directory: File = binary_output_directory.get().asFile + + for (library in CinteropLibraries.values()) { + for (filename in library.getBinaryDependencies(platform)) { + val file: File = bin_directory.resolve(filename) + if (!file.isFile) { + continue + } + + file.copyTo(target_directory.resolve(filename), overwrite = true) + } + } + } + } +} + +fun String.capitalised(lowercase_other_chars: Boolean = false) = + if (lowercase_other_chars) this.first().uppercase() + this.drop(1).lowercase() + else this.first().uppercase() + this.drop(1) + +object Static { + // https://gist.github.com/micolous/c00b14b2dc321fdb0eab8ad796d71b80 + fun pkgConfig( + platform: Platform, + deps_directory: File?, + vararg package_names: String, + cflags: Boolean = false, + libs: Boolean = false + ): List { + if (Platform.getCurrent() == Platform.WINDOWS) { + return emptyList() + } + + require(cflags || libs) + + val process_builder: ProcessBuilder = ProcessBuilder( + listOfNotNull( + findExecutable("pkg-config"), + if (cflags) "--cflags" else null, + if (libs) "--libs" else null + ) + package_names + ) + process_builder.environment()["PKG_CONFIG_PATH"] = ( + System.getenv("PKG_CONFIG_PATH")?.plus(":").orEmpty() + + listOfNotNull( + deps_directory?.resolve("pkgconfig")?.takeIf { it.isDirectory }?.absolutePath, + platform.arch.PKG_CONFIG_PATH, + System.getenv("SPMS_LIB")?.plus("/pkgconfig") + ).joinToString(":") + ) + process_builder.environment()["PKG_CONFIG_ALLOW_SYSTEM_LIBS"] = "1" + + val process: Process = process_builder.start() + process.waitFor(10, TimeUnit.SECONDS) + + if (process.exitValue() != 0) { + // println("pkg-config failed for platform $platform with package_names: ${package_names.toList()}\n" + process.errorStream.bufferedReader().use { it.readText() }) + return emptyList() + } + + return process.inputStream.bufferedReader().use { reader -> + reader.readText().split(" ").mapNotNull { it.trim().takeIf { line -> + line.isNotBlank() && line != "-I/usr/include/x86_64-linux-gnu" && line != "-I/usr/include/aarch64-linux-gnu" + } } + } + } + + private fun findExecutable(name: String): String { + for (dir in System.getenv("PATH")?.split(":").orEmpty()) { + val file: File = File(dir).resolve(name) + if (file.isFile) { + return file.absolutePath + } + } + + return name + } +} diff --git a/library/src/commonMain/kotlin/dev/toastbits/spms/Platform.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/Platform.kt new file mode 100644 index 0000000..d61d460 --- /dev/null +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/Platform.kt @@ -0,0 +1,46 @@ +package dev.toastbits.spms + +import okio.Path +import okio.FileSystem +import dev.toastbits.spms.PLATFORM +import gen.libmpv.LibMpv + +expect val FileSystem.Companion.PLATFORM: FileSystem + +expect fun getHostname(): String +expect fun getOSName(): String + +expect fun getTempDir(): Path + +expect fun getCacheDir(): Path + +expect fun createLibMpv(): LibMpv + +fun getMachineId(): String { + val id_path: Path = getTempDir().resolve("spmp_machine_id.txt") + + if (FileSystem.PLATFORM.exists(id_path)) { + return FileSystem.PLATFORM.read(id_path) { + readUtf8() + } + } + + val parent: Path = id_path.parent!! + if (!FileSystem.PLATFORM.exists(parent)) { + FileSystem.PLATFORM.createDirectories(parent, true) + } + + val id_length: Int = 8 + val allowed_chars: List = ('A'..'Z') + ('a'..'z') + ('0'..'9') + + val new_id: String = (1..id_length).map { allowed_chars.random() }.joinToString("") + + FileSystem.PLATFORM.write(id_path) { + writeUtf8(new_id) + } + + return new_id +} + +fun getDeviceName(): String = + "${getHostname()} on ${getOSName()}" diff --git a/library/src/commonMain/kotlin/dev/toastbits/spms/ReentrantLock.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/ReentrantLock.kt new file mode 100644 index 0000000..70285ab --- /dev/null +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/ReentrantLock.kt @@ -0,0 +1,5 @@ +package dev.toastbits.spms + +expect class ReentrantLock() { + inline fun withLock(action: () -> T): T +} diff --git a/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/SpMsLocalisation.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/SpMsLocalisation.kt new file mode 100644 index 0000000..9b642f4 --- /dev/null +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/SpMsLocalisation.kt @@ -0,0 +1,28 @@ +package dev.toastbits.spms.localisation + +import dev.toastbits.spms.localisation.strings.LocalisationEn +import dev.toastbits.spms.localisation.strings.LocalisationJa +import dev.toastbits.spms.localisation.strings.ServerActionLocalisation +import dev.toastbits.spms.localisation.strings.PlayerActionLocalisation +import dev.toastbits.spms.localisation.strings.ServerLocalisation +import dev.toastbits.spms.socketapi.shared.SpMsLanguage + +typealias LocalisedMessageProvider = SpMsLocalisation.() -> String + +interface SpMsLocalisation { + val language: SpMsLanguage + + val server: ServerLocalisation + val server_actions: ServerActionLocalisation + val player_actions: PlayerActionLocalisation + + fun versionInfoText(api_version: Int): String + + companion object { + fun get(language: SpMsLanguage): SpMsLocalisation = + when (language) { + SpMsLanguage.EN -> LocalisationEn() + SpMsLanguage.JA -> LocalisationJa() + } + } +} diff --git a/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/LocalisationEn.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/LocalisationEn.kt new file mode 100644 index 0000000..8df23bc --- /dev/null +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/LocalisationEn.kt @@ -0,0 +1,15 @@ +package dev.toastbits.spms.localisation.strings + +import dev.toastbits.spms.socketapi.shared.SpMsLanguage +import dev.toastbits.spms.localisation.SpMsLocalisation + +open class LocalisationEn: SpMsLocalisation { + override val language: SpMsLanguage = SpMsLanguage.EN + + override val server: ServerLocalisation = ServerLocalisationEn() + override val server_actions: ServerActionLocalisation = ServerActionLocalisationEn() + override val player_actions: PlayerActionLocalisation = PlayerActionLocalisationEn() + + override fun versionInfoText(api_version: Int): String = + "SpMs API v$api_version" +} diff --git a/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/LocalisationJa.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/LocalisationJa.kt new file mode 100644 index 0000000..bf0e545 --- /dev/null +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/LocalisationJa.kt @@ -0,0 +1,15 @@ +package dev.toastbits.spms.localisation.strings + +import dev.toastbits.spms.socketapi.shared.SpMsLanguage +import dev.toastbits.spms.localisation.SpMsLocalisation + +open class LocalisationJa: SpMsLocalisation { + override val language: SpMsLanguage = SpMsLanguage.JA + + override val server: ServerLocalisation = ServerLocalisationJa() + override val server_actions: ServerActionLocalisation = ServerActionLocalisationJa() + override val player_actions: PlayerActionLocalisation = PlayerActionLocalisationJa() + + override fun versionInfoText(api_version: Int): String = + "SpMs API バージョン v$api_version" +} diff --git a/src/commonMain/kotlin/spms/localisation/strings/PlayerActionLocalisation.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/PlayerActionLocalisation.kt similarity index 92% rename from src/commonMain/kotlin/spms/localisation/strings/PlayerActionLocalisation.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/PlayerActionLocalisation.kt index 02f0272..2e61bfa 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/PlayerActionLocalisation.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/PlayerActionLocalisation.kt @@ -1,4 +1,4 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings interface PlayerActionLocalisation { val set_auth_info_name: String diff --git a/src/commonMain/kotlin/spms/localisation/strings/PlayerActionLocalisationEn.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/PlayerActionLocalisationEn.kt similarity index 96% rename from src/commonMain/kotlin/spms/localisation/strings/PlayerActionLocalisationEn.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/PlayerActionLocalisationEn.kt index 5d80a9f..4fcf9b1 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/PlayerActionLocalisationEn.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/PlayerActionLocalisationEn.kt @@ -1,4 +1,4 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings class PlayerActionLocalisationEn: PlayerActionLocalisation { override val set_auth_info_name: String = "Set authentication info" diff --git a/src/commonMain/kotlin/spms/localisation/strings/PlayerActionLocalisationJa.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/PlayerActionLocalisationJa.kt similarity index 97% rename from src/commonMain/kotlin/spms/localisation/strings/PlayerActionLocalisationJa.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/PlayerActionLocalisationJa.kt index 05cbb28..7c51d62 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/PlayerActionLocalisationJa.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/PlayerActionLocalisationJa.kt @@ -1,4 +1,4 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings class PlayerActionLocalisationJa: PlayerActionLocalisation { override val set_auth_info_name: String = "認証データを設定" diff --git a/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisation.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerActionLocalisation.kt similarity index 86% rename from src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisation.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerActionLocalisation.kt index 6af191d..98a4333 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisation.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerActionLocalisation.kt @@ -1,4 +1,4 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings import kotlin.time.Duration @@ -14,6 +14,14 @@ interface ServerActionLocalisation { val option_help_json: String + val status_key_queue: String + val status_key_state: String + val status_key_is_playing: String + val status_key_current_item_index: String + val status_key_current_position: String + val status_key_duration: String + val status_key_repeat_mode: String + // --- Action-specific strings --- val add_item_name: String diff --git a/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationEn.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerActionLocalisationEn.kt similarity index 90% rename from src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationEn.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerActionLocalisationEn.kt index 3ac05c2..1db593f 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationEn.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerActionLocalisationEn.kt @@ -1,4 +1,4 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings import kotlin.time.Duration @@ -22,6 +22,14 @@ class ServerActionLocalisationEn: ServerActionLocalisation { override val option_help_json: String = "Output result data in JSON format if possible" + override val status_key_queue: String = "Queue" + override val status_key_state: String = "Playback state" + override val status_key_is_playing: String = "Is playing" + override val status_key_current_item_index: String = "Current item index" + override val status_key_current_position: String = "Item position" + override val status_key_duration: String = "Item duration" + override val status_key_repeat_mode: String = "Repeat mode" + override val add_item_name: String = "Add item" override val add_item_help: String = "Add an item to the queue" override val add_item_param_item_id: String = "YouTube ID of the item to add" diff --git a/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationJa.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerActionLocalisationJa.kt similarity index 90% rename from src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationJa.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerActionLocalisationJa.kt index 0e34db7..ca1ce62 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/ServerActionLocalisationJa.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerActionLocalisationJa.kt @@ -1,4 +1,4 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings import kotlin.time.Duration @@ -22,6 +22,14 @@ class ServerActionLocalisationJa: ServerActionLocalisation { override val option_help_json: String = "可能ならば結果のデータをJSONとして出力" + override val status_key_queue: String = "キュー" + override val status_key_state: String = "再生状態" + override val status_key_is_playing: String = "再生中" + override val status_key_current_item_index: String = "再生中のアイテムのインデックス" + override val status_key_current_position: String = "アイテム内の時間" + override val status_key_duration: String = "アイテムの長さ" + override val status_key_repeat_mode: String = "リピートモード" + override val add_item_name: String = "アイテムを追加" override val add_item_help: String = "キューにアイテムを付け足す" override val add_item_param_item_id: String = "入れるアイテムのYouTube ID" diff --git a/src/commonMain/kotlin/spms/localisation/strings/ServerLocalisation.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerLocalisation.kt similarity index 74% rename from src/commonMain/kotlin/spms/localisation/strings/ServerLocalisation.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerLocalisation.kt index a418e56..ae01ae7 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/ServerLocalisation.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerLocalisation.kt @@ -1,4 +1,4 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings interface ServerLocalisation { fun serverBoundToPort(server: String, port: Int): String @@ -12,7 +12,4 @@ interface ServerLocalisation { val option_help_headless: String val option_no_media_session: String val option_help_icon: String - - val indicator_button_open_client: String - val indicator_button_stop_server: String } diff --git a/src/commonMain/kotlin/spms/localisation/strings/ServerLocalisationEn.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerLocalisationEn.kt similarity index 81% rename from src/commonMain/kotlin/spms/localisation/strings/ServerLocalisationEn.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerLocalisationEn.kt index 152b07c..76a0ee0 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/ServerLocalisationEn.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerLocalisationEn.kt @@ -1,4 +1,4 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings class ServerLocalisationEn: ServerLocalisation { override fun serverBoundToPort(server: String, port: Int): String = @@ -13,7 +13,4 @@ class ServerLocalisationEn: ServerLocalisation { override val option_help_headless: String = "Run without mpv" override val option_no_media_session: String = "Don't broadcast media sessiont to system" override val option_help_icon: String = "Path to icon" - - override val indicator_button_open_client: String = "Open client" - override val indicator_button_stop_server: String = "Stop" } diff --git a/src/commonMain/kotlin/spms/localisation/strings/ServerLocalisationJa.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerLocalisationJa.kt similarity index 82% rename from src/commonMain/kotlin/spms/localisation/strings/ServerLocalisationJa.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerLocalisationJa.kt index 5e3d942..debe7a0 100644 --- a/src/commonMain/kotlin/spms/localisation/strings/ServerLocalisationJa.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/localisation/strings/ServerLocalisationJa.kt @@ -1,4 +1,4 @@ -package spms.localisation.strings +package dev.toastbits.spms.localisation.strings class ServerLocalisationJa: ServerLocalisation { override fun serverBoundToPort(server: String, port: Int): String = @@ -13,7 +13,4 @@ class ServerLocalisationJa: ServerLocalisation { override val option_help_headless: String = "mpvプレイヤーなしで実行" override val option_no_media_session: String = "システムにメディアセッションを発信しない" override val option_help_icon: String = "アイコンへのパス" - - override val indicator_button_open_client: String = "クライアントを開く" - override val indicator_button_stop_server: String = "終了" } diff --git a/library/src/commonMain/kotlin/dev/toastbits/spms/mpv/LibMpvClient.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/mpv/LibMpvClient.kt new file mode 100644 index 0000000..ef36da7 --- /dev/null +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/mpv/LibMpvClient.kt @@ -0,0 +1,140 @@ +package dev.toastbits.spms.mpv + +import dev.toastbits.spms.player.Player +import toInt +import kotlin.reflect.KClass +import gen.libmpv.LibMpv +import kjna.struct.mpv_handle +import kjna.struct.mpv_event +import kjna.enum.mpv_format +import dev.toastbits.kjna.runtime.KJnaTypedPointer +import dev.toastbits.kjna.runtime.KJnaMemScope +import dev.toastbits.kjna.runtime.KJnaUtils + +abstract class LibMpvClient( + val libmpv: LibMpv, + headless: Boolean, + playlist_auto_progress: Boolean +): Player { + val is_headless: Boolean = headless + + protected val ctx: KJnaTypedPointer + + init { + try { + KJnaUtils.setLocale( + 1, // LC_NUMERIC + "C" + ) + } + catch (e: Throwable) { + RuntimeException("WARNING: Unable to set LC_NUMERIC locale", e).printStackTrace() + } + + ctx = libmpv.mpv_create() + ?: throw NullPointerException("Creating mpv client failed") + + KJnaMemScope.confined { + val vid: KJnaTypedPointer = alloc() + vid.set(!is_headless) + libmpv.mpv_set_option(ctx, "vid", mpv_format.MPV_FORMAT_FLAG, vid) + + if (!is_headless) { + libmpv.mpv_set_option_string(ctx, "force-window", "immediate") + + val osd_level: KJnaTypedPointer = alloc() + osd_level.set(3) + libmpv.mpv_set_option(ctx, "osd-level", mpv_format.MPV_FORMAT_INT64, osd_level) + } + + if (!playlist_auto_progress) { + libmpv.mpv_set_option_string(ctx, "keep-open", "always") + } + } + + val init_result: Int = libmpv.mpv_initialize(ctx) + check(init_result == 0) { "Initialising mpv client failed ($init_result)" } + } + + override fun release() { + libmpv.mpv_terminate_destroy(ctx) + } + + private fun buildArgs(args: List, terminate: Boolean = true): Array = + Array(args.size + terminate.toInt()) { i -> + args.getOrNull(i)?.toString() + } + + fun runCommand(name: String, vararg args: Any?, check_result: Boolean = true): Int { + val result: Int = KJnaMemScope.confined { + libmpv.mpv_command(ctx, allocStringArray(buildArgs(listOf(name).plus(args)))) + } + + if (check_result) { + check(result == 0) { + "Command $name(${args.toList()}) failed ($result)" + } + } + + return result + } + + internal inline fun getProperty(name: String): T = KJnaMemScope.confined { + if (T::class == String::class) { + return libmpv.mpv_get_property_string(ctx, name)!! as T + } + + val pointer: KJnaTypedPointer = alloc() + val format: mpv_format = getMpvFormatOf(T::class) + libmpv.mpv_get_property(ctx, name, format, pointer) + return@confined pointer.get() + } + + internal inline fun setProperty(name: String, value: T): Int = KJnaMemScope.confined { + if (T::class == String::class) { + return@confined libmpv.mpv_set_property_string(ctx, name, value as String) + } + + val pointer: KJnaTypedPointer = alloc() + val format: mpv_format = getMpvFormatOf(T::class) + pointer.set(value) + libmpv.mpv_set_property(ctx, name, format, pointer) + } + + internal fun observeProperty(name: String, cls: KClass<*>) { + val format: mpv_format = getMpvFormatOf(cls) + + try { + val res: Int = libmpv.mpv_observe_property(ctx, 0UL, name, format) + check(res == 0) { res } + } + catch (e: Throwable) { + throw RuntimeException("Call to observeProperty for $name with format $format failed", e) + } + } + + internal fun waitForEvent(): mpv_event = libmpv.mpv_wait_event(ctx, -1.0)!!.get() + + internal fun requestLogMessages() { + val result: Int = libmpv.mpv_request_log_messages(ctx, "info") + check(result == 0) { "Call to requestLogMessages failed ($result)" } + } + + fun addHook(name: String, priority: Int = 0) { + val result: Int = libmpv.mpv_hook_add(ctx, 0UL, name, priority) + check(result == 0) { "Call to hookAdd with name=$name and priority=$priority failed ($result)" } + } + + fun continueHook(id: ULong) { + libmpv.mpv_hook_continue(ctx, id) + } +} + +fun getMpvFormatOf(cls: KClass<*>): mpv_format = + when (cls) { + Boolean::class -> mpv_format.MPV_FORMAT_FLAG + Int::class -> mpv_format.MPV_FORMAT_INT64 + Double::class -> mpv_format.MPV_FORMAT_DOUBLE + String::class -> mpv_format.MPV_FORMAT_STRING + else -> throw NotImplementedError(cls.toString()) + } diff --git a/src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.enabled b/library/src/commonMain/kotlin/dev/toastbits/spms/mpv/MpvClientEventLoop.kt similarity index 63% rename from src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.enabled rename to library/src/commonMain/kotlin/dev/toastbits/spms/mpv/MpvClientEventLoop.kt index c0fe3fe..76dd7c7 100644 --- a/src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.enabled +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/mpv/MpvClientEventLoop.kt @@ -1,12 +1,15 @@ -package cinterop.mpv +package dev.toastbits.spms.mpv -import libmpv.* import kotlinx.coroutines.* import kotlinx.serialization.json.JsonPrimitive -import spms.server.SpMs -import spms.socketapi.shared.SpMsPlayerEvent -import cinterop.utils.safeToKString -import kotlinx.cinterop.* +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsPlayerEvent +import kjna.enum.mpv_event_id +import kjna.struct.mpv_event +import kjna.struct.mpv_event_start_file +import kjna.struct.mpv_event_property +import kjna.struct.mpv_event_hook +import kjna.struct.mpv_event_log_message internal suspend fun MpvClientImpl.eventLoop() = withContext(Dispatchers.IO) { observeProperty("core-idle", Boolean::class) @@ -15,26 +18,25 @@ internal suspend fun MpvClientImpl.eventLoop() = withContext(Dispatchers.IO) { var waiting_for_seek_end: Boolean = false while (true) { - val event: mpv_event? = waitForEvent() - - when (event?.event_id) { - MPV_EVENT_START_FILE -> { - val data: mpv_event_start_file = event.data.pointedAs() + val event: mpv_event = waitForEvent() + when (event.event_id) { + mpv_event_id.MPV_EVENT_START_FILE -> { + val data: mpv_event_start_file = event.data!!.cast() if (data.playlist_entry_id.toInt() != current_item_playlist_id) { continue } onEvent(SpMsPlayerEvent.ItemTransition(current_item_index), clientless = true) } - MPV_EVENT_PLAYBACK_RESTART -> { + mpv_event_id.MPV_EVENT_PLAYBACK_RESTART -> { onEvent(SpMsPlayerEvent.PropertyChanged("state", JsonPrimitive(state.ordinal)), clientless = true) onEvent(SpMsPlayerEvent.PropertyChanged("duration_ms", JsonPrimitive(duration_ms)), clientless = true) onEvent(SpMsPlayerEvent.SeekedToTime(current_position_ms), clientless = true) } - MPV_EVENT_END_FILE -> { + mpv_event_id.MPV_EVENT_END_FILE -> { onEvent(SpMsPlayerEvent.PropertyChanged("state", JsonPrimitive(state.ordinal)), clientless = true) } - MPV_EVENT_FILE_LOADED -> { + mpv_event_id.MPV_EVENT_FILE_LOADED -> { onEvent(SpMsPlayerEvent.ReadyToPlay(), clientless = true) song_initial_seek_time?.also { time -> @@ -42,15 +44,15 @@ internal suspend fun MpvClientImpl.eventLoop() = withContext(Dispatchers.IO) { song_initial_seek_time = null } } - MPV_EVENT_SHUTDOWN -> { + mpv_event_id.MPV_EVENT_SHUTDOWN -> { onShutdown() } - MPV_EVENT_PROPERTY_CHANGE -> { - val data: mpv_event_property = event.data.pointedAs() + mpv_event_id.MPV_EVENT_PROPERTY_CHANGE -> { + val data: mpv_event_property = event.data!!.cast() - when (data.name?.safeToKString()) { + when (data.name) { "core-idle" -> { - val playing: Boolean = !data.data.pointedAs().value + val playing: Boolean = !data.data!!.cast() onEvent(SpMsPlayerEvent.PropertyChanged("is_playing", JsonPrimitive(playing)), clientless = true) } "seeking" -> { @@ -62,7 +64,7 @@ internal suspend fun MpvClientImpl.eventLoop() = withContext(Dispatchers.IO) { } } - MPV_EVENT_SEEK -> { + mpv_event_id.MPV_EVENT_SEEK -> { if (getProperty("seeking")) { waiting_for_seek_end = true } @@ -71,20 +73,21 @@ internal suspend fun MpvClientImpl.eventLoop() = withContext(Dispatchers.IO) { } } - MPV_EVENT_HOOK -> { - val data: mpv_event_hook = event.data.pointedAs() + mpv_event_id.MPV_EVENT_HOOK -> { + val data: mpv_event_hook = event.data!!.cast() launch { - onMpvHook(data.name?.safeToKString(), data.id) + onMpvHook(data.name, data.id) } } - MPV_EVENT_LOG_MESSAGE -> { + mpv_event_id.MPV_EVENT_LOG_MESSAGE -> { if (SpMs.logging_enabled) { - val message: mpv_event_log_message = event.data.pointedAs() - SpMs.log("From mpv (${message.prefix?.safeToKString()}): ${message.text?.safeToKString()}") + val message: mpv_event_log_message = event.data!!.cast() + SpMs.log("From mpv (${message.prefix}): ${message.text}") } } + + else -> {} } } } - diff --git a/src/commonMain/kotlin/cinterop/mpv/MpvClientImpl.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/mpv/MpvClientImpl.kt similarity index 89% rename from src/commonMain/kotlin/cinterop/mpv/MpvClientImpl.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/mpv/MpvClientImpl.kt index 9dc8181..de011a5 100644 --- a/src/commonMain/kotlin/cinterop/mpv/MpvClientImpl.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/mpv/MpvClientImpl.kt @@ -1,31 +1,26 @@ -package cinterop.mpv +package dev.toastbits.spms.mpv -import kotlinx.cinterop.* import kotlinx.coroutines.* import kotlinx.serialization.json.JsonPrimitive import okio.FileSystem import okio.Path.Companion.toPath -import spms.socketapi.shared.SpMsPlayerEvent -import spms.player.VideoInfoProvider -import spms.server.SpMs -import spms.socketapi.shared.SpMsPlayerRepeatMode -import spms.socketapi.shared.SpMsPlayerState +import dev.toastbits.spms.socketapi.shared.SpMsPlayerEvent +import dev.toastbits.spms.player.VideoInfoProvider +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsPlayerRepeatMode +import dev.toastbits.spms.socketapi.shared.SpMsPlayerState +import dev.toastbits.spms.PLATFORM +import gen.libmpv.LibMpv import kotlin.math.roundToInt import kotlin.time.* -import cinterop.utils.safeToKString private const val URL_PREFIX: String = "spmp://" -@OptIn(ExperimentalForeignApi::class) -inline fun CPointer<*>?.pointedAs(): T = - this!!.reinterpret().pointed - -@OptIn(ExperimentalForeignApi::class) -abstract class MpvClientImpl(headless: Boolean = true, playlist_auto_progress: Boolean = true): LibMpvClient(headless = headless, playlist_auto_progress = playlist_auto_progress) { - companion object { - fun isAvailable(): Boolean = LibMpvClient.isAvailable() - } - +abstract class MpvClientImpl( + libmpv: LibMpv, + headless: Boolean = true, + playlist_auto_progress: Boolean = true +): LibMpvClient(libmpv, headless = headless, playlist_auto_progress = playlist_auto_progress) { private val coroutine_scope = CoroutineScope(Job()) private var auth_headers: Map? = null private val local_files: MutableMap = mutableMapOf() @@ -81,11 +76,13 @@ abstract class MpvClientImpl(headless: Boolean = true, playlist_auto_progress: B return } - setProperty("pause", false) + val result: Int = setProperty("pause", false) + check(result == 0) { "Call to setProperty(\"pause\", false) failed ($result)" } onEvent(SpMsPlayerEvent.PropertyChanged("is_playing", JsonPrimitive(is_playing))) } override fun pause() { - setProperty("pause", true) + val result: Int = setProperty("pause", true) + check(result == 0) { "Call to setProperty(\"pause\", true) failed ($result)" } onEvent(SpMsPlayerEvent.PropertyChanged("is_playing", JsonPrimitive(is_playing))) } override fun playPause() { @@ -254,10 +251,10 @@ abstract class MpvClientImpl(headless: Boolean = true, playlist_auto_progress: B onEvent(SpMsPlayerEvent.CancelRadio()) } - override fun toString(): String = "MpvClientImpl(headless=$headless)" + override fun toString(): String = "MpvClientImpl(is_headless=$is_headless)" private fun initialise() { - // requestLogMessages() + requestLogMessages() addHook("on_load") @@ -274,7 +271,7 @@ abstract class MpvClientImpl(headless: Boolean = true, playlist_auto_progress: B val stream_url: String val local_file_path: String? = local_files[video_id] - if (local_file_path != null && FileSystem.SYSTEM.exists(local_file_path.toPath())) { + if (local_file_path != null && FileSystem.PLATFORM.exists(local_file_path.toPath())) { stream_url = "file://" + local_file_path } else { diff --git a/src/commonMain/kotlin/cinterop/mpv/MpvCommandInterface.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/mpv/MpvCommandInterface.kt similarity index 68% rename from src/commonMain/kotlin/cinterop/mpv/MpvCommandInterface.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/mpv/MpvCommandInterface.kt index 9548c01..49d5123 100644 --- a/src/commonMain/kotlin/cinterop/mpv/MpvCommandInterface.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/mpv/MpvCommandInterface.kt @@ -1,7 +1,7 @@ -package cinterop.mpv +package dev.toastbits.spms.mpv -import spms.player.Player -import spms.socketapi.shared.SpMsServerState +import dev.toastbits.spms.player.Player +import dev.toastbits.spms.socketapi.shared.SpMsServerState fun Player.getCurrentStateJson(): SpMsServerState = SpMsServerState( diff --git a/src/commonMain/kotlin/spms/player/Player.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/player/Player.kt similarity index 82% rename from src/commonMain/kotlin/spms/player/Player.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/player/Player.kt index 72ab23b..68fd6fc 100644 --- a/src/commonMain/kotlin/spms/player/Player.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/player/Player.kt @@ -1,8 +1,8 @@ -package spms.player +package dev.toastbits.spms.player -import spms.socketapi.shared.SpMsPlayerEvent -import spms.socketapi.shared.SpMsPlayerRepeatMode -import spms.socketapi.shared.SpMsPlayerState +import dev.toastbits.spms.socketapi.shared.SpMsPlayerEvent +import dev.toastbits.spms.socketapi.shared.SpMsPlayerRepeatMode +import dev.toastbits.spms.socketapi.shared.SpMsPlayerState data class PlayerStreamInfo( val url: String, @@ -45,7 +45,3 @@ interface Player { fun setVolume(value: Double) } - -fun Boolean.toInt(): Int = - if (this) 1 - else 0 diff --git a/src/commonMain/kotlin/spms/player/VideoInfoProvider.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/player/VideoInfoProvider.kt similarity index 92% rename from src/commonMain/kotlin/spms/player/VideoInfoProvider.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/player/VideoInfoProvider.kt index 966b2e3..5eec1fc 100644 --- a/src/commonMain/kotlin/spms/player/VideoInfoProvider.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/player/VideoInfoProvider.kt @@ -1,12 +1,10 @@ -package spms.player +package dev.toastbits.spms.player -import kotlinx.cinterop.ExperimentalForeignApi import dev.toastbits.ytmkt.impl.youtubei.YoutubeiApi import dev.toastbits.ytmkt.model.external.YoutubeVideoFormat import dev.toastbits.ytmkt.formats.PipedVideoFormatsEndpoint import dev.toastbits.ytmkt.formats.VideoFormatsEndpoint -@OptIn(ExperimentalForeignApi::class) object VideoInfoProvider { private val api: YoutubeiApi = YoutubeiApi() private val piped_formats_endpoint: PipedVideoFormatsEndpoint = PipedVideoFormatsEndpoint(api) diff --git a/src/commonMain/kotlin/spms/player/headless/HeadlessPlayer.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/player/headless/HeadlessPlayer.kt similarity index 97% rename from src/commonMain/kotlin/spms/player/headless/HeadlessPlayer.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/player/headless/HeadlessPlayer.kt index af58d73..78adaf9 100644 --- a/src/commonMain/kotlin/spms/player/headless/HeadlessPlayer.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/player/headless/HeadlessPlayer.kt @@ -1,18 +1,16 @@ -package spms.player.headless +package dev.toastbits.spms.player.headless -import kotlinx.atomicfu.locks.ReentrantLock -import kotlinx.atomicfu.locks.withLock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.serialization.json.JsonPrimitive -import spms.socketapi.shared.SpMsPlayerEvent -import spms.socketapi.shared.SpMsPlayerRepeatMode -import spms.socketapi.shared.SpMsPlayerState -import spms.player.* -import kotlin.system.getTimeNanos +import dev.toastbits.spms.socketapi.shared.SpMsPlayerEvent +import dev.toastbits.spms.socketapi.shared.SpMsPlayerRepeatMode +import dev.toastbits.spms.socketapi.shared.SpMsPlayerState +import dev.toastbits.spms.player.* +import dev.toastbits.spms.ReentrantLock import kotlin.time.* abstract class HeadlessPlayer(private val enable_logging: Boolean = true): Player { diff --git a/src/commonMain/kotlin/spms/player/headless/PlaybackState.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/player/headless/PlaybackState.kt similarity index 96% rename from src/commonMain/kotlin/spms/player/headless/PlaybackState.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/player/headless/PlaybackState.kt index 9b89f7e..0d16a36 100644 --- a/src/commonMain/kotlin/spms/player/headless/PlaybackState.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/player/headless/PlaybackState.kt @@ -1,4 +1,4 @@ -package spms.player.headless +package dev.toastbits.spms.player.headless import kotlin.time.* diff --git a/src/commonMain/kotlin/spms/server/SpMs.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/server/SpMs.kt similarity index 78% rename from src/commonMain/kotlin/spms/server/SpMs.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/server/SpMs.kt index b9e62ae..b1dbdc7 100644 --- a/src/commonMain/kotlin/spms/server/SpMs.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/server/SpMs.kt @@ -1,9 +1,9 @@ -package spms.server +package dev.toastbits.spms.server -import cinterop.mpv.MpvClientImpl -import cinterop.mpv.getCurrentStateJson -import cinterop.zmq.ZmqRouter -import kotlinx.cinterop.* +import dev.toastbits.spms.mpv.MpvClientImpl +import dev.toastbits.spms.mpv.getCurrentStateJson +import dev.toastbits.spms.zmq.ZmqRouter +import dev.toastbits.spms.zmq.ZmqMessage import kotlinx.coroutines.channels.Channel import kotlinx.serialization.SerializationException import kotlinx.serialization.encodeToString @@ -11,29 +11,27 @@ import kotlinx.serialization.json.Json import okio.FileSystem import okio.Path import okio.Path.Companion.toPath -import platform.posix.getenv -import spms.getHostname -import spms.player.Player -import spms.player.headless.HeadlessPlayer -import spms.socketapi.parseSocketMessage -import spms.socketapi.player.PlayerAction -import spms.socketapi.server.ServerAction -import spms.socketapi.shared.* -import spms.localisation.SpMsLocalisation +import dev.toastbits.spms.getHostname +import dev.toastbits.spms.player.Player +import dev.toastbits.spms.player.headless.HeadlessPlayer +import dev.toastbits.spms.socketapi.parseSocketMessage +import dev.toastbits.spms.socketapi.player.PlayerAction +import dev.toastbits.spms.socketapi.server.ServerAction +import dev.toastbits.spms.socketapi.shared.* +import dev.toastbits.spms.localisation.SpMsLocalisation +import dev.toastbits.spms.getMachineId +import dev.toastbits.spms.getDeviceName +import dev.toastbits.spms.createLibMpv import kotlin.experimental.ExperimentalNativeApi -import kotlin.system.exitProcess -import kotlin.system.getTimeMillis import kotlin.time.* +import gen.libmpv.LibMpv private val CLIENT_REPLY_TIMEOUT: Duration = with (Duration) { 100.milliseconds } -@OptIn(ExperimentalForeignApi::class) -class SpMs( - mem_scope: MemScope, - val headless: Boolean = false, - enable_gui: Boolean = false, - enable_media_session: Boolean = false -): ZmqRouter(mem_scope) { +open class SpMs( + val headless: Boolean = !LibMpv.isAvailable(), + enable_gui: Boolean = false +): ZmqRouter() { private var item_durations: MutableMap = mutableMapOf() private val item_durations_channel: Channel = Channel() @@ -44,7 +42,7 @@ class SpMs( private var playback_waiting_for_clients: Boolean = false val player: Player = - if (headless || !MpvClientImpl.isAvailable()) + if (headless) object : HeadlessPlayer() { override fun getCachedItemDuration(item_id: String): Duration? = item_durations[item_id] @@ -62,7 +60,7 @@ class SpMs( override fun onShutdown() = onPlayerShutdown() } else - object : MpvClientImpl(headless = !enable_gui) { + object : MpvClientImpl(createLibMpv(), headless = !enable_gui) { override fun canPlay(): Boolean = this@SpMs.canPlay() override fun onEvent(event: SpMsPlayerEvent, clientless: Boolean) = onPlayerEvent(event, clientless) override fun onShutdown() = onPlayerShutdown() @@ -77,16 +75,6 @@ class SpMs( return true } - private val media_session: SpMsMediaSession? = - try { - if (enable_media_session) SpMsMediaSession.create(player) - else null - } - catch (e: Throwable) { - RuntimeException("Ignoring exception that occurred when creating media session", e).printStackTrace() - null - } - fun getClients(caller: SpMsClientID? = null): List = listOf( SpMsClientInfo( @@ -102,7 +90,7 @@ class SpMs( // Process stray messages (hopefully client handshakes) while (true) { - val message: Message = recvMultipart(null) ?: break + val message: ZmqMessage = recvMultipart(null) ?: break println("Got stray message before polling") onClientMessage(message) } @@ -139,7 +127,7 @@ class SpMs( // Wait for client to reply val wait_start: TimeMark = TimeSource.Monotonic.markNow() - var client_reply: Message? = null + var client_reply: ZmqMessage? = null while (true) { val remaining: Duration = CLIENT_REPLY_TIMEOUT - wait_start.elapsedNow() @@ -147,7 +135,7 @@ class SpMs( break } - val message: Message = recvMultipart(remaining) ?: continue + val message: ZmqMessage = recvMultipart(remaining) ?: continue if (message.client_id.contentHashCode() == client.id) { client_reply = message @@ -209,7 +197,7 @@ class SpMs( } } - private fun processClientMessage(message: Message, client: SpMsClient) { + private fun processClientMessage(message: ZmqMessage, client: SpMsClient) { val reply: List = processClientActions(message.parts, client) if (reply.isNotEmpty()) { @@ -266,7 +254,7 @@ class SpMs( } } - private fun onPlayerEvent(event: SpMsPlayerEvent, clientless: Boolean) { + protected open fun onPlayerEvent(event: SpMsPlayerEvent, clientless: Boolean) { if (event.type == SpMsPlayerEvent.Type.READY_TO_PLAY) { if (!playback_waiting_for_clients) { player.play() @@ -277,8 +265,6 @@ class SpMs( val event_client: SpMsClient? = clients.firstOrNull { it.id == event.client_id } println("Event ($clientless, $event_client): $event") - media_session?.onPlayerEvent(event) - if (clients.isEmpty()) { return } @@ -316,9 +302,7 @@ class SpMs( player_events.add(event) } - private fun onPlayerShutdown() { - exitProcess(0) - } + protected open fun onPlayerShutdown() {} private fun getNewClientName(requested_name: String): String { var num: Int = 1 @@ -331,7 +315,7 @@ class SpMs( return numbered_name } - private fun onClientMessage(message: Message) { + private fun onClientMessage(message: ZmqMessage) { val id: Int = message.client_id.contentHashCode() val content: String? = message.parts.firstOrNull() @@ -380,12 +364,12 @@ class SpMs( device_name = getDeviceName(), spms_api_version = SPMS_API_VERSION, server_state = player.getCurrentStateJson(), - machine_id = SpMs.getMachineId(), + machine_id = getMachineId(), action_replies = action_replies ) sendMultipart( - Message( + ZmqMessage( client.id_bytes, listOf(Json.encodeToString(server_handshake)) ) @@ -425,8 +409,8 @@ class SpMs( event.event_id >= client.event_head && (event.shouldSendToInstigatingClient() || event.client_id != client.id) } - private fun SpMsClient.createMessage(parts: List): ZmqRouter.Message = - ZmqRouter.Message(id_bytes, parts) + private fun SpMsClient.createMessage(parts: List): ZmqMessage = + ZmqMessage(id_bytes, parts) override fun toString(): String = "SpMs(player=$player)" @@ -434,6 +418,14 @@ class SpMs( companion object { const val application_name: String = "spmp-server" + fun isAvailable(headless: Boolean): Boolean { + if (headless) { + return true + } + + return LibMpv.isAvailable() + } + var logging_enabled: Boolean = true fun log(msg: Any?) { if (logging_enabled) { @@ -449,68 +441,5 @@ class SpMs( println(localisation.versionInfoText(SPMS_API_VERSION)) version_printed = true } - - fun getMachineId(): String { - val id_path: Path = - when (Platform.osFamily) { - OsFamily.LINUX -> "/tmp/".toPath() - OsFamily.WINDOWS -> "${getenv("USERPROFILE")!!.toKString()}/AppData/Local/Temp/".toPath() - else -> throw NotImplementedError(Platform.osFamily.name) - }.resolve("spmp_machine_id.txt") - - if (FileSystem.SYSTEM.exists(id_path)) { - return FileSystem.SYSTEM.read(id_path) { - readUtf8() - } - } - - val parent: Path = id_path.parent!! - if (!FileSystem.SYSTEM.exists(parent)) { - FileSystem.SYSTEM.createDirectories(parent, true) - } - - val id_length: Int = 8 - val allowed_chars: List = ('A'..'Z') + ('a'..'z') + ('0'..'9') - - val new_id: String = (1..id_length).map { allowed_chars.random() }.joinToString("") - - FileSystem.SYSTEM.write(id_path) { - writeUtf8(new_id) - } - - return new_id - } - } -} - -@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) -fun getDeviceName(): String { - val hostname: String = memScoped { - val str: CPointer> = allocArray(1024) - getHostname(str, 1023) - return@memScoped str.toKString() } - - val os: String = - when (Platform.osFamily) { - OsFamily.MACOSX -> "OSX" - OsFamily.IOS -> "iOS" - OsFamily.LINUX -> "Linux" - OsFamily.WINDOWS -> "Windows" - OsFamily.ANDROID -> "Android" - OsFamily.WASM -> "Wasm" - OsFamily.TVOS -> "TV" - OsFamily.WATCHOS -> "WatchOS" - OsFamily.UNKNOWN -> "Unknown" - } - - val architecture: String = - when (Platform.cpuArchitecture) { - CpuArchitecture.X86 -> "x86" - CpuArchitecture.X64 -> "x86-64" - CpuArchitecture.UNKNOWN -> "Unknown" - else -> Platform.cpuArchitecture.name - } - - return "$hostname ($os $architecture)" } diff --git a/src/commonMain/kotlin/spms/server/SpMsClient.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/server/SpMsClient.kt similarity index 66% rename from src/commonMain/kotlin/spms/server/SpMsClient.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/server/SpMsClient.kt index 310a891..239f056 100644 --- a/src/commonMain/kotlin/spms/server/SpMsClient.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/server/SpMsClient.kt @@ -1,9 +1,9 @@ -package spms.server +package dev.toastbits.spms.server -import spms.socketapi.shared.SpMsClientID -import spms.socketapi.shared.SpMsClientInfo -import spms.socketapi.shared.SpMsClientType -import spms.socketapi.shared.SpMsLanguage +import dev.toastbits.spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.socketapi.shared.SpMsClientInfo +import dev.toastbits.spms.socketapi.shared.SpMsClientType +import dev.toastbits.spms.socketapi.shared.SpMsLanguage internal class SpMsClient( val id_bytes: ByteArray, diff --git a/src/commonMain/kotlin/spms/socketapi/Action.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/Action.kt similarity index 80% rename from src/commonMain/kotlin/spms/socketapi/Action.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/Action.kt index 3e0a12c..b080ddc 100644 --- a/src/commonMain/kotlin/spms/socketapi/Action.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/Action.kt @@ -1,9 +1,9 @@ -package spms.socketapi +package dev.toastbits.spms.socketapi -import com.github.ajalt.clikt.core.Context import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonPrimitive -import spms.LocalisedMessageProvider +import dev.toastbits.spms.localisation.LocalisedMessageProvider +import dev.toastbits.spms.localisation.SpMsLocalisation abstract class Action { abstract val type: Type @@ -12,7 +12,7 @@ abstract class Action { abstract val help: LocalisedMessageProvider abstract val parameters: List abstract val hidden: Boolean - + enum class Type { PLAYER, SERVER } @@ -43,5 +43,5 @@ abstract class Action { } } - open fun formatResult(result: JsonElement, context: Context) = result.toString() + open fun formatResult(result: JsonElement, localisation: SpMsLocalisation): String = result.toString() } diff --git a/src/commonMain/kotlin/spms/socketapi/ParseMessage.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/ParseMessage.kt similarity index 92% rename from src/commonMain/kotlin/spms/socketapi/ParseMessage.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/ParseMessage.kt index 3f6d90e..fdfa367 100644 --- a/src/commonMain/kotlin/spms/socketapi/ParseMessage.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/ParseMessage.kt @@ -1,9 +1,9 @@ -package spms.socketapi +package dev.toastbits.spms.socketapi import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement -import spms.socketapi.shared.SPMS_EXPECT_REPLY_CHAR -import spms.socketapi.shared.SpMsActionReply +import dev.toastbits.spms.socketapi.shared.SPMS_EXPECT_REPLY_CHAR +import dev.toastbits.spms.socketapi.shared.SpMsActionReply fun parseSocketMessage( parts: List, diff --git a/src/commonMain/kotlin/spms/socketapi/player/PlayerAction.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerAction.kt similarity index 88% rename from src/commonMain/kotlin/spms/socketapi/player/PlayerAction.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerAction.kt index e9835ea..017422e 100644 --- a/src/commonMain/kotlin/spms/socketapi/player/PlayerAction.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerAction.kt @@ -1,9 +1,9 @@ -package spms.socketapi.player +package dev.toastbits.spms.socketapi.player -import cinterop.mpv.MpvClientImpl +import dev.toastbits.spms.mpv.MpvClientImpl import kotlinx.serialization.json.JsonElement -import spms.LocalisedMessageProvider -import spms.socketapi.Action +import dev.toastbits.spms.localisation.LocalisedMessageProvider +import dev.toastbits.spms.socketapi.Action sealed class PlayerAction( override val identifier: String, @@ -18,7 +18,7 @@ sealed class PlayerAction( fun execute(player: MpvClientImpl, parameter_values: List): JsonElement? = execute(player, ActionContext(parameter_values)) - + companion object { private val ALL: List = listOf( PlayerActionSetAuthInfo(), diff --git a/src/commonMain/kotlin/spms/socketapi/player/PlayerActionAddLocalFiles.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionAddLocalFiles.kt similarity index 88% rename from src/commonMain/kotlin/spms/socketapi/player/PlayerActionAddLocalFiles.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionAddLocalFiles.kt index 9fa20a4..e145327 100644 --- a/src/commonMain/kotlin/spms/socketapi/player/PlayerActionAddLocalFiles.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionAddLocalFiles.kt @@ -1,6 +1,6 @@ -package spms.socketapi.player +package dev.toastbits.spms.socketapi.player -import cinterop.mpv.MpvClientImpl +import dev.toastbits.spms.mpv.MpvClientImpl import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonObject diff --git a/src/commonMain/kotlin/spms/socketapi/player/PlayerActionCancelRadio.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionCancelRadio.kt similarity index 83% rename from src/commonMain/kotlin/spms/socketapi/player/PlayerActionCancelRadio.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionCancelRadio.kt index ae4819f..40d4a16 100644 --- a/src/commonMain/kotlin/spms/socketapi/player/PlayerActionCancelRadio.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionCancelRadio.kt @@ -1,6 +1,6 @@ -package spms.socketapi.player +package dev.toastbits.spms.socketapi.player -import cinterop.mpv.MpvClientImpl +import dev.toastbits.spms.mpv.MpvClientImpl import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonObject diff --git a/src/commonMain/kotlin/spms/socketapi/player/PlayerActionRemoveLocalFiles.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionRemoveLocalFiles.kt similarity index 88% rename from src/commonMain/kotlin/spms/socketapi/player/PlayerActionRemoveLocalFiles.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionRemoveLocalFiles.kt index 19526ff..5433352 100644 --- a/src/commonMain/kotlin/spms/socketapi/player/PlayerActionRemoveLocalFiles.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionRemoveLocalFiles.kt @@ -1,6 +1,6 @@ -package spms.socketapi.player +package dev.toastbits.spms.socketapi.player -import cinterop.mpv.MpvClientImpl +import dev.toastbits.spms.mpv.MpvClientImpl import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonArray diff --git a/src/commonMain/kotlin/spms/socketapi/player/PlayerActionSetAuthInfo.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionSetAuthInfo.kt similarity index 88% rename from src/commonMain/kotlin/spms/socketapi/player/PlayerActionSetAuthInfo.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionSetAuthInfo.kt index 697da98..bc4704b 100644 --- a/src/commonMain/kotlin/spms/socketapi/player/PlayerActionSetAuthInfo.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionSetAuthInfo.kt @@ -1,6 +1,6 @@ -package spms.socketapi.player +package dev.toastbits.spms.socketapi.player -import cinterop.mpv.MpvClientImpl +import dev.toastbits.spms.mpv.MpvClientImpl import kotlinx.serialization.json.* class PlayerActionSetAuthInfo: PlayerAction( diff --git a/src/commonMain/kotlin/spms/socketapi/player/PlayerActionSetLocalFiles.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionSetLocalFiles.kt similarity index 88% rename from src/commonMain/kotlin/spms/socketapi/player/PlayerActionSetLocalFiles.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionSetLocalFiles.kt index 13dcaab..d9b7aaa 100644 --- a/src/commonMain/kotlin/spms/socketapi/player/PlayerActionSetLocalFiles.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/player/PlayerActionSetLocalFiles.kt @@ -1,6 +1,6 @@ -package spms.socketapi.player +package dev.toastbits.spms.socketapi.player -import cinterop.mpv.MpvClientImpl +import dev.toastbits.spms.mpv.MpvClientImpl import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonObject diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerAction.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerAction.kt similarity index 88% rename from src/commonMain/kotlin/spms/socketapi/server/ServerAction.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerAction.kt index 7c320c3..eb2b0ac 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerAction.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerAction.kt @@ -1,10 +1,10 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement -import spms.LocalisedMessageProvider -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID -import spms.socketapi.Action +import dev.toastbits.spms.localisation.LocalisedMessageProvider +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.socketapi.Action sealed class ServerAction( override val identifier: String, diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionAddItem.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionAddItem.kt similarity index 87% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionAddItem.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionAddItem.kt index 106b9c5..4e5896d 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionAddItem.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionAddItem.kt @@ -1,10 +1,10 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionAddItem: ServerAction( identifier = "addItem", diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionClearQueue.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionClearQueue.kt similarity index 75% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionClearQueue.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionClearQueue.kt index ffe36b6..1011d11 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionClearQueue.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionClearQueue.kt @@ -1,8 +1,8 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionClearQueue: ServerAction( identifier = "clearQueue", diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionGetClients.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionGetClients.kt similarity index 79% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionGetClients.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionGetClients.kt index 918c5c0..c776c0d 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionGetClients.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionGetClients.kt @@ -1,10 +1,10 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.encodeToJsonElement -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionGetClients: ServerAction( identifier = "clients", diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionGetProperty.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionGetProperty.kt similarity index 73% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionGetProperty.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionGetProperty.kt index 5810d40..67ba491 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionGetProperty.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionGetProperty.kt @@ -1,8 +1,8 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionGetProperty: ServerAction( identifier = "getProperty", diff --git a/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionGetStatus.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionGetStatus.kt new file mode 100644 index 0000000..22f4bf5 --- /dev/null +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionGetStatus.kt @@ -0,0 +1,84 @@ +package dev.toastbits.spms.socketapi.server + +import dev.toastbits.spms.mpv.getCurrentStateJson +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.* +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.socketapi.shared.SpMsPlayerRepeatMode +import dev.toastbits.spms.socketapi.shared.SpMsPlayerState +import dev.toastbits.spms.getCacheDir +import dev.toastbits.spms.localisation.SpMsLocalisation +import dev.toastbits.spms.PLATFORM +import kotlin.time.Duration + +class ServerActionGetStatus: ServerAction( + identifier = "status", + name = { server_actions.status_name }, + help = { server_actions.status_help }, + parameters = emptyList() +) { + override fun execute(server: SpMs, client: SpMsClientID, context: ActionContext): JsonElement { + return Json.encodeToJsonElement(server.player.getCurrentStateJson()) + } + + override fun formatResult(result: JsonElement, localisation: SpMsLocalisation): String { + val string: StringBuilder = StringBuilder("--- ${localisation.server_actions.status_output_start} ---\n") + runBlocking { + val entries: Map = + result.jsonObject.entries.associate { entry -> + val key: String = + with (localisation.server_actions) { + when (entry.key) { + "queue" -> status_key_queue + "state" -> status_key_state + "is_playing" -> status_key_is_playing + "current_item_index" -> status_key_current_item_index + "current_position_ms" -> status_key_current_position + "duration_ms" -> status_key_duration + "repeat_mode" -> status_key_repeat_mode + else -> entry.key.replaceFirstChar { it.uppercaseChar() }.replace('_', ' ') + } + } + val value: String = formatKeyValue(entry.key, entry.value) + + key to value + } + + for ((key, value) in entries) { + string.append("$key: $value\n") + } + } + string.append("---------------------") + + return string.toString() + } + + private fun formatKeyValue(key: String, value: JsonElement): String { + return when (key) { + "queue" -> + buildString { + println("QUEUE $value ${value.jsonArray}") + if (value.jsonArray.isNotEmpty()) { + appendLine() + + for ((index, item) in value.jsonArray.withIndex()) { + append(index) + append(": ") + appendLine(item.jsonPrimitive.content) + } + } + } + "state" -> value.jsonPrimitive.intOrNull?.let { SpMsPlayerState.values().getOrNull(it)?.name } ?: value.jsonPrimitive.toString() + "repeat_mode" -> value.jsonPrimitive.intOrNull?.let { SpMsPlayerRepeatMode.values().getOrNull(it)?.name } ?: value.jsonPrimitive.toString() + "current_position_ms", "duration_ms" -> with (Duration) { + value.jsonPrimitive.longOrNull?.let { it.milliseconds }.toString() + } + else -> value.toString() + } + } +} diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionMoveItem.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionMoveItem.kt similarity index 87% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionMoveItem.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionMoveItem.kt index 19b7a05..0909684 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionMoveItem.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionMoveItem.kt @@ -1,10 +1,10 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionMoveItem: ServerAction( identifier = "moveItem", diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionPause.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionPause.kt similarity index 73% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionPause.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionPause.kt index bd4fadd..2bbaa3c 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionPause.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionPause.kt @@ -1,8 +1,8 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionPause: ServerAction( name = { server_actions.pause_name }, diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionPlay.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionPlay.kt similarity index 73% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionPlay.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionPlay.kt index 792bf22..ea3f7cc 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionPlay.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionPlay.kt @@ -1,8 +1,8 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionPlay: ServerAction( identifier = "play", diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionPlayPause.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionPlayPause.kt similarity index 74% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionPlayPause.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionPlayPause.kt index 78b67f2..4890182 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionPlayPause.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionPlayPause.kt @@ -1,8 +1,8 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionPlayPause: ServerAction( identifier = "playPause", diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionReadyToPlay.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionReadyToPlay.kt similarity index 91% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionReadyToPlay.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionReadyToPlay.kt index 1ff6157..000b30c 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionReadyToPlay.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionReadyToPlay.kt @@ -1,11 +1,11 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.long -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID import kotlin.time.Duration class ServerActionReadyToPlay: ServerAction( diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionRemoveItem.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionRemoveItem.kt similarity index 84% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionRemoveItem.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionRemoveItem.kt index 2a03102..c0068e1 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionRemoveItem.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionRemoveItem.kt @@ -1,10 +1,10 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionRemoveItem: ServerAction( identifier = "removeItem", diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionSeekToItem.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSeekToItem.kt similarity index 87% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionSeekToItem.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSeekToItem.kt index a06bdc9..aaa54e3 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionSeekToItem.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSeekToItem.kt @@ -1,8 +1,8 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.* -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionSeekToItem: ServerAction( identifier = "seekToItem", diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionSeekToNext.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSeekToNext.kt similarity index 75% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionSeekToNext.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSeekToNext.kt index 10b35c2..7669cef 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionSeekToNext.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSeekToNext.kt @@ -1,8 +1,8 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionSeekToNext: ServerAction( identifier = "seekToNext", diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionSeekToPrevious.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSeekToPrevious.kt similarity index 76% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionSeekToPrevious.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSeekToPrevious.kt index e4a2baf..ef6a9c2 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionSeekToPrevious.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSeekToPrevious.kt @@ -1,8 +1,8 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionSeekToPrevious: ServerAction( identifier = "seekToPrevious", diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionSeekToTime.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSeekToTime.kt similarity index 84% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionSeekToTime.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSeekToTime.kt index 5ac323e..aab25a5 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionSeekToTime.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSeekToTime.kt @@ -1,10 +1,10 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.long -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID class ServerActionSeekToTime: ServerAction( identifier = "seekToTime", diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionSetRepeatMode.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSetRepeatMode.kt similarity index 80% rename from src/commonMain/kotlin/spms/socketapi/server/ServerActionSetRepeatMode.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSetRepeatMode.kt index ed07313..75553ae 100644 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionSetRepeatMode.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/server/ServerActionSetRepeatMode.kt @@ -1,11 +1,11 @@ -package spms.socketapi.server +package dev.toastbits.spms.socketapi.server import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID -import spms.socketapi.shared.SpMsPlayerRepeatMode +import dev.toastbits.spms.server.SpMs +import dev.toastbits.spms.socketapi.shared.SpMsClientID +import dev.toastbits.spms.socketapi.shared.SpMsPlayerRepeatMode class ServerActionSetRepeatMode: ServerAction( identifier = "setRepeatMode", diff --git a/src/commonMain/kotlin/spms/socketapi/shared/SpMsActionReply.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/shared/SpMsActionReply.kt similarity index 85% rename from src/commonMain/kotlin/spms/socketapi/shared/SpMsActionReply.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/shared/SpMsActionReply.kt index bcd22c5..6779db6 100644 --- a/src/commonMain/kotlin/spms/socketapi/shared/SpMsActionReply.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/shared/SpMsActionReply.kt @@ -1,4 +1,4 @@ -package spms.socketapi.shared +package dev.toastbits.spms.socketapi.shared import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement diff --git a/src/commonMain/kotlin/spms/socketapi/shared/SpMsClientData.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/shared/SpMsClientData.kt similarity index 97% rename from src/commonMain/kotlin/spms/socketapi/shared/SpMsClientData.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/shared/SpMsClientData.kt index b5080c6..b958500 100644 --- a/src/commonMain/kotlin/spms/socketapi/shared/SpMsClientData.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/shared/SpMsClientData.kt @@ -1,4 +1,4 @@ -package spms.socketapi.shared +package dev.toastbits.spms.socketapi.shared import kotlinx.serialization.Serializable diff --git a/src/commonMain/kotlin/spms/socketapi/shared/SpMsPlayerEvent.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/shared/SpMsPlayerEvent.kt similarity index 98% rename from src/commonMain/kotlin/spms/socketapi/shared/SpMsPlayerEvent.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/shared/SpMsPlayerEvent.kt index c3a41b6..148585c 100644 --- a/src/commonMain/kotlin/spms/socketapi/shared/SpMsPlayerEvent.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/shared/SpMsPlayerEvent.kt @@ -1,4 +1,4 @@ -package spms.socketapi.shared +package dev.toastbits.spms.socketapi.shared import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonPrimitive diff --git a/src/commonMain/kotlin/spms/socketapi/shared/SpMsSocketApi.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/shared/SpMsSocketApi.kt similarity index 97% rename from src/commonMain/kotlin/spms/socketapi/shared/SpMsSocketApi.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/shared/SpMsSocketApi.kt index 37662ee..a917765 100644 --- a/src/commonMain/kotlin/spms/socketapi/shared/SpMsSocketApi.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/socketapi/shared/SpMsSocketApi.kt @@ -1,4 +1,4 @@ -package spms.socketapi.shared +package dev.toastbits.spms.socketapi.shared const val SPMS_API_VERSION: Int = 2 // post-v1.0.0 diff --git a/library/src/commonMain/kotlin/dev/toastbits/spms/zmq/ZmqMessage.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/zmq/ZmqMessage.kt new file mode 100644 index 0000000..55bbd3f --- /dev/null +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/zmq/ZmqMessage.kt @@ -0,0 +1,6 @@ +package dev.toastbits.spms.zmq + +class ZmqMessage(val client_id: ByteArray, val parts: List) { + override fun toString(): String = + "Message(client_id=${client_id.contentHashCode()}, parts=$parts)" +} diff --git a/src/commonMain/kotlin/cinterop/zmq/ZmqRouter.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/zmq/ZmqRouter.kt similarity index 50% rename from src/commonMain/kotlin/cinterop/zmq/ZmqRouter.kt rename to library/src/commonMain/kotlin/dev/toastbits/spms/zmq/ZmqRouter.kt index 07bec6f..3a0ddf6 100644 --- a/src/commonMain/kotlin/cinterop/zmq/ZmqRouter.kt +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/zmq/ZmqRouter.kt @@ -1,21 +1,10 @@ -package cinterop.zmq +package dev.toastbits.spms.zmq -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.MemScope -import kotlinx.cinterop.cstr -import kotlinx.cinterop.toCValues -import libzmq.* -import spms.socketapi.shared.SpMsSocketApi +import dev.toastbits.spms.socketapi.shared.SpMsSocketApi import kotlin.time.Duration -@OptIn(ExperimentalForeignApi::class) -abstract class ZmqRouter(mem_scope: MemScope) { - protected class Message(val client_id: ByteArray, val parts: List) { - override fun toString(): String = - "Message(client_id=${client_id.contentHashCode()}, parts=$parts)" - } - - private val socket: ZmqSocket = ZmqSocket(mem_scope, ZMQ_ROUTER, is_binder = true) +open class ZmqRouter { + private val socket: ZmqSocket = ZmqSocket(ZmqSocketType.ROUTER, is_binder = true) var bound_port: Int? = null private set @@ -37,7 +26,7 @@ abstract class ZmqRouter(mem_scope: MemScope) { socket.release() } - protected fun recvMultipart(timeout: Duration?): Message? { + fun recvMultipart(timeout: Duration?): ZmqMessage? { val parts: List = socket.recvMultipart(timeout) ?: return null var client_id: ByteArray? = null @@ -56,12 +45,12 @@ abstract class ZmqRouter(mem_scope: MemScope) { return null } - return Message(client_id, SpMsSocketApi.decode(message_parts)) + return ZmqMessage(client_id, SpMsSocketApi.decode(message_parts)) } - protected fun sendMultipart(message: Message) { + fun sendMultipart(message: ZmqMessage) { socket.sendMultipart( - listOf(message.client_id.toCValues()) + SpMsSocketApi.encode(message.parts).map { it.cstr } + listOf(message.client_id) + SpMsSocketApi.encode(message.parts).map { it.encodeToByteArray() } ) } } diff --git a/library/src/commonMain/kotlin/dev/toastbits/spms/zmq/ZmqSocket.kt b/library/src/commonMain/kotlin/dev/toastbits/spms/zmq/ZmqSocket.kt new file mode 100644 index 0000000..6d50eb1 --- /dev/null +++ b/library/src/commonMain/kotlin/dev/toastbits/spms/zmq/ZmqSocket.kt @@ -0,0 +1,22 @@ +package dev.toastbits.spms.zmq + +import kotlin.time.Duration + +enum class ZmqSocketType { + REP, DEALER, ROUTER +} + +expect class ZmqSocket(type: ZmqSocketType, is_binder: Boolean) { + fun isConnected(): Boolean + + fun connect(address: String) + fun disconnect() + + fun release() + + fun recvStringMultipart(timeout: Duration?): List? + fun recvMultipart(timeout: Duration?): List? + + fun sendStringMultipart(parts: List) + fun sendMultipart(parts: List) +} diff --git a/library/src/commonMain/kotlin/toInt.kt b/library/src/commonMain/kotlin/toInt.kt new file mode 100644 index 0000000..ce76f1c --- /dev/null +++ b/library/src/commonMain/kotlin/toInt.kt @@ -0,0 +1,3 @@ +fun Boolean.toInt(): Int = + if (this) 1 + else 0 diff --git a/library/src/jvmMain/kotlin/dev/toastbits/spms/Platform.jvm.kt b/library/src/jvmMain/kotlin/dev/toastbits/spms/Platform.jvm.kt new file mode 100644 index 0000000..6865e75 --- /dev/null +++ b/library/src/jvmMain/kotlin/dev/toastbits/spms/Platform.jvm.kt @@ -0,0 +1,49 @@ +package dev.toastbits.spms + +import okio.Path +import okio.Path.Companion.toPath +import okio.FileSystem +import java.net.InetAddress +import java.lang.System.getenv +import dev.toastbits.spms.server.SpMs +import gen.libmpv.LibMpv +import java.io.File + +actual val FileSystem.Companion.PLATFORM: FileSystem get() = FileSystem.SYSTEM + +actual fun getHostname(): String = InetAddress.getLocalHost().hostName +actual fun getOSName(): String = System.getProperty("os.name") + +actual fun getTempDir(): Path = + when (OS.current) { + OS.LINUX -> "/tmp/".toPath() + OS.WINDOWS -> "${getenv("USERPROFILE")!!}/AppData/Local/Temp/".toPath() + } + +actual fun getCacheDir(): Path = + when (OS.current) { + OS.LINUX -> "/home/${getenv("USER")!!}/.cache/".toPath().resolve(SpMs.application_name) + OS.WINDOWS -> "${getenv("USERPROFILE")!!}/AppData/Local/${SpMs.application_name}/cache".toPath() + } + +actual fun createLibMpv(): LibMpv { + return LibMpv() +} + +enum class OS { + LINUX, WINDOWS; + + companion object { + val current: OS get() { + val os: String = System.getProperty("os.name").lowercase() + if (os.contains("windows")) { + return WINDOWS + } + if (os.contains("nix") || os.contains("linux")) { + return LINUX + } + + throw NotImplementedError(os) + } + } +} diff --git a/library/src/jvmMain/kotlin/dev/toastbits/spms/ReentrantLock.jvm.kt b/library/src/jvmMain/kotlin/dev/toastbits/spms/ReentrantLock.jvm.kt new file mode 100644 index 0000000..74186ca --- /dev/null +++ b/library/src/jvmMain/kotlin/dev/toastbits/spms/ReentrantLock.jvm.kt @@ -0,0 +1,10 @@ +@file:OptIn(io.ktor.utils.io.InternalAPI::class) +package dev.toastbits.spms + +import io.ktor.utils.io.locks.withLock + +actual class ReentrantLock { + private val lock = io.ktor.utils.io.locks.ReentrantLock() + @Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE") + actual inline fun withLock(action: () -> T): T = lock.withLock(action) +} \ No newline at end of file diff --git a/library/src/jvmMain/kotlin/dev/toastbits/spms/zmq/ZmqSocket.jvm.kt b/library/src/jvmMain/kotlin/dev/toastbits/spms/zmq/ZmqSocket.jvm.kt new file mode 100644 index 0000000..46324bb --- /dev/null +++ b/library/src/jvmMain/kotlin/dev/toastbits/spms/zmq/ZmqSocket.jvm.kt @@ -0,0 +1,120 @@ +package dev.toastbits.spms.zmq + +import dev.toastbits.spms.socketapi.shared.SpMsSocketApi +import dev.toastbits.spms.socketapi.shared.SPMS_MESSAGE_MAX_SIZE +import kotlin.time.Duration +// import org.zeromq.ZContext +// import org.zeromq.ZMQ.SNDMORE +// import org.zeromq.ZMQ.Msg +// import org.zeromq.SocketType as ZSocketType +// import org.zeromq.ZMQ.Socket as ZSocket +// import org.zeromq.ZMQ.Poller as ZPoller +import org.zeromq.* + +actual class ZmqSocket actual constructor(type: ZmqSocketType, val is_binder: Boolean) { + private val context: ZContext = ZContext() + private val socket: ZMQ.Socket = context.createSocket( + when (type) { + ZmqSocketType.REP -> SocketType.REP + ZmqSocketType.DEALER -> SocketType.DEALER + ZmqSocketType.ROUTER -> SocketType.ROUTER + } + ) + private val poller: ZMQ.Poller = context.createPoller(0) + + private var current_address: String? = null + + init { + context.setLinger(0) + } + + actual fun isConnected(): Boolean = current_address != null + + actual fun connect(address: String) { + check(current_address == null) { + if (is_binder) "Already bound to address $current_address" + else "Already connected to address $current_address" + } + + val result: Boolean = + if (is_binder) socket.bind(address) + else socket.connect(address) + + check(result) { + if (is_binder) "Binding to $address failed ($result)" + else "Connecting to $address failed ($result)" + } + + poller.register(socket, ZPoller.POLLIN) + + current_address = address + } + + actual fun disconnect() { + val address: String? = current_address + check(address != null) { + if (is_binder) "Not bound" + else "Not connected" + } + + poller.unregister(socket) + + if (is_binder) socket.unbind(address) + else socket.disconnect(address) + + current_address = null + } + + actual fun release() { + if (current_address != null) { + disconnect() + check(current_address == null) + } + + socket.close() + context.destroy() + } + + actual fun recvStringMultipart(timeout: Duration?): List? { + val message: List = recvMultipart(timeout) ?: return null + return SpMsSocketApi.decode(message.map { it.decodeToString() }) + } + + actual fun recvMultipart(timeout: Duration?): List? { + if (poller.poll(timeout?.inWholeMilliseconds ?: 1) <= 0) { + return null + } + + val parts: MutableList = mutableListOf() + + while (true) { + for (part in ZMsg.recvMsg(socket)) { + parts.add(part.data) + } + + if (!socket.hasReceiveMore()) { + break + } + } + + return parts + } + + actual fun sendStringMultipart(parts: List) = + sendMultipart(SpMsSocketApi.encode(parts).map { it.encodeToByteArray() }) + + actual fun sendMultipart(parts: List) { + if (parts.isEmpty()) { + return + } + + for ((i, part) in parts.withIndex()) { + sendBytes(part, part.size, if (i + 1 == parts.size) null else ZMQ.SNDMORE) + } + } + + private fun sendBytes(bytes: ByteArray, size: Int, flags: Int? = null) { + val msg: zmq.Msg = zmq.Msg(bytes) + socket.sendMsg(msg, flags ?: 0) + } +} diff --git a/library/src/linuxMain/kotlin/dev/toastbits/spms/NativePlatform.linux.kt b/library/src/linuxMain/kotlin/dev/toastbits/spms/NativePlatform.linux.kt new file mode 100644 index 0000000..c235d58 --- /dev/null +++ b/library/src/linuxMain/kotlin/dev/toastbits/spms/NativePlatform.linux.kt @@ -0,0 +1,7 @@ +package dev.toastbits.spms + +import kotlinx.cinterop.CValuesRef +import libzmq.zmq_poller_wait + +actual fun zmqPollerWait(poller: CValuesRef<*>?, event: CValuesRef?, timeout: Long): Int = + zmq_poller_wait(poller, event, timeout) diff --git a/library/src/linuxMain/kotlin/dev/toastbits/spms/Platform.linux.kt b/library/src/linuxMain/kotlin/dev/toastbits/spms/Platform.linux.kt new file mode 100644 index 0000000..6236b09 --- /dev/null +++ b/library/src/linuxMain/kotlin/dev/toastbits/spms/Platform.linux.kt @@ -0,0 +1,16 @@ +package dev.toastbits.spms + +import okio.Path +import okio.Path.Companion.toPath +import kotlinx.cinterop.* +import platform.posix.gethostname + +actual fun getTempDir(): Path = "/tmp/".toPath() + +@OptIn(ExperimentalForeignApi::class) +actual fun getHostname(): String = + memScoped { + val str: CPointer> = allocArray(1024) + gethostname(str, 1023UL) + return@memScoped str.toKString() + } diff --git a/library/src/mingwMain/kotlin/dev/toastbits/spms/NativePlatform.mingw.kt b/library/src/mingwMain/kotlin/dev/toastbits/spms/NativePlatform.mingw.kt new file mode 100644 index 0000000..0d2a521 --- /dev/null +++ b/library/src/mingwMain/kotlin/dev/toastbits/spms/NativePlatform.mingw.kt @@ -0,0 +1,7 @@ +package dev.toastbits.spms + +import kotlinx.cinterop.CValuesRef +import libzmq.zmq_poller_wait + +actual fun zmqPollerWait(poller: CValuesRef<*>?, event: CValuesRef?, timeout: Long): Int = + zmq_poller_wait(poller, event, timeout.toInt()) diff --git a/library/src/mingwMain/kotlin/dev/toastbits/spms/Platform.mingw.kt b/library/src/mingwMain/kotlin/dev/toastbits/spms/Platform.mingw.kt new file mode 100644 index 0000000..6eff6ad --- /dev/null +++ b/library/src/mingwMain/kotlin/dev/toastbits/spms/Platform.mingw.kt @@ -0,0 +1,18 @@ +package dev.toastbits.spms + +import okio.Path +import okio.Path.Companion.toPath +import platform.posix.getenv +import platform.posix.gethostname +import kotlinx.cinterop.toKString +import kotlinx.cinterop.* + +actual fun getTempDir(): Path = "${getenv("USERPROFILE")!!.toKString()}/AppData/Local/Temp/".toPath() + +@OptIn(ExperimentalForeignApi::class) +actual fun getHostname(): String = + memScoped { + val str: CPointer> = allocArray(1024) + gethostname(str, 1023) + return@memScoped str.toKString() + } diff --git a/src/nativeInterop/buildscripts/libmpv.sh b/library/src/nativeInterop/buildscripts/libmpv.sh similarity index 82% rename from src/nativeInterop/buildscripts/libmpv.sh rename to library/src/nativeInterop/buildscripts/libmpv.sh index ffbfa9d..3db6ed1 100755 --- a/src/nativeInterop/buildscripts/libmpv.sh +++ b/library/src/nativeInterop/buildscripts/libmpv.sh @@ -1,3 +1,20 @@ +# Libraries have been installed in: +# /home/toaster/Projects/Kotlin/spmp/spmp-server/library/src/nativeInterop/buildscripts/build/libmpv/output/lib + +# If you ever happen to want to link against installed libraries +# in a given directory, LIBDIR, you must either use libtool, and +# specify the full pathname of the library, or use the '-LLIBDIR' +# flag during linking and do at least one of the following: +# - add LIBDIR to the 'LD_LIBRARY_PATH' environment variable +# during execution +# - add LIBDIR to the 'LD_RUN_PATH' environment variable +# during linking +# - use the '-Wl,-rpath -Wl,LIBDIR' linker flag +# - have your system administrator add LIBDIR to '/etc/ld.so.conf' + +# See any operating system documentation about shared libraries for +# more information, such as the ld(1) and ld.so(8) manual pages. + # THIS DOES NOT WORK # The libraries compile successfully, but mpv doesn't seem to actually statically link any of these dependencies to libmpv # So when linking libmpv to spmp-server, any reference to a dependency is undefined @@ -46,13 +63,13 @@ fi if [ ! -d libplacebo ]; then echo "Downloading libplacebo..." - git clone https://code.videolan.org/videolan/libplacebo.git --depth=1 --recursive + git clone https://code.videolan.org/videolan/libplacebo.git --depth=1 --recursive --single-branch -b v6.338.2 fi if [ ! -d ffmpeg ]; then echo "Downloading ffmpeg..." - git clone https://git.ffmpeg.org/ffmpeg.git --depth=1 --recursive + git clone https://git.ffmpeg.org/ffmpeg.git --depth=1 --recursive --single-branch -b n7.0 fi if [ ! -d libass ]; then @@ -152,7 +169,7 @@ cd ../.. echo "Compiling libass..." cd libass -./configure \ +LDFLAGS="-L$OUTPUT_DIR/lib" ./configure \ --prefix=$OUTPUT_DIR \ --disable-libunibreak \ --disable-require-system-font-provider \ @@ -175,10 +192,13 @@ cd .. echo "Compiling mpv..." +export CC="$CC -I$OUTPUT_DIR/include" + cd mpv meson setup build \ --prefix=$OUTPUT_DIR \ --libdir=$OUTPUT_DIR/lib \ + --includedir=$OUTPUT_DIR/include \ --reconfigure \ --clearcache \ -Dbuildtype=release \ diff --git a/library/src/nativeMain/kotlin/dev/toastbits/spms/NativePlatform.kt b/library/src/nativeMain/kotlin/dev/toastbits/spms/NativePlatform.kt new file mode 100644 index 0000000..2f70537 --- /dev/null +++ b/library/src/nativeMain/kotlin/dev/toastbits/spms/NativePlatform.kt @@ -0,0 +1,5 @@ +package dev.toastbits.spms + +import kotlinx.cinterop.CValuesRef + +expect fun zmqPollerWait(poller: CValuesRef<*>?, event: CValuesRef?, timeout: Long): Int diff --git a/library/src/nativeMain/kotlin/dev/toastbits/spms/Platform.native.kt b/library/src/nativeMain/kotlin/dev/toastbits/spms/Platform.native.kt new file mode 100644 index 0000000..e931b39 --- /dev/null +++ b/library/src/nativeMain/kotlin/dev/toastbits/spms/Platform.native.kt @@ -0,0 +1,46 @@ +package dev.toastbits.spms + +import kotlinx.cinterop.* +import dev.toastbits.spms.native.safeToKString +import dev.toastbits.spms.server.SpMs +import platform.posix.getenv +import okio.Path +import okio.Path.Companion.toPath +import okio.FileSystem +import gen.libmpv.LibMpv + +actual val FileSystem.Companion.PLATFORM: FileSystem get() = FileSystem.SYSTEM + +actual fun getOSName(): String { + val os: String = + when (Platform.osFamily) { + OsFamily.MACOSX -> "OSX" + OsFamily.IOS -> "iOS" + OsFamily.LINUX -> "Linux" + OsFamily.WINDOWS -> "Windows" + OsFamily.ANDROID -> "Android" + OsFamily.WASM -> "Wasm" + OsFamily.TVOS -> "TV" + OsFamily.WATCHOS -> "WatchOS" + OsFamily.UNKNOWN -> "Unknown" + } + + val architecture: String = + when (Platform.cpuArchitecture) { + CpuArchitecture.X86 -> "x86" + CpuArchitecture.X64 -> "x86-64" + CpuArchitecture.UNKNOWN -> "Unknown arch" + else -> Platform.cpuArchitecture.name + } + + return "$os ($architecture)" +} + +actual fun getCacheDir(): Path = + when (Platform.osFamily) { + OsFamily.LINUX -> "/home/${getenv("USER")!!.toKString()}/.cache/".toPath().resolve(SpMs.application_name) + OsFamily.WINDOWS -> "${getenv("USERPROFILE")!!.toKString()}/AppData/Local/${SpMs.application_name}/cache".toPath() + else -> throw NotImplementedError(Platform.osFamily.name) + } + +actual fun createLibMpv(): LibMpv = LibMpv() diff --git a/library/src/nativeMain/kotlin/dev/toastbits/spms/ReentrantLock.native.kt b/library/src/nativeMain/kotlin/dev/toastbits/spms/ReentrantLock.native.kt new file mode 100644 index 0000000..a1c1ab7 --- /dev/null +++ b/library/src/nativeMain/kotlin/dev/toastbits/spms/ReentrantLock.native.kt @@ -0,0 +1,9 @@ +package dev.toastbits.spms + +import kotlinx.atomicfu.locks.withLock + +actual class ReentrantLock { + private val lock = kotlinx.atomicfu.locks.ReentrantLock() + @Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE") + actual inline fun withLock(action: () -> T): T = lock.withLock(action) +} diff --git a/src/commonMain/kotlin/cinterop/utils/safeToKString.kt b/library/src/nativeMain/kotlin/dev/toastbits/spms/native/safeToKString.kt similarity index 79% rename from src/commonMain/kotlin/cinterop/utils/safeToKString.kt rename to library/src/nativeMain/kotlin/dev/toastbits/spms/native/safeToKString.kt index 0f247f2..ec8ba58 100644 --- a/src/commonMain/kotlin/cinterop/utils/safeToKString.kt +++ b/library/src/nativeMain/kotlin/dev/toastbits/spms/native/safeToKString.kt @@ -1,8 +1,8 @@ -@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") -package cinterop.utils +package dev.toastbits.spms.native import kotlinx.cinterop.* +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") fun CPointer.safeToKString(): String { val nativeBytes = this diff --git a/src/commonMain/kotlin/cinterop/zmq/ZmqSocket.kt b/library/src/nativeMain/kotlin/dev/toastbits/spms/zmq/ZmqSocket.native.kt similarity index 70% rename from src/commonMain/kotlin/cinterop/zmq/ZmqSocket.kt rename to library/src/nativeMain/kotlin/dev/toastbits/spms/zmq/ZmqSocket.native.kt index ea84bf0..485670c 100644 --- a/src/commonMain/kotlin/cinterop/zmq/ZmqSocket.kt +++ b/library/src/nativeMain/kotlin/dev/toastbits/spms/zmq/ZmqSocket.native.kt @@ -1,23 +1,31 @@ -package cinterop.zmq +package dev.toastbits.spms.zmq import kotlinx.cinterop.* import libzmq.* import platform.posix.memcpy -import spms.zmqPollerWait -import spms.socketapi.shared.SpMsSocketApi -import spms.socketapi.shared.SPMS_MESSAGE_MAX_SIZE +import dev.toastbits.spms.zmqPollerWait +import dev.toastbits.spms.socketapi.shared.SpMsSocketApi +import dev.toastbits.spms.socketapi.shared.SPMS_MESSAGE_MAX_SIZE import kotlin.time.Duration @OptIn(ExperimentalForeignApi::class) -class ZmqSocket(mem_scope: MemScope, type: Int, val is_binder: Boolean) { +actual class ZmqSocket actual constructor(type: ZmqSocketType, val is_binder: Boolean) { private val message_buffer: CPointer> private val message_buffer_size: ULong private val has_more: IntVar private val has_more_size: ULongVarOf + private val mem_scope: MemScope = MemScope() private val context = zmq_ctx_new() - private val socket = zmq_socket(context, type) + private val socket = zmq_socket( + context, + when (type) { + ZmqSocketType.REP -> ZMQ_REP + ZmqSocketType.DEALER -> ZMQ_DEALER + ZmqSocketType.ROUTER -> ZMQ_ROUTER + } + ) private val poller = zmq_poller_new() private var current_address: String? = null @@ -34,9 +42,9 @@ class ZmqSocket(mem_scope: MemScope, type: Int, val is_binder: Boolean) { } } - fun isConnected(): Boolean = current_address != null + actual fun isConnected(): Boolean = current_address != null - fun setSocketOption(option: Int, value: Int) { + private fun setSocketOption(option: Int, value: Int) { memScoped { val val_ptr: IntVar = alloc() val_ptr.value = value @@ -44,7 +52,7 @@ class ZmqSocket(mem_scope: MemScope, type: Int, val is_binder: Boolean) { } } - fun connect(address: String) { + actual fun connect(address: String) { check(current_address == null) { if (is_binder) "Already bound to address $current_address" else "Already connected to address $current_address" @@ -64,7 +72,7 @@ class ZmqSocket(mem_scope: MemScope, type: Int, val is_binder: Boolean) { current_address = address } - fun disconnect() { + actual fun disconnect() { val address: String? = current_address check(address != null) { if (is_binder) "Not bound" @@ -79,7 +87,7 @@ class ZmqSocket(mem_scope: MemScope, type: Int, val is_binder: Boolean) { current_address = null } - fun release() { + actual fun release() { if (current_address != null) { disconnect() check(current_address == null) @@ -87,14 +95,17 @@ class ZmqSocket(mem_scope: MemScope, type: Int, val is_binder: Boolean) { zmq_close(socket) zmq_ctx_destroy(context) + + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + mem_scope.clearImpl() } - fun recvStringMultipart(timeout: Duration?): List? { + actual fun recvStringMultipart(timeout: Duration?): List? { val message: List = recvMultipart(timeout) ?: return null return SpMsSocketApi.decode(message.map { it.decodeToString() }) } - fun recvMultipart(timeout: Duration?): List? = memScoped { + actual fun recvMultipart(timeout: Duration?): List? = memScoped { val event: zmq_poller_event_t = alloc() zmqPollerWait(poller, event.ptr, timeout?.inWholeMilliseconds ?: ZMQ_NOBLOCK.toLong()) @@ -118,26 +129,26 @@ class ZmqSocket(mem_scope: MemScope, type: Int, val is_binder: Boolean) { return parts } - fun sendStringMultipart(parts: List) = - sendMultipart(SpMsSocketApi.encode(parts).map { it.cstr }) + actual fun sendStringMultipart(parts: List) = + sendMultipart(SpMsSocketApi.encode(parts).map { it.encodeToByteArray() }) - fun sendMultipart(parts: List>) = memScoped { + actual fun sendMultipart(parts: List) = memScoped { if (parts.isEmpty()) { return } for ((i, part) in parts.withIndex()) { - sendBytes(part.ptr, part.size, if (i + 1 == parts.size) null else ZMQ_SNDMORE) + sendBytes(part.toCValues(), part.size, if (i + 1 == parts.size) null else ZMQ_SNDMORE) } } - private fun MemScope.sendBytes(bytes: CValuesRef, size: Int, flags: Int? = null) { + private fun MemScope.sendBytes(bytes: CValues, size: Int, flags: Int? = null) { val msg: zmq_msg_t = alloc() var rc: Int = zmq_msg_init_size(msg.ptr, size.toULong()) check(rc == 0) { "Could not init message ($rc)" } - memcpy(zmq_msg_data(msg.ptr), bytes, size.toULong()) + memcpy(zmq_msg_data(msg.ptr), bytes.ptr, size.toULong()) rc = zmq_msg_send(msg.ptr, socket, flags ?: 0) check(rc == size) { "zmq_msg_send failed ($rc != $size)" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 43a7f8d..ba3b427 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,40 @@ +rootProject.name = "spms" + +include(":library") +includeBuild("library/build-logic") +include(":app") + pluginManagement { + resolutionStrategy { + eachPlugin { + val kjna_version: String = extra["kjna.version"] as String + if (requested.id.toString() == "dev.toastbits.kjna") { + useModule("dev.toastbits.kjna:plugin:$kjna_version") + } + } + } + + repositories { + mavenLocal() + maven("https://jitpack.io") + mavenCentral() + gradlePluginPortal() + } + plugins { - val kotlin_version = extra["kotlin.version"] as String + val kotlin_version: String = extra["kotlin.version"] as String kotlin("multiplatform").version(kotlin_version) kotlin("plugin.serialization").version(kotlin_version) + + val kjna_version: String = extra["kjna.version"] as String + id("dev.toastbits.kjna").version(kjna_version) + } +} + +dependencyResolutionManagement { + repositories { + mavenLocal() + maven("https://jitpack.io") + mavenCentral() } } diff --git a/src/commonMain/kotlin/cinterop/indicator/TrayIndicatorImpl.kt.linux b/src/commonMain/kotlin/cinterop/indicator/TrayIndicatorImpl.kt.linux deleted file mode 100644 index 494416e..0000000 --- a/src/commonMain/kotlin/cinterop/indicator/TrayIndicatorImpl.kt.linux +++ /dev/null @@ -1,151 +0,0 @@ -@file:Suppress("INVISIBLE_MEMBER") -package cinterop.indicator - -import kotlinx.cinterop.CPointer -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.IntVar -import kotlinx.cinterop.IntVarOf -import kotlinx.cinterop.MemScope -import kotlinx.cinterop.alloc -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.pointed -import kotlinx.cinterop.ptr -import kotlinx.cinterop.reinterpret -import kotlinx.cinterop.staticCFunction -import kotlinx.cinterop.value -import libappindicator.* -import platform.posix.LC_NUMERIC -import platform.posix.setlocale -import platform.posix.getenv - -fun createTrayIndicator(name: String, icon_path: List): TrayIndicator? { - if (getenv("XDG_CURRENT_DESKTOP") == null) { - return null - } - return TrayIndicatorImpl(name, icon_path) -} - -@OptIn(ExperimentalForeignApi::class) -class TrayIndicatorImpl(name: String, icon_path: List): TrayIndicator { - private val mem_scope = MemScope() - private val indicator: CPointer - private val menu: CPointer - private var main_loop: CPointer? = null - - override fun show() { - app_indicator_set_menu(indicator, menu.reinterpret()) - - main_loop = g_main_loop_new(null, FALSE) - g_main_loop_run(main_loop) - main_loop = null - } - - override fun hide() { - main_loop?.also { loop -> - g_main_loop_quit(loop) - } - } - - override fun release() { - hide() - } - - override fun addClickCallback(onClick: ClickCallback) { - // TODO - } - - override fun addButton(label: String, onClick: ButtonCallback?) { - val item: CPointer = gtk_menu_item_new_with_label(label)!! - - if (onClick != null) { - val callback_index: CPointer = button_callbacks.addCallback(onClick, mem_scope) - g_signal_connect_data( - item, - "activate", - staticCFunction { _: CPointer<*>, index: CPointer -> - button_callbacks.getCallback(index).invoke() - }.reinterpret(), - callback_index, - null, - 0U - ) - } - - gtk_menu_shell_append(menu.reinterpret(), item) - gtk_widget_show(item) - } - - override fun addScrollCallback(onScroll: (delta: Int, direction: Int) -> Unit) { - val callback_index: CPointer = scroll_callbacks.addCallback(onScroll, mem_scope) - - g_signal_connect_data( - indicator, - "scroll-event", - staticCFunction { _: CPointer<*>, delta: gint, direction: guint, index: CPointer -> - scroll_callbacks.getCallback(index).invoke(delta, if (direction == 1U) 1 else -1) - }.reinterpret(), - callback_index, - null, - 0U - ) - } - - init { - // Effectively disables GTK warnings - g_log_set_writer_func( - staticCFunction { level: GLogLevelFlags -> - if (level == G_LOG_LEVEL_ERROR || level == G_LOG_LEVEL_CRITICAL) { - G_LOG_WRITER_UNHANDLED - } - else { - G_LOG_WRITER_HANDLED - } - }.reinterpret(), - null, - null - ) - - memScoped { - val gtk_argc: IntVarOf = alloc() - if (gtk_init_check(gtk_argc.ptr, null) == FALSE) { - throw RuntimeException("Call to gtk_init_check failed") - } - } - - indicator = app_indicator_new("spms", "indicator-messages", AppIndicatorCategory.APP_INDICATOR_CATEGORY_APPLICATION_STATUS)!! - - app_indicator_set_title(indicator, name) - app_indicator_set_status(indicator, AppIndicatorStatus.APP_INDICATOR_STATUS_ACTIVE) - - val path: MutableList = icon_path.toMutableList() - - var filename: String = path.removeLast() - val last_dot: Int = filename.lastIndexOf('.') - if (last_dot != -1) { - filename = filename.substring(0, last_dot) - } - - app_indicator_set_icon_theme_path(indicator, '/' + path.joinToString("/")) - app_indicator_set_icon_full(indicator, filename, name) - - menu = gtk_menu_new()!! - - setlocale(LC_NUMERIC, "C") - } - - companion object { - private val click_callbacks: MutableList = mutableListOf() - private val button_callbacks: MutableList = mutableListOf() - private val scroll_callbacks: MutableList = mutableListOf() - - private fun MutableList.addCallback(callback: T, mem_scope: MemScope): CPointer { - val callback_index: IntVarOf = mem_scope.alloc() - callback_index.value = size - add(callback) - return callback_index.ptr - } - - private fun MutableList.getCallback(index: CPointer): T = - get(index.pointed.value) - } -} diff --git a/src/commonMain/kotlin/cinterop/indicator/TrayIndicatorImpl.kt.other b/src/commonMain/kotlin/cinterop/indicator/TrayIndicatorImpl.kt.other deleted file mode 100644 index 0811612..0000000 --- a/src/commonMain/kotlin/cinterop/indicator/TrayIndicatorImpl.kt.other +++ /dev/null @@ -1,17 +0,0 @@ -package cinterop.indicator - -fun createTrayIndicator(name: String, icon_path: List): TrayIndicator? = null - -class TrayIndicatorImpl(name: String, icon_path: List): TrayIndicator { - override fun show() = TODO() - - override fun hide() = TODO() - - override fun release() = TODO() - - override fun addClickCallback(onClick: ClickCallback) = TODO() - - override fun addButton(label: String, onClick: ButtonCallback?) = TODO() - - override fun addScrollCallback(onScroll: (delta: Int, direction: Int) -> Unit) = TODO() -} diff --git a/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.disabled b/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.disabled deleted file mode 100644 index 9249206..0000000 --- a/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.disabled +++ /dev/null @@ -1,38 +0,0 @@ -package cinterop.mpv - -import spms.player.Player -import kotlinx.cinterop.CPrimitiveVar -import kotlinx.cinterop.MemScope -import kotlin.reflect.KClass - -typealias MpvFormat = Unit - -class mpv_event { - val event_id: Int get() = throw NotImplementedError() -} - -abstract class LibMpvClient( - val headless: Boolean = true, - playlist_auto_progress: Boolean = true -): Player { - companion object { - fun isAvailable(): Boolean = false - } - - init { - throw NotImplementedError() - } - - override fun release() { throw NotImplementedError() } - - internal fun runCommand(name: String, vararg args: Any?, check_result: Boolean = true): Int = throw NotImplementedError() - internal inline fun getProperty(name: String): V = throw NotImplementedError() - internal inline fun setProperty(name: String, value: T) { throw NotImplementedError() } - internal fun observeProperty(name: String, cls: KClass<*>) { throw NotImplementedError() } - internal inline fun MemScope.getPointerOf(v: T? = null): CPrimitiveVar = throw NotImplementedError() - internal fun getFormatOf(cls: KClass<*>): MpvFormat = throw NotImplementedError() - internal fun waitForEvent(): mpv_event? = throw NotImplementedError() - internal fun requestLogMessages() { throw NotImplementedError() } - internal fun addHook(name: String, priority: Int = 0) { throw NotImplementedError() } - internal fun continueHook(id: ULong) { throw NotImplementedError() } -} diff --git a/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.enabled b/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.enabled deleted file mode 100644 index 39e979b..0000000 --- a/src/commonMain/kotlin/cinterop/mpv/LibMpvClient.kt.enabled +++ /dev/null @@ -1,158 +0,0 @@ -package cinterop.mpv - -import kotlinx.cinterop.* -import libmpv.* -import spms.player.Player -import spms.player.toInt -import kotlin.reflect.KClass -import cnames.structs.mpv_handle as MpvHandle -import libmpv.mpv_format as MpvFormat - -@OptIn(ExperimentalForeignApi::class) -abstract class LibMpvClient( - val headless: Boolean = true, - playlist_auto_progress: Boolean = true -): Player { - companion object { - fun isAvailable(): Boolean = true - } - - protected val ctx: CPointer - - init { - ctx = mpv_create_client(null, null) - ?: throw NullPointerException("Creating mpv client failed") - - memScoped { - val vid: BooleanVar = alloc() - vid.value = !headless - mpv_set_option(ctx, "vid", MPV_FORMAT_FLAG, vid.ptr) - - if (!headless) { - mpv_set_option_string(ctx, "force-window", "immediate") - - val osd_level: IntVar = alloc() - osd_level.value = 3 - mpv_set_option(ctx, "osd-level", MPV_FORMAT_INT64, osd_level.ptr) - } - - if (!playlist_auto_progress) { - mpv_set_option_string(ctx, "keep-open", "always") - } - } - - val init_result: Int = mpv_initialize(ctx) - check(init_result == 0) { "Initialising mpv client failed ($init_result)" } - } - - override fun release() { - mpv_terminate_destroy(ctx) - } - - private fun MemScope.buildArgs(args: List, terminate: Boolean = true) = - Array(args.size + terminate.toInt()) { i -> - args.getOrNull(i)?.toString()?.cstr?.getPointer(this) - }.toCValues() - - internal fun runCommand(name: String, vararg args: Any?, check_result: Boolean = true): Int = - memScoped { - val result: Int = mpv_command(ctx, buildArgs(listOf(name).plus(args))) - - if (check_result) { - check(result == 0) { - "Command $name(${args.toList()}) failed ($result)" - } - } - - return result - } - - internal inline fun getProperty(name: String): V = - memScoped { - if (V::class == String::class) { - val string: CPointer> = mpv_get_property_string(ctx, name) - ?: throw NullPointerException("Getting string property '$name' failed") - return string.toKString() as V - } - - val pointer: CPrimitiveVar - val format: MpvFormat - val extractor: () -> V - - when (V::class) { - Boolean::class -> { - pointer = alloc() - format = MPV_FORMAT_FLAG - extractor = { pointer.value as V } - } - Int::class -> { - pointer = alloc() - format = MPV_FORMAT_INT64 - extractor = { pointer.value as V } - } - Double::class -> { - pointer = alloc() - format = MPV_FORMAT_DOUBLE - extractor = { pointer.value as V } - } - else -> throw NotImplementedError(V::class.toString()) - } - - mpv_get_property(ctx, name, format, pointer.ptr) - - return extractor() - } - - internal inline fun setProperty(name: String, value: T) = memScoped { - if (value is String) { - mpv_set_property_string(ctx, name, value) - return@memScoped - } - - val pointer: CPrimitiveVar = getPointerOf(value) - val format: MpvFormat = getFormatOf(T::class) - mpv_set_property(ctx, name, format, pointer.ptr) - } - - internal fun observeProperty(name: String, cls: KClass<*>) { - val format: MpvFormat = getFormatOf(cls) - - try { - mpv_observe_property(ctx, 0UL, name, format) - } - catch (e: Throwable) { - throw RuntimeException("Call to mpv_observe_property for $name with format $format failed", e) - } - } - - internal inline fun MemScope.getPointerOf(v: T? = null): CPrimitiveVar = - when (T::class) { - Boolean::class -> alloc().apply { if (v != null) value = v as Boolean } - Int::class -> alloc().apply { if (v != null) value = v as Int } - Double::class -> alloc().apply { if (v != null) value = v as Double } - else -> throw NotImplementedError(T::class.toString()) - } - - internal fun getFormatOf(cls: KClass<*>): MpvFormat = - when (cls) { - Boolean::class -> MPV_FORMAT_FLAG - Int::class -> MPV_FORMAT_INT64 - Double::class -> MPV_FORMAT_DOUBLE - else -> throw NotImplementedError(cls.toString()) - } - - internal fun waitForEvent(): mpv_event? = - mpv_wait_event(ctx, -1.0)?.pointed - - internal fun requestLogMessages() { - mpv_request_log_messages(ctx, "stats") - } - - internal fun addHook(name: String, priority: Int = 0) { - mpv_hook_add(ctx, 0UL, name, priority) - } - - internal fun continueHook(id: ULong) { - mpv_hook_continue(ctx, id) - } -} diff --git a/src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.disabled b/src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.disabled deleted file mode 100644 index fdf44da..0000000 --- a/src/commonMain/kotlin/cinterop/mpv/MpvClientEventLoop.kt.disabled +++ /dev/null @@ -1,5 +0,0 @@ -package cinterop.mpv - -internal suspend fun MpvClientImpl.eventLoop() { - throw NotImplementedError() -} diff --git a/src/commonMain/kotlin/spms/Platform.kt.linux b/src/commonMain/kotlin/spms/Platform.kt.linux deleted file mode 100644 index 3139124..0000000 --- a/src/commonMain/kotlin/spms/Platform.kt.linux +++ /dev/null @@ -1,23 +0,0 @@ -package spms - -import kotlinx.cinterop.ByteVar -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.CValuesRef -import libzmq.zmq_poller_wait -import platform.posix.* - -fun canOpenProcess(): Boolean = true - -@OptIn(ExperimentalForeignApi::class) -fun openProcess(command: String, modes: String) { popen(command, modes) } - -fun canEndProcess(): Boolean = true -fun endProcess() { kill(0, SIGTERM) } - -@OptIn(ExperimentalForeignApi::class) -fun zmqPollerWait(poller: CValuesRef<*>?, event: CValuesRef?, timeout: Long): Int = - zmq_poller_wait(poller, event, timeout) - -@OptIn(ExperimentalForeignApi::class) -fun getHostname(name: CValuesRef?, len: Int): Int = - gethostname(name, len.toULong()) diff --git a/src/commonMain/kotlin/spms/Platform.kt.other b/src/commonMain/kotlin/spms/Platform.kt.other deleted file mode 100644 index 4d09793..0000000 --- a/src/commonMain/kotlin/spms/Platform.kt.other +++ /dev/null @@ -1,21 +0,0 @@ -package spms - -import kotlinx.cinterop.ByteVar -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.CValuesRef -import libzmq.zmq_poller_wait -import platform.posix.* - -fun canOpenProcess(): Boolean = false -fun openProcess(command: String, modes: String) { TODO() } - -fun canEndProcess(): Boolean = false -fun endProcess() { TODO() } - -@OptIn(ExperimentalForeignApi::class) -fun zmqPollerWait(poller: CValuesRef<*>?, event: CValuesRef?, timeout: Long): Int = - zmq_poller_wait(poller, event, timeout.toInt()) - -@OptIn(ExperimentalForeignApi::class) -fun getHostname(name: CValuesRef?, len: Int): Int = - gethostname(name, len) diff --git a/src/commonMain/kotlin/spms/client/cli/CommandLineModeContext.kt b/src/commonMain/kotlin/spms/client/cli/CommandLineModeContext.kt deleted file mode 100644 index 81abe07..0000000 --- a/src/commonMain/kotlin/spms/client/cli/CommandLineModeContext.kt +++ /dev/null @@ -1,18 +0,0 @@ -@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") -package spms.client.cli - -import cinterop.zmq.ZmqSocket -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.MemScope - -@OptIn(ExperimentalForeignApi::class) -data class CommandLineModeContext( - val socket: ZmqSocket, - val mem_scope: MemScope, - val client_name: String -) { - fun release() { - socket.release() - mem_scope.clearImpl() - } -} diff --git a/src/commonMain/kotlin/spms/server/SpMsMediaSession.kt.other b/src/commonMain/kotlin/spms/server/SpMsMediaSession.kt.other deleted file mode 100644 index ce4a23e..0000000 --- a/src/commonMain/kotlin/spms/server/SpMsMediaSession.kt.other +++ /dev/null @@ -1,13 +0,0 @@ -package spms.server - -import spms.player.Player -import spms.socketapi.shared.SpMsPlayerEvent - -class SpMsMediaSession private constructor() { - fun onPlayerEvent(event: SpMsPlayerEvent) { - } - - companion object { - fun create(player: Player): SpMsMediaSession? = null - } -} diff --git a/src/commonMain/kotlin/spms/socketapi/server/ServerActionGetStatus.kt b/src/commonMain/kotlin/spms/socketapi/server/ServerActionGetStatus.kt deleted file mode 100644 index dcaf5c5..0000000 --- a/src/commonMain/kotlin/spms/socketapi/server/ServerActionGetStatus.kt +++ /dev/null @@ -1,121 +0,0 @@ -package spms.socketapi.server - -import cinterop.mpv.getCurrentStateJson -import com.github.ajalt.clikt.core.Context -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.toKString -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.* -import okio.FileSystem -import okio.Path -import okio.Path.Companion.toPath -import platform.posix.getenv -import spms.localisation.loc -import spms.server.SpMs -import spms.socketapi.shared.SpMsClientID -import spms.socketapi.shared.SpMsPlayerRepeatMode -import spms.socketapi.shared.SpMsPlayerState -import kotlin.time.Duration - -@Suppress("OPT_IN_USAGE") -@OptIn(ExperimentalForeignApi::class) -fun getCacheDir(): Path = - when (Platform.osFamily) { - OsFamily.LINUX -> "/home/${getenv("USER")!!.toKString()}/.cache/".toPath().resolve(SpMs.application_name) - OsFamily.WINDOWS -> "${getenv("USERPROFILE")!!.toKString()}/AppData/Local/${SpMs.application_name}/cache".toPath() - else -> throw NotImplementedError(Platform.osFamily.name) - } - -class ServerActionGetStatus: ServerAction( - identifier = "status", - name = { server_actions.status_name }, - help = { server_actions.status_help }, - parameters = emptyList() -) { - override fun execute(server: SpMs, client: SpMsClientID, context: ActionContext): JsonElement { - return Json.encodeToJsonElement(server.player.getCurrentStateJson()) - } - - private val cache_files: MutableMap = mutableMapOf() - - private fun loadCacheFiles() { - cache_files.clear() - val files: List = FileSystem.SYSTEM.listOrNull(getCacheDir()) ?: return - for (file in files) { - val content: String = FileSystem.SYSTEM.read(file) { readUtf8() } - cache_files[file.name.removeSuffix(".json")] = Json.parseToJsonElement(content) - } - } - - private fun saveCacheFiles() { - val cache_dir: Path = getCacheDir() - FileSystem.SYSTEM.createDirectories(cache_dir) - for ((file, data) in cache_files) { - FileSystem.SYSTEM.write(cache_dir.resolve("$file.json")) { - writeUtf8(Json.encodeToString(data)) - } - } - } - - override fun formatResult(result: JsonElement, context: Context): String { - val string: StringBuilder = StringBuilder("--- ${context.loc.server_actions.status_output_start} ---\n") - runBlocking { - val entries: Collection> = result.jsonObject.entries - val formatted_values: Array = arrayOfNulls(entries.size) - - loadCacheFiles() - entries.mapIndexed { index, entry -> - launch(Dispatchers.Default) { - formatted_values[index] = formatKeyValue(entry.key, entry.value) - } - }.joinAll() - saveCacheFiles() - - for ((index, entry) in entries.withIndex()) { - val key_text: String = with (context.loc.cli) { - when (entry.key) { - "queue" -> status_key_queue - "state" -> status_key_state - "is_playing" -> status_key_is_playing - "current_item_index" -> status_key_current_item_index - "current_position_ms" -> status_key_current_position - "duration_ms" -> status_key_duration - "repeat_mode" -> status_key_repeat_mode - else -> entry.key.replaceFirstChar { it.uppercaseChar() }.replace('_', ' ') - } - } - - string.append("$key_text: ${formatted_values[index]}\n") - } - } - string.append("---------------------") - - return string.toString() - } - - private fun formatKeyValue(key: String, value: JsonElement): String = - when (key) { - "queue" -> - buildString { - if (value.jsonArray.isNotEmpty()) { - appendLine() - - for ((index, item) in value.jsonArray.withIndex()) { - append(index) - append(": ") - appendLine(item.jsonPrimitive.content) - } - } - } - "state" -> value.jsonPrimitive.intOrNull?.let { SpMsPlayerState.values().getOrNull(it)?.name } ?: value.jsonPrimitive.toString() - "repeat_mode" -> value.jsonPrimitive.intOrNull?.let { SpMsPlayerRepeatMode.values().getOrNull(it)?.name } ?: value.jsonPrimitive.toString() - "current_position_ms", "duration_ms" -> with (Duration) { - value.jsonPrimitive.longOrNull?.let { it.milliseconds }.toString() - } - else -> value.toString() - } -}