Exemplo n.º 1
0
    def __init__(self):
        self.settings = {
            # ffmpeg is a dependency for this script. ffprobe should be
            # installed along with ffmpeg.
            'path_ffmpeg': 'ffmpeg',
            'path_ffprobe': 'ffprobe',
            # Temporary output filename.
            'path_output': 'tmp.mp4',
            # Version number.
            't2t_version': '0.1',
            # Whether to display ffmpeg/ffprobe output.
            'verbose': False,
            # Whether to only generate the video file without uploading it.
            'generate_only': False,
            # Whether to forego the usage of stored oauth2 tokens.
            # If set to True, you will need to authenticate using your
            # browser each time you use the script.
            'no_stored_auth': False,
            # Default title to use in case the user's own title is
            # an empty string.
            'default_title': '(Empty title)',
            # Default variables to use for the dynamically generated title.
            'default_title_vars': 'artist,title',
            # Whether to use the dynamically generated title
            # from the file's metadata.
            'dynamic_title': True,
            'title': None,
            'title_vars': None
        }

        # Explicitly tell the underlying HTTP transport library not to retry,
        # since we are handling retry logic ourselves.
        httplib2.RETRIES = 1

        # Maximum number of times to retry before giving up.
        self.max_retries = 10

        # Always retry when these exceptions are raised.
        self.retriable_exceptions = (httplib2.HttpLib2Error, IOError,
                                     httplib.NotConnected,
                                     httplib.IncompleteRead,
                                     httplib.ImproperConnectionState,
                                     httplib.CannotSendRequest,
                                     httplib.CannotSendHeader,
                                     httplib.ResponseNotReady,
                                     httplib.BadStatusLine)

        # Always retry when an apiclient.errors.HttpError with one of these
        # status codes is raised.
        self.retriable_status_codes = [500, 502, 503, 504]

        # This OAuth 2.0 access scope allows an application to upload files to
        # the authenticated user's YouTube channel, but doesn't allow other
        # types of access.
        self.youtube_base = 'https://www.googleapis.com'
        self.youtube_upload_scope = self.youtube_base + '/auth/youtube.upload'
        self.youtube_api_service_name = 'youtube'
        self.youtube_api_version = 'v3'

        # We can set our uploaded video to one of these statuses.
        self.valid_privacy_statuses = ('public', 'private', 'unlisted')

        # This variable defines a message to display if
        # the client_secrets_file is missing.
        self.missing_client_secrets_message = '''
%s: Error: Please configure OAuth 2.0.

To make this script run you will need to populate the client_secrets.json file
found at:

   %s

with information from the Developers Console, which can be accessed
through <https://console.developers.google.com/>. See the README.md file
for more details.
'''

        # Set up our command line argument parser.
        # The argparser is initialized in oauth2client/tools.py. We're just
        # adding our own arguments to the ones already defined there.
        argparser.description = '''Generates a video from an image and audio \
file and uploads it to Youtube.'''
        argparser.epilog = '''A Youtube Data API client key is required to \
use this script, as well as ffmpeg. For help on setting up these \
dependencies, see this project\'s Github page \
<http://github.com/msikma/tune2tube/> or the included README.md file.'''
        argparser.add_help = True
        # Manually add a help argument,
        # as it is turned off in oauth2client/tools.py.
        argparser.add_argument('--no_stored_auth',
                               action='store_true',
                               help='Forego using stored oauth2 tokens.')
        argparser.add_argument('audio_file',
                               help='Audio file (MP3, OGG, FLAC, etc).')
        argparser.add_argument('image_file',
                               help='Image file (PNG, JPG, etc).')
        argparser.add_argument(
            '--output',
            help='''Save the output video (.MP4) to a file rather than \
uploading it to Youtube.''')
        argparser.add_argument('--cs_json',
                               help='''Path to the client secrets json file \
(default: client_secrets.json).''',
                               default='client_secrets.json')
        argparser.add_argument(
            '--privacy',
            choices=self.valid_privacy_statuses,
            help='Privacy status of the video (default: unlisted).',
            default='unlisted')
        argparser.add_argument(
            '--category',
            default='10',
            help='''Numeric video category (see the Github wiki for a list; \
the default is 10, Music).''')
        argparser.add_argument(
            '--keywords',
            help='Comma-separated list of video keywords/tags.',
            default='')
        mxgroup = argparser.add_mutually_exclusive_group()
        mxgroup.add_argument(
            '--title',
            help='''Video title string (default: \'%s\'). If neither --title \
nor --title_vars is specified, --title_vars will be used with its default \
value, unless this would result in \
an empty title.''' % self.settings['default_title'])
        mxgroup.add_argument(
            '--title_vars',
            nargs='?',
            help='''Comma-separated list of metadata variables to use as \
the video title (default: %s).''' % self.settings['default_title_vars'])
        argparser.add_argument(
            '--title_sep',
            help='''Separator for the title variables (default: \' - \', \
yielding e.g. \'Artist - Title\'). Ignored if \
using --title_str.''',
            default=' - ')
        argparser.add_argument(
            '--description',
            nargs='?',
            help='Video description string (default: empty string).',
            default='')
        argparser.add_argument(
            '--add_metadata',
            help='''Adds a list of audio file metadata to the \
description (default: True).''',
            default=True)
        argparser.add_argument('-V',
                               '--version',
                               action='version',
                               version='%(prog)s ' +
                               self.settings['t2t_version'],
                               help='Show version number and exit.')
        mxgroup = argparser.add_mutually_exclusive_group()
        mxgroup.add_argument(
            '-v',
            '--verbose',
            action='store_true',
            help='Verbose mode (display ffmpeg/ffprobe output).')
        mxgroup.add_argument('-q',
                             '--quiet',
                             action='store_true',
                             help='Quiet mode.')
        argparser.add_argument('-h',
                               '--help',
                               action='help',
                               default=argparse.SUPPRESS,
                               help='Show this help message and exit.')

        self.tunetags = TuneTags()
Exemplo n.º 2
0
    def __init__(self):
        self.settings = {
            # ffmpeg is a dependency for this script. ffprobe should be
            # installed along with ffmpeg.
            'path_ffmpeg': 'ffmpeg',
            'path_ffprobe': 'ffprobe',
            # Temporary output filename.
            'path_output': 'tmp.mp4',
            # Version number.
            't2t_version': '0.1',
            # Whether to display ffmpeg/ffprobe output.
            'verbose': False,
            # Whether to only generate the video file without uploading it.
            'generate_only': False,
            # Whether to forego the usage of stored oauth2 tokens.
            # If set to True, you will need to authenticate using your
            # browser each time you use the script.
            'no_stored_auth': False,
            # Default title to use in case the user's own title is
            # an empty string.
            'default_title': '(Empty title)',
            # Default variables to use for the dynamically generated title.
            'default_title_vars': 'artist,title',
            # Whether to use the dynamically generated title
            # from the file's metadata.
            'dynamic_title': True,
            'title': None,
            'title_vars': None
        }

        # Explicitly tell the underlying HTTP transport library not to retry,
        # since we are handling retry logic ourselves.
        httplib2.RETRIES = 1

        # Maximum number of times to retry before giving up.
        self.max_retries = 10

        # Always retry when these exceptions are raised.
        self.retriable_exceptions = (
            httplib2.HttpLib2Error, IOError, httplib.NotConnected,
            httplib.IncompleteRead, httplib.ImproperConnectionState,
            httplib.CannotSendRequest, httplib.CannotSendHeader,
            httplib.ResponseNotReady, httplib.BadStatusLine
        )

        # Always retry when an apiclient.errors.HttpError with one of these
        # status codes is raised.
        self.retriable_status_codes = [500, 502, 503, 504]

        # This OAuth 2.0 access scope allows an application to upload files to
        # the authenticated user's YouTube channel, but doesn't allow other
        # types of access.
        self.youtube_base = 'https://www.googleapis.com'
        self.youtube_upload_scope = self.youtube_base + '/auth/youtube.upload'
        self.youtube_api_service_name = 'youtube'
        self.youtube_api_version = 'v3'

        # We can set our uploaded video to one of these statuses.
        self.valid_privacy_statuses = ('public', 'private', 'unlisted')

        # This variable defines a message to display if
        # the client_secrets_file is missing.
        self.missing_client_secrets_message = '''
%s: Error: Please configure OAuth 2.0.

To make this script run you will need to populate the client_secrets.json file
found at:

   %s

with information from the Developers Console, which can be accessed
through <https://console.developers.google.com/>. See the README.md file
for more details.
'''

        # Set up our command line argument parser.
        # The argparser is initialized in oauth2client/tools.py. We're just
        # adding our own arguments to the ones already defined there.
        argparser.description = '''Generates a video from an image and audio \
file and uploads it to Youtube.'''
        argparser.epilog = '''A Youtube Data API client key is required to \
use this script, as well as ffmpeg. For help on setting up these \
dependencies, see this project\'s Github page \
<http://github.com/msikma/tune2tube/> or the included README.md file.'''
        argparser.add_help = True
        # Manually add a help argument,
        # as it is turned off in oauth2client/tools.py.
        argparser.add_argument(
            '--no_stored_auth',
            action='store_true',
            help='Forego using stored oauth2 tokens.'
        )
        argparser.add_argument(
            'audio_file',
            help='Audio file (MP3, OGG, FLAC, etc).'
        )
        argparser.add_argument(
            'image_file',
            help='Image file (PNG, JPG, etc).'
        )
        argparser.add_argument(
            '--output',
            help='''Save the output video (.MP4) to a file rather than \
uploading it to Youtube.'''
        )
        argparser.add_argument(
            '--cs_json',
            help='''Path to the client secrets json file \
(default: client_secrets.json).''',
            default='client_secrets.json'
        )
        argparser.add_argument(
            '--privacy',
            choices=self.valid_privacy_statuses,
            help='Privacy status of the video (default: unlisted).',
            default='unlisted'
        )
        argparser.add_argument(
            '--category',
            default='10',
            help='''Numeric video category (see the Github wiki for a list; \
the default is 10, Music).'''
        )
        argparser.add_argument(
            '--keywords',
            help='Comma-separated list of video keywords/tags.',
            default=''
        )
        mxgroup = argparser.add_mutually_exclusive_group()
        mxgroup.add_argument(
            '--title',
            help='''Video title string (default: \'%s\'). If neither --title \
nor --title_vars is specified, --title_vars will be used with its default \
value, unless this would result in \
an empty title.''' % self.settings['default_title']
        )
        mxgroup.add_argument(
            '--title_vars',
            nargs='?',
            help='''Comma-separated list of metadata variables to use as \
the video title (default: %s).''' % self.settings['default_title_vars']
        )
        argparser.add_argument(
            '--title_sep',
            help='''Separator for the title variables (default: \' - \', \
yielding e.g. \'Artist - Title\'). Ignored if \
using --title_str.''',
            default=' - '
        )
        argparser.add_argument(
            '--description',
            nargs='?',
            help='Video description string (default: empty string).',
            default=''
        )
        argparser.add_argument(
            '--add_metadata',
            action='store_true',
            help='''Adds a list of audio file metadata to the \
description (default: True).''',
            default=True
        )
        argparser.add_argument(
            '-V',
            '--version',
            action='version',
            version='%(prog)s ' + self.settings['t2t_version'],
            help='Show version number and exit.'
        )
        mxgroup = argparser.add_mutually_exclusive_group()
        mxgroup.add_argument(
            '-v',
            '--verbose',
            action='store_true',
            help='Verbose mode (display ffmpeg/ffprobe output).'
        )
        mxgroup.add_argument(
            '-q',
            '--quiet',
            action='store_true',
            help='Quiet mode.'
        )
        argparser.add_argument(
            '-h',
            '--help',
            action='help',
            default=argparse.SUPPRESS,
            help='Show this help message and exit.'
        )

        self.tunetags = TuneTags()
Exemplo n.º 3
0
class Tune2Tube(object):
    def __init__(self):
        self.settings = {
            # ffmpeg is a dependency for this script. ffprobe should be
            # installed along with ffmpeg.
            'path_ffmpeg': 'ffmpeg',
            'path_ffprobe': 'ffprobe',
            # Temporary output filename.
            'path_output': 'tmp.mp4',
            # Version number.
            't2t_version': '0.1',
            # Whether to display ffmpeg/ffprobe output.
            'verbose': False,
            # Whether to only generate the video file without uploading it.
            'generate_only': False,
            # Whether to forego the usage of stored oauth2 tokens.
            # If set to True, you will need to authenticate using your
            # browser each time you use the script.
            'no_stored_auth': False,
            # Default title to use in case the user's own title is
            # an empty string.
            'default_title': '(Empty title)',
            # Default variables to use for the dynamically generated title.
            'default_title_vars': 'artist,title',
            # Whether to use the dynamically generated title
            # from the file's metadata.
            'dynamic_title': True,
            'title': None,
            'title_vars': None
        }

        # Explicitly tell the underlying HTTP transport library not to retry,
        # since we are handling retry logic ourselves.
        httplib2.RETRIES = 1

        # Maximum number of times to retry before giving up.
        self.max_retries = 10

        # Always retry when these exceptions are raised.
        self.retriable_exceptions = (httplib2.HttpLib2Error, IOError,
                                     httplib.NotConnected,
                                     httplib.IncompleteRead,
                                     httplib.ImproperConnectionState,
                                     httplib.CannotSendRequest,
                                     httplib.CannotSendHeader,
                                     httplib.ResponseNotReady,
                                     httplib.BadStatusLine)

        # Always retry when an apiclient.errors.HttpError with one of these
        # status codes is raised.
        self.retriable_status_codes = [500, 502, 503, 504]

        # This OAuth 2.0 access scope allows an application to upload files to
        # the authenticated user's YouTube channel, but doesn't allow other
        # types of access.
        self.youtube_base = 'https://www.googleapis.com'
        self.youtube_upload_scope = self.youtube_base + '/auth/youtube.upload'
        self.youtube_api_service_name = 'youtube'
        self.youtube_api_version = 'v3'

        # We can set our uploaded video to one of these statuses.
        self.valid_privacy_statuses = ('public', 'private', 'unlisted')

        # This variable defines a message to display if
        # the client_secrets_file is missing.
        self.missing_client_secrets_message = '''
%s: Error: Please configure OAuth 2.0.

To make this script run you will need to populate the client_secrets.json file
found at:

   %s

with information from the Developers Console, which can be accessed
through <https://console.developers.google.com/>. See the README.md file
for more details.
'''

        # Set up our command line argument parser.
        # The argparser is initialized in oauth2client/tools.py. We're just
        # adding our own arguments to the ones already defined there.
        argparser.description = '''Generates a video from an image and audio \
file and uploads it to Youtube.'''
        argparser.epilog = '''A Youtube Data API client key is required to \
use this script, as well as ffmpeg. For help on setting up these \
dependencies, see this project\'s Github page \
<http://github.com/msikma/tune2tube/> or the included README.md file.'''
        argparser.add_help = True
        # Manually add a help argument,
        # as it is turned off in oauth2client/tools.py.
        argparser.add_argument('--no_stored_auth',
                               action='store_true',
                               help='Forego using stored oauth2 tokens.')
        argparser.add_argument('audio_file',
                               help='Audio file (MP3, OGG, FLAC, etc).')
        argparser.add_argument('image_file',
                               help='Image file (PNG, JPG, etc).')
        argparser.add_argument(
            '--output',
            help='''Save the output video (.MP4) to a file rather than \
uploading it to Youtube.''')
        argparser.add_argument('--cs_json',
                               help='''Path to the client secrets json file \
(default: client_secrets.json).''',
                               default='client_secrets.json')
        argparser.add_argument(
            '--privacy',
            choices=self.valid_privacy_statuses,
            help='Privacy status of the video (default: unlisted).',
            default='unlisted')
        argparser.add_argument(
            '--category',
            default='10',
            help='''Numeric video category (see the Github wiki for a list; \
the default is 10, Music).''')
        argparser.add_argument(
            '--keywords',
            help='Comma-separated list of video keywords/tags.',
            default='')
        mxgroup = argparser.add_mutually_exclusive_group()
        mxgroup.add_argument(
            '--title',
            help='''Video title string (default: \'%s\'). If neither --title \
nor --title_vars is specified, --title_vars will be used with its default \
value, unless this would result in \
an empty title.''' % self.settings['default_title'])
        mxgroup.add_argument(
            '--title_vars',
            nargs='?',
            help='''Comma-separated list of metadata variables to use as \
the video title (default: %s).''' % self.settings['default_title_vars'])
        argparser.add_argument(
            '--title_sep',
            help='''Separator for the title variables (default: \' - \', \
yielding e.g. \'Artist - Title\'). Ignored if \
using --title_str.''',
            default=' - ')
        argparser.add_argument(
            '--description',
            nargs='?',
            help='Video description string (default: empty string).',
            default='')
        argparser.add_argument(
            '--add_metadata',
            help='''Adds a list of audio file metadata to the \
description (default: True).''',
            default=True)
        argparser.add_argument('-V',
                               '--version',
                               action='version',
                               version='%(prog)s ' +
                               self.settings['t2t_version'],
                               help='Show version number and exit.')
        mxgroup = argparser.add_mutually_exclusive_group()
        mxgroup.add_argument(
            '-v',
            '--verbose',
            action='store_true',
            help='Verbose mode (display ffmpeg/ffprobe output).')
        mxgroup.add_argument('-q',
                             '--quiet',
                             action='store_true',
                             help='Quiet mode.')
        argparser.add_argument('-h',
                               '--help',
                               action='help',
                               default=argparse.SUPPRESS,
                               help='Show this help message and exit.')

        self.tunetags = TuneTags()

    def get_authenticated_service(self, args):
        '''
        Get authenticated and cache the result.
        '''
        flow = flow_from_clientsecrets(
            self.settings['client_secrets_file'],
            scope=self.youtube_upload_scope,
            message=self.missing_client_secrets_message %
            ('tune2tube.py',
             os.path.abspath(
                 os.path.join(os.path.dirname(__file__),
                              self.settings['client_secrets_file']))))

        storage = Storage('%s-oauth2.json' % 'tune2tube.py')
        credentials = storage.get()
        if credentials is None or credentials.invalid \
           or self.settings['no_stored_auth']:
            credentials = run_flow(flow, storage, args)

        return build(self.youtube_api_service_name,
                     self.youtube_api_version,
                     http=credentials.authorize(httplib2.Http()))

    def initialize_upload(self, youtube, args, upfile):
        '''
        Begin a resumable video upload.
        '''
        tags = None

        if self.settings['keywords']:
            tags = self.settings['keywords'].split(',')

        # If we need to generate a dynamic title, do so now.
        if self.settings['dynamic_title']:
            title_vars = self.settings['title_vars'].split(',')
            items = [
                self.settings['metadata'][n] for n in title_vars
                if n in self.settings['metadata']
            ]
            title = self.settings['title_sep'].join(items)
        else:
            title = self.settings['title']

        if title == '':
            title = '(no title)'

        # Add the metadata tags to the description if needed.
        description = self.settings['description'].strip()
        if self.settings['add_metadata']:
            if description is not '':
                description += '\n'
            # Sort the list of metadata, so that items with linebreaks go last.
            metalist = [{
                key: self.settings['metadata'][key]
            } for key in self.settings['metadata']]
            metalist = sorted(metalist,
                              key=lambda x: '\n' in list(x.values())[0])
            for tag in metalist:
                for key in tag:
                    # Prevent pictures from being added to the description.
                    if 'APIC' in key:
                        continue
                    value = tag[key]
                    nice_key = self.tunetags.tag_lookup(key, True)
                    if '\n' in value:
                        description += '\n----\n%s: %s\n' % (nice_key, value)
                    else:
                        description += '\n%s: %s' % (nice_key, value)

        body = {
            'snippet': {
                'title': title,
                'description': description,
                'tags': tags,
                'categoryId': self.settings['category']
            },
            'status': {
                'privacyStatus': self.settings['privacy']
            }
        }

        # Call the API's videos.insert method to create and upload the video.
        insert_request = youtube.videos().insert(part=','.join(body.keys()),
                                                 body=body,
                                                 media_body=MediaFileUpload(
                                                     upfile,
                                                     chunksize=-1,
                                                     resumable=True))

        filesize = os.path.getsize(upfile)
        print('Uploading file... (filesize: %s)' % bytes_to_human(filesize))
        self.resumable_upload(insert_request)

    def resumable_upload(self, insert_request):
        '''
        This method implements an exponential backoff strategy to resume a
        failed upload.
        '''
        response = None
        error = None
        retry = 0
        while response is None:
            try:
                status, response = insert_request.next_chunk()
                if 'id' in response:
                    print('''Video ID `%s' was successfully uploaded. \
Its visibility is set to `%s'.''' % (response['id'], self.settings['privacy']))
                    print('''URL of the newly uploaded video: \
<https://www.youtube.com/watch?v=%s>''' % response['id'])
                    print('''It may take some time for the video to \
finish processing; typically 1-10 minutes.''')
                else:
                    error_exit('''The upload failed with an unexpected \
response: %s''' % response)
            except HttpError, e:
                if e.resp.status in self.retriable_status_codes:
                    error = '''A retriable HTTP error %d occurred:\n%s''' % (
                        e.resp.status, e.content)
                else:
                    raise
            except self.retriable_exceptions, e:
                error = 'A retriable error occurred: %s' % e

            if error is not None:
                print(error)
                retry += 1
                if retry > self.max_retries:
                    error_exit('''Too many upload errors. No longer \
attempting to retry.''')
                max_sleep = 2**retry
                sleep_seconds = random.random() * max_sleep
                print('''Sleeping %f seconds and then \
retrying...''' % sleep_seconds)
                time.sleep(sleep_seconds)
Exemplo n.º 4
0
class Tune2Tube(object):
    def __init__(self):
        self.settings = {
            # ffmpeg is a dependency for this script. ffprobe should be
            # installed along with ffmpeg.
            'path_ffmpeg': 'ffmpeg',
            'path_ffprobe': 'ffprobe',
            # Temporary output filename.
            'path_output': 'tmp.mp4',
            # Version number.
            't2t_version': '0.1',
            # Whether to display ffmpeg/ffprobe output.
            'verbose': False,
            # Whether to only generate the video file without uploading it.
            'generate_only': False,
            # Whether to forego the usage of stored oauth2 tokens.
            # If set to True, you will need to authenticate using your
            # browser each time you use the script.
            'no_stored_auth': False,
            # Default title to use in case the user's own title is
            # an empty string.
            'default_title': '(Empty title)',
            # Default variables to use for the dynamically generated title.
            'default_title_vars': 'artist,title',
            # Whether to use the dynamically generated title
            # from the file's metadata.
            'dynamic_title': True,
            'title': None,
            'title_vars': None
        }

        # Explicitly tell the underlying HTTP transport library not to retry,
        # since we are handling retry logic ourselves.
        httplib2.RETRIES = 1

        # Maximum number of times to retry before giving up.
        self.max_retries = 10

        # Always retry when these exceptions are raised.
        self.retriable_exceptions = (
            httplib2.HttpLib2Error, IOError, httplib.NotConnected,
            httplib.IncompleteRead, httplib.ImproperConnectionState,
            httplib.CannotSendRequest, httplib.CannotSendHeader,
            httplib.ResponseNotReady, httplib.BadStatusLine
        )

        # Always retry when an apiclient.errors.HttpError with one of these
        # status codes is raised.
        self.retriable_status_codes = [500, 502, 503, 504]

        # This OAuth 2.0 access scope allows an application to upload files to
        # the authenticated user's YouTube channel, but doesn't allow other
        # types of access.
        self.youtube_base = 'https://www.googleapis.com'
        self.youtube_upload_scope = self.youtube_base + '/auth/youtube.upload'
        self.youtube_api_service_name = 'youtube'
        self.youtube_api_version = 'v3'

        # We can set our uploaded video to one of these statuses.
        self.valid_privacy_statuses = ('public', 'private', 'unlisted')

        # This variable defines a message to display if
        # the client_secrets_file is missing.
        self.missing_client_secrets_message = '''
%s: Error: Please configure OAuth 2.0.

To make this script run you will need to populate the client_secrets.json file
found at:

   %s

with information from the Developers Console, which can be accessed
through <https://console.developers.google.com/>. See the README.md file
for more details.
'''

        # Set up our command line argument parser.
        # The argparser is initialized in oauth2client/tools.py. We're just
        # adding our own arguments to the ones already defined there.
        argparser.description = '''Generates a video from an image and audio \
file and uploads it to Youtube.'''
        argparser.epilog = '''A Youtube Data API client key is required to \
use this script, as well as ffmpeg. For help on setting up these \
dependencies, see this project\'s Github page \
<http://github.com/msikma/tune2tube/> or the included README.md file.'''
        argparser.add_help = True
        # Manually add a help argument,
        # as it is turned off in oauth2client/tools.py.
        argparser.add_argument(
            '--no_stored_auth',
            action='store_true',
            help='Forego using stored oauth2 tokens.'
        )
        argparser.add_argument(
            'audio_file',
            help='Audio file (MP3, OGG, FLAC, etc).'
        )
        argparser.add_argument(
            'image_file',
            help='Image file (PNG, JPG, etc).'
        )
        argparser.add_argument(
            '--output',
            help='''Save the output video (.MP4) to a file rather than \
uploading it to Youtube.'''
        )
        argparser.add_argument(
            '--cs_json',
            help='''Path to the client secrets json file \
(default: client_secrets.json).''',
            default='client_secrets.json'
        )
        argparser.add_argument(
            '--privacy',
            choices=self.valid_privacy_statuses,
            help='Privacy status of the video (default: unlisted).',
            default='unlisted'
        )
        argparser.add_argument(
            '--category',
            default='10',
            help='''Numeric video category (see the Github wiki for a list; \
the default is 10, Music).'''
        )
        argparser.add_argument(
            '--keywords',
            help='Comma-separated list of video keywords/tags.',
            default=''
        )
        mxgroup = argparser.add_mutually_exclusive_group()
        mxgroup.add_argument(
            '--title',
            help='''Video title string (default: \'%s\'). If neither --title \
nor --title_vars is specified, --title_vars will be used with its default \
value, unless this would result in \
an empty title.''' % self.settings['default_title']
        )
        mxgroup.add_argument(
            '--title_vars',
            nargs='?',
            help='''Comma-separated list of metadata variables to use as \
the video title (default: %s).''' % self.settings['default_title_vars']
        )
        argparser.add_argument(
            '--title_sep',
            help='''Separator for the title variables (default: \' - \', \
yielding e.g. \'Artist - Title\'). Ignored if \
using --title_str.''',
            default=' - '
        )
        argparser.add_argument(
            '--description',
            nargs='?',
            help='Video description string (default: empty string).',
            default=''
        )
        argparser.add_argument(
            '--add_metadata',
            action='store_true',
            help='''Adds a list of audio file metadata to the \
description (default: True).''',
            default=True
        )
        argparser.add_argument(
            '-V',
            '--version',
            action='version',
            version='%(prog)s ' + self.settings['t2t_version'],
            help='Show version number and exit.'
        )
        mxgroup = argparser.add_mutually_exclusive_group()
        mxgroup.add_argument(
            '-v',
            '--verbose',
            action='store_true',
            help='Verbose mode (display ffmpeg/ffprobe output).'
        )
        mxgroup.add_argument(
            '-q',
            '--quiet',
            action='store_true',
            help='Quiet mode.'
        )
        argparser.add_argument(
            '-h',
            '--help',
            action='help',
            default=argparse.SUPPRESS,
            help='Show this help message and exit.'
        )

        self.tunetags = TuneTags()

    def get_authenticated_service(self, args):
        '''
        Get authenticated and cache the result.
        '''
        flow = flow_from_clientsecrets(
            self.settings['client_secrets_file'],
            scope=self.youtube_upload_scope,
            message=self.missing_client_secrets_message % (
                'tune2tube.py',
                os.path.abspath(os.path.join(
                    os.path.dirname(__file__),
                    self.settings['client_secrets_file']
                ))
            )
        )

        storage = Storage('%s-oauth2.json' % 'tune2tube.py')
        credentials = storage.get()
        if credentials is None or credentials.invalid \
           or self.settings['no_stored_auth']:
            credentials = run_flow(flow, storage, args)

        return build(
            self.youtube_api_service_name,
            self.youtube_api_version,
            http=credentials.authorize(httplib2.Http())
        )

    def initialize_upload(self, youtube, args, upfile):
        '''
        Begin a resumable video upload.
        '''
        tags = None

        if self.settings['keywords']:
            tags = self.settings['keywords'].split(',')

        # If we need to generate a dynamic title, do so now.
        if self.settings['dynamic_title']:
            title_vars = self.settings['title_vars'].split(',')
            items = [self.settings['metadata'][n] for n in title_vars
                     if n in self.settings['metadata']]
            title = self.settings['title_sep'].join(items)
        else:
            title = self.settings['title']

        if title == '':
            title = '(no title)'

        # Add the metadata tags to the description if needed.
        description = self.settings['description'].strip()
        if self.settings['add_metadata']:
            if description is not '':
                description += '\n'
            # Sort the list of metadata, so that items with linebreaks go last.
            metalist = [{
                key: self.settings['metadata'][key]
            } for key in self.settings['metadata']]
            metalist = sorted(metalist, key=lambda x: '\n'
                              in list(x.values())[0])
            for tag in metalist:
                for key in tag:
                    value = tag[key]
                    nice_key = self.tunetags.tag_lookup(key, True)
                    if '\n' in value:
                        description += '\n----\n%s: %s\n' % (nice_key, value)
                    else:
                        description += '\n%s: %s' % (nice_key, value)

        body = {
            'snippet': {
                'title': title,
                'description': description,
                'tags': tags,
                'categoryId': self.settings['category']
            },
            'status': {
                'privacyStatus': self.settings['privacy']
            }
        }

        # Call the API's videos.insert method to create and upload the video.
        insert_request = youtube.videos().insert(
            part=','.join(body.keys()),
            body=body,
            media_body=MediaFileUpload(upfile, chunksize=-1, resumable=True)
        )

        filesize = os.path.getsize(upfile)
        print('Uploading file... (filesize: %s)' % bytes_to_human(filesize))
        self.resumable_upload(insert_request)

    def resumable_upload(self, insert_request):
        '''
        This method implements an exponential backoff strategy to resume a
        failed upload.
        '''
        response = None
        error = None
        retry = 0
        while response is None:
            try:
                status, response = insert_request.next_chunk()
                if 'id' in response:
                    print('''Video ID `%s' was successfully uploaded. \
Its visibility is set to `%s'.''' % (response['id'], self.settings['privacy']))
                    print('''URL of the newly uploaded video: \
<https://www.youtube.com/watch?v=%s>''' % response['id'])
                    print('''It may take some time for the video to \
finish processing; typically 1-10 minutes.''')
                else:
                    error_exit('''The upload failed with an unexpected \
response: %s''' % response)
            except HttpError, e:
                if e.resp.status in self.retriable_status_codes:
                    error = '''A retriable HTTP error %d occurred:\n%s''' % (
                        e.resp.status, e.content
                    )
                else:
                    raise
            except self.retriable_exceptions, e:
                error = 'A retriable error occurred: %s' % e

            if error is not None:
                print(error)
                retry += 1
                if retry > self.max_retries:
                    error_exit('''Too many upload errors. No longer \
attempting to retry.''')
                max_sleep = 2 ** retry
                sleep_seconds = random.random() * max_sleep
                print('''Sleeping %f seconds and then \
retrying...''' % sleep_seconds)
                time.sleep(sleep_seconds)