From b3fd84b35651edf2c7b7958a06debec4d3384558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Mon, 10 Mar 2025 00:07:21 +0000 Subject: [PATCH] Move max filename length calculation closer to where it is used --- beets/library.py | 7 +------ beets/util/__init__.py | 30 +++++++++++++++++++----------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/beets/library.py b/beets/library.py index ec530a5dd..1fe253c18 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1125,13 +1125,8 @@ class Item(LibModel): subpath, beets.config["path_sep_replace"].as_str() ) - maxlen = beets.config["max_filename_length"].get(int) - if not maxlen: - # When zero, try to determine from filesystem. - maxlen = util.max_filename_length(db.directory) - lib_path_str, fallback = util.legalize_path( - subpath, db.replacements, maxlen, os.path.splitext(self.path)[1] + subpath, db.replacements, os.path.splitext(self.path)[1] ) if fallback: # Print an error message if legalization fell back to diff --git a/beets/util/__init__.py b/beets/util/__init__.py index dc2bb6089..d8340a978 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -30,6 +30,7 @@ import traceback from collections import Counter from contextlib import suppress from enum import Enum +from functools import cache from importlib import import_module from multiprocessing.pool import ThreadPool from pathlib import Path @@ -47,6 +48,7 @@ from typing import ( from unidecode import unidecode +import beets from beets.util import hidden if TYPE_CHECKING: @@ -694,25 +696,26 @@ def sanitize_path(path: str, replacements: Replacements | None = None) -> str: return os.path.join(*comps) -def truncate_path(path: AnyStr, length: int = MAX_FILENAME_LENGTH) -> AnyStr: +def truncate_path(path: AnyStr) -> AnyStr: """Given a bytestring path or a Unicode path fragment, truncate the components to a legal length. In the last component, the extension is preserved. """ + max_length = get_max_filename_length() comps = components(path) out = [c[:length] for c in comps] base, ext = os.path.splitext(comps[-1]) if ext: # Last component has an extension. - base = base[: length - len(ext)] + base = base[: max_length - len(ext)] out[-1] = base + ext return os.path.join(*out) def _legalize_stage( - path: str, replacements: Replacements | None, length: int, extension: str + path: str, replacements: Replacements | None, extension: str ) -> tuple[str, bool]: """Perform a single round of path legalization steps 1. sanitation/replacement @@ -729,13 +732,13 @@ def _legalize_stage( # Truncate too-long components. pre_truncate_path = path - path = truncate_path(path, length) + path = truncate_path(path) return path, path != pre_truncate_path def legalize_path( - path: str, replacements: Replacements | None, length: int, extension: str + path: str, replacements: Replacements | None, extension: str ) -> tuple[str, bool]: """Given a path-like Unicode string, produce a legal path. Return the path and a flag indicating whether some replacements had to be ignored (see @@ -755,21 +758,21 @@ def legalize_path( after it was truncated); the application should probably log some sort of warning. """ - args = length, as_string(extension) + suffix = as_string(extension) first_stage, _ = os.path.splitext( - _legalize_stage(path, replacements, *args)[0] + _legalize_stage(path, replacements, suffix)[0] ) # Re-sanitize following truncation (including user replacements). - second_stage, truncated = _legalize_stage(first_stage, replacements, *args) + second_stage, truncated = _legalize_stage(first_stage, replacements, suffix) if not truncated: return second_stage, False # If the path was truncated, discard user replacements # and run through one last legalization stage. - return _legalize_stage(first_stage, None, *args)[0], True + return _legalize_stage(first_stage, None, suffix)[0], True def str2bool(value: str) -> bool: @@ -848,16 +851,21 @@ def command_output(cmd: list[BytesOrStr], shell: bool = False) -> CommandOutput: return CommandOutput(stdout, stderr) -def max_filename_length(path: BytesOrStr, limit=MAX_FILENAME_LENGTH) -> int: +@cache +def get_max_filename_length() -> int: """Attempt to determine the maximum filename length for the filesystem containing `path`. If the value is greater than `limit`, then `limit` is used instead (to prevent errors when a filesystem misreports its capacity). If it cannot be determined (e.g., on Windows), return `limit`. """ + if length := beets.config["max_filename_length"].get(int): + return length + + limit = MAX_FILENAME_LENGTH if hasattr(os, "statvfs"): try: - res = os.statvfs(path) + res = os.statvfs(beets.config["directory"].as_str()) except OSError: return limit return min(res[9], limit)