Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ internal sealed interface PresenceSnapshot {
val posterUrl: String?,
val isPlaying: Boolean,
val positionMs: Long,
val durationMs: Long,
) : PresenceSnapshot
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
posterUrl = runtime.poster,
isPlaying = playbackSnapshot.isPlaying,
positionMs = playbackSnapshot.positionMs,
durationMs = playbackSnapshot.durationMs,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import kotlinx.serialization.Serializable

@Serializable
internal data class DiscordActivity(
// Discord activity type: 0 = Playing, 2 = Listening, 3 = Watching, 5 = Competing.
// Sending 3 makes Discord show "Watching …" instead of the default "Playing …".
val type: Int = 0,
// Top line rendered under the username ("<verb> <name>"). When omitted, Discord uses the
// name of the application registered to the client id ("Nuvio"). Recent Discord clients honor
// a custom name here so the media title shows directly under the pseudo; older clients ignore
// it and fall back to the app name (that is why the title is also kept in `details`).
val name: String? = null,
val details: String? = null,
val state: String? = null,
val timestamps: DiscordActivityTimestamps? = null,
Expand All @@ -14,6 +22,8 @@ internal data class DiscordActivity(
@Serializable
internal data class DiscordActivityTimestamps(
val start: Long? = null,
// When both start and end are set, Discord renders a live progress bar with time remaining.
val end: Long? = null,
)

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,25 @@ private fun String.toDiscordEpisodeLabel(): String {
private fun PresenceSnapshot.toDiscordActivity(): DiscordActivity = when (this) {
is PresenceSnapshot.Tab -> DiscordActivity(details = "Browsing ${tab.name}")
is PresenceSnapshot.Details -> DiscordActivity(details = "Viewing $title")
is PresenceSnapshot.Player -> DiscordActivity(
details = if (isPlaying) "Watching: $title" else "Paused: $title",
state = episodeLabel?.toDiscordEpisodeLabel() ?: if (isPlaying) "Watching" else "Paused",
timestamps = if (isPlaying) {
// Discord expects Unix timestamps in seconds, not milliseconds.
DiscordActivityTimestamps(start = (System.currentTimeMillis() - positionMs) / 1_000L)
} else {
null
},
assets = posterUrl?.let { DiscordActivityAssets(largeImage = it, largeText = title) },
)
is PresenceSnapshot.Player -> {
val episode = episodeLabel?.toDiscordEpisodeLabel()
// Discord expects Unix timestamps in seconds, not milliseconds.
val startSecs = (System.currentTimeMillis() - positionMs) / 1_000L
DiscordActivity(
type = 3, // Watching -> "Watching …" instead of the default "Playing …" (game).
name = title, // Show the media title under the pseudo when the client honors it.
details = title, // Always keep the title here as a fallback for clients that ignore `name`.
state = if (isPlaying) episode else episode?.let { "$it • Paused" } ?: "Paused",
timestamps = if (isPlaying) {
// start + end -> Discord renders a live progress bar with time remaining.
DiscordActivityTimestamps(
start = startSecs,
end = if (durationMs > 0L) startSecs + durationMs / 1_000L else null,
)
} else {
null
},
assets = posterUrl?.let { DiscordActivityAssets(largeImage = it, largeText = title) },
)
}
}