Skip to content

Commit

Permalink
Merge 690638c into bc911e9
Browse files Browse the repository at this point in the history
  • Loading branch information
sobrinho committed Apr 20, 2017
2 parents bc911e9 + 690638c commit b4882ac
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 27 deletions.
2 changes: 2 additions & 0 deletions lib/twitter/rest/api.rb
Expand Up @@ -14,6 +14,7 @@
require 'twitter/rest/tweets'
require 'twitter/rest/undocumented'
require 'twitter/rest/users'
require 'twitter/rest/media'

module Twitter
module REST
Expand All @@ -36,6 +37,7 @@ module API
include Twitter::REST::Tweets
include Twitter::REST::Undocumented
include Twitter::REST::Users
include Twitter::REST::Media
end
end
end
133 changes: 133 additions & 0 deletions lib/twitter/rest/media.rb
@@ -0,0 +1,133 @@
module Twitter
module REST
module Media
# Maximum number of times to poll twitter for upload status
MAX_STATUS_CHECKS = 20

# Upload a media file to twitter in one request
#
# @see https://dev.twitter.com/rest/reference/post/media/upload.html
# @note This is only for small files, use the chunked upload for larger ones.
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Integer] The media_id of the uploaded file.
# @param file [File] An image file (PNG, JPEG or GIF).
# @option options [String] :media_category Category with which to
# identify media upload. When this is specified, it enables async
# processing which allows larger uploads. See
# https://dev.twitter.com/rest/media/uploading-media for details.
# Possible values include tweet_image, tweet_gif, and tweet_video.
def upload_media_simple(file, options = {})
Twitter::REST::Request.new(self,
:multipart_post,
'https://upload.twitter.com/1.1/media/upload.json',
key: :media,
file: file,
**options).perform[:media_id]
end

# Upload a media file to twitter in chunks
#
# @see https://dev.twitter.com/rest/reference/post/media/upload.html
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Integer] The media_id of the uploaded file.
# @param file [File] An image or video file (PNG, JPEG, GIF, or MP4).
# @option options [String] :media_category Category with which to
# identify media upload. When this is specified, it enables async
# processing which allows larger uploads. See
# https://dev.twitter.com/rest/media/uploading-media for details.
# Possible values include tweet_image, tweet_gif, and tweet_video.
def upload_media_chunked(file, options = {})
media_id = chunked_upload_init(file, options)[:media_id]
upload_chunks(media_id, file)
poll_status(media_id)

media_id
end

private

# Finalize upload and poll status until upload is ready
#
# @param media_id [Integer] The media_id to check the status of
def poll_status(media_id)
response = chunked_upload_finalize(media_id)
MAX_STATUS_CHECKS.times do
return unless (info = response[:processing_info])
return if info[:state] == 'succeeded'

raise Twitter::Error::ClientError, 'Upload Failed!' if info[:state] == 'failed'

sleep info[:check_after_secs]

response = chunked_upload_status(media_id)
end

raise Twitter::Error::ClientError, 'Max status checks exceeded!'
end

# Initialize a chunked upload
#
# @param file [File] Media file being uploaded
# @param options [Hash] Additional parameters
def chunked_upload_init(file, options)
Twitter::REST::Request.new(self, :post, 'https://upload.twitter.com/1.1/media/upload.json',
command: 'INIT',
media_type: 'video/mp4',
total_bytes: file.size,
**options).perform
end

# Append chunks to the upload
#
# @param media_id [Integer] The media_id of the file being uploaded
# @param file [File] Media file being uploaded
def upload_chunks(media_id, file)
until file.eof?
chunk = file.read(5_000_000)
segment ||= -1
segment += 1
chunked_upload_append(chunk, segment, media_id)
end

file.close
end

# Append a chunk to the upload
#
# @param chunk [String] File chunk to upload
# @param segment [Integer] Index of chunk in file
# @param media_id [Integer] The media_id of the file being uploaded
# @return [Hash] Response JSON
def chunked_upload_append(chunk, segment, media_id)
Twitter::REST::Request.new(self, :multipart_post, 'https://upload.twitter.com/1.1/media/upload.json',
command: 'APPEND',
media_id: media_id,
segment_index: segment,
key: :media,
file: StringIO.new(chunk)).perform
end

# Finalize the upload. This returns the processing status if applicable
#
# @param media_id [Integer] The media_id of the file being uploaded
# @return [Hash] Response JSON
def chunked_upload_finalize(media_id)
Twitter::REST::Request.new(self, :post, 'https://upload.twitter.com/1.1/media/upload.json',
command: 'FINALIZE', media_id: media_id).perform
end

# Check processing status for async uploads
#
# @param media_id [Integer] The media_id of the file being uploaded
# @return [Hash] Response JSON
def chunked_upload_status(media_id)
Twitter::REST::Request.new(self, :get, 'https://upload.twitter.com/1.1/media/upload.json',
command: 'STATUS', media_id: media_id).perform
end
end
end
end
29 changes: 5 additions & 24 deletions lib/twitter/rest/tweets.rb
Expand Up @@ -224,7 +224,7 @@ def retweet!(*args)
def update_with_media(status, media, options = {})
options = options.dup
media_ids = pmap(array_wrap(media)) do |medium|
upload(medium)[:media_id]
upload(medium)
end
update!(status, options.merge(media_ids: media_ids.join(',')))
end
Expand Down Expand Up @@ -323,34 +323,15 @@ def unretweet(*args)

private

# Uploads images and videos. Videos require multiple requests and uploads in chunks of 5 Megabytes.
# Uploads images and videos. Use chunked upload for videos and simple upload for images.
# The only supported video format is mp4.
#
# @see https://dev.twitter.com/rest/public/uploading-media
def upload(media) # rubocop:disable MethodLength, AbcSize
def upload(media)
if !(File.basename(media) =~ /\.mp4$/)
Twitter::REST::Request.new(self, :multipart_post, 'https://upload.twitter.com/1.1/media/upload.json', key: :media, file: media).perform
upload_media_simple(media)
else
init = Twitter::REST::Request.new(self, :post, 'https://upload.twitter.com/1.1/media/upload.json',
command: 'INIT',
media_type: 'video/mp4',
total_bytes: media.size).perform

until media.eof?
chunk = media.read(5_000_000)
seg ||= -1
Twitter::REST::Request.new(self, :multipart_post, 'https://upload.twitter.com/1.1/media/upload.json',
command: 'APPEND',
media_id: init[:media_id],
segment_index: seg += 1,
key: :media,
file: StringIO.new(chunk)).perform
end

media.close

Twitter::REST::Request.new(self, :post, 'https://upload.twitter.com/1.1/media/upload.json',
command: 'FINALIZE', media_id: init[:media_id]).perform
upload_media_chunked(media)
end
end

Expand Down
4 changes: 4 additions & 0 deletions lib/twitter/streaming/keep_alive.rb
@@ -0,0 +1,4 @@
module Twitter
class KeepAlive
end
end
5 changes: 4 additions & 1 deletion lib/twitter/streaming/message_parser.rb
@@ -1,4 +1,5 @@
require 'twitter/direct_message'
require 'twitter/streaming/keep_alive'
require 'twitter/streaming/deleted_tweet'
require 'twitter/streaming/event'
require 'twitter/streaming/friend_list'
Expand All @@ -9,7 +10,9 @@ module Twitter
module Streaming
class MessageParser
def self.parse(data) # rubocop:disable AbcSize, CyclomaticComplexity, MethodLength, PerceivedComplexity
if data[:id]
if data.empty?
KeepAlive.new
elsif data[:id]
Tweet.new(data)
elsif data[:event]
Event.new(data)
Expand Down
7 changes: 5 additions & 2 deletions lib/twitter/streaming/response.rb
Expand Up @@ -26,8 +26,11 @@ def on_headers_complete(_headers)

def on_body(data)
@tokenizer.extract(data).each do |line|
next if line.empty?
@block.call(JSON.parse(line, symbolize_names: true))
if line.empty?
@block.call(line)
else
@block.call(JSON.parse(line, symbolize_names: true))
end
end
end
end
Expand Down
79 changes: 79 additions & 0 deletions spec/twitter/rest/media_spec.rb
@@ -0,0 +1,79 @@
# coding: utf-8
require 'helper'

describe Twitter::REST::Media do
before do
@client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS')
end

describe '#upload_media_simple' do
before do
stub_request(:post, 'https://upload.twitter.com/1.1/media/upload.json').to_return(body: fixture('upload.json'), headers: {content_type: 'application/json; charset=utf-8'})
end

it 'uploads the file' do
@client.upload_media_simple(fixture('pbjt.gif'))
expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made
end

it 'returns the media id' do
media_id = @client.upload_media_simple(fixture('pbjt.gif'))

expect(media_id.to_s).to eq '470030289822314497'
end

it 'accepts a media_category parameter' do
expect(Twitter::REST::Request).to receive(:new)
.with(any_args, hash_including(media_category: 'test'))
.and_return(double(perform: {media_id: 123}))

@client.upload_media_simple(fixture('pbjt.gif'), media_category: 'test')
end
end

describe '#upload_media_chunked' do
context 'synchronous upload' do
before do
stub_request(:post, 'https://upload.twitter.com/1.1/media/upload.json').to_return(body: fixture('upload.json'), headers: {content_type: 'application/json; charset=utf-8'})
end

it 'uploads the file in chunks' do
@client.upload_media_chunked(fixture('1080p.mp4'))

expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made.times(3)
end

it 'returns the media id' do
media_id = @client.upload_media_chunked(fixture('1080p.mp4'))

expect(media_id.to_s).to eq '470030289822314497'
end
end

it 'polls the status until processing is complete' do
stub_request(:post, 'https://upload.twitter.com/1.1/media/upload.json').to_return do |request|
{
headers: {content_type: 'application/json; charset=utf-8'},
body: case request.body
when /command=(INIT|APPEND)/
fixture('upload.json')
when /command=FINALIZE/
'{"processing_info": {"state": "pending", "check_after_secs": 5}}'
end,
}
end
stub_request(:get, 'https://upload.twitter.com/1.1/media/upload.json')
.with(query: {command: 'STATUS', media_id: '470030289822314497'})
.to_return(
headers: {content_type: 'application/json; charset=utf-8'},
body: '{"processing_info": {"state": "succeeded"}}'
)

expect(@client).to receive(:sleep).with(5)

@client.upload_media_chunked(fixture('1080p.mp4'), media_category: 'tweet_video')

expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made.times(3)
end
end
end
1 change: 1 addition & 0 deletions spec/twitter/rest/tweets_spec.rb
Expand Up @@ -411,6 +411,7 @@
before do
stub_post('/1.1/statuses/update.json').to_return(body: fixture('status.json'), headers: {content_type: 'application/json; charset=utf-8'})
stub_request(:post, 'https://upload.twitter.com/1.1/media/upload.json').to_return(body: fixture('upload.json'), headers: {content_type: 'application/json; charset=utf-8'})
stub_request(:get, "https://upload.twitter.com/1.1/media/upload.json?command=STATUS&media_id=12345678901234567890").to_return(:status => 200, :body => "", :headers => {content_type: 'application/json; charset=utf-8'})
end
context 'with a gif image' do
it 'requests the correct resource' do
Expand Down

0 comments on commit b4882ac

Please sign in to comment.