mirror of
https://github.com/beetbox/beets.git
synced 2026-01-08 17:08:12 +01:00
Improve default pretty diff colors (#5949)
- Switch default `text_diff_added` color from bold **red** to bold **green**; unify all diff/case comparisons to use `text_diff_added` and `text_diff_removed` colors consistently. - Simplify color handling and setup: introduce cached color config (validation + legacy normalization), consolidate regexes - Remove unused colors names - Update `ui` configuration docs. ### `beet write -p year:2000..2005` #### Before <img width="1261" height="460" alt="image" src="https://github.com/user-attachments/assets/70207350-6d9a-48d8-a314-457ac07c5ad1" /> #### After <img width="1257" height="536" alt="image" src="https://github.com/user-attachments/assets/efcf3453-016a-490f-84ab-dedfb2aca97b" /> ### `beet move -p albumtype:broadcast` #### Before <img width="1103" height="505" alt="image" src="https://github.com/user-attachments/assets/9e76c5d6-d878-4535-9da3-a949e6c0830c" /> #### After <img width="1134" height="508" alt="image" src="https://github.com/user-attachments/assets/3dc03988-8011-4072-8840-6f0a0d12350f" /> ## Summary by Sourcery Improve default diff colors from red to green and streamline color handling by refactoring ANSI code management, removing legacy logic, and unifying diff highlighting. Also extract a cached changed_prefix property and update UI config documentation. Enhancements: - Introduce cached get_color_config and consolidate ANSI escape regex patterns to simplify color configuration parsing - Refactor diff highlighting to consistently use text_diff_added and text_diff_removed and simplify _colordiff implementation - Add ChangeRepresentation.changed_prefix cached property for consistent ‘not equal’ prefix formatting Documentation: - Update UI configuration documentation to reflect new default colors and removed settings Chores: - Remove unused color names and legacy normalization code
This commit is contained in:
commit
2b4758d440
5 changed files with 107 additions and 160 deletions
|
|
@ -127,19 +127,12 @@ ui:
|
|||
action_default: ['bold', 'cyan']
|
||||
action: ['bold', 'cyan']
|
||||
# New Colors
|
||||
text: ['normal']
|
||||
text_faint: ['faint']
|
||||
import_path: ['bold', 'blue']
|
||||
import_path_items: ['bold', 'blue']
|
||||
added: ['green']
|
||||
removed: ['red']
|
||||
changed: ['yellow']
|
||||
added_highlight: ['bold', 'green']
|
||||
removed_highlight: ['bold', 'red']
|
||||
changed_highlight: ['bold', 'yellow']
|
||||
text_diff_added: ['bold', 'red']
|
||||
text_diff_added: ['bold', 'green']
|
||||
text_diff_removed: ['bold', 'red']
|
||||
text_diff_changed: ['bold', 'red']
|
||||
action_description: ['white']
|
||||
import:
|
||||
indentation:
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ import textwrap
|
|||
import traceback
|
||||
import warnings
|
||||
from difflib import SequenceMatcher
|
||||
from typing import Any, Callable
|
||||
from functools import cache
|
||||
from itertools import chain
|
||||
from typing import Any, Callable, Literal
|
||||
|
||||
import confuse
|
||||
|
||||
|
|
@ -438,7 +440,7 @@ def input_select_objects(prompt, objs, rep, prompt_all=None):
|
|||
# ANSI terminal colorization code heavily inspired by pygments:
|
||||
# https://bitbucket.org/birkenfeld/pygments-main/src/default/pygments/console.py
|
||||
# (pygments is by Tim Hatch, Armin Ronacher, et al.)
|
||||
COLOR_ESCAPE = "\x1b["
|
||||
COLOR_ESCAPE = "\x1b"
|
||||
LEGACY_COLORS = {
|
||||
"black": ["black"],
|
||||
"darkred": ["red"],
|
||||
|
|
@ -463,7 +465,7 @@ LEGACY_COLORS = {
|
|||
"white": ["bold", "white"],
|
||||
}
|
||||
# All ANSI Colors.
|
||||
ANSI_CODES = {
|
||||
CODE_BY_COLOR = {
|
||||
# Styles.
|
||||
"normal": 0,
|
||||
"bold": 1,
|
||||
|
|
@ -494,11 +496,17 @@ ANSI_CODES = {
|
|||
"bg_cyan": 46,
|
||||
"bg_white": 47,
|
||||
}
|
||||
RESET_COLOR = f"{COLOR_ESCAPE}39;49;00m"
|
||||
|
||||
# These abstract COLOR_NAMES are lazily mapped on to the actual color in COLORS
|
||||
# as they are defined in the configuration files, see function: colorize
|
||||
COLOR_NAMES = [
|
||||
RESET_COLOR = f"{COLOR_ESCAPE}[39;49;00m"
|
||||
# Precompile common ANSI-escape regex patterns
|
||||
ANSI_CODE_REGEX = re.compile(rf"({COLOR_ESCAPE}\[[;0-9]*m)")
|
||||
ESC_TEXT_REGEX = re.compile(
|
||||
rf"""(?P<pretext>[^{COLOR_ESCAPE}]*)
|
||||
(?P<esc>(?:{ANSI_CODE_REGEX.pattern})+)
|
||||
(?P<text>[^{COLOR_ESCAPE}]+)(?P<reset>{re.escape(RESET_COLOR)})
|
||||
(?P<posttext>[^{COLOR_ESCAPE}]*)""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
ColorName = Literal[
|
||||
"text_success",
|
||||
"text_warning",
|
||||
"text_error",
|
||||
|
|
@ -507,76 +515,54 @@ COLOR_NAMES = [
|
|||
"action_default",
|
||||
"action",
|
||||
# New Colors
|
||||
"text",
|
||||
"text_faint",
|
||||
"import_path",
|
||||
"import_path_items",
|
||||
"action_description",
|
||||
"added",
|
||||
"removed",
|
||||
"changed",
|
||||
"added_highlight",
|
||||
"removed_highlight",
|
||||
"changed_highlight",
|
||||
"text_diff_added",
|
||||
"text_diff_removed",
|
||||
"text_diff_changed",
|
||||
]
|
||||
COLORS: dict[str, list[str]] | None = None
|
||||
|
||||
|
||||
def _colorize(color, text):
|
||||
"""Returns a string that prints the given text in the given color
|
||||
in a terminal that is ANSI color-aware. The color must be a list of strings
|
||||
from ANSI_CODES.
|
||||
@cache
|
||||
def get_color_config() -> dict[ColorName, str]:
|
||||
"""Parse and validate color configuration, converting names to ANSI codes.
|
||||
|
||||
Processes the UI color configuration, handling both new list format and
|
||||
legacy single-color format. Validates all color names against known codes
|
||||
and raises an error for any invalid entries.
|
||||
"""
|
||||
# Construct escape sequence to be put before the text by iterating
|
||||
# over all "ANSI codes" in `color`.
|
||||
escape = ""
|
||||
for code in color:
|
||||
escape = f"{escape}{COLOR_ESCAPE}{ANSI_CODES[code]}m"
|
||||
return f"{escape}{text}{RESET_COLOR}"
|
||||
colors_by_color_name: dict[ColorName, list[str]] = {
|
||||
k: (v if isinstance(v, list) else LEGACY_COLORS.get(v, [v]))
|
||||
for k, v in config["ui"]["colors"].flatten().items()
|
||||
}
|
||||
|
||||
if invalid_colors := (
|
||||
set(chain.from_iterable(colors_by_color_name.values()))
|
||||
- CODE_BY_COLOR.keys()
|
||||
):
|
||||
raise UserError(
|
||||
f"Invalid color(s) in configuration: {', '.join(invalid_colors)}"
|
||||
)
|
||||
|
||||
return {
|
||||
n: ";".join(str(CODE_BY_COLOR[c]) for c in colors)
|
||||
for n, colors in colors_by_color_name.items()
|
||||
}
|
||||
|
||||
|
||||
def colorize(color_name, text):
|
||||
"""Colorize text if colored output is enabled. (Like _colorize but
|
||||
conditional.)
|
||||
def colorize(color_name: ColorName, text: str) -> str:
|
||||
"""Apply ANSI color formatting to text based on configuration settings.
|
||||
|
||||
Returns colored text when color output is enabled and NO_COLOR environment
|
||||
variable is not set, otherwise returns plain text unchanged.
|
||||
"""
|
||||
if config["ui"]["color"] and "NO_COLOR" not in os.environ:
|
||||
global COLORS
|
||||
if not COLORS:
|
||||
# Read all color configurations and set global variable COLORS.
|
||||
COLORS = dict()
|
||||
for name in COLOR_NAMES:
|
||||
# Convert legacy color definitions (strings) into the new
|
||||
# list-based color definitions. Do this by trying to read the
|
||||
# color definition from the configuration as unicode - if this
|
||||
# is successful, the color definition is a legacy definition
|
||||
# and has to be converted.
|
||||
try:
|
||||
color_def = config["ui"]["colors"][name].get(str)
|
||||
except (confuse.ConfigTypeError, NameError):
|
||||
# Normal color definition (type: list of unicode).
|
||||
color_def = config["ui"]["colors"][name].get(list)
|
||||
else:
|
||||
# Legacy color definition (type: unicode). Convert.
|
||||
if color_def in LEGACY_COLORS:
|
||||
color_def = LEGACY_COLORS[color_def]
|
||||
else:
|
||||
raise UserError("no such color %s", color_def)
|
||||
for code in color_def:
|
||||
if code not in ANSI_CODES.keys():
|
||||
raise ValueError("no such ANSI code %s", code)
|
||||
COLORS[name] = color_def
|
||||
# In case a 3rd party plugin is still passing the actual color ('red')
|
||||
# instead of the abstract color name ('text_error')
|
||||
color = COLORS.get(color_name)
|
||||
if not color:
|
||||
log.debug("Invalid color_name: {}", color_name)
|
||||
color = color_name
|
||||
return _colorize(color, text)
|
||||
else:
|
||||
return text
|
||||
color_code = get_color_config()[color_name]
|
||||
return f"{COLOR_ESCAPE}[{color_code}m{text}{RESET_COLOR}"
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def uncolorize(colored_text):
|
||||
|
|
@ -589,26 +575,22 @@ def uncolorize(colored_text):
|
|||
# [;\d]* - matches a sequence consisting of one or more digits or
|
||||
# semicola
|
||||
# [A-Za-z] - matches a letter
|
||||
ansi_code_regex = re.compile(r"\x1b\[[;\d]*[A-Za-z]", re.VERBOSE)
|
||||
# Strip ANSI codes from `colored_text` using the regular expression.
|
||||
text = ansi_code_regex.sub("", colored_text)
|
||||
return text
|
||||
return ANSI_CODE_REGEX.sub("", colored_text)
|
||||
|
||||
|
||||
def color_split(colored_text, index):
|
||||
ansi_code_regex = re.compile(r"(\x1b\[[;\d]*[A-Za-z])", re.VERBOSE)
|
||||
length = 0
|
||||
pre_split = ""
|
||||
post_split = ""
|
||||
found_color_code = None
|
||||
found_split = False
|
||||
for part in ansi_code_regex.split(colored_text):
|
||||
for part in ANSI_CODE_REGEX.split(colored_text):
|
||||
# Count how many real letters we have passed
|
||||
length += color_len(part)
|
||||
if found_split:
|
||||
post_split += part
|
||||
else:
|
||||
if ansi_code_regex.match(part):
|
||||
if ANSI_CODE_REGEX.match(part):
|
||||
# This is a color code
|
||||
if part == RESET_COLOR:
|
||||
found_color_code = None
|
||||
|
|
@ -642,7 +624,7 @@ def color_len(colored_text):
|
|||
return len(uncolorize(colored_text))
|
||||
|
||||
|
||||
def _colordiff(a, b):
|
||||
def _colordiff(a: Any, b: Any) -> tuple[str, str]:
|
||||
"""Given two values, return the same pair of strings except with
|
||||
their differences highlighted in the specified color. Strings are
|
||||
highlighted intelligently to show differences; other values are
|
||||
|
|
@ -664,35 +646,21 @@ def _colordiff(a, b):
|
|||
colorize("text_diff_added", str(b)),
|
||||
)
|
||||
|
||||
a_out = []
|
||||
b_out = []
|
||||
before = ""
|
||||
after = ""
|
||||
|
||||
matcher = SequenceMatcher(lambda x: False, a, b)
|
||||
for op, a_start, a_end, b_start, b_end in matcher.get_opcodes():
|
||||
if op == "equal":
|
||||
# In both strings.
|
||||
a_out.append(a[a_start:a_end])
|
||||
b_out.append(b[b_start:b_end])
|
||||
elif op == "insert":
|
||||
# Right only.
|
||||
b_out.append(colorize("text_diff_added", b[b_start:b_end]))
|
||||
elif op == "delete":
|
||||
# Left only.
|
||||
a_out.append(colorize("text_diff_removed", a[a_start:a_end]))
|
||||
elif op == "replace":
|
||||
# Right and left differ. Colorise with second highlight if
|
||||
# it's just a case change.
|
||||
if a[a_start:a_end].lower() != b[b_start:b_end].lower():
|
||||
a_color = "text_diff_removed"
|
||||
b_color = "text_diff_added"
|
||||
else:
|
||||
a_color = b_color = "text_highlight_minor"
|
||||
a_out.append(colorize(a_color, a[a_start:a_end]))
|
||||
b_out.append(colorize(b_color, b[b_start:b_end]))
|
||||
else:
|
||||
assert False
|
||||
before_part, after_part = a[a_start:a_end], b[b_start:b_end]
|
||||
if op in {"delete", "replace"}:
|
||||
before_part = colorize("text_diff_removed", before_part)
|
||||
if op in {"insert", "replace"}:
|
||||
after_part = colorize("text_diff_added", after_part)
|
||||
|
||||
return "".join(a_out), "".join(b_out)
|
||||
before += before_part
|
||||
after += after_part
|
||||
|
||||
return before, after
|
||||
|
||||
|
||||
def colordiff(a, b):
|
||||
|
|
@ -765,19 +733,13 @@ def split_into_lines(string, width_tuple):
|
|||
"""
|
||||
first_width, middle_width, last_width = width_tuple
|
||||
words = []
|
||||
esc_text = re.compile(
|
||||
r"""(?P<pretext>[^\x1b]*)
|
||||
(?P<esc>(?:\x1b\[[;\d]*[A-Za-z])+)
|
||||
(?P<text>[^\x1b]+)(?P<reset>\x1b\[39;49;00m)
|
||||
(?P<posttext>[^\x1b]*)""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
if uncolorize(string) == string:
|
||||
# No colors in string
|
||||
words = string.split()
|
||||
else:
|
||||
# Use a regex to find escapes and the text within them.
|
||||
for m in esc_text.finditer(string):
|
||||
for m in ESC_TEXT_REGEX.finditer(string):
|
||||
# m contains four groups:
|
||||
# pretext - any text before escape sequence
|
||||
# esc - intitial escape sequence
|
||||
|
|
@ -1110,8 +1072,8 @@ def _field_diff(field, old, old_fmt, new, new_fmt):
|
|||
if isinstance(oldval, str):
|
||||
oldstr, newstr = colordiff(oldval, newstr)
|
||||
else:
|
||||
oldstr = colorize("text_error", oldstr)
|
||||
newstr = colorize("text_error", newstr)
|
||||
oldstr = colorize("text_diff_removed", oldstr)
|
||||
newstr = colorize("text_diff_added", newstr)
|
||||
|
||||
return f"{oldstr} -> {newstr}"
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import re
|
|||
import textwrap
|
||||
from collections import Counter
|
||||
from collections.abc import Sequence
|
||||
from functools import cached_property
|
||||
from itertools import chain
|
||||
from platform import python_version
|
||||
from typing import Any, NamedTuple
|
||||
|
|
@ -303,6 +304,10 @@ class ChangeRepresentation:
|
|||
TrackMatch object, accordingly.
|
||||
"""
|
||||
|
||||
@cached_property
|
||||
def changed_prefix(self) -> str:
|
||||
return ui.colorize("changed", "\u2260")
|
||||
|
||||
cur_artist = None
|
||||
# cur_album set if album, cur_title set if singleton
|
||||
cur_album = None
|
||||
|
|
@ -394,7 +399,6 @@ class ChangeRepresentation:
|
|||
"""Print out the details of the match, including changes in album name
|
||||
and artist name.
|
||||
"""
|
||||
changed_prefix = ui.colorize("changed", "\u2260")
|
||||
# Artist.
|
||||
artist_l, artist_r = self.cur_artist or "", self.match.info.artist
|
||||
if artist_r == VARIOUS_ARTISTS:
|
||||
|
|
@ -402,9 +406,8 @@ class ChangeRepresentation:
|
|||
artist_l, artist_r = "", ""
|
||||
if artist_l != artist_r:
|
||||
artist_l, artist_r = ui.colordiff(artist_l, artist_r)
|
||||
# Prefix with U+2260: Not Equal To
|
||||
left = {
|
||||
"prefix": f"{changed_prefix} Artist: ",
|
||||
"prefix": f"{self.changed_prefix} Artist: ",
|
||||
"contents": artist_l,
|
||||
"suffix": "",
|
||||
}
|
||||
|
|
@ -422,9 +425,8 @@ class ChangeRepresentation:
|
|||
and self.match.info.album != VARIOUS_ARTISTS
|
||||
):
|
||||
album_l, album_r = ui.colordiff(album_l, album_r)
|
||||
# Prefix with U+2260: Not Equal To
|
||||
left = {
|
||||
"prefix": f"{changed_prefix} Album: ",
|
||||
"prefix": f"{self.changed_prefix} Album: ",
|
||||
"contents": album_l,
|
||||
"suffix": "",
|
||||
}
|
||||
|
|
@ -437,9 +439,8 @@ class ChangeRepresentation:
|
|||
title_l, title_r = self.cur_title or "", self.match.info.title
|
||||
if self.cur_title != self.match.info.title:
|
||||
title_l, title_r = ui.colordiff(title_l, title_r)
|
||||
# Prefix with U+2260: Not Equal To
|
||||
left = {
|
||||
"prefix": f"{changed_prefix} Title: ",
|
||||
"prefix": f"{self.changed_prefix} Title: ",
|
||||
"contents": title_l,
|
||||
"suffix": "",
|
||||
}
|
||||
|
|
@ -568,9 +569,8 @@ class ChangeRepresentation:
|
|||
# the case, thus the 'info' dictionary is unneeded.
|
||||
# penalties = penalty_string(self.match.distance.tracks[track_info])
|
||||
|
||||
prefix = ui.colorize("changed", "\u2260 ") if changed else "* "
|
||||
lhs = {
|
||||
"prefix": f"{prefix}{lhs_track} ",
|
||||
"prefix": f"{self.changed_prefix if changed else '*'} {lhs_track} ",
|
||||
"contents": lhs_title,
|
||||
"suffix": f" {lhs_length}",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,10 @@ Other changes:
|
|||
file. :bug:`5979`
|
||||
- :doc:`plugins/lastgenre`: Updated and streamlined the genre whitelist and
|
||||
canonicalization tree :bug:`5977`
|
||||
- UI: Update default ``text_diff_added`` color from **bold red** to **bold
|
||||
green.**
|
||||
- UI: Use ``text_diff_added`` and ``text_diff_removed`` colors in **all** diff
|
||||
comparisons, including case differences.
|
||||
|
||||
2.3.1 (May 14, 2025)
|
||||
--------------------
|
||||
|
|
|
|||
|
|
@ -429,20 +429,11 @@ UI Options
|
|||
The options that allow for customization of the visual appearance of the console
|
||||
interface.
|
||||
|
||||
These options are available in this section:
|
||||
|
||||
color
|
||||
~~~~~
|
||||
|
||||
Either ``yes`` or ``no``; whether to use color in console output (currently only
|
||||
in the ``import`` command). Turn this off if your terminal doesn't support ANSI
|
||||
colors.
|
||||
|
||||
.. note::
|
||||
|
||||
The ``color`` option was previously a top-level configuration. This is still
|
||||
respected, but a deprecation message will be shown until your top-level
|
||||
``color`` configuration has been nested under ``ui``.
|
||||
Either ``yes`` or ``no``; whether to use color in console output. Turn this off
|
||||
if your terminal doesn't support ANSI colors.
|
||||
|
||||
.. _colors:
|
||||
|
||||
|
|
@ -450,10 +441,9 @@ colors
|
|||
~~~~~~
|
||||
|
||||
The colors that are used throughout the user interface. These are only used if
|
||||
the ``color`` option is set to ``yes``. For example, you might have a section in
|
||||
your configuration file that looks like this:
|
||||
the ``color`` option is set to ``yes``. See the default configuration:
|
||||
|
||||
::
|
||||
.. code-block:: yaml
|
||||
|
||||
ui:
|
||||
colors:
|
||||
|
|
@ -465,28 +455,26 @@ your configuration file that looks like this:
|
|||
action_default: ['bold', 'cyan']
|
||||
action: ['bold', 'cyan']
|
||||
# New colors after UI overhaul
|
||||
text: ['normal']
|
||||
text_faint: ['faint']
|
||||
import_path: ['bold', 'blue']
|
||||
import_path_items: ['bold', 'blue']
|
||||
added: ['green']
|
||||
removed: ['red']
|
||||
changed: ['yellow']
|
||||
added_highlight: ['bold', 'green']
|
||||
removed_highlight: ['bold', 'red']
|
||||
changed_highlight: ['bold', 'yellow']
|
||||
text_diff_added: ['bold', 'red']
|
||||
text_diff_added: ['bold', 'green']
|
||||
text_diff_removed: ['bold', 'red']
|
||||
text_diff_changed: ['bold', 'red']
|
||||
action_description: ['white']
|
||||
|
||||
Available colors: black, darkred, darkgreen, brown (darkyellow), darkblue,
|
||||
purple (darkmagenta), teal (darkcyan), lightgray, darkgray, red, green, yellow,
|
||||
blue, fuchsia (magenta), turquoise (cyan), white
|
||||
Available attributes:
|
||||
|
||||
Legacy UI colors config directive used strings. If any colors value is still a
|
||||
string instead of a list, it will be translated to list automatically. For
|
||||
example ``blue`` will become ``['blue']``.
|
||||
Foreground colors
|
||||
``black``, ``red``, ``green``, ``yellow``, ``blue``, ``magenta``, ``cyan``,
|
||||
``white``
|
||||
|
||||
Background colors
|
||||
``bg_black``, ``bg_red``, ``bg_green``, ``bg_yellow``, ``bg_blue``,
|
||||
``bg_magenta``, ``bg_cyan``, ``bg_white``
|
||||
|
||||
Text styles
|
||||
``normal``, ``bold``, ``faint``, ``underline``, ``reverse``
|
||||
|
||||
terminal_width
|
||||
~~~~~~~~~~~~~~
|
||||
|
|
@ -495,7 +483,7 @@ Controls line wrapping on non-Unix systems. On Unix systems, the width of the
|
|||
terminal is detected automatically. If this fails, or on non-Unix systems, the
|
||||
specified value is used as a fallback. Defaults to ``80`` characters:
|
||||
|
||||
::
|
||||
.. code-block:: yaml
|
||||
|
||||
ui:
|
||||
terminal_width: 80
|
||||
|
|
@ -511,7 +499,7 @@ different track lengths are colored with ``text_highlight_minor``.
|
|||
matching or distance score calculation (see :ref:`match-config`,
|
||||
``distance_weights`` and :ref:`colors`):
|
||||
|
||||
::
|
||||
.. code-block:: yaml
|
||||
|
||||
ui:
|
||||
length_diff_thresh: 10.0
|
||||
|
|
@ -523,18 +511,18 @@ When importing, beets will read several options to configure the visuals of the
|
|||
import dialogue. There are two layouts controlling how horizontal space and line
|
||||
wrapping is dealt with: ``column`` and ``newline``. The indentation of the
|
||||
respective elements of the import UI can also be configured. For example setting
|
||||
``4`` for ``match_header`` will indent the very first block of a proposed match
|
||||
by five characters in the terminal:
|
||||
``2`` for ``match_header`` will indent the very first block of a proposed match
|
||||
by two characters in the terminal:
|
||||
|
||||
::
|
||||
.. code-block:: yaml
|
||||
|
||||
ui:
|
||||
import:
|
||||
indentation:
|
||||
match_header: 4
|
||||
match_details: 4
|
||||
match_tracklist: 7
|
||||
layout: newline
|
||||
match_header: 2
|
||||
match_details: 2
|
||||
match_tracklist: 5
|
||||
layout: column
|
||||
|
||||
Importer Options
|
||||
----------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue