#!/opt/local/bin/python313
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019-2025 A S Lewis
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2.1 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.


"""Module that contains a class storing download options."""


# Import Gtk modules
#   ...


# Import other modules
import os
import re


# Import our modules
import formats
import mainapp
import media
import ttutils
# Use same gettext translations
from mainapp import _


# Classes


class OptionsManager(object):

    """Partially based on the OptionsManager class in youtube-dl-gui.

    This class handles settings for downloading media. Unlike youtube-dl-gui,
    which has one group of download options applied to all downloads, this
    object can be applied to any of the media data classes in media.py (so it
    can be applied to a single video, or a whole channel, or generally to all
    downloads).

    Tartube's options.OptionsManager implements a subset of options implemented
    by the equivalent class in youtube-dl-gui.

    Args:

        uid (int): Unique ID for this options manager

        name (str): A non-unique name for this options manager. Options
            managers that, on creation, are attached to a particular media data
            object have the same name as that object

        dbid (int or None): If this options manager, on create, is attached to
            a particular media data object, the .dbid of that object; otherwise
            None

    Canonical list of options:

    Options are listed here in the same order in which they appear in
    youtube-dl's documentation.

    OPTIONS

        ignore_errors (bool): If True, youtube-dl will ignore the errors and
            continue the download operation

        abort_on_error (bool): If True, youtube-dl will abord downloading
            further playlist videos if an error occurs

        live_from_start (bool): If True, downloads livestreams from the start.
            Currently only supported for YouTube (and marked experimental)

        wait_for_video_min (int): Minimum number of seconds to wait between
            retries for scheduled livestreams to become available (in seconds)

    NETWORK OPTIONS

        proxy (str): Use the specified HTTP/HTTPS proxy. If none is specified,
            then Tartube will cycle through the list of proxies specified in
            mainapp.TartubeApp.dl_proxy_list (if any)

        socket_timeout (str): Time to wait before giving up, in seconds

        source_address (str): Client-side IP address to bind to

        force_ipv4 (str): Make all connections via IPv4

        force_ipv6 (str): Make all connections via IPv6

    GEO-RESTRICTION

        geo_verification_proxy (str): Use this proxy to verify the IP address
            for some geo-restricted sites

        geo_bypass (bool): Bypass geographic restriction via faking
            X-Forwarded-For HTTP header

        no_geo_bypass (bool): Do not bypass geographic restriction via faking
            X-Forwarded-For HTTP header

        geo_bypass_country (str): Force bypass geographic restriction with
            explicitly provided two-letter ISO 3166-2 country code

        geo_bypass_ip_block (str): Force bypass geographic restriction with
            explicitly provided IP block in CIDR notation

    VIDEO SELECTION

        playlist_start (int): Playlist index to start downloading

        playlist_end (int): Playlist index to stop downloading

        playlist_items (str): Comma-separated playlist index of the videos to
            download, in the form '[START]:[STOP][:STEP]' or 'START-STOP'. Use
            negative indices to count from the right and negative STEP to
            download in reverse order, e.g. on a playhlist of 15 videos,
            '1:3,7,-5::2' downloads the videos at index '1,2,3,7,11,13,15'

        max_downloads (int): Maximum number of video files to download from the
            given playlist

        min_filesize (float): Minimum file size of the video file. If the video
            file is smaller than the given size then youtube-dl will abort the
            download operation

        max_filesize (float): Maximum file size of the video file. If the video
            file is larger than the given size then youtube-dl will abort the
            download operation

        date (str): Download only videos uploaded on this date (YYYYMMDD)

        date_before (str): Download only videos uploaded on or before this date
            (YYYYMMDD)

        date_after (str): Download only videos uploaded on or after this date
            (YYYYMMDD)

        min_views (int): Do not download any videos with fewer than this many
            views

        max_views (int): Do not download any videos with more than this many
            views

        match_filter (str): Generic video filter (see the comments in the
            youtube-dl documentation). Tartube automatically adds quotes to
            the beginning and end of the string

        age_limit (str): Download only videos suitable for the given age

        include_ads (bool): Download advertisements (experimental)

    DOWNLOAD OPTIONS

        limit_rate (str): Bandwidth limit, in bytes (example strings: 50K or
            4.2M) (not implemented by youtube-dl-gui)
        NB: Can't be directly modified by user

        retries (int): Number of youtube-dl retries

        abort_on_unavailable_fragment (bool): Abort download if a fragment is
            unavailable (default is to skip unavailable fragments)

        playlist_reverse (bool): When True, download playlist videos in reverse
            order

        playlist_random (bool): When True, download playlist videos in random
            order

        native_hls (bool): When True, youtube-dl will prefer the native HLS
            (HTTP Live Streaming) implementation (rather than prefering FFmpeg,
            which is at the current time the better downloader for general
            compatibility)

        hls_prefer_ffmpeg (bool): When True, youtube-dl will prefer FFmpeg
            (N.B. This should not be confused with the 'prefer_ffmpeg' option)

        external_downloader (str): Use the specified external downloaded.
            youtube-dl currently supports the strings 'aria2c', 'avconv',
            'axel', 'curl', 'ffmpeg', 'httpie', 'wget' (use an empty string to
            disable this option)

        external_arg_string (str): Arguments to pass to the external
            downloader. Tartube automatically adds quotes to the beginning and
            end of the string

    FILESYSTEM OPTIONS

        restrict_filenames (bool): If True, youtube-dl will restrict the
            downloaded file's filename to ASCII characters only

        nomtime (bool): When True will not use the last-modified header to set
            the file modification time (i.e., use the time at which the server
            believes the resources was last modified)

        write_description (bool): If True, youtube-dl will write video
            description to a .description file

        write_info (bool): If True, youtube-dl will write video metadata to an
            .info.json file

        write_annotations (bool): If True, youtube-dl will write video
            annotations to an .annotations.xml file

        cookies_path (str): Path to the cookie jar. If not specified, then
            Tartube will use the cookie jar 'cookies.txt' in the main
            data directory

    THUMBNAIL IMAGES

        write_thumbnail (bool): If True youtube-dl will write thumbnail image
            to disc

    WORKAROUNDS

        force_encoding (str): Force the specified encoding

        no_check_certificate (bool): If True, suppress HTTPS certificate
            validation

        prefer_insecure (bool): If True, use an unencrypted connection to
            retrieve information about the video. (Currently supported only for
            YouTube)

        user_agent (str): Specify a custom user agent for youtube-dl

        referer (str): Specify a custom referer to use if the video access is
            restricted to one domain

        min_sleep_interval (int): Number of seconds to sleep before each
            download when used alone, or a lower bound of a range for
            randomised sleep before each download (minimum possible number of
            seconds to sleep) when used along with 'max_sleep_interval'

        max_sleep_interval (int): Upper bound of a range for randomized sleep
            before each download (maximum possible number of seconds to sleep).
            Can only be used along with a non-zero value for min_sleep_interval

    VIDEO FORMAT OPTIONS

        video_format (str): Video format to download. When this option is set
            to '0' youtube-dl will choose the best video format available for
            the given URL. Otherwise, set in a call to
            OptionsParser.build_video_format(), combining the contents of the
            'video_format_list' and 'video_format_mode' options. The combined
            value is passed to youtube-dl with the -f switch

        all_formats (bool): If True, download all available video formats.
            Also set in the call to OptionsParser.build_video_format()

        prefer_free_formats (bool): If True, prefer free video formats unless
            one is specfied by video_format, etc

        yt_skip_dash (bool): If True, do not download DASH-related data with
            YouTube videos

        merge_output_format (str): If a merge is required (e.g.
            bestvideo+bestaudio), output to this container format. youtube-dl
            supports the strings 'mkv', 'mp4', 'ogg', 'webm', 'flv' (or an
            empty string to ignore this option)

    SUBTITLE OPTIONS

        write_subs (bool): If True, youtube-dl will try to download the
            subtitles file for the given URL

        write_auto_subs (bool): If True, youtube-dl will try to download the
            automatic subtitles file for the given URL

        write_all_subs (bool): If True, youtube-dl will try to download all the
            the available subtitles files for the given URL

        subs_format (str): Subtitle format preference. youtube-dl supports
            'srt', 'ass', 'vtt', 'lrc' or combinations thereof, e.g.
            'ass/srt/best'

        subs_lang (str): Language of the subtitles file to download. Requires
            the 'write_subs' option. Can not be set directly by the user;
            instead, OptionsParser.parse() converts the option 'subs_lang_list'
            to a string, and sets this option to that string

    AUTHENTIFICATION OPTIONS

        username (str): Username to login with

        password (str): Password to login with

        two_factor (str): Two-factor authentification code

        net_rc (bool): If True, use .netrc authentification data

        video_password (str): Video password for the given URL

    ADOBE PASS OPTIONS

        ap_mso (str): Adobe Pass multiple-system operator (TV provider)
            identifier

        ap_username (str): Multiple-system operator account login

        ap_password (str): Multiple-system operator account password. If this
            option is left out, yt-dlp will ask interactively

    POST-PROCESSING OPTIONS

        extract_audio (bool): If True, youtube-dl will post-process the video
            file

        audio_format (str): Audio format of the post-processed file. Available
            values are 'mp3', 'wav', 'aac', 'm4a', 'vorbis', 'opus' & 'flac'

        audio_quality (str): Audio quality of the post-processed file.
            Available values for VBR are '9' ('high'), '5' (medium) or '0'
            ('low'). Other values are '320k', '256k', '192k', '128k', '96k'.
            The default value is 5

        recode_video (str): Encode the video to another format if necessary.
            One of the strings 'avi', 'flv', 'mkv', 'mp4', 'ogg', 'webm', or an
            empty string if disabled

        pp_args (str): Give these arguments to the postprocessor. Tartube
            automatically adds quotes to the beginning and end of the string

        keep_video (bool): If True, youtube-dl will keep the video file after
            post-processing it

        embed_subs (bool): If True, youtube-dl will merge the subtitles file
            with the video (only for .mp4 files)

        embed_thumbnail (bool): When True will embed the thumbnail in the audio
            file as cover art

        add_metadata (bool): When True will write metadata to the video file

        fixup_policy (str): Automatically correct known faults of the file.
            The string can be 'never', 'warn', 'detect_or_worn' or an empty
            string if disabled

        prefer_avconv (bool): Prefer AVConv over FFmpeg for running the
            postprocessors

        prefer_ffmpeg (bool): Prefer FFmpeg over AVConv for running the
            postprocessors

    YT-DLP OPTIONS (will not work with other downloaders)

        [not passed to yt-dlp directly]

        output_format_list (list): List of arguments used with the '--output'
            parameter, in the form TYPES:TEMPLATE. Each argument should have
            a unique TYPES component. If this option is specified,
            'output_format' and 'output_template' are ignored

        output_path_list (list): List of arguments used with the '--paths'
            paremeter, in the form TYPES:PATH. Each argument should have a
            unique TYPES component. If 'output_path_list' contains any values,
            then 'output_format_list' and/or 'output_format' are used without a
            preceding absolute path

        extractor_args_list (list): Pass these arguments to the extractor. You
            can use this option multiple times to give different arguments to
            different extractors. Each item in the list is in the form
            KEY:ARGS

        [Video Selection Options]

        break_on_existing (bool): If True, stops the download process when
            encountering a file that is in the archive

        break_on_reject (bool): If True, stops the download process when
            encountering a file that has been filtered out

        skip_playlist_after_errors (int): Number of allowed failures until the
            rest of the playlist is skipped

        [Download Options]

        concurrent_fragments (int): Number of fragments of a DASH/hlsnative
            video that should be download concurrently (default is 1)

        throttled_rate (int): Minimum download rate in bytes per second below
            which throttling is assumed and the video data is re-extracted
            (e.g. 100K)

        [Filesystem Options]

        windows_filenames (bool): If True, forces filenames to be MS Windows
            compatible

        trim_filenames (int): Limit the filename length (excluding extension)
            to the specified number of characters

        no_overwrites (bool): If True, does not overwrite any files

        force_overwrites (bool): If True, overwrites all video and metadata
            files. This option includes '--no-continue' (for which there is no
            Tartube download option)

        write_playlist_metafiles (bool): If True, writes playlist metadata in
            addition to the video metadata when using --write-info-json,
            --write-description etc. (default)

        no_clean_info_json (bool): If True, writes all fields to the infojson
            (default is to remove some private fields)

        no_cookies (bool): If True, does not read/dump cookies from/to file
            (default)

        cookies_from_browser (str): The name of the browser and (optionally)
            the name/path of the profile to load cookies from; a string in the
            from BROWSER[+KEYRING][:PROFILE]

        no_cookies_from_browser (bool): If True, does not load cookies from the
            browser (default)

        [Internet Shortcut Options]

        write_link (bool): If True, writes an internet shortcut file, depending
            on the current platform (.url, .webloc or .desktop). The URL may be
            cached by the OS

        write_url_link (bool): If True, writes a .url Windows internet
            shortcut. The OS caches the URL based on the file path

        write_webloc_link (bool): If True, writes a .webloc macOS internet
            shortcut

        write_desktop_link (bool): If True, writes a .desktop Linux internet
            shortcut

        [Verbosity and Simulation Options]

        ignore_no_formats_error (bool): If True, ignore "No video formats"
            error. Useful for extracting metadata even if the video is not
            actually available for download (experimental)

        force_write_archive (bool): If True, forces download archive entries to
            be written as far as no errors occur, even if -s or another
            simulation option is used

        [Workaround Options]

        sleep_requests (int): Number of seconds to sleep between requests
            during data extraction

        sleep_subtitles (int): Number of seconds to sleep before each download.
            This is the minimum time to sleep when used along with
            'max_sleep_interval'

        [Video Format Options]

        video_multistreams (bool): If True, allows multiple video streams to be
            merged into a single file

        audio_multistreams (bool): If True, allows multiple audio streams to be
            merged into a single file

        check_formats (bool): If True, checks that the formats selected are
            actually downloadable (Experimental)

        allow_unplayable_formats (bool): If True, allows unplayable formats to
            be listed and downloaded. All video post-processing will also be
            turned off

        [Post-Processing Options]

        remux_video (str): Remux the video into another container if necessary
            (currently supported: mp4|mkv|flv|webm|mov|avi|mp3|mka|m4a|ogg
            |opus). If target container does not support the video/audio codec,
            remuxing will fail. You can specify multiple rules; Eg.
            "aac>m4a/mov>mp4/mkv" will remux aac to m4a, mov to mp4 and
            anything else to mkv.

        embed_metadata (bool): Embed metadata including chapter markers (if
            supported by the format) to the video file

        convert_thumbnails (str): Convert the thumbnails to another format
            (currently supported: jpg|png)

        split_chapters (bool): Split video into multiple files based on
            internal chapters. The "chapter:" prefix can be used with
            'output_path_list' and 'output_template_list' to set the output
            filename for the split files

        [Extractor Options]

        extractor_retries (str): Number of retries for known extractor errors
            (default is '3'), or 'infinite'

        no_allow_dynamic_mpd (bool): If True, do not process dynamic DASH
            manifests

        hls_split_discontinuity (bool): If True, splits HLS playlists to
            different formats at discontinuities such as ad breaks

    YOUTUBE-DL-GUI OPTIONS (not passed to youtube-dl directly)

        [used to build parameters of --output and --paths]

        output_format (int): Option in the range 0-9, which is converted into
            a youtube-dl output template using
            formats.FILE_OUTPUT_CONVERT_DICT. If the value is 0, then the
            custom 'output_template' is used instead

        output_template (str): Can be any output template supported by
            youtube-dl. Ignored if 'output_format' is not 0

        [used in conjunction with the 'min_filesize' & 'max_filesize' options]

            max_filesize_unit (str): Maximum file size unit. Available values:
                '' (for bytes), 'k' (for kilobytes, etc), 'm', 'g', 't', 'p',
                'e', 'z', 'y'

            min_filesize_unit (str): Minimum file size unit. Available values
                as above

        [in youtube-dl-gui, this was named 'cmd_args']

        extra_cmd_string (str): String that contains extra youtube-dl options
            separated by spaces. Components containing whitespace can be
            enclosed within double quotes "..."

        direct_cmd_flag (str): If True, only those command options specified by
            extra_cmd_string (including the source URL) are used; Tartube
            merely adds the downloader (e.g. 'youtube-dl') and the output
            directory switch (i.e. -o)

        direct_url_flag (str): If True, Tartube assumes that
            'extra_cmd_string' contains the URL to check/download. Otherwise,
            the source URL of each affected media data object(s) is used, as
            normal. Ignored if 'direct_cmd_flag' is False

    TARTUBE OPTIONS (not passed to youtube-dl directly)

        move_description (bool):
        move_info (bool):
        move_annotations (bool):
        move_thumbnail (bool):
            During a download operation (real or simulated), if these values
            are True, the video description/JSON/annotations files are moved to
            a '.data' sub-directory, and the thumbnails are moved to a
            '.thumbs' sub-directory, inside the directory containing the videos

        keep_description (bool):
        keep_info (bool):
        keep_annotations (bool):
        keep_thumbnail (bool):
            During a download operation (not simulated, e.g. when the user
            clicks the 'Download all' button), the video description/JSON/
            annotations/thumbnail files are downloaded only if
            'write_description', 'write_info', 'write_annotations' and/or
            'write_thumbnail' are True

            They are initially stored in the same sub-directory in which
            Tartube will store the video

            If these options are True, they stay there; otherwise, they are
            copied into the equivalent location in Tartube's temporary
            directories.

        sim_keep_description (bool):
        sim_keep_info (bool):
        sim_keep_annotations (bool):
        sim_keep_thumbnail (bool):
            During a download operation (simulated, e.g. when the user clicks
            the 'Check all' button), the video's JSON file is always loaded
            into memory

            If 'write_description' and 'sim_keep_description' are both True,
            the description file is written directly to the sub-directory in
            which Tartube would store the video

            If 'write_description' is True but 'sim_keep_description' not, the
            description file is written to the equivalent location in Tartube's
            temporary directories.

            The same applies to the JSON, annotations and thumbnail files.

        use_fixed_folder (str or None): If not None, then all videos are
            downloaded to one of Tartube's fixed folders (not including private
            folders) - currently, that group consists of only 'Temporary
            Videos', 'Unsorted Videos' and 'Video Clips' (or their translated
            equivalents). The value, if not None, should be 'temp' (matches
            mainapp.TartubeApp.fixed_temp_folder), 'misc'
            (matches mainapp.TartubeApp.fixed_misc_folder) or 'clips'
            (matches mainapp.TartubeApp.fixed_clips_folder)

        match_title_list (list): Download only matching titles (regex or
            caseless sub-string). Each item in the list is passed to youtube-dl
            as a separate --match-title argument

        reject_title_list (list): Skip download for any matching titles (regex
            or caseless sub-string). Each item in the list is passed to
            youtube-dl as a separate --reject-title argument

        video_format_list (list): List of video formats to download, in order
            of preference. If an empty list, youtube-dl will choose the best
            video format available for the given URL. Otherwise, the items in
            this list are keys in formats.VIDEO_FORMAT_DICT. The corresponding
            values are combined and stored as the 'video_format' option, first
            being rearrnaged to put video formats before audio formats
            (otherwise youtube-dl won't download the video formats)

        video_format_mode (str): 'all' to download all available formats,
            ignoring the preference list (sets the option 'all_formats').
            'single' to download the first available format in
            'video_format_list'. 'combine' to download the first video and the
            first audio format in the list, to be combined as a single file,
            'multiple' to download all available formats in 'video_format_list'

        subs_lang_list (list): List of language tags which are used to set
            the 'subs_lang' option

        downloader_config (bool): If True, a youtube-dl configuration is
            specified with the '--config-location' option. On Linux/MacOS, the
            user-wide configuration file is used
            ('~/.config/youtube-dl/config'). On MS Windows, a file in the
            Tartube directory is used (youtube-dl.conf)

    """


    # Standard class methods


    def __init__(self, uid, name, dbid=None):

        # IV list - other
        # ---------------
        # Unique ID for this options manager
        self.uid = uid
        # A non-unique name for this options manager. Managers that are
        #   attached to a media data object have the same name as that object
        # (The name is not unique because, for example, videos could have the
        #   same name as a channel; it's up to the user to avoid duplicate
        #   names)
        # Empty strings are not valid as names
        self.name = name
        # A short description, intended for use in the Drag and Drop tab
        # Empty strings are valid as descriptions
        self.descrip = name
        # A list of .dbid value for any media data objects to which this
        #   options manager is attached. (When attached to a media.Folder,
        #   this options manager applies to any child video, channel, playlist
        #   or sub-folder)
        # An empty list if not attached to a media data object
        self.dbid_list = []

        # Dictionary of download options for youtube-dl, set by a call to
        #   self.reset_options
        self.options_dict = {}


        # Code
        # ----

        if dbid is not None:
            self.dbid_list = [dbid]

        # Initialise youtube-dl options
        self.reset_options()


    # Public class methods


    def clone_options(self, other_options_manager_obj):

        """Called by mainapp.TartubeApp.apply_download_options(),
        .clone_download_options() and .clone_download_options_from_window().

        Clones download options from the specified object into this object,
        completely replacing this object's download options.

        Args:

            other_options_manager_obj (options.OptionsManager): The download
                options object (usually the General Options Manager), from
                which options will be cloned

        """

        self.options_dict = other_options_manager_obj.options_dict.copy()

        # In the dictionary's key-value pairs, some values are themselves lists
        #   that must be copied directly
        for key in [
            'match_title_list', 'reject_title_list', 'video_format_list',
            'subs_lang_list',
        ]:
            self.options_dict[key] \
            = other_options_manager_obj.options_dict[key].copy()


    def rearrange_formats(self):

        """Called by config.OptionsEditWin.apply_changes().

        The option 'video_format_list' specifies video formats, audio formats
        or a mixture of both.

        youtube-dl won't download the specified formats properly, if audio
        formats appear before video formats. Therefore, this function is called
        to rearrange the list, putting all video formats above all audio
        formats.
        """

        format_list = self.options_dict['video_format_list']
        video_list = []
        audio_list = []
        comb_list = []

        for code in format_list:

            if code != '0':

                if code in formats.VIDEO_OPTION_TYPE_DICT and \
                formats.VIDEO_OPTION_TYPE_DICT[code] == 'audio':
                    audio_list.append(code)
                else:
                    video_list.append(code)

        comb_list.extend(video_list)
        comb_list.extend(audio_list)

        self.options_dict['video_format_list'] = format_list


    def reset_options(self):

        """Called by self.__init__().

        Resets (or initialises) self.options_dict to its default state.
        """

        if os.name == 'nt':
            windows_filenames_flag = True
        else:
            windows_filenames_flag = False

        self.options_dict = {
            # OPTIONS
            'ignore_errors': True,
            'abort_on_error': False,
            'live_from_start': False,
            'wait_for_video_min': 0,
            # NETWORK OPTIONS
            'proxy': '',
            'socket_timeout': '',
            'source_address': '',
            'force_ipv4': False,
            'force_ipv6': False,
            # GEO-RESTRICTION
            'geo_verification_proxy': '',
            'geo_bypass': False,
            'no_geo_bypass': False,
            'geo_bypass_country': '',
            'geo_bypass_ip_block': '',
            # VIDEO SELECTION
            'playlist_start': 1,
            'playlist_end': 0,
            'playlist_items': '',
            'max_downloads': 0,
            'min_filesize': 0,
            'max_filesize': 0,
            'date': '',
            'date_before': '',
            'date_after': '',
            'min_views': 0,
            'max_views': 0,
            'match_filter': '',
            'age_limit': '',
            'include_ads': False,
            # DOWNLOAD OPTIONS
            'limit_rate': '',             # Can't be directly modified by user
            'retries': 10,
            'abort_on_unavailable_fragment': False,
            'playlist_reverse': False,
            'playlist_random': False,
            'native_hls': True,
            'hls_prefer_ffmpeg': False,
            'external_downloader': '',
            'external_arg_string': '',
            # FILESYSTEM OPTIONS
            'restrict_filenames': False,
            'nomtime': False,
            'write_description': True,
            'write_info': True,
            'write_annotations': False,
            'cookies_path': '',
            # THUMBNAIL IMAGES
            'write_thumbnail': True,
            # VERBOSITY / SIMULATION OPTIONS
            #   (none implemented)
            # WORKAROUNDS
            'force_encoding': '',
            'no_check_certificate': False,
            'prefer_insecure': False,
            'user_agent': '',
            'referer': '',
            'min_sleep_interval': 0,
            'max_sleep_interval': 0,
            # VIDEO FORMAT OPTIONS
            'video_format': '0',
            'all_formats': False,
            'prefer_free_formats': False,
            'yt_skip_dash': False,
            'merge_output_format': '',
            # SUBTITLE OPTIONS
            'write_subs': False,
            'write_auto_subs': False,
            'write_all_subs': False,
            'subs_format': '',
            'subs_lang': '',
            # AUTHENTIFICATION OPTIONS
            'username': '',
            'password': '',
            'two_factor': '',
            'net_rc': False,
            'video_password': '',
            # ADOBE PASS OPTIONS
            'ap_mso': '',
            'ap_username': '',
            'ap_password': '',
            # POST-PROCESSING OPTIONS
            'extract_audio': False,
            'audio_format': '',
            'audio_quality': '5',
            'recode_video': '',
            'pp_args': '',
            'keep_video': False,
            'embed_subs': False,
            'embed_thumbnail': False,
            'add_metadata': False,
            'fixup_policy': '',
            'prefer_avconv': False,
            'prefer_ffmpeg': False,
            # YT-DLP OPTIONS
            # (not passed to yt-dlp directly)
            'output_format_list': [],
            'output_path_list': [],
            'extractor_args_list': [],
            # (Video Selection Options)
            'break_on_existing': False,
            'break_on_reject': False,
            'skip_playlist_after_errors': 0,
            # (Download Options)
            'concurrent_fragments': 1,
            'throttled_rate': 0,
            # (Filesystem Options)
            'windows_filenames': windows_filenames_flag,
            'trim_filenames': 0,
            'no_overwrites': False,
            'force_overwrites': False,
            'write_playlist_metafiles': False,
            'no_clean_info_json': False,
            'no_cookies': False,
            'cookies_from_browser': '',
            'no_cookies_from_browser': True,
            # (Internet Shortcut Options)
            'write_link': False,
            'write_url_link': False,
            'write_webloc_link': False,
            'write_desktop_link': False,
            # (Verbosity and Simulation Options)
            'ignore_no_formats_error': False,
            'force_write_archive': False,
            # (Workaround Options)
            'sleep_requests': 0,
            'sleep_subtitles': 0,
            # (Video Format Options)
            'video_multistreams': False,
            'audio_multistreams': False,
            'check_formats': False,
            'allow_unplayable_formats': False,
            # (Post-Processing Options)
            'remux_video': '',
            'embed_metadata': False,
            'convert_thumbnails': '',
            'split_chapters': False,
            # (Extractor Options)
            'extractor_retries': '3',
            'no_allow_dynamic_mpd': False,
            'hls_split_discontinuity': False,
            # YOUTUBE-DL-GUI OPTIONS
            'output_format': 2,
            'output_template': '%(title)s.%(ext)s',
            'max_filesize_unit': '',
            'min_filesize_unit': '',
            'extra_cmd_string': '',
            'direct_cmd_flag': False,
            'direct_url_flag': False,
            # TARTUBE OPTIONS
            'move_description': False,
            'move_info': False,
            'move_annotations': False,
            'move_thumbnail': False,
            'keep_description': False,
            'keep_info': False,
            'keep_annotations': False,
            'keep_thumbnail': True,
            'sim_keep_description': False,
            'sim_keep_info': False,
            'sim_keep_annotations': False,
            'sim_keep_thumbnail': True,
            'use_fixed_folder': None,
            'match_title_list': [],
            'reject_title_list': [],
            'video_format_list': [],
            'video_format_mode': 'combine',
            'subs_lang_list': [ 'en' ],
            'downloader_config': False,
        }


    def set_general_options(self):

        """Called by mainapp.TartubeApp.start().

        Configures this object with a suitable description.
        """

        self.descrip = _('General (default) download options')


    def set_classic_mode_options(self, no_descrip_flag=False):

        """Called by mainapp.TartubeApp.start() and
        .apply_classic_download_options().

        Also called by self.set_mp3_options().

        Configures this object for use in the Classic Mode tab.

        Args:

            no_descrip_flag (bool): Set to True when called from
                self.set_mp3_options()

        """

        if not no_descrip_flag:
            self.descrip = _('Download options for the Classic Mode tab')

        self.options_dict['write_description'] = False
        self.options_dict['write_info'] = False
        self.options_dict['write_annotations'] = False
        self.options_dict['write_thumbnail'] = False

        self.options_dict['move_description'] = False
        self.options_dict['move_info'] = False
        self.options_dict['move_annotations'] = False
        self.options_dict['move_thumbnail'] = False

        self.options_dict['keep_description'] = False
        self.options_dict['keep_info'] = False
        self.options_dict['keep_annotations'] = False
        self.options_dict['keep_thumbnail'] = False

        self.options_dict['sim_keep_description'] = False
        self.options_dict['sim_keep_info'] = False
        self.options_dict['sim_keep_annotations'] = False
        self.options_dict['sim_keep_thumbnail'] = False


    def set_mp3_options(self):

        """Called by mainapp.TartubeApp.start().

        Configures this object to download MP3s.
        """

        self.descrip = _('Download and convert to MP3 (requires FFmpeg)')

        # (Actual downloads take place in the Classic Mode tab, so use the same
        #   file options)
        self.set_classic_mode_options(True)

        # (Convert everything to MP3)
        self.options_dict['extract_audio'] = True
        self.options_dict['audio_format'] = 'mp3'


    # Set accessors


    def add_dbid(self, dbid):

        # (Don't add duplicates. This might be a concern, when called from
        #   mainapp.TartubeApp.fix_integrity_db() )
        if not dbid in self.dbid_list:
            self.dbid_list.append(dbid)


    def del_dbid(self, dbid):

        self.dbid_list.remove(dbid)


    def reset_dbid(self):

        self.dbid_list = []


class OptionsParser(object):

    """Called by downloads.DownloadManager.__init__() and by
    mainwin.SystemCmdDialogue.update_textbuffer().

    This object converts the download options specified by an
    options.OptionsManager object into a list of youtube-dl command line
    options, whenever required.

    Args:

        app_obj (mainapp.TartubeApp): The main application

    """


    # Standard class methods


    def __init__(self, app_obj):

        # IV list - class objects
        # -----------------------
        # The main application
        self.app_obj = app_obj


        # IV list - other
        # ---------------
        # List of options.OptionHolder objects, with their initial settings
        # The options here are in the same order in which they appear in
        #   youtube-dl's options list
        self.option_holder_list = [
            # OPTIONS
            # -i, --ignore-errors
            OptionHolder('ignore_errors', '-i', False),
            # --abort-on-error
            OptionHolder('abort_on_error', '--abort-on-error', False),
            # --live-from-start
            OptionHolder('live_from_start', '--live-from-start', False),
            # --wait-for-video MIN (N.B. the MAX value is not supported)
            OptionHolder('wait_for_video_min', '--wait-for-video', 0),
            # NETWORK OPTIONS
            # --proxy URL
            OptionHolder('proxy', '--proxy', ''),
            # --socket-timeout SECONDS
            OptionHolder('socket_timeout', '--socket-timeout', ''),
            # --source-address IP
            OptionHolder('source_address', '--source-address', ''),
            # -4, --force-ipv4
            OptionHolder('force_ipv4', '--force-ipv4', False),
            # -6, --force-ipv6
            OptionHolder('force_ipv6', '--force-ipv6', False),
            # GEO-RESTRICTION
            # --geo-verification-proxy URL
            OptionHolder(
                'geo_verification_proxy',
                '--geo-verification-proxy',
                '',
            ),
            # --geo-bypass
            OptionHolder('geo_bypass', '--geo-bypass', False),
            # --no-geo-bypass
            OptionHolder('no_geo_bypass', '--no-geo-bypass', False),
            # --geo-bypass-country CODE
            OptionHolder('geo_bypass_country', '--geo-bypass-country', ''),
            # --geo-bypass-ip-block IP_BLOCK
            OptionHolder('geo_bypass_ip_block', '--geo-bypass-ip-block', ''),
            # VIDEO SELECTION
            # --playlist-start NUMBER
            OptionHolder('playlist_start', '--playlist-start', 1),
            # --playlist-end NUMBER
            OptionHolder('playlist_end', '--playlist-end', 0),
            # --playlist-items ITEM_SPEC
            OptionHolder('playlist_items', '--playlist-items', ''),
            # --max-downloads NUMBER
            OptionHolder('max_downloads', '--max-downloads', 0),
            # --min-filesize SIZE
            OptionHolder('min_filesize', '--min-filesize', 0),
            # --max-filesize SIZE
            OptionHolder('max_filesize', '--max-filesize', 0),
            # --date DATE
            OptionHolder('date', '--date', ''),
            # --datebefore DATE
            OptionHolder('date_before', '--datebefore', ''),
            # --dateafter DATE
            OptionHolder('date_after', '--dateafter', ''),
            # --min-views COUNT
            OptionHolder('min_views', '--min-views', 0),
            # --max-views COUNT
            OptionHolder('max_views', '--max-views', 0),
            # --match-filter FILTER
            OptionHolder('match_filter', '--match-filter', ''),
            # --age-limit YEARS
            OptionHolder('age_limit', '--age-limit', ''),
            # --include-ads
            OptionHolder('include_ads', '--include-ads', False),
            # DOWNLOAD OPTIONS
            # -r, --limit-rate RATE
            OptionHolder('limit_rate', '-r', ''),
            # -R, --retries RETRIES
            OptionHolder('retries', '-R', 10),
            # --abort-on-unavailable-fragment
            OptionHolder(
                'abort_on_unavailable_fragment',
                '--abort-on-unavailable-fragment',
                False
            ),
            # --playlist-reverse
            OptionHolder('playlist_reverse', '--playlist-reverse', False),
            # --playlist-random
            OptionHolder('playlist_random', '--playlist-random', False),
            # --hls-prefer-native
            OptionHolder('native_hls', '--hls-prefer-native', False),
            # --hls-prefer-ffmpeg
            OptionHolder('hls_prefer_ffmpeg', '--hls-prefer-ffmpeg', False),
            # --external-downloader COMMAND
            OptionHolder('external_downloader', '--external-downloader', ''),
            # --external-downloader-args ARGS
            OptionHolder(
                'external_arg_string',
                '--external-downloader-args',
                '',
            ),
            # FILESYSTEM OPTIONS
            # --restrict-filenames
            OptionHolder('restrict_filenames', '--restrict-filenames', False),
            # --no-mtime
            OptionHolder('nomtime', '--no-mtime', False),
            # --write-description
            OptionHolder('write_description', '--write-description', False),
            # --write-info-json
            OptionHolder('write_info', '--write-info-json', False),
            # --write-annotations
            OptionHolder('write_annotations', '--write-annotations', False),
            # --cookies FILE
            OptionHolder('cookies_path', '--cookies', ''),
            # THUMBNAIL IMAGES
            # --write-thumbnail
            OptionHolder('write_thumbnail', '--write-thumbnail', False),
            # VERBOSITY / SIMULATION OPTIONS
            #   (none implemented)
            # WORKAROUNDS
            # --encoding ENCODING
            OptionHolder('force_encoding', '--encoding', ''),
            # --no-check-certificate
            OptionHolder(
                'no_check_certificate',
                '--no-check-certificate',
                False,
            ),
            # --prefer-insecure
            OptionHolder('prefer_insecure', '--prefer-insecure', False),
            # --user-agent UA
            OptionHolder('user_agent', '--user-agent', ''),
            # --referer URL
            OptionHolder('referer', '--referer', ''),
            # --sleep-interval SECONDS
            OptionHolder('min_sleep_interval', '--sleep-interval', 0),
            # --max-sleep-interval SECONDS
            OptionHolder('max_sleep_interval', '--max-sleep-interval', 0),
            # VIDEO FORMAT OPTIONS
            # -f, --format FORMAT
            OptionHolder('video_format', '-f', '0'),
            # --all-formats
            OptionHolder('all_formats', '--all-formats', False),
            # --prefer-free-formats
            OptionHolder(
                'prefer_free_formats',
                '--prefer-free-formats',
                False,
            ),
            # --youtube-skip-dash-manifest
            OptionHolder(
                'yt_skip_dash',
                '--youtube-skip-dash-manifest',
                False,
            ),
            # --merge-output-format FORMAT
            OptionHolder('merge_output_format', '--merge-output-format', ''),
            # SUBTITLE OPTIONS
            # --write-sub
            OptionHolder('write_subs', '--write-sub', False),
            # --write-auto-sub
            OptionHolder('write_auto_subs', '--write-auto-sub', False),
            # --all-subs
            OptionHolder('write_all_subs', '--all-subs', False),
            # --sub-format FORMAT
            OptionHolder('subs_format', '--sub-format', ''),
            # --sub-lang LANGS
            # NB This '--sub-lang' string is not the one used as a switch by
            #   self.parse()
            OptionHolder('subs_lang', '--sub-lang', '', ['write_subs']),
            # AUTHENTIFICATION OPTIONS
            # -u, --username USERNAME
            OptionHolder('username', '-u', ''),
            # -p, --password PASSWORD
            OptionHolder('password', '-p', ''),
            # -2, --twofactor TWOFACTOR
            OptionHolder('two_factor', '--twofactor', ''),
            # -n, --netrc
            OptionHolder('net_rc', '--netrc', False),
            # --video-password PASSWORD
            OptionHolder('video_password', '--video-password', ''),
            # ADOBE PASS OPTIONS
            # --ap-mso MSO
            OptionHolder('ap_mso', '--ap-mso', ''),
            # --ap-username USERNAME
            OptionHolder('ap_username', '--ap-username', ''),
            # --ap-password PASSWORD
            OptionHolder('ap_password', '--ap-password', ''),
            # POST-PROCESSING OPTIONS
            # -x, --extract-audio
            OptionHolder('extract_audio', '-x', False),
            # --audio-format FORMAT
            OptionHolder('audio_format', '--audio-format', ''),
            # --audio-quality QUALITY
            OptionHolder(
                'audio_quality',
                '--audio-quality',
                '5',
                ['extract_audio'],
            ),
            # --recode-video FORMAT
            OptionHolder('recode_video', '--recode-video', ''),
            # --postprocessor-args ARGS
            OptionHolder('pp_args', '--postprocessor-args', ''),
            # -k, --keep-video
            OptionHolder('keep_video', '-k', False),
            # --embed-subs
            OptionHolder(
                'embed_subs',
                '--embed-subs',
                False,
                ['write_auto_subs', 'write_subs'],
            ),
            # --embed-thumbnail
            OptionHolder('embed_thumbnail', '--embed-thumbnail', False),
            # --add-metadata
            OptionHolder('add_metadata', '--add-metadata', False),
            # --fixup POLICY
            OptionHolder('fixup_policy', '--fixup', ''),
            # --prefer-avconv
            OptionHolder('prefer_avconv', '--prefer-avconv', False),
            # --prefer-ffmpeg
            OptionHolder('prefer_ffmpeg', '--prefer-ffmpeg', False),
            # YT-DLP OPTIONS
            # (not given an options.OptionHolder object)
#           OptionHolder('output_format_list', '', []),
#           OptionHolder('output_path_list', '', []),
            # (Video Selection Options)
            # --break-on-existing
            OptionHolder('break_on_existing', '--break-on-existing', False),
            # --break-on-reject
            OptionHolder('break_on_reject', '--break-on-reject', False),
            # --skip-playlist-after-errors N
            OptionHolder(
                'skip_playlist_after_errors',
                '--skip-playlist-after-errors',
                0,
            ),
            # (Download Options)
            # --concurrent-fragments N
            OptionHolder('concurrent_fragments', '--concurrent-fragments', 1),
            # --throttled-rate RATE
            OptionHolder('throttled_rate', '--throttled-rate', 0),
            # (Filesystem Options)
            # --windows-filenames
            OptionHolder('windows_filenames', '--windows-filenames', False),
            # --trim-filenames LENGTH
            OptionHolder('trim_filenames', '--trim-filenames', 0),
            # --no-overwrites
            OptionHolder('no_overwrites', '--no-overwrites', False),
            # --force-overwrites
            OptionHolder('force_overwrites', '--force-overwrites', False),
            # --write-playlist-metafiles
            OptionHolder(
                'write_playlist_metafiles',
                '--write-playlist-metafiles',
                False,
            ),
            # --no-clean-infojson
            OptionHolder('no_clean_info_json', '--no-clean-infojson', False),
            # --no-cookies
            OptionHolder('no_cookies', '--no-cookies', False),
            # --cookies-from-browser BROWSER[+KEYRING][:PROFILE]
            OptionHolder('cookies_from_browser', '--cookies-from-browser', ''),
            # --no-cookies-from-browser
            OptionHolder(
                'no_cookies_from_browser',
                '--no-cookies-from-browser',
                True,
            ),
            # (Internet Shortcut Options)
            # --write-link
            OptionHolder('write_link', '--write-link', False),
            # --write-url-link
            OptionHolder('write_url_link', '--write-url-link', False),
            # --write-webloc-link
            OptionHolder('write_webloc_link', '--write-webloc-link', False),
            # --write-desktop-link
            OptionHolder('write_desktop_link', '--write-desktop-link', False),
            # (Verbosity and Simulation Options)
            # --ignore-no-formats-error
            OptionHolder(
                'ignore_no_formats_error',
                '--ignore-no-formats-error',
                False,
            ),
            # --force-write-archive
            OptionHolder(
                'force_write_archive',
                '--force-write-archive',
                False,
            ),
            # (Workaround Options)
            # --sleep-requests SECONDS
            OptionHolder('sleep_requests', '--sleep-requests', 0),
            # --sleep-subtitles SECONDS
            OptionHolder('sleep_subtitles', '--sleep-subtitles', 0),
            # (Video Format Options)
            # --video-multistreams
            OptionHolder('video_multistreams', '--video-multistreams', False),
            # --audio-multistreams
            OptionHolder('audio_multistreams', '--audio-multistreams', False),
            # --check-formats
            OptionHolder('check_formats', '--check-formats', False),
            # --allow-unplayable-formats
            OptionHolder(
                'allow_unplayable_formats',
                '--allow-unplayable-formats',
                False,
            ),
            # (Post-Processing Options)
            # --remux-video FORMAT
            OptionHolder('remux_video', '--remux-video', ''),
            # --embed-metadata
            OptionHolder('embed_metadata', '--embed-metadata', False),
            # --convert-thumbnails FORMAT
            OptionHolder('convert_thumbnails', '--convert-thumbnails', ''),
            # --split-chapters
            OptionHolder('split_chapters', '--split-chapters', False),
            # (Extractor Options)
            # --extractor-retries RETRIES
            OptionHolder('extractor_retries', '--extractor-retries', '3'),
            # --no-allow-dynamic-mpd
            OptionHolder(
                'no_allow_dynamic_mpd',
                '--no-allow-dynamic-mpd',
                False,
            ),
            # --hls-split-discontinuity
            OptionHolder(
                'hls_split_discontinuity',
                '--hls-split-discontinuity',
                False,
            ),
            # YOUTUBE-DL-GUI OPTIONS (not given an options.OptionHolder object)
#           OptionHolder('output_format', '', 2),
#           OptionHolder('output_template', '', ''),
#           OptionHolder('max_filesize_unit', '', ''),
#           OptionHolder('min_filesize_unit', '', ''),
#           OptionHolder('extra_cmd_string', '', ''),
#           OptionHolder('direct_cmd_flag', '', False),
#           OptionHolder('direct_url_flag', '', False),
            # TARTUBE OPTIONS (not given an options.OptionHolder object)
#           OptionHolder('move_description', '', False),
#           OptionHolder('move_info', '', False),
#           OptionHolder('move_annotations', '', False),
#           OptionHolder('move_thumbnail', '', False),
#           OptionHolder('keep_description', '', False),
#           OptionHolder('keep_info', '', False),
#           OptionHolder('keep_annotations', '', False),
#           OptionHolder('keep_thumbnail', '', False),
#           OptionHolder('sim_keep_description', '', False),
#           OptionHolder('sim_keep_info', '', False),
#           OptionHolder('sim_keep_annotations', '', False),
#           OptionHolder('sim_keep_thumbnail', '', False),
#           OptionHolder('use_fixed_folder', '', None),
#           OptionHolder('match_title_list', '', []),
#           OptionHolder('reject_title_list', '', []),
#           OptionHolder('video_format_list', '', []),
#           OptionHolder('video_format_mode', '', 'combine'),
#           OptionHolder('subs_lang_list', '', []),
#           OptionHolder('downloader_config', '', False),
        ]


    # Public class methods


    def parse(self, media_data_obj, options_manager_obj,
    operation_type='real', scheduled_obj=None):

        """Called by downloads.DownloadWorker.prepare_download() and
        mainwin.MainWin.update_textbuffer() and several other functions.

        Converts the download options stored in the specified
        options.OptionsManager object into a list of youtube-dl command line
        options.

        Args:

            media_data_obj (media.Video, media.Channel, media.Playlist,
                media.Folder): The media data object being downloaded

            options_manager_obj (options.OptionsManager): The object containing
                the download options for this media data object

            operation_type (str): 'sim', 'real', 'custom_sim', 'custom_real',
                'classic_sim', 'classic_real', 'classic_custom' (matching
                possible values of downloads.DownloadManager.operation_type)

            scheduled_obj (media.Scheduled): If a scheduled download is
                involved, the corresponding object (so bandwidth limits can be
                extracted)

        Return values:

            List of strings with all the youtube-dl command line options

        """

        # Force youtube-dl's progress bar to be outputted as separate lines
        options_list = ['--newline']

        # Create a copy of the dictionary
        copy_dict = options_manager_obj.options_dict.copy()

        # Get the directory into which files are downloaded
        dir_path = self.build_dir(
            media_data_obj,
            copy_dict,
            operation_type,
        )

        # Modify various values in the copy. Set the 'video_format' and
        #   'all_formats' options
        self.build_video_format(media_data_obj, copy_dict, operation_type)
        # Set the 'min_filesize' and 'max_filesize' options
        self.build_file_sizes(copy_dict)
        # Set the 'limit_rate' option
        self.build_limit_rate(copy_dict, scheduled_obj)
        # Set the 'proxy' option
        self.build_proxy(copy_dict)

        # Parse basic youtube-dl command line options
        for option_holder_obj in self.option_holder_list:

            # First deal with special cases...
            if option_holder_obj.name == 'extract_audio':
                if copy_dict['audio_format'] == '':
                    value = copy_dict[option_holder_obj.name]

                    if value != option_holder_obj.default_value:
                        options_list.append(option_holder_obj.switch)

            elif option_holder_obj.name == 'audio_format':
                value = copy_dict[option_holder_obj.name]

                if copy_dict['extract_audio'] \
                and value != option_holder_obj.default_value:
                    options_list.append('-x')
                    options_list.append(option_holder_obj.switch)
                    options_list.append(ttutils.to_string(value))

                    # The '-x' / '--audio-quality' switch must precede the
                    #   '--audio-quality' switch, if both are used
                    # Therefore, if the current value of the 'audio_quality'
                    #   option is not the default value ('5'), then insert the
                    #   '--audio-quality' switch into the options list right
                    #   now
                    if copy_dict['audio_quality'] != '5':
                        options_list.append('--audio-quality')
                        options_list.append(
                            ttutils.to_string(copy_dict['audio_quality']),
                        )

            elif option_holder_obj.name == 'audio_quality':
                # If the '--audio-quality' switch was not added by the code
                #   block just above, then follow the standard procedure
                if option_holder_obj.switch not in options_list:
                    if option_holder_obj.check_requirements(copy_dict):
                        value = copy_dict[option_holder_obj.name]

                        if value != option_holder_obj.default_value:
                            options_list.append(option_holder_obj.switch)
                            options_list.append(ttutils.to_string(value))

            elif option_holder_obj.name == 'match_filter':
                value = ttutils.to_string(copy_dict[option_holder_obj.name])
                if self.app_obj.block_livestreams_flag:

                    if value == '':
                        value = '!is_live'
                    else:
                        value += r' & !is_live'

                if value != '':
                    options_list.append(option_holder_obj.switch)
                    options_list.append(value)

            elif option_holder_obj.name == 'external_arg_string' \
            or option_holder_obj.name == 'pp_args':
                value = copy_dict[option_holder_obj.name]
                if value != '':
                    options_list.append(option_holder_obj.switch)
                    options_list.append('"' + ttutils.to_string(value) + '"')

            elif option_holder_obj.name == 'cookies_path':
                cookies_path = copy_dict[option_holder_obj.name]
                options_list.append('--cookies')
                # If no path is specified, use a standard location for the
                #   cookie jar (otherwise youtube-dl will write it to
                #   ../tartube/tartube)
                if cookies_path == '':
                    options_list.append(
                        os.path.abspath(
                            os.path.join(
                                self.app_obj.data_dir,
                                self.app_obj.cookie_file_name,
                            ),
                        ),
                    )
                else:
                    options_list.append(cookies_path)

            elif option_holder_obj.name == 'trim_filenames':
                length = copy_dict[option_holder_obj.name]
                # The --trim-filenames option specifies a length that includes
                #   the directory name. If the user has specified a length that
                #   is shorter than the directory name, then ignore this
                #   option
                if length >= (len(dir_path) + 2):
                    options_list.append('--trim-filenames')
                    options_list.append(str(length))

            # For all other options, just check the value is valid
            elif option_holder_obj.check_requirements(copy_dict):
                value = copy_dict[option_holder_obj.name]

                if value != option_holder_obj.default_value:
                    options_list.append(option_holder_obj.switch)

                    if not option_holder_obj.is_boolean():
                        options_list.append(ttutils.to_string(value))

        # Parse the 'match_title_list' and 'reject_title_list'
        for item in copy_dict['match_title_list']:
            options_list.append('--match-title')
            options_list.append(item)

        for item in copy_dict['reject_title_list']:
            options_list.append('--reject-title')
            options_list.append(item)

        # Parse the 'subs_lang_list' option
        if copy_dict['write_subs'] \
        and not copy_dict['write_auto_subs'] \
        and not copy_dict['write_all_subs'] \
        and copy_dict['subs_lang_list']:

            options_list.append('--sub-lang')
            options_list.append(','.join(copy_dict['subs_lang_list']))

        # Parse the 'extractor_args_list' option
        for item in copy_dict['extractor_args_list']:
            options_list.append('--extractor-args')
            options_list.append(item)

        # Add the SponsorBlock option used in Classic Mode downloads
        if (
            operation_type == 'classic_sim' \
            or operation_type == 'classic_real' \
            or operation_type == 'classic_custom'
        ) and media_data_obj.dummy_sblock_flag:
            options_list.append('--sponsorblock-remove')
            options_list.append('default')

        # Build the --output and --paths options
        options_list = self.build_paths(
            media_data_obj,
            dir_path,
            copy_dict,
            options_list,
        )

        # Parse the 'extra_cmd_string' option, so it overrules everything else.
        #   The option can contain arguments inside double quotes "..."
        #   (arguments that can therefore contain whitespace)
        parsed_list = ttutils.parse_options(copy_dict['extra_cmd_string'])
        for item in parsed_list:
            options_list.append(item)

        # Parse the 'downloader_config' option, so it overrules everything
        #   else
        if copy_dict['downloader_config']:

            options_list.append('--config-location')
            options_list.append(ttutils.get_dl_config_path(self.app_obj))

        # Filter out yt-dlp options, if required. A list of them is specified
        #   in mainapp.TartubeApp.ytdlp_exclusive_options_dict
        if self.app_obj.ytdlp_filter_options_flag \
        and (
            self.app_obj.ytdl_fork is None \
            or self.app_obj.ytdl_fork != 'yt-dlp'
        ):
            filter_list = options_list.copy()
            options_list = []

            while filter_list:

                item = filter_list.pop(0)
                if item in self.app_obj.ytdlp_exclusive_options_dict:

                    if self.app_obj.ytdlp_exclusive_options_dict[item]:
                        # This option takes an argument
                        filter_list.pop(0)

                else:
                    options_list.append(item)

        # Parsing complete
        return options_list


    def build_file_sizes(self, copy_dict):

        """Called by self.parse().

        Build the value of the 'min_filesize' and 'max_filesize' options and
        store them in the options dictionary.

        Args:

            copy_dict (dict): Copy of the original options dictionary.

        """

        if copy_dict['min_filesize']:
            copy_dict['min_filesize'] = \
            ttutils.to_string(copy_dict['min_filesize']) + \
            copy_dict['min_filesize_unit']

        if copy_dict['max_filesize']:
            copy_dict['max_filesize'] = \
            ttutils.to_string(copy_dict['max_filesize']) + \
            copy_dict['max_filesize_unit']


    def build_limit_rate(self, copy_dict, scheduled_obj):

        """Called by self.parse().

        Build the value of the 'limit_rate' option and store it in the options
        dictionary.

        Args:

            copy_dict (dict): Copy of the original options dictionary

            scheduled_obj (media.Scheduled): If a scheduled download is
                involved, the corresponding object (so bandwidth limits can be
                extracted)

        """

        # Set the bandwidth limit (e.g. '50K'). If alternative performance
        #   limits currently apply, use that limit instead
        if self.app_obj.download_manager_obj \
        and scheduled_obj \
        and scheduled_obj.scheduled_bandwidth_apply_flag:

            # The bandwidth limit is divided equally between the workers
            limit = int(
                scheduled_obj.scheduled_bandwidth
                / len(self.app_obj.download_manager_obj.worker_list)
            )

            copy_dict['limit_rate'] = str(limit) + 'K'

        elif self.app_obj.download_manager_obj \
        and self.app_obj.download_manager_obj.alt_limits_flag \
        and self.app_obj.alt_bandwidth_apply_flag:

            limit = int(
                self.app_obj.alt_bandwidth
                / self.app_obj.alt_num_worker
            )

            copy_dict['limit_rate'] = str(limit) + 'K'

        elif self.app_obj.bandwidth_apply_flag:

            limit = int(
                self.app_obj.bandwidth_default
                / self.app_obj.num_worker_default
            )

            copy_dict['limit_rate'] = str(limit) + 'K'


    def build_proxy(self, copy_dict):

        """Called by self.parse().

        Build the value of the 'proxy' option and store it in the options
        dictionary.

        Args:

            copy_dict (dict): Copy of the original options dictionary.

        """

        # If the option is already specified, we use it. Otherwise, cycle
        #   through the proxies in the main appliation's list
        if not copy_dict['proxy']:

            proxy = self.app_obj.get_proxy()
            if proxy is not None:

                copy_dict['proxy'] = proxy


    def build_dir(self, media_data_obj, copy_dict, operation_type):

        """Called by self.parse().

        Finds the directory in which downloaded files are saved.

        Args:

            media_data_obj (media.Video, media.Channel, media.Playlist,
                media.Folder): The media data object being downloaded

            copy_dict (dict): Copy of the original options dictionary

            operation_type (str): 'sim', 'real', 'custom_sim', 'custom_real',
                'classic_sim', 'classic_real', 'classic_custom' (matching
                possible values of downloads.DownloadManager.operation_type)

        Return values:

            The output directory, as a string

        """

        # Fetch the equivalent fixed folder 'Temporary Videos', 'Unsorted
        #   Videos' or 'Video Clips', if specified
        override_obj = self.app_obj.get_fixed_folder(
            copy_dict['use_fixed_folder']
        )

        if operation_type == 'classic_sim' \
        or operation_type == 'classic_real' \
        or operation_type == 'classic_custom':

            # Special case: if a download operation was launched from the
            #   Classic Mode tab, the directory is specified in that tab
            dir_path = media_data_obj.dummy_dir

        elif not isinstance(media_data_obj, media.Video) \
        and override_obj is not None:

            # Because of the override, save all videos to a system folder
            override_obj = other_obj.get_default_dir(self.app_obj)

        elif isinstance(media_data_obj, media.Video):
            dir_path = media_data_obj.parent_obj.get_actual_dir(self.app_obj)

        else:
            dir_path = media_data_obj.get_actual_dir(self.app_obj)

        return dir_path


    def build_paths(self, media_data_obj, dir_path, copy_dict, options_list):

        """Called by self.parse().

        Build the --output and --paths options, adding them directly to the
        options list.

        Args:

            media_data_obj (media.Video, media.Channel, media.Playlist,
                media.Folder): The media data object being downloaded

            dir_path (str): The directory into which files are to be downloaded

            copy_dict (dict): Copy of the original options dictionary

            options_list (list): List of download options compiled so far; this
                function adds options directly to the list

        Return values:

            The modified options_list

        """

        # (When 'output_format_list' is specified, then 'output_format' and
        #   'output_template' are ignored. However, 'output_format_list' is
        #   normally only used with yt-dlp)
        if not copy_dict['output_format_list'] \
        or self.app_obj.ytdl_fork is None \
        or self.app_obj.ytdl_fork != 'yt-dlp' \
        or not self.app_obj.ytdlp_filter_options_flag:
            # Set the youtube-dl output template for the video's file
            template \
            = formats.FILE_OUTPUT_CONVERT_DICT[copy_dict['output_format']]
            # In the case of copy_dict['output_format'] = 0
            if template is None:
                template = copy_dict['output_template']

            options_list.append('--output')

            # (When 'output_path_list' is specified, the template is used
            #   without a preceding directory path)
            if copy_dict['output_path_list']:
                options_list.append(template)
            else:
                options_list.append(
                    os.path.abspath(os.path.join(dir_path, template)),
                )

        else:

            for item in copy_dict['output_format_list']:
                options_list.append('--output')
                options_list.append(item)

        # Thirdly, set the yt-dlp option 'output_path_list'
        if copy_dict['output_path_list']:

            for item in copy_dict['output_path_list']:
                options_list.append('--paths')
                options_list.append(item)

        # Lastly, apply the output override for a media.Video object, if one
        #   is specified
        if media_data_obj.dbid in self.app_obj.temp_output_override_dict:

            # Add it to the end of 'options_list', so anyone examining the
            #   yellow text in the Output Tab can clearly see that this is
            #   an override
            name = self.app_obj.temp_output_override_dict[media_data_obj.dbid]
            options_list.append('--output')
            options_list.append(
                os.path.abspath(os.path.join(dir_path, name + '.%(ext)s')),
            )

        return options_list


    def build_video_format(self, media_data_obj, copy_dict, operation_type):

        """Called by self.parse().

        Build the value of the 'video_format' and 'all_formats' options and
        store them in the options dictionary.

        Args:

            media_data_obj (media.Video, media.Channel, media.Playlist,
                media.Folder): The media data object being downloaded

            copy_dict (dict): Copy of the original options dictionary

            operation_type (str): 'sim', 'real', 'custom_sim', 'custom_real',
                'classic_sim', 'classic_real', 'classic_custom' (matching
                possible values of downloads.DownloadManager.operation_type)

        """

        if isinstance(media_data_obj, media.Video):

            # Special case: if a download operation was launched from the
            #   Classic Mode tab, the video format may be specified by that tab
            if (
                operation_type == 'classic_sim' \
                or operation_type == 'classic_real' \
                or operation_type == 'classic_custom'
            ) and media_data_obj.dummy_format is not None:

                # A string specifying the media format to download, or None if
                #   the user didn't specify one
                # The string is made up of three optional components in a fixed
                #   order and separated by underlines: 'convert', the video/
                #   audio format, and the video resolution, for example 'mp4',
                #   'mp4_720p', 'convert_mp4_720p'
                # Valid values are those specified by formats.VIDEO_FORMAT_LIST
                #   formats.AUDIO_FORMAT_LIST and formats.VIDEO_RESOLUTION_LIST
                convert_flag, this_format, this_res \
                = ttutils.extract_dummy_format(media_data_obj.dummy_format)

                if not convert_flag:

                    # Download the video in the specified format and
                    #   resolution, if available

                    # e.g. Default > 720p > Download in this format
                    if this_format is None and this_res is not None:

                        copy_dict['video_format'] = \
                        'bestvideo[height<=?' + this_res + ']' \
                        + '+bestaudio/best[height<=?' + this_res + ']'
                        copy_dict['all_formats'] = False
                        copy_dict['video_format_list'] = []
                        copy_dict['video_format_mode'] = ''
                        copy_dict['recode_video'] = ''

                        return

                    # e.g. mp4 > Highest > Download in this format
                    elif this_format is not None and this_res is None:

                        copy_dict['video_format'] = this_format
                        copy_dict['all_formats'] = False
                        copy_dict['video_format_list'] = []
                        copy_dict['video_format_mode'] = ''
                        copy_dict['recode_video'] = ''

                        # v2.1.009: Since the user doesn't have the possibility
                        #   of setting the -f and --merge-output-format options
                        #   to the same value (e.g. 'mp4'), we must do so
                        #   artificially
                        # yt-dlp supports: avi, flv, mkv, mov, mp4, webm
                        if this_format in formats.VIDEO_FORMAT_DICT \
                        and this_format != 'ogg':
                            copy_dict['merge_output_format'] = this_format

                        return

                    # e.g. mp4 > 720p > Download in this format
                    else:

                        copy_dict['video_format'] = \
                        'bestvideo[height<=?' + this_res + ']' \
                        + '+bestaudio/best[height<=?' + this_res + ']' \
                        + '[ext=' + this_format + ']'
                        copy_dict['all_formats'] = False
                        copy_dict['video_format_list'] = []
                        copy_dict['video_format_mode'] = ''
                        copy_dict['recode_video'] = ''

                        if this_format in formats.VIDEO_FORMAT_DICT \
                        and this_format != 'ogg':
                            copy_dict['merge_output_format'] = this_format

                        return

                else:

                    # Convert the video after download (which requires
                    #   post-processing)

                    # e.g. Default > 720p > Convert to this format
                    # N.B. This cannot actually be selected
#                   if this_format is None and this_res is not None:
#
#                       copy_dict['video_format'] = \
#                       'bestvideo[height<=?' + this_res + ']' \
#                       + '+bestaudio/best[height<=?' + this_res + ']'
#                       copy_dict['all_formats'] = False
#                       copy_dict['video_format_list'] = []
#                       copy_dict['video_format_mode'] = ''
#                       copy_dict['recode_video'] = ''
#
#                       return

                    # e.g. mp4 > Highest > Convert to this format
                    # e.g. mp4 > 720p > Convert to this format
                    if this_format is not None \
                    and this_format in formats.VIDEO_FORMAT_DICT:

                        if this_res is None:
                            copy_dict['video_format'] = '0'
                        else:
                            copy_dict['video_format'] \
                            = 'bestvideo[height<=?' + this_res + ']' \
                            + '+bestaudio/best[height<=?' + this_res + ']'

                        copy_dict['all_formats'] = False
                        copy_dict['video_format_list'] = []
                        copy_dict['video_format_mode'] = ''
                        copy_dict['recode_video'] = this_format

                        return

                    elif this_format is not None \
                    and this_format in formats.AUDIO_FORMAT_DICT:

                        copy_dict['video_format'] = '0'
                        copy_dict['all_formats'] = False
                        copy_dict['video_format_list'] = []
                        copy_dict['video_format_mode'] = ''
                        copy_dict['extract_audio'] = True
                        copy_dict['audio_format'] = this_format
                        copy_dict['recode_video'] = ''

                        return

        # Special case: for simulated downloads, don't specify any video
        #   formats; if the format isn't available for some videos, we'll get
        #   an error for each of them (rather than the simulated download we
        #   were hoping for)
        if operation_type == 'sim' \
        or operation_type == 'classic_sim' \
        or operation_type == 'custom_sim':

            copy_dict['video_format'] = '0'
            copy_dict['all_formats'] = False
            copy_dict['video_format_list'] = []
            copy_dict['video_format_mode'] = ''

            return

        # The 'video_format_list' options contains values corresponding to the
        #   keys in formats.VIDEO_OPTION_DICT, which are either real extractor
        #   codes (e.g. '35' representing 'flv [480p]') or dummy extractor
        #   codes (e.g. 'mp4')
        # Some dummy extractor codes are in the form '720p', '1080p60' etc,
        #   representing progressive scan resolutions. If the user specifies
        #   at least one of those codes, the first one is used, and all other
        #   extractor codes are ignored
        video_format_list = copy_dict['video_format_list']
        resolution_dict = formats.VIDEO_RESOLUTION_DICT.copy()
        fps_dict = formats.VIDEO_FPS_DICT.copy()

        # If the progressive scan resolution is specified, it overrides all
        #   other video format options
        height = None
        fps = None

        if self.app_obj.video_res_apply_flag:
            height = resolution_dict[self.app_obj.video_res_default]
            # (Currently, formats.VIDEO_FPS_DICT only lists formats with 60fps)
            if self.app_obj.video_res_default in fps_dict:
                fps = fps_dict[self.app_obj.video_res_default]

        else:

            for item in video_format_list:

                if item in resolution_dict:
                    height = resolution_dict[item]
                    if item in fps_dict:
                        fps = fps_dict[item]

                    break

        if height is not None:

            # (Currently, formats.VIDEO_FPS_DICT only lists formats with 60fps)
            if fps is None:

                # Use a youtube-dl argument in the form
                #   'bestvideo[height<=?height]+bestaudio/best[height<=height]'
                copy_dict['video_format'] = 'bestvideo[height<=?' \
                + str(height) + ']+bestaudio/best[height<=?' + str(height) \
                + ']'

            else:

                copy_dict['video_format'] = 'bestvideo[height<=?' \
                + str(height) + '][fps<=?' + str(fps) \
                + ']+bestaudio/best[height<=?' + str(height) + ']'

            copy_dict['all_formats'] = False
            copy_dict['video_format_list'] = []
            copy_dict['video_format_mode'] = ''

        # Not using a progressive scan resolution
        elif video_format_list:

            video_format_mode = copy_dict['video_format_mode']

            if video_format_mode == 'all':
                copy_dict['video_format'] = 0
                copy_dict['all_formats'] = True

            elif video_format_mode == 'multiple':
                copy_dict['video_format'] = ','.join(video_format_list)
                copy_dict['all_formats'] = False

            elif video_format_mode == 'combine':

                # If 'video_format_list' consists only of video-only and
                #   audio-only formats, then they can be combined in the form
                #       -f (V/V/V...)+(A/A/A...)
                # Otherwise, treat 'combine' the same as 'single', combining
                #   the formats in the form
                #       -f V/V/V/A/A/A...
                # (See Git #680)

                video_only_list = []
                audio_only_list = []
                default_list = []
                for value in video_format_list:
                    if not value in formats.VIDEO_OPTION_TYPE_DICT:
                        default_list.append(value)
                    elif formats.VIDEO_OPTION_TYPE_DICT[value] == 'video':
                        video_only_list.append(value)
                    elif formats.VIDEO_OPTION_TYPE_DICT[value] == 'audio':
                        audio_only_list.append(value)
                    else:
                        default_list.append(value)

                if not default_list and video_only_list and audio_only_list:
                    copy_dict['video_format'] = '(' + \
                    '/'.join(video_only_list) \
                    + ')+(' \
                    + '/'.join(audio_only_list) \
                    + ')'
                    copy_dict['all_formats'] = False

                else:
                    # Handle 'video_format_mode' as if its value were 'single'
                    copy_dict['video_format'] = '/'.join(video_format_list)
                    copy_dict['all_formats'] = False

            else:
                # 'video_format_mode' is 'single'
                copy_dict['video_format'] = '/'.join(video_format_list)
                copy_dict['all_formats'] = False

            copy_dict['video_format_list'] = []
            copy_dict['video_format_mode'] = ''


class OptionHolder(object):

    """Called from options.OptionsParser.__init__().

    The options parser object converts the download options specified by an
    options.OptionsManager object into a list of youtube-dl command line
    options, whenever required.

    Each option has a name, a command line switch, a default value and an
    optional list of requirements; they are stored together in an instance of
    this object.

    Args:

        name (str): Option name. Must be a valid option name from the
            optionsmanager.OptionsManager class (see the list in at the
            beginning of the options.OptionsManager class)

        switch (str): The option command line switch. See
            https://github.com/rg3/youtube-dl/#options

        default_value (any): The option default value. Must be the same type
            as the corresponding option from the optionsmanager.OptionsManager
            class.

        requirement_list (list): The requirements for the given option. This
            argument is a list of strings with the name of all the options
            that this specific option needs. If there are no requirements, the
            IV is set to None. (For example 'subs_lang' needs the 'write_subs'
            option to be enabled.)

    """


    # Standard class methods


    def __init__(self, name, switch, default_value, requirement_list=None):

        # IV list - other
        # ---------------
        self.name = name
        self.switch = switch
        self.default_value = default_value
        self.requirement_list = requirement_list


    # Public class methods


    def check_requirements(self, copy_dict):

        """Called by options.OptionsParser.parse().

        Check if options required by another option are enabled, or not.

        Args:

            copy_dict (dict): Copy of the original options dictionary.

        Return values:

            True if any of the required options is enabled, otherwise returns
                False.

        """

        if not self.requirement_list:
            return True

        return any([copy_dict[req] for req in self.requirement_list])


    def is_boolean(self):

        """Called by options.OptionsParser.parse().

        Return values:

            True if the option is a boolean switch, otherwise returns False

        """

        return type(self.default_value) is bool
