Skip to content

Commit bd2b15f

Browse files
committed
feat: beets info --links to make external ids links
1 parent 91d3c86 commit bd2b15f

5 files changed

Lines changed: 83 additions & 4 deletions

File tree

beets/ui/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,20 @@ def colorize(color_name: ColorName, text: str) -> str:
585585
return text
586586

587587

588+
def terminal_link(url: str, text: str | None = None) -> str:
589+
"""Create a clickable terminal hyperlink using a OSC 8 escape sequence.
590+
591+
`text` falls back to `url` if `None`.
592+
"""
593+
display = text if text is not None else url
594+
595+
return (
596+
f"{COLOR_ESCAPE}]8;;{url}{COLOR_ESCAPE}\\"
597+
f"{display}"
598+
f"{COLOR_ESCAPE}]8;;{COLOR_ESCAPE}\\"
599+
)
600+
601+
588602
def uncolorize(colored_text):
589603
"""Remove colors from a string."""
590604
# Define a regular expression to match ANSI codes.

beetsplug/info.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@
2323
from beets.plugins import BeetsPlugin
2424
from beets.util import displayable_path, normpath, syspath
2525

26+
# Mapping from beets field names to URL templates.
27+
FIELD_LINK_TEMPLATES: dict[str, str] = {
28+
"mb_trackid": "https://musicbrainz.org/recording/{value}",
29+
"mb_albumid": "https://musicbrainz.org/release/{value}",
30+
"mb_artistid": "https://musicbrainz.org/artist/{value}",
31+
"mb_albumartistid": "https://musicbrainz.org/artist/{value}",
32+
"mb_releasetrackid": "https://musicbrainz.org/track/{value}",
33+
"mb_releasegroupid": "https://musicbrainz.org/release-group/{value}",
34+
"mb_workid": "https://musicbrainz.org/work/{value}",
35+
"discogs_albumid": "https://www.discogs.com/release/{value}",
36+
"discogs_artistid": "https://www.discogs.com/artist/{value}",
37+
"discogs_labelid": "https://www.discogs.com/label/{value}",
38+
}
39+
2640

2741
def tag_data(lib, args, album=False):
2842
query = []
@@ -92,13 +106,16 @@ def update_summary(summary, tags):
92106
return summary
93107

94108

95-
def print_data(data, item=None, fmt=None):
109+
def print_data(data, item=None, fmt=None, links=False):
96110
"""Print, with optional formatting, the fields of a single element.
97111
98112
If no format string `fmt` is passed, the entries on `data` are printed one
99113
in each line, with the format 'field: value'. If `fmt` is not `None`, the
100114
`item` is printed according to `fmt`, using the `Item.__format__`
101115
machinery.
116+
117+
When `links == True`, external ID fields will be rendered as clickable
118+
terminal hyperlinks using OSC 8 escape sequences.
102119
"""
103120
if fmt:
104121
# use fmt specified by the user
@@ -110,7 +127,10 @@ def print_data(data, item=None, fmt=None):
110127
for key, value in data.items():
111128
if isinstance(value, list):
112129
formatted[key] = "; ".join(value)
113-
if value is not None:
130+
elif value is not None:
131+
if links and key in FIELD_LINK_TEMPLATES:
132+
url = FIELD_LINK_TEMPLATES[key].format(value=value)
133+
value = ui.terminal_link(url, value)
114134
formatted[key] = value
115135

116136
if len(formatted) == 0:
@@ -181,6 +201,11 @@ def commands(self):
181201
action="store_true",
182202
help="show only the keys",
183203
)
204+
cmd.parser.add_option(
205+
"--links",
206+
action="store_true",
207+
help="make ID fields (MusicBrainz, Discogs) clickable terminal hyperlinks",
208+
)
184209
cmd.parser.add_format_option(target="item")
185210
return [cmd]
186211

@@ -231,8 +256,8 @@ def run(self, lib, opts, args):
231256
print_data_keys(data, item)
232257
else:
233258
fmt = [opts.format][0] if opts.format else None
234-
print_data(data, item, fmt)
259+
print_data(data, item, fmt, links=opts.links)
235260
first = False
236261

237262
if opts.summarize:
238-
print_data(summary)
263+
print_data(summary, links=opts.links)

docs/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Unreleased
1212
New features
1313
~~~~~~~~~~~~
1414

15+
- :doc:`plugins/info`: Added ``--links`` commandline flag, which results in
16+
external ids (musicbrainz,discogs) being clickable links in the terminal.
1517
- :doc:`plugins/lastgenre`: Added ``cleanup_existing`` configuration flag to
1618
allow whitelist canonicalization of existing genres.
1719
- Add native support for multiple genres per album/track. The ``genres`` field

docs/plugins/info.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ Additional command-line options include:
4646
item. This uses the same template syntax as beets’ :doc:`path formats
4747
</reference/pathformat>`.
4848
- ``--keys-only`` or ``-k``: Show the name of the tags without the values.
49+
- ``--links``: Make external ids (Discogs/Musicbrainz) clickable links in the
50+
terminal.
4951

5052
.. _id3v2: http://id3v2.sourceforge.net
5153

test/plugins/test_info.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from mediafile import MediaFile
1717

18+
from beets import ui
1819
from beets.test.helper import IOMixin, PluginTestCase
1920
from beets.util import displayable_path
2021

@@ -116,3 +117,38 @@ def test_custom_format(self):
116117
"$track. $title - $artist ($length)",
117118
)
118119
assert "02. tïtle 0 - the artist (0:01)\n" == out
120+
121+
def test_links(self):
122+
(item,) = self.add_item_fixtures()
123+
item.mb_albumid = "album-uuid"
124+
item.mb_trackid = "track-uuid"
125+
item.discogs_albumid = 99999
126+
item.album = "MyAlbum"
127+
item.store()
128+
129+
out = self.run_with_output(
130+
"info",
131+
"--library",
132+
"--include-keys",
133+
"mb_albumid,mb_trackid,discogs_albumid,album",
134+
"--links",
135+
)
136+
137+
# ID fields are wrapped in terminal hyperlinks
138+
mb_album_link = ui.terminal_link(
139+
"https://musicbrainz.org/release/album-uuid", "album-uuid"
140+
)
141+
assert f"mb_albumid: {mb_album_link}" in out
142+
143+
mb_track_link = ui.terminal_link(
144+
"https://musicbrainz.org/recording/track-uuid", "track-uuid"
145+
)
146+
assert f"mb_trackid: {mb_track_link}" in out
147+
148+
discogs_link = ui.terminal_link(
149+
"https://www.discogs.com/release/99999", "99999"
150+
)
151+
assert f"discogs_albumid: {discogs_link}" in out
152+
153+
# Non-ID fields remain plain text
154+
assert "album: MyAlbum" in out

0 commit comments

Comments
 (0)