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'''