Skip to content

feat(discord): display artist name and avatar in rich presence with caching and optimization#4491

Open
Nynele wants to merge 2 commits into
pear-devs:masterfrom
Nynele:master
Open

feat(discord): display artist name and avatar in rich presence with caching and optimization#4491
Nynele wants to merge 2 commits into
pear-devs:masterfrom
Nynele:master

Conversation

@Nynele

@Nynele Nynele commented Jun 23, 2026

Copy link
Copy Markdown

Description of Changes

This Pull Request improves the Discord Rich Presence plugin to display the artist name directly and show the artist's profile picture as a small circular avatar on the cover art.

Key improvements:

  • Direct Artist Name: Changed the presence status to show the artist name (songInfo.artist) on the second line instead of falling back on song tags.
  • Artist Profile Picture: Added a small avatar image in the Discord Rich Presence (smallImageKey/smallImageText) displaying the artist's profile photo.
  • Fast Profile Picture Resolution: Implemented a stream-based parser in song-info.ts that aborts the HTTP request as soon as it matches the og:image meta tag in the <head> of the artist's channel page. This avoids downloading the rest of the 500KB+ page, resolving the image in ~50-100ms.
  • Caching: Added a local cache (artistImageCache) in the backend to store resolved artist images and prevent duplicate network requests when switching between cached tracks.
  • Instant Updates: Added an artistImageChanged check in discord-service.ts to immediately push the RPC update when the avatar resolves, bypassing the standard 15-second update throttle.
  • Configuration Toggle: Added a toggle switch (hideArtistImage) in the plugin menu to allow users to disable the artist's profile picture if desired, complete with i18n support in English and Spanish.

Verification Results

  • Manual Testing: Verified that the artist's name displays correctly and the small circular profile image loads on Discord Rich Presence immediately after changing the track.
  • Configuration Toggle: Verified that enabling "Hide artist avatar image" in the Discord plugin menu successfully removes the avatar.

Summary by CodeRabbit

  • New Features
    • Added a “Hide artist image” option for Discord Rich Presence, including a menu toggle and English/Spanish translations.
    • Implemented automatic artist image loading for improved Rich Presence display (and the new hide setting is respected).
  • Improvements
    • Rich Presence “state” now consistently shows the artist.
    • Rich Presence updates immediately when the artist image becomes available, keeping visuals in sync.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4f28ba51-a537-466d-b009-8714fb9c6b46

📥 Commits

Reviewing files that changed from the base of the PR and between cd57d89 and 7898507.

📒 Files selected for processing (1)
  • src/providers/song-info.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/providers/song-info.ts

📝 Walkthrough

Walkthrough

Adds an artistImageSrc field to SongInfo, a cached fetchArtistImage helper that parses og:image from artist pages, and an ArtistImageLoaded IPC event. The Discord plugin gains a hideArtistImage config boolean, a menu toggle, and reactive activity-update logic that reacts to artist image changes.

Changes

Discord artist image fetch and rich presence toggle

Layer / File(s) Summary
SongInfo contract, fetchArtistImage, and IPC trigger
src/providers/song-info.ts
SongInfo gains artistImageSrc?: string | null; handleData initializes and cache-populates it; fetchArtistImage fetches/parses og:image and stores results in artistImageCache; SongInfoEvent gains ArtistImageLoaded; the peard:video-src-changed handler conditionally fetches the image, updates songInfo under mutex, and emits the new event.
Discord plugin config, menu, service, and i18n
src/plugins/discord/index.ts, src/plugins/discord/menu.ts, src/plugins/discord/discord-service.ts, src/i18n/resources/en.json, src/i18n/resources/es.json
DiscordPluginConfig adds hideArtistImage: boolean defaulting to false; a checkbox menu item toggles it via setConfig; buildActivityInfo switches state text to songInfo.artist directly; updateActivity adds artistImageChanged detection for immediate rich presence updates; English and Spanish i18n strings are added for the new option.

Sequence Diagram(s)

sequenceDiagram
  participant Renderer as Renderer (peard:video-src-changed)
  participant SongInfoProvider as song-info provider
  participant fetchArtistImage
  participant DiscordService

  Renderer->>SongInfoProvider: video src changed event
  SongInfoProvider->>SongInfoProvider: handleData → songInfo with empty artistImageSrc
  SongInfoProvider->>SongInfoProvider: emit VideoSrcChanged
  SongInfoProvider->>fetchArtistImage: fetch og:image for artistUrl
  fetchArtistImage-->>SongInfoProvider: image URL or null
  SongInfoProvider->>SongInfoProvider: update songInfo.artistImageSrc under mutex
  SongInfoProvider->>Renderer: peard:update-song-info (updated songInfo)
  SongInfoProvider->>DiscordService: ArtistImageLoaded callback
  DiscordService->>DiscordService: detect artistImageChanged → setActivity
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 A bunny went searching for art on the web,
Parsed og:image tags as the page data ebbed,
The Discord rich presence now gleams with a face,
The artist's avatar found right in its place,
With a toggle to hide it if you prefer plain grace! 🎨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main enhancement: adding artist name and avatar display to Discord Rich Presence with performance optimization through caching and streaming.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/providers/song-info.ts`:
- Around line 188-216: The response body stream is being consumed twice, which
violates the Fetch API specification. The getReader() call locks the response
body stream, making subsequent calls to response.text() invalid. After the while
loop in the streaming section, instead of calling response.text() on line 215,
use the already-accumulated htmlBuffer variable to perform the final regex match
for the og:image meta tag. This ensures the response body is only consumed once
through the stream reading process.
🪄 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: d7e4a0e2-38b6-425c-9356-153738f2b0ff

📥 Commits

Reviewing files that changed from the base of the PR and between 23e8748 and cd57d89.

📒 Files selected for processing (6)
  • src/i18n/resources/en.json
  • src/i18n/resources/es.json
  • src/plugins/discord/discord-service.ts
  • src/plugins/discord/index.ts
  • src/plugins/discord/menu.ts
  • src/providers/song-info.ts

Comment thread src/providers/song-info.ts Outdated
Comment on lines +188 to +216
if (response.body && typeof response.body.getReader === 'function') {
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let htmlBuffer = '';

try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
htmlBuffer += decoder.decode(value, { stream: true });
const match = htmlBuffer.match(/<meta property="og:image" content="([^"]+)"/);
if (match && match[1]) {
const imgSrc = match[1];
artistImageCache.set(artistUrl, imgSrc);
reader.cancel();
return imgSrc;
}
if (htmlBuffer.includes('</head>')) {
reader.cancel();
break;
}
}
} catch (err) {
// stream error, fallback to full page load
}
}

const html = await response.text();
const match = html.match(/<meta property="og:image" content="([^"]+)"/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find and inspect the file structure
fd -type f -name "song-info.ts"

Repository: pear-devs/pear-desktop

Length of output: 331


🏁 Script executed:

# Examine the specific function in question
sed -n '180,225p' src/providers/song-info.ts

Repository: pear-devs/pear-desktop

Length of output: 1568


🌐 Web query:

Fetch API Response body consumption getReader() and text() same response disturbed

💡 Result:

The Fetch API's Response body is implemented as a ReadableStream [1][2], which can only be consumed once [1][3][4]. When you call a consumption method—such as.text,.json,.blob, or.getReader—the stream is marked as "read" or "locked" [5][2][4]. Any subsequent attempt to consume the body using these methods on the same Response object will result in an error, typically "body stream already read" or "body stream is locked" [1][3][6]. This design choice allows the browser to stream data directly from the network to the consumer (e.g., the JSON parser or an ArrayBuffer) without needing to buffer the entire response in memory [1]. To work around this limitation if you need to use the body data multiple times, use one of the following approaches: 1. Clone the Response: Use the response.clone method before consuming the body [5][3]. This creates a new Response object that is identical to the original and has its own independent, unconsumed stream [3]. const response = await fetch(url); const responseClone = response.clone; const text = await response.text; // First consumption const json = await responseClone.json; // Second consumption (via clone) 2. Buffer the Data: If you intend to use the data in multiple places within your application, consume the stream once (e.g., via.json or.text) and store the resulting data in a variable [7][3]. You can then reference that variable as many times as needed [7]. 3. Use Teeing (for streams): If you are working directly with the ReadableStream via getReader, you can use the stream.tee method to split the stream into two independent branches, allowing you to consume both separately [7].

Citations:


response.text() fallback is invalid after stream consumption.

Line 215 calls response.text() after lines 188-209 already consume the same body via getReader(). Per the Fetch API specification, once a Response body stream is locked (by calling getReader() or any other consumption method), subsequent attempts to consume it will throw an error like "body stream already read" or "body stream is locked".

🐛 Proposed fix
 export const fetchArtistImage = async (artistUrl: string): Promise<string | null> => {
   if (artistImageCache.has(artistUrl)) {
     return artistImageCache.get(artistUrl) ?? null;
   }

   try {
     const response = await net.fetch(artistUrl);
+    const fallbackResponse = response.clone();
     if (!response.ok) return null;

     if (response.body && typeof response.body.getReader === 'function') {
       const reader = response.body.getReader();
       const decoder = new TextDecoder('utf-8');
@@
     }

-    const html = await response.text();
+    const html = await fallbackResponse.text();
     const match = html.match(/<meta property="og:image" content="([^"]+)"/);
     if (match && match[1]) {
       const imgSrc = match[1];
       artistImageCache.set(artistUrl, imgSrc);
       return imgSrc;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/song-info.ts` around lines 188 - 216, The response body stream
is being consumed twice, which violates the Fetch API specification. The
getReader() call locks the response body stream, making subsequent calls to
response.text() invalid. After the while loop in the streaming section, instead
of calling response.text() on line 215, use the already-accumulated htmlBuffer
variable to perform the final regex match for the og:image meta tag. This
ensures the response body is only consumed once through the stream reading
process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant