upload.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import collections
  2. import os
  3. import sys
  4. import socket
  5. import webbrowser
  6. try:
  7. import httplib
  8. except ImportError:
  9. import http.client as httplib
  10. import googleapiclient.errors
  11. import oauth2client
  12. import apiclient.http
  13. import httplib2
  14. from . import lib
  15. from . import playlists
  16. from . import auth
  17. from . import categories
  18. # http://code.google.com/p/python-progressbar (>= 2.3)
  19. try:
  20. import progressbar
  21. except ImportError:
  22. progressbar = None
  23. debug = lib.debug
  24. struct = collections.namedtuple
  25. OPTIONS = {
  26. "title": dict(type=str, description="Video title"),
  27. "category": dict(type=str, description="Video category"),
  28. "description": dict(type=str, description="Video description"),
  29. "tags": dict(type=str, description='Video tags (separated by commas: "tag1, tag2,...")'),
  30. "privacy": dict(type=str, description="Privacy status (public | unlisted | private)"),
  31. "publish_at": dict(type=str, description="Publish date (ISO 8601): YYYY-MM-DDThh:mm:ss.sZ"),
  32. "location": dict(type=str, description="Location: latitude=VAL,longitude=VAL[,altitude=VAL]"),
  33. "recording_date": dict(type=str, description="Recording date (ISO 8601): YYYY-MM-DDThh:mm:ss.sZ"),
  34. "default_language": dict(type=str, description="Default language (ISO 639-1: en | fr | de | ...)"),
  35. "default_audio_language": dict(type=str, description="Default audio language (ISO 639-1: en | fr | de | ...)"),
  36. "thumb": dict(type=str, description="Image file to use as video thumbnail (JPEG or PNG)"),
  37. "playlist": dict(type=str, description="Playlist title (if it does not exist, it will be created)"),
  38. "client_secrets": dict(type=str, description="Client secrets JSON path file"),
  39. "auth_browser": dict(type=bool, description="Open a url in a web browser to display the uploaded video"),
  40. "credentials_file": dict(type=str, description="Credentials JSON path file"),
  41. "open_link": dict(type=str, description="Opens a url in a web browser to display the uploaded video"),
  42. }
  43. Options = struct("YoutubeUploadOptions", OPTIONS.keys())
  44. build_options = Options(*([None] * len(OPTIONS)))._replace
  45. class InvalidCategory(Exception): pass
  46. class AuthenticationError(Exception): pass
  47. class RequestError(Exception): pass
  48. RETRIABLE_EXCEPTIONS = [
  49. socket.error, IOError, httplib2.HttpLib2Error, httplib.NotConnected,
  50. httplib.IncompleteRead, httplib.ImproperConnectionState,
  51. httplib.CannotSendRequest, httplib.CannotSendHeader,
  52. httplib.ResponseNotReady, httplib.BadStatusLine,
  53. ]
  54. def _upload_to_request(request, progress_callback):
  55. """Upload a video to a Youtube request. Return video ID."""
  56. while 1:
  57. status, response = request.next_chunk()
  58. if status and progress_callback:
  59. progress_callback(status.total_size, status.resumable_progress)
  60. if response:
  61. if "id" in response:
  62. return response['id']
  63. else:
  64. raise KeyError("Expected field 'id' not found in response")
  65. def upload(resource, path, body, chunksize=4*1024*1024,
  66. progress_callback=None, max_retries=10):
  67. """Upload video to Youtube. Return video ID."""
  68. body_keys = ",".join(body.keys())
  69. media = apiclient.http.MediaFileUpload(path, chunksize=chunksize,
  70. resumable=True, mimetype="application/octet-stream")
  71. request = resource.videos().insert(part=body_keys, body=body, media_body=media)
  72. upload_fun = lambda: _upload_to_request(request, progress_callback)
  73. return lib.retriable_exceptions(upload_fun,
  74. RETRIABLE_EXCEPTIONS, max_retries=max_retries)
  75. def open_link(url):
  76. """Opens a URL link in the client's browser."""
  77. webbrowser.open(url)
  78. def get_progress_info():
  79. """Return a function callback to update the progressbar."""
  80. progressinfo = struct("ProgressInfo", ["callback", "finish"])
  81. if progressbar:
  82. bar = progressbar.ProgressBar(widgets=[
  83. progressbar.Percentage(), ' ',
  84. progressbar.Bar(), ' ',
  85. progressbar.FileTransferSpeed(),
  86. ])
  87. def _callback(total_size, completed):
  88. if not hasattr(bar, "next_update"):
  89. if hasattr(bar, "maxval"):
  90. bar.maxval = total_size
  91. else:
  92. bar.max_value = total_size
  93. bar.start()
  94. bar.update(completed)
  95. def _finish():
  96. if hasattr(bar, "next_update"):
  97. return bar.finish()
  98. return progressinfo(callback=_callback, finish=_finish)
  99. else:
  100. return progressinfo(callback=None, finish=lambda: True)
  101. def get_category_id(category):
  102. """Return category ID from its name."""
  103. if category:
  104. if category in categories.IDS:
  105. ncategory = categories.IDS[category]
  106. debug("Using category: {0} (id={1})".format(category, ncategory))
  107. return str(categories.IDS[category])
  108. else:
  109. msg = "{0} is not a valid category".format(category)
  110. raise InvalidCategory(msg)
  111. def build_body_and_upload(youtube, options, video_path):
  112. """Upload video."""
  113. u = lib.to_utf8
  114. title = u(options.title)
  115. if hasattr(u('string'), 'decode'):
  116. description = u(options.description or "").decode("string-escape")
  117. else:
  118. description = options.description
  119. if options.publish_at:
  120. debug("Your video will remain private until specified date.")
  121. tags = [u(s.strip()) for s in (options.tags or "").split(",") if s.strip()]
  122. progress = get_progress_info()
  123. category_id = get_category_id(options.category)
  124. request_body = lib.remove_empty_fields_recursively({
  125. "snippet": {
  126. "title": title,
  127. "description": description,
  128. "categoryId": category_id,
  129. "tags": tags,
  130. "defaultLanguage": options.default_language,
  131. "defaultAudioLanguage": options.default_audio_language,
  132. },
  133. "status": {
  134. "privacyStatus": ("private" if options.publish_at else (options.privacy or "public")),
  135. "publishAt": options.publish_at,
  136. },
  137. "recordingDetails": {
  138. "location": lib.string_to_dict(options.location),
  139. "recordingDate": options.recording_date,
  140. },
  141. })
  142. debug("Start upload: {0}".format(video_path))
  143. try:
  144. video_id = upload(youtube, video_path,
  145. request_body, progress_callback=progress.callback)
  146. finally:
  147. progress.finish()
  148. return video_id
  149. def get_resource(options):
  150. """Return the API Youtube object."""
  151. home = os.path.expanduser("~")
  152. default_client_secrets = lib.get_first_existing_filename(
  153. [sys.prefix, os.path.join(sys.prefix, "local")],
  154. "share/youtube_upload/client_secrets.json")
  155. default_credentials = os.path.join(home, ".youtube-upload-credentials.json")
  156. client_secrets = options.client_secrets or default_client_secrets or \
  157. os.path.join(home, ".client_secrets.json")
  158. credentials = options.credentials_file or default_credentials
  159. debug("Using client secrets: {0}".format(client_secrets))
  160. debug("Using credentials file: {0}".format(credentials))
  161. get_code_callback = (auth.browser.get_code
  162. if options.auth_browser else auth.console.get_code)
  163. return auth.get_resource(client_secrets, credentials,
  164. get_code_callback=get_code_callback)
  165. def upload_video(resource, video_path, options):
  166. """Run the main scripts from the parsed options/args."""
  167. if resource:
  168. try:
  169. video_id = build_body_and_upload(resource, options, video_path)
  170. if options.open_link:
  171. open_link(video_url)
  172. if options.thumb:
  173. resource.thumbnails().set(videoId=video_id, media_body=options.thumb).execute()
  174. if options.playlist:
  175. playlists.add_video_to_playlist(resource, video_id,
  176. title=lib.to_utf8(options.playlist), privacy=options.privacy)
  177. except googleapiclient.errors.HttpError as error:
  178. raise RequestError("Server response: {0}".format(bytes.decode(error.content).strip()))
  179. return video_id
  180. else:
  181. raise AuthenticationError("Cannot get youtube resource")