Skip to content

Instantly share code, notes, and snippets.

@maalrron
Last active January 13, 2025 17:19
Show Gist options
  • Save maalrron/877b2edb23cc5d99d6a6b4c22f708e58 to your computer and use it in GitHub Desktop.
Save maalrron/877b2edb23cc5d99d6a6b4c22f708e58 to your computer and use it in GitHub Desktop.
download megascans purchased assets

This code is a modification of quixel.py by ad044.

The key change is that it allows you to download your purchased assets by category and sub categories instead of all the assets. It will show the number of assets it will download and ask for confirmation.

I recommend running the same script twice to make sure no asset failed to download. if it says 0, then you can change the category.

Please avoid downloading everything in one go to avoid stress on the servers.

If someone ends up downloading everything, please tell us in the comments the total disk space it takes.

How to use

  1. copy dlMegascans.py and save it on disk.
  2. copy (preferably download) the very very long ms_asset_categories.json found here (to not make this page unviewable): "https://gist.github.com/maalrron/3ebe6514f8fba184311aa63fb68f841d"
  3. Get your authentication token from megascan:
  • Login into https://quixel.com
  • copy the script from below (gettoken.js)
  • Open devtools (F12) -> Go to "Console" tab
  • Paste in the script and press Enter. (if it doesn't let you paste, on firefox: https://www.youtube.com/watch?v=ekN2i953Nas at 01:14 / on chrome, type “allow pasting” then ENTER first.) if it returns "undefined", disable your add blocker.
  • copy the token, it's the very long line (or multiple lines) of digits and characters.
  • paste it in the token line of the script dlMegascans.py (between quotes).
  1. Set the path in the "download_path" line, e.g: "/home/Downloads".
  2. Set the path in the "json_file_path" line. e.g: "/home/Downloads/ms_asset_categories.json".
  3. Set the path where you want the cache.txt file to be created.
  4. set the target_category to the category and sub categories you want separated with /. one category or sub category at a time:
  • It works by matching every word with the categories of each asset in the json file.
  • The order doesn't matter but they need to be separated with /.
  • It's not case sensitive and ignores any last "s" so "3d plant" will also download what's in "3D Plants"

Notes:

  • Like in the original code, it creates a cache.txt file (in the same location as the script, or wherever you're cd at in the terminal) to avoid downloading the same assets multiple times.

  • find this line: with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: max_workers is the number of file downloadable at the same time. 10 is a good compromise between speed and stress on the network/server. Higher and you might get failed downloads. Lower it if your network can't handle 10 but I don't recommend going higher (and definitely not higher than 20).

  • If someone can figure out how to set the resolution to download, (lowest, mid, highest), I would love to have this feature.

Disclaimer: I'm not a programmer, I'm a chatGPT steerer. The code was only lightly tested but seems to work fine

Debug

  • If you get {'statusCode': 401, 'error': 'Unauthorized', 'message': 'Expired token', 'attributes': {'error': 'Expired token'}} simply do step 3 again. This shouldn't happen if you're running the script shortly after setting it up. I don't know how often the toke changes but it's counted in hours so no rush.

  • If you get an error: Failed to download asset @@@, status code: 502 Response: 502 Bad Gateway, simply wait for the other assets to finish downloading and run the script again.

import os
import json
import concurrent.futures
import requests
import time
# Define the token, download path, and target category
token = "paste token here"
download_path = "path/to/where/you/want/the/zips/to/go" # Update with the correct path to the directory
json_file_path = "path/to/json/ms_asset_categories.json" # Update with the correct path to the file, not the directory
cache_file_path = "path/to/cache.txt"
target_category = "3D plants/grass/lawn"
# Function to normalize category strings
def normalize_category(category):
return category.strip().lower().rstrip('s')
# Function to load asset categories from JSON file
def load_asset_categories(json_file):
with open(json_file, 'r') as f:
return json.load(f)
# Function to convert categories to a space-separated string
def categories_to_string(categories):
result = []
if isinstance(categories, dict):
for key, value in categories.items():
if isinstance(value, dict) and value:
subcategories = categories_to_string(value)
if subcategories:
result.append(f"{key} {subcategories}")
else:
result.append(key)
elif isinstance(value, list):
for item in value:
result.append(normalize_category(item))
else:
result.append(normalize_category(key))
return " ".join(result)
# Load the asset categories from the JSON file
asset_categories_dict = load_asset_categories(json_file_path)
# Print what it's trying to match against
print(f"Trying to match against target category: {target_category}")
def get_asset_download_id(asset_id):
url = "https://quixel.com/v1/downloads"
params = {
"asset": asset_id,
"config": {
"lowerlod_meshes" : True,
"maxlod" : 100000000,
"highpoly": False,
"ztool": False,
"lowerlod_normals": True,
"albedo_lods": True,
"meshMimeType": "application/x-fbx",
"brushes": False,
"lods": [29155, 13541, 6289, 2921],
},
}
headers = {
"X-Api-Key": "your_api_key_here",
"Authorization": "Bearer " + token,
}
print(f"Getting download ID for asset: {asset_id}")
download_url_response = requests.post(url, headers=headers, json=params)
if download_url_response.status_code == 200:
return download_url_response.json()["id"]
else:
print(f"Failed to get asset download url for id: {asset_id}")
print(download_url_response.json())
return None
def download_asset(download_id, download_directory):
os.makedirs(download_directory, exist_ok=True)
url = f"https://assetdownloads.quixel.com/download/{download_id}?preserveStructure=true&url=https%3A%2F%2Fquixel.com%2Fv1%2Fdownloads"
print(f"Attempting to download from: {url}")
response = requests.get(url, stream=True)
if response.status_code == 400:
print(f"Error 400: {response.text}") # Print the response to see what's causing the issue
attempt_count = 0
delay = 5
max_attempts = 5
while attempt_count < max_attempts:
response = requests.get(url, stream=True)
if response.status_code == 200:
content_disposition = response.headers.get("Content-Disposition")
filename = content_disposition.split("filename=")[-1].strip('"') if content_disposition else download_id
file_path = os.path.join(download_directory, filename)
print(f"Downloading file: {file_path}")
try:
with open(file_path, "wb") as file:
for chunk in response.iter_content(chunk_size=8192):
file.write(chunk)
print(f"File downloaded successfully: {file_path}")
return True
except requests.exceptions.ChunkedEncodingError as e:
print(f"Error during download: {e}")
time.sleep(delay)
delay += 5
attempt_count += 1
else:
print(f"Failed to download asset {download_id}, status code: {response.status_code}")
print(f"Response: {response.text}") # Print the response content for more details
return False
print(f"Exceeded maximum retry attempts for asset {download_id}")
return False
# Load cached assets
cached_assets = set()
if os.path.exists(cache_file_path):
with open(cache_file_path, "r") as cache_file:
cached_assets = set(cache_file.read().splitlines())
# Normalize target category for matching
normalized_target_categories = [normalize_category(part) for part in target_category.split("/")]
matching_asset_ids = []
# Check matches for each asset in the loaded categories
for asset_id, categories in asset_categories_dict.items():
# Convert the categories to a single string for matching
categories_str = categories_to_string(categories)
# Check if all parts of target_category exist in the categories string
matches = all(normalize_category(part) in categories_str.lower() for part in normalized_target_categories)
if matches and asset_id not in cached_assets:
print(f"Asset ID: {asset_id} matches target category: {target_category}")
matching_asset_ids.append(asset_id)
if not matching_asset_ids:
print("No assets found for the target category.")
exit()
# Ask the user for confirmation before downloading
print(f"{len(matching_asset_ids)} assets found.")
confirmation = input(f"Do you want to download these {len(matching_asset_ids)} assets? (y/n): ").strip().lower()
if confirmation != "y":
print("Download canceled.")
exit()
# Function to handle downloading for threading
def download_asset_with_id(asset_id):
download_id = get_asset_download_id(asset_id)
if download_id:
return download_asset(download_id, download_path)
# Open the cache file for appending new downloads
with open(cache_file_path, "a+") as cache_file:
# Use threading for faster downloading
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = {executor.submit(download_asset_with_id, asset_id): asset_id for asset_id in matching_asset_ids}
for future in concurrent.futures.as_completed(futures):
asset_id = futures[future]
try:
result = future.result()
if result:
# Add the asset to the cache file after successful download
cache_file.write(f"{asset_id}\n")
cache_file.flush()
except Exception as e:
print(f"Error downloading asset {asset_id}: {e}")
// Function to get the value of a specific cookie by name
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
// Get the auth cookie
const authCookie = getCookie('auth');
// Parse the auth cookie (it should be a JSON string containing the token)
if (authCookie) {
try {
const authData = JSON.parse(decodeURIComponent(authCookie));
const authToken = authData.token;
console.log("Auth Token:", authToken);
} catch (error) {
console.error("Error parsing auth cookie:", error);
}
} else {
console.error("Auth cookie not found. Please make sure you are logged in.");
}
@ProtoNoob
Copy link

OK. Thanks for the info. I seem to be having quite a bit of success with Poly's script (and some of my own minor changes) so I am going to go ahead and try to finish with that. If I start having difficulties with this method I will definitely check out yours.

@ToxicCrack
Copy link

ToxicCrack commented Dec 23, 2024

BTW i *think* we don't have to download the assets at all. You can claim all quixel assets on fab.com with one click. After that, they are available to download at https://www.fab.com/sellers/Quixel forever.
At least thats how i understand this:
All Megascans are free to everyone under Fab’s Standard License until the end of 2024, for all engines and tools. Claim them now and then download from the Quixel profile page when you're ready to use them.

@SkillipEvolver
Copy link

@ToxicCrack I think that you might be missing the point a bit with what's going on here?
The point of the scripts are to download things with a high degree of automation,

@SkillipEvolver
Copy link

@Jarritus @gentlecolts Hey there guys. Sorry to eavesdrop (as such) but I couldn't help noticing the chat going on here..

I'm very late to the party (well, I've had this in mind for weeks but have only just managed to get a new PC put together).
-im getting set up for some 3d modelling, Im mainly into architecture to be honest but this set of assets seems incredible.

So.. big thanks to the author and contributors for sussing out the problems and persevering.

Myself I'm getting a high fail rate, (like above) with the good majority of assets 'expected to be 'X' big, but read to be 'smaller.
And then what follows is often a 502 Bad Gateway error.

--I'm starting with '3D Assets' btw.

reading lower in this thread, it looks like someone had some luck just letting it ride out, then checking things after the process has finished, and finding 'some' positives.

Of course, this is great.

I've stuck with the default 'worker' values, and have a wired conn. UK-based, reliable fiber line.
(don't know exact rates but I'm seeing 100 Mbps averagely in TaskMan.

sat at about 70 and I've been running about 2hours... although i expect the vast majority of these ZIPs will have failed. - i checked already and a good lot of them fail to open/damaged.

At this point, with the 'cut-off' being end of year (end of 31st Dec), I can't see me doing too well but!.. :D

so.. that said.. is there any potential for opening these assets up as an offered upload for folk?
Anyone in that Christmas mood still? ;-)
:-p

@WAUthethird
Copy link

WAUthethird commented Dec 31, 2024

My script checks each asset as it is downloaded and retries automatically if it fails, since there's no point continuing on if the asset download is bad.

I will also say that I have Syncthing up and running for my complete archive (21TB) if anyone wants in - just be prepared for a very long (weeks, maybe more) and very big download.

@ToxicCrack
Copy link

@ToxicCrack I think that you might be missing the point a bit with what's going on here? The point of the scripts are to download things with a high degree of automation,

@SkillipEvolver i know, i wrote one version of that script here

@SkillipEvolver
Copy link

My script checks each asset as it is downloaded and retries automatically if it fails, since there's no point continuing on if the asset download is bad.

I will also say that I have Syncthing up and running for my complete archive (21TB) if anyone wants in - just be prepared for a very long (weeks, maybe more) and very big download.

Thanks WUAtherhird !
-this is a very kind offer!
I'm currently having a last attempt at this by using a script.

I guess there's a chance at some point over night tonight that the megascans servers may no longer function/offer anything.. but if mine fails sir too that (or whatever reason), I would certainly be interested in this idea of SynchThing.

-- I have a question (a general one) and I wonder if anyone can help?
So the script I'm using is currently downloading textures in 8k EXR format.
This is obviously pretty sky high...

An advantage of these brilliant scripts is they allow you to keep the files on your local machine (and there's other desktop tools to let you organise then as well)...

8k might end up being a bit unweildy for me (I don't know yet until I try)..
system is
4070ti Super
32gb DDR5 6000MHz ram
i9 13900KF

as we all know... you rarely ever see anything in a finished form where 8k anything is used..

--since I have already clicked 'claim all' on the Fab website...

If I find that these 8k files are too big to work with, will the Fab website still let me choose to use for example a 4k, or 2k package?

@WAUthethird
Copy link

@SkillipEvolver Yes, Fab will let you download lower resolutions. Also, Quixel isn't going down tonight. Just the ability to claim assets. If you've already claimed them you can continue to download them.

Also, I just wanna push back a bit on the "8K textures are rarely used" comment - for video games they may be somewhat overkill (although imo they do still have use in high-end games) but Megascans are used for much more than games!

@SkillipEvolver
Copy link

@WAUthethird hi William.

I'm having great success using your script, so a massive thanks for your effort in putting it out there.
I just want to enquire about one thing..

You mentioned that it's been done as to obtain the data for archival purposes primarily, but I note that the metadata PY files (large and small) are quite comprehensive..

Is there any way contents of this PY can be leveraged to create a useable folder structure? With my limited knowledge, I can see from a cursory look that a typical zip file (with nonsensical name), will be written alongside an asset 'name' (in the metadata PY file).
... This provides something... but obviously in comparison to some other scripts available, there is no real folder hierarchy available..

is there anything we can realistically do about creating this do you think? 🤔

@WAUthethird
Copy link

WAUthethird commented Jan 1, 2025

@SkillipEvolver Yes! So, the complete metadata (.tar.zst, extracts to .json) file provides pretty much all the data to make any folder structure you want. There are asset categories (3D asset, surface, etc), tags, and many others on a per-asset basis that you can use. That is the reason my script doesn't create a folder structure - I wanted to give people the freedom to make their own structure after the fact.

The code needed to parse the JSON, create folders, and move files is pretty simple to write yourself if you know some basic programming. I'm sure an AI could also write usable code for this sort of a task, too.

@SkillipEvolver
Copy link

hi there @WAUthethird
Thanks for your last message regarding the metadata. I'll have a look at that but I'm not well-versed in scripting to be honest. (just saying).

Wonder if you could help at all? I've downloaded IMPERFECTIONS and DISPLACEMENTS ok, but trying with 3D PLANT and 3D ASSETS today, the script seems to suggest that I already downloaded these, showing '0 to download'.*

  • either that or they are no longer where the script is looking for them on the Quixel server.
  • do you know if there's been any changes to the layout/structure on their side of things? (I haven't used the site enough to be familiar with it's UI layout, to have any idea if something has changed in this way).
  • If the problem is because the script thinks I already downloaded them, where/what file holds that information so that I can look at it/perhaps delete it?

Attached a screenshot of the result of the script running so you can see.

https://imgur.com/a/ZCDmlx5
printscreen

@WAUthethird
Copy link

Ah, the issue there is that I threw together the category download feature pretty quickly, so it doesn't actually check your input for validity

You need to type it exactly as printed, so type "3D asset", not "3d asset"

@SkillipEvolver
Copy link

Thank you @WAUthethird. I tried again, case-sensitive, but had the same result. It now occurs to me that this might be related to an earlier attempt to download the 3D assets group but using a different script (not immediately sure whose, think it was @PolyShifter or maybe @maalrron). That group download did complete, and also created the folder tree; all asset zips were downloaded, but the zip files were all (bar 3x) corrupt and do not open. (that's when I thought I'd try your script).
I'm still thinking there must be a file somewhere on my system that says the asset group has been downloaded already, and that that file is interfering with your script. Any hints on what I should look for please, or any other ideas?
PS - @PolyShifter - if you should see this thread, do you have any ideas why the zips were corrupted? - Thanks

@WAUthethird
Copy link

@SkillipEvolver The only way it knows what assets have already been downloaded is via the checksums.json file that it creates. Check that, if it exists. Otherwise, check the metadata file itself, it might be incomplete or broken in some way.

@SkillipEvolver
Copy link

Thanks @WAUthethird. Not sure why I had the wrong checksums.json, but that was the culprit.
It would still be nice to create the assets' folder tree if you agree & have the time to write something - I'm sure many others would appreciate it as well.
Other than that, all sorted now, thank you.

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