From 4643d70c685daebb3971ddc3180d94b605ca5a9b Mon Sep 17 00:00:00 2001 From: Michael Chacaton Date: Mon, 28 Sep 2015 12:19:20 +0200 Subject: [PATCH] Video Upload --- tweepy/api.py | 148 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/tweepy/api.py b/tweepy/api.py index c652e90..e29faec 100644 --- a/tweepy/api.py +++ b/tweepy/api.py @@ -8,10 +8,11 @@ import os import mimetypes import six +import urllib from tweepy.binder import bind_api from tweepy.error import TweepError -from tweepy.parsers import ModelParser, Parser +from tweepy.parsers import ModelParser, Parser, RawParser from tweepy.utils import list_to_csv @@ -211,6 +212,60 @@ class API(object): upload_api=True )(*args, **kwargs) + def video_upload(self, filename, *args, **kwargs): + """ :reference https://dev.twitter.com/rest/reference/post/media/upload-chunked + :allowed_param: + """ + f = kwargs.pop('file', None) + # Initialize upload (Twitter cannot handle videos > 15 MB) + headers, post_data, fp = API._chunk_video('init', filename, 15360, form_field='media', f=f) + kwargs.update({ 'headers': headers, 'post_data': post_data }) + + # Send the INIT request + media_info = bind_api( + api=self, + path='/media/upload.json', + method='POST', + payload_type='media', + allowed_param=[], + require_auth=True, + upload_api=True + )(*args, **kwargs) + + # If a media ID has been generated, we can send the file + if media_info.media_id: + chunk_size = kwargs.pop('chunk_size', 4096) + fsize = os.path.getsize(filename) + nloops = int(fsize / chunk_size) + (1 if fsize % chunk_size > 0 else 0) + for i in range(nloops): + headers, post_data, fp = API._chunk_video('append', filename, 15360, chunk_size=chunk_size, f=fp, media_id=media_info.media_id, segment_index=i) + kwargs.update({ 'headers': headers, 'post_data': post_data, 'parser': RawParser() }) + # The APPEND command returns an empty response body + bind_api( + api=self, + path='/media/upload.json', + method='POST', + payload_type='media', + allowed_param=[], + require_auth=True, + upload_api=True + )(*args, **kwargs) + # When all chunks have been sent, we can finalize. + headers, post_data, fp = API._chunk_video('finalize', filename, 15360, media_id=media_info.media_id) + kwargs.update({ 'headers': headers, 'post_data': post_data }) + # The FINALIZE command returns media information + return bind_api( + api=self, + path='/media/upload.json', + method='POST', + payload_type='media', + allowed_param=[], + require_auth=True, + upload_api=True + )(*args, **kwargs) + else: + return media_info + def update_with_media(self, filename, *args, **kwargs): """ :reference: https://dev.twitter.com/rest/reference/post/statuses/update_with_media :allowed_param:'status', 'possibly_sensitive', 'in_reply_to_status_id', 'in_reply_to_status_id_str', 'auto_populate_reply_metadata', 'lat', 'long', 'place_id', 'display_coordinates' @@ -1329,3 +1384,94 @@ class API(object): } return headers, body + + @staticmethod + def _chunk_video(command, filename, max_size, form_field="media", chunk_size=4096, f=None, media_id=None, segment_index=0): + fp = None + if command == 'init': + if f is None: + file_size = os.path.getsize(filename) + try: + if file_size > (max_size * 1024): + raise TweepError('File is too big, must be less than %skb.' % max_size) + except os.error as e: + raise TweepError('Unable to access file: %s' % e.strerror) + + # build the mulitpart-formdata body + fp = open(filename, 'rb') + else: + f.seek(0, 2) # Seek to end of file + file_size = f.tell() + if file_size > (max_size * 1024): + raise TweepError('File is too big, must be less than %skb.' % max_size) + f.seek(0) # Reset to beginning of file + fp = f + elif command != 'finalize': + if f is not None: + fp = f + else: + raise TweepError('File input for APPEND is mandatory.') + + # video must be mp4 + file_type = mimetypes.guess_type(filename) + if file_type is None: + raise TweepError('Could not determine file type') + file_type = file_type[0] + if file_type not in ['video/mp4']: + raise TweepError('Invalid file type for video: %s' % file_type) + + BOUNDARY = b'Tw3ePy' + body = list() + if command == 'init': + body.append( + urllib.urlencode({ + 'command': 'INIT', + 'media_type': file_type, + 'total_bytes': file_size + }) + ) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' + } + elif command == 'append': + if media_id is None: + raise TweepError('Media ID is required for APPEND command.') + body.append(b'--' + BOUNDARY) + body.append('Content-Disposition: form-data; name="command"'.encode('utf-8')) + body.append(b'') + body.append(b'APPEND') + body.append(b'--' + BOUNDARY) + body.append('Content-Disposition: form-data; name="media_id"'.encode('utf-8')) + body.append(b'') + body.append(str(media_id).encode('utf-8')) + body.append(b'--' + BOUNDARY) + body.append('Content-Disposition: form-data; name="segment_index"'.encode('utf-8')) + body.append(b'') + body.append(str(segment_index).encode('utf-8')) + body.append(b'--' + BOUNDARY) + body.append('Content-Disposition: form-data; name="{0}"; filename="{1}"'.format(form_field, os.path.basename(filename)).encode('utf-8')) + body.append('Content-Type: {0}'.format(file_type).encode('utf-8')) + body.append(b'') + body.append(fp.read(chunk_size)) + body.append(b'--' + BOUNDARY + b'--') + headers = { + 'Content-Type': 'multipart/form-data; boundary=Tw3ePy' + } + elif command == 'finalize': + if media_id is None: + raise TweepError('Media ID is required for FINALIZE command.') + body.append( + urllib.urlencode({ + 'command': 'FINALIZE', + 'media_id': media_id + }) + ) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' + } + + body = b'\r\n'.join(body) + # build headers + headers['Content-Length'] = str(len(body)) + + return headers, body, fp -- 2.25.1