Skip to content

Instantly share code, notes, and snippets.

@Earlopain
Last active July 4, 2024 01:37
Show Gist options
  • Save Earlopain/af7cc3dd3218943075041ea0e2b85bd0 to your computer and use it in GitHub Desktop.
Save Earlopain/af7cc3dd3218943075041ea0e2b85bd0 to your computer and use it in GitHub Desktop.
Automatically put steam cover images for missing titles in the new steam client
!!!Broken because of api changes!!!
This script will download game covers from igdb.com if there is no image provided by steam.
Remembers if the game has a cover or not if run again.
To run this:
Click raw on the second file and save it to your computer (something like right click => save as...)
You will have to set some options inside the file. Edit the file by right clicking on it => edit or using your favorite editor
Put your values into steamid and both the igdb and steam api key.
You get your steam api key from https://steamcommunity.com/dev/apikey (just put something random into domainname )
And the igdb api key from https://api.igdb.com/signup and creating an account
If you don't know how to get your steamid, google it (it's not your vanity name if you have one)
Your steam id should be 17 character long, may also be called steamid64
If you installed steam to a non default location you will need to write it into steamFolder. Don't forget the quotes
Should you be on windows you will need to replace every \ with a \\ in your folder path
Download NodeJS from https://nodejs.org
Open a terminal inside the folder you saved the js file to (on windows you can do shift+right click in the explorer)
Type npm install request
Type node ./steamCoverDownloader.js
After everything is done you will need to restart steam to apply your changes
Tested to work on windows and linux. Should probably also work on mac, but I can't test that
const fs = require("fs");
const os = require("os");
const request = require("request");
//User config
//Your steam ID
const steamUserID64 = "yoursteamid";
const steamapikey = "yoursteamapikey";
const igdbapikey = "yourigdbapikey";
//Should covers try to download again, if it failed in the past
//Maybe the cover got uploaded to igdb since the last run
const tryDownloadingAgain = false;
//should your already existing stuff be overwritten
//if there will be a version on steam, keep the custom one
const preserveCustomCoverArt = true;
//You can manually specify it, if it is not in the default location. Do not add a trailing slash
//replace every \ with / in the folder path, for example:
//let steamFolder = "D:/Steam";
let steamFolder;
if(steamUserID64.length !== 17){
throw new Error("Not a valid steamid. You need your steamid64. Try steamid.io");
}
const configFile = __dirname + "/steamcovers.json";
if (!fs.existsSync(configFile)) {
console.log("First time running");
fs.writeFileSync(configFile, JSON.stringify({ users: {} }, null, 4), "utf8");
}
//take the steamid, convert it binary and take the last 32bit. Convert back to decimal
const steamUserDataNumber = BigInt("0b" + BigInt(steamUserID64).toString(2).substr(-32)).toString();
if (steamFolder === undefined) {
switch (process.platform) {
case "win32":
steamFolder = "C:/Program Files (x86)/Steam";
break;
case "linux":
steamFolder = os.homedir() + "/.steam";
if(fs.existsSync(steamFolder + "/steam")){
steamFolder = steamFolder + "/steam";
}
break;
case "darwin":
steamFolder = os.homedir() + "/Library/Application Support/Steam";
break;
}
}
if (steamFolder === undefined || !fs.existsSync(steamFolder))
throw new Error("Please manually specify your steam folder at the beginning of the file");
const userDataFolder = steamFolder + "/userdata/" + steamUserDataNumber;
const steamConfigFolder = userDataFolder + "/config";
const steamCoverFolder = steamConfigFolder + "/grid";
if (!fs.existsSync(userDataFolder))
throw new Error("You specified a wrong steamid");
if (!fs.existsSync(steamConfigFolder))
fs.mkdirSync(steamConfigFolder);
if (!fs.existsSync(steamCoverFolder))
fs.mkdirSync(steamCoverFolder);
const storage = JSON.parse(fs.readFileSync(configFile));
const storageBackup = storage;
async function main() {
const allDLC = await getAllSteamDLC();
const games = (await getAllSteamGames()).filter(value => { return (value.img_icon_url !== "" && value.img_logo_url !== "" && !allDLC.includes(value.appid)) });
console.log("You have a total of %s games", games.length);
if (firstRun()) {
storage[steamUserID64] = {};
storage[steamUserID64].status = {};
}
else {
console.log("You are not running this for the first time. You will propably see few to no ouput");
}
for (const game of games) {
let recheckCoverStatus = true;
if (currentUser()[game.appid] === undefined) { //this game has not yet been checked
recheckCoverStatus = false;
currentUser()[game.appid] = {};
currentUser()[game.appid].steamcoverexists = await statusOK("https://steamcdn-a.akamaihd.net/steam/apps/" + game.appid + "/library_600x900_2x.jpg");
currentUser()[game.appid].igdbcoverdownloaded = false;
currentUser()[game.appid].igdbtriedcoverdownload = false;
}
if (currentUser()[game.appid].steamcoverexists === true) { //nothing to do here, cover already exists
continue;
}
//only recheck if entry was not undefined during this run and custom cover art should be deleted
if (!preserveCustomCoverArt && recheckCoverStatus && currentUser()[game.appid].steamcoverexists !== true) {
currentUser()[game.appid].steamcoverexists = await statusOK("https://steamcdn-a.akamaihd.net/steam/apps/" + game.appid + "/library_600x900_2x.jpg");
const filePath = steamCoverFolder + "/" + game.appid + "p.jpg";
if (currentUser()[game.appid].steamcoverexists === true && fs.existsSync(filePath)) {
console.log("%s now has a cover", game.name);
saveConfigFile();
fs.unlinkSync(filePath);
continue;
}
}
//no steam cover, no igdb downloaded yet, but there is cover art, don't overwrite it
if (preserveCustomCoverArt && currentUser()[game.appid].igdbcoverdownloaded === false && coverImageExists(game.appid)) {
console.log("%s already has custom cover art", game.name);
currentUser()[game.appid].igdbcoverdownloaded = true;
saveConfigFile();
continue;
}
//specified to download again but the cover has already been downloaded
//tried downloading custom cover, but no result found, or cover did not have the right format
if ((tryDownloadingAgain && currentUser()[game.appid].igdbcoverdownloaded === true) ||
(currentUser()[game.appid].igdbtriedcoverdownload === true || currentUser()[game.appid].igdbcoverdownloaded === true)) {
continue;
}
const gameName = game.name.replace(/'/g, "").replace(/[\\"']/g, "\\$&").replace(/\u0000/g, "\\0"); //https://stackoverflow.com/a/770533/7873303
const igdbGameArray = await doApiRequest("games", 'fields name,cover; search "' + gameName + '";');
const igdbGame = igdbGameArray.filter(value => { return value.name.replace(/'/g, "").replace(/[\\"']/g, "\\$&").replace(/\u0000/g, "\\0") === gameName })[0];
if (igdbGame === undefined || igdbGame.cover === undefined) { //game could not exist or it does but has no cover
console.log("%s has no cover", game.name)
currentUser()[game.appid].igdbcoverdownloaded = true;
saveConfigFile();
continue;
}
let igdbCover = (await doApiRequest("covers", "fields image_id, height, width; where id = " + igdbGame.cover + ";"))[0];
if (igdbCover === undefined) { //should not happen, but for n++ only the second request delivers results
igdbCover = (await doApiRequest("covers", "fields image_id, height, width; where game = " + igdbGame.id + ";"))[0];
}
//there could be no results overall or the cover is horizontal
if (igdbCover === undefined || igdbCover.width > igdbCover.height) {
console.log(igdbCover.width > igdbCover.height ? "%s cover is horizontal" : "%s has no cover", game.name)
currentUser()[game.appid].igdbtriedcoverdownload = true;
continue;
}
await saveCoverImage(igdbCover.image_id, game.appid);
console.log("%s has been downloaded", game.name)
currentUser()[game.appid].igdbcoverdownloaded = true;
saveConfigFile();
}
//populate statistics
const runNumber = Object.keys(currentUser().status).length;
currentUser().status[runNumber] = {
"currentSteamGames": Object.keys(games).length, "previousSteamCovers": 0, "currentSteamCovers": 0,
"previousTotalCovers": 0, "currentTotalCovers": 0, "timestamp": new Date().getTime()
};
for (const key of Object.keys(currentUser())) {
currentUser().status[runNumber].currentSteamCovers += currentUser()[key].steamcoverexists === true;
currentUser().status[runNumber].currentTotalCovers += currentUser()[key].steamcoverexists === true || currentUser()[key].igdbcoverdownloaded === true;
}
for (const key of Object.keys(currentUserBackup())) {
currentUserBackup().status[runNumber].previousSteamCovers += currentUserBackup()[key].steamcoverexists === true;
currentUserBackup().status[runNumber].previousTotalCovers += currentUserBackup()[key].steamcoverexists === true || currentUserBackup()[key].igdbcoverdownloaded === true;
}
console.log("Stats:");
if (!firstRun()) {
console.log("%s previous steam covers", currentUser().status[runNumber].previousSteamCovers);
console.log("%s previous total covers", currentUser().status[runNumber].previousTotalCovers);
}
console.log("%s current steam covers", currentUser().status[runNumber].currentSteamCovers);
console.log("%s current total covers", currentUser().status[runNumber].currentTotalCovers);
if (!firstRun()) {
console.log("%s official steam diff", currentUser().status[runNumber].currentSteamCovers - currentUser().status[runNumber].previousSteamCovers);
console.log("%s current steam diff", currentUser().status[runNumber].currentTotalCovers - currentUser().status[runNumber].previousTotalCovers);
}
console.log("Changes applied. Restart Steam to see them");
saveConfigFile();
}
main();
function currentUser() {
return storage[steamUserID64];
}
function currentUserBackup() {
return storageBackup[steamUserID64];
}
function firstRun() {
return storage[steamUserID64] === undefined || currentUser().status === undefined;
}
function saveConfigFile() {
fs.writeFileSync(configFile, JSON.stringify(storage, null, 4), "utf8");
}
async function saveCoverImage(igdbCoverImageID, appid) {
const bin = await getBinary("https://images.igdb.com/igdb/image/upload/t_720p/" + igdbCoverImageID + ".jpg");
fs.writeFileSync(steamCoverFolder + "/" + appid + "p.jpg", bin, "binary");
}
//Request related stuff
async function getAllSteamGames() {
const url = "https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/?key=" + steamapikey + "&steamid=" + steamUserID64 + "&include_appinfo=1&include_played_free_games=1";
const games = await getJSON(url);
return games.response.games;
}
async function getAllSteamDLC() {
const url = "https://api.steampowered.com/IStoreService/GetAppList/v1/?key=" + steamapikey + "&include_games=0&include_dlc=1&include_videos=1&include_hardware=1&max_results=50000";
const dlc = await getJSON(url);
return dlc.response.apps.map(value => value.appid);
}
async function saveCoverImage(igdbCoverImageID, appid) {
const bin = await getBinary("https://images.igdb.com/igdb/image/upload/t_720p/" + igdbCoverImageID + ".jpg");
fs.writeFileSync(steamCoverFolder + "/" + appid + "p.jpg", bin, "binary");
}
function coverImageExists(appid) {
return fs.existsSync(steamCoverFolder + "/" + appid + "p.jpg");
}
async function doApiRequest(type, data) {
let response;
while ((response = await requestApi(type, data)) === undefined) {
await sleep(10000);
}
return response;
}
async function getBinary(url) {
return await getURL(url, "binary");
}
async function getJSON(url) {
const json = await getURL(url, "utf8");
return JSON.parse(json);
}
async function getURL(url, formating, header = {}) {
let response;
while ((response = await requestNormal(url, formating, header)) === undefined) {
await new Promise(resolve => { setTimeout(() => resolve(), 10000) });
}
return response;
}
function requestCallback(error, response, body) {
if (error || response.statusCode > 500) {
console.log("Fatal Error");
console.log(error || body);
debugger;
process.exit();
}
else if (response.statusCode !== 200) {
console.log("Unknown Error");
console.log("Response:");
console.log(body);
console.log("Request url:");
console.log(response.request.href);
if (response.request.body !== undefined){
console.log("Request body:");
console.log(response.request.body);
}
debugger;
process.exit();
}
else {
this(body);
}
}
function requestNormal(url, formating, header) {
return new Promise(function (resolve, reject) {
request.get({ url: url, headers: header, encoding: formating }, requestCallback.bind(resolve));
});
}
async function requestApi(type, data) {
const response = await new Promise(resolve => {
request.post({ url: "https://api-v3.igdb.com/" + type, headers: { "user-key": igdbapikey }, body: data }, requestCallback.bind(resolve));
});
return JSON.parse(response);
}
async function statusOK(url) {
const response = await getHeader(url);
if (response === undefined)
return false;
return response.statusCode === 200;
}
async function getHeader(url) {
return new Promise(function (resolve, reject) {
request.head({ url: url }, (error, response, body) => {
resolve(response);
});
});
}
function sleep(time) {
return new Promise(resolve => { setTimeout(() => resolve(), time) })
}
@Earlopain
Copy link
Author

Earlopain commented Oct 10, 2021

Please see the provided comment, it has to be let steamFolder = "C:/Program Files/Steam"

@tr3472
Copy link

tr3472 commented Oct 10, 2021

Thanks. I did not follow the instuctions well. I have the following error now.

You have a total of 256 games
Fatal Error
Error: getaddrinfo ENOTFOUND api-v3.igdb.com
at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:71:26) {
errno: -3008,
code: 'ENOTFOUND',
syscall: 'getaddrinfo',
hostname: 'api-v3.igdb.com'

@Earlopain
Copy link
Author

It looks like they discontinued the api I was using here sometime which means that this will just not work anymore. Sorry about that.
Since I'm not using this myself anymore I don't plan on supporting the new version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment