重要

由于 FAB API 的构建方式是处理搜索分页的,因此可能无法通过此脚本认领所有资产。在上次脚本更新中,总共扫描了 ~600 个页面,其中包含 ~14k 资源。 请注意,点击“全部领取”quixel 按钮,您就已经领取了 2024 年之后所有可供未来免费使用的资产(官方回应)。此脚本仅确保将资产添加到库中。

将所有 quixel megascan 项目添加到 FAB 账户的脚本

作为 quixel megascan 物品将在 2024 年之后停止免费。因此,此脚本将遍历所有 Quixel MegaScan 项目并将它们添加到您的帐户中。Quixel 将开发一个工具,将所有资源自动添加到您的 FAB 账户中,希望你能够免费的使用全部的Quixel,我编写了一个脚本来做到这一点。

  1. 复制以下脚本的内容
  2. 登录您的FAB控制面板
  3. 确保您已接受条款和条件(例如,尝试领取任何免费资产)
  4. 转到 Quixel 卖家列表,网址为 https://www.fab.com/sellers/Quixel
  5. 打开 devtools (F12) -> 转到“控制台”选项卡
  6. 粘贴脚本并按 Enter 键

您可以随意调整脚本最顶部的常量,以减少等待时间,或防止 tiemouts 或 throttles。

async function claimQuixelAssets() { // Configurable constants to resume const INITIAL_PAGE_CURSOR = null; const INITIAL_FILTER_INDEX = 0; const WAIT_TIME_BETWEEN_PAGES = 5; // in seconds const WAIT_TIME_BETWEEN_CLAIMS = 0; // in seconds // Debugging constants const CLEAR_CONSOLE = true; const PARALLEL_CLAIM = true; const CLAIM = true; // Hacky constants to overcome API limitations const SKIP_ITEM_IDS = [ '3e441085-0d1d-4476-93bc-e68d184026f2', 'f74671f7-d8a5-42e7-9dd4-e76639ca0ef0', 'd808f867-2560-4476-ad05-a1d999bd940b', ]; // A list of strings, in case an item constantly fails to claim due to 500 errors const FILTERS = [ // Environments 'listing_types=environment', // Materials 'listing_types=material', // 'categories=art-traditional&listing_types=material', // 'categories=building-human-made&listing_types=material', // 'categories=damage-grunge&listing_types=material', // 'categories=fabric-clothing&listing_types=material', // 'categories=nature-terrain&listing_types=material', // 'categories=organic&listing_types=material', // Decals 'listing_types=decal', // 'categories=art-traditional&listing_types=decal', // 'categories=building-human-made&listing_types=decal', // 'categories=damage-grunge&listing_types=decal', // 'categories=fabric-clothing&listing_types=decal', // 'categories=nature-terrain&listing_types=decal', // 'categories=organic&listing_types=decal', // Brushes 'listing_types=brush', // 'categories=art-traditional&listing_types=brush', // 'categories=building-human-made&listing_types=brush', // 'categories=damage-grunge&listing_types=brush', // Atlas 'listing_types=atlas', // 'categories=building-human-made&listing_types=atlas', // 'categories=damage-grunge&listing_types=atlas', // 'categories=nature-terrain&listing_types=atlas', // 3D models 'listing_types=3d-model', // 'categories=buildings-architecture&listing_types=3d-model', // 'categories=electronics-technology&listing_types=3d-model', // 'categories=food-drink&listing_types=3d-model', // 'categories=furniture-fixtures&listing_types=3d-model', // 'categories=nature-plants&listing_types=3d-model', // 'categories=environments-scenes&listing_types=3d-model', // 'categories=objects-decor&listing_types=3d-model', // ... ] // Counters let totalPages = 0; let claimedAssets = new Set(); // Functions const clearConsole = () => { if (CLEAR_CONSOLE) { console.clear() } else { console.log(`-------------------------------------------------------------------`); } } const sleep = (seconds) => { return new Promise(r => setTimeout(r, seconds * 1000)); } const getCSRFToken = () => { const cookieName = 'sb_csrftoken'; return document.cookie.match('(^|;)\\s*' + cookieName + '\\s*=\\s*([^;]+)')?.pop() || ''; } const postResource = async (path, body) => { const baseUrl = "https://www.fab.com/i/listings"; const formData = new FormData(); for (const name in body) { formData.append(name, body[name]); } const response = await fetch(`${baseUrl}/${path}`, { headers: { "accept": "application/json, text/plain, */*", "x-csrftoken": getCSRFToken(), }, method: "POST", body: formData, }); if (!response.ok) { throw new Error(`Failed request with ${response.status} status code`); } return await response.text(); } const getResource = async (path) => { const baseUrl = "https://www.fab.com/i/listings"; const response = await fetch(`${baseUrl}/${path}`, { headers: { "accept": "application/json, text/plain, */*", "x-csrftoken": getCSRFToken(), }, method: "GET", }); if (!response.ok) { throw new Error(`Failed request with ${response.status} status code`); } return await response.json(); } const fetchItem = async (itemId) => { try { return await getResource(itemId); } catch (error) { throw new Error(`Failed to fetch item ${itemId} due to: ${error}`) } } const claimItem = async (item) => { let offer_id = item.licenses.find((license) => license.slug == "professional").offerId; // offer_id = item.licenses[0].offerId; try { await postResource(`${item.uid}/add-to-library`, { offer_id }); } catch (error) { throw new Error(`Failed to claim item ${item.title} (${item.uid}) due to: ${error}`) } } const handleItem = async (listItem) => { if (SKIP_ITEM_IDS.includes(listItem.uid)) { console.log(`-> Skipping ${listItem.title} (id: ${listItem.uid})`); return; } if (claimedAssets.has(listItem.uid)) { console.log(`-> Already claimed ${listItem.title} (id: ${listItem.uid})`); return; } console.log(`-> Claiming ${listItem.title} (id: ${listItem.uid})`); if (CLAIM) { const item = await fetchItem(listItem.uid); await claimItem(item); } claimedAssets.add(listItem.uid); await sleep(WAIT_TIME_BETWEEN_CLAIMS); } const fetchItems = async (filterIndex, pageCursor) => { const filters = FILTERS[filterIndex]; try { const cursorQuery = pageCursor ? `&cursor=${pageCursor}` : '' return await getResource(`search?seller=Quixel&${filters}${cursorQuery}`); } catch (error) { throw new Error(`Failed to get page ${pageCursor || 'null'} due to: ${error}`); } } const handleListPage = async (filterIndex, pageCursor) => { console.log(`-> Getting content from page cursor ${pageCursor || ''}, with filter index ${filterIndex}...`); let page = await fetchItems(filterIndex, pageCursor); console.log(`-> Found ${page.results.length} results on this page`); if (PARALLEL_CLAIM) { const promises = page.results.map((item) => handleItem(item)); await Promise.all(promises) } else { for (const item of page.results) { await handleItem(item); } } await sleep(WAIT_TIME_BETWEEN_PAGES); clearConsole(); totalPages += 1; if (pageCursor != null && pageCursor == page.cursors.next) { throw new Error(`Cursor loop found on page ${pageCursor}; this is a problem on FAB's API, not this script`); } return page.cursors.next } // Main loop function const main = async () => { clearConsole(); try { for (let filterIndex = INITIAL_FILTER_INDEX; filterIndex < FILTERS.length; filterIndex += 1) { let pageCursor = INITIAL_PAGE_CURSOR; do { console.log(`-> Pages processed so far: ${totalPages} pages`); console.log(`-> Assets claimed so far: ${claimedAssets.size} assets`); pageCursor = await handleListPage(filterIndex, pageCursor) } while (pageCursor != null); } console.log('-> Script execution completed successfully!') console.log(`-> Total pages processed: ${totalPages} pages`); console.log(`-> Total assets claimed: ${claimedAssets.size} assets`); } catch (error) { console.error(`-> ${error.message}`); console.error("-> Try waiting for a couple of minutes, then re-running the script by changing INITIAL_FILTER_INDEX and INITIAL_PAGE_CURSOR"); console.error(error); } } return await main(); } claimQuixelAssets();

 

 

 

 

 

 

 

目前发现脚本运行完后存在有遗漏未入库

 

脚本在页面之间有点慢

默认情况下,该脚本会休眠 5 秒以防止请求限制(老实说,不确定是否有任何限制)。如果您不想等待那么长时间,请将 的值设置为 。WAIT_TIME_BETWEEN_PAGES0

脚本在 X 页后停止

出于某种原因,搜索查询可能会停止,具体取决于所使用的排序键。或者,在某些情况下,一遍又一遍地重复同一页。我目前正在尝试找到解决方案,但这完全取决于服务器如何响应此脚本发出的搜索查询。

我在所有索赔请求中都收到 HTTP 401 错误,即使我将它们添加到SKIP_ITEMS_ID

您必须先接受 FAB 的条款和条件。例如,尝试通过 Web 界面手动将一个免费项目添加到您的库中。然后,尝试运行脚本。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

评论(2)

  • zzz 2024年11月20日 下午5:07

    为什么无法运行呢