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