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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +3 -3
- data/lib/x/media_uploader.rb +38 -33
- data/lib/x/version.rb +1 -1
- data/sig/x.rbs +5 -4
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc174269cbe283b3f360fee2c9a443272d0c1d3aab08cf609d552f22323dabc7
|
4
|
+
data.tar.gz: a2f50383c52f0e499f8afe275ce100cd9ca6f8231bd5fdd1f18c1bfdd7bf5191
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.
|
134
|
+
1. Clone the repo:
|
135
135
|
|
136
|
-
git
|
136
|
+
git clone git@github.com:sferik/x-ruby.git
|
137
137
|
|
138
138
|
2. Enter the repo’s directory:
|
139
139
|
|
data/lib/x/media_uploader.rb
CHANGED
@@ -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:,
|
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:,
|
23
|
-
headers = {"Content-Type" => "multipart/form-data
|
24
|
-
client.post("media/upload
|
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
|
-
|
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:,
|
33
|
-
client.post("media/upload
|
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
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
81
|
-
client.post("media/upload
|
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:,
|
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:,
|
88
|
-
|
89
|
-
|
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:,
|
97
|
-
client.post("media/upload
|
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:,
|
112
|
-
"
|
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: #{
|
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
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, ?
|
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,
|
278
|
-
def upload_chunk: (client: Client,
|
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,
|
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.
|
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.
|
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: []
|