Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: sferik/twitter-ruby
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: amplifr/twitter
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Can’t automatically merge. Don’t worry, you can still create the pull request.
  • 5 commits
  • 5 files changed
  • 3 contributors

Commits on Feb 19, 2017

  1. Add public API for media upload with async support

    Create public methods for Twitter's media upload API. These accept
    additional parameters, including the media_category parameter to enable
    asynchronous uploads. The old upload_with_media method now calls these
    new methods under the hood.
    jonmast committed Feb 19, 2017
    Copy the full SHA
    5fc7237 View commit details

Commits on Mar 14, 2017

  1. Increase processing status checks to 20

    Twitter takes more than 10 status checks for processing very large
    videos. Hypothetically, this number could be infinite since Twitter is
    supposed to signal us if something goes wrong, but having a limit adds
    additional safety.
    jonmast committed Mar 14, 2017
    Copy the full SHA
    7f9d490 View commit details

Commits on Aug 19, 2017

  1. Make chunked and simple upload methods private in favor of #upload

    Make the #upload method public and switch to using file size as the
    heuristic for choosing between simple and chunked upload. Make the
     #upload_media_simple and #upload_media_chunked methods private since
     they are no longer needed.
    jonmast committed Aug 19, 2017
    Copy the full SHA
    8a850ce View commit details

Commits on Oct 8, 2019

  1. Merge pull request #1 from jonmast/master

    Chunked upload functionality
    gazay authored Oct 8, 2019

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    43addd2 View commit details

Commits on Oct 14, 2019

  1. Add condition to chunk upload only videos. As well add media_category

    without which this uploading doesn't work
    gazay committed Oct 14, 2019
    Copy the full SHA
    a3791b3 View commit details
Showing with 251 additions and 3 deletions.
  1. +2 −0 lib/twitter/rest/api.rb
  2. +158 −0 lib/twitter/rest/media.rb
  3. +1 −1 lib/twitter/rest/tweets.rb
  4. +86 −0 spec/twitter/rest/media_spec.rb
  5. +4 −2 spec/twitter/rest/tweets_spec.rb
2 changes: 2 additions & 0 deletions lib/twitter/rest/api.rb
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
require 'twitter/rest/tweets'
require 'twitter/rest/undocumented'
require 'twitter/rest/users'
require 'twitter/rest/media'

module Twitter
module REST
@@ -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
158 changes: 158 additions & 0 deletions lib/twitter/rest/media.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
module Twitter
module REST
module Media
# Maximum number of times to poll twitter for upload status
MAX_STATUS_CHECKS = 20

# Use chunked uploading if file size is greater than 5MB
CHUNKED_UPLOAD_THRESHOLD = (5 * 1024 * 1024)

# Upload a media file to twitter
#
# @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 (PNG, JPEG or GIF) or video (MP4) file.
# @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(file, options = {})
if file.size < CHUNKED_UPLOAD_THRESHOLD && !(File.basename(file) =~ /\.mp4$/)
upload_media_simple(file, options)
else
upload_media_chunked(file, options)
end
end

private

# 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

# 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,
media_category: 'tweet_video',
**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
2 changes: 1 addition & 1 deletion lib/twitter/rest/tweets.rb
Original file line number Diff line number Diff line change
@@ -226,7 +226,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
86 changes: 86 additions & 0 deletions spec/twitter/rest/media_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# 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(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(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(fixture('pbjt.gif'), media_category: 'test')
end
end

describe '#upload_media_chunked' do
let(:video_file) do
video_file = fixture('1080p.mp4')
# Pretend the file is bigger so we get chunked upload
allow(video_file).to receive(:size).and_return(20 * 1024 * 1024)
video_file
end

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(video_file)

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(video_file)

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(video_file, 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
6 changes: 4 additions & 2 deletions spec/twitter/rest/tweets_spec.rb
Original file line number Diff line number Diff line change
@@ -460,9 +460,11 @@
expect(a_post('/1.1/statuses/update.json')).to have_been_made
end
end
context 'with a mp4 video' do
context 'with a 50MB mp4 video' do
it 'requests the correct resources' do
@client.update_with_media('You always have options', fixture('1080p.mp4'))
video_file = fixture('1080p.mp4')
allow(video_file).to receive(:size).and_return(50 * 1024 * 1024)
@client.update_with_media('You always have options', video_file)
expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made.times(3)
expect(a_post('/1.1/statuses/update.json')).to have_been_made
end