diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 47f0f66ca..6f657fb48 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -41,6 +41,8 @@ if TYPE_CHECKING: from beets.library import LibModel + from .types import CanonTree + class LastGenrePlugin(plugins.BeetsPlugin): def __init__(self) -> None: @@ -116,7 +118,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): return [p[1] for p in depth_tag_pairs] @staticmethod - def find_parents(candidate: str, branches: list[list[str]]) -> list[str]: + def find_parents(candidate: str, branches: CanonTree) -> list[str]: """Find parent genres of a given genre, ordered from closest to furthest.""" for branch in branches: try: diff --git a/beetsplug/lastgenre/client.py b/beetsplug/lastgenre/client.py index 21a0bff72..bff472dc0 100644 --- a/beetsplug/lastgenre/client.py +++ b/beetsplug/lastgenre/client.py @@ -31,6 +31,8 @@ if TYPE_CHECKING: from beets.logging import Logger + from .types import GenreCache + LASTFM = pylast.LastFMNetwork(api_key=plugins.LASTFM_KEY) PYLAST_EXCEPTIONS = ( @@ -51,7 +53,7 @@ class LastFmClient: self._log = log self._tunelog = make_tunelog(log) self._min_weight = min_weight - self._genre_cache: dict[str, list[str]] = {} + self._genre_cache: GenreCache = {} def fetch_genre( self, lastfm_obj: pylast.Album | pylast.Artist | pylast.Track diff --git a/beetsplug/lastgenre/loaders.py b/beetsplug/lastgenre/loaders.py index 0992ca14e..1f0720eeb 100644 --- a/beetsplug/lastgenre/loaders.py +++ b/beetsplug/lastgenre/loaders.py @@ -28,6 +28,8 @@ if TYPE_CHECKING: from beets.logging import Logger + from .types import CanonTree, Whitelist + class DataFileLoader: """Loads genre-related data files for the lastgenre plugin.""" @@ -36,8 +38,8 @@ class DataFileLoader: self, log: Logger, plugin_dir: Path, - whitelist: set[str], - c14n_branches: list[list[str]], + whitelist: Whitelist, + c14n_branches: CanonTree, canonicalize: bool, ): """Initialize with pre-loaded data. @@ -83,7 +85,7 @@ class DataFileLoader: @staticmethod def _load_whitelist( log: Logger, config_value: str | bool | None, default_path: str - ) -> set[str]: + ) -> Whitelist: """Load the whitelist from a text file. Returns set of valid genre names (lowercase). @@ -107,12 +109,12 @@ class DataFileLoader: config_value: str | bool | None, default_path: str, prefer_specific: bool, - ) -> tuple[list[list[str]], bool]: + ) -> tuple[CanonTree, bool]: """Load the canonicalization tree from a YAML file. Returns tuple of (branches, canonicalize_enabled). """ - c14n_branches: list[list[str]] = [] + c14n_branches: CanonTree = [] c14n_filename = config_value canonicalize = c14n_filename is not False # Default tree @@ -133,9 +135,24 @@ class DataFileLoader: def flatten_tree( elem: dict[str, Any] | list[Any] | str, path: list[str], - branches: list[list[str]], + branches: CanonTree, ) -> None: - """Flatten nested lists/dictionaries into lists of strings (branches).""" + """Flatten nested YAML structure into genre hierarchy branches. + + Recursively converts nested dicts/lists from YAML into a flat list + of genre paths, where each path goes from general to specific genre. + + Args: + elem: The YAML element to process (dict, list, or string leaf). + path: Current path from root to this element (used in recursion). + branches: OUTPUT PARAMETER - Empty list that will be populated + with genre paths. Gets mutated by this method. + + Example: + branches = [] + flatten_tree({'rock': ['indie', 'punk']}, [], branches) + # branches is now: [['rock', 'indie'], ['rock', 'punk']] + """ if not path: path = [] diff --git a/beetsplug/lastgenre/types.py b/beetsplug/lastgenre/types.py new file mode 100644 index 000000000..f8fa2aba5 --- /dev/null +++ b/beetsplug/lastgenre/types.py @@ -0,0 +1,30 @@ +# This file is part of beets. +# Copyright 2026, J0J0 Todos. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + + +"""Type aliases for the lastgenre plugin.""" + +from __future__ import annotations + +Whitelist = set[str] +"""Set of valid genre names (lowercase). Empty set means all genres allowed.""" + +CanonTree = list[list[str]] +"""Genre hierarchy as list of paths from general to specific. +Example: [['electronic', 'house'], ['electronic', 'techno']]""" + +GenreCache = dict[str, list[str]] +"""Cache mapping entity keys to their genre lists. +Keys are formatted as 'entity.arg1-arg2-...' (e.g., 'album.artist-title'). +Values are lists of lowercase genre strings."""