class VideoFilter(Enum):
    """
    Attributes
    ----------
    has_text
        video segment has detectable text (letters, words, numbers, etc...).
        Supports foreign langauges

    has_cut
        video segment has a detectable cut between shots

    has_low_contrast
        video segment has low contrast (solid color, dark scene, etc...)

    is_repeat
        video segment is a repeat of a video segment already used
    """

    # Content Filters
    has_text = Filter(has_text)
    has_cut = Filter(has_cut)
    has_low_contrast = Filter(has_low_contrast)

    not_has_text = Filter(not_has_text)
    not_has_cut = Filter(not_has_cut)
    not_has_low_contrast = Filter(not_has_low_contrast)

    # Context Filters
    is_repeat = ContextFilter(is_repeat)

    not_is_repeat = ContextFilter(not_is_repeat) class MusicVideoGenerator:
    """
    Attributes
    ----------
    audio
        Audio to use for the music video

    video_sources
        source videos to use for generating the music video

    video_filters
        Video filters that each segment in the music video must pass.

    meta
        Json serializable dictionary with extra metadata
    """

    audio: Audio
    _duration: float
    video_sources: List[Union[VideoSource, List[VideoSourceList]]]
    video_filters: List[Filter]

    meta: dict

    class Meta(str, Enum):
        REJECTED_SEGMENT_STATS = "rejected_segment_stats"

    @convert_time_to_seconds(["duration"])
    def __init__(
        self,
        audio_file: Optional[str] = None,
        video_sources: Optional[
            Union[VideoSourceList, List[Union[VideoSource, str, List[Any]]]]
        ] = None,
        *,
        duration: TIME_FORMAT = None,
        video_filters: Optional[List[str]] = None,
        exclude_video_filters: Optional[List[str]] = None,
        include_video_filters: Optional[List[str]] = None,
        custom_video_filters: Optional[List[Filter]] = None,
    ):
        """
        Parameters
        ----------
        audio_file
            audio file to use for the music video

        video_sources
            Source videos to use for the music video.
            Accepts arbitrarily nested video files, directories, VideoSources, and VideoSourceLists.

        video_filters ~
            Video filters that each segment in the music video must pass.
            See :class:`~mugen.video.video_filters.VideoFilter` for a list of supported values.
            Defaults to :data:`~mugen.video.video_filters.VIDEO_FILTERS_DEFAULT`

        exclude_video_filters
            Video filters to exclude from default video_filters.
            Takes precedence over video_filters

        include_video_filters
            Video filters to use in addition to default video_filters.
            Takes precedence over exclude_video_filters

        custom_video_filters ~
            Custom video filters to use in addition to video_filters.
            Allows functions wrapped by :class:`~mugen.mixins.Filterable.Filter` or
            :class:`~mugen.mixins.Filterable.ContextFilter`
        """
        if not audio_file and not duration:
            raise ParameterError(
                "Must provide either the audio file or duration for the music video."
            )

        self.audio = Audio(audio_file) if audio_file else None
        self._duration = duration

        if video_sources:
            self.video_sources = VideoSourceList(video_sources)

        # Assemble list of video filter names
        video_filter_names = (
            video_filters if video_filters is not None else DEFAULT_VIDEO_FILTERS
        )
        if exclude_video_filters:
            for video_filter in exclude_video_filters:
                video_filter_names.remove(video_filter)
        if include_video_filters:
            video_filter_names.extend(include_video_filters)
        custom_video_filters = custom_video_filters or []

        # Compile video filters
        self.video_filters = []
        for filter_name in video_filter_names:
            try:
                self.video_filters.append(VideoFilter[filter_name].value)
            except KeyError as error:
                raise MugenError(f"Unknown video filter '{filter_name}'") from error
        self.video_filters.extend(custom_video_filters)

        self.meta = {self.Meta.REJECTED_SEGMENT_STATS: []}

    @property
    def duration(self):
        return self.audio.duration if self.audio else self._duration

    def generate_from_events(
        self, events: Union[EventList, List[TIME_FORMAT]], show_progress: bool = True
    ) -> MusicVideo:
        """
        Generates a MusicVideo from a list of events

        Parameters
        ----------
        events
            Events corresponding to cuts which occur in the music video.
            Either a list of events or event locations.

        show_progress
            Whether to output progress information to stdout
        """
        if not isinstance(events, EventList):
            events = EventList(events, end=self.duration)

        # Get segment durations from cut locations
        segment_durations = events.segment_durations

        music_video_segments = self._generate_music_video_segments(
            segment_durations, show_progress=show_progress
        )

        # Assemble music video from music video segments and audio
        music_video = MusicVideo(
            music_video_segments, self.audio.file if self.audio else None
        )

        return music_video

    def _generate_music_video_segments(
        self, durations: List[float], *, show_progress: bool = True
    ) -> List[VideoSegment]:
        """
        Generates a list of sampled video segments which pass all trait filters

        Parameters
        ----------
        durations
            durations for each sampled video segment

        show_progress
            Whether to output progress information to stdout

        Returns
        -------
        Sampled video segments
        """
        video_segments = []
        video_segment_sampler = SourceSampler(self.video_sources)

        # Make deep copies of filters
        video_filters = copy.deepcopy(self.video_filters)

        # Set memory for all ContextFilters
        for video_filter in video_filters:
            if isinstance(video_filter, ContextFilter) and video_filter.memory is None:
                video_filter.memory = video_segments

        for duration in tqdm(durations, disable=not show_progress):
            video_segment = None

            while not video_segment:
                video_segment = video_segment_sampler.sample(duration)

                (
                    video_segment.passed_filters,
                    video_segment.failed_filters,
                ) = video_segment.apply_filters(video_filters)
                if not video_segment.failed_filters:
                    video_segments.append(video_segment)
                else:
                    self.meta[self.Meta.REJECTED_SEGMENT_STATS].append(
                        video_segment.__dict__
                    )
                    video_segment = None

        return video_segments

    @use_temporary_file_fallback("output_path", ".mkv")
    def preview_events(
        self,
        events: Union[EventList, List[TIME_FORMAT]],
        output_path: Optional[str] = None,
        mode: str = PreviewMode.AUDIOVISUAL,
        show_progress: bool = True,
        **kwargs,
    ):
        """
        Creates a new audio file with audible bleeps at event locations

        Parameters
        ----------
        events
            Events to mark in the audio file.

        output_path
            Path to save the output .wav or .mkv file

        mode
            Method of previewing. Visual by default.
            See :class:`~mugen.audio.Audio.PreviewMode` for supported values.

        show_progress
            Whether to output progress information to stdout
        """
        if not isinstance(events, EventList):
            events = EventList(events, end=self.duration)

        if mode == PreviewMode.AUDIO:
            audio_utilities.create_marked_audio_file(
                events.locations,
                output_path,
                audio_file=self.audio.file if self.audio else None,
                duration=self.duration,
            )
        elif mode == PreviewMode.AUDIOVISUAL:
            temp_marked_audio_file = audio_utilities.create_marked_audio_file(
                events.locations,
                audio_file=self.audio.file if self.audio else None,
                duration=self.duration,
            )

            composite_segments = []
            for index, duration in enumerate(events.segment_durations):
                # Alternate black & white
                color = "black" if index % 2 == 0 else "white"
                composite_segments.append(
                    ColorSegment(color, duration, size=(600, 300))
                )

            preview = MusicVideo(composite_segments, temp_marked_audio_file)
            preview.writer.preset = "ultrafast"

            temp_output_path = preview.write_to_video_file(
                audio=True,
                add_auxiliary_tracks=False,
                show_progress=show_progress,
                **kwargs,
            )
            self._add_preview_auxiliary_tracks(temp_output_path, events, output_path)

        return output_path

    @staticmethod
    def _add_preview_auxiliary_tracks(
        video_file: str, events: EventList, output_path: str
    ):
        """
        Adds metadata subtitle tracks to the preview

        Parameters
        ----------
        video_file
            The video file to add tracks to

        events
            Events to create auxiliary tracks from

        output_path
            The final music video output file with added auxiliary tracks
        """
        locations = events.locations
        events_strings = [
            f"{event.index_repr(index)}".replace("<", "").replace(">", "")
            for index, event in enumerate(events)
        ]

        subtitle_track_events = SubtitleTrack.create(
            events_strings, "events", locations=locations, default=True
        )

        subtitle_tracks = [subtitle_track_events]
        tracks.add_tracks_to_video(
            video_file, output_path, subtitle_tracks=subtitle_tracks
        ) class EventGroupList(MugenList):
    """
    An alternate, more useful representation for a list of EventLists
    """

    _selected_groups: List[EventList]

    def __init__(
        self,
        groups: Optional[Union[List[EventList], List[List[TIME_FORMAT]]]] = None,
        *,
        selected: List[EventList] = None,
    ):
        """
        Parameters
        ----------
        groups

        selected
            A subset of groups to track
        """
        if groups is not None:
            for index, group in enumerate(groups):
                if not isinstance(group, EventList):
                    # Convert event locations to EventList
                    groups[index] = EventList(group)

        super().__init__(groups)

        self._selected_groups = selected if selected is not None else []

    def __repr__(self):
        group_reprs = []
        index_count = 0
        for group in self:
            group_indexes = range(index_count, index_count + len(group) - 1)
            group_reprs.append(
                group.list_repr(
                    group_indexes, True if group in self.selected_groups else False
                )
            )
            index_count += len(group)
        return super().pretty_repr(group_reprs)

    @property
    def end(self):
        return self[-1].end if self else None

    @property
    def selected_groups(self) -> "EventGroupList":
        """
        Returns
        -------
        Selected groups
        """
        return EventGroupList([group for group in self._selected_groups])

    @property
    def unselected_groups(self) -> "EventGroupList":
        """
        Returns
        -------
        Unselected groups
        """
        return EventGroupList(
            [group for group in self if group not in self.selected_groups]
        )

    def speed_multiply(
        self, speeds: List[float], offsets: Optional[List[float]] = None
    ):
        """
        Speed multiplies event groups, in order

        See :meth:`~mugen.events.EventList.speed_multiply` for further information.
        """
        if offsets is None:
            offsets = []

        for group, speed, offset in zip_longest(self, speeds, offsets):
            group.speed_multiply(speed if speed is not None else 1, offset)

    def flatten(self) -> EventList:
        """
        Flattens the EventGroupList back into an EventList.

        Returns
        -------
        A flattened EventList for this EventGroupList
        """
        return EventList(lists.flatten(self), end=self.end) class EventList(MugenList):
    """
    A list of Events which occur in some time sequence
    """

    end: Optional[float]

    def __init__(
        self,
        events: Optional[List[Union[Event, TIME_FORMAT]]] = None,
        *,
        end: TIME_FORMAT = None,
    ):
        """
        Parameters
        ----------
        events
            events which occur in the time sequence

        end
            duration of the time sequence
        """
        if events is not None:
            for index, event in enumerate(events):
                if not isinstance(event, Event):
                    # Convert event location to Event
                    events[index] = Event(event)

        self.end = end

        super().__init__(events)

    def __eq__(self, other):
        return super().__eq__(other) and self.end == other.end

    def __add__(self, rhs):
        return type(self)((super().__add__(rhs)), end=rhs.end)

    def __getitem__(self, item):
        result = list.__getitem__(self, item)
        if isinstance(item, slice):
            return type(self)(result, end=self.end)
        else:
            return result

    def __repr__(self):
        event_reprs = [event.index_repr(index) for index, event in enumerate(self)]
        pretty_repr = super().pretty_repr(event_reprs)
        return f"<{pretty_repr}, end: {self.end}>"

    def list_repr(self, indexes: range, selected: bool):
        """
        Repr for use in lists
        """
        return (
            f"<{self.__class__.__name__} {indexes.start}-{indexes.stop} ({len(self)}), "
            f"type: {self.type}, selected: {selected}>"
        )

    @property
    def type(self) -> Union[str, None]:
        if len(self) == 0:
            return None
        elif len(set([event.__class__.__name__ for event in self])) == 1:
            return self[0].__class__.__name__
        else:
            return "mixed"

    @property
    def locations(self) -> List[float]:
        return [event.location for event in self]

    @property
    def intervals(self) -> List[float]:
        return location.intervals_from_locations(self.locations)

    @property
    def segment_locations(self):
        """
        Returns
        -------
        locations of segments between events
        """
        return [0] + [event.location for event in self]

    @property
    @requires_end
    def segment_durations(self):
        """
        Returns
        -------
        durations of segments between events
        """
        return location.intervals_from_locations(self.locations + [self.end])

    @property
    def durations(self) -> List[float]:
        return [event.duration for event in self]

    @property
    def types(self) -> List[str]:
        return [event.__class__.__name__ for event in self]

    def add_events(self, events: List[Union[Event, TIME_FORMAT]]):
        for event in events:
            if not isinstance(event, Event):
                event = Event(event)
            bisect.insort(self, event)

    def offset(self, offset: float):
        """
        Offsets all events by the given amount
        """
        for event in self:
            event.location += offset

    @convert_float_to_fraction("speed")
    def speed_multiply(
        self, speed: Union[float, Fraction], offset: Optional[int] = None
    ):
        """
        Speeds up or slows down events by grouping them together or splitting them up.
        For slowdowns, event type group boundaries and isolated events are preserved.

        Parameters
        ----------
        speed
            Factor to speedup or slowdown by.
            Must be of the form x (speedup) or 1/x (slowdown), where x is a natural number.
            Otherwise, 0 to remove all events.

        offset
            Offsets the grouping of events for slowdowns.
            Takes a max offset of x - 1 for a slowdown of 1/x, where x is a natural number
        """
        if speed == 0:
            self.clear()
        elif speed > 1:
            self._split(speed.numerator)
        elif speed < 1:
            self._merge(speed.denominator, offset)

    def _split(self, pieces_per_split: int):
        """
        Splits events up to form shorter intervals

        Parameters
        ----------
        pieces_per_split
            Number of pieces to split each event into
        """
        splintered_events = EventList()

        # Group events by type
        type_groups = self.group_by_type()

        for group in type_groups:
            if len(group) == 1:
                # Always keep isolated events
                splintered_events.append(group[0])
            else:
                for index, event in enumerate(group):
                    splintered_events.append(event)

                    if index == len(group) - 1:
                        # Skip last event
                        continue

                    next_event = group[index + 1]
                    interval = next_event.location - event.location
                    interval_piece = interval / pieces_per_split

                    location = event.location
                    for _ in range(pieces_per_split - 1):
                        location += interval_piece
                        event_splinter = copy.deepcopy(event)
                        event_splinter.location = location
                        splintered_events.append(event_splinter)

        self[:] = splintered_events

    def _merge(self, pieces_per_merge: int, offset: Optional[int] = None):
        """
        Merges adjacent events of identical type to form longer intervals

        Parameters
        ----------
        pieces_per_merge
            Number of adjacent events to merge at a time

        offset
            Offset for the merging of events
        """
        if offset is None:
            offset = 0

        combined_events = EventList()

        # Group events by type
        type_groups = self.group_by_type()
        for group in type_groups:
            if len(group) == 1:
                # Always keep isolated events
                combined_events.append(group[0])
            else:
                for index, event in enumerate(group):
                    if (index - offset) % pieces_per_merge == 0:
                        combined_events.append(event)

        self[:] = combined_events

    def group_by_type(self, select_types: List[str] = None) -> "EventGroupList":
        """
        Groups events by type

        Attributes
        ----------
        select_types
            A list of types for which to select groups in the resulting EventGroupList.
            If no types are specified, all resulting groups will be selected.

        Returns
        -------
        An EventGroupList partitioned by type
        """
        if select_types is None:
            select_types = []

        groups = [
            EventList(list(group), end=self.end)
            for index, group in groupby(self, key=attrgetter("__class__"))
        ]
        if not select_types:
            selected_groups = groups
        else:
            selected_groups = [group for group in groups if group.type in select_types]

        return EventGroupList(groups, selected=selected_groups)

    def group_by_slices(self, slices: (int, int)) -> "EventGroupList":
        """
        Groups events by slices.
        Does not support negative indexing.

        Slices explicitly passed in will become selected groups in the resulting EventGroupList.

        Returns
        -------
        An EventGroupList partitioned by slice
        """
        slices = [slice(sl[0], sl[1]) for sl in slices]

        # Fill in rest of slices
        all_slices = general.fill_slices(slices, len(self))
        target_indexes = [index for index, sl in enumerate(all_slices) if sl in slices]

        # Group events by slices
        groups = [self[sl] for sl in all_slices]
        selected_groups = [
            group for index, group in enumerate(groups) if index in target_indexes
        ]
        groups = EventGroupList(groups, selected=selected_groups)

        return groups class ParameterError(MugenError):
    """
    Exception class for mal-formed inputs
    """

    pass class Persistable:
    """
    Mixin for persisting objects
    """

    @use_temporary_file_fallback("output_path", PICKLE_EXTENSION)
    def save(self, output_path: Optional[str] = None):
        with open(output_path, "wb") as output_file:
            dill.dump(self, output_file)

        return output_path

    @classmethod
    def load(cls, input_path: str):
        with open(input_path, "rb") as input_file:
            persistable = dill.load(input_file)

        return persistable TEMP_PATH_BASE = tempfile.TemporaryDirectory().name
def touch(filename):
def ensure_directory_exists(*directories):
def recreate_directory(*directories):
def list_directory(path, include_hidden=False):
def list_directory_files(directory: str, include_hidden=False) -> List[str]:
def run_command(command) -> CompletedProcess:
def generate_temp_file_path(extension: str) -> str:
def use_temporary_file_fallback(path_var: str, extension: str):
    def _use_temporary_file_path(path_variable): class VideoWriter:
    """
    Class for writing VideoClips and VideoSegments to file

    Parameters
    ----------
    preset
        Sets the time that FFMPEG will spend optimizing compression while writing the video to file.
        Note that this does not impact the quality of the video, only the size of the video file.
        So choose ultrafast when you are in a hurry and file size does not matter.
        Choices are: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo.

    codec
        Video codec to use when writing the music video to file.

    crf
        Constant rate factor (quality) for the music video (0 - 51).

    audio_codec
        Audio codec to use if no audio_file is provided.

    audio_bitrate
        Audio bitrate (kbps) to use if no audio_file is provided.

    ffmpeg_params
        Any additional ffmpeg parameters you would like to pass as a list of terms,
        like ['-option1', 'value1', '-option2', 'value2']
    """

    preset: str
    codec: str
    crf: int
    audio_codec: str
    audio_bitrate: int
    ffmpeg_params: list

    AUDIO_BITRATE = 320
    AUDIO_CODEC = "libmp3lame"
    VIDEO_PRESET = "medium"
    VIDEO_CODEC = "libx264"
    VIDEO_CRF = 18
    VIDEO_EXTENSION = ".mkv"

    def __init__(
        self,
        preset: str = VIDEO_PRESET,
        codec: str = VIDEO_CODEC,
        crf: int = VIDEO_CRF,
        audio_codec: str = AUDIO_CODEC,
        audio_bitrate: int = AUDIO_BITRATE,
        ffmpeg_params: Optional[List[str]] = None,
    ):
        self.preset = preset
        self.codec = codec
        self.crf = crf
        self.audio_codec = audio_codec
        self.audio_bitrate = audio_bitrate
        self.ffmpeg_params = ffmpeg_params or []

    def write_video_clips_to_directory(
        self,
        video_clips: List[VideoClip],
        directory: str,
        *,
        file_extension: str = VIDEO_EXTENSION,
        audio: Union[str, bool] = True,
        **kwargs
    ):
        """
        Writes a list of video segments to files in the specified directory
        """
        for index, segment in enumerate(tqdm(video_clips)):
            output_path = os.path.join(directory, str(index) + file_extension)
            self.write_video_clip_to_file(
                segment,
                output_path,
                audio=audio,
                verbose=False,
                show_progress=False,
                **kwargs
            )

    @use_temporary_file_fallback("output_path", VIDEO_EXTENSION)
    def write_video_clip_to_file(
        self,
        video_clip: VideoClip,
        output_path: Optional[str] = None,
        *,
        audio: Union[str, bool] = True,
        verbose: bool = False,
        show_progress: bool = True,
        **kwargs
    ):
        """
        Writes a video clip to file in the specified directory

        Parameters
        ----------
        video_clip

        output_path

        audio
            Audio for the video clip. Can be True to enable, False to disable, or an external audio file.

        verbose
            Whether output to stdout should include extra information during writing

        show_progress
            Whether to output progress information to stdout

        kwargs
            List of other keyword arguments to pass to moviepy's write_videofile
        """
        # Prepend crf to ffmpeg_params
        ffmpeg_params = ["-crf", str(self.crf)] + self.ffmpeg_params
        audio_bitrate = str(self.audio_bitrate) + "k"

        video_clip.write_videofile(
            output_path,
            audio=audio,
            preset=self.preset,
            codec=self.codec,
            audio_codec=self.audio_codec,
            audio_bitrate=audio_bitrate,
            ffmpeg_params=ffmpeg_params,
            **kwargs,
            verbose=verbose,
            logger=logger if show_progress else None
        )

        return output_path class PreviewMode(str, Enum):
    """
    audio: Produce an audio preview
    visual: Produce an audio-visual preview
    """

    AUDIO = "audio"
    AUDIOVISUAL = "audiovisual" class VideoSourceList(SourceList):
    """
    A list of VideoSources
    """

    name: Optional[str]

    def __init__(
        self,
        sources=Optional[Union[List[Union[Source, "VideoSourceList"]], str]],
        **kwargs,
    ):
        """
        Parameters
        ----------
        sources
            A list of sources.
            Accepts arbitrarily nested video files, file globs, directories, VideoSources, and VideoSourceLists.
        """
        self.name = None

        if isinstance(sources, str):
            self.name = Path(sources).stem

            # Build list of sources from directory or file glob
            if os.path.isdir(sources):
                sources = self._sources_from_files(system.list_directory_files(sources))
            else:
                sources = self._sources_from_files(globber.glob(sources))
        else:
            # Convert any source files to VideoSources, and any lists, file globs or directories to VideoSourceLists
            sources = self._fill_in_sources(sources)

        super().__init__(sources, **kwargs)

    def list_repr(self):
        """
        Repr for use in lists
        """
        if self.name:
            return f"<{self.__class__.__name__} ({len(self)}): {self.name}, weight: {self.weight}>"

        return super().list_repr()

    @staticmethod
    def _sources_from_files(files: List[str]):
        sources = []

        for file in files:
            try:
                source = VideoSource(file)
            except IOError:
                continue

            sources.append(source)

        return sources

    @staticmethod
    def _fill_in_sources(sources: list):
        for index, source in enumerate(sources):
            if isinstance(source, str):
                try:
                    sources[index] = VideoSource(source)
                except IOError:
                    sources[index] = VideoSourceList(source)
            if isinstance(source, list) and not isinstance(source, VideoSourceList):
                sources[index] = VideoSourceList(source)

        return sources
import argparse
import logging
import os
import re
import sys
import mugen.video.filters as video_filters
from enum import Enum
from fractions import Fraction
from pathlib import Path
from typing import List
from mugen import MusicVideoGenerator, VideoFilter
from mugen.events import EventGroupList, EventList
from mugen.exceptions import ParameterError
from mugen.mixins import Persistable
from mugen.utilities import system
from mugen.video.io.VideoWriter import VideoWriter
from mugen.video.MusicVideoGenerator import PreviewMode
from mugen.video.sources.VideoSource import VideoSourceList
        help="Save all the individual segments that compose the music video.",
    )

    # Audio Common Parameters
    audio_parser.add_argument(
        "-a",
        "--audio-source",
        dest="audio_source",
        required=True,
        help="The audio file for the music video. Supports any audio format supported by ffmpeg, such as wav, aiff, flac, ogg, mp3, etc...",
    )
    audio_parser.add_argument(
        "-uoa",
        "--use-original-audio",
        dest="use_original_audio",
        action="store_true",
        default=False,
        help="Whether or not to use the original audio from the video segments for the music video",
    )

    audio_parser.add_argument(
        "-aem",
        "--audio-events-mode",
        dest="audio_events_mode",
        default=AudioEventsMode.BEATS,
        help=f"Method of generating events from the audio file. Supported values are {[e.value for e in AudioEventsMode]}",
    )

    audio_parser.add_argument(
        "-bm",
        "--beats-mode",
        dest="beats_mode",
        default=BeatsMode.BEATS,
        help=f"Method of generating beat events from the audio file. Supported values are {[e.value for e in BeatsMode]}",
    )
    audio_parser.add_argument(
        "-om",
        "--onsets-mode",
        dest="onsets_mode",
        default=OnsetsMode.ONSETS,
        help=f"Method of generating onset events from the audio file. Supported values are {[e.value for e in OnsetsMode]}",
    )

    audio_parser.add_argument(
        "-ac",
        "--audio-codec",
        dest="audio_codec",
        default=VideoWriter.AUDIO_CODEC,
        help="The audio codec for the music video if --use-original-audio is enabled. Supports any codec supported by moviepy.",
    )
    audio_parser.add_argument(
        "-ab",
        "--audio-bitrate",
        dest="audio_bitrate",
        type=int,
        default=VideoWriter.AUDIO_BITRATE,
        help="The audio bitrate (kbps) for the music video if --use-original-audio is enabled",
    )

    """ COMMANDS """

    # Create Command Parameters
    create_parser = subparsers.add_parser(
        "create",
        parents=[audio_parser, video_parser, event_parser],
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        help="Create a new music video",
    )
    create_parser.set_defaults(func=create_music_video)
    create_parser.add_argument(
        "-v",
        "--video-sources",
        dest="video_sources",
        nargs="+",
        required=True,
        help="""The video sources for the music video.
         Takes a list of files and folders separated by spaces.
         Supports any video format supported by ffmpeg,
         such as .ogv, .mp4, .mpeg, .avi, .mov, etc...""",
    )
    create_parser.add_argument(
        "-vw",
        "--video-source-weights",
        dest="video_source_weights",
        type=float,
        nargs="+",
        default=[],
        help="""Weights for controlling how often each video source should be used
         in the music video. Takes a list of numbers separated by spaces.
         (i.e.) Pass --weights .6 .4 or --weights 6 4 to use the first video source
         (a series of 26 episodes) 60%% of the time, and the second video source
         (a movie) 40%% of the time""",
    )
    create_parser.add_argument(
        "-fi",
        "--fade-in",
        dest="fade_in",
        type=float,
        help="Fade-in for the music video (seconds)",
    )
    create_parser.add_argument(
        "-fo",
        "--fade-out",
        dest="fade_out",
        type=float,
        help="Fade-out for the music video (seconds)",
    )

    # Preview Command Parameters
    preview_parser = subparsers.add_parser(
        "preview",
        parents=[audio_parser, event_parser],
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        help="Create a quick preview of your music video by marking cut locations with beeps and flashes",
    )
    preview_parser.add_argument(
        "-pm",
        "--preview-mode",
        dest="preview_mode",
        default=PreviewMode.AUDIOVISUAL,

        help=f"The method of previewing events. Supported values are {[e.value for e in PreviewMode]}",