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