mirror of
https://github.com/ryankazokas/turbovault-app.git
synced 2026-04-17 05:32:52 +00:00
Adds types
This commit is contained in:
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user