Miniaturas HD, formulario de patrocinadores y sección Lo más visto
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSyR5Zw5eGj6oPTRVjvP1b
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
e8b1f765c8
commit
c8afac0f6d
@@ -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'''
|
||||
<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"><span class="play" aria-hidden="true"></span></span>
|
||||
<span class="thumb"><img src="img/{v['img']}" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
|
||||
<span class="meta"><time>{esc(v['date'])}{' · ' + vistas(v) if show_views and v['views'] else ''}</time><strong>{esc(v['title'])}</strong></span>
|
||||
</a>'''
|
||||
|
||||
@@ -155,7 +190,7 @@ def card(v, n, show_views=False):
|
||||
def short_card(v, n, show_views=False):
|
||||
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="thumb"><img src="img/{v['img']}" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
|
||||
<span class="meta"><strong>{esc(v['title'])}</strong>{'<time>' + vistas(v) + '</time>' if show_views and v['views'] else ''}</span>
|
||||
</a>'''
|
||||
|
||||
@@ -213,7 +248,7 @@ page = f'''<!doctype html>
|
||||
|
||||
<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="thumb"><img src="img/{featured['img']}" alt="" width="1280" height="720"><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>
|
||||
@@ -272,7 +307,7 @@ page = f'''<!doctype html>
|
||||
<section class="wrap reveal" aria-labelledby="venir">
|
||||
<div class="invite">
|
||||
<div><h2 id="venir">¿Quieres venir al programa?</h2><p>Cuéntanos quién eres y de qué te gustaría hablar.</p></div>
|
||||
<a class="btn primary" href="contacto.html">Escríbenos</a>
|
||||
<div class="cta"><a class="btn primary" href="contacto.html">Escríbenos</a><a class="btn" href="patrocinadores.html">Quiero anunciarme</a></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -289,7 +324,7 @@ page = f'''<!doctype html>
|
||||
<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="contacto.html">Contacto</a></p>
|
||||
<p class="links"><a href="privacidad.html">Política de privacidad</a><a href="terminos.html">Condiciones del servicio</a><a href="contacto.html">Contacto</a><a href="patrocinadores.html">Patrocinadores</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user