-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Rafactored beets/random.py and moved into beetsplug/random.py #5924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,37 +1,35 @@ | ||||||
# This file is part of beets. | ||||||
# Copyright 2016, Philippe Mongeau. | ||||||
# | ||||||
# 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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should probably be kept in place. |
||||||
# 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. | ||||||
|
||||||
"""Get a random song or album from the library.""" | ||||||
|
||||||
from __future__ import annotations | ||||||
|
||||||
import random | ||||||
from itertools import groupby, islice | ||||||
from operator import attrgetter | ||||||
from typing import TYPE_CHECKING, Any, Iterable, Sequence, TypeVar | ||||||
|
||||||
from beets.plugins import BeetsPlugin | ||||||
from beets.random import random_objs | ||||||
from beets.ui import Subcommand, print_ | ||||||
|
||||||
if TYPE_CHECKING: | ||||||
import optparse | ||||||
|
||||||
from beets.library import LibModel, Library | ||||||
|
||||||
T = TypeVar("T", bound=LibModel) | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason for a type var? I think we should get away by replacing it with For clarity and to help type-checking it would be ideal to define their types under _format_config_key: str
albumartist: str
length: float
path: bytes |
||||||
|
||||||
def random_func(lib, opts, args): | ||||||
def random_func(lib: Library, opts: optparse.Values, args: list[str]): | ||||||
"""Select some random items or albums and print the results.""" | ||||||
# Fetch all the objects matching the query into a list. | ||||||
if opts.album: | ||||||
objs = list(lib.albums(args)) | ||||||
else: | ||||||
objs = list(lib.items(args)) | ||||||
objs = lib.albums(args) if opts.album else lib.items(args) | ||||||
|
||||||
# Print a random subset. | ||||||
objs = random_objs( | ||||||
objs, opts.album, opts.number, opts.time, opts.equal_chance | ||||||
) | ||||||
for obj in objs: | ||||||
for obj in random_objs( | ||||||
objs=list(objs), | ||||||
number=opts.number, | ||||||
time_minutes=opts.time, | ||||||
equal_chance=opts.equal_chance, | ||||||
): | ||||||
print_(format(obj)) | ||||||
|
||||||
|
||||||
|
@@ -64,3 +62,95 @@ def random_func(lib, opts, args): | |||||
class Random(BeetsPlugin): | ||||||
def commands(self): | ||||||
return [random_cmd] | ||||||
|
||||||
|
||||||
NOT_FOUND_SENTINEL = object() | ||||||
|
||||||
|
||||||
def _equal_chance_permutation( | ||||||
objs: Sequence[T], | ||||||
field: str = "albumartist", | ||||||
random_gen: random.Random | None = None, | ||||||
) -> Iterable[T]: | ||||||
Comment on lines
+72
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This function is never called with these arguments - use them directly within the function implementation instead |
||||||
"""Generate (lazily) a permutation of the objects where every group | ||||||
with equal values for `field` have an equal chance of appearing in | ||||||
any given position. | ||||||
""" | ||||||
rand: random.Random = random_gen or random.Random() | ||||||
|
||||||
# Group the objects by artist so we can sample from them. | ||||||
key = attrgetter(field) | ||||||
|
||||||
def get_attr(obj: T) -> Any: | ||||||
try: | ||||||
return key(obj) | ||||||
except AttributeError: | ||||||
return NOT_FOUND_SENTINEL | ||||||
|
||||||
Comment on lines
+87
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this can possibly happen? |
||||||
groups: dict[Any, list[T]] = { | ||||||
NOT_FOUND_SENTINEL: [], | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function only ever handles the
Suggested change
|
||||||
} | ||||||
for k, values in groupby(objs, key=get_attr): | ||||||
groups[k] = list(values) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
# shuffle in category | ||||||
rand.shuffle(groups[k]) | ||||||
|
||||||
# Remove items without the field value. | ||||||
del groups[NOT_FOUND_SENTINEL] | ||||||
while groups: | ||||||
group = rand.choice(list(groups.keys())) | ||||||
yield groups[group].pop() | ||||||
if not groups[group]: | ||||||
del groups[group] | ||||||
|
||||||
|
||||||
def _take_time( | ||||||
iter: Iterable[T], | ||||||
semohr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
secs: float, | ||||||
) -> Iterable[T]: | ||||||
"""Return a list containing the first values in `iter`, which should | ||||||
be Item or Album objects, that add up to the given amount of time in | ||||||
seconds. | ||||||
""" | ||||||
total_time = 0.0 | ||||||
for obj in iter: | ||||||
length = obj.length | ||||||
if total_time + length <= secs: | ||||||
yield obj | ||||||
total_time += length | ||||||
|
||||||
semohr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
def random_objs( | ||||||
objs: Sequence[T], | ||||||
number: int = 1, | ||||||
time_minutes: float | None = None, | ||||||
equal_chance: bool = False, | ||||||
random_gen: random.Random | None = None, | ||||||
) -> Iterable[T]: | ||||||
Comment on lines
+125
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defaults are still present even though they have no effect. Actually, why not just passing the entire |
||||||
"""Get a random subset of items, optionally constrained by time or count. | ||||||
|
||||||
Args: | ||||||
- objs: The sequence of objects to choose from. | ||||||
- number: The number of objects to select. | ||||||
- time_minutes: If specified, the total length of selected objects | ||||||
should not exceed this many minutes. | ||||||
- equal_chance: If True, each artist has the same chance of being | ||||||
selected, regardless of how many tracks they have. | ||||||
- random_gen: An optional random generator to use for shuffling. | ||||||
""" | ||||||
rand: random.Random = random_gen or random.Random() | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why was |
||||||
# Permute the objects either in a straightforward way or an | ||||||
# artist-balanced way. | ||||||
perm: Iterable[T] | ||||||
if equal_chance: | ||||||
perm = _equal_chance_permutation(objs, random_gen=rand) | ||||||
else: | ||||||
perm = list(objs) | ||||||
rand.shuffle(perm) | ||||||
|
||||||
# Select objects by time our count. | ||||||
if time_minutes: | ||||||
return _take_time(perm, time_minutes * 60) | ||||||
else: | ||||||
return islice(perm, number) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a cached property, otherwise it will end up hitting the db on every access