A lightweight, cross-platform system-tray companion for Shoko Server that plays your anime in mpv, reports watch progress back to the server, (optionally) integrates with the Media Session plugin for remote playback control, and (optionally) shows what you're watching as Discord Rich Presence.
- Binary:
shoko-companion - Display name: Shoko Companion
- Framework: .NET 10 · Avalonia 11
- Platforms: Windows (x64), Linux (x64 / arm64), macOS (arm64)
- Repository: github.com/ShokoAnime/Shoko.Companion
- Plays in mpv — catches
shoko://URLs, launches mpv, plays the stream. - Playback events — periodic position updates, start/pause/resume/stop events, auto-marks watched at ≥ 97.5%.
Play(new media) andResume(unpause current) are separate commands. - Resume support — pre-fetches resume position and seeks mpv on file load.
- Server connections — multiple connections with route fallback; auto-discovered from the
shoko://URLs or manually configured. - Managed folder mappings — resolves "Open Folder" actions from the web UI to local paths.
- Discord Rich Presence — optionally shows series/episode info in Discord.
- Cross-platform tray app — Windows, Linux, macOS. Native notifications.
When the Media Session plugin is installed on the Shoko server, the companion can connect to its SignalR hub for remote playback control. This enables:
- Other devices/clients for the user to see what's playing and send play/pause/seek/stop commands
- Live state updates pushed via SignalR
- Per-connection auto-connect with manual Connect/Disconnect in settings
Registering a session with the plugin is consent to the server writing watch state from the session's own state reports. So the two writers take turns rather than both running:
- While a media session is connected, this companion's own scrobbling is off. It stops the moment the session registers, mid-item included — the server has been watching the same playback through the state reports since it registered, so it can finish a record it takes over.
- When the session goes away, scrobbling resumes at the next item, never mid-item. A watch record describes one whole viewing; taking one over at 80% would mark it watched on the strength of the last 20%. The item playing when the session went away therefore syncs to whatever the server last wrote and no further, which is correct — the server owned that viewing.
- "Gone" means the hub client gave up its session, not that a socket blinked: the connection retries indefinitely and reclaims its session id, so a drop that reconnects changes nothing.
Both ends of the handover are logged (PlaybackSessionManager), because a
handover that goes wrong is otherwise invisible until the watch state is
already wrong.
| Requirement | Notes |
|---|---|
| A running Shoko Server | v3 API reachable from this machine. |
| mpv | Must be installed and discoverable (in PATH or a common install location). mpv.io |
| .NET 10 runtime | Only if you run the framework-dependent build. |
| Discord desktop client | Only if you enable Rich Presence. |
# from the repo root
dotnet build Shoko.Companion/Shoko.Companion.csproj
# run (framework-dependent)
dotnet run --project Shoko.Companion/Shoko.Companion.csproj# Self-contained single-file (bundles .NET runtime)
dotnet publish Shoko.Companion/Shoko.Companion.csproj -c Release -r win-x64 -p:PublishSingleFile=true -p:SelfContained=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true
# Linux x64 / arm64 / macOS arm64 (change -r accordingly)
dotnet publish ... -r linux-x64 -p:PublishSingleFile=true -p:SelfContained=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true
dotnet publish ... -r linux-arm64 -p:PublishSingleFile=true -p:SelfContained=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true
dotnet publish ... -r osx-arm64 -p:PublishSingleFile=true -p:SelfContained=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=trueAvalonia extracts its native libraries (Skia/HarfBuzz) to a temp directory on first run — this is expected.
Settings live in a JSON file under a per-platform config root:
| Platform | Location |
|---|---|---|
| Windows | %APPDATA%\shoko-companion\settings.json |
| macOS | ~/Library/Application Support/shoko-companion/settings.json |
| Linux | $XDG_CONFIG_HOME/shoko-companion/settings.json (falls back to ~/.config/shoko-companion/settings.json if XDG_CONFIG_HOME is not set) |
Logs are written next to it under logs/.
Relocate the entire config/log root two ways (precedence: --home → env var → platform default):
# --home argument
shoko-companion --home /path/to/home
# --force: bypass the running-instance check (e.g. after a crash left a stale lock file)
shoko-companion --force
# SHOKO_COMPANION_HOME environment variable
SHOKO_COMPANION_HOME=/path/to/dev-home dotnet run --project Shoko.Companion/Shoko.Companion.csproj| Key | Type | Default | Description |
|---|---|---|---|
Connections |
array | [] |
List of server connections (see below). |
MpvPath |
string | null |
Path to the mpv binary. Auto-discovered and saved on first use. |
IsFullscreen |
bool | true |
Saved current fullscreen state. Restored when mpv connects and persisted on every change. Replaces the old MpvFullScreen launch toggle. |
OnNewUrlAction |
string | "Append" |
When a new shoko: URL arrives while playing: "Replace" (stop + start new), "Ignore" (silently discard), or "Append" (add to mpv playlist). |
PlaybackSyncingEnabled |
bool | true |
Master toggle for the playback event syncing this companion does itself (start/end/pause/resume). Applies only while no media session is connected — see Who writes the watch state. |
PlaybackSyncingBehavior |
enum | AfterPlayback |
How aggressively to sync playback events: AfterPlayback (stop only), OnEveryEvent (play/pause/resume/stop), LiveSync (everything + periodic progress). |
MediaSessionAutoConnectId |
Guid | null |
Server connection ID to auto-connect for the Media Session SignalR hub. Null disables auto-connect. |
SyncUserDataInitialSkipEventCount |
int | 3 |
Number of initial non-pause events to skip after starting, letting the player settle. |
SyncUserDataLiveScrobbleTickThreshold |
int | 3 |
Number of position events accumulated before sending a live progress update. |
SyncUserDataLivePositionThresholdMs |
int | 5000 |
Minimum position change (ms) required to trigger a live progress update. |
PrivacyModeForRestrictedContent |
bool | false |
Auto-activate privacy mode for restricted content. |
ScrobbleIntervalMs |
int | 10000 |
How often to send progress updates (min 5000). |
DiscordEnabled |
bool | false |
Enable Discord Rich Presence. |
DiscordClientIdOverride |
string | null |
Override for the built-in Discord app ID. |
DiscordIdlePresence |
bool | false |
Show "Browsing" → "Idle" presence when nothing is playing. |
PrivacyMode |
bool | false |
Global privacy mode master switch. Sent to the Media Session plugin as PrivacyModeEnabled, which is what makes the server hide what this device plays from every other viewer and stop recording it. Turning it on mid-session reaches the server and privatises what is already queued. |
PrivacyModeHideDiscord |
bool | false |
Hide anime title and poster from Discord presence; show generic "Watching Anime" instead. Only effective when PrivacyMode is true. Local only — no server setting expresses rich presence. |
PrivacyModeDisableRemoteScreenshots |
bool | false |
Disallow remote screenshot capture via Media Session API. |
PrivacyModeDisablePlaybackEvents |
bool | false |
Disable all scrobble/sync events to the Shoko server. Also sent as DisablePlaybackEventSyncing, so it holds whether or not a Media Session is connected — the two scrobblers take turns, and telling only one leaves the other writing. |
PrivacyModeForRestrictedContent |
bool | false |
When true, restricted (adult) content automatically activates privacy mode. Also sent as AlwaysUsePrivacyModeForRestrictedContent, where the server rules what counts as restricted and applies it to items added after it, never backwards into the queue. |
PrivacyModeMpvKeybinding |
string | Ctrl+p |
mpv keybinding to toggle privacy mode during playback. Sent via JSON IPC; no Lua scripts needed. |
MediaSessionEnabled |
bool | false |
Global enabled switch for Media Session API integration. When disabled, auto-connect won't run and sub-controls are greyed out. |
ScreenshotSubtitleBehavior |
enum | OnlyWhenPaused |
Controls subtitle visibility during screenshots: Disabled (subs always visible), OnlyWhenPaused (hide subs only when paused — avoids visual flicker), Always (hide subs on every capture). |
AlwaysUseConfiguredRoutes |
bool | false |
Skip direct URL reachability check; always use the connection's route table. |
LogLevel |
string | "Info" |
One of: Trace, Debug, Info, Warn, Error. |
Each connection object in Connections:
| Key | Type | Default | Description |
|---|---|---|---|
Name |
string | "" |
Display name for the connection (user-editable). |
Routes |
array | [] |
Ordered list of route objects (see below). Probed in order. |
ApiKey |
string | null |
API key for this server. |
IgnoredManagedFolderIds |
array | [] |
Managed folder IDs the user has silenced. |
ManagedFolderMappings |
array | [] |
Local path mappings for the server's managed folders. |
CachedManagedFolders |
array | null |
Cached folder metadata from the server (not persisted). |
Each route object in Routes:
| Key | Type | Default | Description |
|---|---|---|---|
BaseUrl |
string | "" |
Host, port, and optional sub-path (e.g. server:8111 or 192.168.1.5:8111/subpath). No protocol prefix. |
UseHttps |
bool | false |
Whether to connect via HTTPS. |
You can edit settings.json by hand (the app hot-reloads it) or via the tray menu.
shoko: URL ──▶ DispatchUrl ──┬─▶ PlaybackCoordinator
│ │
│ ┌─────┼────────┬──────────────┐
│ ▼ ▼ ▼ ▼
│ Api Mpv Discord Notifications
│ Client IPC Presence
│
└─▶ FolderActionHandler
│
▼
File Manager
DispatchUrl(inApp) parses the incoming URL and routesplaytoPlaybackCoordinatorandopen-foldertoFolderActionHandler.PlaybackCoordinatororchestrates everything: resolves the playlist JSON, pre-fetches resume position, forwards the stream URL to mpv, observes mpv state, and syncs playback events.MpvIpcClientspeaks mpv's JSON IPC and raises typed property/event callbacks.ShokoApiClientwraps the v3 endpoints (/api/auth,/api/v3/Playlist/Generate,/api/v3/File/{id}/UserData,/api/v3/File/{id}/Scrobble).ShokoUrlParserhandles both legacy and new URL formats.FolderActionHandlerresolves managed folder IDs to local paths and opens the OS file manager.SingleInstanceManagerensures only one instance runs and forwards URLs to it.
See USAGE.md for the step-by-step user guide.
The repo ships VS Code targets (.vscode/launch.json):
- Launch Shoko.Companion — plain run (shows the connection setup window on first run).
- Launch Shoko.Companion (with URL) — passes a sample
shoko:URL so you can step through the full flow.
Both use SHOKO_COMPANION_HOME=${workspaceFolder}/companion-dev so debugging stays isolated from your real config.
With the debug companion running, right-click the tray icon and choose
Register URL Scheme. This registers the debug executable (with its
--home <dev-home> baked in) as the shoko:// handler.
Whenever a shoko:// link is clicked in your browser, the registered
handler launches a secondary instance that immediately forwards the URL
to the running debug session via the named pipe and exits — a lightweight
trampoline. If nothing is running, one starts up.
When you're done, unregister to restore the production handler — either from the tray menu or the command line:
dotnet run --project Shoko.Companion/Shoko.Companion.csproj -- unregisterRun the tests:
dotnet test Shoko.Companion.Tests/Shoko.Companion.Tests.csprojShoko Companion would not exist without these amazing projects:
- Shoko Server — the anime metadata server that powers the entire ecosystem. This companion is just a sidekick.
- mpv — the best media player, with a beautiful JSON IPC that makes programmatic control a joy.
- Avalonia — cross-platform UI framework that lets us target Windows, Linux, and macOS from one codebase. Underpinned by Skia and HarfBuzz.
- DiscordRichPresence by Lachee — clean C# bindings for Discord's Rich Presence SDK.
- NLog — structured logging that matches the server's JSONL format.
- Newtonsoft.Json — the de facto JSON library for .NET.
- xunit and Moq — testing and mocking, keeping things solid.
- .NET — the runtime and ecosystem that makes cross-platform desktop apps viable.
MIT © Shoko