mirror of
https://github.com/beetbox/beets.git
synced 2026-03-26 15:24:05 +01:00
Merge a76ad95af3 into 03b1ab012c
This commit is contained in:
commit
893f814b64
7 changed files with 88 additions and 6 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -80,6 +80,9 @@ venv/
|
|||
.venv/
|
||||
ENV/
|
||||
|
||||
# uv
|
||||
uv.lock
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ from beets import config, library, logging, plugins, util
|
|||
from beets.dbcore import db
|
||||
from beets.dbcore import query as db_query
|
||||
from beets.util import as_string
|
||||
from beets.util.color import colorize
|
||||
from beets.util.color import colorize, terminal_link
|
||||
from beets.util.deprecation import deprecate_for_maintainers
|
||||
from beets.util.diff import get_model_changes
|
||||
from beets.util.functemplate import template
|
||||
|
|
|
|||
|
|
@ -156,6 +156,20 @@ def colorize(color_name: ColorName, text: str) -> str:
|
|||
return text
|
||||
|
||||
|
||||
def terminal_link(url: str, text: str | None = None) -> str:
|
||||
"""Create a clickable terminal hyperlink using a OSC 8 escape sequence.
|
||||
|
||||
`text` falls back to `url` if `None`.
|
||||
"""
|
||||
display = text if text is not None else url
|
||||
|
||||
return (
|
||||
f"{COLOR_ESCAPE}]8;;{url}{COLOR_ESCAPE}\\"
|
||||
f"{display}"
|
||||
f"{COLOR_ESCAPE}]8;;{COLOR_ESCAPE}\\"
|
||||
)
|
||||
|
||||
|
||||
def uncolorize(colored_text: str) -> str:
|
||||
"""Remove colors from a string."""
|
||||
# Define a regular expression to match ANSI codes.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,20 @@ from beets.library import Item
|
|||
from beets.plugins import BeetsPlugin
|
||||
from beets.util import displayable_path, normpath, syspath
|
||||
|
||||
# Mapping from beets field names to URL templates.
|
||||
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}",
|
||||
}
|
||||
|
||||
|
||||
def tag_data(lib, args, album=False):
|
||||
query = []
|
||||
|
|
@ -92,13 +106,16 @@ def update_summary(summary, tags):
|
|||
return summary
|
||||
|
||||
|
||||
def print_data(data, item=None, fmt=None):
|
||||
def print_data(data, item=None, fmt=None, links=False):
|
||||
"""Print, with optional formatting, the fields of a single element.
|
||||
|
||||
If no format string `fmt` is passed, the entries on `data` are printed one
|
||||
in each line, with the format 'field: value'. If `fmt` is not `None`, the
|
||||
`item` is printed according to `fmt`, using the `Item.__format__`
|
||||
machinery.
|
||||
|
||||
When `links == True`, external ID fields will be rendered as clickable
|
||||
terminal hyperlinks using OSC 8 escape sequences.
|
||||
"""
|
||||
if fmt:
|
||||
# use fmt specified by the user
|
||||
|
|
@ -110,7 +127,11 @@ def print_data(data, item=None, fmt=None):
|
|||
for key, value in data.items():
|
||||
if isinstance(value, list):
|
||||
formatted[key] = "; ".join(value)
|
||||
if value is not None:
|
||||
elif value is not None:
|
||||
if links and key in FIELD_LINK_TEMPLATES:
|
||||
value_str = str(value)
|
||||
url = FIELD_LINK_TEMPLATES[key].format(value=value_str)
|
||||
value = ui.terminal_link(url, value_str)
|
||||
formatted[key] = value
|
||||
|
||||
if len(formatted) == 0:
|
||||
|
|
@ -181,6 +202,11 @@ class InfoPlugin(BeetsPlugin):
|
|||
action="store_true",
|
||||
help="show only the keys",
|
||||
)
|
||||
cmd.parser.add_option(
|
||||
"--links",
|
||||
action="store_true",
|
||||
help="make ID fields (MusicBrainz, Discogs) clickable terminal hyperlinks",
|
||||
)
|
||||
cmd.parser.add_format_option(target="item")
|
||||
return [cmd]
|
||||
|
||||
|
|
@ -231,8 +257,8 @@ class InfoPlugin(BeetsPlugin):
|
|||
print_data_keys(data, item)
|
||||
else:
|
||||
fmt = [opts.format][0] if opts.format else None
|
||||
print_data(data, item, fmt)
|
||||
print_data(data, item, fmt, links=opts.links)
|
||||
first = False
|
||||
|
||||
if opts.summarize:
|
||||
print_data(summary)
|
||||
print_data(summary, links=opts.links)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ New features
|
|||
:bug:`2661`
|
||||
- :doc:`plugins/play`: Added ``-R``/``--randomize`` flag to shuffle the playlist
|
||||
order before passing it to the player.
|
||||
- :doc:`plugins/info`: Added ``--links`` command-line flag, which results in
|
||||
external IDs (MusicBrainz, Discogs) being clickable links in the terminal.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
|
@ -103,7 +105,6 @@ Bug fixes
|
|||
|
||||
New features
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- :doc:`plugins/lastgenre`: Added ``cleanup_existing`` configuration flag to
|
||||
allow whitelist canonicalization of existing genres.
|
||||
- Add native support for multiple genres per album/track. The ``genres`` field
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ Additional command-line options include:
|
|||
item. This uses the same template syntax as beets’ :doc:`path formats
|
||||
</reference/pathformat>`.
|
||||
- ``--keys-only`` or ``-k``: Show the name of the tags without the values.
|
||||
- ``--links``: Make external IDs (Discogs/MusicBrainz) clickable links in the
|
||||
terminal.
|
||||
|
||||
.. _id3v2: https://sourceforge.net/projects/id3v2/
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
from mediafile import MediaFile
|
||||
|
||||
from beets import ui
|
||||
from beets.test.helper import IOMixin, PluginTestCase
|
||||
from beets.util import displayable_path
|
||||
|
||||
|
|
@ -116,3 +117,38 @@ class InfoTest(IOMixin, PluginTestCase):
|
|||
"$track. $title - $artist ($length)",
|
||||
)
|
||||
assert "02. tïtle 0 - the artist (0:01)\n" == out
|
||||
|
||||
def test_links(self):
|
||||
(item,) = self.add_item_fixtures()
|
||||
item.mb_albumid = "album-uuid"
|
||||
item.mb_trackid = "track-uuid"
|
||||
item.discogs_albumid = 99999
|
||||
item.album = "MyAlbum"
|
||||
item.store()
|
||||
|
||||
out = self.run_with_output(
|
||||
"info",
|
||||
"--library",
|
||||
"--include-keys",
|
||||
"mb_albumid,mb_trackid,discogs_albumid,album",
|
||||
"--links",
|
||||
)
|
||||
|
||||
# ID fields are wrapped in terminal hyperlinks
|
||||
mb_album_link = ui.terminal_link(
|
||||
"https://musicbrainz.org/release/album-uuid", "album-uuid"
|
||||
)
|
||||
assert f"mb_albumid: {mb_album_link}" in out
|
||||
|
||||
mb_track_link = ui.terminal_link(
|
||||
"https://musicbrainz.org/recording/track-uuid", "track-uuid"
|
||||
)
|
||||
assert f"mb_trackid: {mb_track_link}" in out
|
||||
|
||||
discogs_link = ui.terminal_link(
|
||||
"https://www.discogs.com/release/99999", "99999"
|
||||
)
|
||||
assert f"discogs_albumid: {discogs_link}" in out
|
||||
|
||||
# Non-ID fields remain plain text
|
||||
assert "album: MyAlbum" in out
|
||||
|
|
|
|||
Loading…
Reference in a new issue