Last active
January 8, 2026 16:01
-
-
Save Specnr/e97adbfd1e25efb9dd3d9311df3ca47a to your computer and use it in GitHub Desktop.
Python script to randomize your Minecraft skin + cape (read tutorial in code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import os | |
| import shutil | |
| import sys | |
| import json | |
| import requests | |
| from random import randint | |
| from datetime import datetime, timedelta | |
| # ========================================== | |
| # Tutorial | |
| # ========================================== | |
| # Download Python if you havent already | |
| # https://apps.microsoft.com/detail/9PNRBTZXMB4Z | |
| # Set LAUNCHER_PATH to the path of your MultiMC, Prism or MCSR Launcher | |
| # ex: C:\Users\Spencer\AppData\Local\MCSRLauncher\launcher | |
| # ex: C:\MultiInstanceMC\MultiMC | |
| # Set SKIN_ID and CAPE_ID to the file names of your skin and cape files (should be similar format to the default) | |
| # To get these follow this tutorial - https://youtu.be/izyY35w30II | |
| # Create folders called "skins" and "capes" in the same directory as this script | |
| # Put in your skin and cape files into the folders, renaming them x.png where x matches for the same skin / cape | |
| # ex: skin / cape combo 1 = skins/1.png and capes/1.png | |
| # ex: skin / cape combo 2 = skins/2.png and capes/2.png | |
| # Run this script before starting your instance, making a batch file is recommended | |
| # ex: python ./skin_randomizer.py | |
| # Then you can add the .bat file to the "Program Launching" plugin in Jingle for automation | |
| # ========================================== | |
| # Configuration - You need to edit this | |
| # ========================================== | |
| LAUNCHER_PATH = r"C:\MultiInstanceMC\MultiMC" | |
| SKIN_ID = "523ce73012aaec66a614ec864ca36236897b9e64" | |
| CAPE_ID = "9eeb3aa671dd4b0c9d425756c3dde510e1abcb8d" | |
| # ========================================== | |
| # Constants | |
| # ========================================== | |
| # Generalize skin count by counting files in the skins folder | |
| if os.path.exists("skins"): | |
| SKIN_COUNT = len([f for f in os.listdir("skins") if f.endswith('.png')]) | |
| else: | |
| SKIN_COUNT = 0 | |
| # Derived paths | |
| LAUNCHER_ACCOUNTS_PATH = os.path.join(LAUNCHER_PATH, "accounts.json") | |
| TOKEN_CACHE_FILE = "credentials.json" | |
| # Mojang API URLs | |
| MOJANG_SKIN_API_URL = "https://api.minecraftservices.com/minecraft/profile/skins" | |
| # ========================================== | |
| # Authentication | |
| # ========================================== | |
| class MinecraftOAuth: | |
| """Handles extracting and caching Minecraft authentication tokens.""" | |
| def get_cached_credentials(self): | |
| """Get cached credentials if they exist and are valid.""" | |
| if not os.path.exists(TOKEN_CACHE_FILE): | |
| return None | |
| try: | |
| with open(TOKEN_CACHE_FILE, 'r') as f: | |
| creds = json.load(f) | |
| if "minecraft_token" in creds and "minecraft_expiry" in creds: | |
| expiry = datetime.fromisoformat(creds["minecraft_expiry"]) | |
| # Return token only if it's not about to expire (10 min buffer) | |
| if datetime.now() < (expiry - timedelta(minutes=10)): | |
| return creds["minecraft_token"] | |
| except Exception as e: | |
| print(f"Error reading credentials cache: {e}") | |
| return None | |
| def save_credentials(self, token, expiry_ts): | |
| """Save credentials to cache.""" | |
| try: | |
| new_creds = { | |
| "minecraft_token": token, | |
| "minecraft_expiry": datetime.fromtimestamp(expiry_ts).isoformat() if expiry_ts else (datetime.now() + timedelta(hours=24)).isoformat(), | |
| "updated_at": datetime.now().isoformat(), | |
| "source": "Launcher" | |
| } | |
| with open(TOKEN_CACHE_FILE, 'w') as f: | |
| json.dump(new_creds, f, indent=4) | |
| except Exception as e: | |
| print(f"Error saving credentials cache: {e}") | |
| def get_token_from_launcher(self): | |
| """Extract the active Minecraft token from Launcher's accounts.json.""" | |
| if not os.path.exists(LAUNCHER_ACCOUNTS_PATH): | |
| print(f"Launcher accounts file not found at {LAUNCHER_ACCOUNTS_PATH}") | |
| return None | |
| try: | |
| with open(LAUNCHER_ACCOUNTS_PATH, 'r') as f: | |
| data = json.load(f) | |
| accounts = data.get("accounts", []) | |
| active_account = next((a for a in accounts if a.get("active")), None) | |
| if not active_account and len(accounts) > 0: | |
| active_account = accounts[0] | |
| if not active_account: | |
| print("No active account found in Launcher.") | |
| return None | |
| ygg = active_account.get("ygg", {}) | |
| profile = active_account.get("profile", {}) | |
| if ygg: | |
| token = ygg.get("token") | |
| expiry_ts = ygg.get("exp") | |
| elif profile: | |
| token = profile.get("accessToken") | |
| expiry_ts = int(str(profile.get("expireAt"))[:-3]) | |
| if not token: | |
| print("No token found for the active account in Launcher.") | |
| return None | |
| if expiry_ts and datetime.now().timestamp() > (expiry_ts - 600): | |
| print("The Launcher token is expired. Please open Launcher to refresh it.") | |
| return None | |
| profile_name = active_account.get('profile', {}).get('name', 'Unknown') | |
| print(f"Successfully extracted token for account: {profile_name}") | |
| self.save_credentials(token, expiry_ts) | |
| return token | |
| except Exception as e: | |
| print(f"Error extracting token from Launcher: {e}") | |
| return None | |
| def get_access_token(self): | |
| """Main entry point to get a valid Minecraft Bearer token.""" | |
| # 1. Check if our local cache has a valid token | |
| token = self.get_cached_credentials() | |
| if token: | |
| return token | |
| # 2. If not, try to sync from Launcher | |
| print("Local token expired or missing. Syncing from Launcher...") | |
| return self.get_token_from_launcher() | |
| # ========================================== | |
| # Mojang API Helpers | |
| # ========================================== | |
| def upload_skin_to_mojang(access_token, skin_file_path, variant="classic"): | |
| """Upload skin to Mojang servers.""" | |
| if not os.path.exists(skin_file_path): | |
| print(f"Skin file not found: {skin_file_path}") | |
| return False | |
| headers = {'Authorization': f'Bearer {access_token}'} | |
| data = {'variant': variant} | |
| try: | |
| with open(skin_file_path, 'rb') as skin_file: | |
| files = {'file': ('skin.png', skin_file, 'image/png')} | |
| response = requests.post(MOJANG_SKIN_API_URL, headers=headers, data=data, files=files) | |
| if response.status_code == 200: | |
| print("Skin updated successfully on Mojang servers!") | |
| return True | |
| else: | |
| print(f"Failed to update skin on server: {response.status_code}") | |
| print(f"Response: {response.text}") | |
| return False | |
| except Exception as e: | |
| print(f"Error uploading skin to server: {e}") | |
| return False | |
| # ========================================== | |
| # Main Logic | |
| # ========================================== | |
| def main(): | |
| # 1. Determine which skin to use | |
| if len(sys.argv) > 1: | |
| try: | |
| skin_num = int(sys.argv[1]) | |
| print(f"Using skin number from argument: {skin_num}") | |
| except ValueError: | |
| print(f"Invalid skin number: {sys.argv[1]}. Picking random.") | |
| skin_num = randint(1, SKIN_COUNT) | |
| else: | |
| skin_num = randint(1, SKIN_COUNT) | |
| skin_filename = f"{skin_num}.png" | |
| skin_source = os.path.join("skins", skin_filename) | |
| cape_source = os.path.join("capes", skin_filename) | |
| # 2. Update Local Files (Launcher) | |
| def update_local_asset(asset_id, source_path, asset_type): | |
| if not asset_id or not os.path.exists(source_path): | |
| return | |
| # Paths for Launcher | |
| paths = [ | |
| os.path.join(LAUNCHER_PATH, "assets", "skins", asset_id[:2], asset_id) | |
| ] | |
| for p in paths: | |
| os.makedirs(os.path.dirname(p), exist_ok=True) | |
| shutil.copyfile(source_path, p) | |
| print(f"Local {asset_type} updated: {skin_filename}") | |
| update_local_asset(SKIN_ID, skin_source, "skin") | |
| update_local_asset(CAPE_ID, cape_source, "cape") | |
| # 3. Update Mojang Server-side | |
| if SKIN_ID and os.path.exists(skin_source): | |
| auth = MinecraftOAuth() | |
| access_token = auth.get_access_token() | |
| if access_token: | |
| upload_skin_to_mojang(access_token, skin_source) | |
| else: | |
| print("Skipping server-side skin update - no valid token available.") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment