"""Genera index.html a partir del feed RSS del canal (se ejecuta cada 15 min en el contenedor).""" import html import os import re import unicodedata from collections import Counter import shutil import sys import urllib.request import xml.etree.ElementTree as ET from PIL import Image, ImageStat OUT = os.environ.get('SITE_DIR', 'public') FEED = os.environ.get('FEED_URL', 'https://www.youtube.com/feeds/videos.xml?channel_id=UC8gVtzB3Lqm7nnfMhMvXGxQ') MAX_VIDEOS = 15 NS = {'a': 'http://www.w3.org/2005/Atom', 'y': 'http://www.youtube.com/xml/schemas/2015', 'm': 'http://search.yahoo.com/mrss/'} MESES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'] YT = 'https://www.youtube.com/@andaquenosabesna' TT = 'https://www.tiktok.com/@andaquenosabesna' IG = 'https://www.instagram.com/andaquenosabesna' MAIL = 'andaquenosabesnada@gmail.com' def fetch(url, timeout=20): req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (aqnsn-site)'}) return urllib.request.urlopen(req, timeout=timeout).read() try: root = ET.fromstring(fetch(FEED)) except Exception as exc: # sin red o feed caído: se conserva la web anterior print('feed no disponible, se mantiene index.html:', exc, file=sys.stderr) sys.exit(0) videos = [] for e in root.findall('a:entry', NS)[:MAX_VIDEOS]: d = e.find('a:published', NS).text[:10].split('-') videos.append({ 'id': e.find('y:videoId', NS).text, 'title': e.find('a:title', NS).text, 'date': f'{int(d[2])} {MESES[int(d[1]) - 1]} {d[0]}', 'desc': e.find('m:group/m:description', NS).text or '', }) def sync_thumbs(vids): img_dir = os.path.join(OUT, 'img') os.makedirs(img_dir, exist_ok=True) keep = {'avatar.jpg', 'banner.jpg'} ok = [] for v in vids: path = os.path.join(img_dir, v['id'] + '.jpg') if not os.path.exists(path): try: open(path, 'wb').write(fetch(f"https://i.ytimg.com/vi/{v['id']}/hqdefault.jpg")) except Exception as exc: print('miniatura no descargada', v['id'], exc, file=sys.stderr) continue keep.add(v['id'] + '.jpg') ok.append(v) for f in os.listdir(img_dir): if f not in keep: os.remove(os.path.join(img_dir, f)) return ok videos = sync_thumbs(videos) if not videos: sys.exit(0) def is_short(vid): """Los Shorts llevan barras laterales desenfocadas: poca textura en los laterales.""" im = Image.open(os.path.join(OUT, 'img', vid + '.jpg')).convert('L') w, h = im.size return ImageStat.Stat(im.crop((0, 0, int(w * .18), h))).stddev[0] < 25 # --- Temas: hashtags de las descripciones de YouTube ------------------------------------------- STOP = {'podcast', 'podcastclips', 'andaquenosabesna', 'andaquenosabesna', 'aqnsn', 'shorts', 'short', 'fyp', 'viral', 'parati', 'galaxoide', 'youtube', 'video', 'clips'} LABELS = { # clave normalizada -> texto a mostrar (sinónimos incluidos) 'egipto': 'Egipto', 'egiptomania': 'Egipto', 'educacion': 'Educación', 'arabe': 'Lengua árabe', 'lenguas': 'Lengua árabe', 'religion': 'Religiones', 'religiones': 'Religiones', 'magia': 'Magia', 'burocracia': 'Burocracia', 'poligamia': 'Poligamia', 'economia': 'Economía', 'gastronomia': 'Gastronomía', 'comidaegipcia': 'Comida egipcia', 'robotica': 'Robótica', 'tecnologia': 'Tecnología', 'agricultura': 'Agricultura', 'curiosidades': 'Curiosidades', } PINNED = ['Cultura granaína', 'Entrevistas'] # siempre presentes MAX_TOPICS = 12 def norm(tag): t = unicodedata.normalize('NFKD', tag.lower()) return ''.join(c for c in t if not unicodedata.combining(c)) def topics(vids): count = Counter() for v in vids: seen = set() for tag in re.findall(r'#(\w+)', v['desc']): k = norm(tag) if k in STOP: continue label = LABELS.get(k) or (tag[0].upper() + tag[1:]) if label in seen: # sinónimos del mismo vídeo cuentan una sola vez continue seen.add(label) count[label] += 1 ranked = [t for t, _ in sorted(count.items(), key=lambda kv: (-kv[1], kv[0]))] out = [t for t in ranked if t not in PINNED][:max(MAX_TOPICS - len(PINNED), 0)] return out + PINNED chips = ''.join(f'
  • {html.escape(t)}
  • ' for t in topics(videos)) esc = html.escape featured, rest = videos[0], videos[1:] # el feed viene del más nuevo al más antiguo def url(v): return f"https://www.youtube.com/watch?v={v['id']}" def card(v, n): return f''' {esc(v['title'])} ''' cards = ''.join(card(v, i) for i, v in enumerate(rest)) page = f''' Anda Que No Sabes Ná · El programa nocturno 100% granaíno
    Anda que no sabes ná
    Anda Que No Sabes Ná

    Directo desde Granada, de noche

    El programa nocturno
    100% granaíno

    Charlas sobre lo que sabemos, lo que no, y lo que creemos saber.

    Últimos vídeos

    Todos los vídeos →
    {cards}

    De qué va

    Un programa de noche, con tertulia y sin prisa. Cada semana entra gente distinta, con su oficio y su manía, y se habla de lo que sale: de cómo se educa a un adolescente, de la vida en las calles de El Cairo, de si el árabe es tan antiguo como dicen, de qué tienen en común las religiones o de cómo se hace un truco de cartas.

    Quiénes somos

    Somos Alejandro y Juan, las dos personas detrás de Anda Que No Sabes Ná: un programa hecho en Granada, de noche, con ganas de aprender de quien se sienta a charlar.

    AlejandroDa forma a las ideasArquitecto de formación y líder de equipos de ingeniería. Construye productos digitales y herramientas para equipos técnicos, y anda metido en cómo usar la IA en el día a día.
    JuanCaptura la realidadConsultor en metodología BIM y escáner láser 3D. Ayuda a las empresas de la arquitectura, la ingeniería y la construcción a dar el salto digital con mejor calidad y menos costes.

    ¿Quieres venir al programa?

    Cuéntanos quién eres y de qué te gustaría hablar.

    Escríbenos

    Síguenos

    ''' tmp = os.path.join(OUT, 'index.html.tmp') open(tmp, 'w', encoding='utf-8').write(page) shutil.move(tmp, os.path.join(OUT, 'index.html')) print('index.html', len(page), 'bytes;', len(videos), 'videos; destacado:', featured['id'])