モジュール:Soundtrack
このモジュールについての説明文ページを モジュール:Soundtrack/doc に作成できます
--- Handles track title, number, order and motif inference.
-- @module soundtrack
-- @alias p
-- @require Module:User error
-- @author [[User:KockaAdmiralac|KockaAdmiralac]]
-- <nowiki>
local p = {}
require('strict')
-- Module dependencies.
local title = mw.title.getCurrentTitle()
-- Private logic.
--- Generates a Bandcamp widget for a track, with a fallback link in case the
-- widget does not load.
-- @function bandcamp
-- @local
-- @param {number} trackId ID of the track on Bandcamp
-- @param {string} trackName Name of the track
-- @return {string} Bandcamp widget wikitext
local function bandcamp(trackId, trackName)
if trackName == 'The LEGEND...?' then
return 'the-legend-2'
end
trackName = mw.ustring.gsub(trackName, '[ ’]+', '-')
trackName = mw.ustring.gsub(trackName, '[^a-zA-Z0-9-]', '')
trackName = mw.ustring.lower(trackName)
return tostring(mw.html.create('div'):attr({
['class'] = 'bandcamp-widget',
['data-track'] = trackId
}):wikitext(table.concat({
'[https://tobyfox.bandcamp.com/track/',
trackName,
' Link]'
})))
end
--- Generates the "Listen" row in the table with all tracks.
-- The "Listen" row contains a Bandcamp widget if one is available, or a
-- YouTube Music link if it is not.
-- @function formatListenRow
-- @local
-- @param {table} row Row from the Bucket query for the table
-- @return {string} Bandcamp widget or YouTube link wikitext
local function formatListenRow(row)
local trackTitle = row['track.title']
if row['track.bandcamp'] ~= nil then
return bandcamp(row['track.bandcamp'], row['track.title'])
elseif row['track.youtube'] ~= nil then
return string.format(
'[[File:YouTube Music logo.svg|32px|Play %s on YouTube Music.|link=https://music.youtube.com/watch?v=%s]]',
trackTitle,
row['track.youtube']
)
end
end
--- Formats the "Motifs" row in the table with all tracks, and the corresponding
-- track infobox field. Minor motifs are italicized.
-- @function formatMotifs
-- @local
-- @param {table} motifs List of motifs and importance information
-- @return {string} Motifs formatted with links and italics
local function formatMotifs(motifs)
local list = {}
for _, motif in ipairs(motifs) do
if motif.major then
table.insert(list, string.format('[[Leitmotifs#%s|%s]]', motif.motif, motif.motif))
else
table.insert(list, string.format('\'\'[[Leitmotifs#%s|%s]]\'\'', motif.motif, motif.motif))
end
end
return table.concat(list, ', ')
end
--- Parses the "number" field of a track infobox into a list of track numbers
-- and their corresponding albums.
-- @function parseTrackNumberList
-- @local
-- @param {string} number Track number information
-- @return {table} Parsed track number information
local function parseTrackNumberList(number)
local albumMap = {}
local albums = mw.loadData('Module:Soundtrack/album')
for _, album in ipairs(albums) do
albumMap[album.id] = album.page_name
end
local trackNumbers = {}
for line in mw.text.gsplit(number, '\n', true) do
local trackNumStr, albumIds = mw.ustring.match(line, '^%*?%s*(%d+) %(([^)]+)%)$')
if trackNumStr ~= nil then
local albums = {}
for albumId in mw.text.gsplit(albumIds, ', ', true) do
local albumName = albumMap[albumId]
if albumName ~= nil then
table.insert(albums, {
id = albumId,
name = albumName,
})
end
end
if #albums > 0 then
table.insert(trackNumbers, {
number = tonumber(trackNumStr),
albums = albums,
})
end
end
end
return trackNumbers
end
-- Package items.
--- Records data about a track into [[Bucket:Track]].
-- @function trackData
-- @param {table} frame Scribunto frame object
function p.trackData(frame)
local args = frame:getParent().args
bucket('track').put({
title = args.title or title.fullText,
time = args.time,
location = args.location,
bandcamp = args.bandcamp,
spotify = args.spotify,
youtube = args.youtube,
apple = args.apple,
deezer = args.deezer,
})
end
--- Formats the list of track numbers of a track in all albums for the current
-- track page, and inserts track-belongs-to-album relationship data into
-- [[Bucket:Track album]].
-- @function p.trackNumList
-- @param {table} frame Scribunto frame object
-- @return {string} Wikitext list of track numbers for all albums
function p.trackNumList(frame)
local trackNumbers = parseTrackNumberList(frame.args[1])
local lineList = {}
for _, track in ipairs(trackNumbers) do
local albumList = {}
for _, album in ipairs(track.albums) do
local line = {}
table.insert(line, '[[')
table.insert(line, album.name)
table.insert(line, '|')
table.insert(line, album.id)
table.insert(line, ']]')
if title.namespace == 0 then
bucket('track_album').put({
album = album.name,
number = track.number,
})
table.insert(line, '[[Category:')
table.insert(line, album.name)
table.insert(line, ']]')
end
table.insert(albumList, table.concat(line))
end
table.insert(lineList, string.format(
'* %s (%s)',
track.number,
table.concat(albumList, ', ')
))
end
return table.concat(lineList, '\n')
end
--- Returns a track's displayed title.
-- Usually, this only italicizes the page title.
-- If a page is named in the format "X (Soundtrack)", then " (Soundtrack)" is
-- appended to the end of the displayed title, outside of italics.
-- @function p.trackDisplayTitle
-- @return {string} Title of the track to be passed to DISPLAYTITLE
function p.trackDisplayTitle(frame)
local displayTitle = {'\'\'', frame.args[1], '\'\''}
if mw.ustring.match(title.fullText, ' %(Soundtrack%)$') then
table.insert(displayTitle, ' (Soundtrack)')
end
return table.concat(displayTitle)
end
--- Returns track links for the next/previous navigation in the track infobox.
-- @function p.albumNav
-- @param {table} frame Scribunto frame object
-- @return {string} Next/previous track link(s), if there are any
function p.trackNav(frame)
local trackNumbers = parseTrackNumberList(frame:getParent().args.number)
local offset = tonumber(frame.args[1])
local pageName = frame.args[2] or title.fullText
local conditions = {}
for _, track in ipairs(trackNumbers) do
for _, album in ipairs(track.albums) do
table.insert(conditions, {
['track_album.number'] = track.number + offset,
['track_album.album'] = album.name,
})
end
end
local query = bucket('track_album')
.select('track.title', 'track.page_name', 'album.id', 'album.release')
.join('track', 'track_album.page_name', 'track.page_name')
.join('album', 'track_album.album', 'album.page_name')
.where(bucket.Or(unpack(conditions)))
.orderBy('album.release')
.run()
local trackMap = {}
local trackList = {}
for _, row in ipairs(query) do
local trackTitle = row['track.title']
local albumId = row['album.id']
if trackMap[trackTitle] == nil then
table.insert(trackList, {
title = trackTitle,
page = row['track.page_name'],
albums = {albumId},
})
trackMap[trackTitle] = #trackList
else
table.insert(trackList[trackMap[trackTitle]].albums, albumId)
end
end
if #trackList == 1 then
return string.format(
'\'\'[[%s|%s]]\'\'',
trackList[1].page,
trackList[1].title
)
else
local lineList = {}
for _, track in ipairs(trackList) do
table.insert(lineList, string.format(
'* \'\'[[%s|%s]]\'\' (%s)',
track.page,
track.title,
table.concat(track.albums, ', ')
))
end
return table.concat(lineList, '\n')
end
end
--- Returns a Bandcamp or Spotify widget for the current track page.
-- @function p.listen
-- @param {table} frame Scribunto frame object
-- @return {string} Bandcamp or Spotify widget wikitext
function p.listen(frame)
local args = frame:getParent().args
local bandcampId = tonumber(args.bandcamp)
local spotifyId = args.spotify
if bandcampId ~= nil then
return bandcamp(bandcampId, args.title or title.fullText)
elseif spotifyId ~= nil then
return tostring(mw.html.create('div'):attr({
['class'] = 'bandcamp-widget',
['data-track'] = spotifyId,
['data-platform'] = 'spotify'
}):wikitext(table.concat({
'[https://open.spotify.com/track/',
spotifyId,
' Link]'
})))
end
end
--- Links to tracks on various distribution platforms.
-- @function p.distribution
-- @param {table} frame Scribunto frame object
-- @return {string} Div element with relevant track icon links
function p.distribution(frame)
local data = mw.loadData('Module:Soundtrack/data')
local disttype = frame.args[1]
local args = frame:getParent().args
local links = {'<div class="soundtrack-links">'}
local hasAny = false
for platform, pdata in pairs(data.platforms) do
local id = args[platform]
if id ~= nil then
hasAny = true
table.insert(links, '[[File:')
table.insert(links, pdata.icon)
table.insert(links, '|32px|')
table.insert(links, pdata.title)
table.insert(links, '|link=')
table.insert(links, pdata[disttype])
table.insert(links, id)
table.insert(links, ']]')
end
end
if not hasAny then
return ''
end
table.insert(links, '</div>')
return table.concat(links)
end
--- Formats the list of motifs present in the current track page.
-- @function p.motifs
-- @param {table} frame Scribunto frame object
-- @return {string} Formatted motifs of the current track
function p.motifs(frame)
return formatMotifs(
bucket('track_motif')
.select('motif', 'major')
.where('track', frame.args[1] or title.fullText)
.run()
)
end
--- Automatically links track author names in a provided list.
-- @function p.authors
-- @param {table} frame Scribunto frame object
-- @return {string} Author list with links on individual names
function p.authors(frame)
local data = mw.loadData('Module:Soundtrack/data')
local authors = frame.args[1]
local processedAuthors = {}
for author in mw.text.gsplit(authors, ', ', true) do
if data.authors[author] then
table.insert(processedAuthors, string.format(
'[[%s|%s]]',
data.authors[author],
author
))
else
table.insert(processedAuthors, author)
end
end
return table.concat(processedAuthors, ', ')
end
--- Stores data about the current album into [[Bucket:Album]].
-- @function p.albumData
-- @param {table} frame Scribunto frame object
function p.albumData(frame)
local args = frame:getParent().args
local Date = require('Module:Date')
if args.id ~= nil then
bucket('album').put({
id = args.id,
release = Date(args.release):fmt('%Y-%m-%d'),
})
end
if title.namespace == 0 then
return '[[Category:Music]]'
end
end
--- Returns album links for the next/previous navigation in the album infobox.
-- @function p.albumNav
-- @param {table} frame Scribunto frame object
-- @return {string|nil} Next/previous album link, or nil if there
-- isn't one
function p.albumNav(frame)
local albums = mw.loadData('Module:Soundtrack/album')
local offset = tonumber(frame.args[1])
local albumName = frame.args[2] or title.fullText
for index, album in ipairs(albums) do
if album.page_name == albumName and albums[index + offset] ~= nil then
return table.concat({
'\'\'[[',
albums[index + offset].page_name,
']]\'\''
})
end
end
end
--- Creates a table with all tracks in an album for album pages.
-- @function p.albumTable
-- @param {table} frame Scribunto frame object
-- @return {string} Table with all tracks in the current album
function p.albumTable(frame)
local albumName = frame.args[1] or title.fullText
local query = bucket('track_album')
.join('track', 'track_album.page_name', 'track.page_name')
.join('track_motif', 'track.page_name', 'track_motif.track')
.select(
'track_album.number',
'track.page_name',
'track.title',
'track.time',
'track.location',
'track.bandcamp',
'track.youtube',
'track_motif.motif',
'track_motif.major'
)
.where('track_album.album', albumName)
.orderBy('track_album.number')
.run()
local tracks = {}
for _, row in ipairs(query) do
if #tracks == 0 or tracks[#tracks].page ~= row['track.page_name'] then
table.insert(tracks, {
number = row['track_album.number'],
page = row['track.page_name'],
title = row['track.title'],
time = row['track.time'],
location = row['track.location'],
listen = formatListenRow(row),
motifs = {},
})
end
if row['track_motif.motif'] ~= nil then
table.insert(tracks[#tracks].motifs, {
motif = row['track_motif.motif'],
major = row['track_motif.major'],
})
end
end
local trackTable = mw.html.create('table')
:addClass('wikitable')
:addClass('soundtrack-table')
:tag('tr')
:tag('th')
:addClass('nowrap')
:wikitext('No.')
:done()
:tag('th')
:wikitext('Title')
:done()
:tag('th')
:addClass('nowrap')
:wikitext('Length')
:done()
:tag('th')
:wikitext('Location(s) Played')
:done()
:tag('th')
:tag('abbr')
:attr('title', 'Motifs categorized as minor are italicized.')
:wikitext('Leitmotifs')
:done()
:done()
:tag('th')
:wikitext('Listen')
:done()
:done()
for _, track in ipairs(tracks) do
trackTable:tag('tr')
:tag('td')
:addClass('nowrap')
:wikitext(track.number)
:done()
:tag('td')
:wikitext(string.format('[[%s|%s]]', track.page, track.title))
:done()
:tag('td')
:addClass('nowrap')
:wikitext(track.time)
:done()
:tag('td')
:newline()
:wikitext(track.location)
-- Second newline needed because of cases where the generated
-- HTML is like:
-- * [[Sans]]'s introduction</td>...</tr><tr>...<td>
-- * Encountering [[Papyrus]] for the first time</td>...
-- This makes the wikitext parser interpret the two bullet
-- points from completely different table cells as if they were
-- in the same list, which messes up the list in the second
-- table cell.
:newline()
:done()
:tag('td')
:wikitext(formatMotifs(track.motifs))
:done()
:tag('td')
:wikitext(track.listen)
:done()
:done()
end
return tostring(trackTable:done())
end
--- Stores motif data into [[Bucket:Track motif]] from the [[Leitmotifs]] page.
-- @function p.motifData
-- @param {table} frame Scribunto frame object
function p.motifData(frame)
local currentMotif = nil
local major = true
for line in mw.text.gsplit(title:getContent(), '\n', true) do
local s = mw.ustring.sub(line, 1, 3)
if s == '== ' then
local title = mw.ustring.sub(line, 4, -4)
if title ~= 'Minor Leitmotifs' then
currentMotif = title
major = true
end
elseif s == '===' then
currentMotif = mw.ustring.sub(line, 5, -5)
major = false
elseif s == '* \'' and currentMotif and currentMotif ~= 'Other' then
local linkContent = mw.ustring.match(line, '%* \'\'%[%[([^%]]+)%]%]\'\'')
if linkContent then
local linkPage = mw.text.split(linkContent, '|', true)[1]
local normalizedPage = mw.title.new(linkPage).fullText
bucket('track_motif').put({
track = normalizedPage,
motif = currentMotif,
major = major,
})
end
end
end
end
--- Returns an album's display title.
-- If the current page is a regular album page, the infobox will have the "id"
-- parameter set, so the title should be italicized. Otherwise, we might be
-- invoking this from a page such as [[Unused Music Tracks]].
-- @function p.trackDisplayTitle
-- @return {string} Title of the track to be passed to DISPLAYTITLE
function p.albumDisplayTitle(frame)
if frame:getParent().args.id then
return string.format('\'\'%s\'\'', title.fullText)
end
return title.fullText
end
return p
-- </nowiki>