twitchrb 1.8.0 → 1.8.1
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/Gemfile.lock +1 -1
- data/README.md +81 -0
- data/lib/twitch/client.rb +15 -3
- data/lib/twitch/collection.rb +2 -0
- data/lib/twitch/error_generator.rb +28 -0
- data/lib/twitch/rate_limiter.rb +99 -0
- data/lib/twitch/resource.rb +27 -0
- data/lib/twitch/resources/channels.rb +4 -0
- data/lib/twitch/version.rb +1 -1
- data/lib/twitch.rb +2 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0af94cfa8ea419d970609f4aa3fe44af46930d515afb8db7c7ba948ef0e04db1
|
|
4
|
+
data.tar.gz: cd8f2687ec21aebdc8b283f40ef62f0e00244c9fc1c75b232e7092bf06c14477
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c99ec3d458e8139664062eddc8ff36e17bc4e4e83155492855daa715fdbce478a7d2c894ac764d094a4ede8aa318b52a0d8d22687c24ca69bc7ed828b8be3e9
|
|
7
|
+
data.tar.gz: b90c023cc4fff3ecdde4547871a48c77d9a34c5a841a278cb90abe9d5203bf0324cdd837a0161edc53a048f76e502db027096a27fdabe0fdbe7c53c91da50929
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -68,6 +68,82 @@ results.cursor
|
|
|
68
68
|
#=> Twitch::Collection
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
### Rate Limiting
|
|
72
|
+
|
|
73
|
+
The Twitch API has rate limits to ensure fair usage. TwitchRB automatically tracks rate limit information from API responses and can warn you when approaching limits.
|
|
74
|
+
|
|
75
|
+
#### Automatic Rate Limit Tracking
|
|
76
|
+
|
|
77
|
+
By default, the client tracks rate limit headers from all API responses:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
@client = Twitch::Client.new(client_id: "abc123", access_token: "xyz123")
|
|
81
|
+
|
|
82
|
+
# Make an API call
|
|
83
|
+
user = @client.users.retrieve(id: 12345)
|
|
84
|
+
|
|
85
|
+
# Check current rate limit status
|
|
86
|
+
@client.rate_limiter.remaining #=> 119
|
|
87
|
+
@client.rate_limiter.limit #=> 120
|
|
88
|
+
@client.rate_limiter.reset_at #=> 1701619234 (Unix timestamp)
|
|
89
|
+
@client.rate_limiter.status #=> "119/120 requests remaining"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### Configuring Rate Limit Warnings
|
|
93
|
+
|
|
94
|
+
You can configure the client to warn when approaching the rate limit:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# With logging enabled (e.g., Rails logger)
|
|
98
|
+
@client = Twitch::Client.new(
|
|
99
|
+
client_id: "abc123",
|
|
100
|
+
access_token: "xyz123",
|
|
101
|
+
logger: Rails.logger,
|
|
102
|
+
rate_limit_threshold: 10 # Warn when remaining <= 10
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# When approaching the limit, a warning will be logged:
|
|
106
|
+
# "Twitch API rate limit approaching: 5/120 requests remaining. Resets in 45 seconds."
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Handling Rate Limit Errors
|
|
110
|
+
|
|
111
|
+
When you hit the rate limit (429 response), the library raises a `Twitch::Errors::RateLimitError` with detailed information:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
begin
|
|
115
|
+
@client.users.retrieve(id: 12345)
|
|
116
|
+
rescue Twitch::Errors::RateLimitError => e
|
|
117
|
+
puts e.message
|
|
118
|
+
#=> "Error 429: Your request exceeded the API rate limit. (Resets at 14:23:45, 0/120 requests)"
|
|
119
|
+
|
|
120
|
+
puts e.reset_at #=> 1701619234 (Unix timestamp)
|
|
121
|
+
puts e.remaining #=> 0
|
|
122
|
+
puts e.limit #=> 120
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### Rate Limiter Methods
|
|
127
|
+
|
|
128
|
+
The `rate_limiter` object has several useful methods:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# Check if approaching limit
|
|
132
|
+
@client.rate_limiter.approaching_limit?(threshold: 10) #=> true/false
|
|
133
|
+
|
|
134
|
+
# Get seconds until reset
|
|
135
|
+
@client.rate_limiter.reset_in #=> 45
|
|
136
|
+
|
|
137
|
+
# Check if rate limited
|
|
138
|
+
@client.rate_limiter.rate_limited? #=> false
|
|
139
|
+
|
|
140
|
+
# Wait until rate limit resets (useful for batch operations)
|
|
141
|
+
@client.rate_limiter.wait_if_rate_limited
|
|
142
|
+
|
|
143
|
+
# Reset tracking (useful when creating new client instances)
|
|
144
|
+
@client.rate_limiter.reset
|
|
145
|
+
```
|
|
146
|
+
|
|
71
147
|
### OAuth
|
|
72
148
|
|
|
73
149
|
This library includes the ability to create, refresh and revoke OAuth tokens.
|
|
@@ -196,6 +272,11 @@ attributes = {title: "My new title"}
|
|
|
196
272
|
# Required scope: channel:read:stream_key
|
|
197
273
|
@client.channels.stream_key(broadcaster_id: 123123)
|
|
198
274
|
=> #<Twitch::StreamKey stream_key="live_abc123">
|
|
275
|
+
|
|
276
|
+
# Run a commercial
|
|
277
|
+
# broadcaster_id must match the currently authenticated user
|
|
278
|
+
# Required scope: channel:edit:commercial
|
|
279
|
+
@client.channels.commercial(broadcaster_id: 123123, length: 30)
|
|
199
280
|
```
|
|
200
281
|
|
|
201
282
|
### Videos
|
data/lib/twitch/client.rb
CHANGED
|
@@ -2,12 +2,24 @@ module Twitch
|
|
|
2
2
|
class Client
|
|
3
3
|
BASE_URL = "https://api.twitch.tv/helix"
|
|
4
4
|
|
|
5
|
-
attr_reader :client_id, :access_token, :adapter
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
attr_reader :client_id, :access_token, :adapter, :rate_limiter
|
|
6
|
+
attr_reader :rate_limit_threshold, :auto_retry_rate_limit, :logger
|
|
7
|
+
|
|
8
|
+
def initialize(
|
|
9
|
+
client_id:,
|
|
10
|
+
access_token:,
|
|
11
|
+
adapter: Faraday.default_adapter,
|
|
12
|
+
rate_limit_threshold: 10,
|
|
13
|
+
auto_retry_rate_limit: true,
|
|
14
|
+
logger: nil
|
|
15
|
+
)
|
|
8
16
|
@client_id = client_id
|
|
9
17
|
@access_token = access_token
|
|
10
18
|
@adapter = adapter
|
|
19
|
+
@rate_limit_threshold = rate_limit_threshold
|
|
20
|
+
@auto_retry_rate_limit = auto_retry_rate_limit
|
|
21
|
+
@logger = logger
|
|
22
|
+
@rate_limiter = RateLimiter.new(logger: logger)
|
|
11
23
|
end
|
|
12
24
|
|
|
13
25
|
def users
|
data/lib/twitch/collection.rb
CHANGED
|
@@ -96,6 +96,34 @@ module Twitch
|
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
class RateLimitError < TooManyRequestsError
|
|
100
|
+
attr_reader :reset_at, :remaining, :limit
|
|
101
|
+
|
|
102
|
+
def initialize(response_body, http_status_code, reset_at: nil, remaining: nil, limit: nil)
|
|
103
|
+
@reset_at = reset_at
|
|
104
|
+
@remaining = remaining
|
|
105
|
+
@limit = limit
|
|
106
|
+
super(response_body, http_status_code)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def build_message
|
|
112
|
+
base_message = super
|
|
113
|
+
rate_info = build_rate_info
|
|
114
|
+
"#{base_message}#{rate_info}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_rate_info
|
|
118
|
+
return "" if reset_at.nil?
|
|
119
|
+
|
|
120
|
+
reset_time = Time.at(reset_at).strftime("%H:%M:%S")
|
|
121
|
+
info = " (Resets at #{reset_time}"
|
|
122
|
+
info += ", #{remaining}/#{limit} requests" if remaining && limit
|
|
123
|
+
info + ")"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
99
127
|
class InternalError < ErrorGenerator
|
|
100
128
|
private
|
|
101
129
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module Twitch
|
|
2
|
+
class RateLimiter
|
|
3
|
+
# Twitch API rate limit header names
|
|
4
|
+
LIMIT_HEADER = "ratelimit-limit".freeze
|
|
5
|
+
REMAINING_HEADER = "ratelimit-remaining".freeze
|
|
6
|
+
RESET_HEADER = "ratelimit-reset".freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :limit, :remaining, :reset_at, :logger
|
|
9
|
+
|
|
10
|
+
def initialize(logger: nil)
|
|
11
|
+
@limit = nil
|
|
12
|
+
@remaining = nil
|
|
13
|
+
@reset_at = nil
|
|
14
|
+
@logger = logger
|
|
15
|
+
@last_warn_at = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Update rate limit info from response headers
|
|
19
|
+
def update(response_headers)
|
|
20
|
+
return unless response_headers
|
|
21
|
+
|
|
22
|
+
@limit = response_headers[LIMIT_HEADER]&.to_i
|
|
23
|
+
@remaining = response_headers[REMAINING_HEADER]&.to_i
|
|
24
|
+
@reset_at = response_headers[RESET_HEADER]&.to_i
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Check if we're approaching the rate limit
|
|
28
|
+
def approaching_limit?(threshold: 10)
|
|
29
|
+
return false if remaining.nil? || limit.nil?
|
|
30
|
+
|
|
31
|
+
remaining <= threshold
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get seconds until rate limit resets
|
|
35
|
+
def reset_in
|
|
36
|
+
return nil if reset_at.nil?
|
|
37
|
+
|
|
38
|
+
seconds = reset_at - Time.now.to_i
|
|
39
|
+
[ seconds, 0 ].max # Ensure non-negative
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Check if rate limited (no remaining requests)
|
|
43
|
+
def rate_limited?
|
|
44
|
+
remaining == 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Log rate limit warning if approaching threshold
|
|
48
|
+
def warn_if_approaching(threshold: 10)
|
|
49
|
+
return unless approaching_limit?(threshold: threshold)
|
|
50
|
+
return unless logger
|
|
51
|
+
return if recently_warned?
|
|
52
|
+
|
|
53
|
+
@last_warn_at = Time.now
|
|
54
|
+
logger.warn(
|
|
55
|
+
"Twitch API rate limit approaching: #{remaining}/#{limit} requests remaining. " \
|
|
56
|
+
"Resets in #{reset_in} seconds."
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Wait if rate limited, with exponential backoff
|
|
61
|
+
def wait_if_rate_limited(base_wait: 1.0, max_wait: 60.0)
|
|
62
|
+
return if !rate_limited? || reset_in.nil?
|
|
63
|
+
|
|
64
|
+
wait_seconds = [ reset_in + 1, max_wait ].min # Add 1 second buffer
|
|
65
|
+
|
|
66
|
+
if logger
|
|
67
|
+
logger.warn("Rate limited. Waiting #{wait_seconds} seconds until reset at #{Time.at(reset_at)}")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sleep(wait_seconds)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Reset rate limit tracking
|
|
74
|
+
def reset
|
|
75
|
+
@limit = nil
|
|
76
|
+
@remaining = nil
|
|
77
|
+
@reset_at = nil
|
|
78
|
+
@last_warn_at = nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get formatted status string
|
|
82
|
+
def status
|
|
83
|
+
if limit.nil? || remaining.nil?
|
|
84
|
+
"Rate limit info not available"
|
|
85
|
+
else
|
|
86
|
+
"#{remaining}/#{limit} requests remaining"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def recently_warned?
|
|
93
|
+
return false if @last_warn_at.nil?
|
|
94
|
+
|
|
95
|
+
# Don't warn more than once per minute
|
|
96
|
+
(Time.now - @last_warn_at) < 60
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/twitch/resource.rb
CHANGED
|
@@ -29,6 +29,9 @@ module Twitch
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def handle_response(response)
|
|
32
|
+
# Extract and update rate limit info from response headers
|
|
33
|
+
update_rate_limit(response)
|
|
34
|
+
|
|
32
35
|
return true if response.status == 204
|
|
33
36
|
return response unless error?(response)
|
|
34
37
|
|
|
@@ -41,8 +44,32 @@ module Twitch
|
|
|
41
44
|
end
|
|
42
45
|
|
|
43
46
|
def raise_error(response)
|
|
47
|
+
# Special handling for rate limit errors
|
|
48
|
+
if response.status == 429
|
|
49
|
+
raise_rate_limit_error(response)
|
|
50
|
+
end
|
|
51
|
+
|
|
44
52
|
error = Twitch::ErrorFactory.create(response.body, response.status)
|
|
45
53
|
raise error if error
|
|
46
54
|
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def update_rate_limit(response)
|
|
59
|
+
client.rate_limiter.update(response.headers)
|
|
60
|
+
client.rate_limiter.warn_if_approaching(threshold: client.rate_limit_threshold)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def raise_rate_limit_error(response)
|
|
64
|
+
headers = response.headers
|
|
65
|
+
error = Twitch::Errors::RateLimitError.new(
|
|
66
|
+
response.body,
|
|
67
|
+
response.status,
|
|
68
|
+
reset_at: headers["ratelimit-reset"]&.to_i,
|
|
69
|
+
remaining: headers["ratelimit-remaining"]&.to_i,
|
|
70
|
+
limit: headers["ratelimit-limit"]&.to_i
|
|
71
|
+
)
|
|
72
|
+
raise error
|
|
73
|
+
end
|
|
47
74
|
end
|
|
48
75
|
end
|
|
@@ -50,5 +50,9 @@ module Twitch
|
|
|
50
50
|
response = get_request("streams/key?broadcaster_id=#{broadcaster_id}")
|
|
51
51
|
StreamKey.new(response.body.dig("data")[0])
|
|
52
52
|
end
|
|
53
|
+
|
|
54
|
+
def commercial(broadcaster_id:, length:)
|
|
55
|
+
post_request("channels/commercial", body: { broadcaster_id: broadcaster_id, length: length })
|
|
56
|
+
end
|
|
53
57
|
end
|
|
54
58
|
end
|
data/lib/twitch/version.rb
CHANGED
data/lib/twitch.rb
CHANGED
|
@@ -9,6 +9,8 @@ module Twitch
|
|
|
9
9
|
autoload :ErrorGenerator, "twitch/error_generator"
|
|
10
10
|
autoload :ErrorFactory, "twitch/error_generator"
|
|
11
11
|
|
|
12
|
+
autoload :RateLimiter, "twitch/rate_limiter"
|
|
13
|
+
|
|
12
14
|
autoload :Client, "twitch/client"
|
|
13
15
|
autoload :Collection, "twitch/collection"
|
|
14
16
|
autoload :Resource, "twitch/resource"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: twitchrb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.8.
|
|
4
|
+
version: 1.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dean Perry
|
|
@@ -107,6 +107,7 @@ files:
|
|
|
107
107
|
- lib/twitch/objects/video.rb
|
|
108
108
|
- lib/twitch/objects/vip.rb
|
|
109
109
|
- lib/twitch/objects/warning.rb
|
|
110
|
+
- lib/twitch/rate_limiter.rb
|
|
110
111
|
- lib/twitch/resource.rb
|
|
111
112
|
- lib/twitch/resources/announcements.rb
|
|
112
113
|
- lib/twitch/resources/automod.rb
|