Browse Source

Add build_options

Arnau Sanchez 8 years ago
parent
commit
e09591eb99

+ 2 - 2
bin/youtube-upload

@@ -6,5 +6,5 @@ if __name__ == '__main__':
     import os.path, sys
     sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir))    
     
-    from youtube_upload import main    
-    main.run()
+    from youtube_upload import cli    
+    cli.run()

+ 2 - 0
youtube_upload/__init__.py

@@ -1 +1,3 @@
 VERSION = "0.8.0"
+
+from .upload import build_options, get_resource, upload_video, OPTIONS

+ 8 - 7
youtube_upload/main.py → youtube_upload/cli.py

@@ -37,7 +37,7 @@ EXIT_CODES = {
 
 WATCH_VIDEO_URL = "https://www.youtube.com/watch?v={id}"
 
-def main(arguments):
+def cli(arguments):
     """Upload videos to Youtube."""
     usage = """Usage: %prog [OPTIONS] VIDEO
 
@@ -75,28 +75,29 @@ def main(arguments):
 
     # Authentication
     parser.add_argument('--client-secrets', dest='client_secrets',
-        type=str, help='Client secrets JSON file')
+        type=str, help='Client secrets JSON path file')
     parser.add_argument('--credentials-file', dest='credentials_file',
-        type=str, help='Credentials JSON file')
+        type=str, help='Credentials JSON path file')
     parser.add_argument('--auth-browser', dest='auth_browser', action='store_true',
         help='Open a GUI browser to authenticate if required')
 
     #Additional options
     parser.add_argument('--open-link', dest='open_link', action='store_true',
-        help='Opens a url in a web browser to display the uploaded video')
+        help='Opens video URL in a web browser')
 
     # Positional arguments
     parser.add_argument('video_path', metavar="VIDEO PATH", type=str,
         help="Video to upload (local path)")
-
+  
     options = parser.parse_args(arguments)
-    video_id = upload.upload(options.video_path, options)
+    resource = upload.get_resource(options)
+    video_id = upload.upload_video(resource, options.video_path, options)
     video_url = WATCH_VIDEO_URL.format(id=video_id)
     lib.debug("Video URL: {0}".format(video_url))
     sys.stdout.write(video_id + "\n")
 
 def run():
-    sys.exit(lib.catch_exceptions(EXIT_CODES, main, sys.argv[1:]))
+    sys.exit(lib.catch_exceptions(EXIT_CODES, cli, sys.argv[1:]))
   
 if __name__ == '__main__':
     run()

+ 12 - 0
youtube_upload/lib.py

@@ -63,6 +63,18 @@ def get_first_existing_filename(prefixes, relative_path):
         if os.path.exists(path):
             return path
 
+def remove_empty_fields_recursively(dct):
+    """Remove empty (non true) values from a dict recursing dict-values."""
+    output = {}
+    for key, value in dct.items():
+        if isinstance(value, dict):
+            new_value = remove_empty_fields_recursively(value)
+            if new_value:
+                output[key] = new_value 
+        elif value:
+            output[key] = value
+    return output
+
 def retriable_exceptions(fun, retriable_exceptions, max_retries=None):
     """Run function and retry on some exceptions (with exponential backoff)."""
     retry = 0

+ 74 - 17
youtube_upload/upload.py

@@ -1,15 +1,22 @@
+import collections
 import os
 import sys
-import collections
+import socket
 import webbrowser
 
+try:
+    import httplib
+except ImportError:
+    import http.client as httplib
+
 import googleapiclient.errors
 import oauth2client
+import apiclient.http
+import httplib2
 
 from . import lib
 from . import playlists
 from . import auth
-from . import upload_video
 from . import categories
 
 # http://code.google.com/p/python-progressbar (>= 2.3)
@@ -21,10 +28,62 @@ except ImportError:
 debug = lib.debug
 struct = collections.namedtuple
 
+OPTIONS = {
+    "title": dict(type=str, description="Video title"),
+    "category": dict(type=str, description="Video category"),
+    "description": dict(type=str, description="Video description"),
+    "tags": dict(type=str, description='Video tags (separated by commas: "tag1, tag2,...")'),
+    "privacy": dict(type=str, description="Privacy status (public | unlisted | private)"),
+    "publish_at": dict(type=str, description="Publish date (ISO 8601): YYYY-MM-DDThh:mm:ss.sZ"),
+    "location": dict(type=str, description="Location: latitude=VAL,longitude=VAL[,altitude=VAL]"),
+    "recording_date": dict(type=str, description="Recording date (ISO 8601): YYYY-MM-DDThh:mm:ss.sZ"),
+    "default_language": dict(type=str, description="Default language (ISO 639-1: en | fr | de | ...)"),
+    "default_audio_language": dict(type=str, description="Default audio language (ISO 639-1: en | fr | de | ...)"),
+    "thumb": dict(type=str, description="Image file to use as video thumbnail (JPEG or PNG)"),
+    "playlist": dict(type=str, description="Playlist title (if it does not exist, it will be created)"),
+    "client_secrets": dict(type=str, description="Client secrets JSON path file"),
+    "auth_browser": dict(type=bool, description="Open a url in a web browser to display the uploaded video"),
+    "credentials_file": dict(type=str, description="Credentials JSON path file"),
+    "open_link": dict(type=str, description="Opens a url in a web browser to display the uploaded video"),
+}
+
+Options = struct("YoutubeUploadOptions", OPTIONS.keys())
+build_options = Options(*([None] * len(OPTIONS)))._replace
+
 class InvalidCategory(Exception): pass
 class AuthenticationError(Exception): pass
 class RequestError(Exception): pass
 
+RETRIABLE_EXCEPTIONS = [
+    socket.error, IOError, httplib2.HttpLib2Error, httplib.NotConnected,
+    httplib.IncompleteRead, httplib.ImproperConnectionState,
+    httplib.CannotSendRequest, httplib.CannotSendHeader,
+    httplib.ResponseNotReady, httplib.BadStatusLine,
+]
+
+def _upload_to_request(request, progress_callback):
+    """Upload a video to a Youtube request. Return video ID."""
+    while 1:
+        status, response = request.next_chunk()
+        if status and progress_callback:
+            progress_callback(status.total_size, status.resumable_progress)
+        if response:
+            if "id" in response:
+                return response['id']
+            else:
+                raise KeyError("Expected field 'id' not found in response")
+
+def upload(resource, path, body, chunksize=4*1024*1024, 
+        progress_callback=None, max_retries=10):
+    """Upload video to Youtube. Return video ID."""
+    body_keys = ",".join(body.keys())
+    media = apiclient.http.MediaFileUpload(path, chunksize=chunksize, 
+        resumable=True, mimetype="application/octet-stream")
+    request = resource.videos().insert(part=body_keys, body=body, media_body=media)
+    upload_fun = lambda: _upload_to_request(request, progress_callback)
+    return lib.retriable_exceptions(upload_fun, 
+        RETRIABLE_EXCEPTIONS, max_retries=max_retries)
+
 def open_link(url):
     """Opens a URL link in the client's browser."""
     webbrowser.open(url)
@@ -64,8 +123,8 @@ def get_category_id(category):
         else:
             msg = "{0} is not a valid category".format(category)
             raise InvalidCategory(msg)
-
-def upload_youtube_video(youtube, options, video_path):
+            
+def build_body_and_upload(youtube, options, video_path):
     """Upload video."""
     u = lib.to_utf8
     title = u(options.title)
@@ -76,10 +135,10 @@ def upload_youtube_video(youtube, options, video_path):
     if options.publish_at:    
       debug("Your video will remain private until specified date.")
       
-    tags = [u(s.strip()) for s in (options.tags or "").split(",")]
+    tags = [u(s.strip()) for s in (options.tags or "").split(",") if s.strip()]
     progress = get_progress_info()
     category_id = get_category_id(options.category)
-    request_body = {
+    request_body = lib.remove_empty_fields_recursively({
         "snippet": {
             "title": title,
             "description": description,
@@ -90,7 +149,7 @@ def upload_youtube_video(youtube, options, video_path):
 
         },
         "status": {
-            "privacyStatus": ("private" if options.publish_at else options.privacy),
+            "privacyStatus": ("private" if options.publish_at else (options.privacy or "public")),
             "publishAt": options.publish_at,
 
         },
@@ -98,17 +157,17 @@ def upload_youtube_video(youtube, options, video_path):
             "location": lib.string_to_dict(options.location),
             "recordingDate": options.recording_date,
         },
-    }
+    })
 
     debug("Start upload: {0}".format(video_path))
     try:
-        video_id = upload_video.upload(youtube, video_path, 
+        video_id = upload(youtube, video_path, 
             request_body, progress_callback=progress.callback)
     finally:
         progress.finish()
     return video_id
 
-def get_youtube_handler(options):
+def get_resource(options):
     """Return the API Youtube object."""
     home = os.path.expanduser("~")
     default_client_secrets = lib.get_first_existing_filename(
@@ -125,19 +184,17 @@ def get_youtube_handler(options):
     return auth.get_resource(client_secrets, credentials,
         get_code_callback=get_code_callback)
 
-def upload(video_path, options):
+def upload_video(resource, video_path, options):
     """Run the main scripts from the parsed options/args."""
-    youtube = get_youtube_handler(options)
-
-    if youtube:
+    if resource:
         try:            
-            video_id = upload_youtube_video(youtube, options, video_path)
+            video_id = build_body_and_upload(resource, options, video_path)
             if options.open_link:
                 open_link(video_url)
             if options.thumb:
-                youtube.thumbnails().set(videoId=video_id, media_body=options.thumb).execute()
+                resource.thumbnails().set(videoId=video_id, media_body=options.thumb).execute()
             if options.playlist:
-                playlists.add_video_to_playlist(youtube, video_id, 
+                playlists.add_video_to_playlist(resource, video_id, 
                     title=lib.to_utf8(options.playlist), privacy=options.privacy)
         except googleapiclient.errors.HttpError as error:
             raise RequestError("Server response: {0}".format(bytes.decode(error.content).strip()))

+ 0 - 42
youtube_upload/upload_video.py

@@ -1,42 +0,0 @@
-import socket
-
-try:
-    import httplib
-except ImportError:
-    import http.client as httplib
-
-import googleapiclient.errors
-import apiclient.http
-import httplib2
-
-from . import lib
-
-RETRIABLE_EXCEPTIONS = [
-    socket.error, IOError, httplib2.HttpLib2Error, httplib.NotConnected,
-    httplib.IncompleteRead, httplib.ImproperConnectionState,
-    httplib.CannotSendRequest, httplib.CannotSendHeader,
-    httplib.ResponseNotReady, httplib.BadStatusLine,
-]
-
-def _upload_to_request(request, progress_callback):
-    """Upload a video to a Youtube request. Return video ID."""
-    while 1:
-        status, response = request.next_chunk()
-        if status and progress_callback:
-            progress_callback(status.total_size, status.resumable_progress)
-        if response:
-            if "id" in response:
-                return response['id']
-            else:
-                raise KeyError("Expected field 'id' not found in response")
-
-def upload(resource, path, body, chunksize=4*1024*1024, 
-        progress_callback=None, max_retries=10):
-    """Upload video to Youtube. Return video ID."""
-    body_keys = ",".join(body.keys())
-    media = apiclient.http.MediaFileUpload(path, chunksize=chunksize, 
-        resumable=True, mimetype="application/octet-stream")
-    request = resource.videos().insert(part=body_keys, body=body, media_body=media)
-    upload_fun = lambda: _upload_to_request(request, progress_callback)
-    return lib.retriable_exceptions(upload_fun, 
-        RETRIABLE_EXCEPTIONS, max_retries=max_retries)