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,