Files
andaquenosabesna/build.py
T

201 lines
7.9 KiB
Python

"""Genera index.html a partir del feed RSS del canal (se ejecuta cada 15 min en el contenedor)."""
import html
import os
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'}
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]}',
})
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
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'''
<a class="card reveal" style="--d:{(n % 3) * 70}ms" href="{url(v)}" target="_blank" rel="noopener">
<span class="thumb"><img src="img/{v['id']}.jpg" alt="" loading="lazy" width="480" height="360"{' class="short"' if is_short(v['id']) else ''}><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>{esc(v['date'])}</time><strong>{esc(v['title'])}</strong></span>
</a>'''
cards = ''.join(card(v, i) for i, v in enumerate(rest))
page = f'''<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Anda Que No Sabes Ná · El programa nocturno 100% granaíno</title>
<meta name="description" content="Anda Que No Sabes Ná: el programa nocturno 100% granaíno. Conversaciones sobre educación, Egipto, lenguas, religiones y mucho más.">
<meta name="theme-color" content="#06080d">
<meta property="og:title" content="Anda Que No Sabes Ná">
<meta property="og:description" content="El programa nocturno 100% granaíno.">
<meta property="og:image" content="https://andaquenosabesna.thax.es/img/banner.jpg">
<meta property="og:type" content="website">
<link rel="icon" href="img/avatar.jpg">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<a class="skip" href="#programas">Saltar al contenido</a>
<header class="top">
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
<nav aria-label="Principal">
<a href="#programas">Programas</a>
<a href="#programa">El programa</a>
<a href="#seguir">Seguir</a>
</nav>
</header>
<main>
<section class="hero">
<div class="hero-bg" aria-hidden="true"></div>
<div class="hero-inner">
<img class="neon" src="img/avatar.jpg" alt="Anda Que No Sabes Ná" width="220" height="220">
<p class="kicker">Directo desde Granada, de noche</p>
<h1>El programa nocturno<br><em>100% granaíno</em></h1>
<p class="lead">Charlas sin guion sobre lo que sabemos, lo que no, y lo que creemos saber.</p>
<div class="cta">
<a class="btn primary" href="{YT}" target="_blank" rel="noopener">Ver en YouTube</a>
<a class="btn" href="#programas">Últimos programas</a>
</div>
</div>
<a class="scroll" href="#destacado" aria-label="Bajar"><span></span></a>
</section>
<section id="destacado" class="featured wrap reveal">
<a class="feat" href="{url(featured)}" target="_blank" rel="noopener">
<span class="thumb"><img src="img/{featured['id']}.jpg" alt="" width="480" height="360"><span class="play big" aria-hidden="true"></span></span>
<span class="feat-text">
<span class="tag">Último vídeo</span>
<strong>{esc(featured['title'])}</strong>
<time>{esc(featured['date'])}</time>
<span class="go">Ver el programa <i aria-hidden="true">→</i></span>
</span>
</a>
</section>
<section id="programas" class="wrap">
<div class="sec-head reveal">
<h2>Últimos <em>vídeos</em></h2>
<a class="more" href="{YT}/videos" target="_blank" rel="noopener">Todos los vídeos →</a>
</div>
<div class="grid">{cards}
</div>
</section>
<section id="programa" class="about wrap reveal">
<h2>De qué va</h2>
<p>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.</p>
<ul class="chips" aria-label="Temas">
<li>Educación</li><li>Egipto</li><li>Lengua árabe</li><li>Religiones</li><li>Magia</li><li>Cultura granaína</li><li>Entrevistas</li>
</ul>
</section>
<section id="seguir" class="follow wrap reveal">
<h2>Síguenos</h2>
<div class="socials">
<a href="{YT}" target="_blank" rel="noopener"><b>YouTube</b><span>@andaquenosabesna</span></a>
<a href="{TT}" target="_blank" rel="noopener"><b>TikTok</b><span>@andaquenosabesna</span></a>
<a href="{IG}" target="_blank" rel="noopener"><b>Instagram</b><span>@andaquenosabesna</span></a>
</div>
</section>
</main>
<footer>
<div class="wrap foot">
<p>© 2026 Anda Que No Sabes Ná · Granada</p>
<p class="links"><a href="privacidad.html">Política de privacidad</a><a href="terminos.html">Condiciones del servicio</a><a href="mailto:{MAIL}">Contacto</a></p>
</div>
</footer>
<script>
(function () {{
var els = document.querySelectorAll('.reveal');
if (!('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches) {{
els.forEach(function (e) {{ e.classList.add('in'); }}); return;
}}
var io = new IntersectionObserver(function (items) {{
items.forEach(function (i) {{ if (i.isIntersecting) {{ i.target.classList.add('in'); io.unobserve(i.target); }} }});
}}, {{ threshold: 0.12 }});
els.forEach(function (e) {{ io.observe(e); }});
}})();
</script>
</body>
</html>
'''
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'])