Module: X::MediaUploader

Extended by:
MediaUploader
Included in:
MediaUploader
Defined in:
lib/x/media_uploader.rb

Overview

Uploads media files to the X API

Constant Summary collapse

MAX_RETRIES =

Maximum number of retry attempts for failed uploads

3
BYTES_PER_MB =

Number of bytes per megabyte

1_048_576
MIME_TYPES =

Supported MIME types

%w[image/gif image/jpeg video/mp4 image/png application/x-subrip image/webp].freeze
MIME_TYPE_MAP =

Mapping of file extensions to MIME types

{"gif" => GIF_MIME_TYPE, "jpg" => JPEG_MIME_TYPE, "jpeg" => JPEG_MIME_TYPE, "mp4" => MP4_MIME_TYPE,
"png" => PNG_MIME_TYPE, "srt" => SUBRIP_MIME_TYPE, "webp" => WEBP_MIME_TYPE}.freeze
PROCESSING_INFO_STATES =

Processing states that indicate completion

%w[failed succeeded].freeze

Instance Method Summary collapse

Instance Method Details

#await_processing(client:, media:) ⇒ Hash?

Wait for media processing to complete

Examples:

Wait for processing

MediaUploader.await_processing(client: client, media: media)

Parameters:

  • client (Client)

    the X API client

  • media (Hash)

    the media object with an id

Returns:

  • (Hash, nil)

    the processing status



95
96
97
98
99
100
101
102
# File 'lib/x/media_uploader.rb', line 95

def await_processing(client:, media:)
  loop do
    status = client.get("media/upload?command=STATUS&media_id=#{media["id"]}")&.fetch("data")
    return status if status.nil? || !status["processing_info"] || PROCESSING_INFO_STATES.include?(status["processing_info"]["state"])

    sleep status["processing_info"]["check_after_secs"].to_i
  end
end

#await_processing!(client:, media:) ⇒ Hash?

Wait for media processing and raise on failure

Examples:

Wait for processing with error handling

MediaUploader.await_processing!(client: client, media: media)

Parameters:

  • client (Client)

    the X API client

  • media (Hash)

    the media object with an id

Returns:

  • (Hash, nil)

    the processing status

Raises:

  • (RuntimeError)

    if media processing failed



113
114
115
116
117
118
# File 'lib/x/media_uploader.rb', line 113

def await_processing!(client:, media:)
  status = await_processing(client:, media:)
  raise "Media processing failed" if status&.dig("processing_info", "state") == "failed"

  status
end

#chunked_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path, media_category), boundary: SecureRandom.hex, chunk_size_mb: 1) ⇒ Hash?

Perform a chunked upload for large files

Examples:

Upload a large video

MediaUploader.chunked_upload(client: client, file_path: "video.mp4", media_category: "tweet_video")

Parameters:

  • client (Client)

    the X API client

  • file_path (String)

    the path to the file to upload

  • media_category (String)

    the media category

  • media_type (String) (defaults to: infer_media_type(file_path, media_category))

    the MIME type of the media

  • boundary (String) (defaults to: SecureRandom.hex)

    the multipart boundary

  • chunk_size_mb (Integer) (defaults to: 1)

    the size of each chunk in megabytes

Returns:

  • (Hash, nil)

    the upload response data

Raises:

  • (RuntimeError)

    if the file does not exist

  • (ArgumentError)

    if the media category is invalid



77
78
79
80
81
82
83
84
85
# File 'lib/x/media_uploader.rb', line 77

def chunked_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path, media_category),
  boundary: SecureRandom.hex, chunk_size_mb: 1)
  MediaUploadValidator.validate_file_path!(file_path:)
  MediaUploadValidator.validate_media_category!(media_category:)
  media = init(client:, file_path:, media_type:, media_category:)
  chunk_size = chunk_size_mb * BYTES_PER_MB
  append(client:, file_paths: split(file_path, chunk_size), media:, boundary:)
  client.post("media/upload/#{media["id"]}/finalize")&.fetch("data")
end

#infer_media_type(file_path, media_category) ⇒ String

Infer the media type from file path and category

Examples:

MediaUploader.infer_media_type(“image.png”, “tweet_image”) #=> “image/png”


Parameters:

  • file_path (String)

    the file path

  • media_category (String)

    the media category

Returns:

  • (String)

    the inferred MIME type

Raises:



128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/x/media_uploader.rb', line 128

def infer_media_type(file_path, media_category)
  case media_category.downcase
  when TWEET_GIF, DM_GIF then GIF_MIME_TYPE
  when TWEET_VIDEO, DM_VIDEO then MP4_MIME_TYPE
  when SUBTITLES then SUBRIP_MIME_TYPE
  else
    extension = File.extname(file_path).delete(".").downcase
    MIME_TYPE_MAP.fetch(extension) do
      raise InvalidMediaType, "unable to determine MIME type from file extension: #{file_path.inspect}"
    end
  end
end

#upload(client:, file_path:, media_category:, boundary: SecureRandom.hex) ⇒ Hash?

Upload a file to the X API

Examples:

Upload an image

MediaUploader.upload(client: client, file_path: "image.png", media_category: "tweet_image")

Parameters:

  • client (Client)

    the X API client

  • file_path (String)

    the path to the file to upload

  • media_category (String)

    the media category

  • boundary (String) (defaults to: SecureRandom.hex)

    the multipart boundary

Returns:

  • (Hash, nil)

    the upload response data

Raises:

  • (RuntimeError)

    if the file does not exist



40
41
42
43
# File 'lib/x/media_uploader.rb', line 40

def upload(client:, file_path:, media_category:, boundary: SecureRandom.hex)
  MediaUploadValidator.validate_file_path!(file_path:)
  upload_binary(client:, content: File.binread(file_path), media_category:, boundary:)
end

#upload_binary(client:, content:, media_category:, boundary: SecureRandom.hex) ⇒ Hash?

Upload binary content to the X API

Examples:

Upload binary content

MediaUploader.upload_binary(client: client, content: data, media_category: "tweet_image")

Parameters:

  • client (Client)

    the X API client

  • content (String)

    the binary content to upload

  • media_category (String)

    the media category

  • boundary (String) (defaults to: SecureRandom.hex)

    the multipart boundary

Returns:

  • (Hash, nil)

    the upload response data

Raises:

  • (ArgumentError)

    if the media category is invalid



56
57
58
59
60
61
# File 'lib/x/media_uploader.rb', line 56

def upload_binary(client:, content:, media_category:, boundary: SecureRandom.hex)
  MediaUploadValidator.validate_media_category!(media_category:)
  upload_body = construct_upload_body(content:, media_category:, boundary:)
  headers = {"Content-Type" => "multipart/form-data; boundary=#{boundary}"}
  client.post("media/upload", upload_body, headers:)&.fetch("data")
end