Skip to content

[Feature] Musicbrainz/Discogs terminal links#6453

Draft
Nukesor wants to merge 2 commits intobeetbox:masterfrom
Nukesor:musicbrainz_hyperplinks
Draft

[Feature] Musicbrainz/Discogs terminal links#6453
Nukesor wants to merge 2 commits intobeetbox:masterfrom
Nukesor:musicbrainz_hyperplinks

Conversation

@Nukesor
Copy link
Copy Markdown
Contributor

@Nukesor Nukesor commented Mar 21, 2026

Description

This MR adds a neat little QoL feature that I was missing from beets.
When calling beet info --links, external ids are now clickable terminal links, which directly open the respective website.

I found myself needing this a lot when investigating why something got tagged wrong or when I was unsure whether I messed up a mapping.

Feel free to let me know if anything is missing.

Regarding the implementation, I considered using a library for the ANSI stuff, but since beets seems to roll its own escape handling, I decided to do the same :)

Oh, and I added the uv.lock to the gitignore. I regularily accidentally check it in and I need uv to manage the python version. Let me know if this works out for you!

Cheers!

To Do

  • Documentation. (If you've added a new command-line flag, for example, find the appropriate page under docs/ to describe it.)
  • Changelog. (Add an entry to docs/changelog.rst to the bottom of one of the lists near the top of the document.)
  • Tests. (Very much encouraged but not strictly required.)

@Nukesor Nukesor requested a review from a team as a code owner March 21, 2026 15:06
Copilot AI review requested due to automatic review settings March 21, 2026 15:06
@Nukesor Nukesor force-pushed the musicbrainz_hyperplinks branch from a52a4ad to bd2b15f Compare March 21, 2026 15:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

grug see PR add small QoL for beet info: make MusicBrainz/Discogs ids show as clickable terminal link when user pass --links.

Changes:

  • add --links flag to info plugin and wrap known external-id fields with OSC 8 hyperlink escape
  • add ui.terminal_link() helper for building terminal hyperlinks
  • add docs + changelog entry + test for link output

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
beetsplug/info.py add --links option and map id fields to URL templates, then wrap values with ui.terminal_link()
beets/ui/__init__.py add terminal_link() helper that emits OSC 8 escape sequences
test/plugins/test_info.py add regression test asserting id fields become terminal links with --links
docs/plugins/info.rst document new --links option
docs/changelog.rst add unreleased changelog entry for new flag
.gitignore ignore uv.lock

@Nukesor
Copy link
Copy Markdown
Contributor Author

Nukesor commented Mar 21, 2026

I also considered adding a config for the info plugin, since I pretty much want those links to be there whenever I call info. Since the plugin didn't have a config yet, I wanted to wait for your feedback on this.

@Nukesor Nukesor force-pushed the musicbrainz_hyperplinks branch from aa2e263 to 3c61ef1 Compare March 21, 2026 15:13
@snejus
Copy link
Copy Markdown
Member

snejus commented Mar 21, 2026

Had a quick glance - great direction! This has indeed been missing. On the other hand, I don't want this to be available through info only - I also want to be able to run beet list -f '$url' for example.

I think you can generalize this by adding url properties on Album and Item and adding "url" to their getters.

Comment on lines +27 to +39
FIELD_LINK_TEMPLATES: dict[str, str] = {
"mb_trackid": "https://musicbrainz.org/recording/{value}",
"mb_albumid": "https://musicbrainz.org/release/{value}",
"mb_artistid": "https://musicbrainz.org/artist/{value}",
"mb_albumartistid": "https://musicbrainz.org/artist/{value}",
"mb_releasetrackid": "https://musicbrainz.org/track/{value}",
"mb_releasegroupid": "https://musicbrainz.org/release-group/{value}",
"mb_workid": "https://musicbrainz.org/work/{value}",
"discogs_albumid": "https://www.discogs.com/release/{value}",
"discogs_artistid": "https://www.discogs.com/artist/{value}",
"discogs_labelid": "https://www.discogs.com/label/{value}",
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We want to account for all possible data sources here, I think. See the logic I use in my CLI wrapper around beets database:

CASE
      WHEN ${colmap[data_source]} == 'Discogs' THEN printf('https://discogs.com/release/%s',${colmap[release_id]})
      WHEN ${colmap[data_source]} == 'Deezer' THEN printf('https://deezer.com/us/%s',iif(length(mb_albumid)>0,'album/'||mb_albumid,'track/'||mb_trackid))
      WHEN ${colmap[data_source]} == 'Spotify' THEN printf('https://open.spotify.com/%s',iif(length(mb_albumid)>0,'album/'||mb_albumid,'track/'||mb_trackid))
      WHEN ${colmap[data_source]} == 'MusicBrainz' THEN printf('https://musicbrainz.org/%s',iif(length(mb_albumid)>0,'release/'||mb_albumid,'recording/'||mb_trackid))
      ELSE iif(instr(${colmap[release_id]}, '#'), substr(${colmap[release_id]}, 0, instr(${colmap[release_id]}, '#')), ${colmap[release_id]})
    END

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

^ This applies only to track and album URLs but you can see the idea

@Nukesor Nukesor force-pushed the musicbrainz_hyperplinks branch from 3c61ef1 to a76ad95 Compare March 21, 2026 15:27
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.74%. Comparing base (03b1ab0) to head (a76ad95).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6453      +/-   ##
==========================================
+ Coverage   69.71%   69.74%   +0.02%     
==========================================
  Files         145      145              
  Lines       18515    18524       +9     
  Branches     3015     3016       +1     
==========================================
+ Hits        12908    12919      +11     
+ Misses       4972     4971       -1     
+ Partials      635      634       -1     
Files with missing lines Coverage Δ
beets/ui/__init__.py 82.23% <100.00%> (ø)
beets/util/color.py 100.00% <100.00%> (ø)
beetsplug/info.py 83.46% <100.00%> (+0.81%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Nukesor
Copy link
Copy Markdown
Contributor Author

Nukesor commented Mar 21, 2026

I also want to be able to run beet list -f '$url' for example.

What exactly would $url be here? To be honest, I didn't fully get how list -f is supposed to be used yet.

I think you can generalize this by adding url properties on Album and Item and adding "url" to their getters.

Which getters do you mean exactly? I just looked at the Album/Item models and they seem to have generic _getters/get/__getitem__ functions, but no getter functions for individual keys. I'm unsure how to go ahead and inject a config/a CLI parameter into that to toggle the formatting.

@snejus
Copy link
Copy Markdown
Member

snejus commented Mar 21, 2026

I also want to be able to run beet list -f '$url' for example.

What exactly would $url be here? To be honest, I didn't fully get how list -f is supposed to be used yet.

I think you can generalize this by adding url properties on Album and Item and adding "url" to their getters.

Which getters do you mean exactly? I just looked at the Album/Item models and they seem to have generic _getters/get/__getitem__ functions, but no getter functions for individual keys. I'm unsure how to go ahead and inject a config/a CLI parameter into that to toggle the formatting.

See Item._getters:

    @classmethod
    def _getters(cls):
        getters = plugins.item_field_getters()
        getters["singleton"] = lambda i: i.album_id is None
        getters["filesize"] = Item.try_filesize  # In bytes.
        return getters

and then

$ beet list -f 'title: $title, size: $filesize' | head -10
title: Spadge, size: 411053212
title: The Hakke Show, size: 71675418
title: 01 SN MIX, size: 318295397
title: SN MIX 07, size: 445052073
title: Solar Hive, size: 6860941
title: Live, size: 42281264
title: DRILLCAST 67, size: 63227889
title: Hardcore 002, size: 43728569
title: Hardcore 081, size: 30001318
title: Hardcore 097, size: 27795958

@JOJ0
Copy link
Copy Markdown
Member

JOJ0 commented Mar 30, 2026

Oh how nice! I'm dreaming of this feature for years now and never got to submit something. Thanks @Nukesor great idea! Also I agree with @snejus, we have more possible datasources and all deserve a link! Will be super useful!

@Nukesor
Copy link
Copy Markdown
Contributor Author

Nukesor commented Apr 2, 2026

I'll come back to this soon. I got super side-tracked with another project 😅

@snejus snejus marked this pull request as draft April 3, 2026 00:52
@snejus snejus marked this pull request as draft April 3, 2026 00:52
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.

4 participants