Skip to content

Instantly share code, notes, and snippets.

@connorshea
Last active May 2, 2020 22:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save connorshea/2ff7c8e4471266189d15f9501a61bc08 to your computer and use it in GitHub Desktop.
Save connorshea/2ff7c8e4471266189d15f9501a61bc08 to your computer and use it in GitHub Desktop.
Grouvee-to-vglist Importer Script

This script is meant to be used to transfer a game library from Grouvee to vglist. :)

Import instructions:

  1. Save grouvee_to_vglist_importer.rb locally.
  2. Export your Grouvee library to a CSV file and save it as grouvee.csv in the same directory as grouvee-to-vglist-import.rb.
  3. Create an account on vglist.co if you haven't already.
  4. In your vglist settings, click "Developer" in the left sidebar.
  5. Click "View Token" and copy the token shown.
  6. Check your Ruby version (ruby -v).
    • If it's 2.5.x or lower, you'll need to run gem install bundler to make sure Bundler is installed. This is necessary to run the script.
    • If your Ruby version is older than 2.5, keep in mind that the script may not work. I didn't test it on anything below that.
    • If it's 2.6.x or above, Bundler comes built-in so you can skip installing it.
  7. Run the importer script: VGLIST_USER_EMAIL=EMAIL_HERE VGLIST_USER_TOKEN=TOKEN_HERE ruby grouvee_to_vglist_importer.rb
    • Replace EMAIL_HERE and TOKEN_HERE with your email and token respectively.
  8. Now the script will import your grouvee library into vglist. It'll take a little more than 1 second per game in your library (this is intentionally limited to prevent the script from causing problems for the server), so 1000 games will take something like 16-17 minutes.
    • It will likely miss some of your games. Unfortunately I don't have a great solution for that problem. The script is dependent on matching GiantBomb IDs from Grouvee to GiantBomb IDs in vglist, and not every game from the GiantBomb database is in vglist.
  9. That's it! Thanks for using vglist :)
# 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 = []
CSV.foreach(
File.join(File.dirname(__FILE__), 'grouvee.csv'),
skip_blanks: true,
headers: true
) do |csv_row|
grouvee_library << {
name: csv_row['name'],
giantbomb_id: csv_row['giantbomb_id'].to_i
}
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 checker v1.0",
"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
grouvee_library = get_grouvee_library
puts "Found #{grouvee_library.count} games in grouvee.csv."
puts "Checking #{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"
)
not_found_count = 0
not_found_games = []
# Iterate through every game in the user's Grouvee Library.
grouvee_library.each do |grouvee_game|
# 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
# Add missing games to the array of games that couldn't be found.
if game_id.nil?
progress_bar.log "No vglist entry found for #{grouvee_game[:name]} (GiantBomb ID '3030-#{grouvee_game[:giantbomb_id]}')"
progress_bar.increment
not_found_count += 1
not_found_games << { name: grouvee_game[:name], giantbomb_id: "3030-#{grouvee_game[:giantbomb_id]}" }
next
end
progress_bar.increment
end
progress_bar.finish unless progress_bar.finished?
puts 'Check complete!'
if not_found_count.positive?
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
else
puts 'All games in this CSV were found on vglist!'
end
# 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment