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).""" """Genera index.html a partir del feed RSS del canal (se ejecuta cada 15 min en el contenedor)."""
import html import html
import io
import os import os
import re import re
import unicodedata import unicodedata
@@ -9,6 +10,8 @@ import sys
import urllib.request import urllib.request
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from PIL import Image
OUT = os.environ.get('SITE_DIR', 'public') OUT = os.environ.get('SITE_DIR', 'public')
CHANNEL = 'UC8gVtzB3Lqm7nnfMhMvXGxQ' CHANNEL = 'UC8gVtzB3Lqm7nnfMhMvXGxQ'
# YouTube separa cada tipo en su propia lista: UULF = solo vídeos, UUSH = solo Shorts # 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] 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') img_dir = os.path.join(OUT, 'img')
os.makedirs(img_dir, exist_ok=True) os.makedirs(img_dir, exist_ok=True)
keep = {'avatar.jpg', 'banner.jpg'}
ok = [] ok = []
for v in vids: for v in vids:
path = os.path.join(img_dir, v['id'] + '.jpg') hd, lo = f"{v['id']}-hd.jpg", f"{v['id']}-lo.jpg"
if not os.path.exists(path): if not os.path.exists(os.path.join(img_dir, hd)):
try: data, is_hd = grab(v['id'], kind)
open(path, 'wb').write(fetch(f"https://i.ytimg.com/vi/{v['id']}/hqdefault.jpg")) if data is None:
except Exception as exc: print('miniatura no descargada', v['id'], file=sys.stderr)
print('miniatura no descargada', v['id'], exc, file=sys.stderr)
continue 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) 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 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} okids = {v['id'] for v in ok}
videos = [v for v in videos if v['id'] in okids] videos = [v for v in videos if v['id'] in okids]
shorts = [v for v in shorts 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): def card(v, n, show_views=False):
return f''' return f'''
<a class="card reveal" style="--d:{(n % 3) * 70}ms" href="{url(v)}" target="_blank" rel="noopener"> <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> <span class="meta"><time>{esc(v['date'])}{' · ' + vistas(v) if show_views and v['views'] else ''}</time><strong>{esc(v['title'])}</strong></span>
</a>''' </a>'''
@@ -155,7 +190,7 @@ def card(v, n, show_views=False):
def short_card(v, n, show_views=False): def short_card(v, n, show_views=False):
return f''' return f'''
<a class="scard reveal" style="--d:{(n % 6) * 50}ms" href="https://www.youtube.com/shorts/{v['id']}" target="_blank" rel="noopener"> <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> <span class="meta"><strong>{esc(v['title'])}</strong>{'<time>' + vistas(v) + '</time>' if show_views and v['views'] else ''}</span>
</a>''' </a>'''
@@ -213,7 +248,7 @@ page = f'''<!doctype html>
<section id="destacado" class="featured wrap reveal"> <section id="destacado" class="featured wrap reveal">
<a class="feat" href="{url(featured)}" target="_blank" rel="noopener"> <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="feat-text">
<span class="tag">Último vídeo</span> <span class="tag">Último vídeo</span>
<strong>{esc(featured['title'])}</strong> <strong>{esc(featured['title'])}</strong>
@@ -272,7 +307,7 @@ page = f'''<!doctype html>
<section class="wrap reveal" aria-labelledby="venir"> <section class="wrap reveal" aria-labelledby="venir">
<div class="invite"> <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> <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> </div>
</section> </section>
@@ -289,7 +324,7 @@ page = f'''<!doctype html>
<footer> <footer>
<div class="wrap foot"> <div class="wrap foot">
<p>© 2026 Anda Que No Sabes Ná · Granada</p> <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> </div>
</footer> </footer>
+3 -31
View File
@@ -12,14 +12,14 @@
<body> <body>
<header class="top"> <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> <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> </header>
<main class="legal contact"> <main class="legal contact">
<h1>¿Quieres venir al <em>programa</em>?</h1> <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> <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 <label>Nombre
<input name="nombre" type="text" autocomplete="name" maxlength="100" required> <input name="nombre" type="text" autocomplete="name" maxlength="100" required>
</label> </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> <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> </main>
<script> <script src="form.js"></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>
</body> </body>
</html> </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"> <section id="destacado" class="featured wrap reveal">
<a class="feat" href="https://www.youtube.com/watch?v=_-VaTy2fazY" target="_blank" rel="noopener"> <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="feat-text">
<span class="tag">Último vídeo</span> <span class="tag">Último vídeo</span>
<strong>DOMESTICAR ADOLESCENTES, PLANTAR TOMATES Y CONSTRUIR MINIATURAS | AQNSN Programa 3 - Galaxoide</strong> <strong>DOMESTICAR ADOLESCENTES, PLANTAR TOMATES Y CONSTRUIR MINIATURAS | AQNSN Programa 3 - Galaxoide</strong>
@@ -63,35 +63,35 @@
</div> </div>
<div class="grid"> <div class="grid">
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=7zY2ey_ERlQ" target="_blank" rel="noopener"> <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> <span class="meta"><time>10 oct 2026</time><strong>Un Simple Teletransporte de Cartas...</strong></span>
</a> </a>
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=Q7qYQbvjMxQ" target="_blank" rel="noopener"> <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> <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>
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=mxP0dvn2HD4" target="_blank" rel="noopener"> <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> <span class="meta"><time>6 oct 2026</time><strong>Árabe formal VS Árabe de la calle, ¿qué diferencias hay?</strong></span>
</a> </a>
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=f6UO6jtUgrU" target="_blank" rel="noopener"> <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> <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>
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=A2v_T2CCupo" target="_blank" rel="noopener"> <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> <span class="meta"><time>4 oct 2026</time><strong>¿Cómo es la vida nocturna y callejera en Egipto?</strong></span>
</a> </a>
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=H_yHEe6skW4" target="_blank" rel="noopener"> <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> <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>
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=Ei_VnfM-uLY" target="_blank" rel="noopener"> <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> <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>
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=g614maJ0998" target="_blank" rel="noopener"> <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> <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> </a>
</div> </div>
@@ -104,51 +104,51 @@
</div> </div>
<div class="shorts"> <div class="shorts">
<a class="scard reveal" style="--d:0ms" href="https://www.youtube.com/shorts/PxQHPd5YDLs" target="_blank" rel="noopener"> <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> <span class="meta"><strong>La Tediosa Burocracia en la Educación</strong></span>
</a> </a>
<a class="scard reveal" style="--d:50ms" href="https://www.youtube.com/shorts/rLFSEqe77OY" target="_blank" rel="noopener"> <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> <span class="meta"><strong>¿Cómo dejas de comerte las uñas? El trauma útil de Palín</strong></span>
</a> </a>
<a class="scard reveal" style="--d:100ms" href="https://www.youtube.com/shorts/jmV_Ao85RvI" target="_blank" rel="noopener"> <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> <span class="meta"><strong>¿Cómo de antiguo es el Árabe?</strong></span>
</a> </a>
<a class="scard reveal" style="--d:150ms" href="https://www.youtube.com/shorts/DLcn8i9lV6Q" target="_blank" rel="noopener"> <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> <span class="meta"><strong>Todas las religiones son similares...</strong></span>
</a> </a>
<a class="scard reveal" style="--d:200ms" href="https://www.youtube.com/shorts/E_eULZSlWJs" target="_blank" rel="noopener"> <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> <span class="meta"><strong>La Vida de las Mujeres Egipcias en las zonas costeras</strong></span>
</a> </a>
<a class="scard reveal" style="--d:250ms" href="https://www.youtube.com/shorts/600_XwAHkv0" target="_blank" rel="noopener"> <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> <span class="meta"><strong>Clases religiosas separadas, la política de las escuelas egipcias</strong></span>
</a> </a>
<a class="scard reveal" style="--d:0ms" href="https://www.youtube.com/shorts/XKSVhuk_lQI" target="_blank" rel="noopener"> <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> <span class="meta"><strong>Poligamia en Egipto, de la antigüedad a la actualidad</strong></span>
</a> </a>
<a class="scard reveal" style="--d:50ms" href="https://www.youtube.com/shorts/voK4g7lIv4E" target="_blank" rel="noopener"> <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> <span class="meta"><strong>El Barrio Pijo de Egipto, Masr El Gedida, y sus tiendas de Oro</strong></span>
</a> </a>
<a class="scard reveal" style="--d:100ms" href="https://www.youtube.com/shorts/yzTMyAKQnDQ" target="_blank" rel="noopener"> <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> <span class="meta"><strong>Las Franquicias se extienden en EGIPTO...</strong></span>
</a> </a>
<a class="scard reveal" style="--d:150ms" href="https://www.youtube.com/shorts/DgTa9qX4xH0" target="_blank" rel="noopener"> <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> <span class="meta"><strong>El barrio cristiano del Cairo que RECICLA BASURA</strong></span>
</a> </a>
<a class="scard reveal" style="--d:200ms" href="https://www.youtube.com/shorts/oAoEz2Kwmk8" target="_blank" rel="noopener"> <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> <span class="meta"><strong>Tiendas QUE NO CIERRAN en Egipto 🌇</strong></span>
</a> </a>
<a class="scard reveal" style="--d:250ms" href="https://www.youtube.com/shorts/05m1CMyGyUA" target="_blank" rel="noopener"> <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> <span class="meta"><strong>¿Se parece la comida EGIPCIA a la ESPAÑOLA?</strong></span>
</a> </a>
</div> </div>
@@ -162,46 +162,46 @@
<h3 class="sub reveal">Vídeos</h3> <h3 class="sub reveal">Vídeos</h3>
<div class="grid"> <div class="grid">
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=_-VaTy2fazY" target="_blank" rel="noopener"> <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> <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>
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=A2v_T2CCupo" target="_blank" rel="noopener"> <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> <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>
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=rp9j_Ph9XvI" target="_blank" rel="noopener"> <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> <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>
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=mxP0dvn2HD4" target="_blank" rel="noopener"> <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> <span class="meta"><time>6 oct 2026 · 195 vistas</time><strong>Árabe formal VS Árabe de la calle, ¿qué diferencias hay?</strong></span>
</a> </a>
</div> </div>
<h3 class="sub reveal">Shorts</h3> <h3 class="sub reveal">Shorts</h3>
<div class="shorts"> <div class="shorts">
<a class="scard reveal" style="--d:0ms" href="https://www.youtube.com/shorts/zJ_x4iA8OSs" target="_blank" rel="noopener"> <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> <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>
<a class="scard reveal" style="--d:50ms" href="https://www.youtube.com/shorts/nvF7P_S081o" target="_blank" rel="noopener"> <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> <span class="meta"><strong>😌 &quot;Solo vi a una persona morir sonriendo...&quot;</strong><time>1.545 vistas</time></span>
</a> </a>
<a class="scard reveal" style="--d:100ms" href="https://www.youtube.com/shorts/Wfq5vjJ6sQU" target="_blank" rel="noopener"> <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> <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>
<a class="scard reveal" style="--d:150ms" href="https://www.youtube.com/shorts/XKSVhuk_lQI" target="_blank" rel="noopener"> <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> <span class="meta"><strong>Poligamia en Egipto, de la antigüedad a la actualidad</strong><time>1.207 vistas</time></span>
</a> </a>
<a class="scard reveal" style="--d:200ms" href="https://www.youtube.com/shorts/p5trUANcq_c" target="_blank" rel="noopener"> <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> <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>
<a class="scard reveal" style="--d:250ms" href="https://www.youtube.com/shorts/7PRSukQTCwI" target="_blank" rel="noopener"> <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> <span class="meta"><strong>¿Cómo puede meterte miedo un Tanatopractor? 😖</strong><time>886 vistas</time></span>
</a> </a>
</div> </div>
@@ -225,7 +225,7 @@
<section class="wrap reveal" aria-labelledby="venir"> <section class="wrap reveal" aria-labelledby="venir">
<div class="invite"> <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> <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> </div>
</section> </section>
@@ -242,7 +242,7 @@
<footer> <footer>
<div class="wrap foot"> <div class="wrap foot">
<p>© 2026 Anda Que No Sabes Ná · Granada</p> <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> </div>
</footer> </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>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> <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> <h2>3. Formularios de contacto y de patrocinio</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>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. El formulario incluye medidas antispam que no identifican a nadie. Puedes pedirnos que borremos tus datos escribiendo al correo de contacto.</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> <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> <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', 'tema': 'Propongo un tema',
'otro': 'Otro asunto', '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,}$') EMAIL_RE = re.compile(r'^[^@\s]{1,64}@[^@\s]{1,190}\.[^@\s]{2,}$')
MAX_BODY = 16 * 1024 MAX_BODY = 16 * 1024
RATE_MAX, RATE_WINDOW = 5, 3600 # 5 mensajes por IP y hora RATE_MAX, RATE_WINDOW = 5, 3600 # 5 mensajes por IP y hora
@@ -42,16 +48,16 @@ def rate_ok(ip):
return True 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') 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')): if not (host and user and pwd and os.environ.get('MAIL_TO')):
raise RuntimeError('SMTP no configurado') raise RuntimeError('SMTP no configurado')
msg = EmailMessage() msg = EmailMessage()
msg['Subject'] = f'[Web AQNSN] {MOTIVOS[motivo]} · {nombre}' msg['Subject'] = subject
msg['From'] = os.environ.get('MAIL_FROM', user) msg['From'] = os.environ.get('MAIL_FROM', user)
msg['To'] = os.environ['MAIL_TO'] msg['To'] = os.environ['MAIL_TO']
msg['Reply-To'] = email msg['Reply-To'] = reply_to
msg.set_content(f'Motivo: {MOTIVOS[motivo]}\nNombre: {nombre}\nEmail: {email}\n\n{mensaje}\n') msg.set_content(body)
s = smtplib.SMTP(host, int(os.environ.get('SMTP_PORT', '587')), timeout=20) s = smtplib.SMTP(host, int(os.environ.get('SMTP_PORT', '587')), timeout=20)
try: try:
s.ehlo() s.ehlo()
@@ -83,8 +89,9 @@ class H(BaseHTTPRequestHandler):
self.wfile.write(body) self.wfile.write(body)
def do_POST(self): def do_POST(self):
if self.path != '/api/contacto': if self.path not in ('/api/contacto', '/api/patrocinio'):
return self.reply(404, {'ok': False}) return self.reply(404, {'ok': False})
patro = self.path == '/api/patrocinio'
try: try:
n = int(self.headers.get('Content-Length', '0')) n = int(self.headers.get('Content-Length', '0'))
if n <= 0 or n > MAX_BODY: if n <= 0 or n > MAX_BODY:
@@ -99,12 +106,24 @@ class H(BaseHTTPRequestHandler):
if not rate_ok(ip): if not rate_ok(ip):
return self.reply(429, {'ok': False, 'error': 'Demasiados mensajes. Inténtalo más tarde.'}) 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) 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] 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: 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: try:
send(nombre, email, motivo, mensaje) send(subject, email, body)
except RuntimeError: except RuntimeError:
return self.reply(503, {'ok': False, 'error': 'El formulario no está disponible ahora mismo.'}) return self.reply(503, {'ok': False, 'error': 'El formulario no está disponible ahora mismo.'})
except Exception as exc: except Exception as exc: