-- Ported from anyfeed-server's eldenring-patch-notes.lua. That version
-- fetched each patch's own article page for its full body/description/
-- thumbnail; luafeed's method scripts only get the one page Rust already
-- fetched, with no way to fetch a second page, so this port only ever sees
-- what's on the listing page itself: title, link, and publish date (all
-- present directly on the listing card). `description` is left unset
-- rather than filled with a listing-page fragment.
--
-- site{
-- name = "eldenring-patch-notes",
-- url = "https://en.bandainamcoent.eu/elden-ring/elden-ring/news",
-- method = "eldenring-patch-notes",
-- title = "Elden Ring Patch Notes",
-- }
--
-- The page is sectioned by anchored
titles ("Patch Notes (27)"), each
-- followed by a ; the patch-notes cards link to
-- /elden-ring/news/elden-ring-patch-notes-version- article pages.
-- Entries beyond the first handful load via client-side infinite scroll,
-- which a plain fetch doesn't run, so only the latest patches are ever
-- seen -- the right behavior for a feed tracker, since older patches were
-- already recorded by earlier polls. Cards are recognized by their link
-- slug rather than their position in a particular section, which survives
-- markup reshuffles.
local function origin_of(url)
return url:match("^(https?://[^/]+)")
end
local function resolve_link(origin, href)
if href:match("^https?://") then
return href
end
return origin .. href
end
-- The stable id: the version tail of the article slug.
local function patch_id(href)
return href and href:match("patch%-notes%-version%-(.+)$")
end
local function has_class(node, name)
local class = node:attr("class")
if not class then
return false
end
for token in class:gmatch("%S+") do
if token == name then
return true
end
end
return false
end
local function find_by_class(node, tag, class_name)
local matches = {}
for _, el in ipairs(node:find_all(tag)) do
if has_class(el, class_name) then
table.insert(matches, el)
end
end
return matches
end
-- os.time() interprets a field table as the host's *local* time, and Lua
-- has no timezone-aware alternative in the standard library -- this
-- converts a UTC field table to the right epoch by computing the host's
-- current UTC offset and applying it, rather than assuming the host runs
-- in UTC. (Not exact across a DST transition between now and the target
-- date, an acceptable approximation for ordering feed items.)
local function utc_time(fields)
local now = os.time()
local isdst = os.date("*t", now).isdst
local utc_now = os.date("!*t", now)
utc_now.isdst = isdst
local offset = now - os.time(utc_now)
fields.isdst = isdst
return os.time(fields) + offset
end
local function rfc822_midnight(date_part)
local year, month, day = date_part:match("^(%d%d%d%d)-(%d%d)-(%d%d)$")
if not year then
return nil
end
local timestamp = utc_time({ year = tonumber(year), month = tonumber(month), day = tonumber(day), hour = 0 })
return os.date("!%a, %d %b %Y %H:%M:%S GMT", timestamp)
end
local function build_item(origin, card, seen)
local href = card:attr("href") or ""
local id = patch_id(href)
-- A duplicate card for the same patch elsewhere on the page (the latest
-- patch also appears in the main news list) -- skip it.
if not id or seen[id] then
return nil
end
seen[id] = true
local title_el = find_by_class(card, "h3", "card__title")[1]
if not title_el then
return nil
end
local time_el = card:find_all("time")[1]
local date_part = time_el and (time_el:attr("datetime") or ""):match("^(%d%d%d%d%-%d%d%-%d%d)")
return {
title = title_el:text(),
link = resolve_link(origin, href),
guid = id,
pub_date = date_part and rfc822_midnight(date_part),
}
end
local root, url = ...
local origin = origin_of(url)
local lists = find_by_class(root, "ul", "cards-list")
local items = {}
local seen = {}
for _, list in ipairs(lists) do
for _, card in ipairs(list:find_all("a")) do
local item = build_item(origin, card, seen)
if item then
table.insert(items, item)
end
end
end
return items