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:
Alex
2026-10-20 16:51:36 +02:00
co-authored by Claude Sonnet 5.5
parent e8b1f765c8
commit c8afac0f6d
7 changed files with 207 additions and 93 deletions
+53 -18
View File
@@ -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>
+3 -31
View File
@@ -12,14 +12,14 @@
<body>
<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="/">Inicio</a><a href="/#quienes">Quiénes somos</a></nav>
<nav aria-label="Principal"><a href="/">Inicio</a><a href="/#quienes">Quiénes somos</a><a href="patrocinadores.html">Patrocinadores</a></nav>
</header>
<main class="legal contact">
<h1>¿Quieres venir al <em>programa</em>?</h1>
<p class="lead-c">Cuéntanos quién eres y de qué te gustaría hablar. También puedes proponer un tema o simplemente escribirnos. Te respondemos por correo.</p>
<form id="f" class="form" novalidate>
<form id="f" class="form" novalidate data-endpoint="/api/contacto" data-mail="andaquenosabesnada@gmail.com" data-ok="¡Gracias! Hemos recibido tu mensaje y te responderemos pronto.">
<label>Nombre
<input name="nombre" type="text" autocomplete="name" maxlength="100" required>
</label>
@@ -47,34 +47,6 @@
<noscript><p>Necesitas JavaScript para usar el formulario. Mientras tanto, escríbenos a <a href="mailto:andaquenosabesnada@gmail.com">andaquenosabesnada@gmail.com</a>.</p></noscript>
</main>
<script>
(function () {
var f = document.getElementById('f'), st = document.getElementById('st'), t0 = Date.now(), busy = false;
function say(msg, ok) { st.textContent = msg; st.className = 'status ' + (ok ? 'ok' : 'err'); }
f.addEventListener('submit', function (e) {
e.preventDefault();
if (busy) return;
var d = new FormData(f);
var body = {
nombre: (d.get('nombre') || '').trim(), email: (d.get('email') || '').trim(),
motivo: d.get('motivo'), mensaje: (d.get('mensaje') || '').trim(),
consent: f.consent.checked, web: d.get('web') || '', t: Date.now() - t0
};
if (!body.nombre || !/^[^@\s]+@[^@\s]+\.[^@\s]{2,}$/.test(body.email) || body.mensaje.length < 10 || !body.consent) {
say('Revisa los campos: nombre, correo válido, un mensaje de al menos 10 caracteres y la casilla de privacidad.', false);
return;
}
busy = true; say('Enviando…', true);
fetch('/api/contacto', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
.then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok && j.ok, j: j }; }); })
.then(function (x) {
if (x.ok) { f.reset(); say('¡Gracias! Hemos recibido tu mensaje y te responderemos pronto.', true); }
else { say((x.j && x.j.error) || 'No se pudo enviar. Escríbenos a andaquenosabesnada@gmail.com.', false); }
})
.catch(function () { say('No se pudo enviar. Escríbenos a andaquenosabesnada@gmail.com.', false); })
.then(function () { busy = false; });
});
})();
</script>
<script src="form.js"></script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
(function () {
var f = document.querySelector('form[data-endpoint]');
if (!f) return;
var st = document.getElementById('st'), t0 = Date.now(), busy = false, mail = f.getAttribute('data-mail');
function say(msg, ok) { st.textContent = msg; st.className = 'status ' + (ok ? 'ok' : 'err'); }
f.addEventListener('submit', function (e) {
e.preventDefault();
if (busy) return;
var body = { consent: f.consent.checked, t: Date.now() - t0 };
Array.prototype.forEach.call(f.elements, function (el) {
if (el.name && el.name !== 'consent') body[el.name] = (el.value || '').trim();
});
if (!body.nombre || !/^[^@\s]+@[^@\s]+\.[^@\s]{2,}$/.test(body.email || '') || (body.mensaje || '').length < 10 ||
!body.consent || (f.empresa && !body.empresa)) {
say('Revisa los campos obligatorios, el correo y que el mensaje tenga al menos 10 caracteres, y marca la casilla de privacidad.', false);
return;
}
if (body.sitio && !/^https?:\/\//.test(body.sitio)) { say('La web debe empezar por http:// o https://', false); return; }
busy = true; say('Enviando…', true);
fetch(f.getAttribute('data-endpoint'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
.then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok && j.ok, j: j }; }); })
.then(function (x) {
if (x.ok) { f.reset(); say(f.getAttribute('data-ok'), true); }
else { say((x.j && x.j.error) || 'No se pudo enviar. Escríbenos a ' + mail + '.', false); }
})
.catch(function () { say('No se pudo enviar. Escríbenos a ' + mail + '.', false); })
.then(function () { busy = false; });
});
})();
+33 -33
View File
@@ -46,7 +46,7 @@
<section id="destacado" class="featured wrap reveal">
<a class="feat" href="https://www.youtube.com/watch?v=_-VaTy2fazY" target="_blank" rel="noopener">
<span class="thumb"><img src="img/_-VaTy2fazY.jpg" alt="" width="480" height="360"><span class="play big" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/_-VaTy2fazY-hd.jpg" 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>DOMESTICAR ADOLESCENTES, PLANTAR TOMATES Y CONSTRUIR MINIATURAS | AQNSN Programa 3 - Galaxoide</strong>
@@ -63,35 +63,35 @@
</div>
<div class="grid">
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=7zY2ey_ERlQ" target="_blank" rel="noopener">
<span class="thumb"><img src="img/7zY2ey_ERlQ.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/7zY2ey_ERlQ-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>10 oct 2026</time><strong>Un Simple Teletransporte de Cartas...</strong></span>
</a>
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=Q7qYQbvjMxQ" target="_blank" rel="noopener">
<span class="thumb"><img src="img/Q7qYQbvjMxQ.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/Q7qYQbvjMxQ-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>9 oct 2026</time><strong>El Mahshi y otras RECETAS EXÓTICAS de Egipto que PODRÍAN PERDERSE</strong></span>
</a>
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=mxP0dvn2HD4" target="_blank" rel="noopener">
<span class="thumb"><img src="img/mxP0dvn2HD4.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/mxP0dvn2HD4-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>6 oct 2026</time><strong>Árabe formal VS Árabe de la calle, ¿qué diferencias hay?</strong></span>
</a>
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=f6UO6jtUgrU" target="_blank" rel="noopener">
<span class="thumb"><img src="img/f6UO6jtUgrU.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/f6UO6jtUgrU-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>5 oct 2026</time><strong>&quot;Así robaron el busto de Nefertiti...&quot; - El problema del EXPOLIO DE TESOROS de Egipto</strong></span>
</a>
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=A2v_T2CCupo" target="_blank" rel="noopener">
<span class="thumb"><img src="img/A2v_T2CCupo.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/A2v_T2CCupo-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>4 oct 2026</time><strong>¿Cómo es la vida nocturna y callejera en Egipto?</strong></span>
</a>
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=H_yHEe6skW4" target="_blank" rel="noopener">
<span class="thumb"><img src="img/H_yHEe6skW4.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/H_yHEe6skW4-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>3 oct 2026</time><strong>EGIPTO NO ES COMO TE LO HAN CONTADO | AQNSN Programa 2 con Sara Yasser</strong></span>
</a>
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=Ei_VnfM-uLY" target="_blank" rel="noopener">
<span class="thumb"><img src="img/Ei_VnfM-uLY.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/Ei_VnfM-uLY-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>30 sep 2026</time><strong>TANATOPRACTOR DE DÍA Y ANIMADOR TUPPER SEX DE NOCHE | AQNSN Programa 1 con Javi (Parte 2)</strong></span>
</a>
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=g614maJ0998" target="_blank" rel="noopener">
<span class="thumb"><img src="img/g614maJ0998.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/g614maJ0998-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>30 sep 2026</time><strong>TANATOPRACTOR DE DÍA Y ANIMADOR TUPPER SEX DE NOCHE | AQNSN Programa 1 con Javi (Parte 1)</strong></span>
</a>
</div>
@@ -104,51 +104,51 @@
</div>
<div class="shorts">
<a class="scard reveal" style="--d:0ms" href="https://www.youtube.com/shorts/PxQHPd5YDLs" target="_blank" rel="noopener">
<span class="thumb"><img src="img/PxQHPd5YDLs.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/PxQHPd5YDLs-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>La Tediosa Burocracia en la Educación</strong></span>
</a>
<a class="scard reveal" style="--d:50ms" href="https://www.youtube.com/shorts/rLFSEqe77OY" target="_blank" rel="noopener">
<span class="thumb"><img src="img/rLFSEqe77OY.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/rLFSEqe77OY-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>¿Cómo dejas de comerte las uñas? El trauma útil de Palín</strong></span>
</a>
<a class="scard reveal" style="--d:100ms" href="https://www.youtube.com/shorts/jmV_Ao85RvI" target="_blank" rel="noopener">
<span class="thumb"><img src="img/jmV_Ao85RvI.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/jmV_Ao85RvI-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>¿Cómo de antiguo es el Árabe?</strong></span>
</a>
<a class="scard reveal" style="--d:150ms" href="https://www.youtube.com/shorts/DLcn8i9lV6Q" target="_blank" rel="noopener">
<span class="thumb"><img src="img/DLcn8i9lV6Q.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/DLcn8i9lV6Q-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>Todas las religiones son similares...</strong></span>
</a>
<a class="scard reveal" style="--d:200ms" href="https://www.youtube.com/shorts/E_eULZSlWJs" target="_blank" rel="noopener">
<span class="thumb"><img src="img/E_eULZSlWJs.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/E_eULZSlWJs-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>La Vida de las Mujeres Egipcias en las zonas costeras</strong></span>
</a>
<a class="scard reveal" style="--d:250ms" href="https://www.youtube.com/shorts/600_XwAHkv0" target="_blank" rel="noopener">
<span class="thumb"><img src="img/600_XwAHkv0.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/600_XwAHkv0-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>Clases religiosas separadas, la política de las escuelas egipcias</strong></span>
</a>
<a class="scard reveal" style="--d:0ms" href="https://www.youtube.com/shorts/XKSVhuk_lQI" target="_blank" rel="noopener">
<span class="thumb"><img src="img/XKSVhuk_lQI.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/XKSVhuk_lQI-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>Poligamia en Egipto, de la antigüedad a la actualidad</strong></span>
</a>
<a class="scard reveal" style="--d:50ms" href="https://www.youtube.com/shorts/voK4g7lIv4E" target="_blank" rel="noopener">
<span class="thumb"><img src="img/voK4g7lIv4E.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/voK4g7lIv4E-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>El Barrio Pijo de Egipto, Masr El Gedida, y sus tiendas de Oro</strong></span>
</a>
<a class="scard reveal" style="--d:100ms" href="https://www.youtube.com/shorts/yzTMyAKQnDQ" target="_blank" rel="noopener">
<span class="thumb"><img src="img/yzTMyAKQnDQ.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/yzTMyAKQnDQ-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>Las Franquicias se extienden en EGIPTO...</strong></span>
</a>
<a class="scard reveal" style="--d:150ms" href="https://www.youtube.com/shorts/DgTa9qX4xH0" target="_blank" rel="noopener">
<span class="thumb"><img src="img/DgTa9qX4xH0.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/DgTa9qX4xH0-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>El barrio cristiano del Cairo que RECICLA BASURA</strong></span>
</a>
<a class="scard reveal" style="--d:200ms" href="https://www.youtube.com/shorts/oAoEz2Kwmk8" target="_blank" rel="noopener">
<span class="thumb"><img src="img/oAoEz2Kwmk8.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/oAoEz2Kwmk8-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>Tiendas QUE NO CIERRAN en Egipto 🌇</strong></span>
</a>
<a class="scard reveal" style="--d:250ms" href="https://www.youtube.com/shorts/05m1CMyGyUA" target="_blank" rel="noopener">
<span class="thumb"><img src="img/05m1CMyGyUA.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/05m1CMyGyUA-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>¿Se parece la comida EGIPCIA a la ESPAÑOLA?</strong></span>
</a>
</div>
@@ -162,46 +162,46 @@
<h3 class="sub reveal">Vídeos</h3>
<div class="grid">
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=_-VaTy2fazY" target="_blank" rel="noopener">
<span class="thumb"><img src="img/_-VaTy2fazY.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/_-VaTy2fazY-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>18 oct 2026 · 385 vistas</time><strong>DOMESTICAR ADOLESCENTES, PLANTAR TOMATES Y CONSTRUIR MINIATURAS | AQNSN Programa 3 - Galaxoide</strong></span>
</a>
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=A2v_T2CCupo" target="_blank" rel="noopener">
<span class="thumb"><img src="img/A2v_T2CCupo.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/A2v_T2CCupo-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>4 oct 2026 · 252 vistas</time><strong>¿Cómo es la vida nocturna y callejera en Egipto?</strong></span>
</a>
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=rp9j_Ph9XvI" target="_blank" rel="noopener">
<span class="thumb"><img src="img/rp9j_Ph9XvI.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/rp9j_Ph9XvI-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>16 sep 2026 · 219 vistas</time><strong>ISA BECERRIL NOS PRESENTA SU NOVELA &quot;HIALURÓNICO&quot; | AQNSN Episodio Piloto 0</strong></span>
</a>
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=mxP0dvn2HD4" target="_blank" rel="noopener">
<span class="thumb"><img src="img/mxP0dvn2HD4.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/mxP0dvn2HD4-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><time>6 oct 2026 · 195 vistas</time><strong>Árabe formal VS Árabe de la calle, ¿qué diferencias hay?</strong></span>
</a>
</div>
<h3 class="sub reveal">Shorts</h3>
<div class="shorts">
<a class="scard reveal" style="--d:0ms" href="https://www.youtube.com/shorts/zJ_x4iA8OSs" target="_blank" rel="noopener">
<span class="thumb"><img src="img/zJ_x4iA8OSs.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/zJ_x4iA8OSs-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>El árabe que se estudia y el que se habla en las calles son diferentes</strong><time>1.547 vistas</time></span>
</a>
<a class="scard reveal" style="--d:50ms" href="https://www.youtube.com/shorts/nvF7P_S081o" target="_blank" rel="noopener">
<span class="thumb"><img src="img/nvF7P_S081o.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/nvF7P_S081o-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>😌 &quot;Solo vi a una persona morir sonriendo...&quot;</strong><time>1.545 vistas</time></span>
</a>
<a class="scard reveal" style="--d:100ms" href="https://www.youtube.com/shorts/Wfq5vjJ6sQU" target="_blank" rel="noopener">
<span class="thumb"><img src="img/Wfq5vjJ6sQU.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/Wfq5vjJ6sQU-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>&quot;Recoger a un niño ahogado de dos años, eso...&quot;</strong><time>1.257 vistas</time></span>
</a>
<a class="scard reveal" style="--d:150ms" href="https://www.youtube.com/shorts/XKSVhuk_lQI" target="_blank" rel="noopener">
<span class="thumb"><img src="img/XKSVhuk_lQI.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/XKSVhuk_lQI-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>Poligamia en Egipto, de la antigüedad a la actualidad</strong><time>1.207 vistas</time></span>
</a>
<a class="scard reveal" style="--d:200ms" href="https://www.youtube.com/shorts/p5trUANcq_c" target="_blank" rel="noopener">
<span class="thumb"><img src="img/p5trUANcq_c.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/p5trUANcq_c-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>El número 13 y su relación con la muerte #podcast #granada #misterio #latenight #andaquenosabesna</strong><time>1.043 vistas</time></span>
</a>
<a class="scard reveal" style="--d:250ms" href="https://www.youtube.com/shorts/7PRSukQTCwI" target="_blank" rel="noopener">
<span class="thumb"><img src="img/7PRSukQTCwI.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
<span class="thumb"><img src="img/7PRSukQTCwI-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span></span>
<span class="meta"><strong>¿Cómo puede meterte miedo un Tanatopractor? 😖</strong><time>886 vistas</time></span>
</a>
</div>
@@ -225,7 +225,7 @@
<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>
@@ -242,7 +242,7 @@
<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>
+59
View File
@@ -0,0 +1,59 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Patrocinadores · Anda Que No Sabes Ná</title>
<meta name="description" content="¿Quieres que tu marca suene en Anda Que No Sabes Ná? Cuéntanos qué tienes en mente.">
<meta name="theme-color" content="#06080d">
<link rel="icon" href="img/avatar.jpg">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<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="/">Inicio</a><a href="contacto.html">Contacto</a></nav>
</header>
<main class="legal contact">
<h1>¿Quieres <em>anunciarte</em> en el programa?</h1>
<p class="lead-c">Si tu marca quiere sonar en Anda Que No Sabes Ná, escríbenos. Cuéntanos quién eres, qué tienes en mente y qué te interesa: una mención, patrocinar episodios, una colaboración… Lo hablamos con calma.</p>
<form id="f" class="form" novalidate data-endpoint="/api/patrocinio" data-mail="andaquenosabesnada@gmail.com" data-ok="¡Gracias! Hemos recibido tu propuesta y te escribiremos pronto.">
<label>Empresa o marca
<input name="empresa" type="text" autocomplete="organization" maxlength="120" required>
</label>
<label>Tu nombre
<input name="nombre" type="text" autocomplete="name" maxlength="100" required>
</label>
<label>Correo electrónico
<input name="email" type="email" autocomplete="email" maxlength="200" required>
</label>
<label>Web (opcional)
<input name="sitio" type="text" inputmode="url" maxlength="200" placeholder="https://" autocomplete="url">
</label>
<label>¿Qué te interesa?
<select name="tipo">
<option value="mencion">Anunciarme / que me mencionéis en el programa</option>
<option value="patrocinio">Patrocinar episodios o una temporada</option>
<option value="colab">Colaboración o canje</option>
<option value="otro">Otra propuesta</option>
</select>
</label>
<label>Cuéntanos tu propuesta
<textarea name="mensaje" rows="6" maxlength="3000" required placeholder="Qué hacéis, a quién queréis llegar, qué tenéis en mente…"></textarea>
</label>
<div class="trap" aria-hidden="true"><label>No rellenes este campo <input name="web" type="text" tabindex="-1" autocomplete="off"></label></div>
<label class="check">
<input name="consent" type="checkbox" required>
<span>He leído la <a href="privacidad.html" target="_blank" rel="noopener">política de privacidad</a> y acepto que usen mis datos para responderme.</span>
</label>
<button class="btn primary" type="submit">Enviar propuesta</button>
<p id="st" class="status" role="status" aria-live="polite"></p>
</form>
<noscript><p>Necesitas JavaScript para usar el formulario. Mientras tanto, escríbenos a <a href="mailto:andaquenosabesnada@gmail.com">andaquenosabesnada@gmail.com</a>.</p></noscript>
</main>
<script src="form.js"></script>
</body>
</html>
+3 -3
View File
@@ -28,9 +28,9 @@
<p>Como cualquier servidor web, nuestro servidor puede registrar de forma temporal la dirección IP, la fecha y la página solicitada, únicamente por seguridad y para detectar abusos. No usamos esos registros para identificar a nadie ni los compartimos.</p>
<p>Los enlaces a YouTube, TikTok e Instagram llevan a servicios de terceros, que tienen sus propias políticas de privacidad.</p>
<h2>3. Formulario de contacto</h2>
<p>Si nos escribes desde la página de <a href="contacto.html">contacto</a>, recogemos el nombre, el correo electrónico, el motivo y el mensaje que tú mismo introduces. Los usamos <strong>únicamente para responderte</strong>. La base legal es tu consentimiento, que das al marcar la casilla del formulario.</p>
<p>La web no guarda los mensajes: el formulario los reenvía por correo electrónico, a través de nuestro propio servidor de correo, a la cuenta del programa. Los conservamos solo el tiempo necesario para atender tu consulta y no los cedemos a terceros ni los usamos para otra cosa. El formulario incluye medidas antispam que no identifican a nadie. Puedes pedirnos que borremos tus datos escribiendo al correo de contacto.</p>
<h2>3. Formularios de contacto y de patrocinio</h2>
<p>Si nos escribes desde la página de <a href="contacto.html">contacto</a> o de <a href="patrocinadores.html">patrocinadores</a>, recogemos los datos que tú mismo introduces: nombre, correo electrónico, mensaje y motivo o tipo de colaboración y, en el formulario de patrocinio, también el nombre de la empresa y su web si la indicas. Los usamos <strong>únicamente para responderte</strong>. La base legal es tu consentimiento, que das al marcar la casilla del formulario.</p>
<p>La web no guarda los mensajes: el formulario los reenvía por correo electrónico, a través de nuestro propio servidor de correo, a la cuenta del programa. Los conservamos solo el tiempo necesario para atender tu consulta y no los cedemos a terceros ni los usamos para otra cosa. Los formularios incluyen medidas antispam que no identifican a nadie. Puedes pedirnos que borremos tus datos escribiendo al correo de contacto.</p>
<h2>4. Nuestra aplicación de publicación</h2>
<p>Usamos una aplicación de gestión de redes sociales de código abierto (Postiz), instalada en un servidor propio, para programar y publicar el contenido del programa. La aplicación está pensada para uso interno: <strong>solo la utiliza el propietario de las cuentas</strong> y no está abierta al público.</p>
+27 -8
View File
@@ -21,6 +21,12 @@ MOTIVOS = {
'tema': 'Propongo un tema',
'otro': 'Otro asunto',
}
TIPOS = {
'mencion': 'Anunciarme / que me mencionéis en el programa',
'patrocinio': 'Patrocinar episodios o una temporada',
'colab': 'Colaboración o canje',
'otro': 'Otra propuesta',
}
EMAIL_RE = re.compile(r'^[^@\s]{1,64}@[^@\s]{1,190}\.[^@\s]{2,}$')
MAX_BODY = 16 * 1024
RATE_MAX, RATE_WINDOW = 5, 3600 # 5 mensajes por IP y hora
@@ -42,16 +48,16 @@ def rate_ok(ip):
return True
def send(nombre, email, motivo, mensaje):
def send(subject, reply_to, body):
host, user, pwd = os.environ.get('SMTP_HOST'), os.environ.get('SMTP_USER'), os.environ.get('SMTP_PASS')
if not (host and user and pwd and os.environ.get('MAIL_TO')):
raise RuntimeError('SMTP no configurado')
msg = EmailMessage()
msg['Subject'] = f'[Web AQNSN] {MOTIVOS[motivo]} · {nombre}'
msg['Subject'] = subject
msg['From'] = os.environ.get('MAIL_FROM', user)
msg['To'] = os.environ['MAIL_TO']
msg['Reply-To'] = email
msg.set_content(f'Motivo: {MOTIVOS[motivo]}\nNombre: {nombre}\nEmail: {email}\n\n{mensaje}\n')
msg['Reply-To'] = reply_to
msg.set_content(body)
s = smtplib.SMTP(host, int(os.environ.get('SMTP_PORT', '587')), timeout=20)
try:
s.ehlo()
@@ -83,8 +89,9 @@ class H(BaseHTTPRequestHandler):
self.wfile.write(body)
def do_POST(self):
if self.path != '/api/contacto':
if self.path not in ('/api/contacto', '/api/patrocinio'):
return self.reply(404, {'ok': False})
patro = self.path == '/api/patrocinio'
try:
n = int(self.headers.get('Content-Length', '0'))
if n <= 0 or n > MAX_BODY:
@@ -99,12 +106,24 @@ class H(BaseHTTPRequestHandler):
if not rate_ok(ip):
return self.reply(429, {'ok': False, 'error': 'Demasiados mensajes. Inténtalo más tarde.'})
nombre, email = clean(d.get('nombre'), 100), clean(d.get('email'), 200)
motivo = d.get('motivo') if d.get('motivo') in MOTIVOS else 'otro'
mensaje = str(d.get('mensaje') or '').replace('\r', '').strip()[:3000]
bad = {'ok': False, 'error': 'Revisa los campos del formulario.'}
if not nombre or not EMAIL_RE.match(email) or len(mensaje) < 10 or d.get('consent') is not True:
return self.reply(400, {'ok': False, 'error': 'Revisa los campos del formulario.'})
return self.reply(400, bad)
if patro:
empresa, sitio = clean(d.get('empresa'), 120), clean(d.get('sitio'), 200)
tipo = d.get('tipo') if d.get('tipo') in TIPOS else 'otro'
if not empresa or (sitio and not re.match(r'^https?://', sitio)):
return self.reply(400, bad)
subject = f'[Web AQNSN · Patrocinio] {empresa} · {nombre}'
body = (f'Empresa: {empresa}\nContacto: {nombre}\nEmail: {email}\nWeb: {sitio or "—"}\n'
f'Interés: {TIPOS[tipo]}\n\n{mensaje}\n')
else:
motivo = d.get('motivo') if d.get('motivo') in MOTIVOS else 'otro'
subject = f'[Web AQNSN] {MOTIVOS[motivo]} · {nombre}'
body = f'Motivo: {MOTIVOS[motivo]}\nNombre: {nombre}\nEmail: {email}\n\n{mensaje}\n'
try:
send(nombre, email, motivo, mensaje)
send(subject, email, body)
except RuntimeError:
return self.reply(503, {'ok': False, 'error': 'El formulario no está disponible ahora mismo.'})
except Exception as exc: