Source code for brownify.cli

import argparse
import logging
import os
import shutil
import uuid
from typing import Optional

from brownify.downloaders import YoutubeDownloader
from brownify.errors import BrownifyError, InvalidInputError
from brownify.parsers import ActionParser
from brownify.runners import PipelineProcessor
from brownify.splitters import AudioSplitterFactory, AudioSplitterType

_LOG_LEVELS = {"debug", "info", "warning", "error", "critical"}
_DEFAULT_LOG_LEVEL = "warning"


[docs]def get_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Make your music brown") input_group = parser.add_mutually_exclusive_group(required=True) input_group.add_argument( "--youtube-input", help="URL for a youtube video to use as input" ) input_group.add_argument( "--local-input", help="Path to a local file to use as input" ) parser.add_argument( "output", help="Filename to send output to (will be an mp3)" ) parser.add_argument( "--log", default=_DEFAULT_LOG_LEVEL, choices=_LOG_LEVELS, help="Log level to use for output", ) parser.add_argument( "--preserve", action="store_true", help="Allow for intermediate files to be preserved for debugging", ) group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--recipe", help="Sequence to apply to audio streams") group.add_argument("--recipe-file", help="Path to an existing recipe file") return parser.parse_args()
def _get_log_level(log_level: str) -> int: numeric_log_level = getattr(logging, log_level, None) if not isinstance(numeric_log_level, int): raise InvalidInputError(f"Invalid log level: {log_level}") return numeric_log_level def _get_program(recipe, recipe_file) -> str: if recipe: return recipe if recipe_file: with open(recipe_file, "r") as f: # Read in program and ignore newlines program = f.read().replace("\n", "") return program raise InvalidInputError( "Either a recipe or a recipe file must be provided" ) def _cleanup(downloaded_file, session_id): try: # Clean the intermediate downloaded file if it exists os.remove(downloaded_file) except OSError as ose: logging.warning(f"Could not remove downloaded file {downloaded_file}") logging.debug(ose, exc_info=True) try: # Spleeter stores its files in a directory named after the original # file's base name shutil.rmtree(str(session_id)) except OSError as ose: logging.warning( "Could not remove intermediate processed files under directory " f"{session_id}/" ) logging.debug(ose, exc_info=True)
[docs]def main() -> int: args = get_args() youtube_url: Optional[str] = args.youtube_input local_file: Optional[str] = args.local_input output_file = args.output log_level = args.log.upper() preserve = args.preserve recipe = args.recipe recipe_file = args.recipe_file # Put logging setup in its own block so if it fails, we can fail loudly try: log_level = _get_log_level(log_level) logging.basicConfig(level=log_level) except InvalidInputError as iie: logging.error(iie) return 1 # Create a random ID for tracking this session session_id = str(uuid.uuid4()) # Declare input filename to be set by either the local file # or by downloading an audio file from youtube input_file: Optional[str] = None try: # Get the program from user input program = _get_program(recipe, recipe_file) # Based on the program, instantiate a processing pipeline ap = ActionParser() pipelines = ap.get_pipelines(program) # Grab an audio file based on the provided inputs if youtube_url is not None: # Create download filename based on session ID download_file = f"{session_id}.mp4" YoutubeDownloader.get_audio(youtube_url, download_file) input_file = download_file elif local_file is not None: _, local_file_ext = os.path.splitext(local_file) input_file = f"{session_id}{local_file_ext}" # Create a symlink to the local file for this session os.symlink(local_file, input_file) else: raise InvalidInputError( "For audio input, a path to a local file or a youtube URL " "must be provided" ) # Split the input audio into different tracks splitter = AudioSplitterFactory.get_audio_splitter( AudioSplitterType.FIVE_STEM ) splitter.split(input_file) # Perform processing pipeline over the audio processor = PipelineProcessor(session_id, splitter) processor.process(pipelines, output_file) return 0 except BrownifyError as be: # All Brownify errors should log their message at the error level and # log a stack trace at the debug level logging.error(be) logging.debug(be, exc_info=True) return 1 finally: # Clean up temporary files unless they have been marked for # preservation if not preserve: _cleanup(input_file, session_id)