import json
import os
from bs4 import BeautifulSoup
# 1. Tu catálogo de rutas (slug/subdominio) -> identificador de la app
catalogoApps = {
"posmanager-free": "com.appinitdev.posmanager.free",
"nexuslocal": "com.appinitdev.nexusLocal",
"fertilitybaby": "com.appinitdev.fertilitybaby",
"file-explorer": "com.appinitdev.fileExplorer",
"wallet": "com.appinitdev.wallet",
"imagetools-pro": "com.appinitdev.imagetools.pro",
"goodtime": "com.appinitdev.goodtime",
"posmanager": "com.appinitdev.posmanager",
"musicpro": "com.appinitdev.musicpro",
"imagetools": "com.appinitdev.imagetools",
"turismooax": "com.appinitdev.turismoOax",
"periodictable": "com.appinitdev.periodictable",
"file-manager-free": "com.appinitdev.filefree",
"gallery-free": "com.appinitdev.galleryfree",
"gallery": "com.appinitdev.gallery",
"testers": "com.appinitdev.tester",
"chatok": "com.appinitdev.chatok",
"derivatives-free": "com.appinitdev.derivatives.free",
"methods-free": "com.appinitdev.methods.free",
"methods-premium": "com.appinitdev.methods.premium",
"math": "com.appinitdev.math",
"imc": "com.appinitdev.imc",
"birth-free": "com.appinitdev.gestogram",
"pregnancy-free": "com.appinitdev.development.free",
"notes-premium": "com.appinitdev.notes.premium",
"notes-free": "com.appinitdev.notes.free",
"puzzle-free": "com.appinitdev.puzzle.fre",
"capture-free": "com.appinitdev.capture.premium",
"capture-premium": "com.appinitdev.capture.free",
"color-premium": "com.appinitdev.color.premium",
"color-free": "com.appinitdev.color.free",
"pyrotechnics-free": "com.appinitdev.pyrotechnics.free",
"encrypted": "com.appinitdev.pyrotechnics.premium",
"draw": "com.appinitdev.draw",
"pregnancy": "com.appinitdev.pregnancy",
"canvas-premium": "com.appinitdev.canvas.premium",
"canvas-free": "com.appinitdev.canvas.free",
"amplify-free": "com.appinitdev.music",
"elementalhub": "com.appinitdev.table",
"trace": "com.appinitdev.trace"
}
# 2. Cargar los datos completos del JSON
try:
with open('apps_full_data.json', 'r', encoding='utf-8') as f:
apps_data = json.load(f)
except FileNotFoundError:
print("❌ Error: No se encontró el archivo apps_full_data.json")
exit()
# 3. Leer la plantilla base (app.html)
try:
with open('app.html', 'r', encoding='utf-8') as f:
template_html = f.read()
except FileNotFoundError:
print("❌ Error: No se encontró la plantilla app.html")
exit()
# 4. Crear carpeta base de salida
base_output_dir = 'pages'
os.makedirs(base_output_dir, exist_ok=True)
# 5. Generar una página por cada app en el catálogo
for slug, app_id in catalogoApps.items():
app = next((item for item in apps_data if item.get('appId') == app_id), None)
if not app:
print(f"⚠️ Advertencia: No se encontraron datos en el JSON para {app_id} ({slug})")
continue
soup = BeautifulSoup(template_html, 'html.parser')
# --- DATOS BÁSICOS ---
app_title = app.get('title', 'Aplicación')
title = f"{app_title} - Descargar App | AppInitDev"
desc = app.get('summary', f"Descarga {app_title} desarrollada por {app.get('developer', 'AppInitDev')}.")
icon = app.get('icon', '../../images/logotipo.png').replace('.webp', '.png')
header_bg_url = app.get('headerImage') if app.get('headerImage') else '/images/parallax-03.jpg'
# --- INYECCIÓN DE SEO (META TAGS Y FAVICON) ---
if soup.title: soup.title.string = title
if soup.find(id="meta-title"): soup.find(id="meta-title").string = title
if soup.find(id="meta-description"): soup.find(id="meta-description")['content'] = desc
# Favicon dinámico con la imagen de la app
# Favicon dinámico con la imagen de la app y script de transparencia
favicon_tag = soup.find("link", rel=lambda x: x and 'icon' in x.lower())
if favicon_tag and app.get('icon'):
# Inyecta la URL base por defecto
favicon_tag['href'] = app['icon']
# Crea e inyecta el script que procesa el color blanco a transparente
transparency_script = f"""
document.addEventListener("DOMContentLoaded", function () {{
const favicon = document.querySelector('link[rel="icon"]');
if (favicon) {{
const img = new Image();
img.crossOrigin = "Anonymous";
/* MODIFICACIÓN 2: Asegurar que el script cargue el archivo .png */
img.src = "{app.get('icon', '').replace('.webp', '.png')}";
img.onload = function () {{
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {{
const r = data[i], g = data[i + 1], b = data[i + 2];
if (r > 240 && g > 240 && b > 240) data[i + 3] = 0;
}}
ctx.putImageData(imageData, 0, 0);
favicon.href = canvas.toDataURL('image/png');
}};
}}
}});
"""
# Añadir el script al final del body en el HTML generado
script_tag = soup.new_tag("script")
script_tag.string = transparency_script
soup.body.append(script_tag)
# Open Graph (Redes Sociales)
if soup.find(id="og-title"): soup.find(id="og-title")['content'] = title
if soup.find(id="og-description"): soup.find(id="og-description")['content'] = desc
if soup.find(id="og-image"): soup.find(id="og-image")['content'] = icon
# Twitter Cards
if soup.find(id="twitter-title"): soup.find(id="twitter-title")['content'] = title
if soup.find(id="twitter-description"): soup.find(id="twitter-description")['content'] = desc
if soup.find(id="twitter-image"): soup.find(id="twitter-image")['content'] = icon
# Schema JSON-LD (Estructura de Datos SEO)
schema_tag = soup.find(id="schema-json")
if schema_tag:
# Construimos la URL estática usando el slug como subdominio
url_app = f"https://{slug}.appinitdev.com"
schema_data = {
"@context": "https://schema.org",
"@type": "MobileApplication",
"name": app_title,
"description": app.get('description', app.get('summary', '')),
"url": url_app,
"image": icon,
"operatingSystem": "ANDROID",
"applicationCategory": app.get('genre', 'Application'),
"softwareVersion": app.get('version', ''),
"datePublished": app.get('released', ''),
"author": {
"@type": "Organization",
"name": app.get('developer', 'AppInitDev')
},
"offers": {
"@type": "Offer",
"price": "0" if app.get('free') else str(app.get('price', '0')),
"priceCurrency": app.get('currency', 'USD')
}
}
# Agregamos la calificación solo si existen datos
if app.get('score') and app.get('ratings'):
schema_data["aggregateRating"] = {
"@type": "AggregateRating",
"ratingValue": str(app.get('score')),
"ratingCount": str(app.get('ratings')),
"bestRating": "5",
"worstRating": "1"
}
# Inyectamos el JSON en la etiqueta script
schema_tag.string = json.dumps(schema_data, indent=2, ensure_ascii=False)
# --- INYECCIÓN EN EL DOM (INTERFAZ VISUAL) ---
if soup.find(id="dom-title"): soup.find(id="dom-title").string = app_title
if soup.find(id="dom-developer"): soup.find(id="dom-developer").string = app.get('developer', '')
banner_bg_element = soup.find(class_="header-banner-bg")
if banner_bg_element:
banner_bg_element['style'] = f"background-image: url('{header_bg_url}'); opacity: 0.25; background-size: contain; background-position: center; background-repeat: no-repeat; -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,1) 50%, rgba(0,0,0,0) 85%); mask-image: linear-gradient(to right, rgba(0,0,0,1) 50%, rgba(0,0,0,0) 85%);"
# Email del Desarrollador
if soup.find(id="dom-developer-email"):
email_tag = soup.find(id="dom-developer-email")
if app.get('developerEmail'):
email_tag['href'] = f"mailto:{app['developerEmail']}"
email_tag['style'] = "display: inline-block;"
else:
email_tag['style'] = "display: none;"
if soup.find(id="dom-icon"):
soup.find(id="dom-icon")['src'] = icon
soup.find(id="dom-icon")['alt'] = f"Icono de {app_title}"
if soup.find(id="dom-summary"): soup.find(id="dom-summary").string = app.get('summary', '')
# Precio y Botón de PlayStore
is_free = app.get('free', True)
if soup.find(id="dom-price"):
soup.find(id="dom-price").string = "Gratis" if is_free else "Premium"
if soup.find(id="dom-button-text"):
soup.find(id="dom-button-text").string = "Instalar ahora" if is_free else "Comprar ahora"
if soup.find(id="dom-playstore-link"):
soup.find(id="dom-playstore-link")['href'] = f"https://play.google.com/store/apps/details?id={app_id}"
# Puntuación / Rating
if soup.find(id="dom-rating"):
rating_tag = soup.find(id="dom-rating")
if app.get('score'):
rating_tag.string = f"★ {float(app['score']):.1f}"
rating_tag['style'] = "display: flex;"
else:
rating_tag['style'] = "display: none;"
if soup.find(id="dom-genre-badge"):
soup.find(id="dom-genre-badge").string = app.get('genre', 'App')
# Especificaciones GRID (Descargas, Versión, Lanzamiento, Clasificación)
# Especificaciones GRID (Descargas, Versión, Lanzamiento, Clasificación)
if soup.find(id="dom-installs"):
raw_installs = str(app.get('installs', '0'))
# 1. Extraer solo los números del texto (ej. de "10,000+" o "10000" a "10000")
only_numbers = ''.join(filter(str.isdigit, raw_installs))
if only_numbers:
# 2. Convertir a entero y sumarle los 1000
total_installs = int(only_numbers) + 1000
# 3. Formatear de vuelta con comas y añadir el "+" (ej. "11,000+")
soup.find(id="dom-installs").string = f"{total_installs:,}+"
else:
# Valor por defecto si no hay números válidos
soup.find(id="dom-installs").string = "1,000+"
if soup.find(id="dom-version"):
soup.find(id="dom-version").string = app.get('version', 'Actualizada')
if soup.find(id="dom-released-label") and soup.find(id="dom-released"):
if app.get('lastUpdatedOn'):
soup.find(id="dom-released-label").string = "Actualización"
soup.find(id="dom-released").string = app.get('lastUpdatedOn')
else:
soup.find(id="dom-released-label").string = "Lanzamiento"
soup.find(id="dom-released").string = app.get('released', 'N/D')
if soup.find(id="dom-android-version"):
soup.find(id="dom-android-version").string = app.get('contentRating', 'Para todos')
# Descripción HTML
if soup.find(id="dom-description"):
desc_container = soup.find(id="dom-description")
desc_container.clear()
desc_container.append(BeautifulSoup(app.get('descriptionHTML', ''), 'html.parser'))
# Política de Privacidad
if soup.find(id="privacy-container") and soup.find(id="dom-privacy-link"):
if app.get('privacyPolicy'):
soup.find(id="dom-privacy-link")['href'] = app['privacyPolicy']
soup.find(id="privacy-container")['style'] = "display: block; border-top: 1px solid #e2e8f0; padding-top: 20px; margin-top: 30px;"
else:
soup.find(id="privacy-container")['style'] = "display: none;"
# Control del estado de carga (Mostramos el contenido)
if soup.find(id="loading-state"): soup.find(id="loading-state")['style'] = "display: none;"
if soup.find(id="app-content"): soup.find(id="app-content")['style'] = "display: block;"
# Inyectar Capturas de pantalla y preparar array de imágenes
screenshots_container = soup.find(id="dom-screenshots")
if screenshots_container:
screenshots_container.clear()
if app.get('screenshots'):
for idx, imgUrl in enumerate(app['screenshots']):
img_tag = soup.new_tag("img", src=imgUrl, alt=f"Captura de {app_title}", loading="lazy")
img_tag['class'] = "screenshot-img"
img_tag['onclick'] = f"abrirModal({idx})"
screenshots_container.append(img_tag)
# Inyectar la lista de imágenes para el script del modal
script_tag = soup.new_tag("script")
script_tag.string = f"window.modalImagenes = {json.dumps(app['screenshots'])};"
soup.body.append(script_tag)
else:
if screenshots_container.parent:
screenshots_container.parent['style'] = "display: none;"
# ==========================================
# --- CORRECCIÓN AUTOMÁTICA DE RUTAS ---
# ==========================================
# 1. Arreglar etiquetas (CSS, favicon estáticos, etc.)
for link_tag in soup.find_all('link', href=True):
href = link_tag['href']
if not href.startswith(('http', 'data:', '/')):
link_tag['href'] = f"../../{href}"
# 2. Arreglar etiquetas