From c8afac0f6dd983bca40caf6d98bbf49b16e28260 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 20 Oct 2026 16:51:36 +0200 Subject: [PATCH] =?UTF-8?q?Miniaturas=20HD,=20formulario=20de=20patrocinad?= =?UTF-8?q?ores=20y=20secci=C3=B3n=20Lo=20m=C3=A1s=20visto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01TSyR5Zw5eGj6oPTRVjvP1b --- build.py | 71 ++++++++++++++++++++++++++++---------- public/contacto.html | 34 ++---------------- public/form.js | 29 ++++++++++++++++ public/index.html | 66 +++++++++++++++++------------------ public/patrocinadores.html | 59 +++++++++++++++++++++++++++++++ public/privacidad.html | 6 ++-- server.py | 35 ++++++++++++++----- 7 files changed, 207 insertions(+), 93 deletions(-) create mode 100644 public/form.js create mode 100644 public/patrocinadores.html diff --git a/build.py b/build.py index 17c5b3b..449a0b8 100644 --- a/build.py +++ b/build.py @@ -1,5 +1,6 @@ """Genera index.html a partir del feed RSS del canal (se ejecuta cada 15 min en el contenedor).""" import html +import io import os import re import unicodedata @@ -9,6 +10,8 @@ import sys import urllib.request import xml.etree.ElementTree as ET +from PIL import Image + OUT = os.environ.get('SITE_DIR', 'public') CHANNEL = 'UC8gVtzB3Lqm7nnfMhMvXGxQ' # YouTube separa cada tipo en su propia lista: UULF = solo vídeos, UUSH = solo Shorts @@ -62,28 +65,60 @@ pop_videos = sorted(pop_videos, key=lambda v: -v['views'])[:POP_VIDEOS_SHOWN] pop_shorts = sorted(pop_shorts, key=lambda v: -v['views'])[:POP_SHORTS_SHOWN] -def sync_thumbs(vids): +KEEP = {'avatar.jpg', 'banner.jpg'} +# (nombre en YouTube, ancho máx. al guardar) por orden de preferencia +SOURCES = { + 'video': [('maxresdefault', 1280), ('sddefault', 960), ('hqdefault', 960)], + 'short': [('oar2', 540), ('hqdefault', 480)], # oar2 = miniatura vertical 1080x1920 +} + + +def grab(vid, kind): + """Devuelve (bytes JPEG recomprimido, es_alta_calidad) con la mejor miniatura disponible.""" + for i, (name, width) in enumerate(SOURCES[kind]): + try: + im = Image.open(io.BytesIO(fetch(f'https://i.ytimg.com/vi/{vid}/{name}.jpg'))).convert('RGB') + except Exception: + continue # 404 u otro fallo: se prueba la siguiente + if im.width < 200: # YouTube devuelve un gif de 120x90 cuando no existe + continue + if im.width > width: + im = im.resize((width, round(im.height * width / im.width)), Image.LANCZOS) + buf = io.BytesIO() + im.save(buf, 'JPEG', quality=82, optimize=True, progressive=True) + return buf.getvalue(), i == 0 + return None, False + + +def sync_thumbs(vids, kind): + """Descarga la miniatura HD de cada vídeo (…-hd.jpg). Si aún no existe la HD, usa una + de respaldo (…-lo.jpg) y en la siguiente ejecución vuelve a intentar la HD.""" 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) + hd, lo = f"{v['id']}-hd.jpg", f"{v['id']}-lo.jpg" + if not os.path.exists(os.path.join(img_dir, hd)): + data, is_hd = grab(v['id'], kind) + if data is None: + print('miniatura no descargada', v['id'], file=sys.stderr) continue - keep.add(v['id'] + '.jpg') + open(os.path.join(img_dir, hd if is_hd else lo), 'wb').write(data) + v['img'] = hd if os.path.exists(os.path.join(img_dir, hd)) else lo + KEEP.add(v['img']) 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 -ok = sync_thumbs(videos + shorts + pop_videos + pop_shorts) +def prune_thumbs(): + img_dir = os.path.join(OUT, 'img') + for f in os.listdir(img_dir): + if f not in KEEP: + os.remove(os.path.join(img_dir, f)) + + +ok = sync_thumbs(videos + pop_videos, 'video') + sync_thumbs(shorts + pop_shorts, 'short') +prune_thumbs() 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] @@ -147,7 +182,7 @@ def vistas(v): def card(v, n, show_views=False): return f''' - + {esc(v['title'])} ''' @@ -155,7 +190,7 @@ def card(v, n, show_views=False): def short_card(v, n, show_views=False): return f''' - + {esc(v['title'])}{'' if show_views and v['views'] else ''} ''' @@ -213,7 +248,7 @@ page = f'''