Tool is executed in the album folder, preventing it from renaming it

I have tackled this problem once before here with a less sophisticated script.

Summary:
I have written a python script that renames music folders based on the audio properties of the contained audio files (by appending [MP3 320], [FLAC 24-96], [M4A 128-256 ⌀180], [FLAC 16-44.1, MP3 320] and so on sorted in descending quality).
While I am happy with running the script resursively from the CLI, I don't trust my father (who'd also like to use it) to run it in this way so using it as a tool in Mp3tag would make more sense for him and ensures that he doesn't accidentally run it in a folder where it could potentially cause harm.

Name: Folderrenamer (single)
Path: C:\Users\User\AppData\Local\Programs\Python\Python312\python.exe
Parameter: D:\PythonScripts\folderrenamer.py -d "$regexp(%_folderpath%,(.*)\\,$1)"

Calling it only on the album folder yields this error:

Scanning and renaming folders.: 0 folders [00:00, ? folders/s]Error renaming folder D:\Test\Kings of Convenience\Declaration of Dependence to D:\Test\Kings of Convenience\Declaration of Dependence [FLAC 16-44.1, MP3 320]: [WinError 32] The process cannot access the file because it is being used by another process: 'D:\\Test\\Kings of Convenience\\Declaration of Dependence' -> 'D:\\Test\\Kings of Convenience\\Declaration of Dependence [FLAC 16-44.1, MP3 320]'
Scanning and renaming folders.: 0 folders [00:00, ? folders/s]

By calling the script on the parent folder instead of the album folder like this, it can rename all folders at that level except the folder it was called in:

Name: Folderrenamer (parent)
Path: C:\Users\User\AppData\Local\Programs\Python\Python312\python.exe
Parameter: D:\PythonScripts\folderrenamer.py -d "$regexp(%_folderpath%,(.*)\\.+,$1)"

As can be seen from the output:

Scanning and renaming folders.: 0 folders [00:00, ? folders/s]Error renaming folder D:\Test\Kings of Convenience\Declaration of Dependence to D:\Test\Kings of Convenience\Declaration of Dependence [FLAC 16-44.1, MP3 320]: [WinError 32] The process cannot access the file because it is being used by another process: 'D:\\Test\\Kings of Convenience\\Declaration of Dependence' -> 'D:\\Test\\Kings of Convenience\\Declaration of Dependence [FLAC 16-44.1, MP3 320]'
Scanning and renaming folders.: 8 folders [00:01,  4.91 folders/s, renamed=4]

I also tested if changing the working directory within the script helps to prevent this but it did not change anything so I guess the way Mp3tag calls the tool blocks the folder until the tool is closed.

I could make it work by calling a .bat file as the tool in Mp3tag which then in turn calls the python script like @LyricsLover suggested as a workaround back then. That will likely work since the batch file stops execution the moment it calls the python script, thus no longer blocking the folder, allowing the script to rename it directly afterwards.
However that's a rather clumsy workaround I'd like to avoid if possible.

@Florian is there an easier way I'm unaware of to prevent Mp3tag from blocking the album folder when calling a tool?

I had to laugh when I read this. Seriously, you want this forum to help you solve a family issue? Well, here is the solution: create separate accounts on this PC, one for your dad and one for you, with different passwords. And don't give him your password!

Seems to me like your reading comprehension needs serious work (which is ironic since I'm not even a native speaker). Why do you feel the need to comment if you have nothing useful or related to the issue to say?

We have our own PCs, I just don't think it's wise to let someone with little command line experience execute a script that can quickly rename tens of thousands of folders in one go when you run it in the wrong place.
By using it as an Mp3tag tool this would be limited to the selected albums in the program.
However that's only anecdotal information anyhow.

The issue is simply that Mp3tag executes tools in the album folder which prevents the folder from being renamed by said tools and I'm looking for a cleaner way to overcome this limitation than creating a .bat file, passing the arguments to that and then calling the script from it.

I hope this clears up your misunderstandings.

Nothing useful? On the contrary, I gave you a simple and effective solution: restrict access to yourself. If you do that, then your "issue" with Mp3tag will be moot. Sometimes the simplest solution is best.

The answer to "how do I help a relative use this useful tool safely" is not "don't let them use it."

I don't know the answer to the question you asked, but does your dad need/want the same level of information as you, though? What about just appending the file format to the folder name, e.g. "... [FLAC]" or "... [MP3]"? That can definitely be done by mp3tag actions.

As he contributes to our family collection (which I manage), yes. He already adds the information in question, but does so manually by sorting the tracks in an album by bitrate and then typing it. Which is horribly inefficient and prone to typos.

That's not good enough (and very tricky when you want to handle multiple formats in one folder).
If we have an album in [MP3 128], we very much want to upgrade it. When it's already in [MP3 320] we'll still take a FLAC upgrade but it's already satisfactory.
Even for FLAC only albums we sometimes have different versions. [FLAC 24-96], [FLAC 24-48-5.1] and so on.
My script skips renaming albums that only have default CD quality FLACs (16-44.1) tho as that's the majority in our collection. We only want to know when the quality deviates from that. It also updates the folder name when the content has changed between executions.

The main upside is that my script handles mixed formats in a folder and calculates information based on all tracks in it (and it does so recursively for all subfolders when run from the CLI). You can achieve some of these things in Mp3tag, but neither easily nor efficiently.

I'll write documentation and put it on github if there's interest in the script itself.
Here's a quick demonstration (works recursively but to show the results I chose a flat folder structure):
folderrenamer

Anyhow, I'd still be grateful for a way to use it directly on an individual folder from within Mp3tag without blocking the folder.

I wonder why you have to use an external script to do something that can be done with MP3tag - an action of the type "Format value" or Convert>Tag-Tag for _DIRECTORY would also rename the current folder - without locking messages.

As I've written a couple times already, Mp3tag cannot do what my script does.

I used to calculate the average bitrate for the content of folders that only contain lossy files with 2 action groups.
First an export action:

Export file name:
D:\rips\tmp\$validate(%albumartist%,-) - $validate(%album%,-).txt
☑ One file per directory

$loop(%_filename_ext%)
$puts(Vbitrate,$add($get(Vbitrate),$mul(%_bitrate%,%_length_seconds%)))
$puts(cnt1,$add($get(cnt1),%_length_seconds%))
$loopend()
$div($get(Vbitrate),$get(cnt1))

And then a second action group (since Mp3tag cannot write to and read from a file in the same action group) to import that:


After which my default action used _DIRECTORY to rename the folder.

Which are already 2 steps (I don't count the rename action as I could include it in the 2nd action group), only calculates the average bitrate, leaves behind a text file, changes the modification time of the audio files, does not work for folders with a mix of lossless and lossy files, relies on changing tags while only the folder name should be changed and so on.

And for FLAC only albums I used this very readable and not at all convoluted action to append format information.
Format value:
Field:
ALBUM
Format string:

$regexp(%album%,(.*)(\s\'['\d{2}'['\/-']'\d{2,3}\.?\d?'['\/-']'?(\d\.\d)?\']'),$1)$if($eql($right(%_filename_ext%,4),flac),$if($or($grtr(%_bitspersample%,16),$grtr(%_samplerate%,44100)), '['%_bitspersample%/$cutRight(%_samplerate%,3)$trimRight('.'$right(%_samplerate%,3),'.0')$if($eql(%_mode%,3),/3.0,)$if($eql(%_mode%,4),/4.0,)$if($eql(%_mode%,5),/5.0,)$if($eql(%_mode%,6),/5.1,)']',),)

If you can recreate the functionality of my script with Mp3tag internal means I'd be very impressed. I put considerable time and thought into finding such a solution myself before I gave up and wrote a python script instead that achieves more and can be easily extended.

I see your point. But actually, I am too lazy to find a solution for a problem that I don't have.
In a way, you have found the root cause for your problem: if you want to perform only file system operations, then a dedicated script or program to handle files and folders is the best way - which would be an external program or script.
Yet, it is not very friendly in respect to MP3tag to externally rename a folder that MP3tag has read and still thinks is there. So I would consider to close MP3tag and not use an MP3tag tool but the already mentioned batch jobs.

I have thought of that. This issue can be avoided by passing an argument to my script that makes it add the renamed folders back to Mp3tag via the cli Mp3tag.exe /add /fp:"<updated_folder_path>". Then pressing F5 in Mp3tag after executing the tool will remove the no longer existing folders from view while the renamed and unchanged ones remain.
Executing one tool and then pressing F5 should be simple enough.

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
:ballot_box_with_check: 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.
folderrenamer

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)

Glad to hear that you managed to solve the issue yourself!

I have a suggestion for you on using this forum. Please don't be offended because I really am trying to help. In your first post on a new topic, try something like this:

"When running an Mp3tag Tool, I want to avoid having to use a batch file to rename the current folder. Does anyone know of a way to avoid that?"

Then STOP. You have said all that is needed to start the topic!

So lay out the core issue straight away and in as few words as possible. Then wait for the responses.

According to my word processor, you have used more than a thousand words in your various posts on this subject. That tends to bore and fatigue your readers. We really don't need or want all those details. And I am sure you could better use the time it took to compose them.

Best regards,
Doug

^

I type around 100 words per minute blind so I don't really spend much time writing and prefer to give as much information as possible to avoid having to be asked for information I could just as well provide from the start.

If detailed information bores you, skipping my posts is always an option.

Cheers!

It is not the details so much as their irrelevance.

What you are missing is that this a support forum, not a chat room. It is not a place where you can feel free to indulge your every thought, regardless of its relevance to the topic at hand.

This forum has some hard working and very knowledgable members. So why not respect their time and their expertise by being concise and focused when you post here? I gave you an example of how to start a topic with just two sentences. It is really not that hard. And, if you start doing that, I suspect that forum members will give a sigh of relief.

Sorry a bit late but I have to comment on the tone here.

You should work on your manners and tone. It certainly looks like you're trying to ridicule a forum member. It's rude and takes the joy out of reading.

Are you sincerely trying to help the OP? Because it seems more like arrogant lecturing, and your posts seem kind of passive-aggressive.

I think you need to refine your writing style a whole lot more than the person you criticise.

I was laughing at the situation with his dad, not at the poster. However, I can see how that might have been misinterpreted. I do regret that.

I try to make my style as concise as possible and to the point. My style has worked well for me for a very long time. And I was not criticizing the person, only his post. That is an important distinction.

Surely it is not arrogant to suggest trimming down a post!

In a public forum, there will always be a give and take, back and forth. And people may have different styles of discourse. Unfortunately, some people take any criticism of their posts as a personal attack. I have no control over their reaction.