"""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ínoSaltar al contenidoAnda 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.
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.
{chips}
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.