add: show YTM user pfp / name in Discord RPC#4103
add: show YTM user pfp / name in Discord RPC#4103noxygalaxy wants to merge 8 commits intopear-devs:masterfrom
Conversation
|
Please fix these issues. |
There was a problem hiding this comment.
Pull request overview
This PR adds an option to display the current YouTube Music user’s avatar and name in the Discord Rich Presence, with a corresponding config flag and menu toggle.
Changes:
- Extends the Discord plugin configuration with a
showYouTubeUserflag and exposes it via the plugin’s menu. - Updates
DiscordServiceto fetch the logged-in YT Music user (avatar + name) from the webview and surface it as the small image/text in Discord RPC. - Adds i18n strings for the new menu item in English, Russian, and Ukrainian locales.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/plugins/discord/menu.ts |
Adds a “Show YouTube Music user info” checkbox bound to the new showYouTubeUser config flag in the Discord plugin menu. |
src/plugins/discord/index.ts |
Extends DiscordPluginConfig with showYouTubeUser: boolean and sets its default to true in the plugin’s config. |
src/plugins/discord/discord-service.ts |
Stores fetched YT Music user info, fetches it from the renderer via executeJavaScript, and uses it for smallImageKey/smallImageText in Discord activity, with retry logic on failure. |
src/i18n/resources/en.json |
Adds the plugins.discord.menu.show-youtube-user label in English. |
src/i18n/resources/ru.json |
Adds the plugins.discord.menu.show-youtube-user label in Russian. |
src/i18n/resources/uk.json |
Adds the plugins.discord.menu.show-youtube-user label in Ukrainian. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
done :) |
|
so? |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds extraction and display of YouTube Music account info in Discord Rich Presence: new i18n strings, a plugin config option and timer key, DiscordService caching and payload changes (new public setApplicationUser), an IPC listener for Changes
Sequence DiagramsequenceDiagram
participant YTMusic as "YouTube Music UI"
participant Preload as "Preload Script"
participant IPCRenderer as "ipcRenderer"
participant Main as "Main Process"
participant DiscordSvc as "DiscordService"
participant RPC as "Discord RPC"
Preload->>YTMusic: query avatar & settings button
YTMusic-->>Preload: avatar src / account name (polled)
Preload->>Preload: assemble {name, avatar}
Preload->>IPCRenderer: send('discord:youtube-info', {name, avatar})
IPCRenderer->>Main: deliver IPC event
Main->>DiscordSvc: setApplicationUser({name, avatar})
DiscordSvc->>DiscordSvc: cache user & updateActivity()
DiscordSvc->>RPC: update Rich Presence (smallImageKey/text)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/plugins/discord/discord-service.ts (1)
185-193:⚠️ Potential issue | 🔴 CriticalUndefined identifier in dev check at Line 191.
The module imports
isfromelectron-is, but this block callselectronIs.dev().Suggested fix
- if (electronIs.dev()) { + if (is.dev()) { console.log(LoggerPrefix, t('plugins.discord.backend.disconnected')); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/plugins/discord/discord-service.ts` around lines 185 - 193, The dev-mode check inside resetInfo uses an undefined identifier electronIs.dev(); update it to use the actual imported symbol (is.dev()) or adjust the import to match the usage; specifically modify the dev check in the resetInfo method so it calls is.dev() (or rename the import to electronIs) and keep the rest of the block (console.log(LoggerPrefix, t(...))) unchanged so the timerManager.clearAll(), ready/youtubeUser/lastSongInfo resets remain intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/plugins/discord/discord-service.ts`:
- Around line 439-459: The setYouTubeUser method references an undefined dev()
function; replace those calls with the imported helper is.dev() so logging uses
the correct helper. Update occurrences inside setYouTubeUser (the dev() checks
at the early-return warning and the verbose console logs) to use is.dev(),
ensuring the method uses the module's is helper consistently when gating
console.warn/console.log.
- Around line 132-137: The conditional is using the wrong config flag: replace
references to config.showYouTubeUser with the correct config.showApplicationUser
so the smallImageKey and smallImageText fields honor the plugin schema; update
the two occurrences around smallImageKey and smallImageText (which call
getYouTubeUserAvatar() and getYouTubeUserName()) to check
config.showApplicationUser instead.
In `@src/plugins/discord/menu.ts`:
- Around line 83-90: The menu item is using the wrong i18n and config keys;
update the MenuItem's label, checked property, and setConfig call to use the
defined keys for the application user instead of the YouTube user—replace
t('plugins.discord.menu.show-youtube-user') with
t('plugins.discord.menu.show-application-user'), use config.showApplicationUser
(not config.showYouTubeUser) for the checked value, and call setConfig({
showApplicationUser: item.checked }) inside the click handler so the i18n and
config keys (used where t, config, and setConfig are referenced) are consistent.
In `@src/plugins/discord/preload.ts`:
- Around line 5-108: Gate the scraping by checking the Discord integration
toggle before doing any DOM queries or sending data: in preload.start() (and
before findUserInfo() is ever called), query the main process or a safe
preload-exposed API (e.g., via ipcRenderer.invoke('discord:is-enabled') or a
preload-exposed window API) to see if the Discord option is enabled; if it is
false, do not create the MutationObserver, do not call findUserInfo(), and do
not register the DOMContentLoaded handler. Also update findUserInfo() to avoid
calling ipcRenderer.send('discord:youtube-info', ...) unless the enabled check
passed so no account data is collected or transmitted when the toggle is off.
- Around line 41-72: The code currently treats the placeholder 'Pear Desktop
User' as a successful lookup and always sends
ipcRenderer.send('discord:youtube-info', { name, avatar }) and returns true if
an avatar exists; change this so you only send the info and return true when you
actually located a real account name. In the block that queries
accountNameElement (look for usages of settingsButton and accountNameElement),
set a flag (e.g., foundName) when you successfully extract textContent or title,
and after the wait-loop check that flag (or that name !== 'Pear Desktop User')
before calling ipcRenderer.send('discord:youtube-info', ...); if not found,
avoid sending the placeholder, return false (or send a different payload
indicating no name) so RPC doesn't report a fake name.
- Around line 79-100: findUserInfo is racy: multiple observer callbacks and the
immediate call can re-enter it and emit duplicate discord:youtube-info events.
Add a re-entry guard (e.g., isFinding boolean or inFlight Promise) inside
findUserInfo to return immediately if a lookup is already running, set it at
start and clear it in finally; additionally track whether the YouTube info has
already been emitted (e.g., foundSent or lastEmittedId) so repeated successful
lookups don’t re-emit; ensure observer.disconnect() is still called on success
and that startObserver uses the guarded findUserInfo to avoid overlapping calls.
---
Outside diff comments:
In `@src/plugins/discord/discord-service.ts`:
- Around line 185-193: The dev-mode check inside resetInfo uses an undefined
identifier electronIs.dev(); update it to use the actual imported symbol
(is.dev()) or adjust the import to match the usage; specifically modify the dev
check in the resetInfo method so it calls is.dev() (or rename the import to
electronIs) and keep the rest of the block (console.log(LoggerPrefix, t(...)))
unchanged so the timerManager.clearAll(), ready/youtubeUser/lastSongInfo resets
remain intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c6cdac46-bbaf-442a-b107-f850a06692c3
📒 Files selected for processing (9)
src/i18n/resources/en.jsonsrc/i18n/resources/ru.jsonsrc/i18n/resources/uk.jsonsrc/plugins/discord/constants.tssrc/plugins/discord/discord-service.tssrc/plugins/discord/index.tssrc/plugins/discord/main.tssrc/plugins/discord/menu.tssrc/plugins/discord/preload.ts
| export const preload = createPreload({ | ||
| start() { | ||
| let checkCount = 0; | ||
| const maxChecks = 20; | ||
|
|
||
| const findUserInfo = async () => { | ||
| try { | ||
| let avatar: string | null = null; | ||
|
|
||
| // Find avatar first - this is always visible | ||
| const accountButton = | ||
| document.querySelector<HTMLImageElement>( | ||
| 'ytmusic-settings-button img#img', | ||
| ) || | ||
| document.querySelector<HTMLImageElement>( | ||
| 'ytmusic-settings-button yt-img-shadow img', | ||
| ) || | ||
| document.querySelector<HTMLImageElement>( | ||
| 'ytmusic-settings-button img', | ||
| ); | ||
|
|
||
| if (accountButton) { | ||
| avatar = accountButton.src || accountButton.getAttribute('src'); | ||
| } | ||
|
|
||
| if (!avatar || avatar.startsWith('data:')) { | ||
| return false; | ||
| } | ||
|
|
||
| // Now get the username by clicking the settings button if we don't have it | ||
| 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 as HTMLElement).click(); | ||
|
|
||
| // Wait for the menu to appear | ||
| 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 }), | ||
| ); | ||
| } | ||
|
|
||
| ipcRenderer.send('discord:youtube-info', { name, avatar }); | ||
| return true; | ||
| } catch (e) { | ||
| console.error('Failed to fetch YouTube user info:', e); | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| const observer = new MutationObserver(() => { | ||
| if (checkCount >= maxChecks) { | ||
| observer.disconnect(); | ||
| return; | ||
| } | ||
|
|
||
| findUserInfo().then((found) => { | ||
| if (found) { | ||
| observer.disconnect(); | ||
| } | ||
| }); | ||
| checkCount++; | ||
| }); | ||
|
|
||
| const startObserver = () => { | ||
| observer.observe(document.body, { | ||
| childList: true, | ||
| subtree: true, | ||
| }); | ||
|
|
||
| // Also try immediately | ||
| findUserInfo(); | ||
| }; | ||
|
|
||
| if (document.body) { | ||
| startObserver(); | ||
| } else { | ||
| document.addEventListener('DOMContentLoaded', startObserver); | ||
| } | ||
| }, |
There was a problem hiding this comment.
Gate the scraping behind the user-facing toggle.
This preload harvests the YTM account name/avatar on every startup. If the new Discord option is off, the data is still collected and sent to the main process anyway, which weakens the privacy boundary for an opt-in feature.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/plugins/discord/preload.ts` around lines 5 - 108, Gate the scraping by
checking the Discord integration toggle before doing any DOM queries or sending
data: in preload.start() (and before findUserInfo() is ever called), query the
main process or a safe preload-exposed API (e.g., via
ipcRenderer.invoke('discord:is-enabled') or a preload-exposed window API) to see
if the Discord option is enabled; if it is false, do not create the
MutationObserver, do not call findUserInfo(), and do not register the
DOMContentLoaded handler. Also update findUserInfo() to avoid calling
ipcRenderer.send('discord:youtube-info', ...) unless the enabled check passed so
no account data is collected or transmitted when the toggle is off.
| let name = 'Pear Desktop User'; | ||
|
|
||
| if (settingsButton) { | ||
| // Click to open the menu | ||
| (settingsButton as HTMLElement).click(); | ||
|
|
||
| // Wait for the menu to appear | ||
| 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 }), | ||
| ); | ||
| } | ||
|
|
||
| ipcRenderer.send('discord:youtube-info', { name, avatar }); | ||
| return true; |
There was a problem hiding this comment.
Don't treat the placeholder as a successful account-name lookup.
At Line 71 this sends { name: 'Pear Desktop User', avatar } and returns success whenever an avatar exists. If the menu selector misses or loads too slowly, the observer stops and the RPC can get stuck with a fake name instead of the actual YTM account name.
Suggested fix
- let name = 'Pear Desktop User';
+ let name: string | null = null;
...
if (accountNameElement) {
name =
accountNameElement.textContent?.trim() ||
accountNameElement.getAttribute('title') ||
name;
break;
}
}
...
- ipcRenderer.send('discord:youtube-info', { name, avatar });
+ if (!name) {
+ return false;
+ }
+
+ ipcRenderer.send('discord:youtube-info', { name, avatar });
return true;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let name = 'Pear Desktop User'; | |
| if (settingsButton) { | |
| // Click to open the menu | |
| (settingsButton as HTMLElement).click(); | |
| // Wait for the menu to appear | |
| 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 }), | |
| ); | |
| } | |
| ipcRenderer.send('discord:youtube-info', { name, avatar }); | |
| return true; | |
| let name: string | null = null; | |
| if (settingsButton) { | |
| // Click to open the menu | |
| (settingsButton as HTMLElement).click(); | |
| // Wait for the menu to appear | |
| 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 (!name) { | |
| return false; | |
| } | |
| ipcRenderer.send('discord:youtube-info', { name, avatar }); | |
| return true; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/plugins/discord/preload.ts` around lines 41 - 72, The code currently
treats the placeholder 'Pear Desktop User' as a successful lookup and always
sends ipcRenderer.send('discord:youtube-info', { name, avatar }) and returns
true if an avatar exists; change this so you only send the info and return true
when you actually located a real account name. In the block that queries
accountNameElement (look for usages of settingsButton and accountNameElement),
set a flag (e.g., foundName) when you successfully extract textContent or title,
and after the wait-loop check that flag (or that name !== 'Pear Desktop User')
before calling ipcRenderer.send('discord:youtube-info', ...); if not found,
avoid sending the placeholder, return false (or send a different payload
indicating no name) so RPC doesn't report a fake name.
| const observer = new MutationObserver(() => { | ||
| if (checkCount >= maxChecks) { | ||
| observer.disconnect(); | ||
| return; | ||
| } | ||
|
|
||
| findUserInfo().then((found) => { | ||
| if (found) { | ||
| observer.disconnect(); | ||
| } | ||
| }); | ||
| checkCount++; | ||
| }); | ||
|
|
||
| const startObserver = () => { | ||
| observer.observe(document.body, { | ||
| childList: true, | ||
| subtree: true, | ||
| }); | ||
|
|
||
| // Also try immediately | ||
| findUserInfo(); |
There was a problem hiding this comment.
Guard findUserInfo() against re-entry and duplicate sends.
The observer watches subtree mutations, and findUserInfo() itself opens/closes the menu, so it can trigger more observer callbacks while a previous lookup is still pending. Combined with the immediate call at Line 100, this can overlap lookups and emit discord:youtube-info multiple times.
Suggested fix
let checkCount = 0;
const maxChecks = 20;
+ let lookupInFlight = false;
+ let sent = false;
const findUserInfo = async () => {
+ if (lookupInFlight || sent) {
+ return false;
+ }
+
+ lookupInFlight = true;
try {
let avatar: string | null = null;
...
- ipcRenderer.send('discord:youtube-info', { name, avatar });
+ ipcRenderer.send('discord:youtube-info', { name, avatar });
+ sent = true;
return true;
} catch (e) {
console.error('Failed to fetch YouTube user info:', e);
return false;
+ } finally {
+ lookupInFlight = false;
}
};
...
const startObserver = () => {
- observer.observe(document.body, {
- childList: true,
- subtree: true,
- });
-
- // Also try immediately
- findUserInfo();
+ findUserInfo().then((found) => {
+ if (found) {
+ observer.disconnect();
+ return;
+ }
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ });
+ });
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/plugins/discord/preload.ts` around lines 79 - 100, findUserInfo is racy:
multiple observer callbacks and the immediate call can re-enter it and emit
duplicate discord:youtube-info events. Add a re-entry guard (e.g., isFinding
boolean or inFlight Promise) inside findUserInfo to return immediately if a
lookup is already running, set it at start and clear it in finally; additionally
track whether the YouTube info has already been emitted (e.g., foundSent or
lastEmittedId) so repeated successful lookups don’t re-emit; ensure
observer.disconnect() is still called on success and that startObserver uses the
guarded findUserInfo to avoid overlapping calls.
<3
Summary by CodeRabbit