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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f92c10fcfb6dba2f494d6bd5e0c24bb8cc89662a8dd577d4b8478e744f0f0fa
4
- data.tar.gz: 32082cfe85dfb4fc7ded29a37b80dc25ce68ccb47481af2957c6a0a990a1c09b
3
+ metadata.gz: 0af94cfa8ea419d970609f4aa3fe44af46930d515afb8db7c7ba948ef0e04db1
4
+ data.tar.gz: cd8f2687ec21aebdc8b283f40ef62f0e00244c9fc1c75b232e7092bf06c14477
5
5
  SHA512:
6
- metadata.gz: 0b021607c7e10827384d6d6461b3ba8b9d3f6459996efca1cc276a8e304f9d9ce9c16371e4f6d0a6b136f5ade2cd856d730e38f5f1df94a9d1805f7cf6f1570d
7
- data.tar.gz: 94e26f89f370b871527b4c76f4aeda72ac77093a5012801efc9cd25695d05e5e683f8e3bbcef9342f0747f6e9595904f9060573e5260de017f8fb8806f9cc13d
6
+ metadata.gz: 9c99ec3d458e8139664062eddc8ff36e17bc4e4e83155492855daa715fdbce478a7d2c894ac764d094a4ede8aa318b52a0d8d22687c24ca69bc7ed828b8be3e9
7
+ data.tar.gz: b90c023cc4fff3ecdde4547871a48c77d9a34c5a841a278cb90abe9d5203bf0324cdd837a0161edc53a048f76e502db027096a27fdabe0fdbe7c53c91da50929
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- twitchrb (1.8.0)
4
+ twitchrb (1.8.1)
5
5
  faraday (~> 2.11)
6
6
  ostruct (~> 0.6.0)
7
7
 
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
- def initialize(client_id:, access_token:, adapter: Faraday.default_adapter)
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
@@ -1,5 +1,7 @@
1
1
  module Twitch
2
2
  class Collection
3
+ include Enumerable
4
+
3
5
  attr_reader :data, :total, :cursor
4
6
 
5
7
  def self.from_response(response, type:)
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Twitch
2
- VERSION = "1.8.0"
2
+ VERSION = "1.8.1"
3
3
  end
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.0
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