Skip to content

Instantly share code, notes, and snippets.

@ivannp
Created February 12, 2017 02:28
Show Gist options
  • Save ivannp/ddc501f62f0c159d13202b111fb19721 to your computer and use it in GitHub Desktop.
Save ivannp/ddc501f62f0c159d13202b111fb19721 to your computer and use it in GitHub Desktop.
Converting files (.m4b to .mp3) in python
import glob
import os
import subprocess
def main():
base_dir = os.path.abspath('d:/abooks')
ffmpeg_path = os.path.abspath('C:/tools/ffmpeg/bin/ffmpeg.exe')
pattern = os.path.join(base_dir, '**', '*.m4b')
file_list = [file for file in glob.glob(pattern, recursive = True)]
for file in file_list:
dir_path, file_name = os.path.split(file)
# Drop the file extension
file_name = os.path.splitext(file_name)[0]
output_path = os.path.join(dir_path, file_name + '.mp3')
if not os.path.exists(output_path):
# Convert
print('Converting: ' + file)
subprocess.call([ffmpeg_path, '-i', file, '-acodec', 'libmp3lame', output_path])
else:
print('Skipping: ' + file)
if __name__ == "__main__":
main()
@raphael303
Copy link

raphael303 commented Mar 31, 2025

This version runs on python 3 on a mac, but only without mp4v2.

#!/usr/bin/env python

import argparse
# import ctypes # Unused import removed
import datetime
import logging
import os
import re
import shutil
import subprocess
import sys
from textwrap import dedent


class Chapter:
    """MP4 Chapter.

    Start, end, and duration times are stored in seconds.
    """
    def __init__(self, title=None, start=None, end=None, num=None):
        # Ensure start and end are treated as numbers before division
        try:
            start_ms = int(start)
        except (ValueError, TypeError):
            start_ms = 0
        try:
            end_ms = int(end)
        except (ValueError, TypeError):
            end_ms = 0

        self.title = title
        self.start = round(start_ms / 1000.0, 3)
        self.end = round(end_ms / 1000.0, 3)
        self.num = num

    def duration(self):
        if self.start is None or self.end is None:
            return None
        else:
            # Ensure calculation is safe even if start/end are somehow None
            start_sec = self.start or 0.0
            end_sec = self.end or 0.0
            return round(end_sec - start_sec, 3)

    def __str__(self):
        start_td = datetime.timedelta(seconds=self.start or 0.0)
        end_td = datetime.timedelta(seconds=self.end or 0.0)
        duration_td = datetime.timedelta(seconds=self.duration() or 0.0)
        return '<Chapter Title="%s", Start=%s, End=%s, Duration=%s>' % (
            self.title, start_td, end_td, duration_td)


def run_command(log, cmdstr, values, action, ignore_errors=False, **kwargs):
    cmd = []
    for opt in cmdstr.split(' '):
        # Ensure values are strings before substitution if necessary
        safe_values = {k: str(v) if v is not None else '' for k, v in values.items()}
        try:
             cmd.append(opt % safe_values)
        except TypeError as e:
             log.error(f"Error formatting command part '{opt}' with values {safe_values}: {e}")
             sys.exit(1)

    log.debug(f"Running command: {' '.join(cmd)}") # Log the actual command being run
    # Ensure shell=True only when necessary (like pipe_wav), pass full command list otherwise
    use_shell = kwargs.get('shell', False)
    command_to_run = ' '.join(cmd) if use_shell else cmd

    try:
        proc = subprocess.Popen(command_to_run, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=use_shell)
        (stdout_bytes, stderr_bytes) = proc.communicate() # Get bytes output
    except FileNotFoundError:
        log.error(f"Error: Command not found. Make sure '{cmd[0]}' is installed and in your PATH.")
        sys.exit(1)
    except Exception as e:
        log.error(f"An unexpected error occurred while running the command: {e}")
        sys.exit(1)


    # Decode output here for consistency
    stdout = stdout_bytes.decode('utf-8', errors='ignore')
    stderr = stderr_bytes.decode('utf-8', errors='ignore')
    output = stderr + stdout # Combine decoded streams, often ffmpeg info goes to stderr

    if not ignore_errors and proc.returncode != 0:
        msg = dedent('''
            An error occurred while %s.
              Command: %s
              Return code: %s
              Output: ---->
            %s''')
        # Use the originally intended command string for logging
        log_cmd = cmdstr % values if isinstance(values, dict) else cmdstr
        log.error(msg % (action, log_cmd, proc.returncode, stderr or stdout)) # Log stderr if available
        sys.exit(1)

    # Return the combined decoded output
    return output

def parse_args():
    """Parse command line arguments."""

    parser = argparse.ArgumentParser(
        description='Split m4b audio book by chapters.')

    parser.add_argument('-o', '--output-dir', help='directory to store encoded files',
                        metavar='DIR')
    parser.add_argument('--custom-name', default='%(num)02d-%(title)s', metavar='"STR"', # Changed default
                        help='customize chapter filenames (see README)')
    parser.add_argument('--ffmpeg', default='ffmpeg', metavar='BIN',
                        help='path to ffmpeg binary')
    parser.add_argument('--encoder', metavar='BIN',
                        help='path to encoder binary (default: ffmpeg)')
    parser.add_argument('--encode-opts', default='-y -i %(infile)s -acodec libmp3lame -ar %(sample_rate)d -ab %(bit_rate)dk %(outfile)s',
                        metavar='"STR"', help='custom encoding string (see README)')
    parser.add_argument('--ext', default='mp3', help='extension of encoded files')
    parser.add_argument('--pipe-wav', action='store_true', help='pipe wav to encoder')
    parser.add_argument('--skip-encoding', action='store_true',
                        help='do not encode audio (keep as .mp4)')
    parser.add_argument('--no-mp4v2', action='store_true',
                        help='use ffmpeg to retrieve chapters (not recommended)')
    parser.add_argument('--debug', action='store_true',
                        help='output debug messages and save to log file')
    parser.add_argument('filename', help='m4b file(s) to be converted', nargs='+')

    args = parser.parse_args()

    # Use os.path.abspath to handle empty cwd correctly
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # No need to chdir, use absolute paths instead
    # if not script_dir == '':
    #     os.chdir(script_dir)

    if args.output_dir is None:
        args.output_dir = os.getcwd() # Default to current working directory
    else:
        # Ensure output dir is absolute or relative to cwd
        args.output_dir = os.path.abspath(args.output_dir)


    if args.encoder is None:
        args.encoder = args.ffmpeg

    # Ensure filenames are absolute paths
    args.filename = [os.path.abspath(f) for f in args.filename]

    return args

def setup_logging(args, basename):
    """Setup logger. In debug mode debug messages will be saved to a log file."""

    log = logging.getLogger(basename)
    # Prevent duplicate handlers if called multiple times
    if log.hasHandlers():
        log.handlers.clear()


    sh = logging.StreamHandler(sys.stdout) # Use stdout explicitly
    formatter = logging.Formatter("%(levelname)s: %(message)s")

    sh.setFormatter(formatter)

    if args.debug:
        level = logging.DEBUG
        # Place log file in the script's directory or a defined log directory
        script_dir = os.path.dirname(os.path.abspath(__file__))
        filename = '%s.log' % basename
        log_path = os.path.join(script_dir, filename) # Consider placing logs elsewhere if needed
        try:
            # Use 'a' for append or 'w' for overwrite
            fh = logging.FileHandler(log_path, 'w', encoding='utf-8')
            fh.setFormatter(formatter) # Use same formatter for consistency
            fh.setLevel(level)
            log.addHandler(fh)
        except Exception as e:
             print(f"Warning: Could not create log file at {log_path}: {e}")
    else:
        level = logging.INFO

    log.setLevel(level)
    sh.setLevel(level)
    log.addHandler(sh)

    log.info('m4bsplit started.')
    if args.debug:
        s = ['Options:']
        # Use vars(args) for cleaner access to arguments dictionary
        for k, v in vars(args).items():
            s.append('    %s: %s' % (k, v))
        log.debug('\n'.join(s))
    return log

def ffmpeg_metadata(args, log, filename):
    """Load metadata using the command output from ffmpeg.

    Note: Not all chapter types are supported by ffmpeg and there's no Unicode support.
    """

    chapters = []

    values = dict(ffmpeg=args.ffmpeg, infile=filename)
    cmd = '%(ffmpeg)s -i %(infile)s'
    log.debug('Retrieving metadata from output of command: %s' % (cmd % values))

    # Run command and get combined decoded stdout/stderr
    output = run_command(log, cmd, values, 'retrieving metadata from ffmpeg output',
        ignore_errors=True) # output is now guaranteed to be a string

    # --- Fix applied: output is now a string, proceed with string operations ---
    try:
        # Process output string - be robust against variations in ffmpeg output format
        metadata_part = ""
        chapters_part = ""

        # Try splitting based on common delimiters
        if " Metadata:" in output and " Chapter #" in output:
             parts = output.split(" Chapter #", 1) # Split only once
             metadata_part = parts[0]
             chapters_part = " Chapter #" + parts[1] # Re-add delimiter for regex matching later
        elif "Input #" in output:
             # Fallback if " Chapter #" isn't present but "Input #" is
             metadata_part = output.split("Input #", 1)[1] # Take content after "Input #"
             # Chapters might not be parsable here if " Chapter #" delimiter was missing
             chapters_part = "" # Assume no chapters found in this case
        else:
             # If neither delimiter is found, treat all output as metadata part
             # This might happen with audio files without chapters or unusual ffmpeg versions
             log.warning("Could not reliably identify metadata/chapter sections in ffmpeg output.")
             metadata_part = output
             chapters_part = ""


        # Extract raw metadata string section (example logic, might need refinement)
        # Look for section between "Input #" and the first "Stream #" or "Chapter #"
        input_match = re.search(r'Input #.*? from .*\n', metadata_part, re.DOTALL)
        stream_match = re.search(r'\n\s*Stream #', metadata_part, re.DOTALL)

        if input_match:
             start_index = input_match.end()
             end_index = stream_match.start() if stream_match else len(metadata_part)
             raw_metadata = metadata_part[start_index:end_index]
        else:
             raw_metadata = metadata_part # Less precise fallback

        # Extract raw chapters string section (if chapters_part was populated)
        raw_chapters = []
        if chapters_part:
            # Split chapters based on the " Chapter #..." pattern
            # Using regex to handle variations like #0:0, #0.0 etc.
            raw_chapters = re.split(r'\s*Chapter #\d+[:\.]\d+:', chapters_part)[1:]


        # --- Parse stream, duration, and metadata ---
        # Use regex with error handling and default values

        sample_rate, bit_rate = 44100, 64 # Defaults
        m_stream = re.search(r'Stream .*: Audio: .*, ([\d]+) Hz, .*, .*, ([\d]+) kb\/s', output)
        if m_stream:
            try:
                sample_rate = int(m_stream.group(1))
                # Bit rate might not always be present or reliable from stream info
                # bit_rate = int(m_stream.group(2))
            except (ValueError, IndexError):
                 log.warning("Could not parse sample rate from stream info.")

        metadata = {}
        m_duration = re.search(r'Duration: ([^,]+), start: ([^,]+), bitrate: (\d+)\s*kb\/s', output)
        if m_duration:
            try:
                metadata['duration'] = m_duration.group(1).strip()
                metadata['start_time'] = m_duration.group(2).strip() # Renamed key for clarity
                bit_rate = int(m_duration.group(3)) # Prefer bitrate from duration line
            except (ValueError, IndexError):
                log.warning("Could not parse duration/bitrate line.")
        else:
            # Try alternative bitrate regex if main duration line fails
            m_alt_bitrate = re.search(r'bitrate: (\d+)\s*kb\/s', output)
            if m_alt_bitrate:
                 try:
                      bit_rate = int(m_alt_bitrate.group(1))
                 except ValueError:
                      log.warning("Could not parse alternative bitrate.")


        # General metadata parsing (key: value)
        # Look for lines like "  title           : ..." in the raw_metadata section
        for line in raw_metadata.strip().split('\n'):
            match = re.match(r'\s*(\w+)\s*:\s*(.*)', line)
            if match:
                key = match.group(1).strip()
                value = match.group(2).strip()
                if key: # Ensure key is not empty
                     metadata[key] = value


        # --- Parse chapters ---
        # Regex looking for 'start 123.456, end 789.123' and 'title : Chapter Title'
        # Adjusted regex to be more robust
        re_chapter = re.compile(r'start\s+([\d\.]+),\s+end\s+([\d\.]+)\s*Metadata:\s*title\s*:\s*(.*)', re.IGNORECASE | re.DOTALL)

        n = 1
        for raw_chapter_text in raw_chapters:
            # Each raw_chapter_text now contains the details for one chapter
            m = re_chapter.search(raw_chapter_text)
            if m:
                try:
                    start_sec = float(m.group(1))
                    end_sec = float(m.group(2))
                    # Title might be multi-line, clean it up
                    title = m.group(3).strip()
                    # Remove potential nested metadata lines from title if necessary
                    title = title.split('\n')[0].strip()

                    # Convert seconds to milliseconds for Chapter class
                    start_ms = start_sec * 1000
                    end_ms = end_sec * 1000

                    # Create Chapter object - title is already a string
                    chapter = Chapter(num=n, title=title, start=start_ms, end=end_ms)
                    chapters.append(chapter)
                    n += 1
                except (ValueError, IndexError) as e:
                    log.warning(f"Could not parse chapter data from block: {raw_chapter_text[:100]}... Error: {e}")
            else:
                 log.warning(f"Regex did not match expected chapter format in block: {raw_chapter_text[:100]}...")

    except Exception as e:
        log.error(f"An error occurred during ffmpeg metadata parsing: {e}")
        log.error(f"Problematic ffmpeg output snippet (approx): {output[max(0, output.find('Input #')-50):output.find('Input #')+500]}") # Log relevant part
        # Return empty/default data instead of crashing
        return [], 44100, 64, {}


    return chapters, sample_rate, bit_rate, metadata


def mp4v2_metadata(filename):
    """Load metadata with libmp4v2. Supports both chapter types and Unicode."""
    # This part remains unchanged as it requires the libmp4v2 Python module
    # which is likely unavailable. If it were available, it would be used.
    try:
        from libmp4v2 import MP4File # This import will fail if module not installed

        mp4 = MP4File(filename)
        mp4.load_meta()

        chapters = mp4.chapters
        sample_rate = mp4.sample_rate
        bit_rate = mp4.bit_rate
        metadata = {} # Assuming mp4 object provides metadata if needed

        mp4.close()

        return chapters, sample_rate, bit_rate, metadata
    except ModuleNotFoundError:
         # Log clearly that the preferred method failed
         logging.getLogger().error("Could not find the 'libmp4v2' Python module.")
         logging.getLogger().error("Please install it or use the --no-mp4v2 flag to rely on ffmpeg.")
         # Re-raise the error or exit, as the script cannot proceed via this path
         raise # Or sys.exit(1)

def load_metadata(args, log, filename):
    if args.no_mp4v2:
        log.info('Loading metadata using ffmpeg...')
        return ffmpeg_metadata(args, log, filename)
    else:
        log.info('Loading metadata using libmp4v2...')
        try:
            return mp4v2_metadata(filename)
        except ModuleNotFoundError:
             # If mp4v2 fails due to missing module, exit cleanly.
             # The error message is printed inside mp4v2_metadata.
             sys.exit(1)
        except Exception as e:
             # Catch other potential errors from mp4v2_metadata if it existed
             log.error(f"An unexpected error occurred while using mp4v2: {e}")
             sys.exit(1)


def show_metadata_info(args, log, chapters, sample_rate, bit_rate, metadata):
    """Show a summary of the parsed metadata."""

    # Use f-strings for cleaner formatting
    log.info(f"""
        Metadata:
          Chapters: {len(chapters)}
          Bit rate: {bit_rate} kbit/s
          Sampling freq: {sample_rate} Hz""")

    if args.debug and chapters:
        # Limit logged chapters if there are too many
        chapters_to_log = chapters[:20] # Log first 20 chapters
        log.debug(dedent('''
            Chapter data (first %d):
              %s''' % (len(chapters_to_log), '\n'.join([f'  {c}' for c in chapters_to_log]))))
        if len(chapters) > 20:
             log.debug("  ... (more chapters exist)")


    if args.no_mp4v2 and not chapters:
        log.warning("No chapters were found using ffmpeg.")
        log.warning("This M4B file might not contain chapters, or ffmpeg cannot read them.")
        log.warning("If you believe chapters exist, the --no-mp4v2 method might be insufficient.")
        # Removed the prompt to continue as it used raw_input
        # Decide whether to proceed or exit if no chapters are found
        log.info("Proceeding without chapter splitting as no chapters were detected.")
        # Or sys.exit(1) if you want to stop here

def encode(args, log, output_dir, temp_dir, filename, basename, sample_rate, bit_rate, metadata):
    """Encode audio."""

    # Ensure directories exist using os.makedirs(exist_ok=True)
    os.makedirs(output_dir, exist_ok=True)
    os.makedirs(temp_dir, exist_ok=True)

    if args.skip_encoding:
        log.info("Skipping encoding as requested. Source file will be used for splitting.")
        # We need to return the *source* file path if skipping encoding
        encoded_file = filename
        # args.ext should reflect the source format if skipping
        args.ext = os.path.splitext(filename)[1].lstrip('.')
        return encoded_file
    else:
        fname = '%s.%s' % (basename, args.ext)
        encoded_file = os.path.join(temp_dir, fname)

    cmd_values = dict(ffmpeg=args.ffmpeg, encoder=args.encoder, infile=filename,
        sample_rate=sample_rate, bit_rate=bit_rate, outfile=encoded_file)

    if os.path.isfile(encoded_file):
        log.info(f"Found a previously encoded file '{encoded_file}'.")
        # Use input() for Python 3, provide clear options
        response = input("Do you want to re-encode it? (y/N/q): ").lower().strip()
        if response == 'q':
            log.info("Exiting as requested.")
            sys.exit(0)
        elif response != 'y':
            log.info("Using existing encoded file.")
            return encoded_file
        else:
             log.info("Proceeding with re-encoding.")


    # Build encoding options
    if '%(outfile)s' not in args.encode_opts:
        log.error('%(outfile)s placeholder is missing in the --encode-opts string.')
        sys.exit(1)
    # Ensure infile is also present if not piping
    if not args.pipe_wav and '%(infile)s' not in args.encode_opts:
         log.error('%(infile)s placeholder is missing in the --encode-opts string when not piping.')
         sys.exit(1)


    # Construct command based on whether piping is used
    if args.pipe_wav:
        # Note: Shell=True is required for pipes
        # Ensure the input file is correctly passed to ffmpeg for piping
        pipe_input_cmd = f"{args.ffmpeg} -i \"{filename}\" -f wav pipe:1"
        # Substitute values only in the encoder part
        encoder_cmd_part = args.encode_opts % cmd_values
        # Remove infile placeholder from encoder part if it exists, as input comes from pipe
        encoder_cmd_part = encoder_cmd_part.replace(f"-i {filename}", "").replace(f"-i \"{filename}\"", "") # Basic replace
        encode_cmd = f"{pipe_input_cmd} | {args.encoder} {encoder_cmd_part}"
        # shell=True is necessary for piping
        use_shell = True
    else:
        # No piping, substitute all values directly, shell=False preferred
        encode_cmd = f"{args.encoder} {args.encode_opts}" % cmd_values
        use_shell = False

    log.info('Encoding audio...')
    log.debug('Encoding with command: %s' % encode_cmd) # Log the command string

    # Pass shell=use_shell determined above
    run_command(log, encode_cmd, cmd_values, 'encoding audio', shell=use_shell)


    return encoded_file

def split(args, log, output_dir, encoded_file, chapters):
    """Split encoded audio file into chapters.

    Note: Handles potential Unicode issues in filenames across platforms.
    """
    # Regex to find format specifiers like %(title)s
    re_format = re.compile(r'%\(([A-Za-z0-9_]+)\)s')
    # Regex to remove characters invalid in filenames (adjust as needed)
    re_sub = re.compile(r'[\\/*?"<>|:]+') # Added :

    if not chapters:
        log.warning("No chapters found or provided, skipping splitting process.")
        return # Exit split function if no chapters

    # Ensure output directory exists
    os.makedirs(output_dir, exist_ok=True)

    log.info(f"Splitting into {len(chapters)} chapters...")
    for chapter in chapters:
# Prepare values for filename formatting, ensure they are strings or correct types
        # The 'num' value needs to be an integer for %d formatting
        values = dict(
            num=int(chapter.num or 0), # Ensure num is integer for %d
            title=str(chapter.title or "Chapter"), # Default title if None
            # Add other potential keys if needed by custom_name format string, ensure correct types
            start=str(chapter.start or 0.0),
            end=str(chapter.end or 0.0),
            duration=str(chapter.duration() or 0.0)
        )

        # Format the chapter name using the standard % operator
        try:
            # Use the custom_name template directly with the values dictionary
            chapter_name_raw = args.custom_name % values
        except KeyError as e:
            log.warning(f"Missing key {e} in custom name format '{args.custom_name}'. Check your template and available placeholders (num, title, start, end, duration). Using default name.")
            chapter_name_raw = f"{values['num']:02d}-{values['title']}" # Fallback uses f-string
        except Exception as e:
             log.warning(f"Could not format custom name '{args.custom_name}' for chapter {values['num']}. Using default. Error: {e}")
             chapter_name_raw = f"{values['num']:02d}-{values['title']}" # Fallback uses f-string

        # Sanitize filename: remove invalid characters (keep this part)
        sanitized_chapter_name = re_sub.sub('', chapter_name_raw)
        # Replace potential multiple spaces/hyphens with single ones (keep this part)
        sanitized_chapter_name = re.sub(r'[\s-]+', '_', sanitized_chapter_name).strip('_')

        # Construct final filename (use sanitized name)
        output_filename_base = f"{sanitized_chapter_name}.{args.ext}"
        output_filepath = os.path.join(output_dir, output_filename_base)

        # --- The code continues from here for preparing the split command ---
        # Prepare values for the ffmpeg split command... (Keep this next part as it was)
        # Sanitize filename: remove invalid characters
        sanitized_chapter_name = re_sub.sub('', chapter_name_raw)
        # Replace potential multiple spaces/hyphens with single ones
        sanitized_chapter_name = re.sub(r'[\s-]+', '_', sanitized_chapter_name).strip('_')
        # Limit filename length if necessary (optional)
        # max_len = 100
        # sanitized_chapter_name = sanitized_chapter_name[:max_len]

        # Construct final filename (always use sanitized name)
        output_filename_base = f"{sanitized_chapter_name}.{args.ext}"
        output_filepath = os.path.join(output_dir, output_filename_base)


        # Prepare values for the ffmpeg split command
        split_values = dict(
            ffmpeg=args.ffmpeg,
            # Ensure duration is positive for ffmpeg
            duration=str(max(0.01, chapter.duration() or 0.01)), # Use small positive duration if zero/None
            start=str(chapter.start or 0.0),
            # Use the source encoded file as input
            encoded_file=encoded_file,
            # Use the final sanitized output path
            outfile=output_filepath
            )

        # Define the ffmpeg split command template
        # Use -map_metadata -1 to avoid copying global metadata to chapters
        # Use -vn to ensure no video stream is processed/copied
        split_cmd = ('%(ffmpeg)s -y -i %(encoded_file)s -ss %(start)s -t %(duration)s '
                    '-vn -acodec copy -map_metadata -1 %(outfile)s')


        log.info("Splitting chapter %s '%s'..." % (values['num'], sanitized_chapter_name))
        log.debug('Splitting with command: %s' % (split_cmd % split_values))


        # Run the split command - Use shell=False as no pipes/wildcards here
        run_command(log, split_cmd, split_values, f'splitting chapter {values["num"]}',
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)


        # No need for renaming logic if outfile path is constructed correctly

def main():
    args = parse_args()

    # Process each input file
    for filename in args.filename:
        if not os.path.isfile(filename):
             print(f"Error: Input file not found: {filename}")
             continue # Skip to the next file

        # Derive basename and setup output paths relative to args.output_dir
        basename = os.path.splitext(os.path.basename(filename))[0]
        # Place chapter output directly in args.output_dir or a subdir?
        # Current logic puts chapters in output_dir/basename/
        chapter_output_dir = os.path.join(args.output_dir, basename)

        # Temp dir for encoding intermediate file (if not skipping)
        temp_dir = os.path.join(chapter_output_dir, 'temp') if not args.skip_encoding else chapter_output_dir

        # Setup logging for this specific file processing run
        log = setup_logging(args, basename)

        log.info("="*40)
        log.info(f"Initiating script for file: '{filename}'")
        log.info(f"Output directory: '{chapter_output_dir}'")
        log.info("="*40)

        try:
            # Load metadata (will exit if mp4v2 selected and fails)
            chapters, sample_rate, bit_rate, metadata = load_metadata(args, log, filename)
            show_metadata_info(args, log, chapters, sample_rate, bit_rate, metadata)

            # Encode the source file (or skip)
            encoded_file = encode(args, log, chapter_output_dir, temp_dir, filename,
                basename, sample_rate, bit_rate, metadata)

            # Split the encoded file into chapters (if chapters exist)
            split(args, log, chapter_output_dir, encoded_file, chapters)

            # Cleanup temp directory if encoding was done and temp dir was used
            if not args.skip_encoding and temp_dir != chapter_output_dir and os.path.isdir(temp_dir):
                log.info(f"Cleaning up temporary directory: {temp_dir}")
                try:
                    shutil.rmtree(temp_dir)
                except Exception as e:
                    log.warning(f"Could not remove temporary directory {temp_dir}: {e}")

            log.info(f"Finished processing: '{filename}'")

        except Exception as e:
            # Catch unexpected errors during processing of a single file
            log.error(f"An unexpected error occurred while processing {filename}: {e}", exc_info=args.debug)
            # Continue to the next file if specified

    print("\nAll processing finished.")


if __name__ == '__main__':
    main()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment