diff --git a/config/discord-local.json.example b/config/discord-local.json.example
new file mode 100644
index 00000000..c2e84507
--- /dev/null
+++ b/config/discord-local.json.example
@@ -0,0 +1,11 @@
+[
+ {
+ "name": "MS",
+ "enable": true,
+ "data": {
+ "applicationId": "8190211179716453570",
+ "ipcLocations": ["192.168.0.105:6655"]
+ }
+ }
+ ]
+
\ No newline at end of file
diff --git a/docsite/docs/configuration/clients/discord.mdx b/docsite/docs/configuration/clients/discord/discord-headless.mdx
similarity index 99%
rename from docsite/docs/configuration/clients/discord.mdx
rename to docsite/docs/configuration/clients/discord/discord-headless.mdx
index 8d747b9a..96c0b5d7 100644
--- a/docsite/docs/configuration/clients/discord.mdx
+++ b/docsite/docs/configuration/clients/discord/discord-headless.mdx
@@ -1,18 +1,19 @@
---
-title: Discord
+title: Discord (Headless)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
-import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/discord.json.example';
This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
+This integration is **headless**, it does not depend on any specific Discord application to be running.
+
:::warning[Discord TOS Violation]
This functionality requires you to use your own **User Token** in a way that is **against Discord's policies/terms of service.** If you use this scrobbler you do so **at your risk**.
diff --git a/docsite/docs/configuration/clients/discord/discord-local.mdx b/docsite/docs/configuration/clients/discord/discord-local.mdx
new file mode 100644
index 00000000..9c2813a3
--- /dev/null
+++ b/docsite/docs/configuration/clients/discord/discord-local.mdx
@@ -0,0 +1,234 @@
+---
+title: Discord (Local)
+toc_min_heading_level: 2
+toc_max_heading_level: 5
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import JsonConfig from '!!raw-loader!@site/../config/discord-local.json.example';
+
+This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
+
+
+
+This integration uses an **existing Discord application** to set your Rich Presence.
+
+## Required Setup
+
+### Discord Application
+
+You must create a [Discord App](https://docs.discord.com/developers/quick-start/overview-of-apps) in order to use this integration. Creating an App is free and simple.
+
+Go to the [Discord Developer Portal](https://discord.com/developers/applications)
+
+* Click on "New Application" and give it a name
+* Copy the **Application Id** (Client Id) shown after creation
+* Add this Id to your Multi-Scrobbler config
+ * Env Config => `DISCORD_APPLICATION_ID=12345678`
+ * File Config => `"applicationId": "12345678"`
+
+### Discord Connection
+
+This integration uses an existing, running Discord application to communicate with Discord and set Rich Presence. Multi-scrobbler must be able to communicate with the Discord application, either on the same machine or over a network.
+
+
+
+
+
+In this scenario you have Multi-Scrobbler running on the same machine as Discord.
+
+
+
+Local (Non-Docker) Installation
+
+If MS was [installed locally](/installation#local-installation) (not Docker) then MS should automatically detect Discord. This will work for **any OS**.
+
+
+
+
+
+
+Docker Installation
+
+:::note
+
+This works for **Linux/MacOS only.**
+
+:::
+
+If MS is in a docker container you need to find the location of **unix socket** for discord. This is normally named `discord-ipc-0`.
+
+With Discord running, run this command:
+
+```
+$ sudo netstat -apeen | grep discord
+
+unix 2 [ ACC ] STREAM LISTENING 11026149 1641787/Discord /home/foxx/.var/app/com.discordapp.Discord/cache/scoped_dirnnxNIn/SingletonSocket
+unix 2 [ ACC ] STREAM LISTENING 11036791 1642030/exe /run/user/1000/discord-ipc-0
+```
+
+We see that the path we need is
+
+```
+/run/user/1000/discord-ipc-0
+```
+
+Pass this file into your docker container as a volume and add this path to `DISCORD_IPC_LOCATIONS` `environment:`
+
+```yaml title="docker-compose.yaml"
+
+ environment:
+ # ...
+ DISCORD_IPC_LOCATIONS=/run/user/1000/discord-ipc-0
+ volumes:
+ - /run/user/1000/discord-ipc-0:/run/user/1000/discord-ipc-0
+```
+
+
+
+
+
+
+:::note
+
+This works for **Linux/MacOS only.**
+
+:::
+
+
+:::warning
+
+This method is **insecure.** You should not expose this TCP port over a network unless access to the port is restricted and the network is trusted.
+
+:::
+
+On the machine Discord is running on you need to find the location of **unix socket** for discord. This is normally named `discord-ipc-0`.
+
+With Discord running, run this command:
+
+```
+$ sudo netstat -apeen | grep discord
+
+unix 2 [ ACC ] STREAM LISTENING 11026149 1641787/Discord /home/foxx/.var/app/com.discordapp.Discord/cache/scoped_dirnnxNIn/SingletonSocket
+unix 2 [ ACC ] STREAM LISTENING 11036791 1642030/exe /run/user/1000/discord-ipc-0
+```
+
+We see that the path we need is
+
+```
+/run/user/1000/discord-ipc-0
+```
+
+Use [`socat`](https://linux.die.net/man/1/socat) to bidirectionally relay communication from Discord's unix socket to a listening TCP port. Likely, you will want to setup a service to start this command on login.
+
+```shell
+$ socat -v TCP-LISTEN:6655,reuseaddr,fork UNIX-CONNECT:/run/user/1000/discord-ipc-0
+```
+
+Finally, in your MS docker compose file add the `host:port` of the host running Discord to `DISCORD_IPC_LOCATIONS` `environment:`
+
+```yaml title="docker-compose.yaml"
+
+ environment:
+ # ...
+ DISCORD_IPC_LOCATIONS=192.168.DISCORD_HOST.IP:6655
+```
+
+
+
+
+## Optional Setup
+
+:::note
+
+Due to the limitations of the Discord local API the activity detection and online status customizations found in [Headless](/configuration/discord/discord-headless#optional-setup) are not available for the Local integration.
+
+:::
+
+### Artwork
+
+Multi-scrobbler can display Album Art in your Discord status. The image that is used differs based on if the image URL is publically accessible.
+
+The order in which multi-scrobbler determines what image to use is the same as the order of the tabs shown below.
+
+#### Artwork URL Types
+
+
+
+MS can use external URLs for artwork.
+
+
+
+External URLs that are not from known music services are **disabled by default.** This is to preserve *your* privacy because Discord does not host images, it only links to them and the URL is visible to other users.
+
+
+
+Why Should I Care?
+
+There are some scenarios where you may not want your artwork URLs to be visible, for example:
+
+* A Source may be accessible only on your LAN (Jellyfin at `http://192.168.0.101`)
+* A Source may be internet-facing but you do not want to expose this information to discord/other users
+* A Source may be internet-facing but requires authentication to view the image
+
+
+MS will use your artwork URLs only if:
+
+* artwork url is from a known music service/[Cover Art Archive](./?artworkUrl=caa#artwork-url-types) or
+* artwork url starts with `https` and
+ * `artwork` (`DISCORD_ARTWORK`) is
+ * `true` => always uses the url
+ * a list of custom domains keywords an external artwork url should contain EX
+ * ENV => `DISCORD_ARTWORK=mycdn,jellyfin`
+ * File Config => `"artwork": ["mycdn","jellyfin"]`
+
+
+
+MS can try to use [Cover Art Archive](http://coverartarchive.org/) to get album art. The URL it retrieves is public which means it acts like an External URL that is always available for you.
+
+
+
+MS will try to use Cover Art Archive if:
+
+* The scrobble data does not contain an album art url (no art in dashboard) or the existing url has failed an [External URL](./?artworkUrl=external#artwork-url-types) condition
+* and **the scrobble data contains a [Musicbrainz](/configuration/transforms/musicbrainz) Release (Album) MBID**
+
+Basically, if MS would normally use the [MS Default](./?artworkUrl=ms-default#artwork-url-types) art, it will first try to use Cover Art Archive if the scrobble has a Release MBID.
+
+Some Source, like [Jellyfin](/configuration/sources/jellyfin) and [Plex](/configuration/sources/plex), can automatically provide this ID if the music is "matched" in your library.
+
+For all others, you can [**configure MS to use the Musicbrainz Stage**](/configuration/transforms/musicbrainz#use-metadata-for-discord-album-art) for Discord to try to get this MBID.
+
+
+When these conditions are true:
+
+* Cannot use an [External URL](./?artworkUrl=external#artwork-url-types) and
+* Cannot get album art from [Cover Art Archive](./?artworkUrl=caa#artwork-url-types) and
+* Play does not contain artwork information (no artwork shown in dashboard)
+
+The status artwork will use a default URL instead of your artwork URL.
+
+Without any other configuration, the default URL is an icon from the [multi-scrobbler repository](https://github.com/FoxxMD/multi-scrobbler/blob/master/assets/default-artwork.png):
+
+
+
+This default image can be customized:
+
+* ENV Config => `DISCORD_ARTWORK_DEFAULT_URL=https://cooldomain.com/art.png`
+* File Config => `"artworkDefaultUrl": "https://cooldomain.com/art.png"`
+
+Remember, this URL need to be accessible on the public internet.
+
+
+
+## Configuration
+
+
+ | Environmental Variable | Required? | Default | Description |
+ | :---------------------------- | --------- | ------- | :--------------------------------------------------------------------------------------------------------------- |
+ | `DISCORD_APPLICATION_ID` | Yes | | Application ID |
+ | `DISCORD_IPC_LOCATIONS` | No | | A commera-separated list of `host:port` network locations or file paths to unix socket for Discord |
+ | `DISCORD_ARTWORK` | No | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
+ | `DISCORD_ARTWORK_DEFAULT_URL` | No | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
+
\ No newline at end of file
diff --git a/docsite/docs/configuration/clients/discord/discord.mdx b/docsite/docs/configuration/clients/discord/discord.mdx
new file mode 100644
index 00000000..3f56c0f6
--- /dev/null
+++ b/docsite/docs/configuration/clients/discord/discord.mdx
@@ -0,0 +1,35 @@
+---
+sidebar_position: 2
+title: Discord
+toc_max_heading_level: 4
+---
+
+This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
+
+
+
+There are two types of Discord integrations to choose from:
+
+## Local
+
+The **Local** integration uses an **existing** Discord application on Linux/MacOS to set Rich Presence.
+
+* Uses "permitted" access to Discord, no TOS issues
+* More restricted customization compares to Headless
+* Only active when your existing Discord application is open
+* For instances where Multi-Scrobbler is not on the same machine as Discord
+ * Requires an additional program to relay the Discord connection
+ * This is potentially insecure, or less secure, then Headless
+
+[**Local Docs**](#/configuration/client/discord/discord-local)
+
+## Headless
+
+The **Headless** integration uses Discord cloud Gateway/API, directly, to set Rich Presence.
+
+* Does not require an existing Discord application (uses Discord cloud API)
+* Using this **Violates Discord TOS** because it requires a user token
+* More customization than Local
+* Active if **any** Discord application is online
+
+[**Headless Docs**](#/configuration/client/discord/discord-headless)
\ No newline at end of file
diff --git a/src/backend/common/infrastructure/config/client/discord.ts b/src/backend/common/infrastructure/config/client/discord.ts
index 3feb971a..5fea4ecf 100644
--- a/src/backend/common/infrastructure/config/client/discord.ts
+++ b/src/backend/common/infrastructure/config/client/discord.ts
@@ -8,7 +8,7 @@ export interface DiscordData {
statusOverrideAllow?: string | StatusType[]
activitiesOverrideAllow?: boolean | string | ActivityType[]
applicationsOverrideDisallow?: string | string[]
- ipcLocations?: (string | [number, string])[]
+ ipcLocations?: string | (string | [number, string])[]
}
export interface DiscordClientData extends DiscordData, CommonClientData {}
@@ -37,6 +37,7 @@ export interface DiscordStrongData extends DiscordData {
statusOverrideAllow?: StatusType[]
activitiesOverrideAllow?: ActivityType[]
applicationsOverrideDisallow?: string[]
+ ipcLocations?: (string | [number, string])[]
}
export interface DiscordWSData extends DiscordStrongData {
diff --git a/src/backend/common/vendor/discord/DiscordWSClient.ts b/src/backend/common/vendor/discord/DiscordWSClient.ts
index d83c05dc..727c919b 100644
--- a/src/backend/common/vendor/discord/DiscordWSClient.ts
+++ b/src/backend/common/vendor/discord/DiscordWSClient.ts
@@ -900,7 +900,6 @@ export const configToStrong = (data: DiscordData): DiscordStrongData => {
applicationId,
applicationsOverrideDisallow: parseArrayFromMaybeString(applicationsOverrideDisallow),
artworkDefaultUrl,
- ipcLocations
}
if (typeof artwork === 'boolean' || Array.isArray(artwork)) {
@@ -923,6 +922,15 @@ export const configToStrong = (data: DiscordData): DiscordStrongData => {
strongConfig.activitiesOverrideAllow = aaRaw.map(activityStringToType);
}
+ if(ipcLocations !== undefined) {
+ if(typeof ipcLocations === 'string') {
+ const ipcRaw = parseArrayFromMaybeString(ipcLocations);
+ strongConfig.ipcLocations = ipcRaw;
+ } else {
+ strongConfig.ipcLocations = ipcLocations;
+ }
+ }
+
return strongConfig;
}
diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts
index df951aca..625ca28a 100644
--- a/src/backend/scrobblers/ScrobbleClients.ts
+++ b/src/backend/scrobblers/ScrobbleClients.ts
@@ -302,6 +302,7 @@ export default class ScrobbleClients {
token: process.env.DISCORD_TOKEN,
artwork: process.env.DISCORD_ARTWORK,
applicationId: process.env.DISCORD_APPLICATION_ID,
+ ipcLocations: process.env.DISCORD_IPC_LOCATIONS,
artworkDefaultUrl: process.env.DISCORD_ARTWORK_DEFAULT_URL,
statusOverrideAllow: process.env.DISCORD_STATUS_OVERRIDE_ALLOW,
activitiesOverrideAllow: process.env.DISCORD_ACTIVITIES_OVERRIDE_ALLOW,