13 Commits

Author SHA1 Message Date
atridad b5e9822626 Oopsie! 2026-04-02 16:35:18 -06:00
atridad 2b096b23ff Updated to 1.1.0 2026-04-02 16:00:14 -06:00
atridad dc64de825c O_O 2026-04-02 15:59:19 -06:00
atridad 6c051734cc Remove tests until I can figure out why it hangs 2026-04-02 15:57:50 -06:00
atridad 7de0568bc7 Update .gitignore
Test / Test (push) Has been cancelled
2026-04-02 15:56:37 -06:00
atridad 383789a6a5 Remove .devenv 2026-04-02 15:56:17 -06:00
atridad a903d6cdea Merge pull request 'Custom Activity Name Templates' (#23) from atridadl/main into main
Test / Test (push) Has been cancelled
Reviewed-on: #23
2026-04-02 15:48:21 -06:00
atridad fa381fbc83 Merge
Test / Test (pull_request) Has been cancelled
2026-04-02 15:46:58 -06:00
atridad 87366781f5 Re-branding the fork
Test / Test (push) Failing after 5m27s
2026-04-02 13:01:16 -06:00
atridad 41cf2971c1 Merge branch 'main' into main 2026-04-02 15:34:34 +00:00
Deluan Quintão 4e0f98aa51 Update Navidrome version requirement to 0.61.0 2026-03-31 19:34:06 -04:00
atridad 414021f471 Addressing gemini's suggestion 2026-03-09 23:28:41 -06:00
atridad 24fb4cf752 added custom activity name template 2026-03-09 18:01:58 -06:00
15 changed files with 209 additions and 225 deletions
+1
View File
@@ -0,0 +1 @@
use flake
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

-54
View File
@@ -1,54 +0,0 @@
name: Build
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install TinyGo
run: |
wget https://github.com/tinygo-org/tinygo/releases/download/v0.40.1/tinygo_0.40.1_amd64.deb
sudo dpkg -i tinygo_0.40.1_amd64.deb
sudo apt install -y binaryen
- name: Run tests
run: make test
- name: Append PR info to version
if: github.event_name == 'pull_request'
run: |
PR_NUM=${{ github.event.pull_request.number }}
SHA=$(echo "${{ github.event.pull_request.head.sha }}" | cut -c1-7)
SUFFIX="PR${PR_NUM}-${SHA}"
jq --arg suffix "$SUFFIX" '.version = .version + "-" + $suffix' manifest.json > manifest.tmp && mv manifest.tmp manifest.json
- name: Append git SHA to version
if: github.event_name == 'push'
run: |
SHA=$(echo "${{ github.sha }}" | cut -c1-7)
jq --arg sha "$SHA" '.version = .version + "-" + $sha' manifest.json > manifest.tmp && mv manifest.tmp manifest.json
- name: Build and package plugin
run: make package
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: discord-rich-presence
path: discord-rich-presence.ndp
-89
View File
@@ -1,89 +0,0 @@
name: Create Release
on:
workflow_dispatch:
inputs:
version:
description: "Release version (e.g., 1.2.3, without the 'v' prefix)"
required: true
type: string
beta:
description: "Beta number (1, 2, 3...). Leave empty for stable release"
required: false
type: string
default: ""
permissions:
contents: write
jobs:
create-release:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Validate version format
env:
VERSION: ${{ inputs.version }}
run: |
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid version format '$VERSION'. Use X.X.X (e.g., 1.2.3)"
exit 1
fi
- name: Compute full version
run: |
VERSION="${{ inputs.version }}"
BETA="${{ inputs.beta }}"
if [[ -n "$BETA" && "$BETA" != "0" ]]; then
VERSION="${VERSION}-beta-${BETA}"
fi
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
- name: Check out code
uses: actions/checkout@v5
- name: Check tag does not already exist
run: |
if git ls-remote --tags origin "refs/tags/v${VERSION}" | grep -q .; then
echo "::error::Tag v${VERSION} already exists"
exit 1
fi
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Run tests
run: go test -race ./...
- name: Update manifest.json version
run: |
jq --arg v "$VERSION" '.version = $v' manifest.json > manifest.tmp && mv manifest.tmp manifest.json
- name: Commit, tag, and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add manifest.json
git commit --allow-empty -m "Release v${VERSION}"
git tag "v${VERSION}"
git push origin main "v${VERSION}"
- name: Install TinyGo
run: |
wget https://github.com/tinygo-org/tinygo/releases/download/v0.40.1/tinygo_0.40.1_amd64.deb
sudo dpkg -i tinygo_0.40.1_amd64.deb
sudo apt install -y binaryen
- name: Build and package plugin
run: make package
- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ env.VERSION }}
draft: true
prerelease: ${{ inputs.beta != '' && inputs.beta != '0' }}
files: discord-rich-presence.ndp
generate_release_notes: true
-38
View File
@@ -1,38 +0,0 @@
name: Add download link to PR
on:
workflow_run:
workflows: ["Build"]
types: [completed]
jobs:
pr_comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Get PR number
id: pr
uses: actions/github-script@v7
with:
script: |
const pulls = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open'
});
const pr = pulls.data.find(p => p.head.sha === '${{ github.event.workflow_run.head_sha }}');
if (pr) {
core.setOutput('number', pr.number);
}
- name: Add download link comment
if: steps.pr.outputs.number
uses: marocchino/sticky-pull-request-comment@v2
with:
number: ${{ steps.pr.outputs.number }}
message: |
Download the plugin for this PR: [discord-rich-presence.zip](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}/discord-rich-presence.zip)
Built from ${{ github.event.workflow_run.head_sha }} on ${{ github.event.workflow_run.updated_at }}
+4 -1
View File
@@ -1,4 +1,7 @@
*.wasm *.wasm
*.ndp *.ndp
tmp tmp
discord-rich-presence
discordrome
.DS_Store
.direnv/
+1 -1
View File
@@ -1,7 +1,7 @@
SHELL := /usr/bin/env bash SHELL := /usr/bin/env bash
.PHONY: test build package clean .PHONY: test build package clean
PLUGIN_NAME := discord-rich-presence PLUGIN_NAME := discodrome
WASM_FILE := plugin.wasm WASM_FILE := plugin.wasm
TINYGO := $(shell command -v tinygo 2> /dev/null) TINYGO := $(shell command -v tinygo 2> /dev/null)
+8 -14
View File
@@ -1,15 +1,12 @@
# Discord Rich Presence Plugin for Navidrome # Discodrome, a work of Discord Rich Presence Plugin for Navidrome
[![Build](https://github.com/navidrome/discord-rich-presence-plugin/actions/workflows/build.yml/badge.svg)](https://github.com/navidrome/discord-rich-presence-plugin/actions/workflows/build.yml) **Attention: This plugin requires Navidrome 0.61.0 or later.**
[![Latest](https://img.shields.io/github/v/release/navidrome/discord-rich-presence-plugin)](https://github.com/navidrome/discord-rich-presence-plugin/releases/latest/download/discord-rich-presence.ndp)
**Attention: This plugin requires Navidrome 0.60.2 or later.**
This plugin integrates Navidrome with Discord Rich Presence, displaying your currently playing track in your Discord status. This plugin integrates Navidrome with Discord Rich Presence, displaying your currently playing track in your Discord status.
The goal is to demonstrate the capabilities of Navidrome's plugin system by implementing a real-time presence feature using Discord's Gateway API. The goal is to demonstrate the capabilities of Navidrome's plugin system by implementing a real-time presence feature using Discord's Gateway API.
It demonstrates how a Navidrome plugin can maintain real-time connections to external services while remaining completely stateless. It demonstrates how a Navidrome plugin can maintain real-time connections to external services while remaining completely stateless.
Based on the [Navicord](https://github.com/logixism/navicord) project. Based on the [discord-rich-presence-plugin](https://github.com/navidrome/discord-rich-presence-plugin) project.
**⚠️ WARNING: This plugin requires storing Discord user tokens, which may violate Discord's Terms of Service. Use at your own risk.** **⚠️ WARNING: This plugin requires storing Discord user tokens, which may violate Discord's Terms of Service. Use at your own risk.**
@@ -26,13 +23,12 @@ Based on the [Navicord](https://github.com/logixism/navicord) project.
- Optional album art from [Cover Art Archive](https://coverartarchive.org) for MusicBrainz-tagged music - Optional album art from [Cover Art Archive](https://coverartarchive.org) for MusicBrainz-tagged music
- Optional image hosting via [uguu.se](https://uguu.se) for non-public Navidrome instances - Optional image hosting via [uguu.se](https://uguu.se) for non-public Navidrome instances
<img alt="Discord Rich Presence showing currently playing track with album art, artist, and playback progress" src="https://raw.githubusercontent.com/navidrome/discord-rich-presence-plugin/master/.github/ss-richpresence.webp">
## Installation ## Installation
### Step 1: Download and Install the Plugin ### Step 1: Download and Install the Plugin
1. Download the `discord-rich-presence.ndp` file from the [releases page](https://github.com/navidrome/discord-rich-presence-plugin/releases) 1. Download the `discodrome.ndp` file from the [releases page](https://git.atri.dad/atridad/discodrome/releases)
2. Copy it to your Navidrome plugins folder. Default location: `<navidrome-data-directory>/plugins/` 2. Copy it to your Navidrome plugins folder. Default location: `<navidrome-data-directory>/plugins/`
### Step 2: Create a Discord Application ### Step 2: Create a Discord Application
@@ -115,7 +111,6 @@ For album artwork to display in Discord, Discord needs to be able to access the
Access the plugin configuration in Navidrome: **Settings > Plugins > Discord Rich Presence** Access the plugin configuration in Navidrome: **Settings > Plugins > Discord Rich Presence**
<img alt="Plugin configuration panel showing all available settings" src="https://raw.githubusercontent.com/navidrome/discord-rich-presence-plugin/master/.github/ss-config.webp">
### Configuration Fields ### Configuration Fields
@@ -249,7 +244,7 @@ make build
make package make package
``` ```
The `make package` command creates `discord-rich-presence.ndp` containing the compiled WebAssembly module and manifest. The `make package` command creates `discodrome.ndp` containing the compiled WebAssembly module and manifest.
### Manual Build Options ### Manual Build Options
@@ -257,16 +252,15 @@ The `make package` command creates `discord-rich-presence.ndp` containing the co
```sh ```sh
# Install TinyGo first: https://tinygo.org/getting-started/install/ # Install TinyGo first: https://tinygo.org/getting-started/install/
tinygo build -target wasip1 -buildmode=c-shared -o plugin.wasm -scheduler=none . tinygo build -target wasip1 -buildmode=c-shared -o plugin.wasm -scheduler=none .
zip discord-rich-presence.ndp plugin.wasm manifest.json zip discodrome.ndp plugin.wasm manifest.json
``` ```
#### Using Standard Go #### Using Standard Go
```sh ```sh
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm . GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm .
zip discord-rich-presence.ndp plugin.wasm manifest.json zip discodrome.ndp plugin.wasm manifest.json
``` ```
### Output ### Output
- `plugin.wasm`: The compiled WebAssembly module - `plugin.wasm`: The compiled WebAssembly module
- `discord-rich-presence.ndp`: The complete plugin package ready for installation - `discodrome.ndp`: The complete plugin package ready for installation
Generated
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+32
View File
@@ -0,0 +1,32 @@
{
description = "Discodrome Development Environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
nixpkgs,
flake-utils,
self,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
go
tinygo
gnumake
binaryen
jq
];
};
}
);
}
+1 -1
View File
@@ -1,4 +1,4 @@
module discord-rich-presence module discodrome
go 1.25.0 go 1.25.0
+18 -6
View File
@@ -25,12 +25,13 @@ import (
// Configuration keys // Configuration keys
const ( const (
clientIDKey = "clientid" clientIDKey = "clientid"
usersKey = "users" usersKey = "users"
activityNameKey = "activityname" activityNameKey = "activityname"
spotifyLinksKey = "spotifylinks" activityNameTemplateKey = "activitynametemplate"
caaEnabledKey = "caaenabled" spotifyLinksKey = "spotifylinks"
uguuEnabledKey = "uguuenabled" caaEnabledKey = "caaenabled"
uguuEnabledKey = "uguuenabled"
) )
const ( const (
@@ -47,6 +48,7 @@ const (
activityNameTrack = "Track" activityNameTrack = "Track"
activityNameArtist = "Artist" activityNameArtist = "Artist"
activityNameAlbum = "Album" activityNameAlbum = "Album"
activityNameCustom = "Custom"
) )
// userToken represents a user-token mapping from the config // userToken represents a user-token mapping from the config
@@ -170,6 +172,16 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error {
case activityNameArtist: case activityNameArtist:
activityName = input.Track.Artist activityName = input.Track.Artist
statusDisplayType = statusDisplayName statusDisplayType = statusDisplayName
case activityNameCustom:
template, _ := pdk.GetConfig(activityNameTemplateKey)
if template != "" {
r := strings.NewReplacer(
"{track}", input.Track.Title,
"{artist}", input.Track.Artist,
"{album}", input.Track.Album,
)
activityName = r.Replace(template)
}
} }
// Resolve Spotify URLs if enabled // Resolve Spotify URLs if enabled
+56
View File
@@ -225,6 +225,62 @@ var _ = Describe("discordPlugin", func() {
Entry("uses track album when configured", "Album", true, "Test Album", 0), Entry("uses track album when configured", "Album", true, "Test Album", 0),
Entry("uses track artist when configured", "Artist", true, "Test Artist", 0), Entry("uses track artist when configured", "Artist", true, "Test Artist", 0),
) )
DescribeTable("custom activity name template",
func(template string, templateExists bool, expectedName string) {
pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true)
pdk.PDKMock.On("GetConfig", usersKey).Return(`[{"username":"testuser","token":"test-token"}]`, true)
pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("", false)
pdk.PDKMock.On("GetConfig", activityNameKey).Return("Custom", true)
pdk.PDKMock.On("GetConfig", activityNameTemplateKey).Return(template, templateExists)
pdk.PDKMock.On("GetConfig", spotifyLinksKey).Return("", false)
// Connect mocks
host.CacheMock.On("GetInt", "discord.seq.testuser").Return(int64(0), false, errors.New("not found"))
gatewayResp := []byte(`{"url":"wss://gateway.discord.gg"}`)
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
return req.Method == "GET" && req.URL == "https://discord.com/api/gateway"
})).Return(&host.HTTPResponse{StatusCode: 200, Body: gatewayResp}, nil)
host.WebSocketMock.On("Connect", mock.MatchedBy(func(url string) bool {
return strings.Contains(url, "gateway.discord.gg")
}), mock.Anything, "testuser").Return("testuser", nil)
// Capture the activity payload sent to Discord
var sentPayload string
host.WebSocketMock.On("SendText", "testuser", mock.Anything).Run(func(args mock.Arguments) {
sentPayload = args.Get(1).(string)
}).Return(nil)
host.SchedulerMock.On("ScheduleRecurring", mock.Anything, payloadHeartbeat, "testuser").Return("testuser", nil)
host.SchedulerMock.On("CancelSchedule", "testuser-clear").Return(nil)
// Image mocks
host.CacheMock.On("GetString", discordImageKey).Return("", false, nil)
host.CacheMock.On("SetString", discordImageKey, mock.Anything, mock.Anything).Return(nil)
host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil)
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`{}`)}, nil)
host.SchedulerMock.On("ScheduleOneTime", mock.Anything, payloadClearActivity, "testuser-clear").Return("testuser-clear", nil)
err := plugin.NowPlaying(scrobbler.NowPlayingRequest{
Username: "testuser",
Position: 10,
Track: scrobbler.TrackInfo{
ID: "track1",
Title: "Test Song",
Artist: "Test Artist",
Album: "Test Album",
Duration: 180,
},
})
Expect(err).ToNot(HaveOccurred())
Expect(sentPayload).To(ContainSubstring(fmt.Sprintf(`"name":"%s"`, expectedName)))
},
Entry("uses custom template with all placeholders", "{artist} - {track} ({album})", true, "Test Artist - Test Song (Test Album)"),
Entry("uses custom template with only track", "{track}", true, "Test Song"),
Entry("uses custom template with only artist", "{artist}", true, "Test Artist"),
Entry("uses custom template with only album", "{album}", true, "Test Album"),
Entry("uses custom template with plain text", "Now Playing", true, "Now Playing"),
Entry("falls back to Navidrome when template is empty", "", false, "Navidrome"),
)
}) })
Describe("Scrobble", func() { Describe("Scrobble", func() {
+27 -21
View File
@@ -1,10 +1,10 @@
{ {
"$schema": "https://raw.githubusercontent.com/navidrome/navidrome/refs/heads/master/plugins/manifest-schema.json", "$schema": "https://raw.githubusercontent.com/navidrome/navidrome/refs/heads/master/plugins/manifest-schema.json",
"name": "Discord Rich Presence", "name": "Discodrome",
"author": "Navidrome Team", "author": "Atridad Lahiji",
"version": "1.0.0", "version": "1.1.0",
"description": "Discord Rich Presence integration for Navidrome", "description": "Discord Rich Presence integration for Navidrome",
"website": "https://github.com/navidrome/discord-rich-presence-plugin", "website": "https://git.atri.dad/atridad/discodrome",
"permissions": { "permissions": {
"users": { "users": {
"reason": "To process scrobbles on behalf of users" "reason": "To process scrobbles on behalf of users"
@@ -20,9 +20,7 @@
}, },
"websocket": { "websocket": {
"reason": "To maintain real-time connection with Discord gateway", "reason": "To maintain real-time connection with Discord gateway",
"requiredHosts": [ "requiredHosts": ["gateway.discord.gg"]
"gateway.discord.gg"
]
}, },
"cache": { "cache": {
"reason": "To store connection state and sequence numbers" "reason": "To store connection state and sequence numbers"
@@ -53,14 +51,15 @@
"type": "string", "type": "string",
"title": "Activity Name Display", "title": "Activity Name Display",
"description": "Choose what to display as the activity name in Discord Rich Presence", "description": "Choose what to display as the activity name in Discord Rich Presence",
"enum": [ "enum": ["Default", "Track", "Album", "Artist", "Custom"],
"Default",
"Track",
"Album",
"Artist"
],
"default": "Default" "default": "Default"
}, },
"activitynametemplate": {
"type": "string",
"title": "Custom Activity Name Template",
"description": "Template for the activity name. Available placeholders: {track}, {artist}, {album}",
"default": "{artist} - {track}"
},
"caaenabled": { "caaenabled": {
"type": "boolean", "type": "boolean",
"title": "Use artwork from Cover Art Archive (for MusicBrainz-tagged music)", "title": "Use artwork from Cover Art Archive (for MusicBrainz-tagged music)",
@@ -99,17 +98,11 @@
"minLength": 1 "minLength": 1
} }
}, },
"required": [ "required": ["username", "token"]
"username",
"token"
]
} }
} }
}, },
"required": [ "required": ["clientid", "users"]
"clientid",
"users"
]
}, },
"uiSchema": { "uiSchema": {
"type": "VerticalLayout", "type": "VerticalLayout",
@@ -125,6 +118,19 @@
"format": "radio" "format": "radio"
} }
}, },
{
"type": "Control",
"scope": "#/properties/activitynametemplate",
"rule": {
"effect": "SHOW",
"condition": {
"scope": "#/properties/activityname",
"schema": {
"const": "Custom"
}
}
}
},
{ {
"type": "Control", "type": "Control",
"scope": "#/properties/caaenabled" "scope": "#/properties/caaenabled"