from fastapi import FastAPI from fastapi.responses import StreamingResponse, FileResponse from fastapi.staticfiles import StaticFiles from PIL import Image import requests from io import BytesIO import urllib3 import json import base64 import os import uuid import time from pydantic import BaseModel from typing import Optional urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) app = FastAPI() # ========================= # CONFIG # ========================= BOT_TOKEN = "MTUwMTk1OTI1Njk4MTc2NjE4NA.Gcshyg.YHzk_9k39AVEgLq2IHXtKeLHYMl6AT7GbnUpwE" CHANNEL_ID = "1470082028518113342" MINESKIN_TOKEN = "msk_SBLac6yF_ApAeLAIAUGjlX9FQkiZ2itwwSmuzQ3sBM8KryyVHcD1HrW4KqPCcRGgEfIFPAVSY" # Создаем папку для сохранения скинов os.makedirs("skins", exist_ok=True) # Монтируем статические файлы app.mount("/skins", StaticFiles(directory="skins"), name="skins") # ========================= # MODEL # ========================= class SkinRequest(BaseModel): Playername: str HeadUrl: str BodyUrl: str ArmLeft: Optional[str] = None ArmRight: Optional[str] = None LegLeft: Optional[str] = None LegRight: Optional[str] = None # ========================= # DOWNLOAD SKIN # ========================= def download_skin(url: str): headers = {"User-Agent": "Mozilla/5.0"} r = requests.get(url, headers=headers, timeout=30, verify=False) r.raise_for_status() return Image.open(BytesIO(r.content)).convert("RGBA") # ========================= # PARSE SKIN INPUT # ========================= def parse_skin_input(value: str): if not value: return None if value.startswith("http"): return value try: decoded = base64.b64decode(value).decode("utf-8") data = json.loads(decoded) return data["textures"]["SKIN"]["url"] except: return value # ========================= # MINECRAFT SKIN PARTS (ИЗ ПЕРВОГО КОДА) # ========================= HEAD = [ (0, 0, 32, 16), (32, 0, 64, 16) ] BODY = [ (16, 16, 40, 32), (16, 32, 40, 48) ] RIGHT_ARM = [ (40, 16, 56, 32), (40, 32, 56, 48) ] LEFT_ARM = [ (32, 48, 48, 64), (48, 48, 64, 64) ] RIGHT_LEG = [ (0, 16, 16, 32), (0, 32, 16, 48) ] LEFT_LEG = [ (16, 48, 32, 64), (0, 48, 16, 64) ] # ========================= # CLEAR AREA (ИЗ ПЕРВОГО КОДА) # ========================= def clear_area(img, box): x1, y1, x2, y2 = box empty = Image.new("RGBA", (x2 - x1, y2 - y1), (0, 0, 0, 0)) img.paste(empty, (x1, y1)) # ========================= # COPY PART (ИЗ ПЕРВОГО КОДА) # ========================= def copy(img_from, img_to, box): x1, y1, x2, y2 = box part = img_from.crop((x1, y1, x2, y2)) img_to.paste(part, (x1, y1), part) # ========================= # BUILD SKIN (ИЗ ПЕРВОГО КОДА) # ========================= def create_merged_skin(req: SkinRequest): body = download_skin(parse_skin_input(req.BodyUrl)) head = download_skin(parse_skin_input(req.HeadUrl)) arm_l = download_skin(parse_skin_input(req.ArmLeft)) if req.ArmLeft else None arm_r = download_skin(parse_skin_input(req.ArmRight)) if req.ArmRight else None leg_l = download_skin(parse_skin_input(req.LegLeft)) if req.LegLeft else None leg_r = download_skin(parse_skin_input(req.LegRight)) if req.LegRight else None # CLEAN BASE result = Image.new("RGBA", (64, 64), (0, 0, 0, 0)) # BODY FIRST for b in BODY: copy(body, result, b) # HEAD for b in HEAD: copy(head, result, b) # ARMS (optional override) if arm_r: for b in RIGHT_ARM: clear_area(result, b) copy(arm_r, result, b) if arm_l: for b in LEFT_ARM: clear_area(result, b) copy(arm_l, result, b) # LEGS if leg_r: for b in RIGHT_LEG: clear_area(result, b) copy(leg_r, result, b) if leg_l: for b in LEFT_LEG: clear_area(result, b) copy(leg_l, result, b) return result # ========================= # SAVE SKIN # ========================= def save_skin(image, playername: str): safe_name = "".join(c for c in playername if c.isalnum() or c in ('_', '-')) filename = f"{safe_name}_{uuid.uuid4().hex[:8]}_{int(time.time())}.png" filepath = os.path.join("skins", filename) image.save(filepath, format="PNG") space_url = os.getenv("SPACE_HOST", "dedlepexa-discordapiuse.hf.space") public_url = f"https://{space_url}/skins/{filename}" return public_url, filename # ========================= # ENDPOINTS (ИЗ ВТОРОГО КОДА) # ========================= @app.post("/merge-to-link") def merge_to_link(request: SkinRequest): try: print(f"🎮 Игрок: {request.Playername}") merged_skin = create_merged_skin(request) public_url, filename = save_skin(merged_skin, request.Playername) print(f"🔗 URL: {public_url}") return { "success": True, "Playername": request.Playername, "url": public_url, "filename": filename } except Exception as e: print(f"❌ Ошибка: {str(e)}") return { "success": False, "error": str(e) } @app.get("/merge.png") def merge_png(head_skin: str, body_skin: str): req = SkinRequest(Playername="test", HeadUrl=head_skin, BodyUrl=body_skin) img = create_merged_skin(req) buf = BytesIO() img.save(buf, "PNG") buf.seek(0) return StreamingResponse(buf, media_type="image/png") @app.api_route("/skins/{file}", methods=["GET", "HEAD"]) def skin_file(file: str): return FileResponse(os.path.join("skins", file)) # ========================= # GET/HEAD для Mineskin # ========================= @app.post("/merge-to-mineskin") def merge_to_mineskin(request: SkinRequest): try: print(f"🎮 Игрок: {request.Playername}") merged_skin = create_merged_skin(request) public_url, filename = save_skin( merged_skin, request.Playername ) print(f"🔗 Skin URL: {public_url}") headers = { "Content-Type": "application/json", "Accept": "application/json", "User-Agent": "SkinMerger/1.0", "Authorization": f"Bearer {MINESKIN_TOKEN}" } payload = { "variant": "classic", "name": request.Playername, "visibility": "public", "url": public_url } response = requests.post( "https://api.mineskin.org/v2/generate", headers=headers, json=payload, timeout=60, verify=False ) mineskin_response = response.json() return { "success": True, "Playername": request.Playername, "skin_url": public_url, "filename": filename, "mineskin_response": mineskin_response } except Exception as e: return { "success": False, "error": str(e) } @app.get("/") def root(): return { "status": "ok", "service": "Skin Merger - Fixed", "endpoints": { "POST /merge-to-link": "Создать скин и получить ссылку", "POST /merge-to-mineskin": "Создать скин и загрузить в MineSkin", "GET /merge.png": "Быстрое тестирование", "GET /skins/{filename}": "Доступ к сохраненным скинам" } } # ========================= # RUN # ========================= if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=7860)