Skip to content

Instantly share code, notes, and snippets.

@Saturate
Created October 4, 2022 16:56
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save Saturate/1519244dee074f3b6afdea349580f0e0 to your computer and use it in GitHub Desktop.
Save Saturate/1519244dee074f3b6afdea349580f0e0 to your computer and use it in GitHub Desktop.
Project Zomboid - Generate WorkshopItems from Steam Workshop Collection

Project Zomboid - Generate WorkshopItems from Steam Workshop Collection

This snippet will generate a list suited for the Project Zomboid config from a Steam Workshop Collection.

How to use:

  1. Navigate to your collection (eg. https://steamcommunity.com/sharedfiles/filedetails/?id=2871262277)
  2. Copy the code in console.js
  3. Open the devtools for your browser (F12)
  4. Paste and run the code
  5. Copy the output
  6. Paste the config line into your config.

The output will look like this:

WorkshopItems=2625625421;2619072426;2423906082;2618213077;2522173579;2870394916;2846036306;2811383142;2805630347;2772575623;2642541073;2566953935;2516123638;2489148104;2441990998;2169435993;2478768005;2392709985;2490220997
var modIds = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => {
return mod.id.replace('sharedfile_','');
})
console.log(`This list contains ${modIds.length} mods, copy it to your PZ config.`)
console.log(`WorkshopItems=${modIds.join(';')}`)
@TheVeggum
Copy link

I have no clue why I waited so many years to look up a way to scrape the IDs automatically but thank you so much! You saved me countless hours of future clicking / copy & pasting. I often host PZ servers but funnily enough I actually needed this for Space Engineers since they require a list of IDs too, so this helps so much even outside of PZ. Thank you again!

@Saturate
Copy link
Author

I have no clue why I waited so many years to look up a way to scrape the IDs automatically but thank you so much! You saved me countless hours of future clicking / copy & pasting. I often host PZ servers but funnily enough I actually needed this for Space Engineers since they require a list of IDs too, so this helps so much even outside of PZ. Thank you again!

Happy to help :)

@phra
Copy link

phra commented Mar 7, 2023

i've created an improved version that extracts workshop ids and names:

const URLS = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => {
    return "https://steamcommunity.com/sharedfiles/filedetails/?id=" + mod.id.replace('sharedfile_','');
})

const IDS = []
const NAMES = []

async function getIDS(URLS) {
    let index = 0
    for (url of URLS) {
        console.log(`processing #${index++}/${URLS.length}: ${url}`)
        const res = await fetch(url)
        const html = (await res.text()).replace(/<i>/g, "").replace(/<\/i>/g, "").replace(/<br>/g, " ").replace(/<b>/g, "").replace(/<\/b>/g, "")
        //console.log(html)
        const wks_ids = html.match(/Workshop ?ID: (\d*)/gmi) || html.match(/WID: (\d*)/gmi)
        const ids = wks_ids.map(wks => wks.split(": ")[1])
        IDS.push(...ids)
        console.log(ids)
        const wks_names = html.match(/Mod ?ID: (\d*\w*\d*\w*\d*\.*\d*)/gmi)  || html.match(/ ID: (\d*\w*\d*\w*\d*\.*\d*)/gmi)
        const names = wks_names.map(wks => wks.split(": ")[1])
        NAMES.push(...names)
        console.log(names)
    }
}

await getIDS(URLS)

console.log(`WorkshopItems=${IDS.join(";")}`)
console.log(`Mods=${NAMES.join(";")}`)

@KeskinDeniz
Copy link

for improvement, i have this error

Uncaught TypeError: Cannot read properties of null (reading 'map')
at getIDS (:16:29)
at async :26:1

i've created an improved version that extracts workshop ids and names:

const URLS = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => {
    return "https://steamcommunity.com/sharedfiles/filedetails/?id=" + mod.id.replace('sharedfile_','');
})

const IDS = []
const NAMES = []

async function getIDS(URLS) {
    let index = 0
    for (url of URLS) {
        console.log(`processing #${index++}/${URLS.length}: ${url}`)
        const res = await fetch(url)
        const html = (await res.text()).replace(/<i>/g, "").replace(/<\/i>/g, "").replace(/<br>/g, " ").replace(/<b>/g, "").replace(/<\/b>/g, "")
        //console.log(html)
        const wks_ids = html.match(/Workshop ?ID: (\d*)/gmi) || html.match(/WID: (\d*)/gmi)
        const ids = wks_ids.map(wks => wks.split(": ")[1])
        IDS.push(...ids)
        console.log(ids)
        const wks_names = html.match(/Mod ?ID: (\d*\w*\d*\w*\d*\.*\d*)/gmi)  || html.match(/ ID: (\d*\w*\d*\w*\d*\.*\d*)/gmi)
        const names = wks_names.map(wks => wks.split(": ")[1])
        NAMES.push(...names)
        console.log(names)
    }
}

await getIDS(URLS)

console.log(`WorkshopItems=${IDS.join(";")}`)
console.log(`Mods=${NAMES.join(";")}`)

@CodingPfeffer
Copy link

CodingPfeffer commented Apr 10, 2023

A version which does the following things:

  • Collects Workshop ID (has less issues with duplicates or multiple workshop ids)
  • Collects the Mod IDs. You should still have a look at these, when multiple mod ids are found, since sometimes you have to pick one. Logs an errors if not found.
  • Collects the maps, if available. Adds the default map properly.
  • Ignores duplicate mod ids and maps.
  • Trims whitespaces around mod ids and maps.
const WorkshopIDs = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => {
    return mod.id.replace('sharedfile_','');
})

const WORKSHOP_IDS = new Set()
const MOD_IDS = new Set()
const MAPS = new Set()

async function collectIDS(WorkshopIDs) {

    let index = 1
    for (id of WorkshopIDs) {
		url = "https://steamcommunity.com/sharedfiles/filedetails/?id=" + id;
        console.log(`processing #${index}/${WorkshopIDs.length}: ${url}`)
		
        const res = await fetch(url)
		
        const html = (await res.text()).replace(/<i>/g, "").replace(/<\/i>/g, "").replace(/<br>/g, "\n").replace(/<b>/g, "").replace(/<\/b>/g, "").replace(/&amp;/g, "&")
		
		WORKSHOP_IDS.add(id)
		collect_mod_ids(html, MOD_IDS)
		collect_maps(html, MAPS)
		
		index++
    }
}

function collect_mod_ids(html, target_set) {
	// old version		
	//const wks_names = html.match(/^Mod ?ID: ([a-zA-Z0-9_.\-+& ]+) */gmi) || html.match(/^MID: ([a-zA-Z0-9_.\-+& ]+) */gmi)
		
	const mod_id_matches = html.matchAll(/^(Mod ?ID|MID): *(?<mod_id>([^\r\n\t\f\v \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff<>]+ *)+) */gmi)
	const mod_id_matches_array = [...mod_id_matches]
      
	if (mod_id_matches_array.length > 0) {
		mod_ids_for_workshop_id = new Set()
		for (const match_groups of mod_id_matches_array) {
			const mod_id_group = match_groups.groups.mod_id
			mod_ids_for_workshop_id.add(mod_id_group)
		}
		console.log("Adding Mod IDs: '" + [...mod_ids_for_workshop_id].join("','") + "'")
		if (mod_ids_for_workshop_id.length > 1) {
			console.log("Warning. Multiple Mod IDs should always be checked, because you may need select between them.")
		}
		for (mod_id of mod_ids_for_workshop_id) {
			target_set.add(mod_id.trim())
		}
	} else {
		console.log("Error. No ModID. Expected to find 'Mod ID' in description of mod with URL: " + url)
	}
}

function collect_maps(html, target_set) {
	const map_matches = html.matchAll(/^(Map ?Folder|Folder|Map): (?<map>[a-zA-Z0-9_. ]+)/gmi)
	const map_matches_array = [...map_matches]
	if (map_matches_array.length > 0) {
		maps_for_workshop_id = new Set()
		for (const match_groups of map_matches_array) {
			const map_group = match_groups.groups.map
			maps_for_workshop_id.add(map_group)
		}
		
		console.log("Adding Maps: '" + [...maps_for_workshop_id].join("','") + "'")
		for (map of maps_for_workshop_id) {
			target_set.add(map.trim())
		}
	}
}

await collectIDS(WorkshopIDs)

MAPS.add("Muldraugh, KY")

console.log(`WorkshopItems=${[...WORKSHOP_IDS].join(";")}`)
console.log(`Mods=${[...MOD_IDS].join(";")}`)
console.log(`Map=${[...MAPS].join(";")}`)

Run the script in the developer console of a zomboid mod collection.

@HungryHedgehog
Copy link

HungryHedgehog commented Apr 23, 2023

Simple addition to make the list into a format for Space Engineer servers:

var modIds = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => {
    return mod.id.replace('sharedfile_','');
})
var modItems = '';
modIds.forEach(id => 
{
	 modItems += '<ModItem>\n' + `<Name>${id}.sbm</Name>\n` + \`<PublishedFileId>${id}</PublishedFileId>\n` + '</ModItem>\n';
}); 
console.log(modItems);

@Motzumoto
Copy link

Motzumoto commented Sep 27, 2023

Minorly edited this to be faster and log unprocessed urls.

const WorkshopIDs = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => {
    return mod.id.replace('sharedfile_','');
})

const WORKSHOP_IDS = new Set()
const MOD_IDS = new Set()
const MAPS = new Set()
const UNPROCESSED_URLS = new Set() // Set to store unprocessed URLs

async function collectIDS(WorkshopIDs) {
    const promises = WorkshopIDs.map(async (id, index) => {
        const url = "https://steamcommunity.com/sharedfiles/filedetails/?id=" + id;
        console.log(`processing #${index + 1}/${WorkshopIDs.length}: ${url}`);

        try {
            const res = await fetch(url);
            const html = (await res.text()).replace(/<i>/g, "").replace(/<\/i>/g, "").replace(/<br>/g, "\n").replace(/<b>/g, "").replace(/<\/b>/g, "").replace(/&amp;/g, "&");

            WORKSHOP_IDS.add(id);
            collect_mod_ids(html, MOD_IDS);
            collect_maps(html, MAPS);
        } catch (error) {
            // Handle the error and store the unprocessed URL in the Set
            console.error(`Error processing URL: ${url}`);
            UNPROCESSED_URLS.add(url);
        }
    });

    await Promise.all(promises);
}

function collect_mod_ids(html, target_set) {
    const mod_id_matches = html.matchAll(/^(Mod ?ID|MID): *(?<mod_id>([^\r\n\t\f\v \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff<>]+ *)+) */gmi);
    const mod_id_matches_array = [...mod_id_matches];

    if (mod_id_matches_array.length > 0) {
        const mod_ids_for_workshop_id = new Set();
        for (const match_groups of mod_id_matches_array) {
            const mod_id_group = match_groups.groups.mod_id;
            mod_ids_for_workshop_id.add(mod_id_group);
        }

        if (mod_ids_for_workshop_id.length > 1) {
            console.log("Warning. Multiple Mod IDs should always be checked, because you may need to select between them.");
        }
        for (const mod_id of mod_ids_for_workshop_id) {
            target_set.add(mod_id.trim());
        }
    } else {
        console.log("Error. No ModID. Expected to find 'Mod ID' in the description of mod with URL: " + url);
    }
}

function collect_maps(html, target_set) {
    const map_matches = html.matchAll(/^(Map ?Folder|Folder|Map): (?<map>[a-zA-Z0-9_. ]+)/gmi);
    const map_matches_array = [...map_matches];
    if (map_matches_array.length > 0) {
        const maps_for_workshop_id = new Set();
        for (const match_groups of map_matches_array) {
            const map_group = match_groups.groups.map;
            maps_for_workshop_id.add(map_group);
        }

        for (const map of maps_for_workshop_id) {
            target_set.add(map.trim());
        }
    }
}

(async () => {
    await collectIDS(WorkshopIDs);

    MAPS.add("Muldraugh, KY");

    console.log(`WorkshopItems=${[...WORKSHOP_IDS].join(";")}`);
    console.log(`Mods=${[...MOD_IDS].join(";")}`);
    console.log(`Map=${[...MAPS].join(";")}`);

    // Log unprocessed URLs at the end
    if (UNPROCESSED_URLS.size > 0) {
        console.log("Unprocessed URLs:");
        console.log([...UNPROCESSED_URLS].join("\n"));
    }
})();

@Saturate
Copy link
Author

Looks like this code is popular. Thanks for all the additions guys and girls.

Might be time to make a real project out of it, but what would you recon that would be most useful?

I'm thinking if a nodejs module or a python script could be good here, it could do stuff automatically for the server configuration. Maybe a GUI is needed for some people, but what do you think?

@Motzumoto
Copy link

Motzumoto commented Sep 28, 2023

Looks like this code is popular. Thanks for all the additions guys and girls.

Might be time to make a real project out of it, but what would you recon that would be most useful?

I'm thinking if a nodejs module or a python script could be good here, it could do stuff automatically for the server configuration. Maybe a GUI is needed for some people, but what do you think?

I would love if this became an actual project. theres some minor bugs in it that ive found. With the code I uploaded it includes the collections ID in the list, I cant remember if it did that with your original upload. If you do decide to make this into a GUI and whatnot I'd love it if people who contributed to this would be credited.

I made something similar to this in python but because its python there are some serious limitations. I'm considering closing the repo and linking to this in the future if this does come into fruition.

@contrid
Copy link

contrid commented Mar 12, 2024

i've created an improved version that extracts workshop ids and names:

const URLS = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => {
    return "https://steamcommunity.com/sharedfiles/filedetails/?id=" + mod.id.replace('sharedfile_','');
})

const IDS = []
const NAMES = []

async function getIDS(URLS) {
    let index = 0
    for (url of URLS) {
        console.log(`processing #${index++}/${URLS.length}: ${url}`)
        const res = await fetch(url)
        const html = (await res.text()).replace(/<i>/g, "").replace(/<\/i>/g, "").replace(/<br>/g, " ").replace(/<b>/g, "").replace(/<\/b>/g, "")
        //console.log(html)
        const wks_ids = html.match(/Workshop ?ID: (\d*)/gmi) || html.match(/WID: (\d*)/gmi)
        const ids = wks_ids.map(wks => wks.split(": ")[1])
        IDS.push(...ids)
        console.log(ids)
        const wks_names = html.match(/Mod ?ID: (\d*\w*\d*\w*\d*\.*\d*)/gmi)  || html.match(/ ID: (\d*\w*\d*\w*\d*\.*\d*)/gmi)
        const names = wks_names.map(wks => wks.split(": ")[1])
        NAMES.push(...names)
        console.log(names)
    }
}

await getIDS(URLS)

console.log(`WorkshopItems=${IDS.join(";")}`)
console.log(`Mods=${NAMES.join(";")}`)

Awesome, thank you for posting this! The regex was useful 😁👍🙏

@FlintMcgy
Copy link

FlintMcgy commented Mar 28, 2024

Here is a better snippet made using @contrid code with ChatGPT that is faster and also gives you a Numbered List of all the Mods with their Id's

const URLS = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => 
    "https://steamcommunity.com/sharedfiles/filedetails/?id=" + mod.id.replace('sharedfile_', '')
);

async function getIDS(URLS) {
    return Promise.all(URLS.map(async (url, index) => {
        try {
            console.log(`processing #${index + 1}/${URLS.length}: ${url}`);
            const res = await fetch(url);
            const html = await res.text();
            
            const extractMatches = (pattern) => (html.match(new RegExp(pattern, "gmi")) || []).map(wks => wks.split(": ")[1]);
            
            const ids = extractMatches(/Workshop ?ID: (\d*)/);
            const names = extractMatches(/Mod ?ID: (\d*\w*\d*\w*\d*\.*\d*)/);

            return { ids, names };
        } catch (error) {
            console.error(`Error processing #${index + 1}: ${error.message}`);
            return { ids: [], names: [] };
        }
    }));
}

async function main() {
    const results = await getIDS(URLS);
    const modList = results.map(({ ids, names }, index) => `${index + 1}. WorkshopID:[${ids[0]}] ModIDs[${names.join(", ")}]`);
    const IDS = results.flatMap(({ ids }) => ids);
    const NAMES = results.flatMap(({ names }) => names);

    console.log(`WorkshopItems=${IDS.join(";")}`);
    console.log(`Mods=${NAMES.join(";")}`);
    console.log(`ModList=${modList.join(", ")}`);
}

main();

@sbwns
Copy link

sbwns commented Apr 17, 2024

Made this site so that it's user friendly
https://www.pzutil.com/

Does not log unprocessed URLs yet but will add that shortly

@Motzumoto
Copy link

Made this site so that it's user friendly https://www.pzutil.com/

Does not log unprocessed URLs yet but will add that shortly

Wonderful!

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