Adds types

This commit is contained in:
2026-03-29 02:37:49 -04:00
parent 63276ef8ca
commit 323484a33a
44 changed files with 1273 additions and 121 deletions

View File

@@ -1,26 +1,31 @@
require 'net/http'
require 'json'
require 'uri'
require "net/http"
require "json"
require "uri"
# typed: true
class IgdbService
extend T::Sig
BASE_URL = "https://api.igdb.com/v4"
TOKEN_URL = "https://id.twitch.tv/oauth2/token"
CACHE_KEY = "igdb_access_token"
class ApiError < StandardError; end
class RateLimitError < StandardError; end
sig { void }
def initialize
@client_id = ENV.fetch("IGDB_CLIENT_ID")
@client_secret = ENV.fetch("IGDB_CLIENT_SECRET")
@access_token = get_or_refresh_token
@client_id = T.let(ENV.fetch("IGDB_CLIENT_ID"), String)
@client_secret = T.let(ENV.fetch("IGDB_CLIENT_SECRET"), String)
@access_token = T.let(get_or_refresh_token, String)
end
# Search for games by title and platform
# Returns array of matches with confidence scores
sig { params(title: String, platform: T.nilable(String), limit: Integer).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
def search_game(title, platform = nil, limit = 3)
platform_filter = platform_filter_query(platform)
query = <<~QUERY
search "#{sanitize_search_term(title)}";
fields id, name, slug, cover.url, summary, first_release_date, platforms.name, genres.name;
@@ -29,7 +34,7 @@ class IgdbService
QUERY
results = post("/games", query)
return [] if results.empty?
# Calculate confidence scores and format results
@@ -43,6 +48,7 @@ class IgdbService
end
# Get specific game by IGDB ID
sig { params(igdb_id: Integer).returns(T.nilable(T::Hash[String, T.untyped])) }
def get_game(igdb_id)
query = <<~QUERY
fields id, name, slug, cover.url, summary, first_release_date, platforms.name, genres.name;
@@ -56,6 +62,7 @@ class IgdbService
private
# Get cached token or generate a new one
sig { returns(String) }
def get_or_refresh_token
# Check if we have a cached token
cached_token = Rails.cache.read(CACHE_KEY)
@@ -66,27 +73,28 @@ class IgdbService
end
# Generate a new access token from Twitch
sig { returns(String) }
def generate_access_token
uri = URI(TOKEN_URL)
uri.query = URI.encode_www_form({
client_id: @client_id,
client_secret: @client_secret,
grant_type: 'client_credentials'
grant_type: "client_credentials"
})
response = Net::HTTP.post(uri, '')
response = Net::HTTP.post(uri, "")
if response.code.to_i == 200
data = JSON.parse(response.body)
token = data['access_token']
expires_in = data['expires_in'] # seconds until expiration (usually ~5 million seconds / ~60 days)
token = data["access_token"]
expires_in = data["expires_in"] # seconds until expiration (usually ~5 million seconds / ~60 days)
# Cache token for 90% of its lifetime to be safe
cache_duration = (expires_in * 0.9).to_i
Rails.cache.write(CACHE_KEY, token, expires_in: cache_duration)
Rails.logger.info("Generated new IGDB access token (expires in #{expires_in / 86400} days)")
token
else
raise ApiError, "Failed to get IGDB token: #{response.code} - #{response.body}"
@@ -96,12 +104,13 @@ class IgdbService
raise ApiError, "Cannot authenticate with IGDB: #{e.message}"
end
sig { params(endpoint: String, body: String, retry_count: Integer).returns(T::Array[T::Hash[String, T.untyped]]) }
def post(endpoint, body, retry_count = 0)
uri = URI("#{BASE_URL}#{endpoint}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path)
request["Client-ID"] = @client_id
request["Authorization"] = "Bearer #{@access_token}"
@@ -114,9 +123,9 @@ class IgdbService
sleep(0.3)
response = http.request(request)
Rails.logger.info("IGDB Response: #{response.code} - #{response.body[0..200]}")
case response.code.to_i
when 200
JSON.parse(response.body)
@@ -126,7 +135,7 @@ class IgdbService
Rails.logger.warn("IGDB token invalid, refreshing...")
Rails.cache.delete(CACHE_KEY)
@access_token = generate_access_token
return post(endpoint, body, retry_count + 1)
post(endpoint, body, retry_count + 1)
else
Rails.logger.error("IGDB authentication failed after token refresh")
raise ApiError, "IGDB authentication failed"
@@ -145,6 +154,7 @@ class IgdbService
[]
end
sig { params(platform: T.nilable(String)).returns(String) }
def platform_filter_query(platform)
return "" unless platform
@@ -154,11 +164,13 @@ class IgdbService
"where platforms = (#{igdb_platform_id});"
end
sig { params(term: String).returns(String) }
def sanitize_search_term(term)
# Escape quotes and remove special characters that might break the query
term.gsub('"', '\\"').gsub(/[^\w\s:'-]/, "")
end
sig { params(search_title: String, result_title: String, search_platform: T.nilable(String), result_platforms: T.nilable(T::Array[T::Hash[String, T.untyped]])).returns(Float) }
def calculate_confidence(search_title, result_title, search_platform, result_platforms)
score = 0.0
@@ -183,7 +195,7 @@ class IgdbService
if search_platform && result_platforms
platform_names = result_platforms.map { |p| p["name"].downcase }
igdb_platform_id = IgdbPlatformMapping.igdb_id_for_platform(search_platform)
# Check if our platform is in the result platforms
if igdb_platform_id
# Exact platform match
@@ -197,27 +209,28 @@ class IgdbService
score.round(2)
end
sig { params(game: T::Hash[String, T.untyped], confidence: Float).returns(T::Hash[Symbol, T.untyped]) }
def format_game_result(game, confidence)
cover_id = game.dig("cover", "url")&.split("/")&.last&.sub(".jpg", "")
platform_name = if game["platforms"]&.any?
game["platforms"].first["name"]
else
else
"Unknown"
end
end
release_date = if game["first_release_date"]
Time.at(game["first_release_date"]).to_date
else
else
nil
end
end
# Extract genre names
genre_names = if game["genres"]&.any?
game["genres"].map { |g| g["name"] }
else
else
[]
end
end
{
igdb_id: game["id"],