Skip to content

Commit 916a902

Browse files
committed
wip
1 parent a1c0ebd commit 916a902

File tree

10 files changed

+88
-45
lines changed

10 files changed

+88
-45
lines changed

beets/autotag/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def _apply_metadata(
118118
if value is None and field not in nullable_fields:
119119
continue
120120

121-
db_obj[field] = value
121+
setattr(db_obj, field, value)
122122

123123

124124
def correct_list_fields(m: LibModel) -> None:

beets/autotag/hooks.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def __init__(
100100
country: str | None = None,
101101
style: str | None = None,
102102
genre: str | None = None,
103+
genres: str | None = None,
103104
albumstatus: str | None = None,
104105
media: str | None = None,
105106
albumdisambig: str | None = None,
@@ -143,6 +144,7 @@ def __init__(
143144
self.country = country
144145
self.style = style
145146
self.genre = genre
147+
self.genres = genres or ([genre] if genre else [])
146148
self.albumstatus = albumstatus
147149
self.media = media
148150
self.albumdisambig = albumdisambig
@@ -212,6 +214,7 @@ def __init__(
212214
bpm: str | None = None,
213215
initial_key: str | None = None,
214216
genre: str | None = None,
217+
genres: str | None = None,
215218
album: str | None = None,
216219
**kwargs,
217220
):
@@ -245,7 +248,7 @@ def __init__(
245248
self.work_disambig = work_disambig
246249
self.bpm = bpm
247250
self.initial_key = initial_key
248-
self.genre = genre
251+
self.genres = genres
249252
self.album = album
250253
self.update(kwargs)
251254

beets/autotag/mb.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -614,10 +614,10 @@ def album_info(release: dict) -> beets.autotag.hooks.AlbumInfo:
614614
for source in sources:
615615
for genreitem in source:
616616
genres[genreitem["name"]] += int(genreitem["count"])
617-
info.genre = "; ".join(
617+
info.genres = [
618618
genre
619619
for genre, _count in sorted(genres.items(), key=lambda g: -g[1])
620-
)
620+
]
621621

622622
# We might find links to external sources (Discogs, Bandcamp, ...)
623623
external_ids = config["musicbrainz"]["external_ids"].get()
@@ -808,7 +808,6 @@ def _merge_pseudo_and_actual_album(
808808
"barcode",
809809
"asin",
810810
"style",
811-
"genre",
812811
]
813812
}
814813
merged.update(from_actual)

beets/dbcore/db.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ def copy(self) -> Model:
432432
# Essential field accessors.
433433

434434
@classmethod
435-
def _type(cls, key) -> types.Type:
435+
def _type(cls, key):
436436
"""Get the type of a field, a `Type` instance.
437437
438438
If the field has no explicit type, it is given the base `Type`,
@@ -528,7 +528,7 @@ def all_keys(cls):
528528
def update(self, values):
529529
"""Assign all values in the given dict."""
530530
for key, value in values.items():
531-
self[key] = value
531+
setattr(self, key, value)
532532

533533
def items(self) -> Iterator[tuple[str, Any]]:
534534
"""Iterate over (key, value) pairs that this object contains.
@@ -559,7 +559,11 @@ def __getattr__(self, key):
559559
raise AttributeError(f"no such field {key!r}")
560560

561561
def __setattr__(self, key, value):
562-
if key.startswith("_"):
562+
if (
563+
key.startswith("_")
564+
or key in dir(self)
565+
and isinstance(getattr(self.__class__, key), property)
566+
):
563567
super().__setattr__(key, value)
564568
else:
565569
self[key] = value
@@ -714,7 +718,7 @@ def _parse(cls, key, string: str) -> Any:
714718

715719
def set_parse(self, key, string: str):
716720
"""Set the object's key to a value represented by a string."""
717-
self[key] = self._parse(key, string)
721+
setattr(self, key, self._parse(key, string))
718722

719723

720724
# Database controller and supporting interfaces.

beets/dbcore/types.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,13 @@ def parse(self, string: str):
304304
return []
305305
return string.split(self.delimiter)
306306

307-
def to_sql(self, model_value: list[str]):
307+
def normalize(self, value: Any) -> list[str]:
308+
if not value:
309+
return []
310+
311+
return value.split(self.delimiter) if isinstance(value, str) else value
312+
313+
def to_sql(self, model_value: list[str]) -> str:
308314
return self.delimiter.join(model_value)
309315

310316

beets/library.py

+41-16
Original file line numberDiff line numberDiff line change
@@ -354,10 +354,27 @@ class LibModel(dbcore.Model["Library"]):
354354
def writable_media_fields(cls) -> set[str]:
355355
return set(MediaFile.fields()) & cls._fields.keys()
356356

357+
@property
358+
def genre(self) -> str:
359+
_type: types.DelimitedString = self._type("genres")
360+
return _type.to_sql(self.get("genres"))
361+
362+
@genre.setter
363+
def genre(self, value: str) -> None:
364+
self.genres = value
365+
366+
@classmethod
367+
def _getters(cls):
368+
return {
369+
"genre": lambda m: cls._fields["genres"].delimiter.join(m.genres)
370+
}
371+
357372
def _template_funcs(self):
358-
funcs = DefaultTemplateFunctions(self, self._db).functions()
359-
funcs.update(plugins.template_funcs())
360-
return funcs
373+
return {
374+
**DefaultTemplateFunctions(self, self._db).functions(),
375+
**plugins.template_funcs(),
376+
"genre": "$genres",
377+
}
361378

362379
def store(self, fields=None):
363380
super().store(fields)
@@ -533,7 +550,7 @@ class Item(LibModel):
533550
"albumartists_sort": types.MULTI_VALUE_DSV,
534551
"albumartist_credit": types.STRING,
535552
"albumartists_credit": types.MULTI_VALUE_DSV,
536-
"genre": types.STRING,
553+
"genres": types.SEMICOLON_SPACE_DSV,
537554
"style": types.STRING,
538555
"discogs_albumid": types.INTEGER,
539556
"discogs_artistid": types.INTEGER,
@@ -614,7 +631,7 @@ class Item(LibModel):
614631
"comments",
615632
"album",
616633
"albumartist",
617-
"genre",
634+
"genres",
618635
)
619636

620637
_types = {
@@ -689,10 +706,12 @@ def _cached_album(self, album):
689706

690707
@classmethod
691708
def _getters(cls):
692-
getters = plugins.item_field_getters()
693-
getters["singleton"] = lambda i: i.album_id is None
694-
getters["filesize"] = Item.try_filesize # In bytes.
695-
return getters
709+
return {
710+
**plugins.item_field_getters(),
711+
"singleton": lambda i: i.album_id is None,
712+
"filesize": Item.try_filesize, # In bytes.
713+
"genre": lambda i: cls._fields["genres"].delimiter.join(i.genres),
714+
}
696715

697716
def duplicates_query(self, fields: list[str]) -> dbcore.AndQuery:
698717
"""Return a query for entities with same values in the given fields."""
@@ -768,6 +787,10 @@ def get(self, key, default=None, with_album=True):
768787
769788
Set `with_album` to false to skip album fallback.
770789
"""
790+
if key in dir(self) and isinstance(
791+
getattr(self.__class__, key), property
792+
):
793+
return getattr(self, key)
771794
try:
772795
return self._get(key, default, raise_=with_album)
773796
except KeyError:
@@ -1181,7 +1204,7 @@ class Album(LibModel):
11811204
"albumartists_sort": types.MULTI_VALUE_DSV,
11821205
"albumartists_credit": types.MULTI_VALUE_DSV,
11831206
"album": types.STRING,
1184-
"genre": types.STRING,
1207+
"genres": types.SEMICOLON_SPACE_DSV,
11851208
"style": types.STRING,
11861209
"discogs_albumid": types.INTEGER,
11871210
"discogs_artistid": types.INTEGER,
@@ -1215,7 +1238,7 @@ class Album(LibModel):
12151238
"original_day": types.PaddedInt(2),
12161239
}
12171240

1218-
_search_fields = ("album", "albumartist", "genre")
1241+
_search_fields = ("album", "albumartist", "genres")
12191242

12201243
_types = {
12211244
"path": PathType(),
@@ -1237,7 +1260,7 @@ class Album(LibModel):
12371260
"albumartist_credit",
12381261
"albumartists_credit",
12391262
"album",
1240-
"genre",
1263+
"genres",
12411264
"style",
12421265
"discogs_albumid",
12431266
"discogs_artistid",
@@ -1293,10 +1316,12 @@ def relation_join(cls) -> str:
12931316
def _getters(cls):
12941317
# In addition to plugin-provided computed fields, also expose
12951318
# the album's directory as `path`.
1296-
getters = plugins.album_field_getters()
1297-
getters["path"] = Album.item_dir
1298-
getters["albumtotal"] = Album._albumtotal
1299-
return getters
1319+
return {
1320+
**super()._getters(),
1321+
**plugins.album_field_getters(),
1322+
"path": Album.item_dir,
1323+
"albumtotal": Album._albumtotal,
1324+
}
13001325

13011326
def items(self):
13021327
"""Return an iterable over the items associated with this

docs/changelog.rst

+2
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ New features:
129129
* Beets now uses ``platformdirs`` to determine the default music directory.
130130
This location varies between systems -- for example, users can configure it
131131
on Unix systems via ``user-dirs.dirs(5)``.
132+
* New multi-valued ``genres`` tag. This change brings up the ``genres`` tag to the same state as the ``*artists*`` multi-valued tags (see :bug:`4743` for details).
133+
:bug:`5426`
132134

133135
Bug fixes:
134136

test/test_library.py

+11-7
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,19 @@ def test_store_changes_database_value(self):
6666
assert new_year == 1987
6767

6868
def test_store_only_writes_dirty_fields(self):
69-
original_genre = self.i.genre
70-
self.i._values_fixed["genre"] = "beatboxing" # change w/o dirtying
69+
original_artist = self.i.artist
70+
self.i._values_fixed["artist"] = "beatboxing" # change w/o dirtying
7171
self.i.store()
72-
new_genre = (
73-
self.lib._connection()
74-
.execute("select genre from items where title = ?", (self.i.title,))
75-
.fetchone()["genre"]
72+
assert (
73+
(
74+
self.lib._connection()
75+
.execute(
76+
"select artist from items where title = ?", (self.i.title,)
77+
)
78+
.fetchone()["artist"]
79+
)
80+
== original_artist
7681
)
77-
assert new_genre == original_genre
7882

7983
def test_store_clears_dirty_flags(self):
8084
self.i.composer = "tvp"

test/test_sort.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,23 @@ def setUp(self):
3333
albums = [
3434
Album(
3535
album="Album A",
36-
genre="Rock",
36+
label="Label",
3737
year=2001,
3838
flex1="Flex1-1",
3939
flex2="Flex2-A",
4040
albumartist="Foo",
4141
),
4242
Album(
4343
album="Album B",
44-
genre="Rock",
44+
label="Label",
4545
year=2001,
4646
flex1="Flex1-2",
4747
flex2="Flex2-A",
4848
albumartist="Bar",
4949
),
5050
Album(
5151
album="Album C",
52-
genre="Jazz",
52+
label="Records",
5353
year=2005,
5454
flex1="Flex1-1",
5555
flex2="Flex2-B",
@@ -236,19 +236,19 @@ def test_sort_desc(self):
236236

237237
def test_sort_two_field_asc(self):
238238
q = ""
239-
s1 = dbcore.query.FixedFieldSort("genre", True)
239+
s1 = dbcore.query.FixedFieldSort("label", True)
240240
s2 = dbcore.query.FixedFieldSort("album", True)
241241
sort = dbcore.query.MultipleSort()
242242
sort.add_sort(s1)
243243
sort.add_sort(s2)
244244
results = self.lib.albums(q, sort)
245-
assert results[0]["genre"] <= results[1]["genre"]
246-
assert results[1]["genre"] <= results[2]["genre"]
247-
assert results[1]["genre"] == "Rock"
248-
assert results[2]["genre"] == "Rock"
245+
assert results[0]["label"] <= results[1]["label"]
246+
assert results[1]["label"] <= results[2]["label"]
247+
assert results[1]["label"] == "Label"
248+
assert results[2]["label"] == "Records"
249249
assert results[1]["album"] <= results[2]["album"]
250250
# same thing with query string
251-
q = "genre+ album+"
251+
q = "label+ album+"
252252
results2 = self.lib.albums(q)
253253
for r1, r2 in zip(results, results2):
254254
assert r1.id == r2.id
@@ -388,7 +388,7 @@ def setUp(self):
388388

389389
album = Album(
390390
album="album",
391-
genre="alternative",
391+
label="label",
392392
year="2001",
393393
flex1="flex1",
394394
flex2="flex2-A",

test/test_ui.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ def test_selective_modified_album_metadata_not_moved(self):
690690
mf.album = "differentAlbum"
691691
mf.genre = "differentGenre"
692692
mf.save()
693-
self._update(move=True, fields=["genre"])
693+
self._update(move=True, fields=["genres"])
694694
item = self.lib.items().get()
695695
assert b"differentAlbum" not in item.path
696696
assert item.genre == "differentGenre"
@@ -1445,7 +1445,7 @@ def test_completion(self):
14451445
assert tester.returncode == 0
14461446
assert out == b"completion tests passed\n", (
14471447
"test/test_completion.sh did not execute properly. "
1448-
f'Output:{out.decode("utf-8")}'
1448+
f"Output:{out.decode('utf-8')}"
14491449
)
14501450

14511451

0 commit comments

Comments
 (0)