|
# Run this by setting VGLIST_USER_EMAIL and VGLIST_USER_TOKEN environment |
|
# variables to the user's email and token respectively. The Grouvee CSV |
|
# must be in the same directory, and named grouvee.csv. |
|
|
|
require 'bundler/inline' |
|
|
|
gemfile do |
|
source 'https://rubygems.org' |
|
|
|
gem 'graphql-client', '~> 0.16.0' |
|
gem 'ruby-progressbar', '~> 1.10' |
|
end |
|
|
|
require "uri" |
|
require "csv" |
|
require "json" |
|
require "graphql/client" |
|
require "graphql/client/http" |
|
|
|
# Gets the grouvee library from a grouvee.csv file in the directory this script is in. |
|
def get_grouvee_library |
|
grouvee_library = [] |
|
|
|
# TODO: Map all of the other possible statuses on Grouvee. |
|
completion_map = { |
|
"Main Story" => "COMPLETED", |
|
"Main Story + Extras" => "FULLY_COMPLETED" |
|
} |
|
|
|
# Map of Grouvee platform names to vglist platform IDs. |
|
platform_map = { |
|
"PC": "1", |
|
"Linux": "19", |
|
"PlayStation": "13", |
|
"Nintendo 64": "30", |
|
"Nintendo Entertainment System": "17", |
|
"Xbox": "22", |
|
"Nintendo 3DS": "32", |
|
"Nintendo DS": "9", |
|
"Dreamcast": "31", |
|
"PlayStation 2": "2", |
|
"Android": "14", |
|
"Xbox 360": "8", |
|
"PlayStation Portable": "23", |
|
"Super Nintendo Entertainment System": "10", |
|
"Nintendo 3DS eShop": "32", |
|
"PlayStation 4": "16", |
|
"Arcade": "18", |
|
"Game Boy Advance": "24", |
|
"Genesis": "20", |
|
"GameCube": "26", |
|
"PlayStation 3": "7", |
|
"Game Boy Color": "41", |
|
"Xbox 360 Games Store": "8", |
|
"New Nintendo 3DS": "32" |
|
} |
|
|
|
CSV.foreach( |
|
File.join(File.dirname(__FILE__), 'grouvee.csv'), |
|
skip_blanks: true, |
|
headers: true |
|
) do |csv_row| |
|
|
|
dates = JSON.parse(csv_row['dates']) |
|
statuses = JSON.parse(csv_row['statuses']) |
|
|
|
if csv_row['rating'].nil? |
|
rating = nil |
|
else |
|
rating = csv_row['rating'].to_i * 20 |
|
end |
|
|
|
platforms = [] |
|
unless csv_row['platforms'].nil? |
|
JSON.parse(csv_row['platforms']).keys.each do |key| |
|
next unless platform_map.keys.include?(key.to_sym) |
|
platforms << platform_map[key.to_sym] |
|
end |
|
|
|
platforms.uniq! |
|
end |
|
|
|
if dates.size > 0 |
|
hours_played = (dates.first['seconds_played'].to_f / 3600).round(2) |
|
start_date = Date.parse(dates.first['date_started']).strftime("%F") if dates.first['date_started'] != "None" |
|
completion_date = Date.parse(dates.first['date_finished']).strftime("%F") if dates.first['date_finished'] != "None" |
|
if dates.first['level_of_completion'].nil? |
|
completion_status = nil |
|
else |
|
completion_status = completion_map[dates.first['level_of_completion']] |
|
end |
|
else |
|
hours_played = nil |
|
start_date = nil |
|
completion_date = nil |
|
completion_status = nil |
|
end |
|
|
|
grouvee_library << { |
|
name: csv_row['name'], |
|
giantbomb_id: csv_row['giantbomb_id'].to_i, |
|
hours_played: hours_played, |
|
start_date: start_date, |
|
completion_date: completion_date, |
|
completion_status: completion_status, |
|
rating: rating, |
|
platforms: platforms |
|
} |
|
end |
|
|
|
return grouvee_library |
|
end |
|
|
|
class VGListAPI |
|
def http |
|
GraphQL::Client::HTTP.new("https://vglist.co/graphql") do |
|
def headers(context) |
|
# Set any HTTP headers |
|
{ |
|
"User-Agent": "Grouvee to vglist importer v1.2", |
|
"X-User-Email": ENV["VGLIST_USER_EMAIL"], |
|
"X-User-Token": ENV["VGLIST_USER_TOKEN"], |
|
"Content-Type": "application/json", |
|
"Accept": "*/*" |
|
} |
|
end |
|
end |
|
end |
|
|
|
# Fetch latest schema on init, this will make a network request |
|
def schema |
|
GraphQL::Client.load_schema(http) |
|
end |
|
|
|
def client |
|
GraphQL::Client.new(schema: schema, execute: http) |
|
end |
|
end |
|
|
|
vglist_api = VGListAPI.new |
|
|
|
# Query to find games by their GiantBomb IDs. |
|
GameFromGiantBombIdQuery = vglist_api.client.parse <<-'GRAPHQL' |
|
query($giantbombId: String) { |
|
game(giantbombId: $giantbombId) { |
|
id |
|
name |
|
} |
|
} |
|
GRAPHQL |
|
|
|
# Mutation to add a game to the user's library. |
|
AddGameToLibraryMutation = vglist_api.client.parse <<-'GRAPHQL' |
|
mutation( |
|
$id: ID!, |
|
$hoursPlayed: Float, |
|
$rating: Int, |
|
$completionStatus: GamePurchaseCompletionStatus, |
|
$startDate: ISO8601Date, |
|
$completionDate: ISO8601Date, |
|
$platforms: [ID] |
|
) { |
|
addGameToLibrary( |
|
gameId: $id, |
|
hoursPlayed: $hoursPlayed, |
|
completionStatus: $completionStatus, |
|
rating: $rating, |
|
startDate: $startDate, |
|
completionDate: $completionDate, |
|
platforms: $platforms |
|
) { |
|
gamePurchase { |
|
game { |
|
id |
|
name |
|
} |
|
hoursPlayed |
|
completionStatus |
|
rating |
|
startDate |
|
completionDate |
|
platforms { |
|
nodes { |
|
id |
|
name |
|
} |
|
} |
|
} |
|
} |
|
} |
|
GRAPHQL |
|
|
|
grouvee_library = get_grouvee_library |
|
puts "Found #{grouvee_library.count} games in grouvee.csv." |
|
|
|
puts "Importing #{grouvee_library.count} games from Grouvee library..." |
|
|
|
# Create a progress bar for tracking the importer's progress. |
|
progress_bar = ProgressBar.create( |
|
total: grouvee_library.count, |
|
format: "\e[0;32m%c/%C |%b>%i| %e\e[0m" |
|
) |
|
|
|
added_games_count = 0 |
|
not_found_count = 0 |
|
not_found_games = [] |
|
error_count = 0 |
|
|
|
# Iterate through every game in the user's Grouvee Library. |
|
grouvee_library.each do |grouvee_game| |
|
# Rate limit the importer to prevent the server from being overloaded. |
|
sleep 1 |
|
|
|
# Skip if there's no GiantBomb ID. |
|
if grouvee_game[:giantbomb_id].nil? |
|
progress_bar.log "#{grouvee_game[:name]} has no GiantBomb ID, skipping..." |
|
progress_bar.increment |
|
next |
|
end |
|
|
|
# Search for the game on vglist by its giantbombId. |
|
game_result = vglist_api.client.query(GameFromGiantBombIdQuery, variables: { |
|
giantbombId: "3030-#{grouvee_game[:giantbomb_id]}" |
|
}) |
|
game_id = game_result.data.game&.id |
|
|
|
# Skip if no game is found. |
|
if game_id.nil? |
|
progress_bar.log "No vglist entry found for #{grouvee_game[:name]} (GiantBomb ID '3030-#{grouvee_game[:giantbomb_id]}'), skipping..." |
|
progress_bar.increment |
|
not_found_count += 1 |
|
not_found_games << { name: grouvee_game[:name], giantbomb_id: "3030-#{grouvee_game[:giantbomb_id]}" } |
|
next |
|
end |
|
|
|
variables = { |
|
id: game_id, |
|
hoursPlayed: grouvee_game[:hours_played], |
|
rating: grouvee_game[:rating], |
|
completionStatus: grouvee_game[:completion_status], |
|
startDate: grouvee_game[:start_date], |
|
completionDate: grouvee_game[:completion_date], |
|
platforms: grouvee_game[:platforms] |
|
} |
|
|
|
variables = variables.delete_if { |_key, val| val.nil? } |
|
|
|
game_purchase_result = vglist_api.client.query(AddGameToLibraryMutation, variables: variables) |
|
|
|
if !game_purchase_result.to_h['errors'].nil? && game_purchase_result.to_h['errors'].length > 0 |
|
progress_bar.log "ERRORS: #{game_purchase_result.to_h['errors'].map { |x| x['message'] }.join(", ")}" |
|
progress_bar.increment |
|
error_count += 1 |
|
next |
|
end |
|
|
|
game_purchase = game_purchase_result.data.to_h.dig("addGameToLibrary", "gamePurchase") |
|
# If no game purchase was created by the 'addGameToLibrary' mutation, it |
|
# means either the query had some sort of error or the game was already |
|
# in the user's library. |
|
if game_purchase.nil? |
|
progress_bar.log "#{grouvee_game[:name]} wasn't added to your library, either because there was some sort of error or because the game was already in your library." |
|
progress_bar.increment |
|
error_count += 1 |
|
next |
|
end |
|
|
|
progress_bar.log "Added #{game_purchase["game"]["name"]} to your library." |
|
added_games_count += 1 |
|
progress_bar.increment |
|
end |
|
|
|
progress_bar.finish unless progress_bar.finished? |
|
|
|
puts "Import complete!" |
|
puts "#{added_games_count} games were added to your library." if added_games_count > 0 |
|
puts "#{error_count} games were unable to be added due to errors." if error_count > 0 |
|
if not_found_count > 0 |
|
puts "The following #{not_found_count} games were not found on vglist:" |
|
not_found_games.each do |game| |
|
puts "- #{game[:name]} (#{game[:giantbomb_id]})" |
|
end |
|
end |