You can invoke this module from a template to get back episode information from a string representing an episode code or name in Module:Ep/Array. To get a formatted string similar to {{Ep}}, invoke Module:Ep instead. This module will return blank values if the episode code is missing from Module:Ep/Array, even if it's validly constructed.

In general, this module should not need to be updated. For more detailed directions about updating data modules for new shows or seasons, please see Help:Maintaining data modules.

Invoking from template[edit source]

Episode information[edit source]

This module can return information about an episode, including the episode code, title, pagename, episode number, thumbnail link, or transcript link. Here it is invoked for the latest episode of Campaign 3:

Usage Gives
{{#invoke:Ep/Info|ep|C3 latest}} 3x91
{{#invoke:Ep/Info|ep|C3 latest|title=1}} Campaign 3 Episode 91
{{#invoke:Ep/Info|ep|C3 latest|pagename=1}} Campaign 3 Episode 91
{{#invoke:Ep/Info|ep|C3 latest|prodCode=1}} C3E91
{{#invoke:Ep/Info|ep|C3 latest|epnum=true}} 91
{{#invoke:Ep/Info|ep|C3 latest|thumbnail=true}} Bells Hells logo color.png
{{#invoke:Ep/Info|ep|C3 latest|transcript=true}} Transcript:Campaign 3 Episode 91

Here it is for a case where the pagename and episode title differ:

Usage Gives
{{#invoke:Ep/Info|ep|3x25}} 3x25
{{#invoke:Ep/Info|ep|3x25|title=1}} A Taste of Tal'Dorei
{{#invoke:Ep/Info|ep|3x25|pagename=1}} A Taste of Tal'Dorei (episode)

Module:Ep combines all three of these values into a standardized format:

Campaign and season information[edit source]

This utilizes Module:Ep/Decoder. When appropriate, some seasons are named instead of numbered. Note that true, 1, and any other string is equivalent after the equals sign.

Usage Gives
{{#invoke:Ep/Info|ep|LVM2x04|campaign=true}} LVM
{{#invoke:Ep/Info|ep|LVM2x04|campaignname=true}} The Legend of Vox Machina
{{#invoke:Ep/Info|ep|LVM2x04|season=true}} 2
{{#invoke:Ep/Info|ep|LVM2x04|seasonname=true}} Season 2
Usage Gives
{{#invoke:Ep/Info|ep|E3x02|campaign=true}} E
{{#invoke:Ep/Info|ep|E3x02|campaignname=true}} Exandria Unlimited
{{#invoke:Ep/Info|ep|E3x02|season=true}} 3
{{#invoke:Ep/Info|ep|E3x02|seasonname=true}} Calamity

Arc information[edit source]

This utilizes Module:Ep/Decoder to produce information about Arcs in longer campaigns.

Usage Gives
{{#invoke:Ep/Info|ep|2x105|arcName=true}} Family Ties
{{#invoke:Ep/Info|ep|2x105|arcNum=true}} 5
{{#invoke:Ep/Info|ep|2x105|arcCategory=true}} Category:Family Ties arc
{{#invoke:Ep/Info|ep|2x105|arcCharacterCategory=true}} Category:Characters in the Family Ties arc
{{#invoke:Ep/Info|ep|2x105|arcPage=true}} Campaign 2 Arc 5: Family Ties

Calling from another module[edit source]

You can also get the episode number, title, pagename, campaign, and/or season in another module. Note that in this case, the module expects epCode to be a string, not a frame. Within a function, that might look something like:

local epInfo = require('Module:Ep/Info')
local epName = epInfo.title(epCode)
local pageName = epInfo.pagename(epCode)
local epNum = epInfo.epNum(epCode)
local campaign = epInfo.campaign(epCode)
local campaignName = epInfo.campaignName(epCode)
local season = epInfo.season(epCode)
local seasonName = epInfo.seasonName(epCode)

-- TO DO: update this so other modules don't need to call Module:Ep/Matcher directly
-- TO DO: consider changing so the individual functions can be called directly when invoked (instead of using param)

local p = {}

-- For templates to use this module. First unnamed arg is the value to be interpreted.
function p.ep(frame)
	local ep = frame.args[1]
	local title = frame.args['title'] or nil
	local pagename = frame.args['pagename'] or nil
	local epNum = frame.args['epnum'] or nil
	local epCodeFormatted = frame.args['epCodeFormatted'] or nil
	local campaign = frame.args['campaign'] or nil
	local campaignName = frame.args['campaignname'] or nil
	local season = frame.args['season'] or nil
	local seasonName = frame.args['seasonname'] or nil
	local isCampaign = frame.args['iscampaign'] or nil
	local prodCode = frame.args['prodCode'] or nil
	local arcName = frame.args['arcName'] or nil
	local arcNum = frame.args['arcNum'] or nil
	local arcCategory = frame.args['arcCategory'] or nil
	local arcCharacterCategory = frame.args['arcCharacterCategory'] or nil
	local arcPage = frame.args['arcPage'] or nil
	local thumbnail = frame.args['thumbnail'] or nil
	local transcript = frame.args['transcript'] or nil
	local sortkey = frame.args['sortkey'] or nil
	local sortBy = frame.args['sortby'] or 'airdate'
	local epMatcher = require('Module:Ep/Matcher')
	local epCode = epMatcher.matchCode(ep)
	local data = ''
	if not isempty(title) then
		data = p.title(epCode)
	elseif not isempty(pagename) then
		data = p.pagename(epCode)
	elseif not isempty(epNum) then
		data = p.epNum(epCode)
	elseif not isempty(epCodeFormatted) then
		data = p.epCodeFormatted(epCode)
	elseif not isempty(campaign) then
		data = p.campaign(epCode)
	elseif not isempty(campaignName) then
		data = p.campaignName(epCode)
	elseif not isempty(season) then
		data = p.season(epCode)
	elseif not isempty(seasonName) then
		data = p.seasonName(epCode)
	elseif not isempty(arcName) then
		data = p.arcName(epCode)
	elseif not isempty(prodCode) then
		data = p.prodCode(epCode)
	elseif not isempty(isCampaign) then
		data = p.isCampaign(epCode)
	elseif not isempty(arcNum) then
		data = p.arcNum(epCode)
	elseif not isempty(arcPage) then
		data = p.arcPage(epCode)
	elseif not isempty(arcCategory) then
		data = p.arcCategory(epCode)
	elseif not isempty(arcCharacterCategory) then
		data = p.arcCharacterCategory(epCode)
	elseif not isempty(sortkey) then
		data = p.calculateSortkey(epCode, sortBy)
	elseif not isempty(thumbnail) then
		data = p.thumbnail(epCode)
	elseif not isempty(transcript) then
		data = p.transcript(epCode)
		data = epCode
	return data

-- Functions for querying Module:Ep/Array to get title and/or pagename for output
function p.epInfo(epCode)
	local result

	local epInfos = mw.loadData('Module:Ep/Array')
	if epInfos[epCode] == nil then
		result = ""
		result = epInfos[epCode]

	return result


p.title = function(epCode)
	return p.epInfo(epCode)['title']

p.pagename = function(epCode)
	if p.epInfo(epCode)['pagename'] then
		pagename = p.epInfo(epCode)['pagename']
		pagename = p.title(epCode)
	return pagename

-- functions to parse epCode into campaign, season and epnumber, using EPISODE_DECODER

p.parseCode = function(epCode)
	if type(epCode) ~= "string" then
		epCode = epCode.args[1]
	i, j = string.find(epCode,"x")
	prefix, epNum = '', ''
	if i then
		prefix = string.sub(epCode,1,i-1)
		epNum = string.sub(epCode,j+1)
	return prefix, epNum

p.epNum = function(epCode)
	-- TM exception; find epNum in pagename
	if p.campaign(epCode) == 'TM' then
		local pagename = p.pagename(epCode)
		epNum = string.match(pagename, "%d+")
		local prefix, epNum = p.parseCode(epCode)
	-- remove leading zeroes
	if epNum then
		epNum = string.format("%d", epNum)
	return epNum

-- Parse out shows and (optionally) seasons. Prevents TM from being parsed as having seasons.
p.campSeason = function(epCode)
	local campaign
	local season
	local decoder = mw.loadData('Module:Ep/Decoder')
	local prefix, epNum = p.parseCode(epCode)
	if prefix == 'TMOS' then
		campaign = 'TM'
	elseif (not tonumber(prefix) and prefix:match("^(.*%D)(%d+)$")) then
		campaign, season = prefix:match("^(.*%D)(%d+)$")
	elseif decoder[prefix] then
		campaign = prefix
		campaign = 'Unknown Campaign'
	-- Talks Machina season exception
	if campaign == 'TM' then
		season = ''
	return campaign, season

p.prefix = function(epCode)
	prefix, epNum = p.parseCode(epCode)
	return prefix

p.campaign = function(epCode)
	campaign, season = p.campSeason(epCode)
	return campaign

p.campaignName = function(epCode, noItal)
	if not noItal then noItal = false
	local campaign = p.campaign(epCode)
	if not campaign then
		return "Unknown Campaign"
	local decoder = mw.loadData('Module:Ep/Decoder')
	if not decoder[campaign] then return "Unknown Campaign" end
	local campaignName = decoder[campaign]['title']
	if decoder[campaign]['italics'] == true and not noItal and campaignName then
		campaignName = "''" .. campaignName .. "''"
	return campaignName

p.season = function(epCode)
	campaign, season = p.campSeason(epCode)
	return season

-- displays in the infobox "Episode" field before the episode number
p.seasonName = function(epCode)
	local decoder = mw.loadData('Module:Ep/Decoder')
	local campaign, season = p.campSeason(epCode)
	local valid_seasons
	local pagename
	local nameForDisplay
	local italics

	if campaign and decoder[campaign] then
		valid_seasons = decoder[campaign]['seasons'] or nil

	-- if there's no season or the campaign doesn't have seasons, blank
	if isempty(season) or isempty(valid_seasons) then return ''
	-- is season valid? if not, blank
	elseif not searchKeys(season, valid_seasons) then return ''
	-- get season info from decoder
		nameForDisplay = valid_seasons[season]['name'] or ''
		italics = valid_seasons[season]['italics'] or false

	-- Default display is e.g. "Season 1", other build with season data from decoder
	if isempty(nameForDisplay) then
		nameForDisplay = 'Season ' .. season
	return nameForDisplay

-- determine if episode is from a main campaign
p.isCampaign = function(epCode)
    local prefix, epNum = p.parseCode(epCode)
    return tonumber(prefix) and tonumber(epNum)

-- format epCode as C[Campaign]E[Episode]
p.prodCode = function(epCode)
    if p.isCampaign(epCode) then
        local prefix, epNum = p.parseCode(epCode)
        return 'C' .. prefix .. 'E' .. string.format("%02d", epNum)
        return nil -- for non-campaign episodes

p.arcInfo = function(epCode, arcIndex)
	local campaign = p.campaign(epCode)
	local epNum = tonumber(p.epNum(epCode))
	local decoder = mw.loadData('Module:Ep/Decoder')
	local arc = {}
	if decoder[campaign] then
		local arcs = decoder[campaign].arcs
		if not isempty(arcs) then
			if not arcIndex then
				for index, arcData in ipairs(arcs) do
					if epNum >= arcData.startEpisode and epNum <= arcData.endEpisode then
						arc = arcData
				local selectedArc = arcs[arcIndex]
				if selectedArc then
					arc = selectedArc
	return arc

p.arcNum = function(epCode)
	local arc = p.arcInfo(epCode)
	return arc.arcNum

p.arcName = function(epCode)
	local arc = p.arcInfo(epCode)
	return arc.title

p.arcPage = function(epCode)
	local campaign = p.campaign(epCode)
	local arc = p.arcInfo(epCode)
	local category = 'Campaign  ' .. campaign .. ' Arc ' .. arc.arcNum .. ': ' .. arc.title
	return category

p.arcCategory = function(epCode)
	local arc = p.arcInfo(epCode)
	return 'Category:' .. arc.title .. ' arc'

p.arcCharacterCategory = function(epCode)
	local arc = p.arcInfo(epCode)
	local arcName = removeLeadingThe(arc.title)
	return 'Category:Characters in the ' .. arcName .. ' arc'

-- get default naming for link to thumbnail
p.thumbnail = function(epCode)
	local thumbnailFile = epCode .. ' Episode Thumb.jpg'
	local campaign = p.campaign(epCode)

	-- Check if thumbnailFile exists, if not, fall back to default
	local title ='File:' .. thumbnailFile)
	if not title or not title.exists then
		if campaign == '3' then
			thumbnailFile = 'Bells Hells logo color.png'
		elseif campaign == 'CO' then
			thumbnailFile = 'Candela Obscura Logo.png'
	return thumbnailFile

p.transcript = function(epCode)
	local pagename = p.pagename(epCode)
	local transcript_link = 'Transcript:' .. pagename .. ''
	return transcript_link

-- for formatting the episode code
p.epCodeFormatted = function(epCode)
	local campaign = p.campaign(epCode)
	local decoder = mw.loadData('Module:Ep/Decoder')
	local small = mw.html.create('small')
	local formatKey
	local formatted
	if campaign ~= 'Unknown Campaign' and decoder[campaign] then
		formatKey = decoder[campaign]['codeFormat']
	if formatKey then
		formatted = formatKey:gsub('{campaign}', campaign)
		if p.season(epCode) then
			formatted = formatted:gsub('{season}', p.season(epCode))
		formatted = formatted:gsub('{epNum}', string.format("%02d", p.epNum(epCode)))
		formatted = epCode
	small:wikitext("(" .. formatted .. ")")
	formatted = tostring(small)
	return formatted

-- Function to calculate the sortkey for an episode code
p.calculateSortkey = function(epCode, sortBy)
	local epNum = p.epNum(epCode)
	local campaign, season = p.campSeason(epCode)
	if not season then season = "0" end

	-- map of campaign codes to sort values
	local campaignTransform = {
		["1"] = "01",
		["2"] = "02",
		["3"] = "03",
		["OS"] = "05",
		["CO"] = "06",
		["4SD"] = "07",
		["E"] = "08",
		["M"] = "09",
		["TM"] = "10",
		["U"] = "11",
		["Midst"] = "12",

	-- transform campaign code
	if campaignTransform[campaign] then
		campaign = campaignTransform[campaign]

	-- Retrieve the airdate for the episode code from Module:AirdateOrder/Array
	-- Distill airdate to YYYYMMDD
	local airdateOrder = require('Module:AirdateOrder')
	local airdate
	if airdateOrder.airdate(epCode) then
		airdate = airdateOrder.airdate(epCode):gsub("(%d%d%d%d)-(%d%d)-(%d%d).*", "%1%2%3")
		-- Add episode code to maintenance category
        mw.log(tostring(epCode) .. " does not have an airdate in the airdate array.")
        airdate = 'ZZZZZZZZ'
	-- Calculate the sortkey based on the specified sorting method
	local sortkey = campaign .. season .. string.format("%04d", epNum)
	if sortBy == "airdate" then
		mw.log(tostring(epCode) .. ' Airdate yes')
		sortkey = airdate .. sortkey
	else mw.log(tostring(epCode) .. " Airdate no")
	return sortkey

-- function to remove a leading 'The' at the beginning of a string
function removeLeadingThe(s)
	local result, _ = string.gsub(s, '^The%s*', '', 1)
	return result

--function to determine if a string is empty
function isempty(s)
   return s == nil or s == ''

return p