Use Cover Art Archive for albums with MusicBrainz IDs #12

Closed
sproutsberry wants to merge 11 commits from cover-art-archive into main
sproutsberry commented 2026-02-08 16:34:22 -07:00 (Migrated from github.com)

Closes #10

Closes #10
gemini-code-assist[bot] commented 2026-02-08 16:34:38 -07:00 (Migrated from github.com)

Summary of Changes

Hello @sproutsberry, I'm Gemini Code Assist1 ! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the plugin's ability to retrieve album artwork by integrating with the Cover Art Archive (CAA). It introduces a new configuration option to enable this feature, allowing users to leverage a richer source for album covers. The artwork fetching logic has been updated to intelligently prioritize CAA when enabled, falling back to existing methods, and centralizes caching for improved performance.

Highlights

  • Cover Art Archive Integration: Added functionality to fetch album artwork from the Cover Art Archive (CAA) using MusicBrainz IDs.
  • Configurable Artwork Source Priority: Implemented a new caaenabled configuration option, allowing users to prioritize CAA artwork retrieval over existing methods (uguu.se and direct Navidrome).
  • Centralized Caching: Refactored artwork caching logic into a single getImageURL function, ensuring consistent caching across all artwork sources.
  • API Host Whitelisting: Included coverartarchive.org in the manifest.json to allow API calls to the new artwork source.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • coverart.go
    • Introduced cacheKeyFormat for standardized caching keys.
    • Refactored uguu.se related constants and functions into a dedicated section.
    • Removed redundant caching logic from getImageViaUguu.
    • Added new functions: getAlbumIDFromTrackID, getMusicBrainzIDFromAlbumID, fetchImageFromCAA, and getImageViaCAA to interact with MusicBrainz and Cover Art Archive.
    • Defined new Go structs (subsonicGetSongResponse, subsonicGetAlbumResponse, caaResponse) for parsing JSON responses from Navidrome and CAA APIs.
    • Reimplemented getImageURL to act as the primary artwork retrieval function, incorporating a cache check, conditional CAA fetching, conditional uguu.se fetching, and a fallback to direct Navidrome artwork.
    • Added caaEnabledKey constant for the new configuration.
  • manifest.json
    • Updated requiredHosts to include coverartarchive.org for network access to the Cover Art Archive.
    • Added a new boolean property caaenabled to the plugin configuration schema, allowing users to enable or disable CAA integration.
    • Integrated the caaenabled property into the UI schema for user configuration.
Activity
  • The pull request was opened by sproutsberry.
  • It aims to address and close issue #10.
  • The author describes the current state as a "Very rough prototype" and mentions it's their "first time working with Go".
  • The author intends to provide progress updates on the pull request.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.


  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩︎

## Summary of Changes Hello @sproutsberry, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the plugin's ability to retrieve album artwork by integrating with the Cover Art Archive (CAA). It introduces a new configuration option to enable this feature, allowing users to leverage a richer source for album covers. The artwork fetching logic has been updated to intelligently prioritize CAA when enabled, falling back to existing methods, and centralizes caching for improved performance. ### Highlights * **Cover Art Archive Integration**: Added functionality to fetch album artwork from the Cover Art Archive (CAA) using MusicBrainz IDs. * **Configurable Artwork Source Priority**: Implemented a new `caaenabled` configuration option, allowing users to prioritize CAA artwork retrieval over existing methods (uguu.se and direct Navidrome). * **Centralized Caching**: Refactored artwork caching logic into a single `getImageURL` function, ensuring consistent caching across all artwork sources. * **API Host Whitelisting**: Included `coverartarchive.org` in the `manifest.json` to allow API calls to the new artwork source. 🧠 **New Feature in Public Preview:** You can now enable **Memory** to help **Gemini Code Assist** learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. **Click [here](https://codeassist.google/code-review/login) to enable Memory in your admin console.** <details> <summary><b>Changelog</b></summary> * **coverart.go** * Introduced `cacheKeyFormat` for standardized caching keys. * Refactored `uguu.se` related constants and functions into a dedicated section. * Removed redundant caching logic from `getImageViaUguu`. * Added new functions: `getAlbumIDFromTrackID`, `getMusicBrainzIDFromAlbumID`, `fetchImageFromCAA`, and `getImageViaCAA` to interact with MusicBrainz and Cover Art Archive. * Defined new Go structs (`subsonicGetSongResponse`, `subsonicGetAlbumResponse`, `caaResponse`) for parsing JSON responses from Navidrome and CAA APIs. * Reimplemented `getImageURL` to act as the primary artwork retrieval function, incorporating a cache check, conditional CAA fetching, conditional uguu.se fetching, and a fallback to direct Navidrome artwork. * Added `caaEnabledKey` constant for the new configuration. * **manifest.json** * Updated `requiredHosts` to include `coverartarchive.org` for network access to the Cover Art Archive. * Added a new boolean property `caaenabled` to the plugin configuration schema, allowing users to enable or disable CAA integration. * Integrated the `caaenabled` property into the UI schema for user configuration. </details> <details> <summary><b>Activity</b></summary> * The pull request was opened by sproutsberry. * It aims to address and close issue #10. * The author describes the current state as a "Very rough prototype" and mentions it's their "first time working with Go". * The author intends to provide progress updates on the pull request. </details> <details> <summary><b>Using Gemini Code Assist</b></summary> <br> The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips. <b>Invoking Gemini</b> You can request assistance from Gemini at any point by creating a comment using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands on the current page. Feature | Command | Description --- | --- | --- Code Review | `/gemini review` | Performs a code review for the current pull request in its current state. Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state. Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in pull request comments and review comments. Help | `/gemini help` | Displays a list of available commands. <b>Customization</b> To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github). <b>Limitations & Feedback</b> Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up [here](https://google.qualtrics.com/jfe/form/SV_2cyuGuTWsEw84yG). <b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b> </details> [^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).
gemini-code-assist[bot] (Migrated from github.com) reviewed 2026-02-08 16:36:51 -07:00
gemini-code-assist[bot] (Migrated from github.com) left a comment

Code Review

This pull request introduces a new feature to fetch cover art from the Cover Art Archive using MusicBrainz IDs. While the functionality is a great addition, there are critical security concerns related to URL construction. The current implementation lacks proper URL encoding for both internal Subsonic API calls and external Cover Art Archive requests, which could lead to HTTP Parameter Injection, SSRF, or path traversal vulnerabilities. It is strongly recommended to use the net/url package for proper URL escaping. Additionally, the feature has high-severity caching issues that could impact fallback image providers, and there are medium-severity suggestions to improve code clarity and maintainability, including renaming a confusing struct field and addressing TODO comments.

## Code Review This pull request introduces a new feature to fetch cover art from the `Cover Art Archive` using `MusicBrainz` IDs. While the functionality is a great addition, there are critical security concerns related to URL construction. The current implementation lacks proper URL encoding for both internal `Subsonic` API calls and external `Cover Art Archive` requests, which could lead to `HTTP Parameter Injection`, `SSRF`, or `path traversal` vulnerabilities. It is strongly recommended to use the `net/url` package for proper URL escaping. Additionally, the feature has high-severity caching issues that could impact fallback image providers, and there are medium-severity suggestions to improve code clarity and maintainability, including renaming a confusing struct field and addressing `TODO` comments.
@@ -54,3 +51,4 @@
pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for uguu.se artwork: %s", trackID))
return cachedURL
}
gemini-code-assist[bot] (Migrated from github.com) commented 2026-02-08 16:36:51 -07:00

high

Caching an empty URL string prevents the fallback logic in getImageURL from working on subsequent requests for the same track. The cache should only be set if a valid URL is obtained.

    if url != "" {
        cacheKey := fmt.Sprintf(cacheKeyFormat, trackID)
        _ = host.CacheSetString(cacheKey, url, 9000)
    }

    return url

![high](https://www.gstatic.com/codereviewagent/high-priority.svg) Caching an empty URL string prevents the fallback logic in `getImageURL` from working on subsequent requests for the same track. The cache should only be set if a valid URL is obtained. ```go if url != "" { cacheKey := fmt.Sprintf(cacheKeyFormat, trackID) _ = host.CacheSetString(cacheKey, url, 9000) } return url ```
gemini-code-assist[bot] (Migrated from github.com) commented 2026-02-08 16:36:51 -07:00

high

Caching an empty URL string will prevent the fallback logic in getImageURL (to uguu.se or direct) from triggering on subsequent requests for the same track. The cache should only be set when a valid image URL is found.

    if url != "" {
        cacheKey := fmt.Sprintf(cacheKeyFormat, trackID)
        _ = host.CacheSetString(cacheKey, url, 86400)
    }

    return url

![high](https://www.gstatic.com/codereviewagent/high-priority.svg) Caching an empty URL string will prevent the fallback logic in `getImageURL` (to `uguu.se` or direct) from triggering on subsequent requests for the same track. The cache should only be set when a valid image URL is found. ```go if url != "" { cacheKey := fmt.Sprintf(cacheKeyFormat, trackID) _ = host.CacheSetString(cacheKey, url, 86400) } return url ```
gemini-code-assist[bot] (Migrated from github.com) commented 2026-02-08 16:36:51 -07:00

security-medium medium

The username and trackID variables are concatenated directly into the query string of the Subsonic API call without URL encoding. This makes the application vulnerable to HTTP Parameter Injection (HPP). An attacker could provide a malicious trackID (e.g., id1&u=admin) to inject additional parameters into the internal API request, potentially bypassing access controls or manipulating the API's behavior.

Note: You will need to import the net/url package to use url.QueryEscape.

data, err := host.SubsonicAPICall(fmt.Sprintf("getSong?u=%s&id=%s", url.QueryEscape(username), url.QueryEscape(trackID)))
![security-medium](https://www.gstatic.com/codereviewagent/security-medium-priority.svg) ![medium](https://www.gstatic.com/codereviewagent/medium-priority.svg) The `username` and `trackID` variables are concatenated directly into the query string of the Subsonic API call without URL encoding. This makes the application vulnerable to HTTP Parameter Injection (HPP). An attacker could provide a malicious `trackID` (e.g., `id1&u=admin`) to inject additional parameters into the internal API request, potentially bypassing access controls or manipulating the API's behavior. Note: You will need to import the `net/url` package to use `url.QueryEscape`. ```suggestion data, err := host.SubsonicAPICall(fmt.Sprintf("getSong?u=%s&id=%s", url.QueryEscape(username), url.QueryEscape(trackID))) ```
gemini-code-assist[bot] (Migrated from github.com) commented 2026-02-08 16:36:51 -07:00

security-medium medium

Similar to the getSong call, the getAlbum API call is vulnerable to HTTP Parameter Injection because the username and albumID are not URL-encoded before being concatenated into the query string.

Note: You will need to import the net/url package to use url.QueryEscape.

data, err := host.SubsonicAPICall(fmt.Sprintf("getAlbum?u=%s&id=%s", url.QueryEscape(username), url.QueryEscape(albumID)))
![security-medium](https://www.gstatic.com/codereviewagent/security-medium-priority.svg) ![medium](https://www.gstatic.com/codereviewagent/medium-priority.svg) Similar to the `getSong` call, the `getAlbum` API call is vulnerable to HTTP Parameter Injection because the `username` and `albumID` are not URL-encoded before being concatenated into the query string. Note: You will need to import the `net/url` package to use `url.QueryEscape`. ```suggestion data, err := host.SubsonicAPICall(fmt.Sprintf("getAlbum?u=%s&id=%s", url.QueryEscape(username), url.QueryEscape(albumID))) ```
gemini-code-assist[bot] (Migrated from github.com) commented 2026-02-08 16:36:51 -07:00

security-medium medium

The musicBrainzID is concatenated directly into the URL path for an external request to coverartarchive.org without any validation or escaping. If a malicious MusicBrainz ID containing path traversal sequences (e.g., ../../) is retrieved from the database (which can be populated from untrusted file tags), it could lead to a request being sent to an unintended endpoint on the target host, a form of Server-Side Request Forgery (SSRF).

Note: You will need to import the net/url package to use url.PathEscape.

req := pdk.NewHTTPRequest(pdk.MethodGet, fmt.Sprintf("https://coverartarchive.org/release/%s", url.PathEscape(musicBrainzID)))
![security-medium](https://www.gstatic.com/codereviewagent/security-medium-priority.svg) ![medium](https://www.gstatic.com/codereviewagent/medium-priority.svg) The `musicBrainzID` is concatenated directly into the URL path for an external request to `coverartarchive.org` without any validation or escaping. If a malicious MusicBrainz ID containing path traversal sequences (e.g., `../../`) is retrieved from the database (which can be populated from untrusted file tags), it could lead to a request being sent to an unintended endpoint on the target host, a form of Server-Side Request Forgery (SSRF). Note: You will need to import the `net/url` package to use `url.PathEscape`. ```suggestion req := pdk.NewHTTPRequest(pdk.MethodGet, fmt.Sprintf("https://coverartarchive.org/release/%s", url.PathEscape(musicBrainzID))) ```
gemini-code-assist[bot] (Migrated from github.com) commented 2026-02-08 16:36:51 -07:00

medium

The field name Song is confusing here, as the JSON key is "album". For better code clarity and maintainability, it's recommended to rename this field to Album. You will also need to update the usage of this field in getMusicBrainzIDFromAlbumID. I've added a separate comment for that.

        Album struct {

![medium](https://www.gstatic.com/codereviewagent/medium-priority.svg) The field name `Song` is confusing here, as the JSON key is `"album"`. For better code clarity and maintainability, it's recommended to rename this field to `Album`. You will also need to update the usage of this field in `getMusicBrainzIDFromAlbumID`. I've added a separate comment for that. ```go Album struct { ```
gemini-code-assist[bot] (Migrated from github.com) commented 2026-02-08 16:36:51 -07:00

medium

To correspond with the suggested change of renaming the Song field to Album in subsonicGetAlbumResponse, this line should be updated to access the field via response.Data.Album.

    return response.Data.Album.MusicBrainzId, nil

![medium](https://www.gstatic.com/codereviewagent/medium-priority.svg) To correspond with the suggested change of renaming the `Song` field to `Album` in `subsonicGetAlbumResponse`, this line should be updated to access the field via `response.Data.Album`. ```go return response.Data.Album.MusicBrainzId, nil ```
gemini-code-assist[bot] (Migrated from github.com) commented 2026-02-08 16:36:51 -07:00

medium

The TODO comment here is misleading. In Go, a string variable cannot be nil, so checking for an empty string "" is the correct and sufficient way to see if it has a value. The TODO can be removed to avoid confusion.

    if musicBrainzID == "" {

![medium](https://www.gstatic.com/codereviewagent/medium-priority.svg) The `TODO` comment here is misleading. In Go, a string variable cannot be `nil`, so checking for an empty string `""` is the correct and sufficient way to see if it has a value. The TODO can be removed to avoid confusion. ```go if musicBrainzID == "" { ```
gemini-code-assist[bot] (Migrated from github.com) commented 2026-02-08 16:36:51 -07:00

medium

There's a TODO here to name this section. It's good practice to resolve these before merging. A descriptive name would improve code readability.

// Image URL Resolution

![medium](https://www.gstatic.com/codereviewagent/medium-priority.svg) There's a `TODO` here to name this section. It's good practice to resolve these before merging. A descriptive name would improve code readability. ```go // Image URL Resolution ```
sproutsberry (Migrated from github.com) reviewed 2026-02-08 16:44:26 -07:00
@@ -54,3 +51,4 @@
pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for uguu.se artwork: %s", trackID))
return cachedURL
}
sproutsberry (Migrated from github.com) commented 2026-02-08 16:44:26 -07:00

Not relevant

Not relevant
sproutsberry (Migrated from github.com) reviewed 2026-02-08 16:44:34 -07:00
sproutsberry (Migrated from github.com) commented 2026-02-08 16:44:34 -07:00

Intentional

Intentional
sproutsberry commented 2026-02-09 11:23:23 -07:00 (Migrated from github.com)

Ready to go!

Ready to go!
github-actions[bot] commented 2026-02-09 11:45:49 -07:00 (Migrated from github.com)

Download the plugin for this PR: discord-rich-presence.zip

Built from 371dff6352 on 2026-02-09T19:59:26Z

Download the plugin for this PR: [discord-rich-presence.zip](https://nightly.link/navidrome/discord-rich-presence-plugin/actions/runs/21838440087/discord-rich-presence.zip) Built from 371dff635237b4b5bf6ebd69ccacf203f6853614 on 2026-02-09T19:59:26Z <!-- Sticky Pull Request Comment -->
deluan (Migrated from github.com) reviewed 2026-02-09 12:49:20 -07:00
@@ -64,6 +65,11 @@
"title": "Upload artwork to uguu.se (enable if Navidrome is not publicly accessible)",
deluan (Migrated from github.com) commented 2026-02-09 12:49:21 -07:00

The default should be false.

The default should be false.
deluan commented 2026-02-09 12:59:37 -07:00 (Migrated from github.com)

Thanks! Looks good. I'll test it out in my server before merging.

Thanks! Looks good. I'll test it out in my server before merging.
deluan commented 2026-02-11 21:09:49 -07:00 (Migrated from github.com)

Trying this PR, it seems that it does not guard against (fairly frequent) CAA errors:

time="2026-02-12T04:07:05Z" level=trace msg="Plugin call failed" error="Get \"https://coverartarchive.org/release/699a3fb0-629c-48e0-9da2-3c9eede312de\": read tcp 172.29.0.2:34248->142.132.240.1:443: read: connection reset by peer (recovered by wazero)\nwasm stack trace:\n\textism:host/env.http_request(i64,i64) i64\n\tmain.(*github.com/extism/go-pdk.HTTPRequest).Send(i32,i32)\n\tmain.github.com/navidrome/navidrome/plugins/pdk/go/scrobbler._NdScrobblerNowPlaying#wasmexport() i32" function=nd_scrobbler_now_playing navidromeDuration="358.472µs" plugin=discord-rich-presence pluginDuration=208.7ms requestId=decdb20eb6d0/rR85aSUUDz-009429

This causes the activity to not be updated. We may have to capture this error somehow and fallback gracefully to the other methods.

Trying this PR, it seems that it does not guard against (fairly frequent) CAA errors: ``` time="2026-02-12T04:07:05Z" level=trace msg="Plugin call failed" error="Get \"https://coverartarchive.org/release/699a3fb0-629c-48e0-9da2-3c9eede312de\": read tcp 172.29.0.2:34248->142.132.240.1:443: read: connection reset by peer (recovered by wazero)\nwasm stack trace:\n\textism:host/env.http_request(i64,i64) i64\n\tmain.(*github.com/extism/go-pdk.HTTPRequest).Send(i32,i32)\n\tmain.github.com/navidrome/navidrome/plugins/pdk/go/scrobbler._NdScrobblerNowPlaying#wasmexport() i32" function=nd_scrobbler_now_playing navidromeDuration="358.472µs" plugin=discord-rich-presence pluginDuration=208.7ms requestId=decdb20eb6d0/rR85aSUUDz-009429 ``` This causes the activity to not be updated. We may have to capture this error somehow and fallback gracefully to the other methods.
sproutsberry commented 2026-02-11 21:45:29 -07:00 (Migrated from github.com)

I'm already handling HTTP >=400 gracefully, but I had no idea the API timeouts so readily (and Go doesn't handle it well, I guess)

I'll look into implementing this in a few. I haven't had any problems with the API personally, so I may not be able to debug.

I'm already handling HTTP >=400 gracefully, but I had no idea the API timeouts so readily (and Go doesn't handle it well, I guess) I'll look into implementing this in a few. I haven't had any problems with the API personally, so I may not be able to debug.
sproutsberry commented 2026-02-11 21:47:45 -07:00 (Migrated from github.com)

Ah, MusicBrainz has a maintenance window right around now where a lot of requests fail. Forgot about that.

Ah, MusicBrainz has a maintenance window right around now where a lot of requests fail. Forgot about that.
sproutsberry commented 2026-02-12 18:03:44 -07:00 (Migrated from github.com)

I've implemented a 5 second timeout which should guard for failed and timed out requests. Give it another try and let me know how it goes.

I've implemented a 5 second timeout which should guard for failed and timed out requests. Give it another try and let me know how it goes.
sproutsberry commented 2026-02-12 19:03:55 -07:00 (Migrated from github.com)

This now requires the scheduler, which means it isn't compiling with the recommended tinygo command (which I didn't notice since I've been using standard Go). Since the pdk's HTTP request API is so limited, there aren't really any other options I can use to handle network failures gracefully. This might be bigger in scope than expected haha

This now requires the scheduler, which means it isn't compiling with the recommended tinygo command (which I didn't notice since I've been using standard Go). Since the pdk's HTTP request API is so limited, there aren't really any other options I can use to handle network failures gracefully. This might be bigger in scope than expected haha
deluan commented 2026-02-23 19:19:11 -07:00 (Migrated from github.com)

Well, I may add a Navidrome host function for HTTP calls, with more options than the native Extism one. Once I get that in place I'll ping you here.

Well, I may add a Navidrome host function for HTTP calls, with more options than the native Extism one. Once I get that in place I'll ping you here.

Pull request closed

Sign in to join this conversation.