Raw Source
subtixx / FAB Free Asset Getter

// ==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     2.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/acquired-content?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
  });
})();