Skip to content

Instantly share code, notes, and snippets.

@Specnr
Last active January 8, 2026 16:01
Show Gist options
  • Select an option

  • Save Specnr/e97adbfd1e25efb9dd3d9311df3ca47a to your computer and use it in GitHub Desktop.

Select an option

Save Specnr/e97adbfd1e25efb9dd3d9311df3ca47a to your computer and use it in GitHub Desktop.
Python script to randomize your Minecraft skin + cape (read tutorial in code)
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