main.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. #!/usr/bin/env python
  2. #
  3. # Upload videos to Youtube from the command-line using APIv3.
  4. #
  5. # Author: Arnau Sanchez <pyarnau@gmail.com>
  6. # Project: https://github.com/tokland/youtube-upload
  7. """
  8. Upload a video to Youtube from the command-line.
  9. $ youtube-upload --title="A.S. Mutter playing" \
  10. --description="Anne Sophie Mutter plays Beethoven" \
  11. --category=Music \
  12. --tags="mutter, beethoven" \
  13. anne_sophie_mutter.flv
  14. pxzZ-fYjeYs
  15. """
  16. import os
  17. import sys
  18. import optparse
  19. import collections
  20. import webbrowser
  21. from io import open
  22. import googleapiclient.errors
  23. import oauth2client
  24. from . import auth
  25. from . import upload_video
  26. from . import categories
  27. from . import lib
  28. from . import playlists
  29. # http://code.google.com/p/python-progressbar (>= 2.3)
  30. try:
  31. import progressbar
  32. except ImportError:
  33. progressbar = None
  34. class InvalidCategory(Exception): pass
  35. class OptionsError(Exception): pass
  36. class AuthenticationError(Exception): pass
  37. class RequestError(Exception): pass
  38. EXIT_CODES = {
  39. OptionsError: 2,
  40. InvalidCategory: 3,
  41. RequestError: 3,
  42. AuthenticationError: 4,
  43. oauth2client.client.FlowExchangeError: 4,
  44. NotImplementedError: 5,
  45. }
  46. WATCH_VIDEO_URL = "https://www.youtube.com/watch?v={id}"
  47. debug = lib.debug
  48. struct = collections.namedtuple
  49. def open_link(url):
  50. """Opens a URL link in the client's browser."""
  51. webbrowser.open(url)
  52. def get_progress_info():
  53. """Return a function callback to update the progressbar."""
  54. progressinfo = struct("ProgressInfo", ["callback", "finish"])
  55. if progressbar:
  56. bar = progressbar.ProgressBar(widgets=[
  57. progressbar.Percentage(),
  58. ' ', progressbar.Bar(),
  59. ' ', progressbar.FileTransferSpeed(),
  60. ' ', progressbar.DataSize(), '/', progressbar.DataSize('max_value'),
  61. ' ', progressbar.Timer(),
  62. ' ', progressbar.AdaptiveETA(),
  63. ])
  64. def _callback(total_size, completed):
  65. if not hasattr(bar, "next_update"):
  66. if hasattr(bar, "maxval"):
  67. bar.maxval = total_size
  68. else:
  69. bar.max_value = total_size
  70. bar.start()
  71. bar.update(completed)
  72. def _finish():
  73. if hasattr(bar, "next_update"):
  74. return bar.finish()
  75. return progressinfo(callback=_callback, finish=_finish)
  76. else:
  77. return progressinfo(callback=None, finish=lambda: True)
  78. def get_category_id(category):
  79. """Return category ID from its name."""
  80. if category:
  81. if category in categories.IDS:
  82. ncategory = categories.IDS[category]
  83. debug("Using category: {0} (id={1})".format(category, ncategory))
  84. return str(categories.IDS[category])
  85. else:
  86. msg = "{0} is not a valid category".format(category)
  87. raise InvalidCategory(msg)
  88. def upload_youtube_video(youtube, options, video_path, total_videos, index):
  89. """Upload video with index (for split videos)."""
  90. u = lib.to_utf8
  91. title = u(options.title)
  92. if hasattr(u('string'), 'decode'):
  93. description = u(options.description or "").decode("string-escape")
  94. else:
  95. description = options.description
  96. if options.publish_at:
  97. debug("Your video will remain private until specified date.")
  98. tags = [u(s.strip()) for s in (options.tags or "").split(",")]
  99. ns = dict(title=title, n=index+1, total=total_videos)
  100. title_template = u(options.title_template)
  101. complete_title = (title_template.format(**ns) if total_videos > 1 else title)
  102. progress = get_progress_info()
  103. category_id = get_category_id(options.category)
  104. request_body = {
  105. "snippet": {
  106. "title": complete_title,
  107. "description": description,
  108. "categoryId": category_id,
  109. "tags": tags,
  110. "defaultLanguage": options.default_language,
  111. "defaultAudioLanguage": options.default_audio_language,
  112. },
  113. "status": {
  114. "embeddable": options.embeddable,
  115. "privacyStatus": ("private" if options.publish_at else options.privacy),
  116. "publishAt": options.publish_at,
  117. "license": options.license,
  118. },
  119. "recordingDetails": {
  120. "location": lib.string_to_dict(options.location),
  121. "recordingDate": options.recording_date,
  122. },
  123. }
  124. debug("Start upload: {0}".format(video_path))
  125. try:
  126. video_id = upload_video.upload(youtube, video_path,
  127. request_body, progress_callback=progress.callback,
  128. chunksize=options.chunksize)
  129. finally:
  130. progress.finish()
  131. return video_id
  132. def get_youtube_handler(options):
  133. """Return the API Youtube object."""
  134. home = os.path.expanduser("~")
  135. default_credentials = os.path.join(home, ".youtube-upload-credentials.json")
  136. client_secrets = options.client_secrets or os.path.join(home, ".client_secrets.json")
  137. credentials = options.credentials_file or default_credentials
  138. debug("Using client secrets: {0}".format(client_secrets))
  139. debug("Using credentials file: {0}".format(credentials))
  140. get_code_callback = (auth.browser.get_code
  141. if options.auth_browser else auth.console.get_code)
  142. return auth.get_resource(client_secrets, credentials,
  143. get_code_callback=get_code_callback)
  144. def parse_options_error(parser, options):
  145. """Check errors in options."""
  146. required_options = ["title"]
  147. missing = [opt for opt in required_options if not getattr(options, opt)]
  148. if missing:
  149. parser.print_usage()
  150. msg = "Some required option are missing: {0}".format(", ".join(missing))
  151. raise OptionsError(msg)
  152. def run_main(parser, options, args, output=sys.stdout):
  153. """Run the main scripts from the parsed options/args."""
  154. parse_options_error(parser, options)
  155. youtube = get_youtube_handler(options)
  156. if youtube:
  157. for index, video_path in enumerate(args):
  158. video_id = upload_youtube_video(youtube, options, video_path, len(args), index)
  159. video_url = WATCH_VIDEO_URL.format(id=video_id)
  160. debug("Video URL: {0}".format(video_url))
  161. if options.open_link:
  162. open_link(video_url) #Opens the Youtube Video's link in a webbrowser
  163. if options.thumb:
  164. youtube.thumbnails().set(videoId=video_id, media_body=options.thumb).execute()
  165. if options.playlist:
  166. playlists.add_video_to_playlist(youtube, video_id,
  167. title=lib.to_utf8(options.playlist), privacy=options.privacy)
  168. output.write(video_id + "\n")
  169. else:
  170. raise AuthenticationError("Cannot get youtube resource")
  171. def main(arguments):
  172. """Upload videos to Youtube."""
  173. usage = """Usage: %prog [OPTIONS] VIDEO [VIDEO2 ...]
  174. Upload videos to Youtube."""
  175. parser = optparse.OptionParser(usage)
  176. # Video metadata
  177. parser.add_option('-t', '--title', dest='title', type="string",
  178. help='Video title')
  179. parser.add_option('-c', '--category', dest='category', type="string",
  180. help='Video category')
  181. parser.add_option('-d', '--description', dest='description', type="string",
  182. help='Video description')
  183. parser.add_option('', '--description-file', dest='description_file', type="string",
  184. help='Video description file', default=None)
  185. parser.add_option('', '--tags', dest='tags', type="string",
  186. help='Video tags (separated by commas: "tag1, tag2,...")')
  187. parser.add_option('', '--privacy', dest='privacy', metavar="STRING",
  188. default="public", help='Privacy status (public | unlisted | private)')
  189. parser.add_option('', '--publish-at', dest='publish_at', metavar="datetime",
  190. default=None, help='Publish date (ISO 8601): YYYY-MM-DDThh:mm:ss.sZ')
  191. parser.add_option('', '--license', dest='license', metavar="string",
  192. choices=('youtube', 'creativeCommon'), default='youtube',
  193. help='License for the video, either "youtube" (the default) or "creativeCommon"')
  194. parser.add_option('', '--location', dest='location', type="string",
  195. default=None, metavar="latitude=VAL,longitude=VAL[,altitude=VAL]",
  196. help='Video location"')
  197. parser.add_option('', '--recording-date', dest='recording_date', metavar="datetime",
  198. default=None, help="Recording date (ISO 8601): YYYY-MM-DDThh:mm:ss.sZ")
  199. parser.add_option('', '--default-language', dest='default_language', type="string",
  200. default=None, metavar="string",
  201. help="Default language (ISO 639-1: en | fr | de | ...)")
  202. parser.add_option('', '--default-audio-language', dest='default_audio_language', type="string",
  203. default=None, metavar="string",
  204. help="Default audio language (ISO 639-1: en | fr | de | ...)")
  205. parser.add_option('', '--thumbnail', dest='thumb', type="string", metavar="FILE",
  206. help='Image file to use as video thumbnail (JPEG or PNG)')
  207. parser.add_option('', '--playlist', dest='playlist', type="string",
  208. help='Playlist title (if it does not exist, it will be created)')
  209. parser.add_option('', '--title-template', dest='title_template',
  210. type="string", default="{title} [{n}/{total}]", metavar="string",
  211. help='Template for multiple videos (default: {title} [{n}/{total}])')
  212. parser.add_option('', '--embeddable', dest='embeddable', default=True,
  213. help='Video is embeddable')
  214. # Authentication
  215. parser.add_option('', '--client-secrets', dest='client_secrets',
  216. type="string", help='Client secrets JSON file')
  217. parser.add_option('', '--credentials-file', dest='credentials_file',
  218. type="string", help='Credentials JSON file')
  219. parser.add_option('', '--auth-browser', dest='auth_browser', action='store_true',
  220. help='Open a GUI browser to authenticate if required')
  221. #Additional options
  222. parser.add_option('', '--chunksize', dest='chunksize', type="int",
  223. default = 1024*1024*8, help='Update file chunksize')
  224. parser.add_option('', '--open-link', dest='open_link', action='store_true',
  225. help='Opens a url in a web browser to display the uploaded video')
  226. options, args = parser.parse_args(arguments)
  227. if options.description_file is not None and os.path.exists(options.description_file):
  228. with open(options.description_file, encoding="utf-8") as file:
  229. options.description = file.read()
  230. try:
  231. run_main(parser, options, args)
  232. except googleapiclient.errors.HttpError as error:
  233. response = bytes.decode(error.content, encoding=lib.get_encoding()).strip()
  234. raise RequestError(u"Server response: {0}".format(response))
  235. def run():
  236. sys.exit(lib.catch_exceptions(EXIT_CODES, main, sys.argv[1:]))
  237. if __name__ == '__main__':
  238. run()