Module:Screencap: Difference between revisions
More languages
More actions
No edit summary |
No edit summary |
||
| (2 intermediate revisions by the same user not shown) | |||
| Line 19: | Line 19: | ||
local SERIES_PREFIXES = { | local SERIES_PREFIXES = { | ||
-- Re-imagined Series (RDM / TRS) | -- Re-imagined Series (RDM / TRS) | ||
{ pattern = "^BSG%-TRS%s*[-_]%s*", | { pattern = "^BSG%-TRS%s*[-_]%s*", series = "rdm" }, | ||
{ pattern = "^RDM%s*[-_]%s*", | { pattern = "^RDM%s*[-_]%s*", series = "rdm" }, | ||
{ pattern = "^TRS%s*[-_]%s*", | { pattern = "^TRS%s*[-_]%s*", series = "rdm" }, | ||
{ pattern = "^BSG%s*[-_]%s*S%d", | { pattern = "^BSG%s*[-_]%s*S%d", series = "rdm" }, | ||
{ pattern = "^BSG%s*[-_]%s*%d+x%d+", | { pattern = "^BSG%s*[-_]%s*%d+x%d+", series = "rdm" }, | ||
{ pattern = "^Battlestar Galactica %(2003%)%s+", series = "rdm" }, -- miniseries | |||
{ pattern = "^Battlestar Galactica %(2004%)%s+", series = "rdm" }, | |||
-- Caprica | -- Caprica | ||
{ pattern = "^CAP%s*[-_]%s*", | { pattern = "^CAP%s*[-_]%s*", series = "cap" }, | ||
{ pattern = "^Caprica%s*[-_]%s*", | { pattern = "^Caprica%s*[-_]%s*", series = "cap" }, | ||
-- Blood & Chrome | -- Blood & Chrome | ||
{ pattern = "^B&C%s*[-_]%s*", | { pattern = "^B&C%s*[-_]%s*", series = "bac" }, | ||
{ pattern = "^BAC%s*[-_]%s*", | { pattern = "^BAC%s*[-_]%s*", series = "bac" }, | ||
{ pattern = "^BNC%s*[-_]%s*", | { pattern = "^BNC%s*[-_]%s*", series = "bac" }, -- legacy | ||
-- Original Series (TOS) | -- Original Series (TOS) | ||
{ pattern = "^TOS%s*[-_]%s*", | { pattern = "^TOS%s*[-_]%s*", series = "tos" }, | ||
{ pattern = "^Battlestar Galactica %(1978%)%s+", series = "tos" }, | |||
-- Galactica 1980 | -- Galactica 1980 | ||
{ pattern = "^1980%s*[-_]%s*", | { pattern = "^1980%s*[-_]%s*", series = "1980" }, | ||
{ pattern = "^G1980%s*[-_]%s*", | { pattern = "^G1980%s*[-_]%s*", series = "1980" }, | ||
{ pattern = "^Galactica 1980 %(%d%d%d%d%)%s+", series = "1980" }, | |||
} | } | ||
| Line 87: | Line 91: | ||
-- ---------------------------------------------------------------- | -- ---------------------------------------------------------------- | ||
-- SOURCE | -- KNOWN SOURCE FILE → EPISODE MAPPINGS | ||
-- | |||
-- For screencaps named from a source video file rather than with a | |||
-- per-episode prefix (e.g. "Battlestar Galactica (1978) 4K.mkv - 1493.jpg"), | |||
-- the source filename itself implies a specific episode or release. | |||
-- Pattern matches the base filename before the quality word. | |||
-- ---------------------------------------------------------------- | |||
local SOURCE_FILE_EPISODES = { | |||
{ | |||
pattern = "^Battlestar Galactica %(1978%)", | |||
episode = "Saga of a Star World", | |||
cepisode = "Saga of a Star World (Theatrical Release)", | |||
}, | |||
-- Add further theatrical/TV-movie releases here as needed, e.g.: | |||
-- { pattern = "^Mission Galactica", episode = "...", cepisode = "..." }, | |||
} | |||
local function detectSourceFileEpisode(filename) | |||
if not filename then return nil end | |||
local name = filename:gsub("%.[^%.]+$", "") -- strip .jpg | |||
for _, entry in ipairs(SOURCE_FILE_EPISODES) do | |||
if name:match(entry.pattern) then | |||
return { episode = entry.episode, cepisode = entry.cepisode } | |||
end | |||
end | |||
return nil | |||
end | |||
-- | -- | ||
-- Thresholds (width-based, landscape assumed): | -- Thresholds (width-based, landscape assumed): | ||
| Line 119: | Line 151: | ||
-- ---------------------------------------------------------------- | -- ---------------------------------------------------------------- | ||
-- | -- SOURCE TYPE DETECTION FROM EMBEDDED VIDEO FILENAME | ||
-- | |||
-- Handles screencaps named from a source video file, e.g.: | |||
-- "Battlestar Galactica (1978) 4K.mkv - 1493.jpg" | |||
-- Extracts quality keyword from the segment immediately before the | |||
-- video extension and maps it to a type key. | |||
-- Only fires when a known video extension is present in the name. | |||
-- ---------------------------------------------------------------- | |||
local VIDEO_EXTENSIONS = { | |||
mkv=true, mp4=true, m4v=true, avi=true, m2ts=true, ts=true, wmv=true | |||
} | |||
local FILENAME_QUALITY_HINTS = { | |||
{ pattern = "^4[Kk]$", type = "4k" }, | |||
{ pattern = "^[Uu][Hh][Dd]$", type = "4k" }, | |||
{ pattern = "^2160[pPiI]?$", type = "4k" }, | |||
{ pattern = "^1080[pPiI]$", type = "bluray" }, | |||
{ pattern = "^[Bb]lu[Rr]ay$", type = "bluray" }, | |||
{ pattern = "^[Bb][Rr][Rr]ip$", type = "bluray" }, | |||
{ pattern = "^720[pPiI]$", type = "hdtv" }, | |||
{ pattern = "^[Hh][Dd][Tt][Vv]$", type = "hdtv" }, | |||
{ pattern = "^[Hh][Dd][Dd][Vv][Dd]$", type = "hddvd" }, | |||
{ pattern = "^[Dd][Vv][Dd]$", type = "ntscdvd" }, | |||
} | |||
local function detectTypeFromFilename(filename) | |||
if not filename then return nil end | |||
-- Strip screencap extension (.jpg etc.) to work with the base name | |||
local name = filename:gsub("%.[^%.]+$", "") | |||
-- Look for a video extension preceded by a quality word: | |||
-- "... 4K.mkv ..." or "... 4K.mkv" at end | |||
local beforeExt, ext = name:match("^(.-)%.(%a+)%s*%-") | |||
if not beforeExt then | |||
beforeExt, ext = name:match("^(.-)%.(%a+)$") | |||
end | |||
if not ext or not VIDEO_EXTENSIONS[ext:lower()] then return nil end | |||
-- The quality word is the last whitespace-delimited token before the extension | |||
local qualityWord = beforeExt:match("([^%s%(%)]+)$") or "" | |||
for _, entry in ipairs(FILENAME_QUALITY_HINTS) do | |||
if qualityWord:match(entry.pattern) then | |||
return entry.type | |||
end | |||
end | |||
return nil | |||
end | |||
-- | -- | ||
-- Supported patterns (case-insensitive): | -- Supported patterns (case-insensitive): | ||
| Line 127: | Line 205: | ||
-- ---------------------------------------------------------------- | -- ---------------------------------------------------------------- | ||
local function extractEpisodeInfo(filename) | local function extractEpisodeInfo(filename) | ||
if not filename then return nil, nil, nil end | if not filename then return nil, nil, nil, nil end | ||
-- Remove file extension | -- Remove file extension | ||
local name = filename:gsub("%.[^%.]+$", "") | local name = filename:gsub("%.[^%.]+$", "") | ||
-- | -- Detect season/episode number anywhere in the filename. | ||
-- Don't return early — continue to extract title and timestamp too. | |||
local detectedSeason, detectedEp | |||
local s, e = name:match("[Ss](%d+)[Ee](%d+)") | local s, e = name:match("[Ss](%d+)[Ee](%d+)") | ||
if s and e then | if s and e then | ||
detectedSeason, detectedEp = tonumber(s), tonumber(e) | |||
else | |||
s, e = name:match("(%d+)[xX](%d+)") | |||
if s and e then | |||
detectedSeason, detectedEp = tonumber(s), tonumber(e) | |||
end | |||
end | end | ||
-- Strip the series prefix using the SERIES_PREFIXES table. | |||
-- Handles mixed-case (e.g. "Caprica") and compound (e.g. "BSG-TRS") prefixes. | |||
-- Strip the series prefix using the | |||
-- | |||
local remainder | local remainder | ||
for _, entry in ipairs(SERIES_PREFIXES) do | for _, entry in ipairs(SERIES_PREFIXES) do | ||
| Line 162: | Line 233: | ||
end | end | ||
end | end | ||
local titleHint, timestampStr | local titleHint, timestampStr | ||
if remainder then | if remainder then | ||
-- Try HH-MM-SS.mmm format first (e.g. " - 00-20-30.229") | -- Try HH-MM-SS.mmm format first (e.g. " - 00-20-30.229") | ||
local h, m, | local h, m, ss = remainder:match("%s*[-_]%s*(%d%d)-(%d%d)-(%d%d)%.[%d]+$") | ||
if h and m and | if h and m and ss then | ||
timestampStr = string.format("%02d:%02d:%02d", tonumber(h), tonumber(m), tonumber( | timestampStr = string.format("%02d:%02d:%02d", tonumber(h), tonumber(m), tonumber(ss)) | ||
remainder = remainder:match("^(.-)%s*[-_]%s*%d%d%-%d%d%-%d%d%.[%d]+$") | remainder = remainder:match("^(.-)%s*[-_]%s*%d%d%-%d%d%-%d%d%.[%d]+$") | ||
else | else | ||
-- Try bare integer seconds (e.g. | -- Try bare integer seconds (e.g. " - 322") | ||
local withoutSecs, trailingNum = remainder:match("^(.-)%s*[-_]%s*(%d+)$") | local withoutSecs, trailingNum = remainder:match("^(.-)%s*[-_]%s*(%d+)$") | ||
if withoutSecs and trailingNum then | if withoutSecs and trailingNum then | ||
| Line 181: | Line 253: | ||
end | end | ||
end | end | ||
-- Take | |||
-- Take the first " - " delimited segment as a candidate title | |||
titleHint = remainder:match("^(.-)%s*[-–]%s*.+$") or remainder | titleHint = remainder:match("^(.-)%s*[-–]%s*.+$") or remainder | ||
titleHint = titleHint:match("^%s*(.-)%s*$") | titleHint = titleHint:match("^%s*(.-)%s*$") | ||
-- Discard if | |||
if titleHint:match(" | -- If the first segment is the season/ep code itself (SxxExx or NxNN), | ||
-- skip it and take the next segment as the episode title instead. | |||
-- e.g. remainder = "S03E01 - Occupation" → titleHint becomes "Occupation" | |||
if titleHint:match("^[Ss]%d+[Ee]%d+$") or titleHint:match("^%d+[xX]%d+$") then | |||
titleHint = remainder:match("^[Ss]%d+[Ee]%d+%s*[-–]%s*(.-)%s*[-–]") | |||
or remainder:match("^[Ss]%d+[Ee]%d+%s*[-–]%s*(.+)$") | |||
or remainder:match("^%d+[xX]%d+%s*[-–]%s*(.-)%s*[-–]") | |||
or remainder:match("^%d+[xX]%d+%s*[-–]%s*(.+)$") | |||
if titleHint then titleHint = titleHint:match("^%s*(.-)%s*$") end | |||
end | |||
-- Discard if titleHint is a source video filename (e.g. "4K.mkv") | |||
-- rather than an actual episode title | |||
if titleHint then | |||
local vidExt = titleHint:match("%.(%a+)$") | |||
if vidExt and VIDEO_EXTENSIONS[vidExt:lower()] then | |||
titleHint = nil | |||
end | |||
end | end | ||
-- Discard if purely numeric (bare index with no useful title) | |||
if titleHint and titleHint:match("^%d+$") then titleHint = nil end | |||
end | end | ||
return | return detectedSeason, detectedEp, titleHint, timestampStr | ||
end | end | ||
| Line 243: | Line 334: | ||
-- ---------------------------------------------------------------- | -- ---------------------------------------------------------------- | ||
local function buildCategories(series, typeLower, season, episode, conflicts) | local function buildCategories(series, typeLower, season, episode, conflicts, timestamp) | ||
local cats = {} | local cats = {} | ||
local seriesMeta = SERIES_META[series] | local seriesMeta = SERIES_META[series] | ||
| Line 277: | Line 368: | ||
if typeLower == "ntscdvd" or typeLower == "paldvd" or typeLower == "ntscb" or typeLower == "palb" then | if typeLower == "ntscdvd" or typeLower == "paldvd" or typeLower == "ntscb" or typeLower == "palb" then | ||
table.insert(cats, "[[Category:Screen captures requiring upgrade]]") | table.insert(cats, "[[Category:Screen captures requiring upgrade]]") | ||
end | |||
if not timestamp or timestamp == "" then | |||
table.insert(cats, "[[Category:Screen captures requiring timestamp]]") | |||
end | end | ||
| Line 548: | Line 643: | ||
episode = detectedTitleHint | episode = detectedTitleHint | ||
episodeFromFilename = true | episodeFromFilename = true | ||
end | |||
-- For known source-file-style filenames (theatrical releases, TV movies), | |||
-- look up the implied episode and cepisode if not already supplied. | |||
local sourceEp = detectSourceFileEpisode(filename) | |||
if sourceEp then | |||
if not episode or episodeFromFilename then | |||
episode = sourceEp.episode | |||
cepisode = cepisode ~= "" and cepisode or sourceEp.cepisode | |||
episodeFromFilename = true -- still display as plain text, not wikilink | |||
end | |||
end | end | ||
local episodeTitleHint = nil -- no longer used; hint is now promoted to episode | local episodeTitleHint = nil -- no longer used; hint is now promoted to episode | ||
| Line 565: | Line 671: | ||
-- --- Source Type --- | -- --- Source Type --- | ||
local autoType | local autoType = detectTypeFromDimensions(width, height) | ||
local typeSupplied = (args.type and args.type ~= "") | local filenameType = detectTypeFromFilename(filename) | ||
local typeLower | local typeSupplied = (args.type and args.type ~= "") | ||
-- Resolution order: explicit |type= > dimensions > filename quality hint | |||
local typeLower = typeSupplied and args.type:lower() | |||
or (autoType ~= "unknown" and autoType) | |||
or filenameType | |||
or "unknown" | |||
-- For conflict detection, treat filename-derived type as equivalent to | |||
-- dimension-derived — only explicit |type= triggers conflict checks. | |||
local effectiveAutoType = autoType ~= "unknown" and autoType or filenameType or "unknown" | |||
-- --- Conflict Detection --- | -- --- Conflict Detection --- | ||
| Line 575: | Line 689: | ||
local conflicts = {} | local conflicts = {} | ||
if typeSupplied and | if typeSupplied and effectiveAutoType ~= "unknown" and typeLower ~= effectiveAutoType then | ||
local HD_FAMILY = { ["4k"]=true, bluray=true, hdtv=true, hddvd=true, itunes=true } | local HD_FAMILY = { ["4k"]=true, bluray=true, hdtv=true, hddvd=true, itunes=true } | ||
if not (HD_FAMILY[typeLower] and HD_FAMILY[ | if not (HD_FAMILY[typeLower] and HD_FAMILY[effectiveAutoType]) then | ||
local taggedRank = TYPE_RANK[typeLower] or 0 | local taggedRank = TYPE_RANK[typeLower] or 0 | ||
local autoRank = TYPE_RANK[ | local autoRank = TYPE_RANK[effectiveAutoType] or 0 | ||
if autoRank > taggedRank then | if autoRank > taggedRank then | ||
conflicts.type = "upgrade" | conflicts.type = "upgrade" | ||
else | else | ||
conflicts.type = "downgrade" | conflicts.type = "downgrade" | ||
end | end | ||
| Line 606: | Line 715: | ||
episodeFromFilename = episodeFromFilename, | episodeFromFilename = episodeFromFilename, | ||
typeLower = typeLower, | typeLower = typeLower, | ||
autoType = | autoType = effectiveAutoType, | ||
detectedSeries = detectedSeries or "unknown", | detectedSeries = detectedSeries or "unknown", | ||
typeConflict = conflicts.type, -- "upgrade", "downgrade", or nil | typeConflict = conflicts.type, -- "upgrade", "downgrade", or nil | ||
| Line 620: | Line 729: | ||
local infobox = renderInfobox(data) | local infobox = renderInfobox(data) | ||
local categories = buildCategories(series, typeLower, season, episode, conflicts) | local categories = buildCategories(series, typeLower, season, episode, conflicts, timestamp) | ||
return infobox .. "\n" .. categories | return infobox .. "\n" .. categories | ||
Latest revision as of 04:25, 14 September 2026
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 (
| The an unknown series from an unknown episode. Please help us identify the episode by updating this tag or discussing it on the 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. | |
,
| The original Battlestar Galactica from an unknown episode. Please help us identify the episode by updating this tag or discussing it on the 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. | |
,
| File:BSG WIKI HDTV.png HD broadcast screen capture | |
|---|---|
| The Caprica from an unknown episode. Please help us identify the episode by updating this tag or discussing it on the 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:
- Detects the series from filename prefix patterns
- Detects the season and episode from filename patterns (SxxExx, NxNN, or prose title hint)
- Detects the source type / quality tier from the file's pixel dimensions
- 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_). The compound prefix BSG-TRS - is checked before the bare BSG - patterns to prevent partial matches.
| Filename starts with | Detected series key
|
|---|---|
BSG-TRS -, RDM -, TRS -, BSG - S##, BSG - #x## |
rdm
|
CAP -, Caprica - |
cap
|
B&C -, BAC -, BNC - (legacy) |
bac
|
TOS - |
tos
|
Galactica 1980 -, 1980 -, G1980 - |
1980
|
| (no match) | unknown
|
Each series key maps to a SERIES_META entry containing the human-readable name, category abbreviation(s), wiki article links, and copyright holder used in the infobox. The rdm series carries two abbreviations (TRS primary, RDM secondary) and emits categories under both.
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 - 195.jpg |
Title hint: "The Lost Warrior" / Timestamp: 00:03:15 |
| (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:
- Header — source type icon + label (collapsible toggle)
- Series / episode identification line with language bar links
- Copyright notice (links to Wikipedia)
- Timestamp (or missing-timestamp notice)
- Source description and native resolution
- Detected pixel dimensions (if available)
- Scaling note (if
|scaled=yes) - Cropping note (if
|cropped=yes) - 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 (RDM)]][[Category:Blu-ray screen captures (RDM)]][[Category:Screen captures by season 4 (RDM)]][[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 = "^BAC%s*[-_]%s*", series = "bac" },
-- In SERIES_META: bac = {
name = "Battlestar Galactica: Blood & Chrome", abbr = "BAC", 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
- Template:Screencap — the template that invokes this module
- Template:Screencap/doc — end-user documentation and parameter reference
- Template:Master screencap — legacy parent template (now a wrapper)
- Battlestar Wiki Media:Image tagging policy
-- ============================================================
-- 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 = "^BSG%-TRS%s*[-_]%s*", series = "rdm" },
{ 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" },
{ pattern = "^Battlestar Galactica %(2003%)%s+", series = "rdm" }, -- miniseries
{ pattern = "^Battlestar Galactica %(2004%)%s+", series = "rdm" },
-- Caprica
{ pattern = "^CAP%s*[-_]%s*", series = "cap" },
{ pattern = "^Caprica%s*[-_]%s*", series = "cap" },
-- Blood & Chrome
{ pattern = "^B&C%s*[-_]%s*", series = "bac" },
{ pattern = "^BAC%s*[-_]%s*", series = "bac" },
{ pattern = "^BNC%s*[-_]%s*", series = "bac" }, -- legacy
-- Original Series (TOS)
{ pattern = "^TOS%s*[-_]%s*", series = "tos" },
{ pattern = "^Battlestar Galactica %(1978%)%s+", series = "tos" },
-- Galactica 1980
{ pattern = "^1980%s*[-_]%s*", series = "1980" },
{ pattern = "^G1980%s*[-_]%s*", series = "1980" },
{ pattern = "^Galactica 1980 %(%d%d%d%d%)%s+", series = "1980" },
}
-- Human-readable series names and wiki links
local SERIES_META = {
rdm = {
name = "Re-imagined ''Battlestar Galactica''",
abbr = "TRS",
abbr2 = "RDM", -- secondary initialism, used in categories alongside 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",
},
bac = {
name = "''Battlestar Galactica: Blood & Chrome''",
abbr = "BAC",
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",
},
}
-- Series with only one season — season row is suppressed in infobox
local SINGLE_SEASON_SERIES = { tos = true, cap = true, ["1980"] = true }
-- ----------------------------------------------------------------
-- KNOWN SOURCE FILE → EPISODE MAPPINGS
--
-- For screencaps named from a source video file rather than with a
-- per-episode prefix (e.g. "Battlestar Galactica (1978) 4K.mkv - 1493.jpg"),
-- the source filename itself implies a specific episode or release.
-- Pattern matches the base filename before the quality word.
-- ----------------------------------------------------------------
local SOURCE_FILE_EPISODES = {
{
pattern = "^Battlestar Galactica %(1978%)",
episode = "Saga of a Star World",
cepisode = "Saga of a Star World (Theatrical Release)",
},
-- Add further theatrical/TV-movie releases here as needed, e.g.:
-- { pattern = "^Mission Galactica", episode = "...", cepisode = "..." },
}
local function detectSourceFileEpisode(filename)
if not filename then return nil end
local name = filename:gsub("%.[^%.]+$", "") -- strip .jpg
for _, entry in ipairs(SOURCE_FILE_EPISODES) do
if name:match(entry.pattern) then
return { episode = entry.episode, cepisode = entry.cepisode }
end
end
return nil
end
--
-- 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)
-- 4K: either ultrawide landscape (≥3840) OR any axis at 2160
-- This catches 3840×2160, 2160×2160, 2160×1080, etc.
if long >= 3840 then return "4k" end
if w == 2160 or h == 2160 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
if long >= 720 then return "paldvd" end
return "dvd"
end
-- ----------------------------------------------------------------
-- SOURCE TYPE DETECTION FROM EMBEDDED VIDEO FILENAME
--
-- Handles screencaps named from a source video file, e.g.:
-- "Battlestar Galactica (1978) 4K.mkv - 1493.jpg"
-- Extracts quality keyword from the segment immediately before the
-- video extension and maps it to a type key.
-- Only fires when a known video extension is present in the name.
-- ----------------------------------------------------------------
local VIDEO_EXTENSIONS = {
mkv=true, mp4=true, m4v=true, avi=true, m2ts=true, ts=true, wmv=true
}
local FILENAME_QUALITY_HINTS = {
{ pattern = "^4[Kk]$", type = "4k" },
{ pattern = "^[Uu][Hh][Dd]$", type = "4k" },
{ pattern = "^2160[pPiI]?$", type = "4k" },
{ pattern = "^1080[pPiI]$", type = "bluray" },
{ pattern = "^[Bb]lu[Rr]ay$", type = "bluray" },
{ pattern = "^[Bb][Rr][Rr]ip$", type = "bluray" },
{ pattern = "^720[pPiI]$", type = "hdtv" },
{ pattern = "^[Hh][Dd][Tt][Vv]$", type = "hdtv" },
{ pattern = "^[Hh][Dd][Dd][Vv][Dd]$", type = "hddvd" },
{ pattern = "^[Dd][Vv][Dd]$", type = "ntscdvd" },
}
local function detectTypeFromFilename(filename)
if not filename then return nil end
-- Strip screencap extension (.jpg etc.) to work with the base name
local name = filename:gsub("%.[^%.]+$", "")
-- Look for a video extension preceded by a quality word:
-- "... 4K.mkv ..." or "... 4K.mkv" at end
local beforeExt, ext = name:match("^(.-)%.(%a+)%s*%-")
if not beforeExt then
beforeExt, ext = name:match("^(.-)%.(%a+)$")
end
if not ext or not VIDEO_EXTENSIONS[ext:lower()] then return nil end
-- The quality word is the last whitespace-delimited token before the extension
local qualityWord = beforeExt:match("([^%s%(%)]+)$") or ""
for _, entry in ipairs(FILENAME_QUALITY_HINTS) do
if qualityWord:match(entry.pattern) then
return entry.type
end
end
return nil
end
--
-- 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, nil end
-- Remove file extension
local name = filename:gsub("%.[^%.]+$", "")
-- Detect season/episode number anywhere in the filename.
-- Don't return early — continue to extract title and timestamp too.
local detectedSeason, detectedEp
local s, e = name:match("[Ss](%d+)[Ee](%d+)")
if s and e then
detectedSeason, detectedEp = tonumber(s), tonumber(e)
else
s, e = name:match("(%d+)[xX](%d+)")
if s and e then
detectedSeason, detectedEp = tonumber(s), tonumber(e)
end
end
-- Strip the series prefix using the SERIES_PREFIXES table.
-- Handles mixed-case (e.g. "Caprica") and compound (e.g. "BSG-TRS") prefixes.
local remainder
for _, entry in ipairs(SERIES_PREFIXES) do
local stripped = name:match(entry.pattern .. "(.+)$")
if stripped then
remainder = stripped
break
end
end
local titleHint, timestampStr
if remainder then
-- Try HH-MM-SS.mmm format first (e.g. " - 00-20-30.229")
local h, m, ss = remainder:match("%s*[-_]%s*(%d%d)-(%d%d)-(%d%d)%.[%d]+$")
if h and m and ss then
timestampStr = string.format("%02d:%02d:%02d", tonumber(h), tonumber(m), tonumber(ss))
remainder = remainder:match("^(.-)%s*[-_]%s*%d%d%-%d%d%-%d%d%.[%d]+$")
else
-- Try bare integer seconds (e.g. " - 322")
local withoutSecs, trailingNum = remainder:match("^(.-)%s*[-_]%s*(%d+)$")
if withoutSecs and trailingNum then
local secs = tonumber(trailingNum)
timestampStr = string.format("%02d:%02d:%02d",
math.floor(secs / 3600),
math.floor((secs % 3600) / 60),
math.floor(secs % 60))
remainder = withoutSecs
end
end
-- Take the first " - " delimited segment as a candidate title
titleHint = remainder:match("^(.-)%s*[-–]%s*.+$") or remainder
titleHint = titleHint:match("^%s*(.-)%s*$")
-- If the first segment is the season/ep code itself (SxxExx or NxNN),
-- skip it and take the next segment as the episode title instead.
-- e.g. remainder = "S03E01 - Occupation" → titleHint becomes "Occupation"
if titleHint:match("^[Ss]%d+[Ee]%d+$") or titleHint:match("^%d+[xX]%d+$") then
titleHint = remainder:match("^[Ss]%d+[Ee]%d+%s*[-–]%s*(.-)%s*[-–]")
or remainder:match("^[Ss]%d+[Ee]%d+%s*[-–]%s*(.+)$")
or remainder:match("^%d+[xX]%d+%s*[-–]%s*(.-)%s*[-–]")
or remainder:match("^%d+[xX]%d+%s*[-–]%s*(.+)$")
if titleHint then titleHint = titleHint:match("^%s*(.-)%s*$") end
end
-- Discard if titleHint is a source video filename (e.g. "4K.mkv")
-- rather than an actual episode title
if titleHint then
local vidExt = titleHint:match("%.(%a+)$")
if vidExt and VIDEO_EXTENSIONS[vidExt:lower()] then
titleHint = nil
end
end
-- Discard if purely numeric (bare index with no useful title)
if titleHint and titleHint:match("^%d+$") then titleHint = nil end
end
return detectedSeason, detectedEp, titleHint, timestampStr
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" },
}
-- Quality rank for conflict direction detection (higher = better source)
local TYPE_RANK = {
["4k"] = 7,
bluray = 6,
hddvd = 5,
itunes = 5,
hdtv = 4,
ntscdvd = 3,
paldvd = 3,
ntscb = 2,
palb = 2,
dvd = 1,
promo = 0,
unknown = 0,
}
-- ----------------------------------------------------------------
local function buildCategories(series, typeLower, season, episode, conflicts, timestamp)
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]]")
-- Build list of abbreviations to categorise under (usually one; RDM uses both TRS and RDM)
local abbrs = {}
if abbr ~= "" then table.insert(abbrs, abbr) end
if seriesMeta and seriesMeta.abbr2 then table.insert(abbrs, seriesMeta.abbr2) end
for _, ab in ipairs(abbrs) do
table.insert(cats, string.format("[[Category:Screen captures (%s)]]", ab))
table.insert(cats, string.format("[[Category:%s screen captures (%s)]]", typeShort, ab))
if season then
table.insert(cats, string.format("[[Category:Screen captures by season %s (%s)]]", season, ab))
end
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
if not timestamp or timestamp == "" then
table.insert(cats, "[[Category:Screen captures requiring timestamp]]")
end
-- Conflict maintenance categories
if conflicts then
if conflicts.type == "upgrade" then
table.insert(cats, "[[Category:Screen captures with source type mismatch]]")
end
if conflicts.series then
table.insert(cats, "[[Category:Screen captures with series mismatch]]")
end
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"]
-- Use the [[media:Filename]] interwiki prefix to reference files on
-- media.battlestarwiki.org directly. Unlike [[File:]] this renders as a
-- plain hyperlink, so we pair it with a sized [[File:]] transclusion
-- using the |link= trick. The only cross-wiki-safe wikitext image syntax
-- is to use the shared media repository (ForeignAPIRepo), which already
-- serves these files to all language wikis. Since the icons are in the
-- shared repo at media.battlestarwiki.org, [[File:Name|Xpx]] just works.
local iconMap = {
["4k"] = "[[File:BSG WIKI 4K.png|30px|link=]]",
bluray = "[[File:BSG WIKI Bluray.png|30px|link=]]",
hdtv = "[[File:BSG WIKI HDTV.png|30px|link=]]",
hddvd = "[[File:BSG WIKI HDDVD.png|30px|link=]]",
itunes = "[[File:ITunes Icon.png|30px|link=]]",
ntscdvd = "[[File:BSG WIKI Dvd.png|30px|link=]]",
ntscb = "[[File:BSG WIKI Screencap.png|30px|link=]]",
paldvd = "[[File:BSG WIKI Dvd.png|30px|link=]]",
palb = "[[File:BSG WIKI Screencap.png|30px|link=]]",
promo = "[[File:BSG WIKI Promo.png|30px|link=]]",
dvd = "[[File:BSG WIKI Dvd.png|30px|link=]]",
unknown = "[[File:BSG WIKI Screencap.png|30px|link=]]",
}
local iconWikitext = iconMap[data.typeLower] or iconMap["unknown"]
-- 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 \'\'\'%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
local seasonPart = (data.season and not SINGLE_SEASON_SERIES[data.series])
and (", Season " .. data.season) or ""
local epFormatted
if data.episodeFromFilename then
-- Title came from filename; display as plain italic text, not a wikilink,
-- since we can't guarantee it matches an article name exactly.
epFormatted = string.format("\"''%s''\"", epDisplay)
else
epFormatted = string.format("\"[[%s|%s]]\"", data.episode, epDisplay)
end
episodeText = string.format(
"The %s%s %s episode %s.",
seriesName,
seasonPart,
langBar,
epFormatted
)
else
local currentTitle = mw.title.getCurrentTitle()
local talkText = mw.title.new(currentTitle.text, 7).fullText
local talkLink = "[[" .. talkText .. "|talk page]]"
local seasonPart = (data.season and not SINGLE_SEASON_SERIES[data.series])
and (", Season " .. data.season) or ""
episodeText = string.format(
"The %s%s from an unknown episode. " ..
"Please help us identify the episode by updating this tag or discussing it on the %s.",
seriesName, seasonPart, talkLink
)
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
-- Accepts three input formats:
-- 427 → bare seconds → converted to 00:07:07
-- 00:07:07 → already HH:MM:SS → used as-is
-- 0h20m42s → already formatted → used as-is
if data.timestamp and data.timestamp ~= "" then
local ts = data.timestamp:match("^%s*(.-)%s*$") -- trim whitespace
local display = ts
-- If input is purely numeric, treat as total seconds and convert
local secs = tonumber(ts)
if secs then
local h = math.floor(secs / 3600)
local m = math.floor((secs % 3600) / 60)
local s = math.floor(secs % 60)
display = string.format("%02d:%02d:%02d", h, m, s)
end
row(string.format("Timestamp: <code>%s</code> (HH:MM:SS)", display))
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×%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
-- Conflict warnings: surface mismatches between supplied values and
-- what auto-detection would have produced. The supplied value still wins
-- (we never silently override), but the conflict is shown to editors
-- and a maintenance category is applied so it can be found and fixed.
if data.typeConflict == "upgrade" then
local example = mw.text.nowiki("{{Screencap|type=" .. data.autoType .. "}}")
table.insert(rows, string.format(
'|-\n| style="background:#5c1a00;color:#ffcc66;font-size:85%%" |' ..
" '''⚠ Source type mismatch:'''" ..
" This file is tagged as '''%s''' but its dimensions (%d×%d px)" ..
" suggest it may have been upgraded to '''%s'''." ..
" If so, update the template to <code>%s</code>.",
data.typeLower,
tonumber(data.width) or 0, tonumber(data.height) or 0,
data.autoType,
example
))
elseif data.typeConflict == "downgrade"
and not (data.scaled and data.scaled ~= "")
and not (data.cropped and data.cropped ~= "") then
-- Only show the downgrade note if |scaled= / |cropped= haven't already explained it
table.insert(rows, string.format(
'|-\n| style="background:#2a2a00;color:#ffcc66;font-size:85%%" |' ..
" ℹ Dimension note:" ..
" This file is tagged as '''%s''' but its stored dimensions (%d×%d px)" ..
" are smaller than expected for that source." ..
" If this is a scaled or cropped version, add <code>%s</code> or <code>%s</code>.",
data.typeLower,
tonumber(data.width) or 0, tonumber(data.height) or 0,
mw.text.nowiki("|scaled=yes"),
mw.text.nowiki("|cropped=yes")
))
end
if data.seriesConflict then
table.insert(rows, string.format(
'|-\n| style="background:#5c1a00;color:#ffcc66;font-size:85%%" |' ..
" '''⚠ Series mismatch:'''" ..
" This file is tagged as '''%s''' but its filename suggests '''%s'''." ..
" Please verify the correct series and update the template.",
data.series:upper(),
data.detectedSeries:upper()
))
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 detectedSeries = detectSeriesFromFilename(filename)
local seriesSupplied = (args.series and args.series ~= "")
local series = seriesSupplied and args.series:lower()
or detectedSeries
or "unknown"
-- --- Episode/Season ---
local detectedSeason, _, detectedTitleHint, detectedTimestamp = extractEpisodeInfo(filename)
local season = (args.season and args.season ~= "") and args.season or detectedSeason
-- Single-season series never show a season row
if SINGLE_SEASON_SERIES[series] then season = nil end
local episode = (args.episode and args.episode ~= "") and args.episode or nil
local cepisode = (args.cepisode and args.cepisode ~= "") and args.cepisode or nil
-- If no explicit episode supplied, promote the filename title hint directly.
-- Flag it so the renderer displays it as plain text rather than a wikilink,
-- since we can't guarantee the hint matches an article name exactly.
local episodeFromFilename = false
if not episode and detectedTitleHint and detectedTitleHint ~= "" then
episode = detectedTitleHint
episodeFromFilename = true
end
-- For known source-file-style filenames (theatrical releases, TV movies),
-- look up the implied episode and cepisode if not already supplied.
local sourceEp = detectSourceFileEpisode(filename)
if sourceEp then
if not episode or episodeFromFilename then
episode = sourceEp.episode
cepisode = cepisode ~= "" and cepisode or sourceEp.cepisode
episodeFromFilename = true -- still display as plain text, not wikilink
end
end
local episodeTitleHint = nil -- no longer used; hint is now promoted to episode
-- Timestamp: explicit param wins; fall back to timestamp extracted from filename
local timestamp = (args.timestamp and args.timestamp ~= "") and args.timestamp
or detectedTimestamp
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 filenameType = detectTypeFromFilename(filename)
local typeSupplied = (args.type and args.type ~= "")
-- Resolution order: explicit |type= > dimensions > filename quality hint
local typeLower = typeSupplied and args.type:lower()
or (autoType ~= "unknown" and autoType)
or filenameType
or "unknown"
-- For conflict detection, treat filename-derived type as equivalent to
-- dimension-derived — only explicit |type= triggers conflict checks.
local effectiveAutoType = autoType ~= "unknown" and autoType or filenameType or "unknown"
-- --- Conflict Detection ---
-- Only flag a conflict when a value was explicitly supplied (by a wrapper
-- template or a user override) AND it disagrees with auto-detection.
-- We never flag a conflict when the value was itself auto-detected.
local conflicts = {}
if typeSupplied and effectiveAutoType ~= "unknown" and typeLower ~= effectiveAutoType then
local HD_FAMILY = { ["4k"]=true, bluray=true, hdtv=true, hddvd=true, itunes=true }
if not (HD_FAMILY[typeLower] and HD_FAMILY[effectiveAutoType]) then
local taggedRank = TYPE_RANK[typeLower] or 0
local autoRank = TYPE_RANK[effectiveAutoType] or 0
if autoRank > taggedRank then
conflicts.type = "upgrade"
else
conflicts.type = "downgrade"
end
end
end
if seriesSupplied and detectedSeries and series ~= detectedSeries then
conflicts.series = true
end
-- --- Render ---
local data = {
series = series,
season = season,
episode = episode,
cepisode = cepisode,
episodeTitleHint = episodeTitleHint,
episodeFromFilename = episodeFromFilename,
typeLower = typeLower,
autoType = effectiveAutoType,
detectedSeries = detectedSeries or "unknown",
typeConflict = conflicts.type, -- "upgrade", "downgrade", or nil
seriesConflict = conflicts.series,
timestamp = 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, conflicts, timestamp)
return infobox .. "\n" .. categories
end
return p