diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json index 4cf482aa3b..6dfbacf5c0 100644 --- a/src/i18n/resources/en.json +++ b/src/i18n/resources/en.json @@ -456,6 +456,7 @@ "disconnected": "Disconnected", "hide-duration-left": "Hide duration left", "hide-github-button": "Hide GitHub link Button", + "show-youtube-user": "Show YouTube Music user info", "play-on-pear-desktop": "Play on Pear Desktop", "set-inactivity-timeout": "Set inactivity timeout", "set-status-display-type": { diff --git a/src/i18n/resources/ru.json b/src/i18n/resources/ru.json index a443dda1ce..78509a06fc 100644 --- a/src/i18n/resources/ru.json +++ b/src/i18n/resources/ru.json @@ -456,6 +456,7 @@ "disconnected": "Отключено", "hide-duration-left": "Скрыть сколько осталось времени", "hide-github-button": "Скрыть ссылку на GitHub", + "show-youtube-user": "Показать информацию пользователя YouTube Music", "play-on-pear-desktop": "Воспроизвести на Pear Desktop", "set-inactivity-timeout": "Поставить таймер неактивности", "set-status-display-type": { diff --git a/src/i18n/resources/uk.json b/src/i18n/resources/uk.json index 7709ef50fb..fc117ef7cf 100644 --- a/src/i18n/resources/uk.json +++ b/src/i18n/resources/uk.json @@ -456,6 +456,7 @@ "disconnected": "Від'єднано", "hide-duration-left": "Приховати тривалість, яка залишилася", "hide-github-button": "Приховати посилання на GitHub", + "show-youtube-user": "Показати інформацію користувача YouTube Music", "play-on-pear-desktop": "Слухати на Pear Desktop", "set-inactivity-timeout": "Встановити тайм-аут бездіяльності", "set-status-display-type": { diff --git a/src/plugins/discord/discord-service.ts b/src/plugins/discord/discord-service.ts index 728836c0ef..5564a6bb70 100644 --- a/src/plugins/discord/discord-service.ts +++ b/src/plugins/discord/discord-service.ts @@ -48,6 +48,11 @@ export class DiscordService { mainWindow: Electron.BrowserWindow; + /** + * Stores the logged-in YouTube Music user information. + */ + private youtubeUser: { name: string; avatar: string } | null = null; + /** * Initializes the Discord service with configuration and main window reference. * Sets up RPC event listeners. @@ -71,6 +76,10 @@ export class DiscordService { this.rpc.on('ready', () => { this.ready = true; + // Fetch YouTube Music user info after connecting if enabled + if (this.config?.showYouTubeUser) { + this.fetchYouTubeUserInfo(); + } if (this.lastSongInfo && this.config) { this.updateActivity(this.lastSongInfo); } @@ -107,6 +116,8 @@ export class DiscordService { largeImageText: songInfo.album ? truncateString(songInfo.album, 128) : undefined, + smallImageKey: config.showYouTubeUser ? this.getYouTubeUserAvatar() : undefined, + smallImageText: config.showYouTubeUser ? this.getYouTubeUserName() : undefined, buttons: buildDiscordButtons(config, songInfo), }; @@ -280,6 +291,11 @@ export class DiscordService { // Cache the latest song info this.timerManager.clear(TimerKey.ClearActivity); + // Fetch YouTube user info if not already available and feature is enabled + if (!this.youtubeUser && this.config?.showYouTubeUser) { + this.fetchYouTubeUserInfo(); + } + if (!this.rpc || !this.ready) { // skip update if not ready return; @@ -400,6 +416,101 @@ export class DiscordService { return this.rpc.isConnected && this.ready; } + + /** + * Fetches the YouTube Music user avatar and name from the page. + * This method opens the settings menu to access the username. + */ + private async fetchYouTubeUserInfo(): Promise { + try { + const result = await this.mainWindow.webContents.executeJavaScript(` + (async function() { + try { + // Find avatar first - this is always visible + const accountButton = document.querySelector('ytmusic-settings-button img#img') + || document.querySelector('ytmusic-settings-button yt-img-shadow img') + || document.querySelector('ytmusic-settings-button img'); + + let avatar = null; + if (accountButton) { + avatar = accountButton.src || accountButton.getAttribute('src'); + } + + // Now get the username by clicking the settings button + const settingsButton = document.querySelector('ytmusic-settings-button button') + || document.querySelector('ytmusic-settings-button tp-yt-paper-icon-button'); + + let name = 'Pear Desktop User'; + + if (settingsButton) { + // Click to open the menu + settingsButton.click(); + + // Wait for the menu to appear (check multiple times) + for (let i = 0; i < 20; i++) { + await new Promise(resolve => setTimeout(resolve, 50)); + + const accountNameElement = document.querySelector('ytd-active-account-header-renderer #account-name') + || document.querySelector('yt-formatted-string#account-name'); + + if (accountNameElement) { + name = accountNameElement.textContent?.trim() + || accountNameElement.getAttribute('title') + || name; + break; + } + } + + // Close the menu by pressing Escape + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27 })); + } + + if (avatar) { + return { avatar, name }; + } + + return null; + } catch (e) { + console.error('Failed to fetch YouTube user info:', e); + return null; + } + })(); + `); + + if (result && result.avatar) { + this.youtubeUser = { + name: result.name, + avatar: result.avatar, + }; + console.log(LoggerPrefix, `Fetched YouTube user: ${result.name}`); + console.log(LoggerPrefix, `Fetched Avatar URL: ${result.avatar}`); + } else { + console.log(LoggerPrefix, 'Could not fetch YouTube user info - retrying in 5 seconds'); + // Retry after a delay if enabled + if (this.config?.showYouTubeUser) { + setTimeout(() => this.fetchYouTubeUserInfo(), 5000); + } + } + } catch (err) { + console.error(LoggerPrefix, 'Failed to fetch YouTube user info:', err); + } + } + + /** + * Get the YouTube user's avatar URL for use in rich presence. + * @returns The avatar URL or undefined if not available + */ + private getYouTubeUserAvatar(): string | undefined { + return this.youtubeUser?.avatar ?? undefined; + } + + /** + * Get the YouTube user's name for use in rich presence. + * @returns The username or undefined if not available + */ + private getYouTubeUserName(): string | undefined { + return this.youtubeUser?.name ?? undefined; + } /** * Cleans up resources: disconnects RPC, clears all timers, and clears callbacks. * Should be called when the plugin stops or the application quits. @@ -408,4 +519,4 @@ export class DiscordService { this.disconnect(); this.refreshCallbacks = []; } -} +} \ No newline at end of file diff --git a/src/plugins/discord/index.ts b/src/plugins/discord/index.ts index 6503c8a151..047234ee33 100644 --- a/src/plugins/discord/index.ts +++ b/src/plugins/discord/index.ts @@ -39,6 +39,12 @@ export type DiscordPluginConfig = { * Controls which field is displayed in the Discord status text */ statusDisplayType: (typeof StatusDisplayType)[keyof typeof StatusDisplayType]; + /** + * Show YouTube Music user avatar and username in Discord Rich Presence + * + * @default true + */ + showYouTubeUser: boolean; }; export default createPlugin({ @@ -54,6 +60,7 @@ export default createPlugin({ hideGitHubButton: false, hideDurationLeft: false, statusDisplayType: StatusDisplayType.Details, + showYouTubeUser: true, } as DiscordPluginConfig, menu: onMenu, backend, diff --git a/src/plugins/discord/menu.ts b/src/plugins/discord/menu.ts index 2d9a1de16f..1f91d5fb90 100644 --- a/src/plugins/discord/menu.ts +++ b/src/plugins/discord/menu.ts @@ -76,6 +76,16 @@ export const onMenu = async ({ }); }, }, + { + label: t('plugins.discord.menu.show-youtube-user'), + type: 'checkbox', + checked: config.showYouTubeUser, + click(item: Electron.MenuItem) { + setConfig({ + showYouTubeUser: item.checked, + }); + }, + }, { label: t('plugins.discord.menu.hide-github-button'), type: 'checkbox',