Skip to content

Commit 22cb9ef

Browse files
committed
Add a Discord status text field to Settings
1 parent 9420878 commit 22cb9ef

9 files changed

Lines changed: 90 additions & 12 deletions

‎src/app-config.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export function parseAppConfig(text) {
8080
// "upload"; keep that, so an upgrade never starts sending covers.
8181
discordArtworkUpload: parsed.discord?.artworkUpload ?? parsed.discord?.artworkLookup === "upload",
8282
discordTimestamps: parsed.discord?.timestamps,
83+
discordName: parsed.discord?.name,
84+
discordStatusDisplayType: parsed.discord?.statusDisplayType,
8385
...(parsed.hosted ? { hostedEnabled: parsed.hosted.enabled, hostedUrl: parsed.hosted.url } : {}),
8486
...(parsed.privacy !== undefined ? { privacy: parsed.privacy } : {}),
8587
...(parsed.card !== undefined ? { card: parsed.card } : {}),
@@ -220,7 +222,7 @@ export function startDiscordFromConfig(config, provider, { env = process.env, bu
220222
// Hidden titles or album art: never look covers up by title.
221223
const policy = privacyPolicyFromConfig(config);
222224
const artwork = createArtwork(policy.redactTitles || policy.hideArtwork ? { ...config.discord, artworkLookup: "off", artworkUpload: false } : config.discord);
223-
const loop = createDiscordPresenceLoop({ getPresence: () => provider.getPresence(), client, artwork, idleBehavior: config.discord.idleBehavior, timestamps: config.discord.timestamps ?? "both", ...(intervalMs ? { intervalMs } : {}), ...(stuckAfterMs ? { stuckAfterMs } : {}), ...(now ? { now } : {}) });
225+
const loop = createDiscordPresenceLoop({ getPresence: () => provider.getPresence(), client, artwork, idleBehavior: config.discord.idleBehavior, timestamps: config.discord.timestamps ?? "both", activityFields: { ...(config.discord.name ? { name: config.discord.name } : {}), ...(config.discord.statusDisplayType ? { statusDisplayType: config.discord.statusDisplayType } : {}) }, ...(intervalMs ? { intervalMs } : {}), ...(stuckAfterMs ? { stuckAfterMs } : {}), ...(now ? { now } : {}) });
224226
loop.start();
225227
// Refresh artwork: forget cached covers, then update Discord straight away.
226228
async function refreshArtwork() {

‎src/app-settings.js‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IDLE_BEHAVIORS } from "./discord-presence.js";
88
// checked against the same rules as setup, then config.json is replaced in one
99
// step (temp file + rename) so a crash never leaves half a file.
1010

11-
const DISCORD_KEYS = new Set(["enabled", "timestamps", "artworkLookup", "artworkUpload", "idleBehavior"]);
11+
const DISCORD_KEYS = new Set(["enabled", "timestamps", "artworkLookup", "artworkUpload", "idleBehavior", "name", "statusDisplayType"]);
1212
const HOSTED_KEYS = new Set(["enabled"]);
1313
const PRIVACY_KEYS = new Set(["hideTitles", "hideArtwork", "hideProgress", "hideMovies", "hideEpisodes", "hideMusic"]);
1414
const PRIVACY_KIND_KEYS = Object.freeze([["hideMovies", "movie"], ["hideEpisodes", "episode"], ["hideMusic", "track"]]);
@@ -20,6 +20,8 @@ export function discordSettingsView(config) {
2020
artworkLookup: config.discord?.artworkLookup ?? "off",
2121
artworkUpload: config.discord?.artworkUpload !== false,
2222
idleBehavior: config.discord?.idleBehavior ?? "clear",
23+
name: config.discord?.name ?? "",
24+
statusDisplayType: config.discord?.statusDisplayType ?? "name",
2325
});
2426
}
2527

@@ -91,6 +93,8 @@ function rewrite(config, { servers = config.servers, discord = { ...discordSetti
9193
discordArtworkLookup: discord.artworkLookup,
9294
discordArtworkUpload: discord.artworkUpload,
9395
discordTimestamps: discord.timestamps,
96+
discordName: discord.name,
97+
discordStatusDisplayType: discord.statusDisplayType,
9498
...(hosted ? { hostedEnabled: hosted.enabled, ...(hosted.url ? { hostedUrl: hosted.url } : {}) } : {}),
9599
...(privacy ? { privacy } : {}),
96100
...(card ? { card } : {}),
@@ -104,6 +108,8 @@ export function applyDiscordChanges(config, changes) {
104108
checkChanges(changes, DISCORD_KEYS, "discord");
105109
if (changes.idleBehavior !== undefined && !IDLE_BEHAVIORS.includes(changes.idleBehavior)) throw new TypeError("discord settings: idleBehavior is invalid");
106110
if (changes.artworkUpload !== undefined && typeof changes.artworkUpload !== "boolean") throw new TypeError("discord settings: artworkUpload must be true or false");
111+
if (changes.name !== undefined && (typeof changes.name !== "string" || changes.name.length > 128)) throw new TypeError("discord settings: name must be text of at most 128 characters");
112+
if (changes.statusDisplayType !== undefined && !["name", "state", "details"].includes(changes.statusDisplayType)) throw new TypeError("discord settings: statusDisplayType is invalid");
107113
return rewrite(config, { discord: { ...discordSettingsView(config), ...changes } });
108114
}
109115

‎src/discord-presence.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ function validateLoopOptions({ getPresence, client, idleBehavior, timestamps, in
2323
}
2424

2525
export function createDiscordPresenceLoop({
26-
getPresence, client, idleBehavior = "clear", timestamps = "both", artwork,
26+
getPresence, client, idleBehavior = "clear", timestamps = "both", artwork, activityFields = {},
2727
intervalMs = 15_000, graceMs = 120_000, stuckAfterMs = 300_000, stallAfterMs = 60_000, now = () => performance.now(),
2828
setTimer = setTimeout, clearTimer = clearTimeout,
2929
} = {}) {
3030
validateLoopOptions({ getPresence, client, idleBehavior, timestamps, intervalMs, graceMs, stuckAfterMs, stallAfterMs });
3131

32-
const live = createDiscordController({ client, settings: { idleBehavior: idleBehavior === "show" ? "show" : "clear", timestamps }, ...(artwork ? { artwork } : {}) });
33-
const frozen = createDiscordController({ client, settings: { timestamps: "none" }, ...(artwork ? { artwork } : {}) });
32+
const live = createDiscordController({ client, settings: { ...activityFields, idleBehavior: idleBehavior === "show" ? "show" : "clear", timestamps }, ...(artwork ? { artwork } : {}) });
33+
const frozen = createDiscordController({ client, settings: { ...activityFields, timestamps: "none" }, ...(artwork ? { artwork } : {}) });
3434
let lastActive = null;
3535
let idleSince = null;
3636
let timer = null;

‎src/settings-page-handler.js‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ const buildPage = (startupCopy) => `<!doctype html>
7171
<option value="show">Show that nothing is playing</option>
7272
<option value="recent">Show what I played last</option>
7373
</select></p>
74+
<p class="row"><label for="discord-name">Status text</label>
75+
<input type="text" id="discord-name" name="name" maxlength="128" placeholder="{artist} on {service}" aria-describedby="discord-name-help"></p>
76+
<p class="hint" id="discord-name-help">What Discord shows in your status, next to your name. Leave it empty for "{artist} on {service}". Fields you can use: {artist}, {title}, {subtitle}, {album}, {year}, {series}, {season}, {episode} and {service} (Spotify, Navidrome and so on). Anything else you type is kept as it is. To show a literal brace, write {{ or }}.</p>
77+
<p class="row"><label for="discord-status-type">Status shows</label>
78+
<select id="discord-status-type" name="statusDisplayType">
79+
<option value="name">The status text above</option>
80+
<option value="state">The second line (artist or episode)</option>
81+
<option value="details">The first line (title)</option>
82+
</select></p>
7483
<p class="row"><label for="discord-upload">Show my server's cover</label>
7584
<input type="checkbox" id="discord-upload" name="artworkUpload" aria-describedby="discord-upload-help"></p>
7685
<p class="hint" id="discord-upload-help">Discord can only show a picture from a public address, so this uploads just the cover image to litterbox.catbox.moe, where it is kept for 72 hours. No title, artist, token or server address is sent. Turn it off and a private server's art is never sent to Discord.</p>
@@ -481,11 +490,11 @@ refreshVersion(); refreshLogs(); setInterval(refreshLogs, 3000); setInterval(ref
481490

482491
const buildScript = (startupCopy) => `"use strict";
483492
const form = document.getElementById("discord-form");
484-
const fields = { enabled: document.getElementById("discord-enabled"), timestamps: document.getElementById("discord-timestamps"), artworkLookup: document.getElementById("discord-artwork"), artworkUpload: document.getElementById("discord-upload"), idleBehavior: document.getElementById("discord-idle") };
493+
const fields = { enabled: document.getElementById("discord-enabled"), timestamps: document.getElementById("discord-timestamps"), artworkLookup: document.getElementById("discord-artwork"), artworkUpload: document.getElementById("discord-upload"), idleBehavior: document.getElementById("discord-idle"), name: document.getElementById("discord-name"), statusDisplayType: document.getElementById("discord-status-type") };
485494
const save = document.getElementById("discord-save");
486495
function say(text, tone) { const el = document.getElementById("discord-result"); el.textContent = text; el.className = tone || ""; }
487-
function show(d) { fields.enabled.checked = d.enabled; fields.timestamps.value = d.timestamps; fields.artworkLookup.value = d.artworkLookup; fields.artworkUpload.checked = d.artworkUpload !== false; fields.idleBehavior.value = d.idleBehavior || "clear"; toggle(); }
488-
function toggle() { fields.timestamps.disabled = fields.artworkLookup.disabled = fields.artworkUpload.disabled = fields.idleBehavior.disabled = !fields.enabled.checked; }
496+
function show(d) { fields.enabled.checked = d.enabled; fields.timestamps.value = d.timestamps; fields.artworkLookup.value = d.artworkLookup; fields.artworkUpload.checked = d.artworkUpload !== false; fields.idleBehavior.value = d.idleBehavior || "clear"; fields.name.value = d.name || ""; fields.statusDisplayType.value = d.statusDisplayType || "name"; toggle(); }
497+
function toggle() { fields.timestamps.disabled = fields.artworkLookup.disabled = fields.artworkUpload.disabled = fields.idleBehavior.disabled = fields.name.disabled = fields.statusDisplayType.disabled = !fields.enabled.checked; }
489498
fields.enabled.addEventListener("change", toggle);
490499
async function load() {
491500
try {
@@ -510,7 +519,7 @@ form.addEventListener("submit", async (event) => {
510519
save.disabled = true;
511520
say("Saving...", "warn");
512521
try {
513-
const body = { discord: { enabled: fields.enabled.checked, timestamps: fields.timestamps.value, artworkLookup: fields.artworkLookup.value, artworkUpload: fields.artworkUpload.checked, idleBehavior: fields.idleBehavior.value } };
522+
const body = { discord: { enabled: fields.enabled.checked, timestamps: fields.timestamps.value, artworkLookup: fields.artworkLookup.value, artworkUpload: fields.artworkUpload.checked, idleBehavior: fields.idleBehavior.value, name: fields.name.value.trim(), statusDisplayType: fields.statusDisplayType.value } };
514523
const res = await fetch("/api/settings", { method: "PUT", cache: "no-store", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(body) });
515524
if (!res.ok) throw new Error(String(res.status));
516525
show((await res.json()).discord);

‎src/settings-servers.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export function createSettingsServers({ file, credentialStore, deviceId, version
4646
discordArtworkLookup: existing?.discord?.artworkLookup ?? "off",
4747
...(typeof existing?.discord?.artworkUpload === "boolean" ? { discordArtworkUpload: existing.discord.artworkUpload } : {}),
4848
...(existing?.discord?.timestamps ? { discordTimestamps: existing.discord.timestamps } : {}),
49+
...(existing?.discord?.name ? { discordName: existing.discord.name } : {}),
50+
...(existing?.discord?.statusDisplayType ? { discordStatusDisplayType: existing.discord.statusDisplayType } : {}),
4951
...(existing?.hosted ? { hostedEnabled: existing.hosted.enabled, ...(existing.hosted.url ? { hostedUrl: existing.hosted.url } : {}) } : {}),
5052
...(existing?.privacy ? { privacy: existing.privacy } : {}),
5153
...(existing?.card ? { card: existing.card } : {}),

‎src/setup-app.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ function keptSettings(current) {
7676
return {
7777
...(typeof current.discord?.artworkUpload === "boolean" ? { discordArtworkUpload: current.discord.artworkUpload } : {}),
7878
...(current.discord?.timestamps !== undefined ? { discordTimestamps: current.discord.timestamps } : {}),
79+
...(current.discord?.name ? { discordName: current.discord.name } : {}),
80+
...(current.discord?.statusDisplayType ? { discordStatusDisplayType: current.discord.statusDisplayType } : {}),
7981
...(current.hosted ? { hostedEnabled: current.hosted.enabled, ...(current.hosted.url ? { hostedUrl: current.hosted.url } : {}) } : {}),
8082
...(current.privacy ? { privacy: current.privacy } : {}),
8183
...(current.card ? { card: current.card } : {}),

‎src/setup-config.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { normalizeHostedUrl } from "./hosted-uploader.js";
55

66
const PROVIDERS = new Set(["plex", "jellyfin", "navidrome", "emby"]);
77
const ARTWORK_LOOKUPS = new Set(["off", "musicbrainz"]);
8+
const STATUS_DISPLAY_TYPES = new Set(["name", "state", "details"]);
89
const TIMESTAMP_MODES = new Set(["elapsed", "remaining", "both", "none"]);
910
const PRIVACY_FLAGS = ["redactTitles", "hideArtwork", "hideProgress"];
1011
const PRIVACY_KINDS = new Set(["movie", "episode", "track"]);
@@ -124,6 +125,12 @@ export function createSetupConfig(input = {}) {
124125
if (input.discordTimestamps !== undefined && !TIMESTAMP_MODES.has(input.discordTimestamps)) {
125126
throw new TypeError("setup config.discordTimestamps must be elapsed, remaining, both or none");
126127
}
128+
if (input.discordName !== undefined && (typeof input.discordName !== "string" || input.discordName.length > 128)) {
129+
throw new TypeError("setup config.discordName must be a string of at most 128 characters");
130+
}
131+
if (input.discordStatusDisplayType !== undefined && !STATUS_DISPLAY_TYPES.has(input.discordStatusDisplayType)) {
132+
throw new TypeError("setup config.discordStatusDisplayType must be name, state or details");
133+
}
127134
if (input.hostedEnabled !== undefined && typeof input.hostedEnabled !== "boolean") {
128135
throw new TypeError("setup config.hostedEnabled must be a boolean");
129136
}
@@ -151,6 +158,9 @@ export function createSetupConfig(input = {}) {
151158
// Discord timer (set from the settings page): elapsed, remaining, both
152159
// or none. Left out means "both", so setup's own output is unchanged.
153160
...(input.discordTimestamps !== undefined ? { timestamps: input.discordTimestamps } : {}),
161+
// The Discord header text template and which field it shows (left out = defaults).
162+
...(input.discordName ? { name: input.discordName } : {}),
163+
...(input.discordStatusDisplayType !== undefined && input.discordStatusDisplayType !== "name" ? { statusDisplayType: input.discordStatusDisplayType } : {}),
154164
}),
155165
// Hosted card upload (#140) is off unless the user turns it on.
156166
...(input.hostedEnabled !== undefined || hostedUrl ? {

‎test/app-settings.test.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ async function configFile(input = JELLYFIN) {
1818

1919
test("shows the Discord settings with defaults for older configs", () => {
2020
const config = parseAppConfig(serializeSetupConfig({ ...JELLYFIN, discordArtworkLookup: undefined }));
21-
assert.deepEqual({ ...discordSettingsView(config) }, { enabled: true, timestamps: "both", artworkLookup: "off", artworkUpload: false, idleBehavior: "clear" });
21+
assert.deepEqual({ ...discordSettingsView(config) }, { enabled: true, timestamps: "both", artworkLookup: "off", artworkUpload: false, idleBehavior: "clear", name: "", statusDisplayType: "name" });
2222
});
2323

2424
test("applies Discord changes and keeps everything else", () => {
@@ -74,7 +74,7 @@ test("the running app saves Discord settings from its own page only", async () =
7474
assert.equal(page.status, 200);
7575
assert.match(page.headers.get("content-security-policy"), /script-src 'self'/);
7676
const cookie = page.headers.get("set-cookie").split(";")[0];
77-
assert.deepEqual((await (await fetch(`${app.url}/api/settings`)).json()).discord, { enabled: true, timestamps: "both", artworkLookup: "musicbrainz", artworkUpload: false, idleBehavior: "clear" });
77+
assert.deepEqual((await (await fetch(`${app.url}/api/settings`)).json()).discord, { enabled: true, timestamps: "both", artworkLookup: "musicbrainz", artworkUpload: false, idleBehavior: "clear", name: "", statusDisplayType: "name" });
7878
const put = (headers, body = { discord: { enabled: false, timestamps: "elapsed" } }) => fetch(`${app.url}/api/settings`, { method: "PUT", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify(body) });
7979
// No session cookie, or a request from another site: refused, nothing saved.
8080
assert.equal((await put({})).status, 403);
@@ -85,7 +85,7 @@ test("the running app saves Discord settings from its own page only", async () =
8585
assert.equal((await put({ Cookie: cookie }, { discord: { enabled: true }, hosted: { enabled: true } })).status, 400);
8686
const saved = await put({ Cookie: cookie });
8787
assert.equal(saved.status, 200);
88-
assert.deepEqual((await saved.json()).discord, { enabled: false, timestamps: "elapsed", artworkLookup: "musicbrainz", artworkUpload: false, idleBehavior: "clear" });
88+
assert.deepEqual((await saved.json()).discord, { enabled: false, timestamps: "elapsed", artworkLookup: "musicbrainz", artworkUpload: false, idleBehavior: "clear", name: "", statusDisplayType: "name" });
8989
assert.equal(parseAppConfig(await readFile(file, "utf8")).discord.timestamps, "elapsed");
9090
const refresh = await fetch(`${app.url}/api/settings/discord/refresh-artwork`, { method: "POST", headers: { "Content-Type": "application/json", Cookie: cookie }, body: "{}" });
9191
assert.deepEqual([refresh.status, await refresh.json()], [200, { dropped: 0 }]);

0 commit comments

Comments
 (0)