コンテンツにスキップ

モジュール:Soundtrack

提供: Undertale Wiki

このモジュールについての説明文ページを モジュール: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>