import os import re import shutil import subprocess from pathlib import Path # Detectar la estructura del proyecto SCRIPT_DIR = Path(__file__).resolve().parent # /proyecto/generador/ PROJECT_ROOT = SCRIPT_DIR.parent # /proyecto/ # Carpeta de entrada (busca archivos XML/TXT en la raíz del proyecto) CARPETA_ENTRADA = PROJECT_ROOT # Ruta absoluta hacia /fastlane/metadata/android/ (al mismo nivel que /generador/) FASTLANE_BASE_DIR = PROJECT_ROOT / "fastlane" / "metadata" / "android" # Etiquetas HTML permitidas por Google Play Console TAGS_PERMITIDAS = {'b', 'i', 'u', 'a', 'font', 'sub', 'sup', 'br'} def limpiar_carpeta_android(): """ Elimina por completo el contenido de fastlane/metadata/android y la recrea vacía. """ if FASTLANE_BASE_DIR.exists(): shutil.rmtree(FASTLANE_BASE_DIR) print(f"🧹 Carpeta eliminada para limpieza: {FASTLANE_BASE_DIR}") FASTLANE_BASE_DIR.mkdir(parents=True, exist_ok=True) print(f"📁 Carpeta de destino preparada (vacía): {FASTLANE_BASE_DIR}\n") def validar_html_google_play(texto_html): tags_encontradas = re.findall(r']*>', texto_html) tags_prohibidas = set() for tag in tags_encontradas: tag_lower = tag.lower() if tag_lower not in TAGS_PERMITIDAS: tags_prohibidas.add(tag_lower) return list(tags_prohibidas) def procesar_bloque_aso(texto_xml, nombre_archivo, version_code): entries = re.findall(r'(.*?)', texto_xml, re.DOTALL) if not entries: return [] idiomas_procesados = [] for locale, content in entries: locale = locale.strip() titulo_match = re.search(r'(.*?)', content, re.DOTALL) corta_match = re.search(r'(.*?)', content, re.DOTALL) larga_match = re.search(r'(.*?)', content, re.DOTALL) notas_match = re.search(r'(.*?)', content, re.DOTALL) titulo = titulo_match.group(1).strip().replace('&', '&') if titulo_match else "" desc_corta = corta_match.group(1).strip().replace('&', '&').removesuffix('.') if corta_match else "" desc_larga = larga_match.group(1).strip().replace('&', '&') if larga_match else "" notas = notas_match.group(1).strip().replace('&', '&') if notas_match else "" # Reemplazo automático de por desc_larga = re.sub(r'(.*?)', r'\1', desc_larga, flags=re.IGNORECASE | re.DOTALL) # Enlace web al final desc_larga += '

https://appinitdev.com/' # Validación de límites y HTML errores = [] if len(titulo) > 30: errores.append(f"Título excede límite: {len(titulo)}/30 chars ('{titulo}')") if len(desc_corta) > 80: errores.append(f"Desc. Corta excede límite: {len(desc_corta)}/80 chars") if len(desc_larga) > 4000: errores.append(f"Desc. Larga excede límite: {len(desc_larga)}/4000 chars") if len(notas) > 500: errores.append(f"Notas de Versión exceden límite: {len(notas)}/500 chars") tags_invalidas = validar_html_google_play(desc_larga) if tags_invalidas: errores.append(f"Desc. Larga contiene HTML prohibido: {', '.join(['<' + t + '>' for t in tags_invalidas])}") if errores: print(f"❌ ERRORES EN IDIOMA [{locale}] (Archivo: {nombre_archivo}):") for err in errores: print(f" - {err}") print(" 👉 Corrige este archivo antes de continuar.\n") continue # Crear rutas para el idioma actual base_path = FASTLANE_BASE_DIR / locale changelogs_path = base_path / "changelogs" os.makedirs(base_path, exist_ok=True) os.makedirs(changelogs_path, exist_ok=True) # Guardar archivos with open(base_path / "title.txt", "w", encoding="utf-8") as f: f.write(titulo) with open(base_path / "short_description.txt", "w", encoding="utf-8") as f: f.write(desc_corta) with open(base_path / "full_description.txt", "w", encoding="utf-8") as f: f.write(desc_larga) with open(changelogs_path / f"{version_code}.txt", "w", encoding="utf-8") as f: f.write(notas) print(f"✔️ [{locale}] extraído -> fastlane/metadata/android/{locale}/ (changelogs/{version_code}.txt)") idiomas_procesados.append(locale) return idiomas_procesados def procesar_archivos_aso(directorio, version_code): archivos = [ f for f in os.listdir(directorio) if (f.endswith('.xml') ) and f != 'script.py' ] if not archivos: print(f"⚠️ No se encontraron archivos XML o TXT en '{directorio}'.") return False # Limpieza total de la carpeta destino antes de generar los nuevos archivos limpiar_carpeta_android() print(f"🔍 Escaneando {len(archivos)} archivo(s) en '{os.path.abspath(directorio)}':\n") todos_los_idiomas = [] for archivo in sorted(archivos): ruta_completa = os.path.join(directorio, archivo) try: with open(ruta_completa, "r", encoding="utf-8") as f: contenido = f.read() idiomas_obtenidos = procesar_bloque_aso(contenido, archivo, version_code) todos_los_idiomas.extend(idiomas_obtenidos) except Exception as e: print(f"⚠️ Error al leer {archivo}: {e}") print("\n" + "=" * 50) print(f"🎉 Proceso completado: Se generaron {len(todos_los_idiomas)} estructura(s) en Fastlane.") if todos_los_idiomas: print(f"📋 Idiomas procesados ({len(todos_los_idiomas)}): {', '.join(todos_los_idiomas)}") print("=" * 50) return len(todos_los_idiomas) > 0 def ejecutar_fastlane_supply(version_code): """ Ejecuta el comando fastlane supply directamente desde la raíz del proyecto. """ print(f"\n🚀 Ejecutando Fastlane Supply para Version Code {version_code}...") comando = [ "fastlane", "supply", "--version_code", str(version_code), "--skip_upload_apk", "--skip_upload_aab", "--skip_upload_images", "--skip_upload_screenshots" ] try: # shell=True permite a Windows reconocer fastlane.bat / fastlane.cmd en el PATH resultado = subprocess.run( comando, cwd=PROJECT_ROOT, check=True, text=True, shell=True ) print("\n✅ Subida de texto e información ASO completada con éxito en Google Play Console.") except subprocess.CalledProcessError as e: print(f"\n❌ Ocurrió un error al ejecutar Fastlane Supply (Código de salida: {e.returncode}).") except Exception as e: print(f"\n❌ Error al intentar invocar Fastlane: {e}") if __name__ == "__main__": version_input = input("Ingresa el Version Code (Presiona Enter para usar 1): ").strip() version_code_final = version_input if version_input else "1" # Procesar metadatos y, si se generaron archivos válidos, ejecutar Fastlane exito = procesar_archivos_aso(CARPETA_ENTRADA, version_code_final) if exito: ejecutar_fastlane_supply(version_code_final)