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
This commit is contained in:
JellyBrick
2025-09-06 10:25:54 +09:00
parent 555817e2f5
commit 26fa1f85b2
10 changed files with 61 additions and 39 deletions

View File

@ -758,6 +758,7 @@
"token": "Enter ListenBrainz user token" "token": "Enter ListenBrainz user token"
}, },
"scrobble-alternative-title": "Use alternative titles", "scrobble-alternative-title": "Use alternative titles",
"scrobble-alternative-artist": "Use alternative artists",
"scrobble-other-media": "Scrobble other media" "scrobble-other-media": "Scrobble other media"
}, },
"name": "Scrobbler", "name": "Scrobbler",

View File

@ -23,13 +23,3 @@ export enum TimerKey {
UpdateTimeout = 'updateTimeout', // Timer for throttled activity updates UpdateTimeout = 'updateTimeout', // Timer for throttled activity updates
DiscordConnectRetry = 'discordConnectRetry', // Timer for Discord connection retries 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;

View File

@ -99,9 +99,9 @@ export class DiscordService {
const activityInfo: SetActivity = { const activityInfo: SetActivity = {
type: ActivityType.Listening, type: ActivityType.Listening,
statusDisplayType: config.statusDisplayType, statusDisplayType: config.statusDisplayType,
details: truncateString(songInfo.title, 128), // Song title details: truncateString(songInfo.alternativeTitle ?? songInfo.title, 128), // Song title
detailsUrl: songInfo.url ?? undefined, 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, stateUrl: songInfo.artistUrl,
largeImageKey: songInfo.imageSrc ?? undefined, largeImageKey: songInfo.imageSrc ?? undefined,
largeImageText: songInfo.album largeImageText: songInfo.album

View File

@ -1,8 +1,9 @@
import { StatusDisplayType } from 'discord-api-types/v10';
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
import { backend } from './main'; import { backend } from './main';
import { onMenu } from './menu'; import { onMenu } from './menu';
import { t } from '@/i18n'; import { t } from '@/i18n';
import { DiscordStatusDisplayType } from './constants';
export type DiscordPluginConfig = { export type DiscordPluginConfig = {
enabled: boolean; enabled: boolean;
@ -37,7 +38,7 @@ export type DiscordPluginConfig = {
/** /**
* Controls which field is displayed in the Discord status text * 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({ export default createPlugin({
@ -52,7 +53,7 @@ export default createPlugin({
playOnYouTubeMusic: true, playOnYouTubeMusic: true,
hideGitHubButton: false, hideGitHubButton: false,
hideDurationLeft: false, hideDurationLeft: false,
statusDisplayType: DiscordStatusDisplayType.ARTIST, statusDisplayType: StatusDisplayType.Details,
} as DiscordPluginConfig, } as DiscordPluginConfig,
menu: onMenu, menu: onMenu,
backend, backend,

View File

@ -1,30 +1,27 @@
import prompt from 'custom-electron-prompt'; 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 { singleton } from '@/providers/decorators';
import promptOptions from '@/providers/prompt-options'; import promptOptions from '@/providers/prompt-options';
import { setMenuOptions } from '@/config/plugins'; import { setMenuOptions } from '@/config/plugins';
import { t } from '@/i18n'; import { t } from '@/i18n';
import { DiscordStatusDisplayType } from './constants';
import type { MenuContext } from '@/types/contexts'; import type { MenuContext } from '@/types/contexts';
import type { DiscordPluginConfig } from './index'; import type { DiscordPluginConfig } from './index';
import type { MenuTemplate } from '@/menu'; import type { MenuTemplate } from '@/menu';
const registerRefreshOnce = singleton((refreshMenu: () => void) => { const registerRefreshOnce = singleton((refreshMenu: () => void) => {
discordService?.registerRefreshCallback(refreshMenu); discordService?.registerRefreshCallback(refreshMenu);
}); });
const DiscordStatusDisplayTypeLabels = { const DiscordStatusDisplayTypeLabels: Record<StatusDisplayType, string> = {
[DiscordStatusDisplayType.YOUTUBE_MUSIC]: [StatusDisplayType.Name]:
'plugins.discord.menu.set-status-display-type.submenu.youtube-music', 'plugins.discord.menu.set-status-display-type.submenu.youtube-music',
[DiscordStatusDisplayType.ARTIST]: [StatusDisplayType.State]:
'plugins.discord.menu.set-status-display-type.submenu.artist', 'plugins.discord.menu.set-status-display-type.submenu.artist',
[DiscordStatusDisplayType.TITLE]: [StatusDisplayType.Details]:
'plugins.discord.menu.set-status-display-type.submenu.title', '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'), label: t('plugins.discord.menu.set-status-display-type.label'),
submenu: Object.values(DiscordStatusDisplayType).map( submenu: Object.values(StatusDisplayType)
(statusDisplayType) => ({ .filter(
label: t(DiscordStatusDisplayTypeLabels[statusDisplayType]), (v) => typeof StatusDisplayType[v as StatusDisplayType] !== 'number',
)
.map((statusDisplayType) => ({
label: t(
DiscordStatusDisplayTypeLabels[
statusDisplayType as StatusDisplayType
],
),
type: 'radio', type: 'radio',
checked: config.statusDisplayType == statusDisplayType, checked: config.statusDisplayType === statusDisplayType,
click() { click() {
setConfig({ setConfig({
statusDisplayType, statusDisplayType: statusDisplayType as StatusDisplayType,
}); });
}, },
}), })),
),
}, },
]; ];
}; };

View File

@ -13,11 +13,17 @@ export interface ScrobblerPluginConfig {
*/ */
scrobbleOtherMedia: boolean; 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; alternativeTitles: boolean;
/**
* Use alternative artist for scrobbling (e.g., DECO27 & (or) PinocchioP -> DECO27 / marasy -> まらしぃ)
*
* @default true
*/
alternativeArtist: boolean;
scrobblers: { scrobblers: {
lastfm: { lastfm: {
/** /**
@ -77,7 +83,8 @@ export interface ScrobblerPluginConfig {
export const defaultConfig: ScrobblerPluginConfig = { export const defaultConfig: ScrobblerPluginConfig = {
enabled: false, enabled: false,
scrobbleOtherMedia: true, scrobbleOtherMedia: true,
alternativeTitles: false, alternativeTitles: true,
alternativeArtist: true,
scrobblers: { scrobblers: {
lastfm: { lastfm: {
enabled: false, enabled: false,

View File

@ -105,6 +105,15 @@ export const onMenu = async ({
setConfig(config); 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', label: 'Last.fm',
submenu: [ submenu: [

View File

@ -132,10 +132,15 @@ export class LastFmScrobbler extends ScrobblerBase {
? songInfo.alternativeTitle ? songInfo.alternativeTitle
: songInfo.title; : songInfo.title;
const artist =
config.alternativeArtist && songInfo.tags?.at(0) !== undefined
? songInfo.tags?.at(0)
: songInfo.artist;
const postData: LastFmSongData = { const postData: LastFmSongData = {
track: title, track: title,
duration: songInfo.songDuration, duration: songInfo.songDuration,
artist: songInfo.artist, artist: artist,
...(songInfo.album ? { album: songInfo.album } : undefined), // Will be undefined if current song is a video ...(songInfo.album ? { album: songInfo.album } : undefined), // Will be undefined if current song is a video
api_key: config.scrobblers.lastfm.apiKey, api_key: config.scrobblers.lastfm.apiKey,
sk: config.scrobblers.lastfm.sessionKey, sk: config.scrobblers.lastfm.sessionKey,

View File

@ -81,8 +81,13 @@ function createRequestBody(
? songInfo.alternativeTitle ? songInfo.alternativeTitle
: songInfo.title; : songInfo.title;
const artist =
config.alternativeArtist && songInfo.tags?.at(0) !== undefined
? songInfo.tags?.at(0)
: songInfo.artist;
const trackMetadata = { const trackMetadata = {
artist_name: songInfo.artist, artist_name: artist,
track_name: title, track_name: title,
release_name: songInfo.album ?? undefined, release_name: songInfo.album ?? undefined,
additional_info: { additional_info: {

View File

@ -3,7 +3,8 @@ import { fileURLToPath } from 'node:url';
import { globSync } from 'glob'; import { globSync } from 'glob';
import { Project } from 'ts-morph'; import { Project } from 'ts-morph';
import { Platform } from '../src/types/plugins'
import { Platform } from '../src/types/plugins';
const kebabToCamel = (text: string) => const kebabToCamel = (text: string) =>
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase()); text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
@ -75,7 +76,7 @@ export const pluginVirtualModuleGenerator = (
} }
writer.blankLine(); writer.blankLine();
if (mode === "main" || mode === "preload") { if (mode === 'main' || mode === 'preload') {
writer.writeLine("import * as is from 'electron-is';"); writer.writeLine("import * as is from 'electron-is';");
writer.writeLine('globalThis.electronIs = is;'); writer.writeLine('globalThis.electronIs = is;');
} }
@ -137,7 +138,7 @@ export const pluginVirtualModuleGenerator = (
}; };
function supportsPlatform({ platform }: { platform: string }) { function supportsPlatform({ platform }: { platform: string }) {
if (typeof platform !== "number") return true; if (typeof platform !== 'number') return true;
const is = globalThis.electronIs; const is = globalThis.electronIs;