mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 16:42:42 +01:00
replaygain: typings
also, minor clean-up (remove unused function after_version, f-string conversion)
This commit is contained in:
parent
1fdf075402
commit
05383a0dab
5 changed files with 207 additions and 115 deletions
|
|
@ -16,7 +16,7 @@
|
||||||
Library.
|
Library.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .db import Database, Model
|
from .db import Database, Model, Results
|
||||||
from .query import (
|
from .query import (
|
||||||
AndQuery,
|
AndQuery,
|
||||||
FieldQuery,
|
FieldQuery,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ from mediafile import MediaFile, UnreadableFileError
|
||||||
|
|
||||||
import beets
|
import beets
|
||||||
from beets import dbcore, logging, plugins, util
|
from beets import dbcore, logging, plugins, util
|
||||||
from beets.dbcore import types
|
from beets.dbcore import Results, types
|
||||||
from beets.util import (
|
from beets.util import (
|
||||||
MoveOperation,
|
MoveOperation,
|
||||||
bytestring_path,
|
bytestring_path,
|
||||||
|
|
@ -1665,11 +1665,11 @@ class Library(dbcore.Database):
|
||||||
Item, beets.config["sort_item"].as_str_seq()
|
Item, beets.config["sort_item"].as_str_seq()
|
||||||
)
|
)
|
||||||
|
|
||||||
def albums(self, query=None, sort=None):
|
def albums(self, query=None, sort=None) -> Results[Album]:
|
||||||
"""Get :class:`Album` objects matching the query."""
|
"""Get :class:`Album` objects matching the query."""
|
||||||
return self._fetch(Album, query, sort or self.get_default_album_sort())
|
return self._fetch(Album, query, sort or self.get_default_album_sort())
|
||||||
|
|
||||||
def items(self, query=None, sort=None):
|
def items(self, query=None, sort=None) -> Results[Item]:
|
||||||
"""Get :class:`Item` objects matching the query."""
|
"""Get :class:`Item` objects matching the query."""
|
||||||
return self._fetch(Item, query, sort or self.get_default_item_sort())
|
return self._fetch(Item, query, sort or self.get_default_item_sort())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
import traceback
|
import traceback
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
|
from typing import Any, Callable, List
|
||||||
|
|
||||||
import confuse
|
import confuse
|
||||||
|
|
||||||
|
|
@ -1450,6 +1451,8 @@ class Subcommand:
|
||||||
invoked by a SubcommandOptionParser.
|
invoked by a SubcommandOptionParser.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
func: Callable[[library.Library, optparse.Values, List[str]], Any]
|
||||||
|
|
||||||
def __init__(self, name, parser=None, help="", aliases=(), hide=False):
|
def __init__(self, name, parser=None, help="", aliases=(), hide=False):
|
||||||
"""Creates a new subcommand. name is the primary way to invoke
|
"""Creates a new subcommand. name is the primary way to invoke
|
||||||
the subcommand; aliases are alternate names. parser is an
|
the subcommand; aliases are alternate names. parser is an
|
||||||
|
|
|
||||||
|
|
@ -405,7 +405,10 @@ def bytestring_path(path: Bytes_or_String) -> bytes:
|
||||||
PATH_SEP: bytes = bytestring_path(os.sep)
|
PATH_SEP: bytes = bytestring_path(os.sep)
|
||||||
|
|
||||||
|
|
||||||
def displayable_path(path: bytes, separator: str = "; ") -> str:
|
def displayable_path(
|
||||||
|
path: Union[bytes, str, Tuple[Union[bytes, str], ...]],
|
||||||
|
separator: str = "; ",
|
||||||
|
) -> str:
|
||||||
"""Attempts to decode a bytestring path to a unicode object for the
|
"""Attempts to decode a bytestring path to a unicode object for the
|
||||||
purpose of displaying it to the user. If the `path` argument is a
|
purpose of displaying it to the user. If the `path` argument is a
|
||||||
list or a tuple, the elements are joined with `separator`.
|
list or a tuple, the elements are joined with `separator`.
|
||||||
|
|
@ -801,7 +804,7 @@ def legalize_path(
|
||||||
return second_stage_path, retruncated
|
return second_stage_path, retruncated
|
||||||
|
|
||||||
|
|
||||||
def py3_path(path: AnyStr) -> str:
|
def py3_path(path: Union[bytes, str]) -> str:
|
||||||
"""Convert a bytestring path to Unicode.
|
"""Convert a bytestring path to Unicode.
|
||||||
|
|
||||||
This helps deal with APIs on Python 3 that *only* accept Unicode
|
This helps deal with APIs on Python 3 that *only* accept Unicode
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,38 @@
|
||||||
import collections
|
import collections
|
||||||
import enum
|
import enum
|
||||||
import math
|
import math
|
||||||
|
import optparse
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from logging import Logger
|
||||||
from multiprocessing.pool import ThreadPool
|
from multiprocessing.pool import ThreadPool
|
||||||
from threading import Event, Thread
|
from threading import Event, Thread
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
DefaultDict,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
|
from confuse import ConfigView
|
||||||
|
|
||||||
from beets import ui
|
from beets import ui
|
||||||
|
from beets.importer import ImportSession, ImportTask
|
||||||
|
from beets.library import Album, Item, Library
|
||||||
from beets.plugins import BeetsPlugin
|
from beets.plugins import BeetsPlugin
|
||||||
from beets.util import (
|
from beets.util import (
|
||||||
command_output,
|
command_output,
|
||||||
|
|
@ -53,7 +75,7 @@ class FatalGstreamerPluginReplayGainError(FatalReplayGainError):
|
||||||
loading the required plugins."""
|
loading the required plugins."""
|
||||||
|
|
||||||
|
|
||||||
def call(args, log, **kwargs):
|
def call(args: List[Any], log: Logger, **kwargs: Any):
|
||||||
"""Execute the command and return its output or raise a
|
"""Execute the command and return its output or raise a
|
||||||
ReplayGainError on failure.
|
ReplayGainError on failure.
|
||||||
"""
|
"""
|
||||||
|
|
@ -71,13 +93,7 @@ def call(args, log, **kwargs):
|
||||||
raise ReplayGainError("argument encoding failed")
|
raise ReplayGainError("argument encoding failed")
|
||||||
|
|
||||||
|
|
||||||
def after_version(version_a, version_b):
|
def db_to_lufs(db: float) -> float:
|
||||||
return tuple(int(s) for s in version_a.split(".")) >= tuple(
|
|
||||||
int(s) for s in version_b.split(".")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def db_to_lufs(db):
|
|
||||||
"""Convert db to LUFS.
|
"""Convert db to LUFS.
|
||||||
|
|
||||||
According to https://wiki.hydrogenaud.io/index.php?title=
|
According to https://wiki.hydrogenaud.io/index.php?title=
|
||||||
|
|
@ -86,7 +102,7 @@ def db_to_lufs(db):
|
||||||
return db - 107
|
return db - 107
|
||||||
|
|
||||||
|
|
||||||
def lufs_to_db(db):
|
def lufs_to_db(db: float) -> float:
|
||||||
"""Convert LUFS to db.
|
"""Convert LUFS to db.
|
||||||
|
|
||||||
According to https://wiki.hydrogenaud.io/index.php?title=
|
According to https://wiki.hydrogenaud.io/index.php?title=
|
||||||
|
|
@ -97,9 +113,13 @@ def lufs_to_db(db):
|
||||||
|
|
||||||
# Backend base and plumbing classes.
|
# Backend base and plumbing classes.
|
||||||
|
|
||||||
# gain: in LU to reference level
|
|
||||||
# peak: part of full scale (FS is 1.0)
|
@dataclass
|
||||||
Gain = collections.namedtuple("Gain", "gain peak")
|
class Gain:
|
||||||
|
# gain: in LU to reference level
|
||||||
|
gain: float
|
||||||
|
# peak: part of full scale (FS is 1.0)
|
||||||
|
peak: float
|
||||||
|
|
||||||
|
|
||||||
class PeakMethod(enum.Enum):
|
class PeakMethod(enum.Enum):
|
||||||
|
|
@ -118,7 +138,13 @@ class RgTask:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, items, album, target_level, peak_method, backend_name, log
|
self,
|
||||||
|
items: Sequence[Item],
|
||||||
|
album: Optional[Album],
|
||||||
|
target_level: float,
|
||||||
|
peak_method: Optional[PeakMethod],
|
||||||
|
backend_name: str,
|
||||||
|
log: Logger,
|
||||||
):
|
):
|
||||||
self.items = items
|
self.items = items
|
||||||
self.album = album
|
self.album = album
|
||||||
|
|
@ -126,10 +152,10 @@ class RgTask:
|
||||||
self.peak_method = peak_method
|
self.peak_method = peak_method
|
||||||
self.backend_name = backend_name
|
self.backend_name = backend_name
|
||||||
self._log = log
|
self._log = log
|
||||||
self.album_gain = None
|
self.album_gain: Optional[Gain] = None
|
||||||
self.track_gains = None
|
self.track_gains: Optional[List[Gain]] = None
|
||||||
|
|
||||||
def _store_track_gain(self, item, track_gain):
|
def _store_track_gain(self, item: Item, track_gain: Gain):
|
||||||
"""Store track gain for a single item in the database."""
|
"""Store track gain for a single item in the database."""
|
||||||
item.rg_track_gain = track_gain.gain
|
item.rg_track_gain = track_gain.gain
|
||||||
item.rg_track_peak = track_gain.peak
|
item.rg_track_peak = track_gain.peak
|
||||||
|
|
@ -140,13 +166,13 @@ class RgTask:
|
||||||
item.rg_track_peak,
|
item.rg_track_peak,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _store_album_gain(self, item):
|
def _store_album_gain(self, item: Item, album_gain: Gain):
|
||||||
"""Store album gain for a single item in the database.
|
"""Store album gain for a single item in the database.
|
||||||
|
|
||||||
The caller needs to ensure that `self.album_gain is not None`.
|
The caller needs to ensure that `self.album_gain is not None`.
|
||||||
"""
|
"""
|
||||||
item.rg_album_gain = self.album_gain.gain
|
item.rg_album_gain = album_gain.gain
|
||||||
item.rg_album_peak = self.album_gain.peak
|
item.rg_album_peak = album_gain.peak
|
||||||
item.store()
|
item.store()
|
||||||
self._log.debug(
|
self._log.debug(
|
||||||
"applied album gain {0} LU, peak {1} of FS",
|
"applied album gain {0} LU, peak {1} of FS",
|
||||||
|
|
@ -154,7 +180,7 @@ class RgTask:
|
||||||
item.rg_album_peak,
|
item.rg_album_peak,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _store_track(self, write):
|
def _store_track(self, write: bool):
|
||||||
"""Store track gain for the first track of the task in the database."""
|
"""Store track gain for the first track of the task in the database."""
|
||||||
item = self.items[0]
|
item = self.items[0]
|
||||||
if self.track_gains is None or len(self.track_gains) != 1:
|
if self.track_gains is None or len(self.track_gains) != 1:
|
||||||
|
|
@ -172,7 +198,7 @@ class RgTask:
|
||||||
item.try_write()
|
item.try_write()
|
||||||
self._log.debug("done analyzing {0}", item)
|
self._log.debug("done analyzing {0}", item)
|
||||||
|
|
||||||
def _store_album(self, write):
|
def _store_album(self, write: bool):
|
||||||
"""Store track/album gains for all tracks of the task in the database."""
|
"""Store track/album gains for all tracks of the task in the database."""
|
||||||
if (
|
if (
|
||||||
self.album_gain is None
|
self.album_gain is None
|
||||||
|
|
@ -190,12 +216,12 @@ class RgTask:
|
||||||
)
|
)
|
||||||
for item, track_gain in zip(self.items, self.track_gains):
|
for item, track_gain in zip(self.items, self.track_gains):
|
||||||
self._store_track_gain(item, track_gain)
|
self._store_track_gain(item, track_gain)
|
||||||
self._store_album_gain(item)
|
self._store_album_gain(item, self.album_gain)
|
||||||
if write:
|
if write:
|
||||||
item.try_write()
|
item.try_write()
|
||||||
self._log.debug("done analyzing {0}", item)
|
self._log.debug("done analyzing {0}", item)
|
||||||
|
|
||||||
def store(self, write):
|
def store(self, write: bool):
|
||||||
"""Store computed gains for the items of this task in the database."""
|
"""Store computed gains for the items of this task in the database."""
|
||||||
if self.album is not None:
|
if self.album is not None:
|
||||||
self._store_album(write)
|
self._store_album(write)
|
||||||
|
|
@ -213,44 +239,56 @@ class R128Task(RgTask):
|
||||||
tags.
|
tags.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, items, album, target_level, backend_name, log):
|
def __init__(
|
||||||
|
self,
|
||||||
|
items: Sequence[Item],
|
||||||
|
album: Optional[Album],
|
||||||
|
target_level: float,
|
||||||
|
backend_name: str,
|
||||||
|
log: Logger,
|
||||||
|
):
|
||||||
# R128_* tags do not store the track/album peak
|
# R128_* tags do not store the track/album peak
|
||||||
super().__init__(items, album, target_level, None, backend_name, log)
|
super().__init__(items, album, target_level, None, backend_name, log)
|
||||||
|
|
||||||
def _store_track_gain(self, item, track_gain):
|
def _store_track_gain(self, item: Item, track_gain: Gain):
|
||||||
item.r128_track_gain = track_gain.gain
|
item.r128_track_gain = track_gain.gain
|
||||||
item.store()
|
item.store()
|
||||||
self._log.debug("applied r128 track gain {0} LU", item.r128_track_gain)
|
self._log.debug("applied r128 track gain {0} LU", item.r128_track_gain)
|
||||||
|
|
||||||
def _store_album_gain(self, item):
|
def _store_album_gain(self, item: Item, album_gain: Gain):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
The caller needs to ensure that `self.album_gain is not None`.
|
The caller needs to ensure that `self.album_gain is not None`.
|
||||||
"""
|
"""
|
||||||
item.r128_album_gain = self.album_gain.gain
|
item.r128_album_gain = album_gain.gain
|
||||||
item.store()
|
item.store()
|
||||||
self._log.debug("applied r128 album gain {0} LU", item.r128_album_gain)
|
self._log.debug("applied r128 album gain {0} LU", item.r128_album_gain)
|
||||||
|
|
||||||
|
|
||||||
class Backend:
|
AnyRgTask = TypeVar("AnyRgTask", bound=RgTask)
|
||||||
|
|
||||||
|
|
||||||
|
class Backend(ABC):
|
||||||
"""An abstract class representing engine for calculating RG values."""
|
"""An abstract class representing engine for calculating RG values."""
|
||||||
|
|
||||||
NAME = ""
|
NAME = ""
|
||||||
do_parallel = False
|
do_parallel = False
|
||||||
|
|
||||||
def __init__(self, config, log):
|
def __init__(self, config: ConfigView, log: Logger):
|
||||||
"""Initialize the backend with the configuration view for the
|
"""Initialize the backend with the configuration view for the
|
||||||
plugin.
|
plugin.
|
||||||
"""
|
"""
|
||||||
self._log = log
|
self._log = log
|
||||||
|
|
||||||
def compute_track_gain(self, task):
|
@abstractmethod
|
||||||
|
def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
|
||||||
"""Computes the track gain for the tracks belonging to `task`, and sets
|
"""Computes the track gain for the tracks belonging to `task`, and sets
|
||||||
the `track_gains` attribute on the task. Returns `task`.
|
the `track_gains` attribute on the task. Returns `task`.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def compute_album_gain(self, task):
|
@abstractmethod
|
||||||
|
def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
|
||||||
"""Computes the album gain for the album belonging to `task`, and sets
|
"""Computes the album gain for the album belonging to `task`, and sets
|
||||||
the `album_gain` attribute on the task. Returns `task`.
|
the `album_gain` attribute on the task. Returns `task`.
|
||||||
"""
|
"""
|
||||||
|
|
@ -264,7 +302,7 @@ class FfmpegBackend(Backend):
|
||||||
NAME = "ffmpeg"
|
NAME = "ffmpeg"
|
||||||
do_parallel = True
|
do_parallel = True
|
||||||
|
|
||||||
def __init__(self, config, log):
|
def __init__(self, config: ConfigView, log: Logger):
|
||||||
super().__init__(config, log)
|
super().__init__(config, log)
|
||||||
self._ffmpeg_path = "ffmpeg"
|
self._ffmpeg_path = "ffmpeg"
|
||||||
|
|
||||||
|
|
@ -292,7 +330,7 @@ class FfmpegBackend(Backend):
|
||||||
"the --enable-libebur128 configuration option is required."
|
"the --enable-libebur128 configuration option is required."
|
||||||
)
|
)
|
||||||
|
|
||||||
def compute_track_gain(self, task):
|
def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
|
||||||
"""Computes the track gain for the tracks belonging to `task`, and sets
|
"""Computes the track gain for the tracks belonging to `task`, and sets
|
||||||
the `track_gains` attribute on the task. Returns `task`.
|
the `track_gains` attribute on the task. Returns `task`.
|
||||||
"""
|
"""
|
||||||
|
|
@ -310,7 +348,7 @@ class FfmpegBackend(Backend):
|
||||||
|
|
||||||
return task
|
return task
|
||||||
|
|
||||||
def compute_album_gain(self, task):
|
def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
|
||||||
"""Computes the album gain for the album belonging to `task`, and sets
|
"""Computes the album gain for the album belonging to `task`, and sets
|
||||||
the `album_gain` attribute on the task. Returns `task`.
|
the `album_gain` attribute on the task. Returns `task`.
|
||||||
"""
|
"""
|
||||||
|
|
@ -318,7 +356,7 @@ class FfmpegBackend(Backend):
|
||||||
|
|
||||||
# analyse tracks
|
# analyse tracks
|
||||||
# Gives a list of tuples (track_gain, track_n_blocks)
|
# Gives a list of tuples (track_gain, track_n_blocks)
|
||||||
track_results = [
|
track_results: List[Tuple[Gain, int]] = [
|
||||||
self._analyse_item(
|
self._analyse_item(
|
||||||
item,
|
item,
|
||||||
task.target_level,
|
task.target_level,
|
||||||
|
|
@ -328,8 +366,7 @@ class FfmpegBackend(Backend):
|
||||||
for item in task.items
|
for item in task.items
|
||||||
]
|
]
|
||||||
|
|
||||||
# list of track Gain objects
|
track_gains: List[Gain] = [tg for tg, _nb in track_results]
|
||||||
track_gains = [tg for tg, _nb in track_results]
|
|
||||||
|
|
||||||
# Album peak is maximum track peak
|
# Album peak is maximum track peak
|
||||||
album_peak = max(tg.peak for tg in track_gains)
|
album_peak = max(tg.peak for tg in track_gains)
|
||||||
|
|
@ -337,7 +374,7 @@ class FfmpegBackend(Backend):
|
||||||
# Total number of BS.1770 gating blocks
|
# Total number of BS.1770 gating blocks
|
||||||
n_blocks = sum(nb for _tg, nb in track_results)
|
n_blocks = sum(nb for _tg, nb in track_results)
|
||||||
|
|
||||||
def sum_of_track_powers(track_gain, track_n_blocks):
|
def sum_of_track_powers(track_gain: Gain, track_n_blocks: int):
|
||||||
# convert `LU to target_level` -> LUFS
|
# convert `LU to target_level` -> LUFS
|
||||||
loudness = target_level_lufs - track_gain.gain
|
loudness = target_level_lufs - track_gain.gain
|
||||||
|
|
||||||
|
|
@ -363,6 +400,7 @@ class FfmpegBackend(Backend):
|
||||||
album_gain = -0.691 + 10 * math.log10(sum_powers / n_blocks)
|
album_gain = -0.691 + 10 * math.log10(sum_powers / n_blocks)
|
||||||
else:
|
else:
|
||||||
album_gain = -70
|
album_gain = -70
|
||||||
|
|
||||||
# convert LUFS -> `LU to target_level`
|
# convert LUFS -> `LU to target_level`
|
||||||
album_gain = target_level_lufs - album_gain
|
album_gain = target_level_lufs - album_gain
|
||||||
|
|
||||||
|
|
@ -378,7 +416,9 @@ class FfmpegBackend(Backend):
|
||||||
|
|
||||||
return task
|
return task
|
||||||
|
|
||||||
def _construct_cmd(self, item, peak_method):
|
def _construct_cmd(
|
||||||
|
self, item: Item, peak_method: Optional[PeakMethod]
|
||||||
|
) -> List[Union[str, bytes]]:
|
||||||
"""Construct the shell command to analyse items."""
|
"""Construct the shell command to analyse items."""
|
||||||
return [
|
return [
|
||||||
self._ffmpeg_path,
|
self._ffmpeg_path,
|
||||||
|
|
@ -397,7 +437,13 @@ class FfmpegBackend(Backend):
|
||||||
"-",
|
"-",
|
||||||
]
|
]
|
||||||
|
|
||||||
def _analyse_item(self, item, target_level, peak_method, count_blocks=True):
|
def _analyse_item(
|
||||||
|
self,
|
||||||
|
item: Item,
|
||||||
|
target_level: float,
|
||||||
|
peak_method: Optional[PeakMethod],
|
||||||
|
count_blocks: bool = True,
|
||||||
|
) -> Tuple[Gain, int]:
|
||||||
"""Analyse item. Return a pair of a Gain object and the number
|
"""Analyse item. Return a pair of a Gain object and the number
|
||||||
of gating blocks above the threshold.
|
of gating blocks above the threshold.
|
||||||
|
|
||||||
|
|
@ -415,7 +461,7 @@ class FfmpegBackend(Backend):
|
||||||
# parse output
|
# parse output
|
||||||
|
|
||||||
if peak_method is None:
|
if peak_method is None:
|
||||||
peak = 0
|
peak = 0.0
|
||||||
else:
|
else:
|
||||||
line_peak = self._find_line(
|
line_peak = self._find_line(
|
||||||
output,
|
output,
|
||||||
|
|
@ -486,7 +532,13 @@ class FfmpegBackend(Backend):
|
||||||
|
|
||||||
return Gain(gain, peak), n_blocks
|
return Gain(gain, peak), n_blocks
|
||||||
|
|
||||||
def _find_line(self, output, search, start_line=0, step_size=1):
|
def _find_line(
|
||||||
|
self,
|
||||||
|
output: Sequence[bytes],
|
||||||
|
search: bytes,
|
||||||
|
start_line: int = 0,
|
||||||
|
step_size: int = 1,
|
||||||
|
) -> int:
|
||||||
"""Return index of line beginning with `search`.
|
"""Return index of line beginning with `search`.
|
||||||
|
|
||||||
Begins searching at index `start_line` in `output`.
|
Begins searching at index `start_line` in `output`.
|
||||||
|
|
@ -501,19 +553,19 @@ class FfmpegBackend(Backend):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _parse_float(self, line):
|
def _parse_float(self, line: bytes) -> float:
|
||||||
"""Extract a float from a key value pair in `line`.
|
"""Extract a float from a key value pair in `line`.
|
||||||
|
|
||||||
This format is expected: /[^:]:[[:space:]]*value.*/, where `value` is
|
This format is expected: /[^:]:[[:space:]]*value.*/, where `value` is
|
||||||
the float.
|
the float.
|
||||||
"""
|
"""
|
||||||
# extract value
|
# extract value
|
||||||
value = line.split(b":", 1)
|
parts = line.split(b":", 1)
|
||||||
if len(value) < 2:
|
if len(parts) < 2:
|
||||||
raise ReplayGainError(
|
raise ReplayGainError(
|
||||||
"ffmpeg output: expected key value pair, found {}".format(line)
|
f"ffmpeg output: expected key value pair, found {line!r}"
|
||||||
)
|
)
|
||||||
value = value[1].lstrip()
|
value = parts[1].lstrip()
|
||||||
# strip unit
|
# strip unit
|
||||||
value = value.split(b" ", 1)[0]
|
value = value.split(b" ", 1)[0]
|
||||||
# cast value to float
|
# cast value to float
|
||||||
|
|
@ -521,7 +573,7 @@ class FfmpegBackend(Backend):
|
||||||
return float(value)
|
return float(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ReplayGainError(
|
raise ReplayGainError(
|
||||||
"ffmpeg output: expected float value, found {}".format(value)
|
f"ffmpeg output: expected float value, found {value!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -530,7 +582,7 @@ class CommandBackend(Backend):
|
||||||
NAME = "command"
|
NAME = "command"
|
||||||
do_parallel = True
|
do_parallel = True
|
||||||
|
|
||||||
def __init__(self, config, log):
|
def __init__(self, config: ConfigView, log: Logger):
|
||||||
super().__init__(config, log)
|
super().__init__(config, log)
|
||||||
config.add(
|
config.add(
|
||||||
{
|
{
|
||||||
|
|
@ -539,7 +591,7 @@ class CommandBackend(Backend):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.command = config["command"].as_str()
|
self.command = cast(str, config["command"].as_str())
|
||||||
|
|
||||||
if self.command:
|
if self.command:
|
||||||
# Explicit executable path.
|
# Explicit executable path.
|
||||||
|
|
@ -562,7 +614,7 @@ class CommandBackend(Backend):
|
||||||
|
|
||||||
self.noclip = config["noclip"].get(bool)
|
self.noclip = config["noclip"].get(bool)
|
||||||
|
|
||||||
def compute_track_gain(self, task):
|
def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
|
||||||
"""Computes the track gain for the tracks belonging to `task`, and sets
|
"""Computes the track gain for the tracks belonging to `task`, and sets
|
||||||
the `track_gains` attribute on the task. Returns `task`.
|
the `track_gains` attribute on the task. Returns `task`.
|
||||||
"""
|
"""
|
||||||
|
|
@ -571,7 +623,7 @@ class CommandBackend(Backend):
|
||||||
task.track_gains = output
|
task.track_gains = output
|
||||||
return task
|
return task
|
||||||
|
|
||||||
def compute_album_gain(self, task):
|
def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
|
||||||
"""Computes the album gain for the album belonging to `task`, and sets
|
"""Computes the album gain for the album belonging to `task`, and sets
|
||||||
the `album_gain` attribute on the task. Returns `task`.
|
the `album_gain` attribute on the task. Returns `task`.
|
||||||
"""
|
"""
|
||||||
|
|
@ -590,7 +642,7 @@ class CommandBackend(Backend):
|
||||||
task.track_gains = output[:-1]
|
task.track_gains = output[:-1]
|
||||||
return task
|
return task
|
||||||
|
|
||||||
def format_supported(self, item):
|
def format_supported(self, item: Item) -> bool:
|
||||||
"""Checks whether the given item is supported by the selected tool."""
|
"""Checks whether the given item is supported by the selected tool."""
|
||||||
if "mp3gain" in self.command and item.format != "MP3":
|
if "mp3gain" in self.command and item.format != "MP3":
|
||||||
return False
|
return False
|
||||||
|
|
@ -598,7 +650,12 @@ class CommandBackend(Backend):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def compute_gain(self, items, target_level, is_album):
|
def compute_gain(
|
||||||
|
self,
|
||||||
|
items: Sequence[Item],
|
||||||
|
target_level: float,
|
||||||
|
is_album: bool,
|
||||||
|
) -> List[Gain]:
|
||||||
"""Computes the track or album gain of a list of items, returns
|
"""Computes the track or album gain of a list of items, returns
|
||||||
a list of TrackGain objects.
|
a list of TrackGain objects.
|
||||||
|
|
||||||
|
|
@ -618,7 +675,7 @@ class CommandBackend(Backend):
|
||||||
# tag-writing; this turns the mp3gain/aacgain tool into a gain
|
# tag-writing; this turns the mp3gain/aacgain tool into a gain
|
||||||
# calculator rather than a tag manipulator because we take care
|
# calculator rather than a tag manipulator because we take care
|
||||||
# of changing tags ourselves.
|
# of changing tags ourselves.
|
||||||
cmd = [self.command, "-o", "-s", "s"]
|
cmd: List[Union[bytes, str]] = [self.command, "-o", "-s", "s"]
|
||||||
if self.noclip:
|
if self.noclip:
|
||||||
# Adjust to avoid clipping.
|
# Adjust to avoid clipping.
|
||||||
cmd = cmd + ["-k"]
|
cmd = cmd + ["-k"]
|
||||||
|
|
@ -636,7 +693,7 @@ class CommandBackend(Backend):
|
||||||
output, len(items) + (1 if is_album else 0)
|
output, len(items) + (1 if is_album else 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_tool_output(self, text, num_lines):
|
def parse_tool_output(self, text: bytes, num_lines: int) -> List[Gain]:
|
||||||
"""Given the tab-delimited output from an invocation of mp3gain
|
"""Given the tab-delimited output from an invocation of mp3gain
|
||||||
or aacgain, parse the text and return a list of dictionaries
|
or aacgain, parse the text and return a list of dictionaries
|
||||||
containing information about each analyzed file.
|
containing information about each analyzed file.
|
||||||
|
|
@ -647,15 +704,15 @@ class CommandBackend(Backend):
|
||||||
if len(parts) != 6 or parts[0] == b"File":
|
if len(parts) != 6 or parts[0] == b"File":
|
||||||
self._log.debug("bad tool output: {0}", text)
|
self._log.debug("bad tool output: {0}", text)
|
||||||
raise ReplayGainError("mp3gain failed")
|
raise ReplayGainError("mp3gain failed")
|
||||||
d = {
|
|
||||||
"file": parts[0],
|
# _file = parts[0]
|
||||||
"mp3gain": int(parts[1]),
|
# _mp3gain = int(parts[1])
|
||||||
"gain": float(parts[2]),
|
gain = float(parts[2])
|
||||||
"peak": float(parts[3]) / (1 << 15),
|
peak = float(parts[3]) / (1 << 15)
|
||||||
"maxgain": int(parts[4]),
|
# _maxgain = int(parts[4])
|
||||||
"mingain": int(parts[5]),
|
# _mingain = int(parts[5])
|
||||||
}
|
|
||||||
out.append(Gain(d["gain"], d["peak"]))
|
out.append(Gain(gain, peak))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -665,7 +722,7 @@ class CommandBackend(Backend):
|
||||||
class GStreamerBackend(Backend):
|
class GStreamerBackend(Backend):
|
||||||
NAME = "gstreamer"
|
NAME = "gstreamer"
|
||||||
|
|
||||||
def __init__(self, config, log):
|
def __init__(self, config: ConfigView, log: Logger):
|
||||||
super().__init__(config, log)
|
super().__init__(config, log)
|
||||||
self._import_gst()
|
self._import_gst()
|
||||||
|
|
||||||
|
|
@ -722,7 +779,7 @@ class GStreamerBackend(Backend):
|
||||||
|
|
||||||
self._main_loop = self.GLib.MainLoop()
|
self._main_loop = self.GLib.MainLoop()
|
||||||
|
|
||||||
self._files = []
|
self._files: List[bytes] = []
|
||||||
|
|
||||||
def _import_gst(self):
|
def _import_gst(self):
|
||||||
"""Import the necessary GObject-related modules and assign `Gst`
|
"""Import the necessary GObject-related modules and assign `Gst`
|
||||||
|
|
@ -754,14 +811,17 @@ class GStreamerBackend(Backend):
|
||||||
self.GLib = GLib
|
self.GLib = GLib
|
||||||
self.Gst = Gst
|
self.Gst = Gst
|
||||||
|
|
||||||
def compute(self, items, target_level, album):
|
def compute(self, items: Sequence[Item], target_level: float, album: bool):
|
||||||
if len(items) == 0:
|
if len(items) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._error = None
|
self._error = None
|
||||||
self._files = [i.path for i in items]
|
self._files = [i.path for i in items]
|
||||||
|
|
||||||
self._file_tags = collections.defaultdict(dict)
|
# FIXME: Turn this into DefaultDict[bytes, Gain]
|
||||||
|
self._file_tags: DefaultDict[
|
||||||
|
bytes, Dict[str, float]
|
||||||
|
] = collections.defaultdict(dict)
|
||||||
|
|
||||||
self._rg.set_property("reference-level", target_level)
|
self._rg.set_property("reference-level", target_level)
|
||||||
|
|
||||||
|
|
@ -773,7 +833,7 @@ class GStreamerBackend(Backend):
|
||||||
if self._error is not None:
|
if self._error is not None:
|
||||||
raise self._error
|
raise self._error
|
||||||
|
|
||||||
def compute_track_gain(self, task):
|
def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
|
||||||
"""Computes the track gain for the tracks belonging to `task`, and sets
|
"""Computes the track gain for the tracks belonging to `task`, and sets
|
||||||
the `track_gains` attribute on the task. Returns `task`.
|
the `track_gains` attribute on the task. Returns `task`.
|
||||||
"""
|
"""
|
||||||
|
|
@ -793,7 +853,7 @@ class GStreamerBackend(Backend):
|
||||||
task.track_gains = ret
|
task.track_gains = ret
|
||||||
return task
|
return task
|
||||||
|
|
||||||
def compute_album_gain(self, task):
|
def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
|
||||||
"""Computes the album gain for the album belonging to `task`, and sets
|
"""Computes the album gain for the album belonging to `task`, and sets
|
||||||
the `album_gain` attribute on the task. Returns `task`.
|
the `album_gain` attribute on the task. Returns `task`.
|
||||||
"""
|
"""
|
||||||
|
|
@ -876,7 +936,7 @@ class GStreamerBackend(Backend):
|
||||||
|
|
||||||
tags.foreach(handle_tag, None)
|
tags.foreach(handle_tag, None)
|
||||||
|
|
||||||
def _set_first_file(self):
|
def _set_first_file(self) -> bool:
|
||||||
if len(self._files) == 0:
|
if len(self._files) == 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -886,7 +946,7 @@ class GStreamerBackend(Backend):
|
||||||
self._pipe.set_state(self.Gst.State.PLAYING)
|
self._pipe.set_state(self.Gst.State.PLAYING)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _set_file(self):
|
def _set_file(self) -> bool:
|
||||||
"""Initialize the filesrc element with the next file to be analyzed."""
|
"""Initialize the filesrc element with the next file to be analyzed."""
|
||||||
# No more files, we're done
|
# No more files, we're done
|
||||||
if len(self._files) == 0:
|
if len(self._files) == 0:
|
||||||
|
|
@ -919,7 +979,7 @@ class GStreamerBackend(Backend):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _set_next_file(self):
|
def _set_next_file(self) -> bool:
|
||||||
"""Set the next file to be analyzed while keeping the pipeline
|
"""Set the next file to be analyzed while keeping the pipeline
|
||||||
in the PAUSED state so that the rganalysis element can correctly
|
in the PAUSED state so that the rganalysis element can correctly
|
||||||
handle album gain.
|
handle album gain.
|
||||||
|
|
@ -960,7 +1020,7 @@ class AudioToolsBackend(Backend):
|
||||||
|
|
||||||
NAME = "audiotools"
|
NAME = "audiotools"
|
||||||
|
|
||||||
def __init__(self, config, log):
|
def __init__(self, config: ConfigView, log: Logger):
|
||||||
super().__init__(config, log)
|
super().__init__(config, log)
|
||||||
self._import_audiotools()
|
self._import_audiotools()
|
||||||
|
|
||||||
|
|
@ -980,7 +1040,7 @@ class AudioToolsBackend(Backend):
|
||||||
self._mod_audiotools = audiotools
|
self._mod_audiotools = audiotools
|
||||||
self._mod_replaygain = audiotools.replaygain
|
self._mod_replaygain = audiotools.replaygain
|
||||||
|
|
||||||
def open_audio_file(self, item):
|
def open_audio_file(self, item: Item):
|
||||||
"""Open the file to read the PCM stream from the using
|
"""Open the file to read the PCM stream from the using
|
||||||
``item.path``.
|
``item.path``.
|
||||||
|
|
||||||
|
|
@ -998,7 +1058,7 @@ class AudioToolsBackend(Backend):
|
||||||
|
|
||||||
return audiofile
|
return audiofile
|
||||||
|
|
||||||
def init_replaygain(self, audiofile, item):
|
def init_replaygain(self, audiofile, item: Item):
|
||||||
"""Return an initialized :class:`audiotools.replaygain.ReplayGain`
|
"""Return an initialized :class:`audiotools.replaygain.ReplayGain`
|
||||||
instance, which requires the sample rate of the song(s) on which
|
instance, which requires the sample rate of the song(s) on which
|
||||||
the ReplayGain values will be computed. The item is passed in case
|
the ReplayGain values will be computed. The item is passed in case
|
||||||
|
|
@ -1015,7 +1075,7 @@ class AudioToolsBackend(Backend):
|
||||||
return
|
return
|
||||||
return rg
|
return rg
|
||||||
|
|
||||||
def compute_track_gain(self, task):
|
def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
|
||||||
"""Computes the track gain for the tracks belonging to `task`, and sets
|
"""Computes the track gain for the tracks belonging to `task`, and sets
|
||||||
the `track_gains` attribute on the task. Returns `task`.
|
the `track_gains` attribute on the task. Returns `task`.
|
||||||
"""
|
"""
|
||||||
|
|
@ -1025,14 +1085,14 @@ class AudioToolsBackend(Backend):
|
||||||
task.track_gains = gains
|
task.track_gains = gains
|
||||||
return task
|
return task
|
||||||
|
|
||||||
def _with_target_level(self, gain, target_level):
|
def _with_target_level(self, gain: float, target_level: float):
|
||||||
"""Return `gain` relative to `target_level`.
|
"""Return `gain` relative to `target_level`.
|
||||||
|
|
||||||
Assumes `gain` is relative to 89 db.
|
Assumes `gain` is relative to 89 db.
|
||||||
"""
|
"""
|
||||||
return gain + (target_level - 89)
|
return gain + (target_level - 89)
|
||||||
|
|
||||||
def _title_gain(self, rg, audiofile, target_level):
|
def _title_gain(self, rg, audiofile, target_level: float):
|
||||||
"""Get the gain result pair from PyAudioTools using the `ReplayGain`
|
"""Get the gain result pair from PyAudioTools using the `ReplayGain`
|
||||||
instance `rg` for the given `audiofile`.
|
instance `rg` for the given `audiofile`.
|
||||||
|
|
||||||
|
|
@ -1050,7 +1110,7 @@ class AudioToolsBackend(Backend):
|
||||||
raise ReplayGainError("audiotools audio data error")
|
raise ReplayGainError("audiotools audio data error")
|
||||||
return self._with_target_level(gain, target_level), peak
|
return self._with_target_level(gain, target_level), peak
|
||||||
|
|
||||||
def _compute_track_gain(self, item, target_level):
|
def _compute_track_gain(self, item: Item, target_level: float):
|
||||||
"""Compute ReplayGain value for the requested item.
|
"""Compute ReplayGain value for the requested item.
|
||||||
|
|
||||||
:rtype: :class:`Gain`
|
:rtype: :class:`Gain`
|
||||||
|
|
@ -1073,7 +1133,7 @@ class AudioToolsBackend(Backend):
|
||||||
)
|
)
|
||||||
return Gain(gain=rg_track_gain, peak=rg_track_peak)
|
return Gain(gain=rg_track_gain, peak=rg_track_peak)
|
||||||
|
|
||||||
def compute_album_gain(self, task):
|
def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
|
||||||
"""Computes the album gain for the album belonging to `task`, and sets
|
"""Computes the album gain for the album belonging to `task`, and sets
|
||||||
the `album_gain` attribute on the task. Returns `task`.
|
the `album_gain` attribute on the task. Returns `task`.
|
||||||
"""
|
"""
|
||||||
|
|
@ -1121,7 +1181,7 @@ class ExceptionWatcher(Thread):
|
||||||
Once an exception occurs, raise it and execute a callback.
|
Once an exception occurs, raise it and execute a callback.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, queue, callback):
|
def __init__(self, queue: queue.Queue, callback: Callable[[], None]):
|
||||||
self._queue = queue
|
self._queue = queue
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
self._stopevent = Event()
|
self._stopevent = Event()
|
||||||
|
|
@ -1138,20 +1198,20 @@ class ExceptionWatcher(Thread):
|
||||||
# whether `_stopevent` is set
|
# whether `_stopevent` is set
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def join(self, timeout=None):
|
def join(self, timeout: Optional[float] = None):
|
||||||
self._stopevent.set()
|
self._stopevent.set()
|
||||||
Thread.join(self, timeout)
|
Thread.join(self, timeout)
|
||||||
|
|
||||||
|
|
||||||
# Main plugin logic.
|
# Main plugin logic.
|
||||||
|
|
||||||
BACKEND_CLASSES = [
|
BACKEND_CLASSES: List[Type[Backend]] = [
|
||||||
CommandBackend,
|
CommandBackend,
|
||||||
GStreamerBackend,
|
GStreamerBackend,
|
||||||
AudioToolsBackend,
|
AudioToolsBackend,
|
||||||
FfmpegBackend,
|
FfmpegBackend,
|
||||||
]
|
]
|
||||||
BACKENDS = {b.NAME: b for b in BACKEND_CLASSES}
|
BACKENDS: Dict[str, Type[Backend]] = {b.NAME: b for b in BACKEND_CLASSES}
|
||||||
|
|
||||||
|
|
||||||
class ReplayGainPlugin(BeetsPlugin):
|
class ReplayGainPlugin(BeetsPlugin):
|
||||||
|
|
@ -1178,7 +1238,7 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
|
|
||||||
# FIXME: Consider renaming the configuration option and deprecating the
|
# FIXME: Consider renaming the configuration option and deprecating the
|
||||||
# old name 'overwrite'.
|
# old name 'overwrite'.
|
||||||
self.force_on_import = self.config["overwrite"].get(bool)
|
self.force_on_import = cast(bool, self.config["overwrite"].get(bool))
|
||||||
|
|
||||||
# Remember which backend is used for CLI feedback
|
# Remember which backend is used for CLI feedback
|
||||||
self.backend_name = self.config["backend"].as_str()
|
self.backend_name = self.config["backend"].as_str()
|
||||||
|
|
@ -1224,21 +1284,21 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
# Start threadpool lazily.
|
# Start threadpool lazily.
|
||||||
self.pool = None
|
self.pool = None
|
||||||
|
|
||||||
def should_use_r128(self, item):
|
def should_use_r128(self, item: Item) -> bool:
|
||||||
"""Checks the plugin setting to decide whether the calculation
|
"""Checks the plugin setting to decide whether the calculation
|
||||||
should be done using the EBU R128 standard and use R128_ tags instead.
|
should be done using the EBU R128 standard and use R128_ tags instead.
|
||||||
"""
|
"""
|
||||||
return item.format in self.r128_whitelist
|
return item.format in self.r128_whitelist
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_r128_track_data(item):
|
def has_r128_track_data(item: Item) -> bool:
|
||||||
return item.r128_track_gain is not None
|
return item.r128_track_gain is not None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_rg_track_data(item):
|
def has_rg_track_data(item: Item) -> bool:
|
||||||
return item.rg_track_gain is not None and item.rg_track_peak is not None
|
return item.rg_track_gain is not None and item.rg_track_peak is not None
|
||||||
|
|
||||||
def track_requires_gain(self, item):
|
def track_requires_gain(self, item: Item) -> bool:
|
||||||
if self.should_use_r128(item):
|
if self.should_use_r128(item):
|
||||||
if not self.has_r128_track_data(item):
|
if not self.has_r128_track_data(item):
|
||||||
return True
|
return True
|
||||||
|
|
@ -1249,17 +1309,17 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_r128_album_data(item):
|
def has_r128_album_data(item: Item) -> bool:
|
||||||
return (
|
return (
|
||||||
item.r128_track_gain is not None
|
item.r128_track_gain is not None
|
||||||
and item.r128_album_gain is not None
|
and item.r128_album_gain is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_rg_album_data(item):
|
def has_rg_album_data(item: Item) -> bool:
|
||||||
return item.rg_album_gain is not None and item.rg_album_peak is not None
|
return item.rg_album_gain is not None and item.rg_album_peak is not None
|
||||||
|
|
||||||
def album_requires_gain(self, album):
|
def album_requires_gain(self, album: Album) -> bool:
|
||||||
# Skip calculating gain only when *all* files don't need
|
# Skip calculating gain only when *all* files don't need
|
||||||
# recalculation. This way, if any file among an album's tracks
|
# recalculation. This way, if any file among an album's tracks
|
||||||
# needs recalculation, we still get an accurate album gain
|
# needs recalculation, we still get an accurate album gain
|
||||||
|
|
@ -1274,7 +1334,12 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def create_task(self, items, use_r128, album=None):
|
def create_task(
|
||||||
|
self,
|
||||||
|
items: Sequence[Item],
|
||||||
|
use_r128: bool,
|
||||||
|
album: Optional[Album] = None,
|
||||||
|
) -> RgTask:
|
||||||
if use_r128:
|
if use_r128:
|
||||||
return R128Task(
|
return R128Task(
|
||||||
items,
|
items,
|
||||||
|
|
@ -1293,7 +1358,7 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
self._log,
|
self._log,
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_album(self, album, write, force=False):
|
def handle_album(self, album: Album, write: bool, force: bool = False):
|
||||||
"""Compute album and track replay gain store it in all of the
|
"""Compute album and track replay gain store it in all of the
|
||||||
album's items.
|
album's items.
|
||||||
|
|
||||||
|
|
@ -1316,7 +1381,7 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
|
|
||||||
self._log.info("analyzing {0}", album)
|
self._log.info("analyzing {0}", album)
|
||||||
|
|
||||||
discs = {}
|
discs: Dict[int, List[Item]] = {}
|
||||||
if self.config["per_disc"].get(bool):
|
if self.config["per_disc"].get(bool):
|
||||||
for item in album.items():
|
for item in album.items():
|
||||||
if discs.get(item.disc) is None:
|
if discs.get(item.disc) is None:
|
||||||
|
|
@ -1325,6 +1390,9 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
else:
|
else:
|
||||||
discs[1] = album.items()
|
discs[1] = album.items()
|
||||||
|
|
||||||
|
def store_cb(task: RgTask):
|
||||||
|
task.store(write)
|
||||||
|
|
||||||
for discnumber, items in discs.items():
|
for discnumber, items in discs.items():
|
||||||
task = self.create_task(items, use_r128, album=album)
|
task = self.create_task(items, use_r128, album=album)
|
||||||
try:
|
try:
|
||||||
|
|
@ -1332,14 +1400,14 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
self.backend_instance.compute_album_gain,
|
self.backend_instance.compute_album_gain,
|
||||||
args=[task],
|
args=[task],
|
||||||
kwds={},
|
kwds={},
|
||||||
callback=lambda task: task.store(write),
|
callback=store_cb,
|
||||||
)
|
)
|
||||||
except ReplayGainError as e:
|
except ReplayGainError as e:
|
||||||
self._log.info("ReplayGain error: {0}", e)
|
self._log.info("ReplayGain error: {0}", e)
|
||||||
except FatalReplayGainError as e:
|
except FatalReplayGainError as e:
|
||||||
raise ui.UserError(f"Fatal replay gain error: {e}")
|
raise ui.UserError(f"Fatal replay gain error: {e}")
|
||||||
|
|
||||||
def handle_track(self, item, write, force=False):
|
def handle_track(self, item: Item, write: bool, force: bool = False):
|
||||||
"""Compute track replay gain and store it in the item.
|
"""Compute track replay gain and store it in the item.
|
||||||
|
|
||||||
If ``write`` is truthy then ``item.write()`` is called to write
|
If ``write`` is truthy then ``item.write()`` is called to write
|
||||||
|
|
@ -1352,24 +1420,27 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
|
|
||||||
use_r128 = self.should_use_r128(item)
|
use_r128 = self.should_use_r128(item)
|
||||||
|
|
||||||
|
def store_cb(task: RgTask):
|
||||||
|
task.store(write)
|
||||||
|
|
||||||
task = self.create_task([item], use_r128)
|
task = self.create_task([item], use_r128)
|
||||||
try:
|
try:
|
||||||
self._apply(
|
self._apply(
|
||||||
self.backend_instance.compute_track_gain,
|
self.backend_instance.compute_track_gain,
|
||||||
args=[task],
|
args=[task],
|
||||||
kwds={},
|
kwds={},
|
||||||
callback=lambda task: task.store(write),
|
callback=store_cb,
|
||||||
)
|
)
|
||||||
except ReplayGainError as e:
|
except ReplayGainError as e:
|
||||||
self._log.info("ReplayGain error: {0}", e)
|
self._log.info("ReplayGain error: {0}", e)
|
||||||
except FatalReplayGainError as e:
|
except FatalReplayGainError as e:
|
||||||
raise ui.UserError(f"Fatal replay gain error: {e}")
|
raise ui.UserError(f"Fatal replay gain error: {e}")
|
||||||
|
|
||||||
def open_pool(self, threads):
|
def open_pool(self, threads: int):
|
||||||
"""Open a `ThreadPool` instance in `self.pool`"""
|
"""Open a `ThreadPool` instance in `self.pool`"""
|
||||||
if self.pool is None and self.backend_instance.do_parallel:
|
if self.pool is None and self.backend_instance.do_parallel:
|
||||||
self.pool = ThreadPool(threads)
|
self.pool = ThreadPool(threads)
|
||||||
self.exc_queue = queue.Queue()
|
self.exc_queue: queue.Queue[Exception] = queue.Queue()
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, self._interrupt)
|
signal.signal(signal.SIGINT, self._interrupt)
|
||||||
|
|
||||||
|
|
@ -1379,7 +1450,13 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
)
|
)
|
||||||
self.exc_watcher.start()
|
self.exc_watcher.start()
|
||||||
|
|
||||||
def _apply(self, func, args, kwds, callback):
|
def _apply(
|
||||||
|
self,
|
||||||
|
func: Callable[..., AnyRgTask],
|
||||||
|
args: List[Any],
|
||||||
|
kwds: Dict[str, Any],
|
||||||
|
callback: Callable[[AnyRgTask], Any],
|
||||||
|
):
|
||||||
if self.pool is not None:
|
if self.pool is not None:
|
||||||
|
|
||||||
def handle_exc(exc):
|
def handle_exc(exc):
|
||||||
|
|
@ -1425,9 +1502,9 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
self.exc_watcher.join()
|
self.exc_watcher.join()
|
||||||
self.pool = None
|
self.pool = None
|
||||||
|
|
||||||
def import_begin(self, session):
|
def import_begin(self, session: ImportSession):
|
||||||
"""Handle `import_begin` event -> open pool"""
|
"""Handle `import_begin` event -> open pool"""
|
||||||
threads = self.config["threads"].get(int)
|
threads = cast(int, self.config["threads"].get(int))
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.config["parallel_on_import"]
|
self.config["parallel_on_import"]
|
||||||
|
|
@ -1440,22 +1517,31 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
"""Handle `import` event -> close pool"""
|
"""Handle `import` event -> close pool"""
|
||||||
self.close_pool()
|
self.close_pool()
|
||||||
|
|
||||||
def imported(self, session, task):
|
def imported(self, session: ImportSession, task: ImportTask):
|
||||||
"""Add replay gain info to items or albums of ``task``."""
|
"""Add replay gain info to items or albums of ``task``."""
|
||||||
if self.config["auto"]:
|
if self.config["auto"]:
|
||||||
if task.is_album:
|
if task.is_album:
|
||||||
self.handle_album(task.album, False, self.force_on_import)
|
self.handle_album(task.album, False, self.force_on_import)
|
||||||
else:
|
else:
|
||||||
|
# Should be a SingletonImportTask
|
||||||
|
assert hasattr(task, "item")
|
||||||
self.handle_track(task.item, False, self.force_on_import)
|
self.handle_track(task.item, False, self.force_on_import)
|
||||||
|
|
||||||
def command_func(self, lib, opts, args):
|
def command_func(
|
||||||
|
self,
|
||||||
|
lib: Library,
|
||||||
|
opts: optparse.Values,
|
||||||
|
args: List[str],
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
write = ui.should_write(opts.write)
|
write = ui.should_write(opts.write)
|
||||||
force = opts.force
|
force = opts.force
|
||||||
|
|
||||||
# Bypass self.open_pool() if called with `--threads 0`
|
# Bypass self.open_pool() if called with `--threads 0`
|
||||||
if opts.threads != 0:
|
if opts.threads != 0:
|
||||||
threads = opts.threads or self.config["threads"].get(int)
|
threads = opts.threads or cast(
|
||||||
|
int, self.config["threads"].get(int)
|
||||||
|
)
|
||||||
self.open_pool(threads)
|
self.open_pool(threads)
|
||||||
|
|
||||||
if opts.album:
|
if opts.album:
|
||||||
|
|
@ -1482,7 +1568,7 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
# Silence interrupt exceptions
|
# Silence interrupt exceptions
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def commands(self):
|
def commands(self) -> List[ui.Subcommand]:
|
||||||
"""Return the "replaygain" ui subcommand."""
|
"""Return the "replaygain" ui subcommand."""
|
||||||
cmd = ui.Subcommand("replaygain", help="analyze for ReplayGain")
|
cmd = ui.Subcommand("replaygain", help="analyze for ReplayGain")
|
||||||
cmd.parser.add_album_option()
|
cmd.parser.add_album_option()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue