-
-
Save Subtixx/47f6db9ceabf51c3f689db1594a50b37 to your computer and use it in GitHub Desktop.
| // ==UserScript== | |
| // @name FAB Free Asset Getter | |
| // @namespace Violentmonkey Scripts | |
| // @copyright 2024, subtixx (https://openuserjs.org/users/subtixx) | |
| // @match https://www.fab.com/channels/* | |
| // @match https://www.fab.com/de/channels/* | |
| // @match https://www.fab.com/search* | |
| // @match https://www.fab.com/de/search* | |
| // @grant none | |
| // @license AGPL-3.0-or-later | |
| // @version 1.0 | |
| // @author Dominic Hock <d.hock@it-hock.de> | |
| // @description A script to get all free assets from the FAB marketplace | |
| // @downloadURL https://update.greasyfork.org/scripts/518732/FAB%20Free%20Asset%20Getter.user.js | |
| // @updateURL https://update.greasyfork.org/scripts/518732/FAB%20Free%20Asset%20Getter.meta.js | |
| // ==/UserScript== | |
| (function () { | |
| `use strict`; | |
| var added = false; | |
| var notificationQueueContainer = null; | |
| var assetProgressbar = null; | |
| var innerAssetsProgressbar = null; | |
| var assetStatus = null; | |
| const resultGridID = ".oeSuy4_9"; | |
| // Function to show toast | |
| function showToast(message, type = 'success', onfinish) { | |
| const toast = document.createElement('div'); | |
| toast.textContent = message; | |
| //toast.style.position = 'fixed'; | |
| //toast.style.bottom = '20px'; | |
| //toast.style.right = '20px'; | |
| toast.style.margin = "5px 0 5px 0"; | |
| toast.style.padding = '15px'; | |
| toast.style.backgroundColor = type === 'success' ? '#28a745' : '#dc3545'; // Green for success, red for error | |
| toast.style.color = 'white'; | |
| toast.style.borderRadius = '5px'; | |
| toast.style.zIndex = '10000'; | |
| toast.style.fontFamily = 'Arial, sans-serif'; | |
| toast.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; | |
| toast.style.opacity = '0'; | |
| toast.style.transition = 'opacity 0.5s ease'; | |
| // Append to body | |
| notificationQueueContainer.appendChild(toast); | |
| // Fade in | |
| setTimeout(() => { | |
| toast.style.opacity = '1'; | |
| }, 100); | |
| // Auto-remove after 3 seconds | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| setTimeout(() => { | |
| document.body.removeChild(toast); | |
| if(onfinish !== null && onfinish !== undefined) | |
| { | |
| onfinish(); | |
| } | |
| }, 500); | |
| }, 3000); | |
| } | |
| function getCSRFToken() { | |
| // Get from fab_csrftoken cookie | |
| let cookies = document.cookie.split(";"); | |
| for (let i = 0; i < cookies.length; i++) { | |
| let cookie = cookies[i].trim(); | |
| if (cookie.startsWith("fab_csrftoken=")) { | |
| return cookie.split("=")[1]; | |
| } | |
| } | |
| return ""; | |
| } | |
| async function getAcquiredIds(listings) { | |
| assetStatus.innerText = "Requesting which items you own!"; | |
| console.log("Getting acquired ids"); | |
| // max listings is 24 so just cut | |
| if (listings.length > 24) { | |
| showToast("More than 24 listings requested. Not possible!", "error"); | |
| console.error("Too many listings"); | |
| return []; | |
| } | |
| let filteredListings = listings.filter(listing => !listing.isOwned); | |
| if(filteredListings.length === 0) | |
| { | |
| showToast("No listings to check!"); | |
| return listings; | |
| } | |
| // Convert uid array to listing_ids=X&listing_ids=Y&listing_ids=Z | |
| let ids = filteredListings | |
| .map(listing => listing.id) | |
| .join("&listing_ids="); | |
| //[{"uid":"5059af80-527f-4dda-8e75-7dde4dfcdf81","acquired":true,"rating":null}] | |
| let result = await fetch("https://www.fab.com/i/users/me/listings-states?listing_ids=" + ids, { | |
| "credentials": "include", | |
| "headers": { | |
| "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0", | |
| "Accept": "application/json, text/plain, */*", | |
| "Accept-Language": "en", | |
| "X-Requested-With": "XMLHttpRequest", | |
| "X-CsrfToken": getCSRFToken(), | |
| "Sec-GPC": "1", | |
| "Sec-Fetch-Dest": "empty", | |
| "Sec-Fetch-Mode": "cors", | |
| "Sec-Fetch-Site": "same-origin" | |
| }, | |
| "referrer": "https://www.fab.com/channels/unreal-engine?is_free=1&sort_by=-createdAt&is_ai_generated=0", | |
| "method": "GET", | |
| "mode": "cors" | |
| }); | |
| let json = await result.json(); | |
| let acquired = []; | |
| for (let i = 0; i < json.length; i++) { | |
| if (json[i].acquired) { | |
| acquired.push(json[i].uid); | |
| } | |
| } | |
| let alreadyAcquired = listings.filter(listing => listing.isOwned).length; | |
| console.log("Acquired " + acquired.length + " of " + listings.length + " listings (" + alreadyAcquired + " already acquired were skipped)"); | |
| return acquired; | |
| } | |
| async function getIds() { | |
| let resultGrid = document.querySelector(resultGridID); | |
| if(!resultGrid) { | |
| showToast("Failed to find results!", "error"); | |
| return; | |
| } | |
| let foundItems = resultGrid.querySelectorAll(".fabkit-Stack-root.d6kADL5Y.Bf_zHIaU"); | |
| if(!foundItems || foundItems.length === 0){ | |
| showToast("No items found? Check console!", "error"); | |
| console.error(resultGrid); | |
| return; | |
| } | |
| let currentListings = []; | |
| for (let i = 0; i < foundItems.length; i++) { | |
| let root = foundItems[i]; | |
| let nameContainer = root.querySelector("a > div.fabkit-Typography-ellipsisWrapper"); | |
| if(!nameContainer) { | |
| console.error(root); | |
| showToast("Failed to get name for item. Check Console!", "error"); | |
| continue; | |
| } | |
| let name = nameContainer.innerText; | |
| let url = root.querySelector("a").href; | |
| let isOwned = root.querySelector("div > i.fabkit-Icon--intent-success") !== null; | |
| if (url === undefined) { | |
| console.error(url, root); | |
| showToast("Failed to get url. Please check console!", "error"); | |
| return; | |
| } | |
| // Extract id | |
| let id = url.split("/").pop(); | |
| if(!id){ | |
| showToast("Can't get id? Please check console!"); | |
| console.error(id); | |
| return; | |
| } | |
| console.log(id, name, isOwned, url); | |
| currentListings.push({ | |
| isOwned: isOwned, | |
| name: name, | |
| id: id | |
| }); | |
| } | |
| assetStatus.style.display = "block"; | |
| let acquired = []; | |
| console.log("Need to check " + currentListings.length + " listings"); | |
| assetStatus.innerText = "Need to check " + currentListings.length + " listings"; | |
| if (currentListings.length > 24) { | |
| showToast("Too many listings, splitting into 24 chunks!"); | |
| console.log("Too many listings, splitting into 24 chunks"); | |
| // Slice, request, join, until we are finished | |
| for (let i = 0; i < currentListings.length; i += 24) { | |
| let partial = await getAcquiredIds(currentListings.slice(i, i + 24)); | |
| acquired = acquired.concat(partial); | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| } | |
| } | |
| else { | |
| acquired = await getAcquiredIds(currentListings); | |
| } | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| assetProgressbar.style.display = "block"; | |
| // [{id:"",offerId:""}] | |
| let offers = []; | |
| for (let i = 0; i < currentListings.length; i++) { | |
| assetStatus.innerText = "Checking " + currentListings[i].name + " (" + currentListings[i].id + ")"; | |
| innerAssetsProgressbar.style.width = (i / currentListings.length * 100) + "%"; | |
| let currentListing = currentListings[i]; | |
| if (acquired.includes(currentListing.id) || currentListing.isOwned) { | |
| console.log(currentListing.name + " (" + currentListing.id + ") already acquired"); | |
| showToast("You already own " + currentListing.name + " (" + currentListing.id + ")"); | |
| continue; | |
| } | |
| let result = await fetch("https://www.fab.com/i/listings/" + currentListing.id, { | |
| "credentials": "include", | |
| "headers": { | |
| "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0", | |
| "Accept": "application/json, text/plain, */*", | |
| "Accept-Language": "en", | |
| "X-Requested-With": "XMLHttpRequest", | |
| "X-CsrfToken": getCSRFToken(), | |
| "Sec-GPC": "1", | |
| "Sec-Fetch-Dest": "empty", | |
| "Sec-Fetch-Mode": "cors", | |
| "Sec-Fetch-Site": "same-origin", | |
| "Priority": "u=0" | |
| }, | |
| "referrer": "https://www.fab.com/listings/" + currentListing.id, | |
| "method": "GET", | |
| "mode": "cors" | |
| }); | |
| // licenses -> foreach -> get where price 0 -> buy | |
| let json = await result.json(); | |
| let listingOffers = []; | |
| for (let j = 0; j < json.licenses.length; j++) { | |
| let license = json.licenses[j]; | |
| if (license.priceTier.price != 0) { | |
| continue; | |
| } | |
| offers.push({ | |
| name: currentListing.name, | |
| id: currentListing.id, | |
| offerId: license.offerId | |
| }); | |
| listingOffers.push(license.offerId); | |
| console.log("Found free offer for " + currentListing.name + " (" + currentListing.id + ")"); | |
| } | |
| if (listingOffers.length == 0) { | |
| console.log("No free offers found for " + currentListing.name + " (" + currentListing.id + ")"); | |
| } | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| } | |
| for (let i = 0; i < offers.length; i++) { | |
| console.log("Trying to add " + offers[i].name + " (" + offers[i].id + ")"); | |
| let result = await fetch("https://www.fab.com/i/listings/" + offers[i].id + "/add-to-library", { | |
| "credentials": "include", | |
| "headers": { | |
| "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0", | |
| "Accept": "application/json, text/plain, */*", | |
| "Accept-Language": "en", | |
| "X-Requested-With": "XMLHttpRequest", | |
| "X-CsrfToken": getCSRFToken(), | |
| "Content-Type": "multipart/form-data; boundary=---------------------------4056384097365570293376228769", | |
| "Sec-GPC": "1", | |
| "Sec-Fetch-Dest": "empty", | |
| "Sec-Fetch-Mode": "cors", | |
| "Sec-Fetch-Site": "same-origin", | |
| "Priority": "u=0" | |
| }, | |
| "referrer": "https://www.fab.com/listings/" + offers[i].id, | |
| "body": "-----------------------------4056384097365570293376228769\r\nContent-Disposition: form-data; name=\"offer_id\"\r\n\r\n" + offers[i].offerId + "\r\n-----------------------------4056384097365570293376228769\r\n-----------------------------4056384097365570293376228769--\r\n", | |
| "method": "POST", | |
| "mode": "cors" | |
| }); | |
| // check for 200 | |
| if (result.status == 200 || result.status == 201 || result.status == 202 || result.status == 204) { | |
| showToast("Added " + offers[i].name + " (" + offers[i].id + ")"); | |
| } | |
| else { | |
| console.log(); | |
| showToast("Failed to add " + offers[i].name + " (" + offers[i].id + ")", "error"); | |
| } | |
| console.log("Progress: " + (i + 1) + "/" + offers.length + " (" + ((i + 1) / offers.length * 100).toFixed(2) + "%)"); | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| } | |
| return foundItems[foundItems.length - 1]; | |
| } | |
| async function getAll() { | |
| let last; | |
| last = await getIds(); | |
| for (let i = 0; i < 64; i++) { | |
| // Scroll to last item and wait for 5 seconds | |
| last.scrollIntoView(); | |
| showToast("Scrolling..."); | |
| await new Promise(resolve => setTimeout(resolve, 5000)); | |
| showToast("Refreshing..."); | |
| last = await getIds(); | |
| showToast("Done!"); | |
| } | |
| } | |
| function getSortContainer() { | |
| return document.querySelector(`div.odQtzXCJ > ul._oqSjPnA`); | |
| } | |
| function addControls() { | |
| notificationQueueContainer = document.createElement("div"); | |
| notificationQueueContainer.style.position = 'fixed'; | |
| notificationQueueContainer.style.bottom = '20px'; | |
| notificationQueueContainer.style.right = '20px'; | |
| document.body.appendChild(notificationQueueContainer); | |
| var getAssetsButton = document.createElement("button"); | |
| getAssetsButton.className = "fabkit-Button-root fabkit-Button--sm fabkit-Button--menu"; | |
| getAssetsButton.type = "button"; | |
| getAssetsButton.innerHTML = `<span class="fabkit-Button-label">Add Free Assets</span>`; | |
| getAssetsButton.addEventListener(`click`, function () { | |
| getAll(); | |
| }); | |
| assetProgressbar = document.createElement("div"); | |
| assetProgressbar.style.width = "100%"; | |
| assetProgressbar.style.height = "32px"; | |
| assetProgressbar.style.background = "#1C1C20"; | |
| assetProgressbar.style.margin = "0 0 15px 0"; | |
| assetProgressbar.style.display = "none"; | |
| innerAssetsProgressbar = document.createElement("div"); | |
| innerAssetsProgressbar.style.width = "0"; | |
| innerAssetsProgressbar.style.height = "32px"; | |
| innerAssetsProgressbar.style.background = "#45C761"; | |
| innerAssetsProgressbar.style.color = "1C1C20"; | |
| innerAssetsProgressbar.style.weight = "bold"; | |
| innerAssetsProgressbar.style.padding = "6px"; | |
| assetProgressbar.appendChild(innerAssetsProgressbar); | |
| //<div style="width: 100%;background: #1C1C20;height: 32px;"><div style="width: 50px;background: #45C761;height: 32px;padding: 6px;color: #1c1c20;font-weight: bold;">50%</div></div> | |
| assetStatus = document.createElement("div"); | |
| assetStatus.style.font.size = "initial"; | |
| assetStatus.style.font.weight = "normal"; | |
| assetStatus.style.background = "#45C761"; | |
| assetStatus.style.color = "#1C1C20"; | |
| assetStatus.style.padding = "10px"; | |
| assetStatus.style.borderRadius = "10px"; | |
| assetStatus.style.display = "none"; | |
| //<div style="font-size: initial;font-weight: initial;background: #55FF55;border-radius: 10px;padding: 10px;color: #282A36;">Need to check 24 listings</div> | |
| var titleContainer = document.querySelector(".ArhVH7Um"); | |
| if(!titleContainer) { | |
| showToast("Failed to find title container!", "error"); | |
| return; | |
| } | |
| titleContainer.prepend(assetStatus); | |
| titleContainer.prepend(assetProgressbar); | |
| var sortContainer = getSortContainer(); | |
| if(!sortContainer) { | |
| showToast("Failed to find sort container!", "error"); | |
| return; | |
| } | |
| sortContainer.appendChild(getAssetsButton); | |
| added = true; | |
| } | |
| function onBodyChange(mut) { | |
| if (!added) { | |
| addControls(); | |
| } | |
| } | |
| var mo = new MutationObserver(onBodyChange); | |
| mo.observe(document.body, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| })(); |
Woahhh buddy... It's not wise to post your microsoft e-mail links. I've removed them for you.
I don't see any issues realted to the script though.
thanks mate, i am not very familiar with greasy monkey. I installed the plugin on firefox and added your script. Then i went to fab.com/search and the greasy monkey is showing me the script is running, but no button poped up and the web seams to be freezing. I wonder what I have been missing
I've just experienced the same behavior with the page freezing


Don't know if you'll be able to view this, but I've attached a profiler result:
https://share.firefox.dev/4hQeX2t
EDIT: I managed to get the script debugger to finally load and this is the result:
Script terminated by timeout at:
showToast@moz-extension://c1961467-c12f-403d-8a10-294394d4be31/%20FAB%20Free%20Asset%20Getter.user.js#1:28:23
addControls@moz-extension://c1961467-c12f-403d-8a10-294394d4be31/%20FAB%20Free%20Asset%20Getter.user.js#1:365:16
onBodyChange@moz-extension://c1961467-c12f-403d-8a10-294394d4be31/%20FAB%20Free%20Asset%20Getter.user.js#1:374:7
showToast@moz-extension://c1961467-c12f-403d-8a10-294394d4be31/%20FAB%20Free%20Asset%20Getter.user.js#1:28:23
addControls@moz-extension://c1961467-c12f-403d-8a10-294394d4be31/%20FAB%20Free%20Asset%20Getter.user.js#1:365:16
onBodyChange@moz-extension://c1961467-c12f-403d-8a10-294394d4be31/%20FAB%20Free%20Asset%20Getter.user.js#1:374:7
The selector for the assets in the grid is wrong.
To fix it change this:
let foundItems = resultGrid.querySelectorAll(".fabkit-Stack-root.d6kADL5Y.Bf_zHIaU");
To this:
let foundItems = resultGrid.querySelectorAll(".fabkit-Stack-root.fabkit-Surface-root");
This is extremely weird, on my laptop I can use the script no problem. On my PC I tried 3 different browsers, all hangs on Fab.com if the script is enabled making it unusable. Cannot figure out how that's possible.
i couldn't get it to work, the fab website just freeze and i get the following errors in console, please help me to see what I am missing, many thanks)