|
@@ -1,15 +1,22 @@
|
|
|
|
+import collections
|
|
import os
|
|
import os
|
|
import sys
|
|
import sys
|
|
-import collections
|
|
|
|
|
|
+import socket
|
|
import webbrowser
|
|
import webbrowser
|
|
|
|
|
|
|
|
+try:
|
|
|
|
+ import httplib
|
|
|
|
+except ImportError:
|
|
|
|
+ import http.client as httplib
|
|
|
|
+
|
|
import googleapiclient.errors
|
|
import googleapiclient.errors
|
|
import oauth2client
|
|
import oauth2client
|
|
|
|
+import apiclient.http
|
|
|
|
+import httplib2
|
|
|
|
|
|
from . import lib
|
|
from . import lib
|
|
from . import playlists
|
|
from . import playlists
|
|
from . import auth
|
|
from . import auth
|
|
-from . import upload_video
|
|
|
|
from . import categories
|
|
from . import categories
|
|
|
|
|
|
# http://code.google.com/p/python-progressbar (>= 2.3)
|
|
# http://code.google.com/p/python-progressbar (>= 2.3)
|
|
@@ -21,10 +28,62 @@ except ImportError:
|
|
debug = lib.debug
|
|
debug = lib.debug
|
|
struct = collections.namedtuple
|
|
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 InvalidCategory(Exception): pass
|
|
class AuthenticationError(Exception): pass
|
|
class AuthenticationError(Exception): pass
|
|
class RequestError(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):
|
|
def open_link(url):
|
|
"""Opens a URL link in the client's browser."""
|
|
"""Opens a URL link in the client's browser."""
|
|
webbrowser.open(url)
|
|
webbrowser.open(url)
|
|
@@ -64,8 +123,8 @@ def get_category_id(category):
|
|
else:
|
|
else:
|
|
msg = "{0} is not a valid category".format(category)
|
|
msg = "{0} is not a valid category".format(category)
|
|
raise InvalidCategory(msg)
|
|
raise InvalidCategory(msg)
|
|
-
|
|
|
|
-def upload_youtube_video(youtube, options, video_path):
|
|
|
|
|
|
+
|
|
|
|
+def build_body_and_upload(youtube, options, video_path):
|
|
"""Upload video."""
|
|
"""Upload video."""
|
|
u = lib.to_utf8
|
|
u = lib.to_utf8
|
|
title = u(options.title)
|
|
title = u(options.title)
|
|
@@ -76,10 +135,10 @@ def upload_youtube_video(youtube, options, video_path):
|
|
if options.publish_at:
|
|
if options.publish_at:
|
|
debug("Your video will remain private until specified date.")
|
|
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()
|
|
progress = get_progress_info()
|
|
category_id = get_category_id(options.category)
|
|
category_id = get_category_id(options.category)
|
|
- request_body = {
|
|
|
|
|
|
+ request_body = lib.remove_empty_fields_recursively({
|
|
"snippet": {
|
|
"snippet": {
|
|
"title": title,
|
|
"title": title,
|
|
"description": description,
|
|
"description": description,
|
|
@@ -90,7 +149,7 @@ def upload_youtube_video(youtube, options, video_path):
|
|
|
|
|
|
},
|
|
},
|
|
"status": {
|
|
"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,
|
|
"publishAt": options.publish_at,
|
|
|
|
|
|
},
|
|
},
|
|
@@ -98,17 +157,17 @@ def upload_youtube_video(youtube, options, video_path):
|
|
"location": lib.string_to_dict(options.location),
|
|
"location": lib.string_to_dict(options.location),
|
|
"recordingDate": options.recording_date,
|
|
"recordingDate": options.recording_date,
|
|
},
|
|
},
|
|
- }
|
|
|
|
|
|
+ })
|
|
|
|
|
|
debug("Start upload: {0}".format(video_path))
|
|
debug("Start upload: {0}".format(video_path))
|
|
try:
|
|
try:
|
|
- video_id = upload_video.upload(youtube, video_path,
|
|
|
|
|
|
+ video_id = upload(youtube, video_path,
|
|
request_body, progress_callback=progress.callback)
|
|
request_body, progress_callback=progress.callback)
|
|
finally:
|
|
finally:
|
|
progress.finish()
|
|
progress.finish()
|
|
return video_id
|
|
return video_id
|
|
|
|
|
|
-def get_youtube_handler(options):
|
|
|
|
|
|
+def get_resource(options):
|
|
"""Return the API Youtube object."""
|
|
"""Return the API Youtube object."""
|
|
home = os.path.expanduser("~")
|
|
home = os.path.expanduser("~")
|
|
default_client_secrets = lib.get_first_existing_filename(
|
|
default_client_secrets = lib.get_first_existing_filename(
|
|
@@ -125,19 +184,17 @@ def get_youtube_handler(options):
|
|
return auth.get_resource(client_secrets, credentials,
|
|
return auth.get_resource(client_secrets, credentials,
|
|
get_code_callback=get_code_callback)
|
|
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."""
|
|
"""Run the main scripts from the parsed options/args."""
|
|
- youtube = get_youtube_handler(options)
|
|
|
|
-
|
|
|
|
- if youtube:
|
|
|
|
|
|
+ if resource:
|
|
try:
|
|
try:
|
|
- video_id = upload_youtube_video(youtube, options, video_path)
|
|
|
|
|
|
+ video_id = build_body_and_upload(resource, options, video_path)
|
|
if options.open_link:
|
|
if options.open_link:
|
|
open_link(video_url)
|
|
open_link(video_url)
|
|
if options.thumb:
|
|
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:
|
|
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)
|
|
title=lib.to_utf8(options.playlist), privacy=options.privacy)
|
|
except googleapiclient.errors.HttpError as error:
|
|
except googleapiclient.errors.HttpError as error:
|
|
raise RequestError("Server response: {0}".format(bytes.decode(error.content).strip()))
|
|
raise RequestError("Server response: {0}".format(bytes.decode(error.content).strip()))
|