Módulo:Cuenta atrás
De MundoConan Wiki
Más acciones
La documentación para este módulo puede ser creada en Módulo:Cuenta atrás/doc
-- Módulo:Cuenta atrás — cuentas atrás de estrenos (portada y artículos).
--
-- Portada: {{#invoke:Cuenta atrás|portada|pagina=Plantilla:Portada/Estrenos}}
-- Lee los bloques {{Estreno ...}} de esa página y muestra el próximo de cada uno.
-- Suelto: {{Cuenta atrás|título=Película 30|fecha=2027-04-16 00:00|serie=película}}
--
-- Parámetros de cada estreno:
-- serie anime | manga | película | especial | otro (icono y color)
-- título texto; «{n}» se sustituye por el número calculado (p. ej. «Episodio {n}»)
-- número número del estreno que cae en «fecha» (opcional)
-- fecha AAAA-MM-DD HH:MM (o solo AAAA-MM-DD)
-- zona Japón (por defecto) | España | UTC
-- repetir semanal | quincenal | no -> si la fecha ya pasó, salta sola a la siguiente
-- saltar fechas sin estreno, separadas por comas (AAAA-MM-DD): pausas, especiales...
-- hasta AAAA-MM-DD: deja de repetir después de esta fecha
-- duración minutos que se muestra «¡En emisión!» tras empezar (por defecto 30)
-- enlace artículo al que lleva el título
-- texto línea extra opcional
-- La cuenta se actualiza en directo en el navegador (MediaWiki:Common.js); sin JavaScript
-- se muestran los días que faltan, calculados al generar la página.
local p = {}
local Icono = require('Módulo:Icono')
local SERIES = {
anime = { 'anime', 'Próximo episodio' },
manga = { 'manga', 'Próximo capítulo' },
['película'] = { 'pelicula', 'Próxima película' },
pelicula = { 'pelicula', 'Próxima película' },
especial = { 'estrella', 'Próximo especial' },
otro = { 'calendario', 'Próximamente' },
}
local DIAS = { 'domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado' }
local MESES = { 'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto',
'septiembre', 'octubre', 'noviembre', 'diciembre' }
local SEMANA = 7 * 86400
local function limpio(v)
v = v and mw.text.trim(v)
return (v ~= '' and v) or nil
end
-- Segundos UTC de una fecha «civil» UTC (sin depender de la zona horaria del servidor)
local function utc(an, mes, dia, h, mi)
local a = an - (mes <= 2 and 1 or 0)
local era = math.floor(a / 400)
local yoe = a - era * 400
local mp = (mes + 9) % 12
local doy = math.floor((153 * mp + 2) / 5) + dia - 1
local doe = yoe * 365 + math.floor(yoe / 4) - math.floor(yoe / 100) + doy
local dias = era * 146097 + doe - 719468
return dias * 86400 + (h or 0) * 3600 + (mi or 0) * 60
end
-- Último domingo de un mes (para el horario de verano europeo)
local function ultimoDomingo(an, mes)
local diasMes = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
local d = diasMes[mes]
local t = utc(an, mes, d)
local wd = tonumber(os.date('!%w', t))
return d - wd
end
-- Desfase de España peninsular (CET/CEST) en un instante UTC
local function desfaseEspana(t)
local an = tonumber(os.date('!%Y', t))
local ini = utc(an, 3, ultimoDomingo(an, 3), 1)
local fin = utc(an, 10, ultimoDomingo(an, 10), 1)
return (t >= ini and t < fin) and 7200 or 3600
end
local function desfase(zona, tLocalAprox)
zona = mw.ustring.lower(zona or 'japón')
if zona == 'utc' then
return 0
elseif zona == 'españa' or zona == 'espana' or zona == 'madrid' then
return desfaseEspana(tLocalAprox - 3600)
end
return 9 * 3600 -- Japón (sin horario de verano)
end
-- «2026-10-24 18:00» en la zona dada -> segundos UTC
local function leerFecha(s, zona)
if not s then
return nil, false
end
local an, mes, dia = s:match('^%s*(%d%d%d%d)%-(%d%d?)%-(%d%d?)')
if not an then
return nil, false
end
local h, mi = s:match('(%d%d?):(%d%d)%s*$')
local conHora = h ~= nil
local t = utc(tonumber(an), tonumber(mes), tonumber(dia), tonumber(h or 0), tonumber(mi or 0))
return t - desfase(zona, t), conHora
end
local function diaClave(t, zona)
return os.date('!%Y-%m-%d', t + desfase(zona, t))
end
local function ahora(frame)
-- Usar CURRENTTIMESTAMP hace que MediaWiki guarde la página en caché como mucho una hora
local ts = frame:preprocess('{{CURRENTTIMESTAMP}}')
local an, mes, dia, h, mi, s = ts:match('^(%d%d%d%d)(%d%d)(%d%d)(%d%d)(%d%d)(%d%d)$')
if not an then
return os.time()
end
return utc(tonumber(an), tonumber(mes), tonumber(dia), tonumber(h), tonumber(mi)) + tonumber(s)
end
-- Calcula el próximo estreno de una entrada: devuelve t (UTC), número, ya empezado (bool)
local function proximo(e, t0, now)
local paso = (e.repetir == 'semanal' and SEMANA) or (e.repetir == 'quincenal' and 2 * SEMANA) or nil
local dur = (tonumber(e['duración'] or e.duracion) or 30) * 60
local num = tonumber(e['número'] or e.numero)
if not paso or t0 + dur > now then
return t0, num, (t0 <= now and now < t0 + dur)
end
local saltar = {}
for f in mw.text.gsplit(e.saltar or '', ',', true) do
f = mw.text.trim(f)
if f ~= '' then
saltar[f] = true
end
end
local hasta = e.hasta and leerFecha(e.hasta, 'utc')
local function valido(x)
return not saltar[diaClave(x, e.zona)]
end
local t = t0
local n = num
local guardas = 0
-- Avanza hasta el primer estreno que no ha terminado y no cae en una fecha saltada;
-- el número solo sube en las semanas con estreno
while (t + dur <= now or not valido(t)) and guardas < 2000 do
t = t + paso
guardas = guardas + 1
if n and valido(t) then
n = n + 1
end
end
if hasta and t > hasta + 86400 then
return nil, n, false
end
return t, n, (t <= now and now < t + dur)
end
local function fechaLegible(t, zona, conHora)
local local_ = t + desfase(zona, t)
local d = os.date('!*t', local_)
local txt = DIAS[d.wday] .. ' ' .. d.day .. ' de ' .. MESES[d.month]
if d.year ~= tonumber(os.date('!%Y')) then
txt = txt .. ' de ' .. d.year
end
if conHora then
local z = mw.ustring.lower(zona or 'japón')
if z == 'españa' or z == 'espana' or z == 'madrid' then
txt = txt .. ' · ' .. os.date('!%H:%M', local_) .. ' (España)'
else
local es = t + desfaseEspana(t)
txt = txt .. ' · ' .. os.date('!%H:%M', local_) .. (z == 'utc' and ' UTC' or ' en Japón')
.. ' · ' .. os.date('!%H:%M', es) .. ' en España'
end
end
return txt
end
local function tarjeta(e, now)
local serie = mw.ustring.lower(e.serie or 'otro')
local info = SERIES[serie] or SERIES.otro
local t0, conHora = leerFecha(e.fecha, e.zona)
local caja = mw.html.create('div'):addClass('mc-ca mc-ca--' .. (SERIES[serie] and serie:gsub('í', 'i') or 'otro'))
caja:tag('div'):addClass('mc-ca__serie'):wikitext(Icono.html(info[1], { estilo = 'plano', tam = 'p' }) .. ' ' .. (e.etiqueta or info[2]))
local t, num, enCurso
if t0 then
t, num, enCurso = proximo(e, t0, now)
end
local titulo = e['título'] or e.titulo or ''
if not num and titulo:find('{n}', 1, true) then
-- Sin número: «Episodio {n}» -> «Nuevo episodio»
titulo = 'Nuevo ' .. mw.ustring.lower(mw.text.trim((titulo:gsub('{n}', ''))))
end
titulo = titulo:gsub('{n}', num and tostring(num) or '')
titulo = mw.text.trim(titulo:gsub('%s+', ' '))
if titulo == '' then
titulo = e.enlace or 'Por anunciar'
end
if e.enlace then
titulo = '[[' .. e.enlace .. '|' .. titulo .. ']]'
end
caja:tag('div'):addClass('mc-ca__titulo'):wikitext(titulo)
local cuenta = caja:tag('div'):addClass('mc-ca__cuenta')
if not t then
cuenta:addClass('mc-ca__cuenta--sin'):wikitext('Fecha por anunciar')
else
caja:attr('data-mc-fecha', os.date('!%Y-%m-%dT%H:%M:%SZ', t))
:attr('data-mc-duracion', tostring((tonumber(e['duración'] or e.duracion) or 30) * 60))
if not conHora then
caja:attr('data-mc-sin-hora', '1')
end
local falta = t - now
if enCurso then
cuenta:addClass('mc-ca__cuenta--ya'):wikitext('¡En emisión!')
elseif falta <= 0 then
cuenta:addClass('mc-ca__cuenta--ya'):wikitext('¡Ya disponible!')
else
local dias = math.floor(falta / 86400)
local horas = math.floor((falta % 86400) / 3600)
if dias >= 1 then
cuenta:wikitext('<b>' .. dias .. '</b> ' .. (dias == 1 and 'día' or 'días')
.. (conHora and (' <b>' .. horas .. '</b> h') or ''))
else
cuenta:wikitext(conHora and ('<b>' .. horas .. '</b> h') or '¡Hoy!')
end
end
caja:tag('div'):addClass('mc-ca__fecha'):wikitext(fechaLegible(t, e.zona, conHora))
end
if e.texto then
caja:tag('div'):addClass('mc-ca__texto'):wikitext(e.texto)
end
return caja, t
end
-- Parte «| a = b | c = [[x|y]]» respetando los [[...]]
local function parametros(cuerpo)
local args, prof, actual = {}, 0, ''
local i = 1
local function guardar(trozo)
local k, v = trozo:match('^%s*([^=]-)%s*=%s*(.-)%s*$')
if k and k ~= '' then
args[mw.ustring.lower(k)] = limpio(v)
end
end
while i <= #cuerpo do
local dos = cuerpo:sub(i, i + 1)
if dos == '[[' then
prof = prof + 1; actual = actual .. dos; i = i + 2
elseif dos == ']]' then
prof = math.max(0, prof - 1); actual = actual .. dos; i = i + 2
else
local c = cuerpo:sub(i, i)
if c == '|' and prof == 0 then
guardar(actual); actual = ''
else
actual = actual .. c
end
i = i + 1
end
end
guardar(actual)
return args
end
local function estilos(frame)
return frame:extensionTag('templatestyles', '', { src = 'Plantilla:Cuenta atrás/styles.css' })
end
function p.portada(frame)
local pagina = frame.args.pagina or 'Plantilla:Portada/Estrenos'
local t = mw.title.new(pagina)
local contenido = t and t:getContent()
if not contenido then
return "''Aún no hay estrenos. Añádelos en [[" .. pagina .. "]].''"
end
contenido = contenido:gsub('<noinclude>.-</noinclude>', ''):gsub('<!%-%-.-%-%->', '')
local now = ahora(frame)
local tarjetas = {}
for cuerpo in contenido:gmatch('{{%s*[Ee]streno%s*(.-)}}') do
local e = parametros(cuerpo)
local caja, cuando = tarjeta(e, now)
table.insert(tarjetas, { caja = caja, t = cuando or math.huge, orden = #tarjetas })
end
if #tarjetas == 0 then
return "''Aún no hay estrenos. Añádelos en [[" .. pagina .. "]].''"
end
table.sort(tarjetas, function(a, b)
if a.t == b.t then
return a.orden < b.orden
end
return a.t < b.t
end)
local fila = mw.html.create('div'):addClass('mc-ca-fila')
for _, x in ipairs(tarjetas) do
fila:node(x.caja)
end
return estilos(frame) .. tostring(fila)
end
-- {{Cuenta atrás|...}} y {{Estreno|...}} sueltos
function p.tarjeta(frame)
local e = {}
for k, v in pairs(frame:getParent().args) do
if type(k) == 'string' then
e[mw.ustring.lower(mw.text.trim(k))] = limpio(v)
end
end
local caja = tarjeta(e, ahora(frame))
return estilos(frame) .. tostring(mw.html.create('div'):addClass('mc-ca-fila mc-ca-fila--suelta'):node(caja))
end
return p