x 0.15.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eda8225e3c33ea58d75a363a4ee8806397fab2835672ad426890d3ab78d43f0d
4
- data.tar.gz: 633c05ab7dbbcb5ff3b97daa9cea32e219d47c783428b59305a9e9353972a8e5
3
+ metadata.gz: cc174269cbe283b3f360fee2c9a443272d0c1d3aab08cf609d552f22323dabc7
4
+ data.tar.gz: a2f50383c52f0e499f8afe275ce100cd9ca6f8231bd5fdd1f18c1bfdd7bf5191
5
5
  SHA512:
6
- metadata.gz: a65146220ce9312edabd2178ea1fef63cbbd3300d9d08fe694e714bbb1333b3160e0fa30cb1b7a457358c568faa7302afb5511d5f4aa88583af1e9ca3d15f652
7
- data.tar.gz: 906acc33745a72b8c58414ebe8c6b6af7bc234afe932a90a1ac3eb8efcca7c083e5caa42bd733ee0c2aced0fe264c849267e306efc7841e78d0050a2b3cc965f
6
+ metadata.gz: d9d9d70a98d38037f5a9606ec4770bc1fb79b7201a4c6f0a3d6a8a7e9360d360730d42606059fe40365ef008bab229cafe14feef5c3558518528afe2c15f6676
7
+ data.tar.gz: bceff542ee42df84cc8b96df31e311db88bafb7ecec138c2caf7c07d2c106c535750c1cdc603d22e1743586798f79f9d624c9073dee282093f89dc305e886286
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.16.0] - 2025-06-24
2
+ * Remove media_type parameter from non-chunked upload and append methods (f1f38b5)
3
+ * Fix media upload (dcb418a)
4
+ * Add await_processing! method to handle media upload failures (6cfc973)
5
+ * Move media_category in body for media upload (b790636)
6
+
7
+ ## [0.15.4] - 2025-05-02
8
+ * Use dedicated endpoints for chunked media upload (d54d0d0)
9
+
1
10
  ## [0.15.3] - 2025-04-24
2
11
  * Add missing base64 dependency (3ca8512)
3
12
  * Set binary read for media files to be uploaded (fd066e6)
data/README.md CHANGED
@@ -23,7 +23,7 @@ Or, if Bundler is not being used to manage dependencies:
23
23
 
24
24
  ## Usage
25
25
 
26
- First, obtain X credentails from <https://developer.x.com>.
26
+ First, obtain X credentials from <https://developer.x.com>.
27
27
 
28
28
  ```ruby
29
29
  require "x"
@@ -131,9 +131,9 @@ Many thanks to our sponsors (listed in order of when they sponsored this project
131
131
 
132
132
  ## Development
133
133
 
134
- 1. Checkout and repo:
134
+ 1. Clone the repo:
135
135
 
136
- git checkout git@github.com:sferik/x-ruby.git
136
+ git clone git@github.com:sferik/x-ruby.git
137
137
 
138
138
  2. Enter the repo’s directory:
139
139
 
@@ -1,3 +1,4 @@
1
+ require "json"
1
2
  require "securerandom"
2
3
  require "tmpdir"
3
4
 
@@ -16,32 +17,38 @@ module X
16
17
  "png" => PNG_MIME_TYPE, "srt" => SUBRIP_MIME_TYPE, "webp" => WEBP_MIME_TYPE}.freeze
17
18
  PROCESSING_INFO_STATES = %w[failed succeeded].freeze
18
19
 
19
- def upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path, media_category),
20
- boundary: SecureRandom.hex)
20
+ def upload(client:, file_path:, media_category:, boundary: SecureRandom.hex)
21
21
  validate!(file_path:, media_category:)
22
- upload_body = construct_upload_body(file_path:, media_type:, boundary:)
23
- headers = {"Content-Type" => "multipart/form-data, boundary=#{boundary}"}
24
- client.post("media/upload?media_category=#{media_category}", upload_body, headers:)
22
+ upload_body = construct_upload_body(file_path:, media_category:, boundary:)
23
+ headers = {"Content-Type" => "multipart/form-data; boundary=#{boundary}"}
24
+ client.post("media/upload", upload_body, headers:)&.fetch("data")
25
25
  end
26
26
 
27
- def chunked_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path,
28
- media_category), boundary: SecureRandom.hex, chunk_size_mb: 1)
27
+ def chunked_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path, media_category),
28
+ boundary: SecureRandom.hex, chunk_size_mb: 1)
29
29
  validate!(file_path:, media_category:)
30
30
  media = init(client:, file_path:, media_type:, media_category:)
31
31
  chunk_size = chunk_size_mb * BYTES_PER_MB
32
- append(client:, file_paths: split(file_path, chunk_size), media:, media_type:, boundary:)
33
- client.post("media/upload?command=FINALIZE&media_id=#{media["id"]}")&.fetch("data")
32
+ append(client:, file_paths: split(file_path, chunk_size), media:, boundary:)
33
+ client.post("media/upload/#{media["id"]}/finalize")&.fetch("data")
34
34
  end
35
35
 
36
36
  def await_processing(client:, media:)
37
37
  loop do
38
38
  status = client.get("media/upload?command=STATUS&media_id=#{media["id"]}")&.fetch("data")
39
- return status if !status["processing_info"] || PROCESSING_INFO_STATES.include?(status["processing_info"]["state"])
39
+ return status if status.nil? || !status["processing_info"] || PROCESSING_INFO_STATES.include?(status["processing_info"]["state"])
40
40
 
41
41
  sleep status["processing_info"]["check_after_secs"].to_i
42
42
  end
43
43
  end
44
44
 
45
+ def await_processing!(client:, media:)
46
+ status = await_processing(client:, media:)
47
+ raise "Media processing failed" if status&.dig("processing_info", "state") == "failed"
48
+
49
+ status
50
+ end
51
+
45
52
  private
46
53
 
47
54
  def validate!(file_path:, media_category:)
@@ -57,44 +64,39 @@ module X
57
64
  when TWEET_GIF, DM_GIF then GIF_MIME_TYPE
58
65
  when TWEET_VIDEO, DM_VIDEO then MP4_MIME_TYPE
59
66
  when SUBTITLES then SUBRIP_MIME_TYPE
60
- else MIME_TYPE_MAP[File.extname(file_path).delete(".").downcase] || DEFAULT_MIME_TYPE
67
+ else MIME_TYPE_MAP.fetch(File.extname(file_path).delete(".").downcase, DEFAULT_MIME_TYPE)
61
68
  end
62
69
  end
63
70
 
64
71
  def split(file_path, chunk_size)
65
- file_number = -1
66
- file_paths = [] # @type var file_paths: Array[String]
67
-
68
- File.open(file_path, "rb") do |f|
69
- while (chunk = f.read(chunk_size))
70
- path = "#{Dir.mktmpdir}/x#{format("%03d", file_number += 1)}"
71
- File.binwrite(path, chunk)
72
- file_paths << path
73
- end
72
+ file_size = File.size(file_path)
73
+ segment_count = (file_size.to_f / chunk_size).ceil
74
+ (0...segment_count).map do |segment_index|
75
+ segment_path = "#{Dir.mktmpdir}/x#{format("%03d", segment_index + 1)}"
76
+ File.binwrite(segment_path, File.binread(file_path, chunk_size, segment_index * chunk_size))
77
+ segment_path
74
78
  end
75
- file_paths
76
79
  end
77
80
 
78
81
  def init(client:, file_path:, media_type:, media_category:)
79
82
  total_bytes = File.size(file_path)
80
- query = "command=INIT&media_type=#{media_type}&media_category=#{media_category}&total_bytes=#{total_bytes}"
81
- client.post("media/upload?#{query}")&.fetch("data")
83
+ data = {media_type:, media_category:, total_bytes:}.to_json
84
+ client.post("media/upload/initialize", data)&.fetch("data")
82
85
  end
83
86
 
84
- def append(client:, file_paths:, media:, media_type:, boundary: SecureRandom.hex)
87
+ def append(client:, file_paths:, media:, boundary: SecureRandom.hex)
85
88
  threads = file_paths.map.with_index do |file_path, index|
86
89
  Thread.new do
87
- upload_body = construct_upload_body(file_path:, media_type:, boundary:)
88
- query = "command=APPEND&media_id=#{media["id"]}&segment_index=#{index}"
89
- headers = {"Content-Type" => "multipart/form-data, boundary=#{boundary}"}
90
- upload_chunk(client:, query:, upload_body:, file_path:, headers:)
90
+ upload_body = construct_upload_body(file_path:, segment_index: index, boundary:)
91
+ headers = {"Content-Type" => "multipart/form-data; boundary=#{boundary}"}
92
+ upload_chunk(client:, media_id: media["id"], upload_body:, file_path:, headers:)
91
93
  end
92
94
  end
93
95
  threads.each(&:join)
94
96
  end
95
97
 
96
- def upload_chunk(client:, query:, upload_body:, file_path:, headers: {})
97
- client.post("media/upload?#{query}", upload_body, headers:)
98
+ def upload_chunk(client:, media_id:, upload_body:, file_path:, headers: {})
99
+ client.post("media/upload/#{media_id}/append", upload_body, headers:)
98
100
  rescue NetworkError, ServerError
99
101
  retries ||= 0
100
102
  ((retries += 1) < MAX_RETRIES) ? retry : raise
@@ -108,10 +110,13 @@ module X
108
110
  Dir.delete(dirname) if Dir.empty?(dirname)
109
111
  end
110
112
 
111
- def construct_upload_body(file_path:, media_type:, boundary: SecureRandom.hex)
112
- "--#{boundary}\r\n" \
113
+ def construct_upload_body(file_path:, media_category: nil, segment_index: nil, boundary: SecureRandom.hex)
114
+ body = ""
115
+ body += "--#{boundary}\r\nContent-Disposition: form-data; name=\"segment_index\"\r\n\r\n#{segment_index}\r\n" if segment_index
116
+ body += "--#{boundary}\r\nContent-Disposition: form-data; name=\"media_category\"\r\n\r\n#{media_category}\r\n" if media_category
117
+ "#{body}--#{boundary}\r\n" \
113
118
  "Content-Disposition: form-data; name=\"media\"; filename=\"#{File.basename(file_path)}\"\r\n" \
114
- "Content-Type: #{media_type}\r\n\r\n" \
119
+ "Content-Type: #{DEFAULT_MIME_TYPE}\r\n\r\n" \
115
120
  "#{File.binread(file_path)}\r\n" \
116
121
  "--#{boundary}--\r\n"
117
122
  end
data/lib/x/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require "rubygems/version"
2
2
 
3
3
  module X
4
- VERSION = Gem::Version.create("0.15.3")
4
+ VERSION = Gem::Version.create("0.16.0")
5
5
  end
data/sig/x.rbs CHANGED
@@ -265,19 +265,20 @@ module X
265
265
  PROCESSING_INFO_STATES: Array[String]
266
266
  extend MediaUploader
267
267
 
268
- def upload: (client: Client, file_path: String, media_category: String, ?media_type: String, ?boundary: String) -> untyped
268
+ def upload: (client: Client, file_path: String, media_category: String, ?boundary: String) -> untyped
269
269
  def chunked_upload: (client: Client, file_path: String, media_category: String, ?media_type: String, ?boundary: String, ?chunk_size_mb: Integer) -> untyped
270
270
  def await_processing: (client: Client, media: untyped) -> untyped
271
+ def await_processing!: (client: Client, media: untyped) -> untyped
271
272
 
272
273
  private
273
274
  def validate!: (file_path: String, media_category: String) -> nil
274
275
  def infer_media_type: (String file_path, String media_category) -> String
275
276
  def split: (String file_path, Integer chunk_size) -> Array[String]
276
277
  def init: (client: Client, file_path: String, media_type: String, media_category: String) -> untyped
277
- def append: (client: Client, file_paths: Array[String], media: untyped, media_type: String, ?boundary: String) -> Array[String]
278
- def upload_chunk: (client: Client, query: String, upload_body: String, file_path: String, ?headers: Hash[String, String]) -> Integer?
278
+ def append: (client: Client, file_paths: Array[String], media: untyped, ?boundary: String) -> Array[String]
279
+ def upload_chunk: (client: Client, media_id: String, upload_body: String, file_path: String, ?headers: Hash[String, String]) -> Integer?
279
280
  def cleanup_file: (String file_path) -> Integer?
280
281
  def finalize: (client: Client, media: untyped) -> untyped
281
- def construct_upload_body: (file_path: String, media_type: String, ?boundary: String) -> String
282
+ def construct_upload_body: (file_path: String, ?media_category: String, ?segment_index: Integer, ?boundary: String) -> String
282
283
  end
283
284
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: x
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.3
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin
@@ -72,12 +72,13 @@ licenses:
72
72
  - MIT
73
73
  metadata:
74
74
  allowed_push_host: https://rubygems.org
75
- rubygems_mfa_required: 'true'
76
- homepage_uri: https://sferik.github.io/x-ruby
77
- source_code_uri: https://github.com/sferik/x-ruby
78
- changelog_uri: https://github.com/sferik/x-ruby/blob/master/CHANGELOG.md
79
75
  bug_tracker_uri: https://github.com/sferik/x-ruby/issues
76
+ changelog_uri: https://github.com/sferik/x-ruby/blob/master/CHANGELOG.md
80
77
  documentation_uri: https://rubydoc.info/gems/x/
78
+ funding_uri: https://github.com/sponsors/sferik/
79
+ homepage_uri: https://sferik.github.io/x-ruby
80
+ rubygems_mfa_required: 'true'
81
+ source_code_uri: https://github.com/sferik/x-ruby
81
82
  rdoc_options: []
82
83
  require_paths:
83
84
  - lib
@@ -92,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
93
  - !ruby/object:Gem::Version
93
94
  version: '0'
94
95
  requirements: []
95
- rubygems_version: 3.6.8
96
+ rubygems_version: 3.6.9
96
97
  specification_version: 4
97
98
  summary: A Ruby interface to the X API.
98
99
  test_files: []