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'?([a-zA-Z0-9]+)[^>]*>', 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)