Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 277 lines (233 sloc) 9.63 KB
#!/usr/bin/env python3
# Adapted from https://github.com/youtube/api-samples/blob/master/python/upload_video.py
import http.client
import io
import mimetypes
import os
import random
import subprocess
import sys
import time
import googleapiclient.discovery
import googleapiclient.errors
import googleapiclient.http
import httplib2
import auth
import config
import mail
import playlists
from common import BIN, logger, mail_on_exception
from utils import ProgressBar
mail_on_exception()
# 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,
http.client.BadStatusLine,
http.client.CannotSendHeader,
http.client.CannotSendRequest,
http.client.ImproperConnectionState,
http.client.IncompleteRead,
http.client.NotConnected,
http.client.ResponseNotReady,
)
# Always retry when a googleapiclient.errors.HttpError with one of
# these status codes is raised.
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
# Remember to call <obj>.bar.done() when done with the file to finish
# the progress bar.
class FileReaderWithProgressBar(io.BufferedReader):
def __init__(self, file):
raw = io.FileIO(file)
buffering = io.DEFAULT_BUFFER_SIZE
stat = os.fstat(raw.fileno())
size = stat.st_size
try:
block_size = stat.st_blksize
except AttributeError:
pass
else:
if block_size > 1:
buffering = block_size
super().__init__(raw, buffering)
self.bar = ProgressBar(size)
def seek(self, pos, whence=0):
abspos = super().seek(pos, whence)
self.bar.update(abspos)
return abspos
def read(self, size=-1):
result = super().read(size)
self.bar.update(self.tell())
return result
# Remember to call <obj>.bar.done() when done with the upload to finish
# the progress bar.
class MediaFileUploadWithProgressBar(googleapiclient.http.MediaIoBaseUpload):
def __init__(self, file, mimetype=None, chunksize=googleapiclient.http.DEFAULT_CHUNK_SIZE,
resumable=False):
fp = FileReaderWithProgressBar(file)
self.bar = fp.bar
if mimetype is None:
mimetype, _ = mimetypes.guess_type(file)
# The uploader requires video/* or application/octet-stream
if mimetype is None or not mimetype.startswith('video/'):
mimetype = 'application/octet-stream'
super().__init__(fp, mimetype, chunksize=chunksize, resumable=resumable)
# https://developers.google.com/youtube/v3/docs/videos/insert
def initialize_upload(youtube_client, args):
# If the intended privacy is public and we wait for processing, set
# privacy to private at upload time, and only switch to public after
# the video is processed and ready to be published.
initial_privacy = args.privacy
if initial_privacy == 'public' and args.wait:
initial_privacy = 'private'
body = dict(
snippet=dict(
title=args.title,
description=args.description,
tags=args.tags.split(',') if args.tags else None,
categoryId=args.category,
defaultLanguage=args.language,
defaultAudioLanguage=args.language,
),
status=dict(
privacyStatus=initial_privacy,
publicStatsViewable=False,
),
)
# Call the API's videos.insert method to create and upload the video.
# https://developers.google.com/resources/api-libraries/documentation/youtube/v3/python/latest/youtube_v3.videos.html#insert
media = MediaFileUploadWithProgressBar(args.file, chunksize=-1, resumable=True)
insert_request = youtube_client.videos().insert(
part=','.join(body.keys()),
body=body,
# chunksize=-1 (stream in a single request) is most efficient.
# https://google.github.io/google-api-python-client/docs/epy/googleapiclient.http.MediaFileUpload-class.html
media_body=media,
)
video_id = resumable_upload(insert_request, bar=media.bar)
if config.main.notifications:
mail.send_mail(f'Uploaded {args.title}', f'https://youtu.be/{video_id}',
config.main.mailto)
logger.info(f'Sent notification to {config.main.mailto}')
if args.thumbnail:
logger.info('Setting thumbnail...')
subprocess.run([BIN / 'set-thumbnail', '--', video_id, args.thumbnail])
if args.wait:
while True:
list_response = youtube_client.videos().list(part='status', id=video_id).execute()
status_dict = list_response['items'][0]['status']
upload_status = status_dict['uploadStatus']
if upload_status == 'processed':
logger.info(f'{video_id} has been processed')
if config.main.notifications:
mail.send_mail(f'Processed {args.title}', f'https://youtu.be/{video_id}',
config.main.mailto)
logger.info(f'Sent notification to {config.main.mailto}')
break
elif upload_status == 'uploaded':
logger.info(f'{video_id} is being processed')
time.sleep(15)
continue
else:
if upload_status == 'deleted':
logger.info(f'{video_id} has been deleted')
if upload_status == 'failed':
logger.info(f'Upload of {video_id} failed: {status_dict["failureReason"]}')
if upload_status == 'rejected':
logger.info(f'{video_id} has been rejected: {status_dict["rejectionReason"]}')
sys.exit(1)
if args.privacy == 'public':
subprocess.check_call([BIN / 'update-metadata', 'publish', '--', video_id])
if args.playlists:
logger.info('Inserting into playlists...')
playlist_names = args.playlists.split(',')
playlist_ids = []
for name in playlist_names:
playlist_id = playlists.name2id(name)
if playlist_id:
playlist_ids.append(playlist_id)
# update-metadata insert-playlist [-h] video_ids playlist_ids
subprocess.check_call([BIN / 'update-metadata', 'insert-playlist', '--',
video_id, ','.join(playlist_ids)])
# This method implements an exponential backoff strategy to resume a
# failed upload.
def resumable_upload(insert_request, bar=None):
response = None
error = None
retry = 0
logger.info('Uploading file...')
if bar:
bar.activate()
while response is None:
try:
_, response = insert_request.next_chunk()
if response is not None:
if 'id' in response:
video_id = response['id']
if bar:
bar.done()
logger.info(f"Video id '{video_id}' was successfully uploaded.")
return video_id
else:
if bar:
bar.done()
exit(f'The upload failed with an unexpected response: {response}')
except googleapiclient.errors.HttpError as e:
if e.resp.status in RETRIABLE_STATUS_CODES:
error = f'A retriable HTTP error {e.resp.status} occurred:\n{e.content}'
else:
raise
except RETRIABLE_EXCEPTIONS as e:
error = f'A retriable error occurred: {e}'
if error is not None:
if bar:
bar.done()
logger.error(error)
retry += 1
if retry > MAX_RETRIES:
exit('No longer attempting to retry.')
max_sleep = 2 ** retry
sleep_seconds = random.random() * max_sleep
logger.info(f'Sleeping {sleep_seconds} seconds and then retrying...')
time.sleep(sleep_seconds)
if bar:
bar.activate()
def main():
parser = auth.ArgumentParser()
add = parser.add_argument
add('file',
help='video file to upload')
add('-t', '--title', default=None,
help='video title (default is the filename)')
add('-d', '--description', default='',
help='video description')
add('--tags', default='SNH48',
help='comma-delimited list of video tags; default is SNH48')
add('-c', '--category', default=24,
help='mumeric video category code (https://git.io/vSqOz); default is 24 for Entertainment')
add('-p', '--privacy', choices=VALID_PRIVACY_STATUSES, default='unlisted',
help='video privacy status; default is unlisted')
add('-l', '--language', default='zh-CN',
help="""language of the video's title, description, and default audio track;
default is 'zh-CN'""")
add('--thumbnail', default=None,
help='path to the custom thumbnail for the video')
add('--playlists', default=None,
help='comma-delimited list of names of playlists to insert into')
add('--wait', action='store_true',
help='wait for video processing to finish')
args = parser.parse_args()
if not os.path.isfile(args.file):
exit(f'Error: {args.file} not found.')
if args.title is None:
args.title = os.path.basename(os.path.splitext(args.file)[0])
youtube_client = auth.get_youtube_client(args, 'youtube' if args.wait else 'youtube.upload')
initialize_upload(youtube_client, args)
if __name__ == '__main__':
main()
You can’t perform that action at this time.