mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
feat: add Discord Rich Presence integration for enhanced media playback visibility
This commit is contained in:
parent
16f10b5ba2
commit
74c345d53c
80
composeApp/Cargo.lock
generated
80
composeApp/Cargo.lock
generated
@ -41,7 +41,7 @@ dependencies = [
|
|||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -131,6 +131,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|||||||
name = "compose-app"
|
name = "compose-app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"discord-rich-presence",
|
||||||
"lofty",
|
"lofty",
|
||||||
"uniffi",
|
"uniffi",
|
||||||
]
|
]
|
||||||
@ -150,6 +151,21 @@ version = "2.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "discord-rich-presence"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90c55d69cab17c19677ce3a5f8face993a9e6eaf847fecac3547f3a3ff4a2494"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"thiserror",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@ -191,6 +207,17 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@ -282,7 +309,7 @@ checksum = "458ace39169e4b83c4f77ae3d42d5d1d11c422feef590219a97c973d3b524557"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -416,7 +443,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -456,7 +483,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -472,6 +499,17 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_repr"
|
||||||
|
version = "0.1.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.10"
|
version = "0.3.10"
|
||||||
@ -507,6 +545,17 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
@ -514,7 +563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom",
|
"getrandom 0.4.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
@ -546,7 +595,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -626,7 +675,7 @@ dependencies = [
|
|||||||
"indexmap",
|
"indexmap",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -641,7 +690,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
"toml",
|
"toml",
|
||||||
"uniffi_meta",
|
"uniffi_meta",
|
||||||
]
|
]
|
||||||
@ -683,6 +732,21 @@ dependencies = [
|
|||||||
"weedle2",
|
"weedle2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weedle2"
|
name = "weedle2"
|
||||||
version = "5.0.0"
|
version = "5.0.0"
|
||||||
|
|||||||
@ -6,6 +6,7 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
uniffi = "0.29.4"
|
uniffi = "0.29.4"
|
||||||
lofty = "0.24.0"
|
lofty = "0.24.0"
|
||||||
|
discord-rich-presence = "1.1.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "staticlib"]
|
crate-type = ["cdylib", "staticlib"]
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueueRepository
|
|||||||
import dev.krtirtho.spotube.core.audioplayer.DeviceAudioPlayerQueue
|
import dev.krtirtho.spotube.core.audioplayer.DeviceAudioPlayerQueue
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueStateRepository
|
import dev.krtirtho.spotube.core.audioplayer.QueueStateRepository
|
||||||
import dev.krtirtho.spotube.core.db.Database
|
import dev.krtirtho.spotube.core.db.Database
|
||||||
|
import dev.krtirtho.spotube.core.discord.DiscordRpcService
|
||||||
import dev.krtirtho.spotube.core.navigation.navigationModule
|
import dev.krtirtho.spotube.core.navigation.navigationModule
|
||||||
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
||||||
import dev.krtirtho.spotube.core.server.AlternativeTracksRepository
|
import dev.krtirtho.spotube.core.server.AlternativeTracksRepository
|
||||||
@ -172,6 +173,8 @@ val sharedModules = module {
|
|||||||
DeviceAudioPlayerQueue(get(), get(), get(), get())
|
DeviceAudioPlayerQueue(get(), get(), get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
single { DiscordRpcService(get(), get()) } withOptions { createdAtStart() }
|
||||||
|
|
||||||
factory { (tag: String?) ->
|
factory { (tag: String?) ->
|
||||||
if (tag != null) {
|
if (tag != null) {
|
||||||
Logger.withTag(tag)
|
Logger.withTag(tag)
|
||||||
|
|||||||
@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.krtirtho.spotube.core.discord
|
||||||
|
|
||||||
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface
|
||||||
|
import dev.krtirtho.spotube.core.audioplayer.PlayerState
|
||||||
|
import dev.krtirtho.spotube.core.di.injectLogger
|
||||||
|
import dev.krtirtho.spotube.modules.settings.SettingsProvider
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.IO
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import uniffi.compose_app.DiscordRpcClient
|
||||||
|
|
||||||
|
class DiscordRpcService(
|
||||||
|
private val audioPlayer: AudioPlayerInterface,
|
||||||
|
private val settingsProvider: SettingsProvider,
|
||||||
|
) : KoinComponent, AutoCloseable {
|
||||||
|
|
||||||
|
private val logger by injectLogger<DiscordRpcService>()
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
private var observerJob: Job? = null
|
||||||
|
|
||||||
|
private val clientId = "1176718791388975124"
|
||||||
|
private var rpcClient: DiscordRpcClient? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun start() {
|
||||||
|
logger.d { "Starting Discord RPC service" }
|
||||||
|
observerJob?.cancel()
|
||||||
|
observerJob = scope.launch {
|
||||||
|
combine(
|
||||||
|
settingsProvider.settingsState,
|
||||||
|
audioPlayer.playerStateFlow,
|
||||||
|
audioPlayer.currentMediaItemFlow,
|
||||||
|
audioPlayer.positionFlow,
|
||||||
|
audioPlayer.durationFlow,
|
||||||
|
) { settings, playerState, mediaItem, position, duration ->
|
||||||
|
DiscordRpcState(settings?.discordRichPresence == true, playerState, mediaItem, position, duration)
|
||||||
|
}.collect { state ->
|
||||||
|
handleStateChange(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleStateChange(state: DiscordRpcState) {
|
||||||
|
if (!state.enabled) {
|
||||||
|
disconnectClient()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (state.playerState) {
|
||||||
|
PlayerState.PLAYING -> {
|
||||||
|
val mediaItem = state.mediaItem ?: return
|
||||||
|
ensureConnected()
|
||||||
|
updatePresence(
|
||||||
|
title = mediaItem.title,
|
||||||
|
artist = mediaItem.artist,
|
||||||
|
album = mediaItem.album,
|
||||||
|
coverUrl = mediaItem.coverURL,
|
||||||
|
positionMs = state.position.inWholeMilliseconds,
|
||||||
|
durationMs = state.duration.inWholeMilliseconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PlayerState.PAUSED -> {
|
||||||
|
val mediaItem = state.mediaItem ?: return
|
||||||
|
ensureConnected()
|
||||||
|
updatePresence(
|
||||||
|
title = mediaItem.title,
|
||||||
|
artist = mediaItem.artist,
|
||||||
|
album = mediaItem.album,
|
||||||
|
coverUrl = mediaItem.coverURL,
|
||||||
|
positionMs = state.position.inWholeMilliseconds,
|
||||||
|
durationMs = state.duration.inWholeMilliseconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PlayerState.IDLE, PlayerState.COMPLETED -> {
|
||||||
|
clearPresence()
|
||||||
|
}
|
||||||
|
PlayerState.BUFFERING, PlayerState.READY -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureConnected() {
|
||||||
|
if (rpcClient != null) return
|
||||||
|
try {
|
||||||
|
val client = DiscordRpcClient(clientId)
|
||||||
|
client.connect()
|
||||||
|
rpcClient = client
|
||||||
|
logger.i { "Connected to Discord RPC" }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.e(e) { "Failed to connect to Discord RPC" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePresence(
|
||||||
|
title: String,
|
||||||
|
artist: String,
|
||||||
|
album: String,
|
||||||
|
coverUrl: String,
|
||||||
|
positionMs: Long,
|
||||||
|
durationMs: Long,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
rpcClient?.updatePresence(
|
||||||
|
title = title,
|
||||||
|
artist = artist,
|
||||||
|
album = album,
|
||||||
|
coverUrl = coverUrl,
|
||||||
|
positionMs = positionMs,
|
||||||
|
durationMs = durationMs,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.e(e) { "Failed to update Discord RPC presence" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearPresence() {
|
||||||
|
try {
|
||||||
|
rpcClient?.clearPresence()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.e(e) { "Failed to clear Discord RPC presence" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disconnectClient() {
|
||||||
|
try {
|
||||||
|
rpcClient?.disconnect()
|
||||||
|
rpcClient = null
|
||||||
|
logger.d { "Disconnected from Discord RPC" }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.e(e) { "Failed to disconnect from Discord RPC" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
observerJob?.cancel()
|
||||||
|
disconnectClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class DiscordRpcState(
|
||||||
|
val enabled: Boolean,
|
||||||
|
val playerState: PlayerState,
|
||||||
|
val mediaItem: dev.krtirtho.spotube.core.audioplayer.MediaItem?,
|
||||||
|
val position: kotlin.time.Duration,
|
||||||
|
val duration: kotlin.time.Duration,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,9 +1,13 @@
|
|||||||
|
use discord_rich_presence::activity::{Activity, ActivityType, Assets, Timestamps};
|
||||||
|
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
|
||||||
use lofty::config::WriteOptions;
|
use lofty::config::WriteOptions;
|
||||||
use lofty::file::{AudioFile, FileType, TaggedFileExt};
|
use lofty::file::{AudioFile, FileType, TaggedFileExt};
|
||||||
use lofty::picture::Picture;
|
use lofty::picture::Picture;
|
||||||
use lofty::probe::Probe;
|
use lofty::probe::Probe;
|
||||||
use lofty::tag::{Accessor, Tag, TagExt};
|
use lofty::tag::{Accessor, Tag, TagExt};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
#[derive(uniffi::Record)]
|
#[derive(uniffi::Record)]
|
||||||
pub struct AudioMetadata {
|
pub struct AudioMetadata {
|
||||||
@ -157,4 +161,138 @@ fn write_audio_metadata(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, uniffi::Error)]
|
||||||
|
pub enum DiscordRpcError {
|
||||||
|
ConnectionError { reason: String },
|
||||||
|
UpdateError { reason: String },
|
||||||
|
NotConnected,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DiscordRpcError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
DiscordRpcError::ConnectionError { reason } => write!(f, "Connection error: {}", reason),
|
||||||
|
DiscordRpcError::UpdateError { reason } => write!(f, "Update error: {}", reason),
|
||||||
|
DiscordRpcError::NotConnected => write!(f, "Not connected"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(uniffi::Object)]
|
||||||
|
pub struct DiscordRpcClient {
|
||||||
|
client_id: String,
|
||||||
|
client: Mutex<Option<DiscordIpcClient>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
impl DiscordRpcClient {
|
||||||
|
#[uniffi::constructor]
|
||||||
|
pub fn new(client_id: String) -> Self {
|
||||||
|
Self {
|
||||||
|
client_id,
|
||||||
|
client: Mutex::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect(&self) -> Result<(), DiscordRpcError> {
|
||||||
|
let mut client_guard = self.client.lock().unwrap();
|
||||||
|
if client_guard.is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut client = DiscordIpcClient::new(&self.client_id);
|
||||||
|
|
||||||
|
client
|
||||||
|
.connect()
|
||||||
|
.map_err(|e| DiscordRpcError::ConnectionError { reason: e.to_string() })?;
|
||||||
|
|
||||||
|
*client_guard = Some(client);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disconnect(&self) -> Result<(), DiscordRpcError> {
|
||||||
|
let mut client_guard = self.client.lock().unwrap();
|
||||||
|
if let Some(mut client) = client_guard.take() {
|
||||||
|
client
|
||||||
|
.close()
|
||||||
|
.map_err(|e| DiscordRpcError::ConnectionError { reason: e.to_string() })?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_presence(
|
||||||
|
&self,
|
||||||
|
title: String,
|
||||||
|
artist: String,
|
||||||
|
album: String,
|
||||||
|
cover_url: String,
|
||||||
|
position_ms: i64,
|
||||||
|
duration_ms: i64,
|
||||||
|
) -> Result<(), DiscordRpcError> {
|
||||||
|
let mut client_guard = self.client.lock().unwrap();
|
||||||
|
let client = client_guard
|
||||||
|
.as_mut()
|
||||||
|
.ok_or(DiscordRpcError::NotConnected)?;
|
||||||
|
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs() as i64;
|
||||||
|
|
||||||
|
let start = now - (position_ms / 1000);
|
||||||
|
let end = start + (duration_ms / 1000);
|
||||||
|
|
||||||
|
let details = if title.is_empty() {
|
||||||
|
"Unknown Track".to_string()
|
||||||
|
} else {
|
||||||
|
title
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = if artist.is_empty() {
|
||||||
|
"Unknown Artist".to_string()
|
||||||
|
} else {
|
||||||
|
format!("by {}", artist)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut activity = Activity::new()
|
||||||
|
.details(&details)
|
||||||
|
.state(&state)
|
||||||
|
.timestamps(Timestamps::new().start(start).end(end))
|
||||||
|
.activity_type(ActivityType::Listening);
|
||||||
|
|
||||||
|
if !album.is_empty() {
|
||||||
|
let mut assets = Assets::new().large_text(&album);
|
||||||
|
if !cover_url.is_empty() {
|
||||||
|
assets = assets.large_image(&cover_url);
|
||||||
|
}
|
||||||
|
activity = activity.assets(assets);
|
||||||
|
} else if !cover_url.is_empty() {
|
||||||
|
activity = activity.assets(Assets::new().large_image(&cover_url));
|
||||||
|
}
|
||||||
|
|
||||||
|
client
|
||||||
|
.set_activity(activity)
|
||||||
|
.map_err(|e| DiscordRpcError::UpdateError { reason: e.to_string() })?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_presence(&self) -> Result<(), DiscordRpcError> {
|
||||||
|
let mut client_guard = self.client.lock().unwrap();
|
||||||
|
let client = client_guard
|
||||||
|
.as_mut()
|
||||||
|
.ok_or(DiscordRpcError::NotConnected)?;
|
||||||
|
|
||||||
|
client
|
||||||
|
.clear_activity()
|
||||||
|
.map_err(|e| DiscordRpcError::UpdateError { reason: e.to_string() })?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_connected(&self) -> bool {
|
||||||
|
self.client.lock().unwrap().is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uniffi::setup_scaffolding!();
|
uniffi::setup_scaffolding!();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user