From 26fa1f85b2774bf4badb593659290defe4cc26ef Mon Sep 17 00:00:00 2001 From: JellyBrick Date: Sat, 6 Sep 2025 10:25:54 +0900 Subject: [PATCH] fix(discord-rpc, scrobbler): Align artist and title with the last.fm's de facto standard - Display only the main artist. - Display the title in its original language without romanization. - fix #3358 - fix #3641 --- src/i18n/resources/en.json | 1 + src/plugins/discord/constants.ts | 10 ------ src/plugins/discord/discord-service.ts | 4 +-- src/plugins/discord/index.ts | 7 ++-- src/plugins/discord/menu.ts | 35 ++++++++++--------- src/plugins/scrobbler/index.ts | 13 +++++-- src/plugins/scrobbler/menu.ts | 9 +++++ src/plugins/scrobbler/services/lastfm.ts | 7 +++- .../scrobbler/services/listenbrainz.ts | 7 +++- vite-plugins/plugin-importer.mts | 7 ++-- 10 files changed, 61 insertions(+), 39 deletions(-) diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json index 66fff3ac..72eeea47 100644 --- a/src/i18n/resources/en.json +++ b/src/i18n/resources/en.json @@ -758,6 +758,7 @@ "token": "Enter ListenBrainz user token" }, "scrobble-alternative-title": "Use alternative titles", + "scrobble-alternative-artist": "Use alternative artists", "scrobble-other-media": "Scrobble other media" }, "name": "Scrobbler", diff --git a/src/plugins/discord/constants.ts b/src/plugins/discord/constants.ts index e66c92ca..c2abcc02 100644 --- a/src/plugins/discord/constants.ts +++ b/src/plugins/discord/constants.ts @@ -23,13 +23,3 @@ export enum TimerKey { UpdateTimeout = 'updateTimeout', // Timer for throttled activity updates DiscordConnectRetry = 'discordConnectRetry', // Timer for Discord connection retries } - -/** - * An enum for Discord's activity.status_display_type field, governing which field of the activity should be used after - * "Listening to..." in the user's Discord status. - */ -export const DiscordStatusDisplayType = { - YOUTUBE_MUSIC: 0, - ARTIST: 1, - TITLE: 2, -} as const; diff --git a/src/plugins/discord/discord-service.ts b/src/plugins/discord/discord-service.ts index 60bc52a5..728836c0 100644 --- a/src/plugins/discord/discord-service.ts +++ b/src/plugins/discord/discord-service.ts @@ -99,9 +99,9 @@ export class DiscordService { const activityInfo: SetActivity = { type: ActivityType.Listening, statusDisplayType: config.statusDisplayType, - details: truncateString(songInfo.title, 128), // Song title + details: truncateString(songInfo.alternativeTitle ?? songInfo.title, 128), // Song title detailsUrl: songInfo.url ?? undefined, - state: truncateString(songInfo.artist, 128), // Artist name + state: truncateString(songInfo.tags?.at(0) ?? songInfo.artist, 128), // Artist name stateUrl: songInfo.artistUrl, largeImageKey: songInfo.imageSrc ?? undefined, largeImageText: songInfo.album diff --git a/src/plugins/discord/index.ts b/src/plugins/discord/index.ts index c76e0eac..27298a4e 100644 --- a/src/plugins/discord/index.ts +++ b/src/plugins/discord/index.ts @@ -1,8 +1,9 @@ +import { StatusDisplayType } from 'discord-api-types/v10'; + import { createPlugin } from '@/utils'; import { backend } from './main'; import { onMenu } from './menu'; import { t } from '@/i18n'; -import { DiscordStatusDisplayType } from './constants'; export type DiscordPluginConfig = { enabled: boolean; @@ -37,7 +38,7 @@ export type DiscordPluginConfig = { /** * Controls which field is displayed in the Discord status text */ - statusDisplayType: (typeof DiscordStatusDisplayType)[keyof typeof DiscordStatusDisplayType]; + statusDisplayType: (typeof StatusDisplayType)[keyof typeof StatusDisplayType]; }; export default createPlugin({ @@ -52,7 +53,7 @@ export default createPlugin({ playOnYouTubeMusic: true, hideGitHubButton: false, hideDurationLeft: false, - statusDisplayType: DiscordStatusDisplayType.ARTIST, + statusDisplayType: StatusDisplayType.Details, } as DiscordPluginConfig, menu: onMenu, backend, diff --git a/src/plugins/discord/menu.ts b/src/plugins/discord/menu.ts index 640db72f..cd2f1dc7 100644 --- a/src/plugins/discord/menu.ts +++ b/src/plugins/discord/menu.ts @@ -1,30 +1,27 @@ import prompt from 'custom-electron-prompt'; -import { discordService } from './main'; +import { StatusDisplayType } from 'discord-api-types/v10'; +import { discordService } from './main'; import { singleton } from '@/providers/decorators'; import promptOptions from '@/providers/prompt-options'; import { setMenuOptions } from '@/config/plugins'; - import { t } from '@/i18n'; -import { DiscordStatusDisplayType } from './constants'; - import type { MenuContext } from '@/types/contexts'; import type { DiscordPluginConfig } from './index'; - import type { MenuTemplate } from '@/menu'; const registerRefreshOnce = singleton((refreshMenu: () => void) => { discordService?.registerRefreshCallback(refreshMenu); }); -const DiscordStatusDisplayTypeLabels = { - [DiscordStatusDisplayType.YOUTUBE_MUSIC]: +const DiscordStatusDisplayTypeLabels: Record = { + [StatusDisplayType.Name]: 'plugins.discord.menu.set-status-display-type.submenu.youtube-music', - [DiscordStatusDisplayType.ARTIST]: + [StatusDisplayType.State]: 'plugins.discord.menu.set-status-display-type.submenu.artist', - [DiscordStatusDisplayType.TITLE]: + [StatusDisplayType.Details]: 'plugins.discord.menu.set-status-display-type.submenu.title', }; @@ -105,18 +102,24 @@ export const onMenu = async ({ }, { label: t('plugins.discord.menu.set-status-display-type.label'), - submenu: Object.values(DiscordStatusDisplayType).map( - (statusDisplayType) => ({ - label: t(DiscordStatusDisplayTypeLabels[statusDisplayType]), + submenu: Object.values(StatusDisplayType) + .filter( + (v) => typeof StatusDisplayType[v as StatusDisplayType] !== 'number', + ) + .map((statusDisplayType) => ({ + label: t( + DiscordStatusDisplayTypeLabels[ + statusDisplayType as StatusDisplayType + ], + ), type: 'radio', - checked: config.statusDisplayType == statusDisplayType, + checked: config.statusDisplayType === statusDisplayType, click() { setConfig({ - statusDisplayType, + statusDisplayType: statusDisplayType as StatusDisplayType, }); }, - }), - ), + })), }, ]; }; diff --git a/src/plugins/scrobbler/index.ts b/src/plugins/scrobbler/index.ts index ceed9d77..46aa394e 100644 --- a/src/plugins/scrobbler/index.ts +++ b/src/plugins/scrobbler/index.ts @@ -13,11 +13,17 @@ export interface ScrobblerPluginConfig { */ scrobbleOtherMedia: boolean; /** - * Use alternative titles for scrobbling (Useful for non-roman song titles) + * Use alternative titles for scrobbling (Useful for non-roman song titles, e.g. (Not) A Devil -> デビルじゃないもん) * - * @default false + * @default true */ alternativeTitles: boolean; + /** + * Use alternative artist for scrobbling (e.g., DECO27 & (or) PinocchioP -> DECO27 / marasy -> まらしぃ) + * + * @default true + */ + alternativeArtist: boolean; scrobblers: { lastfm: { /** @@ -77,7 +83,8 @@ export interface ScrobblerPluginConfig { export const defaultConfig: ScrobblerPluginConfig = { enabled: false, scrobbleOtherMedia: true, - alternativeTitles: false, + alternativeTitles: true, + alternativeArtist: true, scrobblers: { lastfm: { enabled: false, diff --git a/src/plugins/scrobbler/menu.ts b/src/plugins/scrobbler/menu.ts index 279741b7..2db16e90 100644 --- a/src/plugins/scrobbler/menu.ts +++ b/src/plugins/scrobbler/menu.ts @@ -105,6 +105,15 @@ export const onMenu = async ({ setConfig(config); }, }, + { + label: t('plugins.scrobbler.menu.scrobble-alternative-artist'), + type: 'checkbox', + checked: Boolean(config.alternativeArtist), + click(item) { + config.alternativeArtist = item.checked; + setConfig(config); + }, + }, { label: 'Last.fm', submenu: [ diff --git a/src/plugins/scrobbler/services/lastfm.ts b/src/plugins/scrobbler/services/lastfm.ts index 3a6e59b3..8397a7de 100644 --- a/src/plugins/scrobbler/services/lastfm.ts +++ b/src/plugins/scrobbler/services/lastfm.ts @@ -132,10 +132,15 @@ export class LastFmScrobbler extends ScrobblerBase { ? songInfo.alternativeTitle : songInfo.title; + const artist = + config.alternativeArtist && songInfo.tags?.at(0) !== undefined + ? songInfo.tags?.at(0) + : songInfo.artist; + const postData: LastFmSongData = { track: title, duration: songInfo.songDuration, - artist: songInfo.artist, + artist: artist, ...(songInfo.album ? { album: songInfo.album } : undefined), // Will be undefined if current song is a video api_key: config.scrobblers.lastfm.apiKey, sk: config.scrobblers.lastfm.sessionKey, diff --git a/src/plugins/scrobbler/services/listenbrainz.ts b/src/plugins/scrobbler/services/listenbrainz.ts index 56fc4131..0bc24603 100644 --- a/src/plugins/scrobbler/services/listenbrainz.ts +++ b/src/plugins/scrobbler/services/listenbrainz.ts @@ -81,8 +81,13 @@ function createRequestBody( ? songInfo.alternativeTitle : songInfo.title; + const artist = + config.alternativeArtist && songInfo.tags?.at(0) !== undefined + ? songInfo.tags?.at(0) + : songInfo.artist; + const trackMetadata = { - artist_name: songInfo.artist, + artist_name: artist, track_name: title, release_name: songInfo.album ?? undefined, additional_info: { diff --git a/vite-plugins/plugin-importer.mts b/vite-plugins/plugin-importer.mts index 533b8139..b53e9835 100644 --- a/vite-plugins/plugin-importer.mts +++ b/vite-plugins/plugin-importer.mts @@ -3,7 +3,8 @@ import { fileURLToPath } from 'node:url'; import { globSync } from 'glob'; import { Project } from 'ts-morph'; -import { Platform } from '../src/types/plugins' + +import { Platform } from '../src/types/plugins'; const kebabToCamel = (text: string) => text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase()); @@ -75,7 +76,7 @@ export const pluginVirtualModuleGenerator = ( } writer.blankLine(); - if (mode === "main" || mode === "preload") { + if (mode === 'main' || mode === 'preload') { writer.writeLine("import * as is from 'electron-is';"); writer.writeLine('globalThis.electronIs = is;'); } @@ -137,7 +138,7 @@ export const pluginVirtualModuleGenerator = ( }; function supportsPlatform({ platform }: { platform: string }) { - if (typeof platform !== "number") return true; + if (typeof platform !== 'number') return true; const is = globalThis.electronIs;