Repository: bear/python-twitter

Issue:
Issue with PostUpdate and <1MB mp4 media files
When trying post an update using the following code:
```python
api = twitter.Api(options)
api.VerifyCredentials()
api.PostUpdate('My status', media='/my/local/file.mp4')
```
Twitter would respond with the error 'media type unrecognized'.  The call worked sometimes but not always but I was finally able to narrow down the issue to the size of the mp4 file. While searching for the error message I found this http://stackoverflow.com/a/32250580.  According to the author if the upload is a video file (mp4) it must always be uploaded via the chunked upload API endpoint.  I was unable to verify this in the Twitter API docs (like here: https://dev.twitter.com/rest/media/uploading-media.html) but once I added a smaller chunk_size to my init (chunk_size=1024*16) all PostUpdate calls worked as expected. This (to me) is a dirty fix but it works for me and it should work for anyone else with a similar issue.

The question is should the library here be updated to automatically reduce the chunk size when uploading mp4 files?  I am not sure as I was unable to find any other information in the Twitter docs as to this mp4 chunked upload only restriction. But if the linked article is correct there should probably be some logic added into the PostUpdate code that would take into account if the video file is < 1MB in size. 

Discussion:

Patch:
diff --git a/twitter/api.py b/twitter/api.py
index 94ec354..d41a648 100644
--- a/twitter/api.py
+++ b/twitter/api.py
@@ -1026,6 +1026,7 @@ class Api(object):
             parameters['attachment_url'] = attachment_url
 
         if media:
+            chunked_types = ['video/mp4', 'video/quicktime', 'image/gif']
             media_ids = []
             if isinstance(media, int):
                 media_ids.append(media)
@@ -1041,9 +1042,8 @@ class Api(object):
                     _, _, file_size, media_type = parse_media_file(media_file)
                     if media_type == 'image/gif' or media_type == 'video/mp4':
                         raise TwitterError(
-                            'You cannot post more than 1 GIF or 1 video in a '
-                            'single status.')
-                    if file_size > self.chunk_size:
+                            'You cannot post more than 1 GIF or 1 video in a single status.')
+                    if file_size > self.chunk_size or media_type in chunked_types:
                         media_id = self.UploadMediaChunked(
                             media=media_file,
                             additional_owners=media_additional_owners,
@@ -1055,13 +1055,11 @@ class Api(object):
                             media_category=media_category)
                     media_ids.append(media_id)
             else:
-                _, _, file_size, _ = parse_media_file(media)
-                if file_size > self.chunk_size:
-                    media_ids.append(
-                        self.UploadMediaChunked(media, media_additional_owners))
+                _, _, file_size, media_type = parse_media_file(media)
+                if file_size > self.chunk_size or media_type in chunked_types:
+                    media_ids.append(self.UploadMediaChunked(media, media_additional_owners))
                 else:
-                    media_ids.append(
-                        self.UploadMediaSimple(media, media_additional_owners))
+                    media_ids.append(self.UploadMediaSimple(media, media_additional_owners))
             parameters['media_ids'] = ','.join([str(mid) for mid in media_ids])
 
         if latitude is not None and longitude is not None:
@@ -1263,7 +1261,7 @@ class Api(object):
 
         try:
             media_fp.close()
-        except:
+        except Exception as e:
             pass
 
         return True
diff --git a/twitter/twitter_utils.py b/twitter/twitter_utils.py
index 0b2af5b..66ce8b2 100644
--- a/twitter/twitter_utils.py
+++ b/twitter/twitter_utils.py
@@ -223,8 +223,8 @@ def parse_media_file(passed_media):
     # Otherwise, if a file object was passed in the first place,
     # create the standard reference to media_file (i.e., rename it to fp).
     else:
-        if passed_media.mode != 'rb':
-            raise TwitterError({'message': 'File mode must be "rb".'})
+        if passed_media.mode not in ['rb', 'rb+', 'w+b']:
+            raise TwitterError('File mode must be "rb" or "rb+"')
         filename = os.path.basename(passed_media.name)
         data_file = passed_media
 
@@ -233,7 +233,7 @@ def parse_media_file(passed_media):
 
     try:
         data_file.seek(0)
-    except:
+    except Exception as e:
         pass
 
     media_type = mimetypes.guess_type(os.path.basename(filename))[0]


Test patch:
diff --git a/tests/test_api_30.py b/tests/test_api_30.py
index b966c70..67d7b33 100644
--- a/tests/test_api_30.py
+++ b/tests/test_api_30.py
@@ -2,9 +2,15 @@
 from __future__ import unicode_literals, print_function
 
 import json
+import os
 import re
 import sys
+from tempfile import NamedTemporaryFile
 import unittest
+try:
+    from unittest.mock import patch
+except ImportError:
+    from mock import patch
 import warnings
 
 import twitter
@@ -1713,3 +1719,16 @@ class ApiTest(unittest.TestCase):
         with warnings.catch_warnings(record=True) as w:
             resp = self.api.UpdateBackgroundImage(image='testdata/168NQ.jpg')
             self.assertTrue(issubclass(w[0].category, DeprecationWarning))
+
+    @responses.activate
+    @patch('twitter.api.Api.UploadMediaChunked')
+    def test_UploadSmallVideoUsesChunkedData(self, mocker):
+        responses.add(POST, DEFAULT_URL, body='{}')
+        video = NamedTemporaryFile(suffix='.mp4')
+        video.write(b'10' * 1024)
+        video.seek(0, 0)
+
+        resp = self.api.PostUpdate('test', media=video)
+        assert os.path.getsize(video.name) <= 1024 * 1024
+        assert isinstance(resp, twitter.Status)
+        assert twitter.api.Api.UploadMediaChunked.called