Separa vídeos y Shorts con los feeds por tipo de YouTube; destacado = último vídeo

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSyR5Zw5eGj6oPTRVjvP1b
This commit is contained in:
Alex
2026-10-20 16:38:18 +02:00
co-authored by Claude Sonnet 5.5
parent 7e1ae9841b
commit 1b93a2e2aa
3 changed files with 147 additions and 80 deletions
+50 -27
View File
@@ -9,11 +9,13 @@ 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
CHANNEL = 'UC8gVtzB3Lqm7nnfMhMvXGxQ'
# YouTube separa cada tipo en su propia lista: UULF = solo vídeos, UUSH = solo Shorts
FEED_VIDEOS = os.environ.get('FEED_VIDEOS_URL', f'https://www.youtube.com/feeds/videos.xml?playlist_id=UULF{CHANNEL[2:]}')
FEED_SHORTS = os.environ.get('FEED_SHORTS_URL', f'https://www.youtube.com/feeds/videos.xml?playlist_id=UUSH{CHANNEL[2:]}')
VIDEOS_SHOWN = 8 # además del destacado
SHORTS_SHOWN = 12
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']
@@ -28,21 +30,27 @@ def fetch(url, timeout=20):
return urllib.request.urlopen(req, timeout=timeout).read()
def read_feed(url):
root = ET.fromstring(fetch(url))
out = []
for e in root.findall('a:entry', NS)[:15]:
d = e.find('a:published', NS).text[:10].split('-')
out.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 '',
})
return out
try:
root = ET.fromstring(fetch(FEED))
videos, shorts = read_feed(FEED_VIDEOS), read_feed(FEED_SHORTS)
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, shorts = videos[:VIDEOS_SHOWN + 1], shorts[:SHORTS_SHOWN]
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')
@@ -65,18 +73,14 @@ def sync_thumbs(vids):
return ok
videos = sync_thumbs(videos)
ok = sync_thumbs(videos + shorts)
okids = {v['id'] for v in ok}
videos = [v for v in videos if v['id'] in okids]
shorts = [v for v in shorts if v['id'] in okids]
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'}
@@ -114,10 +118,10 @@ def topics(vids):
return out + PINNED
chips = ''.join(f'<li>{html.escape(t)}</li>' for t in topics(videos))
chips = ''.join(f'<li>{html.escape(t)}</li>' for t in topics(videos + shorts))
esc = html.escape
featured, rest = videos[0], videos[1:] # el feed viene del más nuevo al más antiguo
featured, rest = videos[0], videos[1:] # el destacado es el último vídeo (no Short)
def url(v):
@@ -127,12 +131,21 @@ def url(v):
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="thumb"><img src="img/{v['id']}.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>{esc(v['date'])}</time><strong>{esc(v['title'])}</strong></span>
</a>'''
def short_card(v, n):
return f'''
<a class="scard reveal" style="--d:{(n % 6) * 50}ms" href="https://www.youtube.com/shorts/{v['id']}" target="_blank" rel="noopener">
<span class="thumb"><img src="img/{v['id']}.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>{esc(v['title'])}</strong></span>
</a>'''
cards = ''.join(card(v, i) for i, v in enumerate(rest))
scards = ''.join(short_card(v, i) for i, v in enumerate(shorts))
page = f'''<!doctype html>
<html lang="es">
@@ -155,7 +168,8 @@ page = f'''<!doctype html>
<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="#programas">Vídeos</a>
<a href="#shorts">Shorts</a>
<a href="#programa">El programa</a>
<a href="#quienes">Quiénes somos</a>
<a href="#seguir">Seguir</a>
@@ -173,7 +187,7 @@ page = f'''<!doctype html>
<p class="lead">Charlas 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>
<a class="btn" href="#programas">Últimos vídeos</a>
</div>
</div>
<a class="scroll" href="#destacado" aria-label="Bajar"><span></span></a>
@@ -200,6 +214,15 @@ page = f'''<!doctype html>
</div>
</section>
<section id="shorts" class="wrap">
<div class="sec-head reveal">
<h2>Últimos <em>Shorts</em></h2>
<a class="more" href="{YT}/shorts" target="_blank" rel="noopener">Todos los Shorts →</a>
</div>
<div class="shorts">{scards}
</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>