Jump to content
Menú alternativo
Alternar el menú de preferencias
Menú alternativo personal
No has accedido
Tu dirección IP será visible si haces alguna edición

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