Follow Twitter guidelines for media upload by conforming to the "STATUS" phase, when required, and by providing "media_category" information. These will, for example, allow users to upload videos that exceed the basic 30 second limit. (#9261)

See:
 - https://twittercommunity.com/t/media-category-values/64781/7
 - https://twittercommunity.com/t/duration-too-long-maximim-30000/68760
 - https://dev.twitter.com/rest/reference/get/media/upload-status.html
This commit is contained in:
Mike Christianson 2017-09-05 18:49:40 -07:00 committed by Paulus Schoutsen
parent 9ade8002ac
commit e7a5f7bcdf

View File

@ -8,6 +8,8 @@ import json
import logging import logging
import mimetypes import mimetypes
import os import os
from datetime import timedelta, datetime
from functools import partial
import voluptuous as vol import voluptuous as vol
@ -15,6 +17,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import ( from homeassistant.components.notify import (
ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME
from homeassistant.helpers.event import async_track_point_in_time
REQUIREMENTS = ['TwitterAPI==2.4.6'] REQUIREMENTS = ['TwitterAPI==2.4.6']
@ -68,49 +71,67 @@ class TwitterNotificationService(BaseNotificationService):
_LOGGER.warning("'%s' is not a whitelisted directory", media) _LOGGER.warning("'%s' is not a whitelisted directory", media)
return return
media_id = self.upload_media(media) callback = partial(self.send_message_callback, message)
self.upload_media_then_callback(callback, media)
def send_message_callback(self, message, media_id):
"""Tweet a message, optionally with media."""
if self.user: if self.user:
resp = self.api.request('direct_messages/new', resp = self.api.request('direct_messages/new',
{'text': message, 'user': self.user, {'user': self.user,
'text': message,
'media_ids': media_id}) 'media_ids': media_id})
else: else:
resp = self.api.request('statuses/update', resp = self.api.request('statuses/update',
{'status': message, 'media_ids': media_id}) {'status': message,
'media_ids': media_id})
if resp.status_code != 200: if resp.status_code != 200:
self.log_error_resp(resp) self.log_error_resp(resp)
else:
_LOGGER.debug("Message posted: %s", resp.json())
def upload_media(self, media_path=None): def upload_media_then_callback(self, callback, media_path=None):
"""Upload media.""" """Upload media."""
if not media_path: if not media_path:
return None return None
with open(media_path, 'rb') as file:
total_bytes = os.path.getsize(media_path)
(media_category, media_type) = self.media_info(media_path)
resp = self.upload_media_init(
media_type, media_category, total_bytes
)
if 199 > resp.status_code < 300:
self.log_error_resp(resp)
return None
media_id = resp.json()['media_id']
media_id = self.upload_media_chunked(file, total_bytes, media_id)
resp = self.upload_media_finalize(media_id)
if 199 > resp.status_code < 300:
self.log_error_resp(resp)
return None
self.check_status_until_done(media_id, callback)
def media_info(self, media_path):
"""Determine mime type and Twitter media category for given media."""
(media_type, _) = mimetypes.guess_type(media_path) (media_type, _) = mimetypes.guess_type(media_path)
total_bytes = os.path.getsize(media_path) media_category = self.media_category_for_type(media_type)
_LOGGER.debug("media %s is mime type %s and translates to %s",
media_path, media_type, media_category)
return media_category, media_type
file = open(media_path, 'rb') def upload_media_init(self, media_type, media_category, total_bytes):
resp = self.upload_media_init(media_type, total_bytes)
if 199 > resp.status_code < 300:
self.log_error_resp(resp)
return None
media_id = resp.json()['media_id']
media_id = self.upload_media_chunked(file, total_bytes, media_id)
resp = self.upload_media_finalize(media_id)
if 199 > resp.status_code < 300:
self.log_error_resp(resp)
return media_id
def upload_media_init(self, media_type, total_bytes):
"""Upload media, INIT phase.""" """Upload media, INIT phase."""
resp = self.api.request('media/upload', return self.api.request('media/upload',
{'command': 'INIT', 'media_type': media_type, {'command': 'INIT', 'media_type': media_type,
'media_category': media_category,
'total_bytes': total_bytes}) 'total_bytes': total_bytes})
return resp
def upload_media_chunked(self, file, total_bytes, media_id): def upload_media_chunked(self, file, total_bytes, media_id):
"""Upload media, chunked append.""" """Upload media, chunked append."""
@ -128,17 +149,55 @@ class TwitterNotificationService(BaseNotificationService):
return media_id return media_id
def upload_media_append(self, chunk, media_id, segment_id): def upload_media_append(self, chunk, media_id, segment_id):
"""Upload media, append phase.""" """Upload media, APPEND phase."""
return self.api.request('media/upload', return self.api.request('media/upload',
{'command': 'APPEND', 'media_id': media_id, {'command': 'APPEND', 'media_id': media_id,
'segment_index': segment_id}, 'segment_index': segment_id},
{'media': chunk}) {'media': chunk})
def upload_media_finalize(self, media_id): def upload_media_finalize(self, media_id):
"""Upload media, finalize phase.""" """Upload media, FINALIZE phase."""
return self.api.request('media/upload', return self.api.request('media/upload',
{'command': 'FINALIZE', 'media_id': media_id}) {'command': 'FINALIZE', 'media_id': media_id})
def check_status_until_done(self, media_id, callback, *args):
"""Upload media, STATUS phase."""
resp = self.api.request('media/upload',
{'command': 'STATUS', 'media_id': media_id},
method_override='GET')
if resp.status_code != 200:
_LOGGER.error("media processing error: %s", resp.json())
processing_info = resp.json()['processing_info']
_LOGGER.debug("media processing %s status: %s", media_id,
processing_info)
if processing_info['state'] in {u'succeeded', u'failed'}:
return callback(media_id)
check_after_secs = processing_info['check_after_secs']
_LOGGER.debug("media processing waiting %s seconds to check status",
str(check_after_secs))
when = datetime.now() + timedelta(seconds=check_after_secs)
myself = partial(self.check_status_until_done, media_id, callback)
async_track_point_in_time(self.hass, myself, when)
@staticmethod
def media_category_for_type(media_type):
"""Determine Twitter media category by mime type."""
if media_type is None:
return None
if media_type.startswith('image/gif'):
return 'tweet_gif'
elif media_type.startswith('video/'):
return 'tweet_video'
elif media_type.startswith('image/'):
return 'tweet_image'
return None
@staticmethod @staticmethod
def log_bytes_sent(bytes_sent, total_bytes): def log_bytes_sent(bytes_sent, total_bytes):
"""Log upload progress.""" """Log upload progress."""