No hay ' . ($tipo === 'video' ? 'vídeo' : 'texto') . ' disponible para este día.
'; + } + $c = apply_filters('the_content', $post->post_content); + return '
';
+ $html .= '';
+ if ($title) {
+ $html .= '' + . '' + . esc_html($title) . '
'; + } + return $html; +}); + +// ── Reescribir links internos al idioma activo (Polylang) ───────────────── +add_filter('the_content', function($content) { + if (!function_exists('pll_current_language') || !function_exists('pll_get_post')) return $content; + $lang = pll_current_language(); + if (!$lang || $lang === 'es') return $content; + // Los archivos de cartas pueden contener cientos de enlaces. Resolver cada uno + // con url_to_postid() en un listado agota memoria; la reescritura solo aporta + // valor al mostrar el contenido completo de una entrada o página. + if (!is_singular()) return $content; + + return preg_replace_callback( + '/]*\s)?href=["\']([^"\']+)["\']([^>]*)>/i', + function($m) use ($lang) { + $href = $m[2]; + $home = home_url(); + if (strpos($href, $home) === false) return $m[0]; + + $post_id = url_to_postid($href); + if (!$post_id) return $m[0]; + + $translated_id = pll_get_post($post_id, $lang); + if (!$translated_id || $translated_id === $post_id) return $m[0]; + + $new_url = get_permalink($translated_id); + if (!$new_url) return $m[0]; + + return str_replace($href, $new_url, $m[0]); + }, + $content + ); +}, 20); + +// ── Ordenar categoría Evangelios y Comentarios por título ASC ───────────── +add_action('pre_get_posts', function($query) { + if ($query->is_main_query() && $query->is_category('evangelios-y-comentarios')) { + $query->set('orderby', 'title'); + $query->set('order', 'ASC'); + } +}); + + +// ── Acordeón de versículos en posts de Evangelios y Comentarios ─────────── +add_action('wp_footer', function() { + if (!is_single()) return; + global $post; + if (!has_category('evangelios-y-comentarios', $post)) return; + ?> + + + ]*href=["\']([^"\']+)["\']/', $post->post_content, $m); + $slugs = []; + foreach ($m[1] as $url) { + $slug = basename(rtrim(parse_url($url, PHP_URL_PATH), '/')); + if ($slug) { + $slugs[] = $slug; + } + } + $slugs = array_values(array_unique(array_filter($slugs))); + if (empty($slugs)) return; + + // Una sola query: slug → author_id + $ph = implode(',', array_fill(0, count($slugs), '%s')); + $rows = $wpdb->get_results( + $wpdb->prepare( + "SELECT post_name, post_author FROM {$wpdb->posts} + WHERE post_name IN ($ph) AND post_status IN ('publish','draft') AND post_type='post'", + $slugs + ) + ); + + // Construir mapa slug → avatar_url (reutilizando cache de autor) + $author_cache = []; + $avatar_map = []; + foreach ($rows as $row) { + $aid = (int) $row->post_author; + if (!isset($author_cache[$aid])) { + $user = get_userdata($aid); + $author_cache[$aid] = ['src' => get_avatar_url($aid, ['size' => 40, 'default' => 'identicon']), 'name' => $user ? $user->display_name : '']; + } + if ($author_cache[$aid]) { + $avatar_map[$row->post_name] = $author_cache[$aid]; + } + } + if (empty($avatar_map)) return; + ?> + + + 0 ? "HAVING cnt >= $min_count" : ''; + $order = $min_count > 0 ? 'cnt DESC' : 'u.display_name ASC'; + return $wpdb->get_results(" + SELECT u.ID, u.display_name, COUNT(*) as cnt + FROM {$wpdb->posts} p + JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID + JOIN {$wpdb->users} u ON u.ID = p.post_author + WHERE p.post_type = 'post' + AND p.post_status = 'publish' + AND tr.term_taxonomy_id = $ttid + AND u.ID NOT IN ($excl) + GROUP BY p.post_author + $having + ORDER BY $order + "); +} + +function fea_autores_html($rows, $show_count = false, $extra_class = "") { + $extra = $extra_class ? ' ' . $extra_class : ''; + $out = 'No hay datos disponibles.
'; + $n = count($rows); + $out = 'No hay datos disponibles.
'; + $n = count($rows); + $out = 'No hay contenido en esta sección todavía.
'; + + $items = []; + foreach ($posts as $p) { + $url = get_permalink($p->ID); + $thumb = get_the_post_thumbnail_url($p->ID, 'medium'); + if (!$thumb && preg_match('~youtube\.com/watch\?v=([a-zA-Z0-9_-]+)~', $p->post_content, $m)) { + $thumb = 'https://img.youtube.com/vi/' . $m[1] . '/mqdefault.jpg'; + } + if (!$thumb && preg_match('~| '; + $html .= ' |
| '; + $html .= ' |
No hay comentarios para este evangelio.
'; + + // Group by cita + $groups = []; + foreach ($rows as $r) $groups[$r->cita][] = $r; + + // Sort groups numerically by chapter, then verse + uksort($groups, function($a, $b) { + [$ca, $va] = fea_cita_sort_key($a); + [$cb, $vb] = fea_cita_sort_key($b); + return $ca !== $cb ? $ca - $cb : $va - $vb; + }); + + // Avatar URL by email (Gravatar, with fallback) + $avatar_cache = []; + $avatar_url = function(string $email, int $id) use (&$avatar_cache): string { + if (!isset($avatar_cache[$id])) { + $url = get_avatar_url($id, ['size' => 40, 'default' => 'mystery']); + $avatar_cache[$id] = $url ?: 'https://www.gravatar.com/avatar/' . md5(strtolower(trim($email))) . '?s=40&d=mystery'; + } + return $avatar_cache[$id]; + }; + + $html = ']*)>\s*(]*>)\s*((JUAN|LUCAS|MARCOS|MATEO)\s+(\d+),\s*([\d\-–\s]+))\s*()\s*
~i', + function($m) use ($bookmap) { + $abbr = $bookmap[strtoupper($m[4])] ?? ''; + if (!$abbr) return $m[0]; + $anchor = strtolower($abbr . '-' . $m[5] . '-' . preg_replace('/[\s–\-]+/', '-', trim($m[6]))); + $anchor = rtrim($anchor, '-'); + return '' . $m[2] . $m[3] . $m[7] . '
'; + }, + $content + ); +}, 9); // priority 9 so it runs before the_content shortcode processing + +add_filter('the_content', function($content) { + if (!is_single()) return $content; + global $post; + if (!in_category('comentarios-al-evangelio', $post)) return $content; + + $carta_id = get_post_meta($post->ID, '_carta_id', true); + $cita = get_post_meta($post->ID, '_cita_evangelio', true); + + if (!$carta_id && !$cita) return $content; + + $html = ''; // .fea-com-footer + return $content . $html; +}); + +add_action('wp_head', function() { + if (!is_single()) return; + global $post; + if (!$post || !in_category('comentarios-al-evangelio', $post)) return; + ?> + + [en, fr, it, pt]. Claves normalizadas con trim. */ +function fea_menu_map(): array { + static $m = null; + if ($m !== null) return $m; + $m = [ + // ── Menú principal (header) ── + 'PORTADA' => ['Home', 'Accueil', 'Home', 'Início'], + 'Quiénes somos' => ['About us', 'À propos', 'Chi siamo', 'Quem somos'], + 'Colaboradores' => ['Contributors', 'Collaborateurs', 'Collaboratori', 'Colaboradores'], + 'Este portal' => ['This portal', 'Ce portail', 'Questo portale', 'Este portal'], + 'Para poner al día la Fe' => ['Bringing faith up to date', 'Mettre la foi à jour', 'Aggiornare la fede', 'Atualizar a fé'], + 'Cartas' => ['Letters', 'Lettres', 'Lettere', 'Cartas'], + 'Esta semana' => ['This week', 'Cette semaine', 'Questa settimana', 'Esta semana'], + 'Semana pasada' => ['Last week', 'Semaine dernière', 'Settimana scorsa', 'Semana passada'], + 'Otras semanas' => ['Other weeks', 'Autres semaines', 'Altre settimane', 'Outras semanas'], + 'Acceso a webs anteriores:' => ['Previous websites:', 'Anciens sites :', 'Siti precedenti:', 'Sites anteriores:'], + 'Web V1 — FrontPage (2006-2012)' => ['Site V1 — FrontPage (2006-2012)', 'Site V1 — FrontPage (2006-2012)', 'Sito V1 — FrontPage (2006-2012)', 'Site V1 — FrontPage (2006-2012)'], + 'Web V2 — Joomla (2012-2026)' => ['Site V2 — Joomla (2012-2026)', 'Site V2 — Joomla (2012-2026)', 'Sito V2 — Joomla (2012-2026)', 'Site V2 — Joomla (2012-2026)'], + 'Nueva política de datos' => ['New data policy', 'Nouvelle politique de données', 'Nuova politica sui dati', 'Nova política de dados'], + 'Contactar' => ['Contact', 'Contact', 'Contatti', 'Contactar'], + 'Para contactar con nosotros' => ['Contact us', 'Nous contacter', 'Per contattarci', 'Para contactar-nos'], + 'Para recibir la carta de novedades' => ['Subscribe to the newsletter', 'Recevoir la newsletter', 'Ricevere la newsletter', 'Receber a newsletter'], + 'Para inscribirse en la Escuela' => ['Enrol in the School', 'S\'inscrire à l\'École', 'Iscriversi alla Scuola', 'Inscrever-se na Escola'], + '🎓 Escuela' => ['🎓 School', '🎓 École', '🎓 Scuola', '🎓 Escola'], + '📚 Librería 🛒' => ['📚 Bookshop 🛒', '📚 Librairie 🛒', '📚 Libreria 🛒', '📚 Livraria 🛒'], + 'Buscar' => ['Search', 'Rechercher', 'Cerca', 'Pesquisar'], + // ── Menús del pie ── + 'Cartas que nos llegan' => ['Letters we receive', 'Lettres que nous recevons', 'Lettere che riceviamo', 'Cartas que recebemos'], + 'Tablón de anuncios' => ['Notice board', 'Tableau d\'annonces', 'Bacheca', 'Mural de avisos'], + 'Asociación FeAdulta' => ['FeAdulta Association', 'Association FeAdulta', 'Associazione FeAdulta', 'Associação FeAdulta'], + 'La suma de todos' => ['The sum of all', 'La somme de tous', 'La somma di tutti', 'A soma de todos'], + 'Comunidades cristianas' => ['Christian communities', 'Communautés chrétiennes', 'Comunità cristiane', 'Comunidades cristãs'], + 'El Evangelio de cada día' => ['The daily Gospel', 'L\'Évangile de chaque jour', 'Il Vangelo di ogni giorno', 'O Evangelho de cada dia'], + 'Índice cronológico' => ['Chronological index', 'Index chronologique', 'Indice cronologico', 'Índice cronológico'], + 'Índice cronológico' => ['Chronological index', 'Index chronologique', 'Indice cronologico', 'Índice cronológico'], + 'Evangelios y comentarios' => ['Gospels and commentaries', 'Évangiles et commentaires', 'Vangeli e commenti', 'Evangelhos e comentários'], + 'Oraciones eucarísticas' => ['Eucharistic prayers', 'Prières eucharistiques', 'Preghiere eucaristiche', 'Orações eucarísticas'], + 'A modo de salmos' => ['In the manner of psalms', 'À la manière de psaumes', 'A mo\' di salmi', 'À maneira de salmos'], + 'Preces y oraciones varias' => ['Prayers and various orations', 'Prières et oraisons diverses', 'Preci e orazioni varie', 'Preces e orações várias'], + 'Primeras lecturas' => ['First readings', 'Premières lectures', 'Prime letture', 'Primeiras leituras'], + 'Autores' => ['Authors', 'Auteurs', 'Autori', 'Autores'], + 'Temas' => ['Topics', 'Thèmes', 'Temi', 'Temas'], + 'Multimedia' => ['Multimedia', 'Multimédia', 'Multimedia', 'Multimédia'], + 'Índice de pensamientos' => ['Index of reflections', 'Index des pensées', 'Indice dei pensieri', 'Índice de pensamentos'], + 'Cantoral' => ['Hymnal', 'Recueil de chants', 'Canzoniere', 'Cancioneiro'], + 'Películas' => ['Films', 'Films', 'Film', 'Filmes'], + 'Reseñas de libros' => ['Book reviews', 'Critiques de livres', 'Recensioni di libri', 'Resenhas de livros'], + 'In memoriam' => ['In memoriam', 'In memoriam', 'In memoriam', 'In memoriam'], + ]; + return $m; +} + +/** Índice de columna por idioma. */ +function fea_menu_lang_col(string $lang): int { + return ['en' => 0, 'fr' => 1, 'it' => 2, 'pt' => 3][$lang] ?? -1; +} + +function fea_menu_tr(string $label, int $col): ?string { + $map = fea_menu_map(); + $key = trim($label); + if (isset($map[$key][$col]) && $map[$key][$col] !== '') return $map[$key][$col]; + return null; +} + +/** + * Remapea una URL ES al destino traducido si existe (post/página/categoría). + * Devuelve la URL original si no hay traducción o no se resuelve. + */ +function fea_menu_localize_url(string $url, string $lang): string { + if ($url === '' || preg_match('~^(mailto:|tel:|javascript:|#)~i', $url)) return $url; + // Solo enlaces internos de ESTE sitio (no Librería, no webs anteriores externas). + $host = parse_url($url, PHP_URL_HOST); + if ($host) { + $site_host = parse_url(home_url('/'), PHP_URL_HOST); + if ($host !== $site_host) return $url; // externo + } + $path = trim((string) parse_url($url, PHP_URL_PATH), '/'); + // quitar subcarpeta local (fea) y prefijo de idioma (es/en/…), con o sin barra final + $path = preg_replace('#^fea(/|$)#', '', $path); + $path = preg_replace('#^(es|en|fr|it|pt)(/|$)#', '', $path); + if ($path === '') { + // raíz del sitio → portada del idioma + return function_exists('pll_home_url') ? pll_home_url($lang) : $url; + } + + // categoría: category/Galería no disponible.
'; + } + + $per_page = max(12, min(144, (int) $atts['per_page'])); + $total = count($files); + $pages = max(1, (int) ceil($total / $per_page)); + $param = fea_gallery_page_param($dir); + $page = isset($_GET[$param]) ? max(1, (int) $_GET[$param]) : 1; + $page = min($page, $pages); + $offset = ($page - 1) * $per_page; + $visible = array_slice($files, $offset, $per_page); + + $html = 'Recopilatorio no disponible.
'; + + $per_page = max(20, min(500, (int) $atts['per_page'])); + $order = strtolower($atts['order']) === 'asc' ? 'ASC' : 'DESC'; + $paged = isset($_GET['recop']) ? max(1, (int) $_GET['recop']) : 1; + + $q = new WP_Query([ + 'post_type' => 'post', + 'post_status' => 'publish', + 'cat' => $term_id, + 'posts_per_page' => $per_page, + 'paged' => $paged, + 'orderby' => 'date', + 'order' => $order, + 'ignore_sticky_posts' => true, + 'no_found_rows' => false, + ]); + + if (!$q->have_posts()) { + wp_reset_postdata(); + return 'Todavía no hay entradas en esta sección.
'; + } + + $by_year = ($atts['group'] === 'year'); + $html = 'Todavía no hay multimedia disponible.
'; + } + + $html = '' . esc_html($data['progress_note']) . '
'; + } + $html .= '{p}
\n" for p in paras) + + +def fetch(date_s, lang_code): + fp = os.path.join(CACHE, f"{date_s}_{lang_code}.json") + if os.path.exists(fp): + try: + return json.load(open(fp)) + except Exception: + pass + url = f"https://publication.evangelizo.ws/{lang_code}/days/{date_s}" + for a in range(3): + try: + req = urllib.request.Request(url, headers={"User-Agent": "fea-lect/1.0"}) + with urllib.request.urlopen(req, timeout=30) as r: + data = json.load(r) + out = [] + for rd in data.get("data", {}).get("readings", []) or []: + out.append({ + "code": rd.get("reading_code", ""), + "ref": (rd.get("reference_displayed") or "").strip().rstrip("."), + "book": (rd.get("book") or {}).get("full_title", ""), + "text": clean(rd.get("text", "")), + }) + json.dump(out, open(fp, "w"), ensure_ascii=False) + time.sleep(0.15) + return out + except Exception: + if a == 2: + json.dump([], open(fp, "w")) + return [] + time.sleep(1.0) + + +def key_from(book_full, ref): + m = re.match(r"(\d{1,3})\s*,\s*(\d{1,3})", ref) + if not m: + return None + return f"{norm_book(book_full)}|{int(m.group(1))}|{int(m.group(2))}" + + +def main(): + d0 = date.fromisoformat(sys.argv[1]) + d1 = date.fromisoformat(sys.argv[2]) + days = (d1 - d0).days + 1 + + # Las fiestas trasladadas caen en fechas distintas por país/idioma → NO se puede + # casar dentro del mismo día. Indexamos por reading_code (estable entre idiomas) + # acumulando el texto de cada idioma desde CUALQUIER día donde aparezca. + code_text = {wl: {} for wl in LANGS.values()} # lang -> {code: text} + code_book = {} # code -> norm_book (del SP) + cur, n = d0, 0 + while cur <= d1: + ds = cur.isoformat() + for lc, wl in LANGS.items(): + for rd in fetch(ds, lc): + code = rd["code"] + if not code: + continue + if rd["text"] and code not in code_text[wl]: + code_text[wl][code] = rd["text"] + if wl == "es" and code not in code_book: + nb = norm_book(rd["book"]) + m = re.search(r"(\d{1,3})\s*,\s*(\d{1,3})", code) + if nb and m: + code_book[code] = f"{nb}|{int(m.group(1))}|{int(m.group(2))}" + n += 1 + if n % 60 == 0: + print(f" {n}/{days} días codes_es={len(code_text['es'])}", flush=True) + cur += timedelta(days=1) + + # combinar: para cada code con clave-ES y texto en los 4 idiomas + index = {} + for code, key in code_book.items(): + if key in index: + continue + entry = {} + ok = True + for wl in ("es", "en", "fr", "it", "pt"): + t = code_text[wl].get(code) + if not t: + ok = (wl == "es") and ok # es siempre presente; faltar otro descarta + if wl != "es": + ok = False + break + entry[wl] = t + if ok and all(l in entry for l in ("en", "fr", "it", "pt")): + index[key] = entry + json.dump(index, open(INDEX, "w"), ensure_ascii=False) + print(f"FIN. {n} días. codes_es={len(code_book)} → índice {len(index)} referencias en {INDEX}") + + +if __name__ == "__main__": + main() diff --git a/scripts/carta-semana-plugin.php b/scripts/carta-semana-plugin.php new file mode 100755 index 0000000..98a4dba --- /dev/null +++ b/scripts/carta-semana-plugin.php @@ -0,0 +1,75 @@ +term_id)) return; + + $source_cat_id = (int) $cat->term_id; + if (function_exists('pll_get_term')) { + $spanish_cat_id = (int) pll_get_term($source_cat_id, 'es'); + if ($spanish_cat_id) $source_cat_id = $spanish_cat_id; + } + if (!in_array($source_cat_id, [6, 22], true)) return; + + global $wpdb; + $source_post_id = (int) $wpdb->get_var($wpdb->prepare( + "SELECT p.ID + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID + INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id + WHERE tt.taxonomy = 'category' AND tt.term_id = %d + AND p.post_type = 'post' AND p.post_status = 'publish' + ORDER BY p.post_date DESC, p.ID DESC + LIMIT 1", + $source_cat_id + )); + if (!$source_post_id) return; + + $post_id = $source_post_id; + if (function_exists('pll_current_language') && function_exists('pll_get_post')) { + $lang = pll_current_language(); + $translated = $lang ? (int) pll_get_post($source_post_id, $lang) : 0; + if ($translated) $post_id = $translated; + } + + $url = get_permalink($post_id); + if (!$url) return; + wp_safe_redirect($url, 302); + exit; +}, 9); + +// Mostrar 50 artículos por página en los archivos de cartas +add_action("pre_get_posts", function($query) { + if (!$query->is_main_query() || is_admin()) return; + if ($query->is_category([ + "cartasemana", "carta-semana-pasada", "cartas-de-otras-semanas", + "letter-of-the-week", "lettre-de-la-semaine", "lettera-della-settimana", "carta-da-semana", + "carta-semana-pasada-en", "carta-semana-pasada-fr", + "carta-semana-pasada-it", "carta-semana-pasada-pt", + "letters-from-other-weeks", "lettres-des-autres-semaines", + "lettere-delle-altre-settimane", "cartas-de-outras-semanas", + ])) { + $query->set("posts_per_page", 50); + } +}); diff --git a/scripts/create_buscar_page.php b/scripts/create_buscar_page.php new file mode 100644 index 0000000..44eae16 --- /dev/null +++ b/scripts/create_buscar_page.php @@ -0,0 +1,109 @@ + 'Búsqueda avanzada', + 'en' => 'Advanced Search', + 'fr' => 'Recherche avancée', + 'it' => 'Ricerca avanzata', + 'pt' => 'Pesquisa avançada', +]; +// Contenido mínimo (sin bloques Gutenberg). El formulario se inyecta vía the_content. +$content = 'Utiliza el formulario de búsqueda avanzada para encontrar reflexiones, artículos y más.
'; + +$has_pll = function_exists('pll_set_post_language') && function_exists('pll_save_post_translations'); +$pll_langs = (function_exists('pll_languages_list')) + ? pll_languages_list(['fields' => 'slug']) + : ['es']; + +/** Crea (o devuelve si existe) una página por slug, con idioma Polylang. */ +function fea_buscar_ensure_page(string $slug, string $title, string $content, string $lang, bool $apply, bool $has_pll) { + $existing = get_page_by_path($slug, OBJECT, 'page'); + if ($existing) { + // Asegurar que está publicada + if ($apply && $existing->post_status !== 'publish') { + wp_update_post(['ID' => $existing->ID, 'post_status' => 'publish']); + echo " · ({$lang}) página '{$slug}' existía en estado {$existing->post_status} → publicada (ID {$existing->ID})\n"; + } else { + echo " · ({$lang}) página '{$slug}' ya existe y publicada (ID {$existing->ID})\n"; + } + // Asegurar idioma asignado + if ($apply && $has_pll && function_exists('pll_get_post_language')) { + $cur = pll_get_post_language($existing->ID); + if ($cur !== $lang) { pll_set_post_language($existing->ID, $lang); echo " idioma → {$lang}\n"; } + } + return (int) $existing->ID; + } + + echo ($apply ? " · ({$lang}) creando" : " · ({$lang}) [dry] crearía") . " página '{$slug}'\n"; + if (!$apply) return 0; + + $id = wp_insert_post([ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_name' => $slug, + 'post_title' => $title, + 'post_content' => $content, + 'post_author' => 1, + ], true); + if (is_wp_error($id)) { echo " ERROR: " . $id->get_error_message() . "\n"; return 0; } + if ($has_pll) pll_set_post_language($id, $lang); + echo " creada (ID {$id})\n"; + return (int) $id; +} + +echo ($apply ? "APLICANDO" : "DRY-RUN") . " — crear/reparar /buscar\n"; + +// 1) Página ES (slug 'buscar') +$translations = []; +$es_id = fea_buscar_ensure_page('buscar', $titles['es'], $content, 'es', $apply, $has_pll); +if ($es_id) $translations['es'] = $es_id; + +// 2) Traducciones (en/fr/it/pt) sólo si Polylang activo y el idioma existe +if ($has_pll) { + foreach (['en', 'fr', 'it', 'pt'] as $lang) { + if (!in_array($lang, $pll_langs, true)) { echo " · ({$lang}) idioma no configurado en Polylang, omitido\n"; continue; } + + // Si ya hay traducción vinculada a la ES, reusarla + $linked = ($es_id && function_exists('pll_get_post')) ? (int) pll_get_post($es_id, $lang) : 0; + if ($linked) { + $p = get_post($linked); + if ($p && $p->post_status !== 'publish' && $apply) { + wp_update_post(['ID' => $linked, 'post_status' => 'publish']); + echo " · ({$lang}) traducción vinculada (ID {$linked}) estaba {$p->post_status} → publicada\n"; + } else { + echo " · ({$lang}) traducción ya vinculada (ID {$linked})\n"; + } + $translations[$lang] = $linked; + continue; + } + + // Crear/reparar por slug + $tl_id = fea_buscar_ensure_page('buscar-' . $lang, $titles[$lang], $content, $lang, $apply, $has_pll); + if ($tl_id) $translations[$lang] = $tl_id; + } + + // 3) Revincular todas las traducciones + if ($apply && count($translations) > 1) { + pll_save_post_translations($translations); + echo " · traducciones revinculadas: " . implode(', ', array_map( + fn($l, $id) => "{$l}={$id}", array_keys($translations), $translations)) . "\n"; + } +} + +if (function_exists('wp_cache_flush')) wp_cache_flush(); +echo ($apply ? "APLICADO" : "DRY-RUN") . "\n"; diff --git a/scripts/create_lecturas.php b/scripts/create_lecturas.php new file mode 100644 index 0000000..96b8d64 --- /dev/null +++ b/scripts/create_lecturas.php @@ -0,0 +1,43 @@ +'MATTHEW','fr'=>'MATTHIEU','it'=>'MATTEO','pt'=>'MATEUS']; +$VER = ['en'=>'Douay-Rheims Bible','fr'=>'Bible du Semeur 2015','it'=>'Nuova Riveduta 2006','pt'=>'Bíblia CNBB 2002']; +$REST = getenv('LECTURA_TITULO_ES') ?: 'MATEO 10, 26-33'; // título es +$tail = preg_replace('~^MATEO~','',$REST); // " 10, 26-33" +$es_cats = wp_get_post_categories($ES); +$target_ids = array_values(array_filter(array_map('intval', explode(',', getenv('TARGET_IDS') ?: '')))); +$target_map = []; +foreach (['en','fr','it','pt'] as $idx => $lang0) { + if (!empty($target_ids[$idx])) $target_map[$lang0] = (int)$target_ids[$idx]; +} + +$grp = pll_get_post_translations($ES); if(!$grp) $grp=['es'=>$ES]; +foreach (['en','fr','it','pt'] as $lang) { + $exist = (int)pll_get_post($ES,$lang); + if ($exist && get_post($exist)) { echo "$lang ya existe #$exist — saltado\n"; $grp[$lang]=$exist; continue; } + $title = $BOOK[$lang].$tail; + $postarr = [ + 'post_title'=>$title,'post_content'=>$tr_html[$lang],'post_status'=>'publish', + 'post_type'=>'post','post_author'=>(int)$src->post_author,'post_date'=>$src->post_date, + ]; + $target = (int)($target_map[$lang] ?? 0); + if ($target && get_post($target)) { + $postarr['ID'] = $target; + } elseif ($target) { + $postarr['import_id'] = $target; + } + $id = wp_insert_post($postarr, true); + if (is_wp_error($id)) { echo "$lang ERROR ".$id->get_error_message()."\n"; continue; } + if ($target && (int)$id !== $target) { echo "$lang ERROR id esperado $target creado $id\n"; continue; } + pll_set_post_language($id,$lang); + $mapped=[]; foreach($es_cats as $c){ $tc=(int)pll_get_term($c,$lang); $mapped[]=$tc?:$c; } + wp_set_post_categories($id, array_values(array_unique($mapped))); + $grp[$lang]=$id; pll_save_post_translations($grp); + update_post_meta($id,'lectura_fuente',$VER[$lang]); + update_post_meta($id,'traduccion_origen',$ES); + echo "$lang creado #$id «$title» [".$VER[$lang]."]\n"; +} +echo "Grupo final: ".json_encode(pll_get_post_translations($ES))."\n"; diff --git a/scripts/cutover_feadulta_com.sh b/scripts/cutover_feadulta_com.sh new file mode 100755 index 0000000..a9a10c5 --- /dev/null +++ b/scripts/cutover_feadulta_com.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# ============================================================================= +# Script de cutover DNS: feadulta.org → feadulta.com +# Ejecutar DESPUÉS de apuntar el DNS de feadulta.com al servidor de producción +# ============================================================================= +# +# Este script reemplaza todas las URLs internas de feadulta.org por feadulta.com +# en la base de datos WordPress de producción. +# +# Servidor: 185.42.105.48 +# DB: 278025353wordpress20260112013937 / myfeadultaa5 / KjyGU29h +# ============================================================================= + +set -e + +DB_HOST="127.0.0.1" +DB_NAME="278025353wordpress20260112013937" +DB_USER="myfeadultaa5" +DB_PASS="KjyGU29h" +OLD_URL="http://feadulta.org" +NEW_URL="https://feadulta.com" + +MYSQL="mysql -h $DB_HOST -u $DB_USER -p$DB_PASS $DB_NAME" + +echo "=== Cutover feadulta.org → feadulta.com ===" +echo "OLD: $OLD_URL" +echo "NEW: $NEW_URL" +echo "" +echo "Ejecutando en 5 segundos... (Ctrl+C para cancelar)" +sleep 5 + +echo "[1/6] Actualizando siteurl y home..." +$MYSQL -e " + UPDATE wp_options SET option_value = '$NEW_URL' WHERE option_name = 'siteurl'; + UPDATE wp_options SET option_value = '$NEW_URL' WHERE option_name = 'home'; +" + +echo "[2/6] Reemplazando en post_content..." +$MYSQL -e "UPDATE wp_posts SET post_content = REPLACE(post_content, '$OLD_URL', '$NEW_URL');" + +echo "[3/6] Reemplazando en guid..." +$MYSQL -e "UPDATE wp_posts SET guid = REPLACE(guid, '$OLD_URL', '$NEW_URL');" + +echo "[4/6] Reemplazando en postmeta..." +$MYSQL -e "UPDATE wp_postmeta SET meta_value = REPLACE(meta_value, '$OLD_URL', '$NEW_URL');" + +echo "[5/6] Reemplazando en wp_options (no serializados)..." +$MYSQL -e " + UPDATE wp_options SET option_value = REPLACE(option_value, '$OLD_URL', '$NEW_URL') + WHERE option_name NOT IN ('wpseo', 'fgj2wp_save_posts', 'bsr_data') + AND option_value LIKE '%feadulta.org%'; +" + +echo "[6/6] Actualizando wp-config.php..." +ssh feadultada@185.42.105.48 " + sed -i \"s|define('WP_SITEURL','http://feadulta.org')|define('WP_SITEURL','https://feadulta.com')|\" /web/wp-config.php + sed -i \"s|define('WP_HOME','http://feadulta.org')|define('WP_HOME','https://feadulta.com')|\" /web/wp-config.php +" + +echo "" +echo "=== Verificación ===" +$MYSQL -e "SELECT option_name, option_value FROM wp_options WHERE option_name IN ('siteurl','home');" +$MYSQL -e "SELECT COUNT(*) as pendientes_feadulta_org FROM wp_posts WHERE post_content LIKE '%feadulta.org%';" + +echo "" +echo "=== Cutover completado ===" +echo "IMPORTANTE: Limpiar caché de WordPress y Cloudflare después de este paso." +echo "IMPORTANTE: Activar plugins: AdSense, Wordfence, TTS." +echo "IMPORTANTE: Verificar redirects de feadulta.com/images/Musica/ (ya no hacen falta si los MP3 están en el mismo servidor)." diff --git a/scripts/demote_old_cartasemana.php b/scripts/demote_old_cartasemana.php new file mode 100755 index 0000000..506656b --- /dev/null +++ b/scripts/demote_old_cartasemana.php @@ -0,0 +1,69 @@ + php demote_old_cartasemana.php (dry-run) + * APPLY=1 CARTA={p}
\n" for p in paras) + result[b][wl] = {"title": short_title(header), "html": htmlc, "header": header} + break + json.dump(result, sys.stdout, ensure_ascii=False, indent=2) + + +if __name__ == "__main__": + main() diff --git a/scripts/export_cat_translations.py b/scripts/export_cat_translations.py new file mode 100644 index 0000000..d57a154 --- /dev/null +++ b/scripts/export_cat_translations.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +export_cat_translations.py + +Exports Polylang category translation data from local DB and generates SQL +for production. Handles: +1. wp_terms for translated categories +2. wp_term_taxonomy (category + language taxonomy rows) +3. wp_term_taxonomy (term_translations groups) +4. wp_term_relationships (post→translated category assignments) +""" +import pymysql, re + +DB = dict(host='172.18.0.2', port=3306, user='wordpress_user', + password='wordpress_pass', database='wordpress_db', charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor) + +# Translated category term_ids on local (ES parent → lang: local_term_id) +TRANS_CATS = { + 6: {'en': 3077, 'fr': 3083, 'it': 3089, 'pt': 3095}, + 21: {'en': 3080, 'fr': 3086, 'it': 3092, 'pt': 3098}, + 1646: {'en': 2982, 'fr': 3032, 'it': 3048, 'pt': 3063}, + 1647: {'en': 2986, 'fr': 3035, 'it': 3051, 'pt': 3066}, + 1648: {'en': 2971, 'fr': 3029, 'it': 3045, 'pt': 3060}, + 1650: {'en': 2964, 'fr': 3023, 'it': 3039, 'pt': 3054}, +} + +# Collect all translated term_ids +all_trans_ids = [] +for mapping in TRANS_CATS.values(): + all_trans_ids.extend(mapping.values()) +all_trans_ids = sorted(set(all_trans_ids)) +all_es_ids = sorted(TRANS_CATS.keys()) + +db = pymysql.connect(**DB) +c = db.cursor() + +sql_lines = [ + "-- Polylang category translations export", + "-- Generated by export_cat_translations.py", + "-- Run on production AFTER verifying no term_id conflicts", + "", + "SET NAMES utf8mb4;", + "SET foreign_key_checks = 0;", + "", +] + +# ─── 1. wp_terms ────────────────────────────────────────────────────────────── +ids_str = ','.join(str(i) for i in all_trans_ids) +c.execute(f"SELECT term_id, name, slug, term_group FROM wp_terms WHERE term_id IN ({ids_str})") +rows = c.fetchall() +sql_lines.append("-- 1. wp_terms (translated category names/slugs)") +for r in rows: + name = r['name'].replace("'", "''") + slug = r['slug'].replace("'", "''") + sql_lines.append( + f"INSERT IGNORE INTO wp_terms (term_id, name, slug, term_group) " + f"VALUES ({r['term_id']}, '{name}', '{slug}', {r['term_group']});" + ) +sql_lines.append("") + +# ─── 2. wp_term_taxonomy (category rows for translated terms) ──────────────── +c.execute(f""" + SELECT term_taxonomy_id, term_id, taxonomy, description, parent, count + FROM wp_term_taxonomy + WHERE term_id IN ({ids_str}) AND taxonomy='category' +""") +cat_rows = c.fetchall() +sql_lines.append("-- 2. wp_term_taxonomy (taxonomy='category' for translated terms)") +for r in cat_rows: + desc = r['description'].replace("'", "''") if r['description'] else '' + sql_lines.append( + f"INSERT IGNORE INTO wp_term_taxonomy " + f"(term_taxonomy_id, term_id, taxonomy, description, parent, count) " + f"VALUES ({r['term_taxonomy_id']}, {r['term_id']}, 'category', " + f"'{desc}', {r['parent']}, {r['count']});" + ) +sql_lines.append("") + +# ─── 3. wp_term_taxonomy (language rows for translated terms) ──────────────── +c.execute(f""" + SELECT term_taxonomy_id, term_id, taxonomy, description, parent, count + FROM wp_term_taxonomy + WHERE term_id IN ({ids_str}) AND taxonomy='language' +""") +lang_rows = c.fetchall() +sql_lines.append("-- 3. wp_term_taxonomy (taxonomy='language' for translated terms)") +for r in lang_rows: + desc = r['description'].replace("'", "''") if r['description'] else '' + sql_lines.append( + f"INSERT IGNORE INTO wp_term_taxonomy " + f"(term_taxonomy_id, term_id, taxonomy, description, parent, count) " + f"VALUES ({r['term_taxonomy_id']}, {r['term_id']}, 'language', " + f"'{desc}', {r['parent']}, {r['count']});" + ) +sql_lines.append("") + +# ─── 4. wp_term_taxonomy (term_translations groups for our ES categories) ─── +# Get translation groups that contain any of our ES or translated term_ids +all_ids_str = ','.join(str(i) for i in all_es_ids + all_trans_ids) +c.execute(""" + SELECT DISTINCT tt.term_taxonomy_id, tt.term_id, tt.taxonomy, + tt.description, tt.parent, tt.count + FROM wp_term_taxonomy tt + WHERE tt.taxonomy = 'term_translations' +""") +all_tt_rows = c.fetchall() + +# Filter to only those that reference our category term_ids +relevant_tt = [] +for r in all_tt_rows: + desc = r['description'] or '' + # Check if any of our term_ids appear in the description + for tid in all_es_ids + all_trans_ids: + if f'i:{tid};' in desc or f'i:{tid}' == desc.strip(): + relevant_tt.append(r) + break + +sql_lines.append("-- 4. wp_term_taxonomy (taxonomy='term_translations' groups for our categories)") +for r in relevant_tt: + desc = r['description'].replace("'", "''") if r['description'] else '' + sql_lines.append( + f"INSERT INTO wp_term_taxonomy " + f"(term_taxonomy_id, term_id, taxonomy, description, parent, count) " + f"VALUES ({r['term_taxonomy_id']}, {r['term_id']}, 'term_translations', " + f"'{desc}', {r['parent']}, {r['count']}) " + f"ON DUPLICATE KEY UPDATE description=VALUES(description), count=VALUES(count);" + ) +sql_lines.append("") + +# ─── 5. wp_terms for term_translations taxonomy entries ───────────────────── +tt_term_ids = [r['term_id'] for r in relevant_tt] +if tt_term_ids: + tt_ids_str = ','.join(str(i) for i in tt_term_ids) + c.execute(f"SELECT term_id, name, slug, term_group FROM wp_terms WHERE term_id IN ({tt_ids_str})") + tt_term_rows = c.fetchall() + # Insert before the term_taxonomy rows (we need to reorder — prepend) + term_inserts = [] + for r in tt_term_rows: + name = r['name'].replace("'", "''") + slug = r['slug'].replace("'", "''") + term_inserts.append( + f"INSERT IGNORE INTO wp_terms (term_id, name, slug, term_group) " + f"VALUES ({r['term_id']}, '{name}', '{slug}', {r['term_group']});" + ) + # Insert after section 1 + idx = sql_lines.index("-- 2. wp_term_taxonomy (taxonomy='category' for translated terms)") + sql_lines[idx:idx] = ["-- 1b. wp_terms for term_translations taxonomy"] + term_inserts + [""] + +# ─── 6. wp_term_relationships (post→translated category) ───────────────────── +# Get term_taxonomy_ids for translated categories +cat_tt_ids = [r['term_taxonomy_id'] for r in cat_rows] +if cat_tt_ids: + cat_tt_str = ','.join(str(i) for i in cat_tt_ids) + c.execute(f""" + SELECT object_id, term_taxonomy_id, term_order + FROM wp_term_relationships + WHERE term_taxonomy_id IN ({cat_tt_str}) + ORDER BY term_taxonomy_id, object_id + """) + rel_rows = c.fetchall() + sql_lines.append("-- 5. wp_term_relationships (posts → translated categories)") + sql_lines.append(f"-- {len(rel_rows)} relationships") + # Batch INSERT for efficiency + if rel_rows: + batch = [] + for r in rel_rows: + batch.append(f"({r['object_id']},{r['term_taxonomy_id']},{r['term_order']})") + if len(batch) >= 500: + sql_lines.append( + "INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id, term_order) VALUES " + + ','.join(batch) + ";" + ) + batch = [] + if batch: + sql_lines.append( + "INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id, term_order) VALUES " + + ','.join(batch) + ";" + ) + sql_lines.append("") + +# ─── 7. wp_term_relationships (translated terms → language taxonomy) ───────── +lang_tt_ids = [r['term_taxonomy_id'] for r in lang_rows] +if lang_tt_ids: + lang_tt_str = ','.join(str(i) for i in lang_tt_ids) + c.execute(f""" + SELECT object_id, term_taxonomy_id, term_order + FROM wp_term_relationships + WHERE term_taxonomy_id IN ({lang_tt_str}) + """) + lang_rel_rows = c.fetchall() + sql_lines.append("-- 6. wp_term_relationships (translated category terms → language taxonomy)") + sql_lines.append(f"-- {len(lang_rel_rows)} relationships") + if lang_rel_rows: + batch = [] + for r in lang_rel_rows: + batch.append(f"({r['object_id']},{r['term_taxonomy_id']},{r['term_order']})") + if len(batch) >= 500: + sql_lines.append( + "INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id, term_order) VALUES " + + ','.join(batch) + ";" + ) + batch = [] + if batch: + sql_lines.append( + "INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id, term_order) VALUES " + + ','.join(batch) + ";" + ) + sql_lines.append("") + +sql_lines.append("SET foreign_key_checks = 1;") +sql_lines.append("") +sql_lines.append("-- Done.") + +db.close() + +output = '\n'.join(sql_lines) +with open('/tmp/cat_translations_prod.sql', 'w', encoding='utf-8') as f: + f.write(output) + +print(f"Written to /tmp/cat_translations_prod.sql") +print(f" {len(rows)} translated terms (wp_terms)") +print(f" {len(cat_rows)} category taxonomy rows") +print(f" {len(lang_rows)} language taxonomy rows") +print(f" {len(relevant_tt)} term_translations groups") +if cat_tt_ids: + print(f" {len(rel_rows)} post→category relationships") +if lang_tt_ids: + print(f" {len(lang_rel_rows)} term→language relationships") diff --git a/scripts/export_translations.py b/scripts/export_translations.py new file mode 100644 index 0000000..ade8a29 --- /dev/null +++ b/scripts/export_translations.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +""" +export_translations.py + +Genera SQL para importar todos los posts traducidos (ID > 42760) +de la DB local a producción, con remapeo correcto de language IDs (FR↔PT). +""" + +import pymysql + +DB_LOCAL = dict(host='172.18.0.2', port=3306, user='wordpress_user', + password='wordpress_pass', database='wordpress_db', charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor) + +# Local lang term_taxonomy_id → Production term_taxonomy_id +# Local: en=1407, es=1404, fr=1419, it=1415, pt=1411 +# Prod: en=1407, es=1404, fr=1411, it=1415, pt=1419 +LANG_MAP = {1407: 1407, 1404: 1404, 1419: 1411, 1415: 1415, 1411: 1419} + +def esc(s): + if s is None: + return 'NULL' + return "'" + str(s).replace('\\', '\\\\').replace("'", "\\'") + "'" + +db = pymysql.connect(**DB_LOCAL) +c = db.cursor() + +lines = [] +lines.append("SET NAMES utf8mb4;") +lines.append("SET foreign_key_checks = 0;") +lines.append("") + +# ── 1. wp_posts ─────────────────────────────────────────────────────────────── +lines.append("-- ============================================================") +lines.append("-- 1. POSTS (ID > 42760)") +lines.append("-- ============================================================") + +c.execute(""" + SELECT ID, post_author, post_date, post_date_gmt, post_content, post_title, + post_excerpt, post_status, comment_status, ping_status, post_password, + post_name, to_ping, pinged, post_modified, post_modified_gmt, + post_content_filtered, post_parent, guid, menu_order, post_type, + post_mime_type, comment_count + FROM wp_posts + WHERE ID > 42760 AND post_status='publish' AND post_type='post' + ORDER BY ID +""") +posts = c.fetchall() +lines.append(f"-- {len(posts)} posts") + +for p in posts: + cols = ['ID','post_author','post_date','post_date_gmt','post_content','post_title', + 'post_excerpt','post_status','comment_status','ping_status','post_password', + 'post_name','to_ping','pinged','post_modified','post_modified_gmt', + 'post_content_filtered','post_parent','guid','menu_order','post_type', + 'post_mime_type','comment_count'] + vals = ', '.join(esc(p[col]) for col in cols) + lines.append( + f"INSERT IGNORE INTO wp_posts ({', '.join(cols)}) VALUES ({vals});" + ) + +lines.append("") + +# ── 2. wp_term_relationships — language ────────────────────────────────────── +lines.append("-- ============================================================") +lines.append("-- 2. LANGUAGE ASSIGNMENTS (remapped FR↔PT)") +lines.append("-- ============================================================") + +post_ids = [p['ID'] for p in posts] +fmt = ','.join(str(i) for i in post_ids) + +c.execute(f""" + SELECT object_id, term_taxonomy_id + FROM wp_term_relationships + WHERE object_id IN ({fmt}) + AND term_taxonomy_id IN (1404,1407,1411,1415,1419) +""") +lang_rels = c.fetchall() +lines.append(f"-- {len(lang_rels)} language assignments") + +for r in lang_rels: + prod_ttid = LANG_MAP[r['term_taxonomy_id']] + lines.append( + f"INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id, term_order) " + f"VALUES ({r['object_id']}, {prod_ttid}, 0);" + ) + +lines.append("") + +# ── 3. wp_terms + wp_term_taxonomy — post_translations groups ───────────────── +lines.append("-- ============================================================") +lines.append("-- 3. POST_TRANSLATIONS GROUPS (term_taxonomy_id 2705-3043)") +lines.append("-- ============================================================") + +c.execute(f""" + SELECT DISTINCT tt.term_taxonomy_id, tt.term_id, tt.taxonomy, tt.description, + tt.parent, tt.count, t.name, t.slug, t.term_group + FROM wp_term_taxonomy tt + JOIN wp_terms t ON tt.term_id=t.term_id + JOIN wp_term_relationships tr ON tt.term_taxonomy_id=tr.term_taxonomy_id + WHERE tt.taxonomy='post_translations' + AND tr.object_id IN ({fmt}) + ORDER BY tt.term_taxonomy_id +""") +pt_groups = c.fetchall() +lines.append(f"-- {len(pt_groups)} translation groups") + +for g in pt_groups: + # wp_terms + lines.append( + f"INSERT IGNORE INTO wp_terms (term_id, name, slug, term_group) " + f"VALUES ({g['term_id']}, {esc(g['name'])}, {esc(g['slug'])}, {g['term_group']});" + ) + # wp_term_taxonomy + lines.append( + f"INSERT IGNORE INTO wp_term_taxonomy (term_taxonomy_id, term_id, taxonomy, description, parent, count) " + f"VALUES ({g['term_taxonomy_id']}, {g['term_id']}, 'post_translations', " + f"{esc(g['description'])}, {g['parent']}, {g['count']});" + ) + +lines.append("") + +# ── 4. wp_term_relationships — post_translations (ALL members of each group) ── +lines.append("-- ============================================================") +lines.append("-- 4. POST_TRANSLATIONS RELATIONSHIPS (all group members)") +lines.append("-- ============================================================") + +pt_ttids = [g['term_taxonomy_id'] for g in pt_groups] +fmt_tt = ','.join(str(i) for i in pt_ttids) + +c.execute(f""" + SELECT object_id, term_taxonomy_id + FROM wp_term_relationships + WHERE term_taxonomy_id IN ({fmt_tt}) + ORDER BY term_taxonomy_id, object_id +""") +pt_rels = c.fetchall() +lines.append(f"-- {len(pt_rels)} translation group relationships") + +for r in pt_rels: + lines.append( + f"INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id, term_order) " + f"VALUES ({r['object_id']}, {r['term_taxonomy_id']}, 0);" + ) + +lines.append("") + +# ── 5. wp_term_relationships — categories ───────────────────────────────────── +lines.append("-- ============================================================") +lines.append("-- 5. CATEGORY ASSIGNMENTS") +lines.append("-- ============================================================") + +c.execute(f""" + SELECT tr.object_id, tr.term_taxonomy_id + FROM wp_term_relationships tr + JOIN wp_term_taxonomy tt ON tr.term_taxonomy_id=tt.term_taxonomy_id + WHERE tr.object_id IN ({fmt}) + AND tt.taxonomy='category' + ORDER BY tr.object_id +""") +cat_rels = c.fetchall() +lines.append(f"-- {len(cat_rels)} category assignments") + +for r in cat_rels: + lines.append( + f"INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id, term_order) " + f"VALUES ({r['object_id']}, {r['term_taxonomy_id']}, 0);" + ) + +lines.append("") +lines.append("-- ============================================================") +lines.append("-- 6. UPDATE term_taxonomy counts") +lines.append("-- ============================================================") +lines.append(""" +UPDATE wp_term_taxonomy tt +SET count = ( + SELECT COUNT(*) FROM wp_term_relationships tr + JOIN wp_posts p ON tr.object_id=p.ID + WHERE tr.term_taxonomy_id=tt.term_taxonomy_id + AND p.post_status='publish' +) +WHERE tt.taxonomy IN ('language','post_translations','category'); +""") + +lines.append("SET foreign_key_checks = 1;") +lines.append(f"-- Export complete: {len(posts)} posts, {len(pt_groups)} translation groups") + +db.close() + +output = '\n'.join(lines) +with open('/tmp/translations_export.sql', 'w', encoding='utf-8') as f: + f.write(output) + +print(f"SQL written to /tmp/translations_export.sql") +print(f" Posts: {len(posts)}") +print(f" Language rels: {len(lang_rels)}") +print(f" Translation groups: {len(pt_groups)}") +print(f" Group rels: {len(pt_rels)}") +print(f" Category rels: {len(cat_rels)}") +print(f" File size: {len(output)//1024} KB") diff --git a/scripts/face_crop_avatar.py b/scripts/face_crop_avatar.py new file mode 100644 index 0000000..456ebd2 --- /dev/null +++ b/scripts/face_crop_avatar.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +""" +Recorta avatares cuadrados centrados en la cara, usando Haar cascade de OpenCV. + +Uso: + python3 face_crop_avatar.py{n} {v[n]}
' for n in range(V0,V1+1) if n in v) +json.dump(out,open("/tmp/lecturas_fetched.json","w"),ensure_ascii=False) +print("OK ->/tmp/lecturas_fetched.json", {k:len(x) for k,x in out.items()}) diff --git a/scripts/fix_carta_content_links.php b/scripts/fix_carta_content_links.php new file mode 100644 index 0000000..895f148 --- /dev/null +++ b/scripts/fix_carta_content_links.php @@ -0,0 +1,65 @@ +/