Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:Screencap

From Battlestar Wiki Media

Overview

Module:Screencap is the Lua backend for Template:Screencap, the unified image tag for BattlestarWiki Media. It replaces the fragmented family of legacy sub-templates (

<img src="//media.battlestarwiki.org/w/index.php?title=Special:Redirect/file/BSG_WIKI_Screencap.png" alt="Unknown Source" width="30" height="30" style="vertical-align:middle">   screen capture (source unknown)
The an unknown series, Season "Unknown" from an unknown episode. Please help us identify the episode by updating this tag or discussing it on the [[{{TALKPAGENAME}}|talk page]].
This capture is copyright by the copyright holder. Use of it here is believed to be fair use.
The timestamp for this image is unknown. Please supply the time index for this capture.
Screen capture (source unknown). Native resolution: unknown.

,

<img src="//media.battlestarwiki.org/w/index.php?title=Special:Redirect/file/BSG_WIKI_Bluray.png" alt="Blu-ray" width="30" height="30" style="vertical-align:middle">   Blu-ray screen capture
The original Battlestar Galactica, Season "Unknown" from an unknown episode. Please help us identify the episode by updating this tag or discussing it on the [[{{TALKPAGENAME}}|talk page]].
This capture is copyright by Universal Studios. Use of it here is believed to be fair use.
The timestamp for this image is unknown. Please supply the time index for this capture.
Blu-ray screen capture. Native resolution: 1920×1080.

,

<img src="//media.battlestarwiki.org/w/index.php?title=Special:Redirect/file/BSG_WIKI_HDTV.png" alt="HDTV" width="30" height="30" style="vertical-align:middle">   HD broadcast screen capture
The Caprica, Season "Unknown" from an unknown episode. Please help us identify the episode by updating this tag or discussing it on the [[{{TALKPAGENAME}}|talk page]].
This capture is copyright by Universal Studios / Syfy. Use of it here is believed to be fair use.
The timestamp for this image is unknown. Please supply the time index for this capture.
HD broadcast screen capture. Native resolution: 1280×720 or 1920×1080.

, etc.) with a single module that auto-detects as much metadata as possible from the file itself, while allowing any detected value to be overridden via named template parameters.

The module does four things:

  1. Detects the series from filename prefix patterns
  2. Detects the season and episode from filename patterns (SxxExx, NxNN, or prose title hint)
  3. Detects the source type / quality tier from the file's pixel dimensions
  4. Renders a collapsible infobox and applies maintenance categories

Entry Point

The module exposes one public function:

Function Called by Description
p.main(frame) {{#invoke:Screencap|main}} in Template:Screencap Full pipeline: detect → render infobox → emit categories

Series Detection

Series is detected by matching the start of the filename against a prefix table. Matching is case-sensitive on the prefix but tolerates spaces or underscores as separators (e.g. RDM -, RDM_).

Filename starts with Detected series key
RDM -, TRS -, BSG - S##, BSG - #x## rdm
CAP -, Caprica - cap
B&C -, BNC - bnc
TOS - tos
1980 -, G1980 - 1980
(no match) unknown

Each series key maps to a SERIES_META entry containing the human-readable name, category abbreviation, wiki article links, and copyright holder used in the infobox.

To add a new series, add a new entry to both SERIES_PREFIXES and SERIES_META in the module.

Episode & Season Detection

The module attempts to extract season and episode numbers from the filename in this priority order:

Pattern Example filename Result
SxxExx (standard) RDM - S04E20 - Daybreak.jpg Season 4, Episode 20
NxNN (alternate) BSG - 1x05 - Act of Contrition.jpg Season 1, Episode 5
Prose title hint TOS - The Lost Warrior - Apollo.jpg Title hint: "The Lost Warrior - Apollo"
(no match) Adamabed.jpg No season, no episode, no hint

When only a title hint is extracted (no season/episode numbers), the infobox displays the hint with a message asking editors to verify and supply the full episode name. When nothing is extracted at all, the infobox flags the image as unidentified and links to the talk page.

Note: the |episode= parameter is not auto-detected from the filename — it must be explicitly supplied if you want a linked episode name in the infobox. The filename extraction only produces season/episode numbers or a hint string.

Source Type Detection

When no |type= override is supplied, the module reads the file's pixel dimensions via mw.title.getCurrentTitle().file and classifies the source using the longest pixel axis (so portrait-orientation promotional photographs are handled correctly alongside landscape screencaps).

Longest pixel dimension Detected type Label shown
≥ 3840 px 4k 4K Ultra HD screen capture
≥ 1920 px bluray Blu-ray screen capture
≥ 1280 px hdtv HD broadcast screen capture
≥ 853 px ntscdvd NTSC DVD screen capture
≥ 720 px paldvd PAL DVD screen capture
< 720 px dvd DVD screen capture
dimensions unavailable unknown screen capture (source unknown)

Dimensions are unavailable when the module runs outside a File: page context (e.g. during template sandbox testing), in which case it falls back gracefully to unknown rather than erroring.

All valid type keys that can be used as overrides:

Key Description
4k 4K UHD Blu-ray (≥ 3840px)
bluray Blu-ray / 1080p stream
hdtv HD broadcast or download
hddvd HD DVD (manual override only — not auto-detected)
itunes iTunes / digital download (manual override only)
ntscdvd NTSC DVD
ntscb NTSC broadcast (manual override only)
paldvd PAL DVD
palb PAL broadcast (manual override only)
promo Promotional photograph (manual override only)
dvd Generic / low-res DVD
unknown Source unknown

hddvd, itunes, ntscb, palb, and promo cannot be auto-detected from dimensions alone and must always be set via |type=.

Timestamp Handling

The |timestamp= parameter accepts three input formats:

Input Interpretation Displayed as
427 Bare integer → total seconds 00:07:07
3662 Bare integer → total seconds 01:01:02
00:07:07 Already HH:MM:SS 00:07:07
0h20m42s Non-numeric string → passed through 0h20m42s

Any input that does not parse as a pure number via tonumber() is passed through as-is, so existing timestamps in any format continue to work without changes.

Infobox Output

The rendered infobox is a standard MediaWiki wikitable with class wikitable mw-collapsible screencap-infobox. Rows are emitted in this order:

  1. Header — source type icon + label (collapsible toggle)
  2. Series / episode identification line with language bar links
  3. Copyright notice (links to Wikipedia)
  4. Timestamp (or missing-timestamp notice)
  5. Source description and native resolution
  6. Detected pixel dimensions (if available)
  7. Scaling note (if |scaled=yes)
  8. Cropping note (if |cropped=yes)
  9. Aspect ratio confirmation (if |aspect=yes)

Cross-wiki icon rendering

Source type icons are referenced using standard [[File:Name|30px|link=]] wikitext. The language wikis (de., fr., etc.) serve these files via ForeignAPIRepo from media.battlestarwiki.org, so the icons render correctly everywhere without any special URL construction.

Category Output

Categories are appended after the infobox and applied automatically — no manual [[Category:...]] tags are needed on file pages.

For a Blu-ray RDM image of "Daybreak" from Season 4, the following categories would be applied:

  • [[Category:Screen captures]]
  • [[Category:Screen captures (TRS)]]
  • [[Category:Blu-ray screen captures (TRS)]]
  • [[Category:Screen captures by season 4 (TRS)]]
  • [[Category:Screen captures (Daybreak)]]
  • [[Category:Blu-ray screen captures (Daybreak)]]

Additional maintenance categories are applied automatically based on conditions:

Condition Maintenance category added
type is unknown Screen captures requiring source identification
type is ntscdvd, paldvd, ntscb, or palb Screen captures requiring upgrade

Extending the Module

Adding a new series

Add one or more entries to SERIES_PREFIXES (for filename detection) and a corresponding entry to SERIES_META (for display metadata):

<syntaxhighlight lang="lua"> -- In SERIES_PREFIXES: { pattern = "^BLOOD%s*[-_]%s*", series = "bnc" },

-- In SERIES_META: bnc = {

   name      = "Battlestar Galactica: Blood & Chrome",
   abbr      = "BNC",
   enlink    = "Battlestar Galactica: Blood & Chrome",
   copyright = "Universal Studios / Syfy",

}, </syntaxhighlight>

Adding a new source type

Add an entry to TYPE_LABELS and a corresponding entry to the iconMap inside renderInfobox:

<syntaxhighlight lang="lua"> -- In TYPE_LABELS: webdl = { short = "Web DL", long = "web download screen capture", badge = "Web DL", res = "variable" },

-- In iconMap inside renderInfobox: webdl = "File:BSG WIKI WebDL.png", </syntaxhighlight>

See Also


-- ============================================================
-- Module:Screencap
-- BattlestarWiki Media — Unified Screen Capture / Image Tag
--
-- Auto-detects series, episode, season, source type and quality
-- from the current file's name and its pixel dimensions, then
-- formats the standard infobox + maintenance categories.
--
-- All auto-detected values are overridable via template params.
-- ============================================================

local p = {}

-- ----------------------------------------------------------------
-- SERIES DETECTION TABLE
-- Maps filename prefix patterns → canonical series keys
-- Extend this table as new series/spin-offs are added.
-- ----------------------------------------------------------------
local SERIES_PREFIXES = {
    -- Re-imagined Series (RDM / TRS)
    { pattern = "^RDM%s*[-_]%s*",        series = "rdm"  },
    { pattern = "^TRS%s*[-_]%s*",        series = "rdm"  },
    { pattern = "^BSG%s*[-_]%s*S%d",     series = "rdm"  },
    { pattern = "^BSG%s*[-_]%s*%d+x%d+", series = "rdm"  },
    -- Caprica
    { pattern = "^CAP%s*[-_]%s*",        series = "cap"  },
    { pattern = "^Caprica%s*[-_]%s*",    series = "cap"  },
    -- Blood & Chrome
    { pattern = "^B&C%s*[-_]%s*",        series = "bnc"  },
    { pattern = "^BNC%s*[-_]%s*",        series = "bnc"  },
    -- Original Series (TOS)
    { pattern = "^TOS%s*[-_]%s*",        series = "tos"  },
    -- Galactica 1980
    { pattern = "^1980%s*[-_]%s*",       series = "1980" },
    { pattern = "^G1980%s*[-_]%s*",      series = "1980" },
}

-- Human-readable series names and wiki links
local SERIES_META = {
    rdm  = {
        name    = "Re-imagined ''Battlestar Galactica''",
        abbr    = "TRS",
        enlink  = "Battlestar Galactica (RDM)",
        delink  = "Battlestar Galactica (RDM)",
        frlink  = "Battlestar Galactica (LSR)",
        copyright = "Universal Studios",
    },
    cap  = {
        name    = "''Caprica''",
        abbr    = "CAP",
        enlink  = "Caprica",
        delink  = "Caprica",
        frlink  = "Caprica",
        copyright = "Universal Studios / Syfy",
    },
    bnc  = {
        name    = "''Battlestar Galactica: Blood &amp; Chrome''",
        abbr    = "BNC",
        enlink  = "Battlestar Galactica: Blood & Chrome",
        delink  = "Battlestar Galactica: Blood & Chrome",
        frlink  = "Battlestar Galactica: Blood & Chrome",
        copyright = "Universal Studios / Syfy",
    },
    tos  = {
        name    = "original ''Battlestar Galactica''",
        abbr    = "TOS",
        enlink  = "Battlestar Galactica (TOS)",
        delink  = "Battlestar Galactica (TOS)",
        frlink  = "Battlestar Galactica (TOS)",
        copyright = "Universal Studios",
    },
    ["1980"] = {
        name    = "''Galactica 1980''",
        abbr    = "1980",
        enlink  = "Galactica 1980",
        delink  = "Galactica 1980",
        frlink  = "Galactica 1980",
        copyright = "Universal Studios",
    },
}

-- ----------------------------------------------------------------
-- SOURCE TYPE (quality tier) DETECTION FROM DIMENSIONS
--
-- Thresholds (width-based, landscape assumed):
--   ≥3840  → 4k      (UHD Blu-ray)
--   ≥1920  → bluray  (1080p Blu-ray / stream)
--   ≥1280  → hdtv    (720p HD broadcast / download)
--   ≥853   → dvd     (NTSC anamorphic widescreen upscaled)
--   ≥720   → ntscdvd (native NTSC DVD)
--   ≥1024  → paldvd  (native PAL DVD upscaled)
--   other  → unknown
--
-- Portrait images (height > width) use the longer dimension.
-- ----------------------------------------------------------------
local function detectTypeFromDimensions(width, height)
    if not width or not height then return "unknown" end
    local w = tonumber(width)
    local h = tonumber(height)
    if not w or not h then return "unknown" end
    -- Use longer axis so portrait promo shots also work
    local long = math.max(w, h)
    if long >= 3840 then return "4k"      end
    if long >= 1920 then return "bluray"  end
    if long >= 1280 then return "hdtv"    end
    if long >= 853  then return "ntscdvd" end  -- NTSC anamorphic ≈ 853×480
    if long >= 720  then return "paldvd"  end  -- PAL 720×576
    return "dvd"
end

-- ----------------------------------------------------------------
-- EPISODE / SEASON EXTRACTION FROM FILENAME
--
-- Supported patterns (case-insensitive):
--   SxxExx  e.g. S01E05, S04E20          → standard
--   xxXxx   e.g. 1x05, 4x20              → alternate
--   BSG-era prefix then episode title    → title after prefix
-- ----------------------------------------------------------------
local function extractEpisodeInfo(filename)
    if not filename then return nil, nil, nil end

    -- Remove file extension
    local name = filename:gsub("%.[^%.]+$", "")

    -- SxxExx pattern
    local s, e = name:match("[Ss](%d+)[Ee](%d+)")
    if s and e then
        return tonumber(s), tonumber(e), nil
    end

    -- NxNN or NN×NN pattern
    s, e = name:match("(%d+)[xX](%d+)")
    if s and e then
        return tonumber(s), tonumber(e), nil
    end

    -- Attempt to extract a prose episode title after known prefixes
    -- e.g. "RDM - 33 - Adama helmet.jpg" → episode hint "33"
    -- Returns nil for season/ep numbers but a title hint
    local titleHint = name:match("^[A-Z0-9&]+%s*[-_]%s*(.+)$")
    if titleHint then
        -- Strip trailing frame number pattern like "- 042" or "- Frame042"
        titleHint = titleHint:gsub("%s*[-_]%s*[Ff]rame%s*%d+$", "")
        titleHint = titleHint:gsub("%s*[-_]%s*%d+$", "")
        -- Trim
        titleHint = titleHint:match("^%s*(.-)%s*$")
    end

    return nil, nil, titleHint
end

-- ----------------------------------------------------------------
-- SERIES DETECTION FROM FILENAME
-- ----------------------------------------------------------------
local function detectSeriesFromFilename(filename)
    if not filename then return nil end
    for _, entry in ipairs(SERIES_PREFIXES) do
        if filename:match(entry.pattern) then
            return entry.series
        end
    end
    return nil
end

-- ----------------------------------------------------------------
-- SOURCE TYPE LABELS
-- ----------------------------------------------------------------
local TYPE_LABELS = {
    ["4k"]      = { short = "4K UHD",         long = "4K Ultra HD screen capture",          badge = "4K UHD Blu-ray",   res = "3840×2160 (or equivalent 4K source)" },
    bluray      = { short = "Blu-ray",         long = "Blu-ray screen capture",              badge = "Blu-ray",          res = "1920×1080" },
    hdtv        = { short = "HDTV",            long = "HD broadcast screen capture",         badge = "HDTV",             res = "1280×720 or 1920×1080" },
    hddvd       = { short = "HD DVD",          long = "HD DVD screen capture",               badge = "HD DVD",           res = "1920×1080" },
    itunes      = { short = "iTunes/Download", long = "digital download screen capture",     badge = "iTunes/Download",  res = "variable" },
    ntscdvd     = { short = "NTSC DVD",        long = "NTSC DVD screen capture",             badge = "NTSC DVD",         res = "720×480 (anamorphic widescreen → 853×480)" },
    ntscb       = { short = "NTSC Broadcast",  long = "NTSC broadcast screen capture",       badge = "NTSC Broadcast",   res = "720×480 (fullscreen)" },
    paldvd      = { short = "PAL DVD",         long = "PAL DVD screen capture",              badge = "PAL DVD",          res = "720×576 (anamorphic widescreen → 1024×576)" },
    palb        = { short = "PAL Broadcast",   long = "PAL broadcast screen capture",        badge = "PAL Broadcast",    res = "720×576" },
    promo       = { short = "Promotional",     long = "promotional photograph",              badge = "Promo Photo",      res = "variable (original photography)" },
    dvd         = { short = "DVD",             long = "DVD screen capture",                  badge = "DVD",              res = "variable" },
    unknown     = { short = "Unknown",         long = "screen capture (source unknown)",     badge = "Unknown Source",   res = "unknown" },
}

-- ----------------------------------------------------------------
-- CATEGORY BUILDER
-- ----------------------------------------------------------------
local function buildCategories(series, typeLower, season, episode)
    local cats = {}
    local seriesMeta = SERIES_META[series]
    local abbr = seriesMeta and seriesMeta.abbr or (series and series:upper() or "")
    local typeLabel = TYPE_LABELS[typeLower]
    local typeShort = typeLabel and typeLabel.short or typeLower

    -- Always add base category
    table.insert(cats, "[[Category:Screen captures]]")

    if abbr ~= "" then
        table.insert(cats, string.format("[[Category:Screen captures (%s)]]", abbr))
        table.insert(cats, string.format("[[Category:%s screen captures (%s)]]", typeShort, abbr))
    end

    if season and abbr ~= "" then
        table.insert(cats, string.format("[[Category:Screen captures by season %s (%s)]]", season, abbr))
    end

    if episode then
        table.insert(cats, string.format("[[Category:Screen captures (%s)]]", episode))
        table.insert(cats, string.format("[[Category:%s screen captures (%s)]]", typeShort, episode))
    end

    -- Quality maintenance categories
    if typeLower == "unknown" then
        table.insert(cats, "[[Category:Screen captures requiring source identification]]")
    end
    if typeLower == "ntscdvd" or typeLower == "paldvd" or typeLower == "ntscb" or typeLower == "palb" then
        table.insert(cats, "[[Category:Screen captures requiring upgrade]]")
    end

    return table.concat(cats, "\n")
end

-- ----------------------------------------------------------------
-- LANGUAGE BAR BUILDER
-- ----------------------------------------------------------------
local function buildLangBar(args, seriesMeta, episode, cepisode)
    if not seriesMeta then return "" end
    local parts = {}

    local function addLang(langCode, wikiBase, epName, cepName, label)
        if epName and epName ~= "" then
            local displayName = (cepName and cepName ~= "") and cepName or epName
            local link = string.format("[%s/%s %s]", wikiBase, epName:gsub(" ", "_"), label)
            table.insert(parts, link)
        end
    end

    -- English always present
    if episode and episode ~= "" then
        local enDisplay = (cepisode and cepisode ~= "") and cepisode or episode
        table.insert(parts, string.format("[[%s|en]]", seriesMeta.enlink))
    end

    local langs = {
        { code = "de", base = "https://de.battlestarwiki.org",           label = "de", argEp = args.de, argCep = args.cde },
        { code = "es", base = "https://es.battlestarwiki.org",           label = "es", argEp = args.es, argCep = args.ces },
        { code = "fr", base = "https://fr.battlestarwiki.ddns.net/wiki", label = "fr", argEp = args.fr, argCep = args.cfr },
        { code = "tr", base = "https://tr.battlestarwiki.org",           label = "tr", argEp = args.tr, argCep = args.ctr },
        { code = "zh", base = "http://zh.battlestarwiki.org/wiki",       label = "zh", argEp = args.zh, argCep = args.czh },
    }
    for _, lang in ipairs(langs) do
        if lang.argEp and lang.argEp ~= "" then
            local displayName = (lang.argCep and lang.argCep ~= "") and lang.argCep or lang.argEp
            table.insert(parts, string.format("[%s/%s %s]",
                lang.base, lang.argEp:gsub(" ", "_"), lang.label))
        end
    end

    if #parts == 0 then return "" end
    return "(" .. table.concat(parts, ") (") .. ")"
end

-- ----------------------------------------------------------------
-- INFOBOX RENDERER
-- ----------------------------------------------------------------
local function renderInfobox(data)
    local rows = {}

    local function row(content)
        table.insert(rows, string.format("|-\n| %s", content))
    end

    -- Header row with badge icon and type label
    local typeInfo = TYPE_LABELS[data.typeLower] or TYPE_LABELS["unknown"]

    -- Map type to full absolute icon URLs using media.battlestarwiki.org's
    -- Special:Redirect/file endpoint. This bypasses the MD5-hashed /w/images/
    -- directory structure and works correctly when the infobox is transcluded
    -- to foreign wikis (de., fr., etc.) — those wikis don't have these files
    -- locally so mw.title and relative paths both fail there.
    -- Protocol-relative (//) so it works over both http and https.
    local BASE = "//media.battlestarwiki.org/w/index.php?title=Special:Redirect/file/"
    local iconMap = {
        ["4k"]    = BASE .. "BSG_WIKI_4K.png",
        bluray    = BASE .. "BSG_WIKI_Bluray.png",
        hdtv      = BASE .. "BSG_WIKI_HDTV.png",
        hddvd     = BASE .. "BSG_WIKI_HDDVD.png",
        itunes    = BASE .. "ITunes_Icon.png",
        ntscdvd   = BASE .. "BSG_WIKI_Dvd.png",
        ntscb     = BASE .. "BSG_WIKI_Screencap.png",
        paldvd    = BASE .. "BSG_WIKI_Dvd.png",
        palb      = BASE .. "BSG_WIKI_Screencap.png",
        promo     = BASE .. "BSG_WIKI_Promo.png",
        dvd       = BASE .. "BSG_WIKI_Dvd.png",
        unknown   = BASE .. "BSG_WIKI_Screencap.png",
    }
    local iconUrl = iconMap[data.typeLower] or iconMap["unknown"]
    local iconWikitext = string.format(
        '<img src="%s" alt="%s" width="30" height="30" style="vertical-align:middle">',
        iconUrl, typeInfo.badge
    )

    -- Main header
    local header = string.format(
        '{| class="wikitable mw-collapsible screencap-infobox" style="width:100%%;font-size:90%%"\n' ..
        '! colspan="2" style="background:#1a1a2e;color:#c8a951;text-align:left" |' ..
        ' %s &nbsp; \'\'\'%s\'\'\'',
        iconWikitext, typeInfo.long
    )
    table.insert(rows, header)

    -- Series / episode identification row
    local seriesMeta = SERIES_META[data.series]
    local seriesName = seriesMeta and seriesMeta.name or "an unknown series"
    local copyright  = seriesMeta and seriesMeta.copyright or "the copyright holder"

    local langBar = buildLangBar(data.args, seriesMeta, data.episode, data.cepisode)

    local episodeText
    if data.episode and data.episode ~= "" then
        local epDisplay = (data.cepisode and data.cepisode ~= "") and data.cepisode or data.episode
        episodeText = string.format(
            "The %s %s, Season %s %s episode \"[[%s|%s]]\".",
            seriesName,
            langBar,
            data.season or "''Unknown''",
            langBar ~= "" and "" or "",
            data.episode,
            epDisplay
        )
    elseif data.episodeTitleHint and data.episodeTitleHint ~= "" then
        episodeText = string.format(
            "The %s. Episode identified from filename as \"''%s''\" — " ..
            "please verify and supply the full episode name.",
            seriesName, data.episodeTitleHint
        )
    else
        episodeText = string.format(
            "The %s, Season \"''Unknown''\" from an unknown episode. " ..
            "Please help us identify the episode by updating this tag or discussing it on the [[{{TALKPAGENAME}}|talk page]].",
            seriesName
        )
    end
    row(episodeText)

    -- Copyright notice
    row(string.format(
        "This capture is copyright by [http://www.wikipedia.org/wiki/%s %s]. " ..
        "Use of it here is believed to be [http://www.wikipedia.org/wiki/fair_use fair use].",
        copyright:gsub(" ", "_"), copyright
    ))

    -- Timestamp row
    if data.timestamp and data.timestamp ~= "" then
        row(string.format("Timestamp: <code>%s</code> (HH:MM:SS)", data.timestamp))
    else
        row("The timestamp for this image is unknown. Please supply the time index for this capture.")
    end

    -- Source/quality description
    row(string.format("%s. Native resolution: %s.", typeInfo.long:gsub("^%l", string.upper), typeInfo.res))

    -- Dimensions / scaling notes
    if data.width and data.height then
        local w, h = tonumber(data.width), tonumber(data.height)
        if w and h then
            row(string.format("Detected image dimensions: %d&times;%d pixels.", w, h))
        end
    end

    if data.scaled and data.scaled ~= "" then
        row("This image has been scaled from its original dimensions.")
    end
    if data.cropped and data.cropped ~= "" then
        row("This image has been cropped from its original dimensions.")
    end
    if data.aspect and data.aspect == "yes" then
        row("Aspect ratio has been confirmed correct.")
    end

    -- Close table
    table.insert(rows, "|}")

    return table.concat(rows, "\n")
end

-- ----------------------------------------------------------------
-- MAIN ENTRY POINT
-- ----------------------------------------------------------------
function p.main(frame)
    local args    = frame:getParent().args
    local title   = mw.title.getCurrentTitle()
    local filename = title.text  -- e.g. "RDM - 33 - 042.jpg"

    -- --- Series ---
    local series = (args.series and args.series ~= "") and args.series:lower()
                   or detectSeriesFromFilename(filename)
                   or "unknown"

    -- --- Episode/Season ---
    local detectedSeason, detectedEp, detectedTitleHint = extractEpisodeInfo(filename)

    local season       = (args.season and args.season ~= "")   and args.season   or detectedSeason
    local episode      = (args.episode and args.episode ~= "") and args.episode  or nil
    local cepisode     = (args.cepisode and args.cepisode ~= "") and args.cepisode or nil
    local episodeTitleHint = (episode == nil) and detectedTitleHint or nil

    -- --- Image Dimensions (from EXIF via file object) ---
    local fileObj = title.file
    local width, height
    if fileObj then
        width  = fileObj.width
        height = fileObj.height
    end

    -- --- Source Type ---
    local autoType = detectTypeFromDimensions(width, height)
    local typeLower = (args.type and args.type ~= "") and args.type:lower() or autoType

    -- --- Render ---
    local data = {
        series           = series,
        season           = season,
        episode          = episode,
        cepisode         = cepisode,
        episodeTitleHint = episodeTitleHint,
        typeLower        = typeLower,
        timestamp        = args.timestamp,
        scaled           = args.scaled,
        cropped          = args.cropped,
        aspect           = args.aspect,
        width            = width,
        height           = height,
        args             = args,
    }

    local infobox = renderInfobox(data)
    local categories = buildCategories(series, typeLower, season, episode or episodeTitleHint)

    return infobox .. "\n" .. categories
end

return p