Module:Soundtrack

From Undertale Yellow Wiki
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)" &rarr; "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>