-
-
Save gnuton/ec2c3c2097f7aeaea8bb7d1256e4b212 to your computer and use it in GitHub Desktop.
| // Mixamo Animation downloadeer | |
| // | |
| // Author: Antonio Aloisio <[email protected]> | |
| // Contributions: kriNon | |
| // | |
| // The following script make use of mixamo2 API to download all anims for a single character that you choose. | |
| // The animations are saved with descriptive long names instead of the short ones used by default by mixamo UI. | |
| // | |
| // This script has been written by [email protected] and the author is not responsible of its usage | |
| // | |
| // How to use this script | |
| // 1. Browse mixamo.com | |
| // 2. Log in | |
| // 3. Open JS console (F12 on chrome) | |
| // 4. Download an animation and get the character ID from the Network tab | |
| // 5. Then past the character id in the "character" variable at beginning of this script | |
| // 6. Copy and paste the full script in the mixamo.com javascript console | |
| // 7. The script will open a new blank page.. you will start to see animations downloading | |
| // 8. keep the blank page opened and keep on pressing "Allow multiple downlaods" | |
| // NOTE. This doesn't really work for me, but it was supposed too | |
| // Chrome will ask you all the time to allow multiple downloads | |
| // You can disable this as follow: | |
| // chrome://settings/ > Advanced > Content > Automatic downloads > uncheck "Do not allow any site to download multiple file automatically" | |
| // CHANGE THIS VAR TO DOWNLOAD ANIMATIONS FOR A DIFFERENT CHARACTER | |
| // const character = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' | |
| const character = 'ef7eb018-7cf3-4ae1-99ac-bab1c2c5d419' | |
| //================================================================================================= | |
| const bearer = localStorage.access_token | |
| var oldAnimId = "" | |
| const getAnimationList = (page) => { | |
| console.log('getAnimationList page=', page); | |
| const init = { | |
| method: 'GET', | |
| headers: { | |
| 'Accept': 'application/json', | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${bearer}`, | |
| 'X-Api-Key': 'mixamo2' | |
| } | |
| }; | |
| const listUrl = `https://www.mixamo.com/api/v1/products?page=${page}&limit=96&order=&type=Motion%2CMotionPack&query=`; | |
| return fetch(listUrl, init).then((res) => res.json()).then((json) => json).catch(() => Promise.reject('Failed to download animation list')) | |
| } | |
| // retrieves json.details.gms_hash | |
| const getProduct = (animId, character) => { | |
| console.log('getProduct animId=', animId, ' character=', character); | |
| const init = { | |
| method: 'GET', | |
| headers: { | |
| 'Accept': 'application/json', | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${bearer}`, | |
| 'X-Api-Key': 'mixamo2' | |
| } | |
| }; | |
| const productUrl = `https://www.mixamo.com/api/v1/products/${animId}?similar=0&character_id=${character}`; | |
| return fetch(productUrl, init).then((res) => res.json()).then((json) => json).catch(() => Promise.reject('Failed to download product details')) | |
| } | |
| const downloadAnimation = (animId, character, product_name) => { | |
| console.log('downloadAnimation animId=', animId, ' character=', character, ' prod name=', product_name); | |
| // skip packs | |
| if (product_name.indexOf(',') > -1) { | |
| console.log('Skipping pack ', product_name); | |
| return Promise.resolve('Skip pack!'); | |
| } else { | |
| return getProduct(animId, character) | |
| .then((json) => json.details.gms_hash) | |
| .then((gms_hash) => { | |
| const pvals = gms_hash.params.map((param) => param[1]).join(',') | |
| const _gms_hash = Object.assign({}, gms_hash, { params: pvals }) // Anim is baked with default param values | |
| return exportAnimation(character, [_gms_hash], product_name) | |
| }) | |
| .then((json) => monitorAnimation(character,animId)) | |
| .catch(() => Promise.reject("Unable to download animation " + animId)) | |
| } | |
| } | |
| const downloadAnimLoop = (o) => { | |
| console.log('downloadAnimLoop'); | |
| if (!o.anims.length) { | |
| return downloadAnimsInPage(o.currentPage + 1, o.totPages, o.character); // no anims left, get a new page | |
| } | |
| const head = o.anims[0]; | |
| const tail = o.anims.slice(1); | |
| o.anims = tail; | |
| return downloadAnimation(head.id, o.character, head.name) | |
| .then(() => downloadAnimLoop(o)) //loop | |
| .catch(() => { | |
| console.log("Recovering from animation failed to downlaod"); | |
| return downloadAnimLoop(o) // keep on looping | |
| }) | |
| } | |
| var downloadAnimsInPage = (page, totPages, character) => { | |
| console.log('downloadAnimsInPage page=', page, ' totPages', totPages, ' character=', character); | |
| if (page >= totPages) { | |
| console.log('All pages have been downloaded'); | |
| return Promise.resolve('All pages have been downlaoded'); | |
| } | |
| return getAnimationList(page) | |
| .then((json) => ( | |
| { | |
| anims: json.results, | |
| currentPage: json.pagination.page, | |
| totPages: json.pagination.num_pages, | |
| character | |
| })) | |
| .then((o) => downloadAnimLoop(o)) | |
| .catch((e) => Promise.reject("Unable to download all animations error ", e)) | |
| } | |
| const start = () => { | |
| console.log('start'); | |
| if (!character) { | |
| console.error("Please add a valid character ID at the beginnig of the script"); | |
| return | |
| } | |
| downloadAnimsInPage(1, 100, character); | |
| } | |
| const exportAnimation = (character_id, gmsHashArray, product_name) => { | |
| console.log('Exporting Anim´:' + character_id + " to file:" + product_name) | |
| const exportUrl = 'https://www.mixamo.com/api/v1/animations/export' | |
| const exportBody = { | |
| character_id, | |
| gms_hash: gmsHashArray, //[{ "model-id": 103120902, "mirror": false, "trim": [0, 100], "overdrive": 0, "params": "0,0,0", "arm-space": 0, "inplace": false }], | |
| preferences: { format: "fbx7", skin: "false", fps: "30", reducekf: "0" }, // To download collada use format: "dae_mixamo" | |
| product_name, | |
| type: "Motion" | |
| }; | |
| const exportInit = { | |
| method: 'POST', | |
| headers: { | |
| 'Accept': 'application/json', | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${bearer}`, | |
| 'X-Api-Key': 'mixamo2', | |
| 'X-Requested-With': 'XMLHttpRequest' | |
| }, | |
| body: JSON.stringify(exportBody) | |
| } | |
| return fetch(exportUrl, exportInit) | |
| .then((res) => { | |
| switch (res.status) { | |
| case 429:{ | |
| console.log('ERROR 429, Too many requests, looping'); | |
| sleep(500) | |
| return exportAnimation(character_id, gmsHashArray, product_name); | |
| } break; | |
| default: | |
| res.json().then((json) => json) | |
| } | |
| }) | |
| } | |
| function sleep(milliseconds) { | |
| const date = Date.now(); | |
| let currentDate = null; | |
| do { | |
| currentDate = Date.now(); | |
| } while (currentDate - date < milliseconds); | |
| } | |
| const monitorAnimation = (characterId,animId) => { | |
| if (true) | |
| { | |
| const monitorUrl = `https://www.mixamo.com/api/v1/characters/${characterId}/monitor`; | |
| const monitorInit = { | |
| method: 'GET', | |
| headers: { | |
| 'Accept': 'application/json', | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${bearer}`, | |
| 'X-Api-Key': 'mixamo2' | |
| } | |
| }; | |
| return fetch(monitorUrl, monitorInit) | |
| .then((res) => { | |
| switch (res.status) { | |
| case 404: { | |
| const errorMsg = ('ERROR: Monitor got 404 error: ' + res.error + ' message=' + res.message); | |
| console.error(errorMsg); | |
| throw new Error(errorMsg); | |
| } break | |
| case 429:{ | |
| console.log('ERROR 429, Too many requests, looping'); | |
| sleep(500) | |
| return monitorAnimation(characterId,animId); | |
| } break; | |
| case 202: | |
| case 200: { | |
| return res.json() | |
| } break | |
| default: | |
| throw new Error('Response not handled', res); | |
| } | |
| }).then((msg) => { | |
| switch (msg.status) { | |
| case 'completed': | |
| if (oldAnimId != msg.job_result) | |
| { | |
| oldAnimId = msg.job_result | |
| console.log('Downloading: ', msg.job_result); | |
| downloadingTab.location.href = msg.job_result; | |
| return msg.job_result; | |
| } | |
| break; | |
| case 'processing': | |
| console.log('Animation is processing... looping'); | |
| return monitorAnimation(characterId,animId); | |
| break;// loop | |
| case 'failed': | |
| default: | |
| const errorMsg = ('ERROR: Monitor status:' + msg.status + ' message:' + msg.message + 'result:' + JSON.stringify(msg.job_result)); | |
| console.error(errorMsg); | |
| throw new Error(errorMsg); | |
| } | |
| }).catch((e) => Promise.reject("Unable to monitor job for character " + characterId + e)) | |
| } | |
| } | |
| // Workaround for downloading files from a promise | |
| // NOTE that chrome will detect you are downloading multiple files in a single TAB. Please allow it! | |
| const downloadingTab = window.open('', '_blank'); | |
| start() |
Yo. I made a few fixes to the original script. Posting it here in case it helps people.
This version prevents the script from downloading multiple copies of a file, and when the API throws a 429 error it will wait 0.5 seconds and try again (until it eventually works).
https://gist.github.com/kriNon/60036376f3c61b69573a042107d481e2
For inplace animations add this before line 155:
gmsHashArray[0].inplace = true;
Hi, actually I changed it to 104 since I saw this is the last page number, and the problem occurs with that number. Another issue I now noticed is that it downloads a lot of duplicates, i.e it sometimes downloads the same animation several times. Thank you for your help!
I think it does that cause there's more than one like "jumping down" for example
If anybody is using this script try it in Microsoft Edge I did it in windows 11 and it worked just fine I got them all in Chrome it was a little hairy
@gnuton please can you update the script to include a delay in downloading the next animation? I've tried using this multiple times & while it's great, the duplicate downloads are causing it to bypass some downloads, leaving around 1000 animations unobtainable via this method. Unfortunately I don't know JS enough to add this in.