FAB Free Asset Getter

A script to get all free assets from the FAB marketplace

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        FAB Free Asset Getter
// @namespace   Violentmonkey Scripts
// @copyright 2024, subtixx (https://openuserjs.org/users/subtixx)
// @match       https://www.fab.com/channels/*
// @grant       none
// @license     AGPL-3.0-or-later
// @version     1.0
// @author      Dominic Hock <[email protected]>
// @description A script to get all free assets from the FAB marketplace
// ==/UserScript==

(function () {
  `use strict`;

  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) {
    console.log("Getting acquired ids");
    // max listings is 24 so just cut
    if (listings.length > 24) {
      console.error("Too many listings");
      return [];
    }
    // Convert uid array to listing_ids=X&listing_ids=Y&listing_ids=Z
    let ids = listings
      .filter(listing => !listing.isOwned)
      .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 results = document.getElementsByClassName("fabkit-ResultGrid-root")[0];
    let foundItems = results.childNodes;

    let currentListings = [];
    for (let i = 0; i < foundItems.length; i++) {
      let element = foundItems[i];
      // Check if we have a listing
      if (foundItems[i].getElementsByClassName("fabkit-Stack-root").length <= 0) {
        console.error("No listing found?? " + element);
        continue;
      }
      let root = foundItems[i].getElementsByClassName("fabkit-Stack-root")[0];
      if (root.getElementsByClassName("fabkit-Stack-root").length <= 0) {
        console.error("No listing found?? " + element);
        continue;
      }
      let root2 = root.getElementsByClassName("fabkit-Stack-root")[1];
      let thumbOverlay = root.getElementsByClassName("fabkit-Thumbnail-overlay")[0];
      let name = root2.getElementsByClassName("fabkit-Typography-root")[0].innerText;
      let url = thumbOverlay.href;
      let isOwned = root2.getElementsByClassName("fabkit-Typography-root")[2].innerText === "Owned";
      console.debug(name + " - " + url + ": " + isOwned);

      if (url === undefined) {
        console.log("Not loaded???? ", url, element)
        return;
      }
      // Extract id
      let id = url.split("/").pop();

      currentListings.push({
        isOwned: isOwned,
        name: name,
        id: id
      });
    }

    let acquired = [];
    console.log("Need to check " + currentListings.length + " listings");
    if (currentListings.length > 24) {
      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));
    // [{id:"",offerId:""}]
    let offers = [];
    for (let i = 0; i < currentListings.length; i++) {
      console.log("Checking " + currentListings[i].name + " (" + currentListings[i].id + ")");
      let currentListing = currentListings[i];
      if (acquired.includes(currentListing.id)) {
        console.log(currentListing.name + " (" + currentListing.id + ") already acquired");
        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) {
        console.log("Added " + offers[i].name + " (" + offers[i].id + ")");
      }
      else {
        console.log("Failed to add " + offers[i].name + " (" + offers[i].id + ")");
      }
      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();

      console.log("Scrolling...");
      await new Promise(resolve => setTimeout(resolve, 5000));
      console.log("Refreshing...");
      last = await getIds();
      console.log("Done");
    }
  }

  function doControlsExist() {
    var sortContainer = getSortContainer();
    return sortContainer.querySelector(`.tmnky-custom-controld`);
  }

  function getSortContainer() {
    return document.getElementsByClassName(`fabkit-Surface-root`)[0].childNodes[0];
  }

  function addControls() {
    var hideOwnedCheckbox = createCheckbox(`Get Free Assets`);

    var sortContainer = getSortContainer();
    var onSaleCheckbox = sortContainer.querySelector(`:nth-child(4)`)

    if (onSaleCheckbox && onSaleCheckbox.parentElement === sortContainer) {
      sortContainer.insertBefore(hideOwnedCheckbox, onSaleCheckbox);
    }
  }

  function createCheckbox(text) {
    var checkboxAccordionHeaderContainer = document.createElement(`h2`);
    checkboxAccordionHeaderContainer.className = `fabkit-Accordion-headerContainer tmnky-custom-controld`;

    var checkboxAccordionHeader = document.createElement(`label`);
    checkboxAccordionHeader.className = `fabkit-Accordion-header`;
    var textElement = document.createTextNode(text);
    checkboxAccordionHeader.appendChild(textElement);
    checkboxAccordionHeaderContainer.appendChild(checkboxAccordionHeader);

    var checkboxAccordionHeaderRight = document.createElement(`div`);
    checkboxAccordionHeaderRight.className = `fabkit-Accordion-headerRight`;
    checkboxAccordionHeader.appendChild(checkboxAccordionHeaderRight);

    var checkboxElement = document.createElement(`button`);
    checkboxElement.addEventListener(`click`, function () {
      getAll();
    });
    checkboxElement.innerText = "Get";
    checkboxAccordionHeaderRight.appendChild(checkboxElement);

    return checkboxAccordionHeaderContainer;
  }

  function onBodyChange(mut) {

    if (!doControlsExist()) {
      addControls();
    }
  }

  var mo = new MutationObserver(onBodyChange);
  mo.observe(document.body, {
    childList: true,
    subtree: true
  });
})();