Scripting way to upload videos to peertube and youtube
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

302 lines
11 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
  1. #!/usr/bin/env python2
  2. # coding: utf-8
  3. # From Youtube samples : https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa
  4. import httplib
  5. import httplib2
  6. import random
  7. import time
  8. import copy
  9. import json
  10. from os.path import splitext, basename, exists
  11. import google.oauth2.credentials
  12. import datetime
  13. import pytz
  14. import logging
  15. from tzlocal import get_localzone
  16. from googleapiclient.discovery import build
  17. from googleapiclient.errors import HttpError
  18. from googleapiclient.http import MediaFileUpload
  19. from google_auth_oauthlib.flow import InstalledAppFlow
  20. import utils
  21. logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)
  22. # Explicitly tell the underlying HTTP transport library not to retry, since
  23. # we are handling retry logic ourselves.
  24. httplib2.RETRIES = 1
  25. # Maximum number of times to retry before giving up.
  26. MAX_RETRIES = 10
  27. # Youtube retriables cases
  28. RETRIABLE_EXCEPTIONS = (
  29. IOError,
  30. httplib2.HttpLib2Error,
  31. httplib.NotConnected,
  32. httplib.IncompleteRead,
  33. httplib.ImproperConnectionState,
  34. httplib.CannotSendRequest,
  35. httplib.CannotSendHeader,
  36. httplib.ResponseNotReady,
  37. httplib.BadStatusLine,
  38. )
  39. RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
  40. CLIENT_SECRETS_FILE = 'youtube_secret.json'
  41. CREDENTIALS_PATH = ".youtube_credentials.json"
  42. SCOPES = ['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl']
  43. API_SERVICE_NAME = 'youtube'
  44. API_VERSION = 'v3'
  45. # Authorize the request and store authorization credentials.
  46. def get_authenticated_service():
  47. check_authenticated_scopes()
  48. flow = InstalledAppFlow.from_client_secrets_file(
  49. CLIENT_SECRETS_FILE, SCOPES)
  50. if exists(CREDENTIALS_PATH):
  51. with open(CREDENTIALS_PATH, 'r') as f:
  52. credential_params = json.load(f)
  53. credentials = google.oauth2.credentials.Credentials(
  54. credential_params["token"],
  55. refresh_token=credential_params["_refresh_token"],
  56. token_uri=credential_params["_token_uri"],
  57. client_id=credential_params["_client_id"],
  58. client_secret=credential_params["_client_secret"]
  59. )
  60. else:
  61. credentials = flow.run_local_server()
  62. with open(CREDENTIALS_PATH, 'w') as f:
  63. p = copy.deepcopy(vars(credentials))
  64. del p["expiry"]
  65. json.dump(p, f)
  66. return build(API_SERVICE_NAME, API_VERSION, credentials=credentials, cache_discovery=False)
  67. def initialize_upload(youtube, options):
  68. path = options.get('--file')
  69. tags = None
  70. if options.get('--tags'):
  71. tags = options.get('--tags').split(',')
  72. category = None
  73. if options.get('--category'):
  74. category = utils.getCategory(options.get('--category'), 'youtube')
  75. language = None
  76. if options.get('--language'):
  77. language = utils.getLanguage(options.get('--language'), "youtube")
  78. license = None
  79. if options.get('--cca'):
  80. license = "creativeCommon"
  81. body = {
  82. "snippet": {
  83. "title": options.get('--name') or splitext(basename(path))[0],
  84. "description": options.get('--description') or "default description",
  85. "tags": tags,
  86. # if no category, set default to 1 (Films)
  87. "categoryId": str(category or 1),
  88. "defaultAudioLanguage": str(language or 'en')
  89. },
  90. "status": {
  91. "privacyStatus": str(options.get('--privacy') or "private"),
  92. "license": str(license or "youtube"),
  93. }
  94. }
  95. if options.get('--publishAt'):
  96. # Youtube needs microsecond and the local timezone from ISO 8601
  97. publishAt = options.get('--publishAt') + ".000001"
  98. publishAt = datetime.datetime.strptime(publishAt, '%Y-%m-%dT%H:%M:%S.%f')
  99. tz = get_localzone()
  100. tz = pytz.timezone(str(tz))
  101. publishAt = tz.localize(publishAt).isoformat()
  102. body['status']['publishAt'] = str(publishAt)
  103. if options.get('--playlist'):
  104. playlist_id = get_playlist_by_name(youtube, options.get('--playlist'))
  105. if not playlist_id and options.get('--playlistCreate'):
  106. playlist_id = create_playlist(youtube, options.get('--playlist'))
  107. elif not playlist_id:
  108. logging.warning("Youtube: Playlist `" + options.get('--playlist') + "` is unknown.")
  109. logging.warning("If you want to create it, set the --playlistCreate option.")
  110. playlist_id = ""
  111. else:
  112. playlist_id = ""
  113. # Call the API's videos.insert method to create and upload the video.
  114. insert_request = youtube.videos().insert(
  115. part=','.join(body.keys()),
  116. body=body,
  117. media_body=MediaFileUpload(path, chunksize=-1, resumable=True)
  118. )
  119. video_id = resumable_upload(insert_request, 'video', 'insert')
  120. # If we get a video_id, upload is successful and we are able to set thumbnail
  121. if video_id and options.get('--thumbnail'):
  122. set_thumbnail(youtube, options.get('--thumbnail'), videoId=video_id)
  123. # If we get a video_id, upload is successful and we are able to set playlist
  124. if video_id and options.get('--playlist'):
  125. set_playlist(youtube, playlist_id, video_id)
  126. def get_playlist_by_name(youtube, playlist_name):
  127. response = youtube.playlists().list(
  128. part='snippet,id',
  129. mine=True,
  130. maxResults=50
  131. ).execute()
  132. for playlist in response["items"]:
  133. if playlist["snippet"]['title'] == playlist_name:
  134. return playlist['id']
  135. def create_playlist(youtube, playlist_name):
  136. template = ('Youtube: Playlist %s does not exist, creating it.')
  137. logging.info(template % (str(playlist_name)))
  138. resources = build_resource({'snippet.title': playlist_name,
  139. 'snippet.description': '',
  140. 'status.privacyStatus': 'public'})
  141. response = youtube.playlists().insert(
  142. body=resources,
  143. part='status,snippet,id'
  144. ).execute()
  145. return response["id"]
  146. def build_resource(properties):
  147. resource = {}
  148. for p in properties:
  149. # Given a key like "snippet.title", split into "snippet" and "title", where
  150. # "snippet" will be an object and "title" will be a property in that object.
  151. prop_array = p.split('.')
  152. ref = resource
  153. for pa in range(0, len(prop_array)):
  154. is_array = False
  155. key = prop_array[pa]
  156. # For properties that have array values, convert a name like
  157. # "snippet.tags[]" to snippet.tags, and set a flag to handle
  158. # the value as an array.
  159. if key[-2:] == '[]':
  160. key = key[0:len(key)-2:]
  161. is_array = True
  162. if pa == (len(prop_array) - 1):
  163. # Leave properties without values out of inserted resource.
  164. if properties[p]:
  165. if is_array:
  166. ref[key] = properties[p].split(',')
  167. else:
  168. ref[key] = properties[p]
  169. elif key not in ref:
  170. # For example, the property is "snippet.title", but the resource does
  171. # not yet have a "snippet" object. Create the snippet object here.
  172. # Setting "ref = ref[key]" means that in the next time through the
  173. # "for pa in range ..." loop, we will be setting a property in the
  174. # resource's "snippet" object.
  175. ref[key] = {}
  176. ref = ref[key]
  177. else:
  178. # For example, the property is "snippet.description", and the resource
  179. # already has a "snippet" object.
  180. ref = ref[key]
  181. return resource
  182. def set_thumbnail(youtube, media_file, **kwargs):
  183. kwargs = utils.remove_empty_kwargs(**kwargs)
  184. request = youtube.thumbnails().set(
  185. media_body=MediaFileUpload(media_file, chunksize=-1,
  186. resumable=True),
  187. **kwargs
  188. )
  189. # See full sample for function
  190. return resumable_upload(request, 'thumbnail', 'set')
  191. def set_playlist(youtube, playlist_id, video_id):
  192. logging.info('Youtube: Configuring playlist...')
  193. resource = build_resource({'snippet.playlistId': playlist_id,
  194. 'snippet.resourceId.kind': 'youtube#video',
  195. 'snippet.resourceId.videoId': video_id,
  196. 'snippet.position': ''}
  197. )
  198. try:
  199. youtube.playlistItems().insert(
  200. body=resource,
  201. part='snippet'
  202. ).execute()
  203. except Exception as e:
  204. if hasattr(e, 'message'):
  205. logging.error("Youtube: Error: " + str(e.message))
  206. else:
  207. logging.error("Youtube: Error: " + str(e))
  208. logging.info('Youtube: Video is correclty added to the playlist.')
  209. # This method implements an exponential backoff strategy to resume a
  210. # failed upload.
  211. def resumable_upload(request, resource, method):
  212. response = None
  213. error = None
  214. retry = 0
  215. while response is None:
  216. try:
  217. template = 'Youtube: Uploading %s...'
  218. logging.info(template % resource)
  219. status, response = request.next_chunk()
  220. if response is not None:
  221. if method == 'insert' and 'id' in response:
  222. logging.info('Youtube : Video was successfully uploaded.')
  223. template = 'Youtube: Watch it at https://youtu.be/%s (post-encoding could take some time)'
  224. logging.info(template % response['id'])
  225. return response['id']
  226. elif method != 'insert' or "id" not in response:
  227. logging.info('Youtube: Thumbnail was successfully set.')
  228. else:
  229. template = ('Youtube : The upload failed with an '
  230. 'unexpected response: %s')
  231. logging.error(template % response)
  232. exit(1)
  233. except HttpError as e:
  234. if e.resp.status in RETRIABLE_STATUS_CODES:
  235. template = 'Youtube : A retriable HTTP error %d occurred:\n%s'
  236. error = template % (e.resp.status, e.content)
  237. else:
  238. raise
  239. except RETRIABLE_EXCEPTIONS as e:
  240. error = 'Youtube : A retriable error occurred: %s' % e
  241. if error is not None:
  242. logging.warning(error)
  243. retry += 1
  244. if retry > MAX_RETRIES:
  245. logging.error('Youtube : No longer attempting to retry.')
  246. exit(1)
  247. max_sleep = 2 ** retry
  248. sleep_seconds = random.random() * max_sleep
  249. logging.warning('Youtube : Sleeping %f seconds and then retrying...'
  250. % sleep_seconds)
  251. time.sleep(sleep_seconds)
  252. def run(options):
  253. youtube = get_authenticated_service()
  254. try:
  255. initialize_upload(youtube, options)
  256. except HttpError as e:
  257. logging.error('Youtube : An HTTP error %d occurred:\n%s' % (e.resp.status,
  258. e.content))