How embarrassing, apparently I managed to bungle this test:
As it turns out, changing the working directory within the script is enough to not block the folder after all.
Tool:
Name: Folderrenamer
Path: C:\Users\<User>\AppData\Local\Programs\Python\Python312\python.exe
Parameter:
D:\PythonScripts\folderrenamer.py -d "$regexp(%_folderpath%,(.*)\\,$1)" --mp3tag -s
for all selected files
Combining this with a filter like %track% IS 1 OR %track% IS 01 to only run it once per directory works as intended as you can see.

Sorry for wasting your time, everyone!
In case some of you want to try my script, here's the current version.
It requires Python, ffmpeg on PATH, Mp3tag on PATH and tqdm (pip3 install tqdm) and if you want to run it anywhere from the CLI instead of as an Mp3tag tool, I recommend adding the script (or the script folder) to PATH as well. There are many guides how you can add something to PATH.
WARNING:
Only use it from the CLI if you know what you're doing. Since it recursively scans and renames folders, you could very easily rename program folders that happen to have audio files in them if you execute it in the wrong directory! As a Mp3tag tool it's limited to renaming the folder of the file that you call it on.
Should someone want/need further explanations (basic usage instructions are available via -h or --help) I can add the script to github and write documentation. This is simply the version I use.
folderrenamer.py
import os
import sys
import subprocess
import re
import argparse
from tqdm import tqdm
from collections import defaultdict
# List of supported audio file extensions
AUDIO_EXTENSIONS = {"flac", "mp3", "m4a", "ogg"}
def parse_arguments():
def dir_path(path):
if os.path.isdir(path) and path != None:
return path
else:
raise argparse.ArgumentTypeError(f"readable_dir:{path} is not a valid path")
parser = argparse.ArgumentParser(description='Rename folder(s) based on the properties of contained audio files and append the format(s) in square brackets.')
parser.add_argument('-d', '--directory',
help='The directory that will be scanned for audio files and renamed based on their properties.', type=dir_path, default=".", const=".", nargs="?")
parser.add_argument('--mp3tag', action='store_true',
help='Add the renamed folders to Mp3tag. Press F5 in Mp3tag afterwards to refresh the file list.')
parser.add_argument('-s', '--single_folder', action='store_true',
help='Only scan a single folder for audio files and rename it.')
args: argparse.Namespace = parser.parse_args()
return args
def extract_audio_properties(file_path):
"""
Extracts the bitrate, bit depth, sampling rate, and number of channels from an audio file using ffprobe.
"""
try:
result = subprocess.run(
[
'ffprobe', '-v', 'error', '-select_streams', 'a:0',
'-show_entries', 'stream=bit_rate,sample_rate,bits_per_raw_sample,channels',
'-of', 'default=noprint_wrappers=1:nokey=1', file_path
],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
values = result.stdout.split('\n')
bitrate = int(values[2]) // 1000 if values[2].isdigit() else None # convert from bps to kbps
sampling_rate = int(values[0]) if values[0].isdigit() else None
bit_depth = int(values[3]) if values[3].isdigit() else None
channels = int(values[1]) if values[1].isdigit() else None
return bitrate, sampling_rate, bit_depth, channels
except Exception as e:
print(f"Error extracting properties from {file_path}: {e}")
return None, None, None, None
def analyze_folder(folder_path):
"""
Analyzes the audio files in a folder, collecting file types and audio properties.
"""
properties = defaultdict(list) # Structure: {filetype: [(bitrate, sampling_rate, bit_depth, channels)]}
mixed_formats = set()
for file in os.listdir(folder_path):
file_ext = os.path.splitext(file)[-1][1:].lower()
if file_ext in AUDIO_EXTENSIONS:
file_path = os.path.join(folder_path, file)
bitrate, sampling_rate, bit_depth, channels = extract_audio_properties(file_path)
if bitrate or sampling_rate or bit_depth or channels:
properties[file_ext].append((bitrate, sampling_rate, bit_depth, channels))
mixed_formats.add(file_ext.upper())
return properties, mixed_formats
def format_folder_name(base_name, properties, mixed_formats):
"""
Generate the new folder name based on the audio properties and file types found.
"""
if len(mixed_formats) > 2: # Don't needlessly bloat the filename with more than 2 formats
return f"{base_name} [MIXED FORMATS]"
# Define the custom order for formats
format_priority = {"flac": 1, "mp3": 2, "m4a": 3, "ogg": 4}
parts = []
# Process FLAC files first, sorted by descending quality
for filetype, prop_list in sorted(properties.items(), key=lambda x: format_priority.get(x[0], float('inf'))):
if filetype == 'flac':
flac_variants = []
for bitrate, sampling_rate, bit_depth, channels in sorted(
set(prop_list), key=lambda x: (-x[2], -x[1], -x[3] if x[3] else 0)
):
variant_parts = [f"{bit_depth}", f"{sampling_rate / 1000:g}"] # Convert Hz to kHz
if channels and channels > 2:
channel_map = {3: "2.1", 6: "5.1", 8: "7.1"}
variant_parts.append(channel_map.get(channels, str(channels)))
flac_variants.append("-".join(variant_parts))
parts.append(f"FLAC {', '.join(flac_variants)}")
else:
# Process other formats (e.g., MP3, M4A, OGG)
bitrates = sorted(set(bitrate for bitrate, _, _, _ in prop_list if bitrate), reverse=True)
if bitrates:
if len(bitrates) > 1:
all_bitrates = [bitrate for bitrate, _, _, _ in prop_list if bitrate]
avg_bitrate = round(sum(all_bitrates) / len(all_bitrates))
if len(bitrates) == 1:
parts.append(f"{filetype.upper()} {bitrates[0]}")
else:
parts.append(f"{filetype.upper()} {bitrates[-1]}-{bitrates[0]} ⌀{avg_bitrate}")
# Skip renaming folders that only contain CD quality FLAC files
if parts and parts != ["FLAC 16-44.1"]:
return f"{base_name} [{', '.join(parts)}]"
else:
return base_name
def main(args):
directory = args.directory
single_folder = args.single_folder
mp3tag = args.mp3tag
if mp3tag: os.chdir("..")
match_folder = re.compile(r"^(.*) \[(?:FLAC [\d,⌀ .-]+)?(?:MP3 [\d,⌀ .-]+)?(?:M4A [\d,⌀ .-]+)?(?:OGG [\d,⌀ .-]+)?\]$|^(.*) \[MIXED FORMATS\]$")
renamed = 0
skipped = 0
renamed_folders = []
"""
Recursively processes subfolders in the given base path, analyzing audio properties and renaming folders accordingly.
"""
if not single_folder:
with tqdm(desc="Scanning and renaming folders.", unit=" folders", ncols=100) as pbar:
for root, dirs, files in os.walk(directory, topdown=False): # Bottom-up to rename folders after processing their contents
folder_name = os.path.basename(root)
parent_path = os.path.dirname(root)
# Check if the folder has already been renamed
match = re.match(match_folder, folder_name)
base_name = match.group(1) or match.group(2) if match else folder_name
properties, mixed_formats = analyze_folder(root)
if not mixed_formats:
continue
new_folder_name = format_folder_name(base_name, properties, mixed_formats)
if new_folder_name != folder_name: # Compare to the original folder name
new_path = os.path.join(parent_path, new_folder_name)
try:
os.rename(root, new_path)
pbar.update(1)
renamed+=1
pbar.set_postfix({"renamed": renamed}, {"skipped": skipped})
renamed_folders.append(os.path.abspath(new_path))
except Exception as e:
print(f"Error renaming folder {root} to {new_path}: {e}")
else:
pbar.update(1)
skipped+=1
pbar.set_postfix({"renamed": renamed}, {"skipped": skipped})
else:
folder_name = os.path.basename(directory)
parent_path = os.path.dirname(directory)
# Check if the folder has already been renamed
match = re.match(match_folder, folder_name)
base_name = match.group(1) or match.group(2) if match else folder_name
properties, mixed_formats = analyze_folder(directory)
if not mixed_formats:
print ("No music files found in the folder, exiting.")
sys.exit()
new_folder_name = format_folder_name(base_name, properties, mixed_formats)
if new_folder_name != folder_name: # Compare to the original folder name
new_path = os.path.join(parent_path, new_folder_name)
try:
os.rename(directory, new_path)
print(f"Renamed: {directory} -> {new_path}")
renamed_folders.append(os.path.abspath(new_path))
except Exception as e:
print(f"Error renaming folder {directory} to {new_path}: {e}")
if mp3tag and renamed_folders:
for folder in renamed_folders:
try:
subprocess.run(["mp3tag", "/add", f'/fp:"{folder}"'])
except subprocess.CalledProcessError:
print(f"Error while adding {folder} to Mp3tag.")
# Uncomment when you want to inspect the output
#input("Press Enter to continue...")
if __name__ == "__main__":
args = parse_arguments()
try:
main(args)
except KeyboardInterrupt:
print("Interrupted")
try:
sys.exit(130)
except SystemExit:
os._exit(130)