From 95cef2de2b97ec0949ebfc5b415b3d49d5b16112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Fri, 30 Jan 2026 00:18:02 +0000 Subject: [PATCH] Fix grouping for list fields and stabilize equal-chance order - Handle list-valued fields when grouping for --field/--equal-chance to avoid "TypeError: unhashable type: 'list'" (e.g., artists). - Sort items by the grouping key before building groups so equal-chance permutation preserves the same item set as `beet list`, only randomized. --- beetsplug/random.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/beetsplug/random.py b/beetsplug/random.py index 9714c8e53..aa65dd5d5 100644 --- a/beetsplug/random.py +++ b/beetsplug/random.py @@ -17,8 +17,8 @@ from __future__ import annotations import random from itertools import groupby, islice -from operator import attrgetter -from typing import TYPE_CHECKING, Any +from operator import methodcaller +from typing import TYPE_CHECKING from beets.plugins import BeetsPlugin from beets.ui import Subcommand, print_ @@ -84,9 +84,6 @@ class Random(BeetsPlugin): return [random_cmd] -NOT_FOUND_SENTINEL = object() - - def _equal_chance_permutation( objs: Iterable[LibModel], field: str ) -> Iterable[LibModel]: @@ -95,26 +92,16 @@ def _equal_chance_permutation( any given position. """ # Group the objects by field so we can sample from them. - key = attrgetter(field) + get_attr = methodcaller("get", field) - def get_attr(obj: LibModel) -> Any: - try: - return key(obj) - except AttributeError: - return NOT_FOUND_SENTINEL + groups = {} + for k, values in groupby(sorted(objs, key=get_attr), key=get_attr): + if k is not None: + vals = list(values) + # shuffle in category + random.shuffle(vals) + groups[str(k)] = vals - sorted(objs, key=get_attr) - - groups: dict[str | object, list[LibModel]] = { - NOT_FOUND_SENTINEL: [], - } - for k, values in groupby(objs, key=get_attr): - groups[k] = list(values) - # shuffle in category - random.shuffle(groups[k]) - - # Remove items without the field value. - del groups[NOT_FOUND_SENTINEL] while groups: group = random.choice(list(groups.keys())) yield groups[group].pop()