Uploading and Claiming a Video

Note: The YouTube Content ID API is intended for use by YouTube content partners and is not accessible to all developers or to all YouTube users. If you do not see the YouTube Content ID API as one of the services listed in the Google API Console , see the YouTube Help Center to learn more about the YouTube Partner Program.

This code sample demonstrates how to upload a YouTube video and apply a monetization policy to it. To monetize a video, you must claim the video with an asset in the YouTube rights management system. The sample uploads the video, creates a new asset, claims the video using the asset, and applies a monetization policy to the video.

This example is presented as the series of steps involved along with the relevant sections of the code. You can find the entire script at the end of this page. The code is written in Python. Client libraries for other popular programming languages are also available.

Requirements

Step 1: Common housekeeping functions

The first sections of the code sample perform basic housekeeping functions that are common to many scripts: parse the command line, authenticate the user, and obtain the necessary API services.

Parse the command line

The parse_options method uses OptionParser from the Python client library to create an options object that contains each command-line argument as a property. Subsequent methods retrieve values from the options object as necessary.

The sample script's command-line arguments are listed below. The first two ( file and channelId ) are required; the rest are optional.

  • file : The name and location of the video file to upload.

    Example: --file="/home/path/to/file.mov"
    
  • channelId : The YouTube channel to which you want to upload the video. The channel must be managed by the YouTube Content Manager account of the authenticated user. You can retrieve the channel ID in the YouTube account settings for the authenticated user or by using the channels.list method.

    Example: --channelId="UC_x5XG1OV2P6uZZ5FSM9Ttw"
    
  • title : A title for the video that you're uploading. The default value is Test title .

    Example: --title="Summer vacation in California"
    
  • description : A description of the video that you're uploading. The default value is Test description .

    Example: --description="Had a great time surfing in Santa Cruz"
    
  • category : The category ID for the YouTube video category associated with the video. The default value is 22 , which refers to the People & Blogs category.

    Example: --category=22
    
  • keywords : A comma-separated list of keywords associated with the video. The default value is an empty string.

    Example: --keywords="surfing, beach volleyball"
    
  • privacyStatus : The privacy status of the video. The default behavior is for an uploaded video to be publicly visible ( public ). When uploading test videos, you may want to specify a --privacyStatus argument value to ensure that those videos are private or unlisted. Valid values are public , private , and unlisted .

    Example: --privacyStatus="private"
    
  • policyId : The monetization policy to apply to the uploaded video. The policy must be associated with the YouTube Content Manager account of the authenticated user. The default is the standard YouTube "monetize" policy.

    Example: --policyId="S309961703555739"
    
def parse_options():
  parser = OptionParser()
  parser.add_option("--file", dest="file", help="Video file to upload")
  parser.add_option("--title", dest="title", help="Video title",
    default="Test Title")
  parser.add_option("--description", dest="description",
    help="Video description",
    default="Test Description")
  parser.add_option("--category", dest="category",
    help="Numeric video category. " +
      "See https://developers.google.com/youtube/v3/docs/videoCategories/list",
    default="22")
  parser.add_option("--keywords", dest="keywords",
    help="Video keywords, comma separated", default="")
  parser.add_option("--privacyStatus", dest="privacyStatus",
    help="Video privacy status: public, private or unlisted",
    default="public")
  parser.add_option("--policyId", dest="policyId",
    help="Optional id of a saved claim policy")
  parser.add_option("--channelId", dest="channelId",
    help="Id of the channel to upload to. Must be managed by your CMS account")
  (options, args) = parser.parse_args()

  return options

Authorize the request

In this step, we incorporate OAuth 2.0 authorization into the script. This enables the user running the script to authorize the script to perform API requests attributed to the user's account.

Create a client_secrets.json file

The type of authorization shown in the sample requires a client_secrets.json file, which contains information from the Google API Console , to perform authorization. You also need to register your application . For a more complete explanation of how authorization works see the authorization guide . Note that this sample requires both the YouTube Data API V3 and the YouTube Content ID API service to be configured in the API Console for your project.

 {
  "web": {
    "client_id": "
INSERT CLIENT ID HERE
",
    "client_secret": "
INSERT CLIENT SECRET HERE
",
    "redirect_uris": [],
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://accounts.google.com/o/oauth2/token"
  }
}

Authorization code in the script

The script includes these import statements to enable user authentication and authorization:

from oauth2client.file import Storage
from oauth2client.client import flow_from_clientsecrets
from oauth2client.tools import run

Next, the get_authenticated_services method creates a FLOW object using the data from the client_secrets.json file configured in the previous step. If the user authorizes our application to submit API requests on the user's behalf, the resulting credentials are stored in a Storage object for later use. The user needs to reauthorize our application if the credentials expire.

YOUTUBE_SCOPES = (
  # An OAuth 2 access scope that allows for full read/write access.
  "https://www.googleapis.com/auth/youtube",
  # A scope that grants access to YouTube Partner API functionality.
  "https://www.googleapis.com/auth/youtubepartner")

flow = flow_from_clientsecrets(
  CLIENT_SECRETS_FILE,
  scope=" ".join(YOUTUBE_SCOPES),
  message=MISSING_CLIENT_SECRETS_MESSAGE
)

storage = Storage(CACHED_CREDENTIALS_FILE)
credentials = storage.get()

if credentials is None or credentials.invalid:
  credentials = run(flow, storage)

Obtain services

After successful authorization we obtain the necessary services for the operations we want to perform. The sample uses the YouTube Data API to upload the video and the YouTube Content ID API to create the asset and claim the video. We create separate services to provide authorized access to the functionality of the two APIs.

from googleapiclient.discovery import build
import httplib2

YOUTUBE_API_SERVICE_NAME = "youtube"
YOUTUBE_API_VERSION = "v3"
YOUTUBE_CONTENT_ID_API_SERVICE_NAME = "youtubePartner"
YOUTUBE_CONTENT_ID_API_VERSION = "v1"

youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
  http=credentials.authorize(httplib2.Http()))

youtube_partner = build(YOUTUBE_CONTENT_ID_API_SERVICE_NAME,
  YOUTUBE_CONTENT_ID_API_VERSION, http=credentials.authorize(httplib2.Http()),
  static_discovery=False)

return (youtube, youtube_partner)

Step 2: Identify the content owner

To create assets and make claims, the authenticated user must have a YouTube Content Manager account. The Content Manager account holds the rights management objects for one or more content owners . The content owner is the copyright holder who has the right to decide whether to monetize, track, or block a video.

The get_content_owner method retrieves the ID of the content owner in the authenticated user's Content Manager account. Most accounts have a single content owner (the authenticated user), but if the account has multiple content owners, the method returns the first.

def get_content_owner_id(youtube_partner):
  try:
    content_owners_list_response = youtube_partner.contentOwners().list(
      fetchMine=True
    ).execute()
  except HttpError, e:
    if INVALID_CREDENTIALS in e.content:
      logging.error("The request is not authorized by a Google Account that "
        "is linked to a YouTube content owner. Please delete '%s' and "
        "re-authenticate with a YouTube content owner account." %
        CACHED_CREDENTIALS_FILE)
      exit(1)
    else:
      raise

  # This returns the CMS user id of the first entry returned
  # by youtubePartner.contentOwners.list()
  # See https://developers.google.com/youtube/partner/reference/rest/v1/contentOwners/list
  # Normally this is what you want, but if you authorize with a Google Account
  # that has access to multiple YouTube content owner accounts, you need to
  # iterate through the results.
  return content_owners_list_response["items"][0]["id"]

Step 3: Upload the video

To upload a video, we build a partial JSON resource representing the video and pass it to the videos.insert method. We set the video metadata using values from the options object created when we parsed the command line . For the media file itself, we use MediaFileUpload to be able to use resumable uploading. See Uploading a video for further details.

The upload method returns the video ID for the new video, and the script needs to pass that value to other methods in later steps.

def upload(youtube, content_owner_id, options):
  if options.keywords:
    tags = options.keywords.split(",")
  else:
    tags = None

  insert_request = youtube.videos().insert(
    onBehalfOfContentOwner=content_owner_id,
    onBehalfOfContentOwnerChannel=options.channelId,
    part="snippet,status",
    body=dict(
      snippet=dict(
        title=options.title,
        description=options.description,
        tags=tags,
        categoryId=options.category
      ),
      status=dict(
        privacyStatus=options.privacyStatus
      )
    ),
    # chunksize=-1 means that the entire file will be uploaded in a single
    # HTTP request. (If the upload fails, it will still be retried where it
    # left off.) This is usually a best practice, but if you're using Python
    # older than 2.6 or if you're running on App Engine, you should set the
    # chunksize to something like 1024 * 1024 (1 megabyte).
    media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True)
  )

  response = None
  error = None
  retry = 0
  duration_seconds=0
  while response is None:
    try:
      logging.debug("Uploading file...")

      start_seconds = time.time()
      status, response = insert_request.next_chunk()
      delta_seconds = time.time() - start_seconds
      duration_seconds += delta_seconds

      if "id" in response:
        return (response["id"], duration_seconds)
      else:
        logging.error("The upload failed with an unexpected response: %s" %
          response)
        exit(1)
    except HttpError, e:
      if e.resp.status in RETRIABLE_STATUS_CODES:
        error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status,
                                                             e.content)
      else:
        raise
    except RETRIABLE_EXCEPTIONS, e:
      error = "A retriable error occurred: %s" % e

    if error is not None:
      logging.error(error)
      retry += 1
      if retry > MAX_RETRIES:
        logging.error("No longer attempting to retry.")
        exit(1)

      max_sleep = 2 ** retry
      sleep_seconds = random.random() * max_sleep
      logging.debug("Sleeping %f seconds and then retrying..." % sleep_seconds)
      time.sleep(sleep_seconds)

Step 4: Create an asset

To monetize a YouTube video, you first have to associate it with an asset. The create_asset method creates a new asset for the newly uploaded video.

Just like we did for the video, we build a partial JSON resource that identifies the type of asset to create (a web video) and provides a title and description for the new asset. We pass the JSON resource to the assets.insert method, which creates the asset and returns its unique ID. Again, the script needs to pass that value to other methods in later steps.

def create_asset(youtube_partner, content_owner_id, title, description):
  # This creates a new asset corresponding to a video on the web.
  # The asset is linked to the corresponding YouTube video via a
  # claim that will be created later.
  body = dict(
    type="web",
    metadata=dict(
      title=title,
      description=description
    )
  )

  assets_insert_response = youtube_partner.assets().insert(
    onBehalfOfContentOwner=content_owner_id,
    body=body
  ).execute()

  return assets_insert_response["id"]

Step 5: Update the ownership

Before you can monetize a video, YouTube must know who owns the associated asset. So, with the asset created, we now configure the asset's ownership . In the sample, we specify that the content owner has worldwide ownership of the asset.

  def set_asset_ownership(youtube_partner, content_owner_id, asset_id):
  # This specifies that content_owner_id owns 100% of the asset worldwide.
  # Adjust as needed.
  body = dict(
    general=[dict(
      owner=content_owner_id,
      ratio=100,
      type="exclude",
      territories=[]
    )]
  )

  youtube_partner.ownership().update(
    onBehalfOfContentOwner=content_owner_id,
    assetId=asset_id,
    body=body
  ).execute()

Step 6: Claim the video

The next step is to associate the uploaded video with its corresponding asset by claiming the video. The claim provides the link between the video and the YouTube rights management system, which establishes ownership of the video and enables the owner to set a monetization policy.

The claim_video method claims audiovisual rights. If you include the policyId parameter on the command line, the method applies the specified policy to the video; if you don't include the parameter, the method applies the standard "monetize" policy.

def claim_video(youtube_partner, content_owner_id, asset_id, video_id,
  policy_id):
  # policy_id can be set to the id of an existing policy, which can be obtained
  # via youtubePartner.policies.list()
  # See https://developers.google.com/youtube/partner/reference/rest/v1/policies/list
  # If you later update that existing policy, the claim will also be updated.
  if policy_id:
    policy = dict(
      id=policy_id
    )
  # If policy_id is not provided, a new inline policy is created.
  else:
    policy = dict(
      rules=[dict(
        action="monetize"
      )]
    )

  body = dict(
    assetId=asset_id,
    videoId=video_id,
    policy=policy,
    contentType="audiovisual"
  )

  youtube_partner.claims().insert(
    onBehalfOfContentOwner=content_owner_id,
    body=body
  ).execute()

Step 7: Set the advertising options

We've claimed the video and applied a monetization policy to it. The final step is to specify what type of advertisements to show in the video. Whenever the "monetize" policy applies, YouTube checks the advertising options and shows the highest-revenue type of advertisements available.

The sample tells YouTube to show in-stream TrueView advertisements with this video.

def set_advertising_options(youtube_partner, content_owner_id, video_id):
  # This enables the TrueView ad format for the given video.
  # Adjust as needed.
  body = dict(
    adFormats=["trueview_instream"]
  )

  youtube_partner.videoAdvertisingOptions().update(
    videoId=video_id,
    onBehalfOfContentOwner=content_owner_id,
    body=body
  ).execute()

Complete code sample

The complete working sample upload_monetize_video_example.py is listed below:

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Simple command-line sample for Youtube Partner API.

Command-line application that creates an asset, uploads and claims a video for that asset.

Usage:
  $ python upload_monetize_video_example.py --file=VIDEO_FILE --channelID=CHANNEL_ID \
      [--title=VIDEO_TITLE] [--description=VIDEO_DESCRIPTION] [--category=CATEGORY_ID] \
      [--keywords=KEYWORDS] [--privacyStatus=PRIVACY_STATUS] [--policyId=POLICY_ID] 

You can also get help on all the command-line flags the program understands
by running:

  $ python upload_monetize_video_example.py --help
"""

__author__ = 'jeffy+pub@google.com (Jeffrey Posnick)'

import httplib
import httplib2
import logging
import os
import random
import sys
import time

from apiclient.discovery import build
from apiclient.errors import HttpError
from apiclient.http import MediaFileUpload
from oauth2client.file import Storage
from oauth2client.client import flow_from_clientsecrets
from oauth2client.tools import run
from optparse import OptionParser


# 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.
MAX_RETRIES = 10

# Always retry when these exceptions are raised.
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.
RETRIABLE_STATUS_CODES = (500, 502, 503, 504,)

# The message associated with the HTTP 401 error that's returned when a request
# is authorized by a user whose account is not associated with a YouTube
# content owner.
INVALID_CREDENTIALS = "Invalid Credentials"

# The CLIENT_SECRETS_FILE variable specifies the name of a file that contains
# the OAuth 2.0 information for this application, including its client_id and
# client_secret. You can acquire an OAuth 2.0 client ID and client secret from
# the Google API Console at
# https://console.cloud.google.com/.
# See the "Registering your application" instructions for an explanation
# of how to find these values:
# https://developers.google.com/youtube/partner/guides/registering_an_application
# For more information about using OAuth2 to access Google APIs, please visit:
#   https://developers.google.com/accounts/docs/OAuth2
# For more information about the client_secrets.json file format, please visit:
#   https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
CLIENT_SECRETS_FILE = "client_secrets.json"

# The local file used to store the cached OAuth 2 credentials after going
# through a one-time browser-based login.
CACHED_CREDENTIALS_FILE = "%s-oauth2.json" % sys.argv[0]

YOUTUBE_SCOPES = (
  # An OAuth 2 access scope that allows for full read/write access.
  "https://www.googleapis.com/auth/youtube",
  # A scope that grants access to YouTube Partner API functionality.
  "https://www.googleapis.com/auth/youtubepartner",)
YOUTUBE_API_SERVICE_NAME = "youtube"
YOUTUBE_API_VERSION = "v3"
YOUTUBE_CONTENT_ID_API_SERVICE_NAME = "youtubePartner"
YOUTUBE_CONTENT_ID_API_VERSION = "v1"

# Helpful message to display if the CLIENT_SECRETS_FILE is missing.
MISSING_CLIENT_SECRETS_MESSAGE = """
WARNING: Please configure OAuth 2.0

To make this sample run you need to populate the client_secrets.json file at:

   %s

with information from the API Console
https://console.cloud.google.com/

For more information about the client_secrets.json file format, please visit:
https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
""" % os.path.abspath(os.path.join(os.path.dirname(__file__),
                                   CLIENT_SECRETS_FILE))

def parse_options():
  parser = OptionParser()
  parser.add_option("--file", dest="file", help="Video file to upload")
  parser.add_option("--title", dest="title", help="Video title",
    default="Test Title")
  parser.add_option("--description", dest="description",
    help="Video description",
    default="Test Description")
  parser.add_option("--category", dest="category",
    help="Numeric video category. " +
      "See https://developers.google.com/youtube/v3/docs/videoCategories/list",
    default="22")
  parser.add_option("--keywords", dest="keywords",
    help="Video keywords, comma separated", default="")
  parser.add_option("--privacyStatus", dest="privacyStatus",
    help="Video privacy status: public, private or unlisted",
    default="public")
  parser.add_option("--policyId", dest="policyId",
    help="Optional id of a saved claim policy")
  parser.add_option("--channelId", dest="channelId",
    help="Id of the channel to upload to. Must be managed by your CMS account")
  (options, args) = parser.parse_args()

  return options

def get_authenticated_services():
  flow = flow_from_clientsecrets(
    CLIENT_SECRETS_FILE,
    scope=" ".join(YOUTUBE_SCOPES),
    message=MISSING_CLIENT_SECRETS_MESSAGE
  )

  storage = Storage(CACHED_CREDENTIALS_FILE)
  credentials = storage.get()

  if credentials is None or credentials.invalid:
    credentials = run(flow, storage)

  youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
    http=credentials.authorize(httplib2.Http()))

  youtube_partner = build(YOUTUBE_CONTENT_ID_API_SERVICE_NAME,
    YOUTUBE_CONTENT_ID_API_VERSION, http=credentials.authorize(httplib2.Http()),
    static_discovery=False)

  return (youtube, youtube_partner)

def get_content_owner_id(youtube_partner):
  try:
    content_owners_list_response = youtube_partner.contentOwners().list(
      fetchMine=True
    ).execute()
  except HttpError, e:
    if INVALID_CREDENTIALS in e.content:
      logging.error("Your request is not authorized by a Google Account that "
        "is associated with a YouTube content owner. Please delete '%s' and "
        "re-authenticate with an account that is associated "
        "with a content owner." % CACHED_CREDENTIALS_FILE)
      exit(1)
    else:
      raise

  # This returns the CMS user id of the first entry returned
  # by youtubePartner.contentOwners.list()
  # See https://developers.google.com/youtube/partner/reference/rest/v1/contentOwners/list
  # Normally this is what you want, but if you authorize with a Google Account
  # that has access to multiple YouTube content owner accounts, you need to
  # iterate through the results.
  return content_owners_list_response["items"][0]["id"]

def upload(youtube, content_owner_id, options):
  if options.keywords:
    tags = options.keywords.split(",")
  else:
    tags = None

  insert_request = youtube.videos().insert(
    onBehalfOfContentOwner=content_owner_id,
    onBehalfOfContentOwnerChannel=options.channelId,
    part="snippet,status",
    body=dict(
      snippet=dict(
        title=options.title,
        description=options.description,
        tags=tags,
        categoryId=options.category
      ),
      status=dict(
        privacyStatus=options.privacyStatus
      )
    ),
    # chunksize=-1 means that the entire file will be uploaded in a single
    # HTTP request. (If the upload fails, it will still be retried where it
    # left off.) This is usually a best practice, but if you're using Python
    # older than 2.6 or if you're running on App Engine, you should set the
    # chunksize to something like 1024 * 1024 (1 megabyte).
    media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True)
  )

  response = None
  error = None
  retry = 0
  duration_seconds=0
  while response is None:
    try:
      logging.debug("Uploading file...")

      start_seconds = time.time()
      status, response = insert_request.next_chunk()
      delta_seconds = time.time() - start_seconds
      duration_seconds += delta_seconds

      if "id" in response:
        return (response["id"], duration_seconds)
      else:
        logging.error("The upload failed with an unexpected response: %s" %
          response)
        exit(1)
    except HttpError, e:
      if e.resp.status in RETRIABLE_STATUS_CODES:
        error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status,
                                                             e.content)
      else:
        raise
    except RETRIABLE_EXCEPTIONS, e:
      error = "A retriable error occurred: %s" % e

    if error is not None:
      logging.error(error)
      retry += 1
      if retry > MAX_RETRIES:
        logging.error("No longer attempting to retry.")
        exit(1)

      max_sleep = 2 ** retry
      sleep_seconds = random.random() * max_sleep
      logging.debug("Sleeping %f seconds and then retrying..." % sleep_seconds)
      time.sleep(sleep_seconds)

def create_asset(youtube_partner, content_owner_id, title, description):
  # This creates a new asset corresponding to a video on the web.
  # The asset is linked to the corresponding YouTube video via a
  # claim that will be created later.
  body = dict(
    type="web",
    metadata=dict(
      title=title,
      description=description
    )
  )

  assets_insert_response = youtube_partner.assets().insert(
    onBehalfOfContentOwner=content_owner_id,
    body=body
  ).execute()

  return assets_insert_response["id"]

def set_asset_ownership(youtube_partner, content_owner_id, asset_id):
  # This specifies that content_owner_id owns 100% of the asset worldwide.
  # Adjust as needed.
  body = dict(
    general=[dict(
      owner=content_owner_id,
      ratio=100,
      type="exclude",
      territories=[]
    )]
  )

  youtube_partner.ownership().update(
    onBehalfOfContentOwner=content_owner_id,
    assetId=asset_id,
    body=body
  ).execute()

def claim_video(youtube_partner, content_owner_id, asset_id, video_id,
  policy_id):
  # policy_id can be set to the id of an existing policy, which can be obtained
  # via youtubePartner.policies.list()
  # See https://developers.google.com/youtube/partner/reference/rest/v1/policies/list
  # If you later update that existing policy, the claim will also be updated.
  if policy_id:
    policy = dict(
      id=policy_id
    )
  # If policy_id is not provided, a new inline policy is created.
  else:
    policy = dict(
      rules=[dict(
        action="monetize"
      )]
    )

  body = dict(
    assetId=asset_id,
    videoId=video_id,
    policy=policy,
    contentType="audiovisual"
  )

  claims_insert_response = youtube_partner.claims().insert(
    onBehalfOfContentOwner=content_owner_id,
    body=body
  ).execute()

  return claims_insert_response["id"]

def set_advertising_options(youtube_partner, content_owner_id, video_id):
  # This enables the true view ad format for the given video.
  # Adjust as needed.
  body = dict(
    adFormats=["trueview_instream"]
  )

  youtube_partner.videoAdvertisingOptions().update(
    videoId=video_id,
    onBehalfOfContentOwner=content_owner_id,
    body=body
  ).execute()


if __name__ == '__main__':
  logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
  )

  options = parse_options()

  if options.file is None or not os.path.exists(options.file):
    logging.error("Please specify a valid file using the --file= parameter.")
    exit(1)

  # The channel ID looks something like "UC..." and needs to correspond to a
  # channel managed by the YouTube content owner authorizing the request.
  # youtube.channels.list(part="snippet", managedByMe=true,
  #                       onBehalfOfContentOwner=*CONTENT_OWNER_ID*)
  # can be used to retrieve a list of managed channels and their channel IDs.
  # See https://developers.google.com/youtube/v3/docs/channels/list
  if options.channelId is None:
    logging.error("Please specify a channel ID via the --channelId= parameter.")
    exit(1)

  (youtube, youtube_partner) = get_authenticated_services()

  content_owner_id = get_content_owner_id(youtube_partner)
  logging.info("Authorized by content owner ID '%s'." % content_owner_id)

  (video_id, duration_seconds) = upload(youtube, content_owner_id, options)
  logging.info("Successfully uploaded video ID '%s'." % video_id)

  file_size_bytes = os.path.getsize(options.file)
  logging.debug("Uploaded %d bytes in %0.2f seconds (%0.2f megabytes/second)." %
    (file_size_bytes, duration_seconds,
      (file_size_bytes / (1024 * 1024)) / duration_seconds))

  asset_id = create_asset(youtube_partner, content_owner_id,
    options.title, options.description)
  logging.info("Created new asset ID '%s'." % asset_id)

  set_asset_ownership(youtube_partner, content_owner_id, asset_id)
  logging.info("Successfully set asset ownership.")

  claim_id = claim_video(youtube_partner, content_owner_id, asset_id,
    video_id, options.policyId)
  logging.info("Successfully claimed video.")

  set_advertising_options(youtube_partner, content_owner_id, video_id)
  logging.info("Successfully set advertising options.")

  logging.info("All done!")