/home/alex/dev/rename.py (1)

From RaySoft
#!/usr/bin/env python3
# ------------------------------------------------------------------------------
# rename.py
# =========
#
# Scope     Native
# Copyright (C) 2024 by RaySoft, Zurich, Switzerland
# License   GNU General Public License (GPL) 2.0
#           https://www.gnu.org/licenses/gpl2.txt
#
# ------------------------------------------------------------------------------

DEFAULT_EXT = 'jpg'
BACKUP_EXT = 'bkp'

# ------------------------------------------------------------------------------

PROGRAM_NAME = 'rename'
PROGRAM_VERSION = '0.1'

# ------------------------------------------------------------------------------

from argparse import ArgumentParser
from locale import setlocale, strxfrm, LC_COLLATE
from os import getcwd, listdir
from os.path import basename, isdir, isfile
from random import SystemRandom
from re import compile, search, IGNORECASE
from shutil import get_terminal_size, move
from sys import exit, stderr


# ------------------------------------------------------------------------------

def main():
    parser = ArgumentParser(
        description='Rename all files with a given extension in a directory.',
        prog=PROGRAM_NAME,
    )
    parser.add_argument(
        '-V', '--version', action='version',
        version=f'%(prog)s {PROGRAM_VERSION}',
    )
    group1 = parser.add_mutually_exclusive_group()
    group1.add_argument(
        '-q', '--quiet', action='store_true',
        help='increase verbosity',
    )
    group1.add_argument(
        '-v', '--verbose', action='store_true',
        help='suppress non-error messages',
    )
    parser.add_argument(
        '-r', '--random', action='store_true',
        help='sort files randomly',
    )
    parser.add_argument(
        '-p', '--path', metavar='PATH', dest='path',
        action='store', default=getcwd(),
        help="proceed in path (default is '.')",
    )
    group2 = parser.add_mutually_exclusive_group()
    group2.add_argument(
        '--all', action='store_true',
        help='rename all files',
    )
    group2.add_argument(
        '--ignore', action='store_true',
        help='do not rename files which have already the correct '
             'prefix and separator (default)',
    )
    parser.add_argument(
        '--prefix', metavar='PRE', dest='prefix', action='store',
        help='use file prefix (default is the basename of PATH)',
    )
    parser.add_argument(
        '--separator', metavar='SEP', dest='separator',
        action='store', default='-',
        help='use separator between prefix and file number '
             "(default is '-')",
    )
    group3 = parser.add_mutually_exclusive_group()
    group3.add_argument(
        '--extension', metavar='EXT', dest='extension',
        action='store', default=DEFAULT_EXT,
        help=f"define file extension (default is '{DEFAULT_EXT}')",
    )
    group3.add_argument(
        '--pattern', metavar='PAT', dest='pattern', action='store',
        help='use case insensitive regex pattern to filter the files '
             "(default is '\\.EXT$')",
    )

    args = parser.parse_args()

    if args.all is None:
        args.ignore = True

    if args.pattern is None:
        args.pattern = f'\\.{args.extension}$'

    if args.prefix is None:
        args.prefix = basename(args.path)

    if not isdir(args.path):
        print(f"Error finding directory '{args.path}'!", file=stderr)
        parser.print_usage(file=stderr)
        return 1

    file_index = 0
    file_counter = 0
    move_counter = 0
    progressbar = False

    files_2_backup = []
    files_2_ignore = []
    files_2_process = list(
        filter(
            lambda file: search(args.pattern, file, flags=IGNORECASE),
            listdir(args.path),
        )
    )

    amount_files = len(files_2_process)
    amount_files_2_ignore = 0
    amount_files_2_process = amount_files

    setlocale(LC_COLLATE, 'C')

    if amount_files <= 0:
        print(f"Error finding files with pattern '{args.pattern}' in "
              f"'{args.path}'!", file=stderr)
        return 1

    if not args.quiet:
        print(f"{amount_files} files with extension '{args.pattern}' found in "
              f"'{args.path}'.")

    if args.ignore:
        position = 0
        pattern = compile(rf'^{args.prefix}{args.separator}')

        while position < len(files_2_process):
            if pattern.match(r'{!s}'.format(files_2_process[position])):
                files_2_ignore.append(files_2_process.pop(position))
            else:
                position += 1

        amount_files_2_process = len(files_2_process)
        amount_files_2_ignore = len(files_2_ignore)

        if amount_files_2_ignore > 0:
            files_2_ignore.sort(key=strxfrm)

            match = search(r'{0!s}{1!s}(\d+)\..*'.format(args.prefix,
                           args.separator), files_2_ignore[-1])

            if match:
                file_index = int(match.group(1))

                if len(str(amount_files)) != len(str(file_index)):
                    print("Remove the 'ignore' flag and prozess all "
                          "files due to different index lengths!")

                    file_index = 0
                    amount_files_2_ignore = 0
                    amount_files_2_process = amount_files

                    files_2_process.extend(files_2_ignore)
            else:
                print(f"Error getting the index of the last file "
                      f"'{files_2_ignore[-1]}'!", file=stderr)
                return 1

        if not args.quiet:
            if amount_files_2_process == 0:
                print('ALL files are ignored!')
            else:
                print(f'{amount_files_2_process} files are moved and  '
                      f'{amount_files_2_ignore} are ignored. Counter starts at '
                      f'{file_index + 1}.')

    if args.random:
        urandom = SystemRandom()
        urandom.shuffle(files_2_process)
    else:
        files_2_process.sort(key=strxfrm)

    file_index_length = len(str(file_index + amount_files_2_process))

    if not (args.quiet or args.verbose) and amount_files_2_process > 10:
        progressbar = True
        bar_lenght = int(get_terminal_size()[0]) - 12
    elif args.verbose:
        print()

    for old_file in files_2_process:
        file_index += 1
        file_counter += 1

        old_file_path = '{0!s}/{1!s}'.format(args.path, old_file)
        new_file = '{0!s}{1!s}{2:0{3}d}.{4!s}'.format(
            args.prefix, args.separator, file_index, file_index_length,
            args.extension
        )
        new_file_path = '{0!s}/{1!s}'.format(args.path, new_file)

        if progressbar:
            progress = file_counter / amount_files_2_process

            print('\r{0:>4.0%} |{1:{2}s}|'.format(progress,
                  int(bar_lenght * progress) * '=', bar_lenght), end='')

        if not args.ignore and old_file_path == new_file_path:
            if args.verbose:
                print(f"New file is equal old one: '{new_file}'")

            continue

        if isfile(new_file_path):
            if args.verbose:
                print(f"Moving file: '{new_file}' > '{new_file}.{BACKUP_EXT}'")

            try:
                move(new_file_path, new_file_path + '.' + BACKUP_EXT)
            except (OSError, IOError) as error:
                print(f"Error moving file: '{new_file}' > "
                      f"'{new_file}.{BACKUP_EXT}': {error}", file=stderr)
                continue
            else:
                files_2_backup.append(new_file_path)
                move_counter += 1

        if old_file_path in files_2_backup:
            old_file += '.' + BACKUP_EXT
            old_file_path += '.' + BACKUP_EXT

        if args.verbose:
            print(f"Moving file: '{old_file}' > '{new_file}'")

        try:
            move(old_file_path, new_file_path)
        except (OSError, IOError) as error:
            print(f"Error moving file: '{old_file}' > '{new_file}': {error}",
                  file=stderr)
            continue
        else:
            move_counter += 1

    if progressbar:
        print(' done')

    if not args.quiet and amount_files_2_process > 0:
        if args.verbose:
            print()

        max_moves = 2 * amount_files

        print(f'Optimization: {move_counter} moves used instead of {max_moves}.'
              f'\nSavings: {1 - move_counter / max_moves:0.0%}')


# ------------------------------------------------------------------------------

if __name__ == '__main__':
    return_vaule = main()

    exit(return_vaule)

Usage

~/dev/rename.py --extension='jpg' --path="${HOME}/pictures" --random