Skip to content

Commit c8199cb

Browse files
committed
lastgenre: Load configured blacklist file
Uses a custom text file format since YAML, INI, TOML, ... all have their flaws with parsing regex patterns.
1 parent 15557cf commit c8199cb

1 file changed

Lines changed: 52 additions & 0 deletions

File tree

beetsplug/lastgenre/__init__.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
import codecs
2626
import os
2727
import traceback
28+
from collections import defaultdict
2829
from typing import Union
2930

3031
import pylast
3132
import yaml
3233

3334
from beets import config, library, plugins, ui
3435
from beets.library import Album, Item
36+
from beets.ui import UserError
3537
from beets.util import normpath, plurality, unique_list
3638

3739
LASTFM = pylast.LastFMNetwork(api_key=plugins.LASTFM_KEY)
@@ -105,6 +107,7 @@ def __init__(self):
105107
"prefer_specific": False,
106108
"title_case": True,
107109
"extended_debug": False,
110+
"blacklist": False,
108111
}
109112
)
110113
self.setup()
@@ -117,6 +120,7 @@ def setup(self):
117120
self._genre_cache = {}
118121
self.whitelist = self._load_whitelist()
119122
self.c14n_branches, self.canonicalize = self._load_c14n_tree()
123+
self.blacklist = self._load_blacklist()
120124

121125
def _load_whitelist(self) -> set[str]:
122126
whitelist = set()
@@ -151,6 +155,54 @@ def _load_c14n_tree(self) -> tuple[list[list[str]], bool]:
151155
flatten_tree(genres_tree, [], c14n_branches)
152156
return c14n_branches, canonicalize
153157

158+
def _load_blacklist(self) -> defaultdict[list[str]]:
159+
"""Load the blacklist from a configured file path.
160+
161+
For maximum compatibility with regex patterns, a custom format is used:
162+
- Each section starts with an artist name, followed by a colon.
163+
- Subsequent lines are indented and contain a regex pattern to match a genre.
164+
165+
Eg.:
166+
artist name 1:
167+
- genre pattern 1
168+
- genre pattern 2
169+
artist name 2:
170+
- genre pattern 3
171+
172+
Raises:
173+
UserError: if the file format is invalid.
174+
"""
175+
blacklist = defaultdict(list)
176+
if not (bl_filename := self.config["blacklist"].get()):
177+
return blacklist
178+
if not os.path.isfile(bl_filename := normpath(bl_filename)):
179+
self._log.error("Blacklist file not found: {} .", bl_filename)
180+
return blacklist
181+
182+
current = None
183+
with open(bl_filename, "rb") as f:
184+
for lineno, line in enumerate(f, 1):
185+
line = line.strip()
186+
if not line or line.startswith(b"#"):
187+
continue
188+
if not line.startswith(b' '):
189+
if not line.endswith(b':'):
190+
raise UserError(
191+
f"Malformed blacklist section header (eg 'artist 1:') "
192+
f"at line {lineno}: {line.decode('utf-8', 'replace')}"
193+
)
194+
current = line.rstrip(b':').decode("utf-8", "replace")
195+
else:
196+
if current is None:
197+
raise UserError(
198+
f"Blacklist regex pattern line before any section header "
199+
f"at line {lineno}: {line.decode('utf-8', 'replace')}"
200+
)
201+
blacklist[current].append(
202+
line.strip().decode("utf-8", "replace")
203+
)
204+
return blacklist
205+
154206
@property
155207
def sources(self) -> tuple[str, ...]:
156208
"""A tuple of allowed genre sources. May contain 'track',

0 commit comments

Comments
 (0)