Module:Soundtrack
Jump to navigation
Jump to search
Implements navigation between tracks for Template:Infobox music.
Documentation
Package items
soundtrack.getFrmTrack(articleName)
(function)- Gets track name from raw article name. e.g. "Alphys (track)" → "Alphys"
- Parameter:
articleName
Article title (string) - Returns: Track name (string)
soundtrack.getListing(trackPage)
(function)- Gets track's index in track list from frame.
- Parameter:
trackPage
Track's article title (used as an override) (string|nil) - Errors:
- "Something has gone horribly wrong in Module:Soundtrack." (string; line 118)
- "This article is not a valid soundtrack page" (string; line 142)
- Returns: Track's index and whether it is in multiple albums. (number, boolean)
soundtrack.incr_track_wrapper(incr)
(function)- Wrapper function for getting the next and previous track.
- Parameter:
incr
How much to increment the current's track index to get to the next track. (number) - Returns: Wrapped function (function)
soundtrack.next_track()
(function)- Gets the next track field.
- Returns: Next track field's contents (string)
soundtrack.prev_track()
(function)- Gets the previous track field.
- Returns: Next track field's contents (string)
soundtrack.curr_track(frame)
(function)- Gets the current track's title.
- Parameter:
frame
Scribunto frame object (table) - Returns: Current track's title, and accompanying DISPLAYTITLE
soundtrack.get_albums(frame)
(function)- Returns the names of the albums that the current track (current article) belongs in.
- Parameter:
frame
Scribunto frame object (table) - Returns: Albums that the current track belongs to (string)
soundtrack.get_num(frame)
(function)- Gets the current track number in the OST.
- Parameter:
frame
Scribunto frame object (table) - Returns: Current track number, or a list of them if it belongs to multiple albums and a specific album was not given (string)
soundtrack.get_url(frame)
(function)- Gets the URL of a track's music file from Game Jolt CDN.
- Parameter:
frame
Scribunto frame object (table) - Returns: Track's audio file URL from Game Jolt CDN (string)
soundtrack.leitmotifs()
(function)- Gets leitmotifs of the current track by reading them from the Leitmotifs page.
- Returns: Leitmotifs of the current track (string)
soundtrack.authors(frame)
(function)- Automatically links track author names in a provided list.
- Parameter:
frame
Scribunto frame object (table) - Returns: Author list with links on individual names (string)
--- Implements navigation between tracks for [[Template:Infobox music]].
-- @module soundtrack
-- @alias p
-- @require Dev:User error
-- @author [[User:The JoTS|The JoTS]]
-- <nowiki>
local p = {}
require('strict')
-- Module dependencies
local ostList = mw.loadData "Module:Soundtrack/OST List" -- read-only, psuedo-table, use pairs() and *not* next()
local userError = require "Dev:User error"
-- Private logic.
--- A wrapper function that will catch uny uncaught errors in func and display
-- the error to the editor.
-- @function nag_wrap
-- @param {function} func Function whose errors should be caught
-- @returns {function} A wrapped function with described behavior
-- @local
local function nag_wrap(func)
return function(...)
local results = { pcall(func, ...) }
local success = results[1]
table.remove(results, 1) -- remove success arg from results
if success then
return unpack(results)
else
return userError(results[1], "Pages with script errors")
end
end
end
--- Returns whether the % (force article name) sentinel is appended to the name,
-- and returns the real article name.
-- For cases such as "Home (Music box)".
-- @function p.isNameForced
-- @param {string} name Article name to be checked
-- @return {string, boolean} Real article title, and whether the
-- article name is forced
local function isNameForced(name)
local frmName,forced = name:gsub("%%$",'')
forced = forced > 0
return frmName, forced
end
p.isNameForced = isNameForced
--- Normalizes track names in the following way:
-- 1. Turns the track name lowercase (so it can properly match "sans.")
-- 2. Removes underscores from the track name (so it can properly match
-- "Soulmate_Located")
-- 3. Trims the track name (so it can properly match "END OF THE LINE_")
-- This is used when matching a track name with its corresponding article name.
-- @function normalizeTrackName
-- @param {string} name Track name to be normalized
-- @return {string} Normalized track name
-- @local
local function normalizeTrackName(name)
local lowerName = mw.ustring.lower(name)
local noUnderscores, _ = mw.ustring.gsub(lowerName, '_', ' ')
local trimmed = mw.text.trim(noUnderscores)
return trimmed
end
--- Creates a MediaWiki link.
-- @function formatLink
-- @param {string} name Text in the link
-- @param[opt] {string} link Page to link to (article name is used when
-- not specified)
local function formatLink(name, link)
local name,nameForced = isNameForced(name)
local link = nameForced and name or link
return (link == name or (not link))
and table.concat({"[[", name, "]]"})
or table.concat({"[[", link, '|', name, "]]"})
end
--- "Shorthand" function to check all OSTs.
-- May ultimately diminish readability by running loop in another func, but...
-- @function chkOST
-- @param {function} Function to run on each album
-- @local
local function chkOST(func)
for ost,list in pairs(ostList.data) do
func(ost, list)
end
end
-- Package items.
--- Gets track name from raw article name.
-- e.g. "Alphys (track)" → "Alphys"
-- @function p.getFrmTrack
-- @param {string} articleName Article title
-- @returns {string} Track name
function p.getFrmTrack(articleName)
return articleName:gsub("%s+%b()$", "")
end
--- Gets track's index in track list from frame.
-- @function p.getListing
-- @param {string|nil} trackPage Track's article title (used as an
-- override)
-- @returns {number, boolean} Track's index and whether it is in
-- multiple albums.
-- @error[118] {string} "Something has gone horribly wrong in
-- [[Module:Soundtrack]]."
-- @error[142] {string} "This article is not a valid soundtrack page"
function p.getListing(trackPage)
local trackPg = trackPage
and mw.title.new(trackPage)
or mw.title.getCurrentTitle()
local normTrack = trackPg and normalizeTrackName(trackPg.fullText)
or error("Something has gone horribly wrong in [[Module:Soundtrack]].")
local trackNum = {} -- to hold the track number(s)
local multiOST = 0 -- a counter of the number of OSTs the soundtrack is present in
-- Example case for documentation
if trackPg.namespace == 10 then -- I'm just gonna do it for the entire namespace.
-- change "OST" below to some key in /OST List, should "OST" be changed there.
return { OST = 14 }, false
end
chkOST(function(ost, list)
-- Perform a linear search
for i,name in pairs(list) do
-- NOTE: Leave compared strings in normalized form.
if normalizeTrackName(ostList.redirects[name] or isNameForced(name)) == normTrack then
-- Track found
trackNum[ost] = i
multiOST = multiOST + 1
break -- only breaks the inner-most loop
end
end
end)
assert(multiOST > 0, "The article '" ..
trackPg.fullText ..
"' is not a valid soundtrack page. Please visit Module:Soundtrack/OST_List to check if everything is in order!"
)
return trackNum, multiOST > 1
end
--- Wrapper function for getting the next and previous track.
-- @function p.incr_track_wrapper
-- @param {number} incr How much to increment the current's track
-- index to get to the next track.
-- @returns {function} Wrapped function
function p.incr_track_wrapper(incr)
return function(frame)
local str = {} -- hold the sequential track info
local trackPresent = {} -- holds keys of tracks and string vals regarding which OST lists the track as present
local trackCount = 0 -- counter for number of tracks to be returned
local indices,multiOST = p.getListing()
-- Get sequential track information
chkOST(function(ost, list)
local rTrack = indices[ost] and list[indices[ost]+ incr] -- raw track name
if rTrack then
trackCount = trackCount + (trackPresent[rTrack] and 0 or 1) -- increment
trackPresent[rTrack] = table.concat({trackPresent[rTrack] or "", ost, " + "})
end
end)
-- Generate string
for rTrack, info in pairs(trackPresent) do
local album = info:sub(1, -4)
table.insert(str, formatLink(p.getFrmTrack(rTrack), rTrack))
if trackCount ~= 1 or album ~= "OST" then
table.insert(str, " (")
table.insert(str, album)
table.insert(str, ")\n")
end
end
return table.concat(str)
end
end
--- Gets the next track field.
-- @function p.next_track
-- @returns {string} Next track field's contents
p.next_track = nag_wrap(p.incr_track_wrapper( 1))
--- Gets the previous track field.
-- @function p.prev_track
-- @returns {string} Next track field's contents
p.prev_track = nag_wrap(p.incr_track_wrapper(-1))
--- Gets the current track's title.
-- @function p.curr_track
-- @param {table} frame Scribunto frame object
-- @returns Current track's title, and accompanying DISPLAYTITLE
p.curr_track = nag_wrap(function(frame)
local indicies = p.getListing(frame.args[1])
local name = ''
local forced = false
chkOST(function(ost, list)
if indicies[ost] then
name = list[indicies[ost]]
name = ostList.redirects[name] or name
name, forced = isNameForced(name)
name = forced
and name
or p.getFrmTrack(name)
end
end)
return name ~= ''
and frame:preprocess(table.concat({ name, '{{DISPLAYTITLE:', name, '}}' }))
or mw.title.getCurrentTitle().fulltext
end)
--- Returns the names of the albums that the current track (current article)
-- belongs in.
-- @function p.get_albums
-- @param {table} frame Scribunto frame object
-- @returns {string} Albums that the current track belongs to
p.get_albums = function(frame)
local albums = p.getListing()
local str = {}
for ost in pairs(albums) do
table.insert(str, formatLink(ost))
table.insert(str, '\n')
end
return table.concat(str)
end
--- Gets the current track number in the OST.
-- @function p.get_num
-- @param {table} frame Scribunto frame object
-- @returns {string} Current track number, or a list of them if it
-- belongs to multiple albums and a specific album
-- was not given
function p.get_num(frame)
local albums, multi = p.getListing(frame.args[1])
local album = frame.args[2]
local str = {}
local createList = multi and album == nil
for ost, id in pairs(albums) do
if album == nil or album == ost then
if createList then
table.insert(str, '* ')
end
table.insert(str, id)
if createList then
table.insert(str, ' (')
table.insert(str, ost)
table.insert(str, ')\n')
end
end
end
return table.concat(str)
end
--- Gets the URL of a track's music file from Game Jolt CDN.
-- @function p.get_url
-- @param {table} frame Scribunto frame object
-- @returns {string} Track's audio file URL from Game Jolt CDN
function p.get_url(frame)
local trackPage = frame.args[1]
and mw.title.new(frame.args[1])
or mw.title.getCurrentTitle()
if trackPage.namespace == 10 then
trackPage = mw.title.new('Forlorn')
end
local albums, multi = p.getListing(trackPage.text)
local trackNum = nil
local realTrackName = trackPage.fullText
for num, trackName in pairs(ostList.data['OST']) do
local trackNameStripped, forced = isNameForced(trackName)
if normalizeTrackName(trackPage.fullText) == normalizeTrackName(trackNameStripped) then
trackNum = num
realTrackName = forced
and trackNameStripped
or p.getFrmTrack(trackNameStripped)
end
end
if not trackNum then
return ''
end
local paddedNum = mw.ustring.format('%03d', trackNum)
local normalizedName = mw.ustring.lower(realTrackName)
-- This is a hack for the discrepancy between Game Jolt names for specimen
-- tracks and their names on all other platforms
normalizedName = mw.ustring.gsub(normalizedName, ': ', ' ')
-- We can't use mw.ustring.gsub because Game Jolt does byte-wise replacement
normalizedName = normalizedName:gsub('[^0-9a-z_.]', '-')
local trackUrl = {
paddedNum,
'---',
normalizedName
}
if ostList.suffixes[trackPage.text] then
table.insert(trackUrl, '-')
table.insert(trackUrl, ostList.suffixes[trackPage.text])
end
table.insert(trackUrl, '.mp3')
return table.concat(trackUrl)
end
--- Gets leitmotifs of the current track by reading them from the [[Leitmotifs]]
-- page.
-- @returns {string} Leitmotifs of the current track
function p.leitmotifs()
local leitmotifsPage = mw.title.new('Leitmotifs')
local currentTrack = mw.title.getCurrentTitle().fullText
local leitmotifs = {}
if not leitmotifsPage.exists then
error('Fatal error: Leitmotifs page does not exist!')
end
local currentLeitmotif
local major = true
for line in mw.text.gsplit(leitmotifsPage:getContent(), '\n', true) do
local s = mw.ustring.sub(line, 1, 3)
if s == '== ' then
local title = mw.ustring.sub(line, 4, -4)
major = title ~= 'Minor Leitmotifs'
if major then
currentLeitmotif = title
end
elseif s == '===' then
currentLeitmotif = mw.ustring.sub(line, 5, -5)
elseif s == '* \'' and currentLeitmotif and currentLeitmotif ~= 'Other' then
local linkContent = mw.ustring.match(line, '%* \'\'%[%[([^%]]+)%]%]\'\'')
if linkContent then
local normalizedTrack = normalizeTrackName(mw.text.split(linkContent, '|', true)[1])
if normalizedTrack == normalizeTrackName(currentTrack) then
local formatting = major and '' or '\'\''
table.insert(leitmotifs, table.concat({
formatting,
'[[Leitmotifs#',
currentLeitmotif,
'|',
currentLeitmotif,
']]',
formatting
}))
end
end
end
end
return table.concat(leitmotifs, ', ')
end
--- Automatically links track author names in a provided list.
-- @function p.authors
-- @param {table} frame Scribunto frame object
-- @returns {string} Author list with links on individual names
function p.authors(frame)
local authors = frame.args[1]
local processedAuthors = {}
for author in mw.text.gsplit(authors, ', ', true) do
if ostList.authors[author] then
table.insert(processedAuthors, table.concat({
'[[',
ostList.authors[author],
'|',
author,
']]'
}))
else
table.insert(processedAuthors, author)
end
end
return table.concat(processedAuthors, ', ')
end
return p
-- </nowiki>