Module:Studio sessions: Difference between revisions
From PickiPedia: A knowledge base of bluegrass, old time psychedelic jams, and other public domain music
Jump to navigationJump to search
Split into Creative Commons and other studio work, and name whose record it is (via update-page on MediaWiki MCP Server) |
Move recordings onto composition pages: records and musicians now read them rather than holding them (via update-page on MediaWiki MCP Server) |
||
| Line 1: | Line 1: | ||
-- Studio | -- Studio recordings: who played what, on which recording of which composition. | ||
-- | -- | ||
-- The | -- The composition is the page. A recording of it is a subobject on that page, | ||
-- | -- naming the record it was cut for and everyone who played on it. See | ||
-- | -- PickiPedia:Compositions. | ||
-- | -- | ||
-- | -- Everything else reads that. A record page asks which compositions name it; a | ||
-- | -- musician's page asks which recordings name them. Neither keeps its own copy, | ||
-- so a lineup is corrected in one place. | |||
-- | |||
-- | -- | ||
-- Note for future editors: do not wrap this header in a --[==[ ]==] block | -- Note for future editors: do not wrap this header in a --[==[ ]==] block | ||
| Line 16: | Line 15: | ||
local p = {} | local p = {} | ||
-- Parameters of {{Studio | -- Parameters of {{Studio version}} that describe the session rather than name | ||
-- player. Anything else is a musician. | -- a player. Anything else is a musician. | ||
local NOT_A_PLAYER = { | local NOT_A_PLAYER = { | ||
record = true, | |||
number = true, | number = true, | ||
studio = true, | studio = true, | ||
recorded = true, | recorded = true, | ||
engineer = true, | engineer = true, | ||
} | } | ||
| Line 44: | Line 42: | ||
-- | -- | ||
-- #invoke gives the module only the arguments written in the invoke itself, | -- #invoke gives the module only the arguments written in the invoke itself, | ||
-- which for a one-line wrapper | -- which for a one-line wrapper is none at all; the template's own parameters | ||
-- are on the parent frame. pairs(), not next(): Scribunto's args table is lazy, | |||
-- | -- so next() reports no arguments on a call that has plenty. | ||
local function callArgs( frame ) | local function callArgs( frame ) | ||
local parent = frame:getParent() | local parent = frame:getParent() | ||
if parent then | if parent then | ||
for _ in pairs( parent.args ) do | for _ in pairs( parent.args ) do | ||
return parent.args | return parent.args | ||
| Line 61: | Line 55: | ||
end | end | ||
-- | -- Everything the parser wraps around a value that is not part of the value: | ||
-- | -- the strip marker, SMW's span markup, and SMW's own [[SMW::off]] switch, | ||
-- | -- which brackets template-formatted results. Left in, a page name is not a | ||
-- name: an | -- name — "[[" .. it .. "]]" renders as a warning about a property label | ||
-- containing "[". | |||
local function plain( text ) | |||
text = mw.text.unstrip( text or "" ) | |||
text = text:gsub( "\127[^\127]*\127", "" ):gsub( "<[^>]+>", "" ) | |||
return ( text:gsub( "%[%[SMW::o[nf]f?%]%]", "" ) ) | |||
end | |||
-- A pipe or an equals sign would end a parameter early. | |||
local function escape( value ) | |||
return ( value:gsub( "|", "{{!}}" ):gsub( "=", "{{=}}" ) ) | |||
end | |||
-- The title without its namespace, for showing rather than linking. | |||
local function bare( title ) | |||
return ( title:gsub( "^[^:]+:", "" ) ) | |||
end | |||
local function players( args ) | local function players( args ) | ||
local named = {} | local named = {} | ||
| Line 76: | Line 87: | ||
end | end | ||
-- Record one player on one recording. | |||
-- Record one player on one | |||
-- | -- | ||
-- Written as wikitext and run back through the preprocessor rather than called | -- Written as wikitext and run back through the preprocessor rather than called | ||
-- with frame:callParserFunction | -- with frame:callParserFunction: this wiki has no Semantic Scribunto, and a | ||
-- #subobject invoked that way renders and is then forgotten | -- #subobject invoked that way renders and is then forgotten. | ||
local function annotate( frame, record, number, player ) | |||
local function annotate( frame, | |||
local parts = { | local parts = { | ||
"Has | "Has record=" .. escape( record ), | ||
"Has performer=" .. escape( player.name ), | "Has performer=" .. escape( player.name ), | ||
} | } | ||
| Line 117: | Line 106: | ||
end | end | ||
-- One | -- One studio recording of this composition. | ||
function p. | function p.version( frame ) | ||
local args = callArgs( frame ) | local args = callArgs( frame ) | ||
local | local record = trim( args.record ) | ||
if | if record == "" then | ||
return '<span class="error">Studio | return '<span class="error">Studio version: no record given</span>' | ||
end | end | ||
local number = trim( args.number ) | local number = trim( args.number ) | ||
local shown = {} | local shown = {} | ||
for _, player in ipairs( | for _, player in ipairs( players( args ) ) do | ||
annotate( frame, | annotate( frame, record, number, player ) | ||
-- The icon comes from the instrument played here, not from whatever | -- The icon comes from the instrument played here, not from whatever | ||
-- musician's page calls their main one: a guitarist who played | -- the musician's page calls their main one: a guitarist who played | ||
-- | -- dobro on this session played dobro. | ||
local first = split( player.instruments )[1] or "" | local first = split( player.instruments )[1] or "" | ||
local link = frame:expandTemplate{ title = "m", args = { player.name, first } } | local link = frame:expandTemplate{ title = "m", args = { player.name, first } } | ||
| Line 142: | Line 130: | ||
end | end | ||
local session = {} | local session = {} | ||
for _, field in ipairs( { "recorded", "studio", "engineer" } ) do | for _, field in ipairs( { "recorded", "studio", "engineer" } ) do | ||
| Line 152: | Line 139: | ||
local out = { '<div style="padding:.5em .2em; border-bottom:1px solid #d4dacd;">' } | local out = { '<div style="padding:.5em .2em; border-bottom:1px solid #d4dacd;">' } | ||
table.insert( out, '<div style="font-weight:600;">' .. | table.insert( out, '<div style="font-weight:600;">[[' .. record .. "]]" | ||
.. ( number ~= "" and ( ' <span style="font-weight:400; color:#6b7566;">track ' | |||
.. number .. "</span>" ) or "" ) .. "</div>" ) | |||
if #shown > 0 then | if #shown > 0 then | ||
table.insert( out, '<div style="font-size:.95em; line-height:1.9;">' | table.insert( out, '<div style="font-size:.95em; line-height:1.9;">' | ||
| Line 165: | Line 154: | ||
end | end | ||
-- One property of one page, as plain text | -- One property of one page, as plain text. | ||
local function property( frame, page, name ) | local function property( frame, page, name ) | ||
return trim( plain( frame:callParserFunction( "#show", { | return trim( plain( frame:callParserFunction( "#show", { | ||
| Line 183: | Line 170: | ||
end | end | ||
-- Every | -- Rows of "first||second||third||fourth", from Template:Studio appearance row. | ||
local function rows( frame, query, printouts, mainlabel ) | |||
local args = { query } | |||
for _, printout in ipairs( printouts ) do | |||
table.insert( args, printout ) | |||
end | |||
for _, option in ipairs( { | |||
"mainlabel=" .. mainlabel, "format=plainlist", "link=none", "sep=@@", | |||
"template=Studio appearance row", "searchlabel=", "limit=500", | |||
} ) do | |||
table.insert( args, option ) | |||
end | |||
local raw = plain( frame:callParserFunction( "#ask", args ) ) | |||
local out = {} | |||
if trim( raw ) == "" then | |||
return out | |||
end | |||
for _, row in ipairs( mw.text.split( raw, "@@", true ) ) do | |||
local a, b, c, d = row:match( "^(.-)||(.-)||(.-)||(.*)$" ) | |||
if a and trim( a ) ~= "" then | |||
-- A subobject's subject prints as "Song:Barlows#_abc123"; the page | |||
-- is the part before the fragment. | |||
table.insert( out, { | |||
subject = trim( ( a:match( "^([^#]*)" ) or a ) ), | |||
b = trim( b ), c = trim( c ), d = trim( d ), | |||
} ) | |||
end | |||
end | |||
return out | |||
end | |||
local function subjectOf( frame ) | |||
local given = trim( callArgs( frame )[1] or frame.args[1] ) | |||
if given ~= "" then | |||
return given | |||
end | |||
return mw.title.getCurrentTitle().fullText | |||
end | |||
-- Every recording a musician played on, grouped by record. | |||
-- | -- | ||
-- @param wanted "free" for records anybody may share, "rest" for the others. | -- @param wanted "free" for records anybody may share, "rest" for the others. | ||
local function appearances( frame, wanted, heading ) | |||
local function | local who = subjectOf( frame ) | ||
local who = | local found = rows( frame, "[[Has performer::" .. who .. "]]", { | ||
"?Has record=record", | |||
local | |||
"?Has | |||
"?Has track number=number", | "?Has track number=number", | ||
"?Has instrument=instrument", | "?Has instrument=instrument", | ||
}, "song" ) | |||
local order, byRecord = {}, {} | local order, byRecord = {}, {} | ||
for _, row in ipairs( | for _, row in ipairs( found ) do | ||
local record | local record = row.b | ||
if record | if record ~= "" then | ||
if not byRecord[ record ] then | if not byRecord[ record ] then | ||
byRecord[ record ] = { | byRecord[ record ] = { songs = {}, instruments = {}, seen = {} } | ||
table.insert( order, record ) | table.insert( order, record ) | ||
end | end | ||
local entry = byRecord[ record ] | local entry = byRecord[ record ] | ||
if | if not entry.seen[ row.subject ] then | ||
entry.seen[ | entry.seen[ row.subject ] = true | ||
table.insert( entry. | table.insert( entry.songs, | ||
{ title = row.subject, number = tonumber( row.c ) } ) | |||
end | end | ||
for _, one in ipairs( split( | for _, one in ipairs( split( row.d ) ) do | ||
if not entry.instruments[ one ] then | if not entry.instruments[ one ] then | ||
entry.instruments[ one ] = true | entry.instruments[ one ] = true | ||
| Line 242: | Line 247: | ||
for _, record in ipairs( order ) do | for _, record in ipairs( order ) do | ||
local entry = byRecord[ record ] | local entry = byRecord[ record ] | ||
table.sort( entry. | local isFree, licence = freelyLicensed( frame, record ) | ||
if ( wanted == "free" ) == isFree then | |||
table.sort( entry.songs, function( a, b ) | |||
if a.number and b.number then return a.number < b.number end | |||
return a.title < b.title | |||
end ) | |||
local titles = {} | |||
for _, song in ipairs( entry.songs ) do | |||
table.insert( titles, "[[" .. song.title .. "|" .. bare( song.title ) .. "]]" ) | |||
end | |||
local artist = property( frame, record, "Has record artist" ) | local artist = property( frame, record, "Has record artist" ) | ||
local line = { | local line = { | ||
'<div style="padding:.45em .2em; border-bottom:1px solid #d4dacd;">', | '<div style="padding:.45em .2em; border-bottom:1px solid #d4dacd;">', | ||
'<div style="font-weight:600;">[[' .. record .. "]]", | '<div style="font-weight:600;">[[' .. record .. "]]", | ||
} | } | ||
if artist ~= "" then | if artist ~= "" then | ||
table.insert( line, ' <span style="font-weight:400; color:#5a6858;">by [[' | table.insert( line, ' <span style="font-weight:400; color:#5a6858;">by [[' | ||
| Line 273: | Line 275: | ||
table.insert( line, "</div>" ) | table.insert( line, "</div>" ) | ||
table.insert( line, '<div style="font-size:.9em; color:#3d4a3e;">' | table.insert( line, '<div style="font-size:.9em; color:#3d4a3e;">' | ||
.. ( instruments | .. ( #entry.instruments > 0 | ||
and ( table.concat( entry.instruments, ", " ) .. " · " ) or "" ) | |||
.. table.concat( titles, ", " ) .. "</div>" ) | |||
table.insert( line, "</div>" ) | table.insert( line, "</div>" ) | ||
table.insert( blocks, table.concat( line, "" ) ) | table.insert( blocks, table.concat( line, "" ) ) | ||
end | end | ||
| Line 285: | Line 286: | ||
return "" | return "" | ||
end | end | ||
table.insert( blocks, 1, "== " .. heading .. " ==" ) | table.insert( blocks, 1, "== " .. heading .. " ==" ) | ||
return table.concat( blocks, "\n" ) | return table.concat( blocks, "\n" ) | ||
end | end | ||
function p.appearances( frame ) | function p.appearances( frame ) | ||
return | return appearances( frame, "free", | ||
"[[PickiPedia:Creative Commons|Creative Commons]] Studio Work" ) | "[[PickiPedia:Creative Commons|Creative Commons]] Studio Work" ) | ||
end | end | ||
function p.other( frame ) | function p.other( frame ) | ||
return | return appearances( frame, "rest", "Other studio work" ) | ||
end | |||
-- The track listing of a record, assembled from the compositions that name it. | |||
-- | |||
-- The record page keeps no list of its own. A track is a recording, recordings | |||
-- live on the composition's page, and a page that copied them here would be | |||
-- one more thing to keep in step. | |||
function p.tracks( frame ) | |||
local record = subjectOf( frame ) | |||
local found = rows( frame, "[[Has record::" .. record .. "]]", { | |||
"?Has track number=number", | |||
"?Has performer=performer", | |||
"?Has instrument=instrument", | |||
}, "song" ) | |||
local order, bySong = {}, {} | |||
for _, row in ipairs( found ) do | |||
if not bySong[ row.subject ] then | |||
bySong[ row.subject ] = { number = tonumber( row.b ), players = {}, seen = {} } | |||
table.insert( order, row.subject ) | |||
end | |||
local entry = bySong[ row.subject ] | |||
entry.number = entry.number or tonumber( row.b ) | |||
if row.c ~= "" and not entry.seen[ row.c ] then | |||
entry.seen[ row.c ] = true | |||
table.insert( entry.players, { name = row.c, instrument = split( row.d )[1] or "" } ) | |||
end | |||
end | |||
if #order == 0 then | |||
return "" | |||
end | |||
table.sort( order, function( a, b ) | |||
local na, nb = bySong[ a ].number, bySong[ b ].number | |||
if na and nb then return na < nb end | |||
if na then return true end | |||
if nb then return false end | |||
return a < b | |||
end ) | |||
local out = {} | |||
for _, song in ipairs( order ) do | |||
local entry = bySong[ song ] | |||
table.sort( entry.players, function( a, b ) return a.name < b.name end ) | |||
local shown = {} | |||
for _, player in ipairs( entry.players ) do | |||
table.insert( shown, frame:expandTemplate{ | |||
title = "m", args = { player.name, player.instrument } } ) | |||
end | |||
local line = { '<div style="padding:.45em .2em; border-bottom:1px solid #d4dacd;">' } | |||
table.insert( line, '<div style="font-weight:600;">' | |||
.. ( entry.number and ( entry.number .. ". " ) or "" ) | |||
.. "[[" .. song .. "|" .. bare( song ) .. "]]</div>" ) | |||
if #shown > 0 then | |||
table.insert( line, '<div style="font-size:.9em; line-height:1.9; color:#3d4a3e;">' | |||
.. table.concat( shown, " · " ) .. "</div>" ) | |||
end | |||
table.insert( line, "</div>" ) | |||
table.insert( out, table.concat( line, "" ) ) | |||
end | |||
return table.concat( out, "\n" ) | |||
end | |||
-- Deprecated: the previous shape, where recordings lived on the record's page | |||
-- and named their track by title. | |||
-- | |||
-- Kept only so the three record pages written that way keep rendering until | |||
-- the importer has moved them onto composition pages. Nothing should call it | |||
-- afterwards; see PickiPedia:Compositions for why the data moved. | |||
function p.track( frame ) | |||
local args = callArgs( frame ) | |||
local track = trim( args.track ) | |||
if track == "" then | |||
return '<span class="error">Studio track: no track name given</span>' | |||
end | |||
local number = trim( args.number ) | |||
local shown = {} | |||
for _, player in ipairs( players( args ) ) do | |||
local parts = { | |||
"Has track name=" .. escape( track ), | |||
"Has performer=" .. escape( player.name ), | |||
} | |||
if number ~= "" then | |||
table.insert( parts, "Has track number=" .. escape( number ) ) | |||
end | |||
for _, instrument in ipairs( split( player.instruments ) ) do | |||
table.insert( parts, "Has instrument=" .. escape( instrument ) ) | |||
end | |||
frame:preprocess( "{{#subobject:|" .. table.concat( parts, "|" ) .. "}}" ) | |||
local first = split( player.instruments )[1] or "" | |||
local link = frame:expandTemplate{ title = "m", args = { player.name, first } } | |||
if player.instruments ~= "" then | |||
link = link .. ' <span style="color:#6b7566;">' .. player.instruments .. "</span>" | |||
end | |||
table.insert( shown, link ) | |||
end | |||
local label = number ~= "" and ( number .. ". " .. track ) or track | |||
return '<div style="padding:.5em .2em; border-bottom:1px solid #d4dacd;">' | |||
.. '<div style="font-weight:600;">' .. label .. "</div>" | |||
.. '<div style="font-size:.95em; line-height:1.9;">' | |||
.. table.concat( shown, " · " ) .. "</div></div>" | |||
end | end | ||
return p | return p | ||
Revision as of 23:42, 18 September 2026
Documentation for this module may be created at Module:Studio sessions/doc
-- Studio recordings: who played what, on which recording of which composition.
--
-- The composition is the page. A recording of it is a subobject on that page,
-- naming the record it was cut for and everyone who played on it. See
-- PickiPedia:Compositions.
--
-- Everything else reads that. A record page asks which compositions name it; a
-- musician's page asks which recordings name them. Neither keeps its own copy,
-- so a lineup is corrected in one place.
--
-- Note for future editors: do not wrap this header in a --[==[ ]==] block
-- comment containing wiki link syntax. Lua reads the inner brackets as nested
-- long brackets and refuses to load the module.
local p = {}
-- Parameters of {{Studio version}} that describe the session rather than name
-- a player. Anything else is a musician.
local NOT_A_PLAYER = {
record = true,
number = true,
studio = true,
recorded = true,
engineer = true,
}
local function trim( s )
return ( s or "" ):gsub( "^%s*(.-)%s*$", "%1" )
end
local function split( raw )
local out = {}
for part in ( ( raw or "" ) .. "," ):gmatch( "%s*(.-)%s*," ) do
if part ~= "" then
table.insert( out, part )
end
end
return out
end
-- The arguments of the template that invoked us.
--
-- #invoke gives the module only the arguments written in the invoke itself,
-- which for a one-line wrapper is none at all; the template's own parameters
-- are on the parent frame. pairs(), not next(): Scribunto's args table is lazy,
-- so next() reports no arguments on a call that has plenty.
local function callArgs( frame )
local parent = frame:getParent()
if parent then
for _ in pairs( parent.args ) do
return parent.args
end
end
return frame.args
end
-- Everything the parser wraps around a value that is not part of the value:
-- the strip marker, SMW's span markup, and SMW's own [[SMW::off]] switch,
-- which brackets template-formatted results. Left in, a page name is not a
-- name — "[[" .. it .. "]]" renders as a warning about a property label
-- containing "[".
local function plain( text )
text = mw.text.unstrip( text or "" )
text = text:gsub( "\127[^\127]*\127", "" ):gsub( "<[^>]+>", "" )
return ( text:gsub( "%[%[SMW::o[nf]f?%]%]", "" ) )
end
-- A pipe or an equals sign would end a parameter early.
local function escape( value )
return ( value:gsub( "|", "{{!}}" ):gsub( "=", "{{=}}" ) )
end
-- The title without its namespace, for showing rather than linking.
local function bare( title )
return ( title:gsub( "^[^:]+:", "" ) )
end
local function players( args )
local named = {}
for name, value in pairs( args ) do
if type( name ) == "string" and not NOT_A_PLAYER[ name ] then
table.insert( named, { name = trim( name ), instruments = trim( value ) } )
end
end
table.sort( named, function( a, b ) return a.name < b.name end )
return named
end
-- Record one player on one recording.
--
-- Written as wikitext and run back through the preprocessor rather than called
-- with frame:callParserFunction: this wiki has no Semantic Scribunto, and a
-- #subobject invoked that way renders and is then forgotten.
local function annotate( frame, record, number, player )
local parts = {
"Has record=" .. escape( record ),
"Has performer=" .. escape( player.name ),
}
if number ~= "" then
table.insert( parts, "Has track number=" .. escape( number ) )
end
for _, instrument in ipairs( split( player.instruments ) ) do
table.insert( parts, "Has instrument=" .. escape( instrument ) )
end
frame:preprocess( "{{#subobject:|" .. table.concat( parts, "|" ) .. "}}" )
end
-- One studio recording of this composition.
function p.version( frame )
local args = callArgs( frame )
local record = trim( args.record )
if record == "" then
return '<span class="error">Studio version: no record given</span>'
end
local number = trim( args.number )
local shown = {}
for _, player in ipairs( players( args ) ) do
annotate( frame, record, number, player )
-- The icon comes from the instrument played here, not from whatever
-- the musician's page calls their main one: a guitarist who played
-- dobro on this session played dobro.
local first = split( player.instruments )[1] or ""
local link = frame:expandTemplate{ title = "m", args = { player.name, first } }
if player.instruments ~= "" then
link = link .. ' <span style="color:#6b7566;">' .. player.instruments .. "</span>"
end
table.insert( shown, link )
end
local session = {}
for _, field in ipairs( { "recorded", "studio", "engineer" } ) do
local value = trim( args[ field ] )
if value ~= "" then
table.insert( session, field == "engineer" and ( "engineer " .. value ) or value )
end
end
local out = { '<div style="padding:.5em .2em; border-bottom:1px solid #d4dacd;">' }
table.insert( out, '<div style="font-weight:600;">[[' .. record .. "]]"
.. ( number ~= "" and ( ' <span style="font-weight:400; color:#6b7566;">track '
.. number .. "</span>" ) or "" ) .. "</div>" )
if #shown > 0 then
table.insert( out, '<div style="font-size:.95em; line-height:1.9;">'
.. table.concat( shown, " · " ) .. "</div>" )
end
if #session > 0 then
table.insert( out, '<div style="font-size:.8em; color:#6b7566;">'
.. table.concat( session, " · " ) .. "</div>" )
end
table.insert( out, "</div>" )
return table.concat( out, "\n" )
end
-- One property of one page, as plain text.
local function property( frame, page, name )
return trim( plain( frame:callParserFunction( "#show", {
page, "?" .. name, "link=none", "default=",
} ) ) )
end
-- Whether a record is under a licence that lets people share it.
local function freelyLicensed( frame, record )
local licence = property( frame, record, "Has license" )
local lower = licence:lower()
local free = lower:find( "cc " ) or lower:find( "creative commons" )
or lower:find( "cc0" ) or lower:find( "public domain" )
return ( free ~= nil ), licence
end
-- Rows of "first||second||third||fourth", from Template:Studio appearance row.
local function rows( frame, query, printouts, mainlabel )
local args = { query }
for _, printout in ipairs( printouts ) do
table.insert( args, printout )
end
for _, option in ipairs( {
"mainlabel=" .. mainlabel, "format=plainlist", "link=none", "sep=@@",
"template=Studio appearance row", "searchlabel=", "limit=500",
} ) do
table.insert( args, option )
end
local raw = plain( frame:callParserFunction( "#ask", args ) )
local out = {}
if trim( raw ) == "" then
return out
end
for _, row in ipairs( mw.text.split( raw, "@@", true ) ) do
local a, b, c, d = row:match( "^(.-)||(.-)||(.-)||(.*)$" )
if a and trim( a ) ~= "" then
-- A subobject's subject prints as "Song:Barlows#_abc123"; the page
-- is the part before the fragment.
table.insert( out, {
subject = trim( ( a:match( "^([^#]*)" ) or a ) ),
b = trim( b ), c = trim( c ), d = trim( d ),
} )
end
end
return out
end
local function subjectOf( frame )
local given = trim( callArgs( frame )[1] or frame.args[1] )
if given ~= "" then
return given
end
return mw.title.getCurrentTitle().fullText
end
-- Every recording a musician played on, grouped by record.
--
-- @param wanted "free" for records anybody may share, "rest" for the others.
local function appearances( frame, wanted, heading )
local who = subjectOf( frame )
local found = rows( frame, "[[Has performer::" .. who .. "]]", {
"?Has record=record",
"?Has track number=number",
"?Has instrument=instrument",
}, "song" )
local order, byRecord = {}, {}
for _, row in ipairs( found ) do
local record = row.b
if record ~= "" then
if not byRecord[ record ] then
byRecord[ record ] = { songs = {}, instruments = {}, seen = {} }
table.insert( order, record )
end
local entry = byRecord[ record ]
if not entry.seen[ row.subject ] then
entry.seen[ row.subject ] = true
table.insert( entry.songs,
{ title = row.subject, number = tonumber( row.c ) } )
end
for _, one in ipairs( split( row.d ) ) do
if not entry.instruments[ one ] then
entry.instruments[ one ] = true
table.insert( entry.instruments, one )
end
end
end
end
local blocks = {}
for _, record in ipairs( order ) do
local entry = byRecord[ record ]
local isFree, licence = freelyLicensed( frame, record )
if ( wanted == "free" ) == isFree then
table.sort( entry.songs, function( a, b )
if a.number and b.number then return a.number < b.number end
return a.title < b.title
end )
local titles = {}
for _, song in ipairs( entry.songs ) do
table.insert( titles, "[[" .. song.title .. "|" .. bare( song.title ) .. "]]" )
end
local artist = property( frame, record, "Has record artist" )
local line = {
'<div style="padding:.45em .2em; border-bottom:1px solid #d4dacd;">',
'<div style="font-weight:600;">[[' .. record .. "]]",
}
if artist ~= "" then
table.insert( line, ' <span style="font-weight:400; color:#5a6858;">by [['
.. artist .. "]]</span>" )
end
if isFree and licence ~= "" then
table.insert( line, ' <span style="font-size:.72em; letter-spacing:.08em;'
.. ' text-transform:uppercase; color:#3d6b3d; border:1px solid #9bb89b;'
.. ' border-radius:3px; padding:0 .35em;">' .. licence .. "</span>" )
end
table.insert( line, "</div>" )
table.insert( line, '<div style="font-size:.9em; color:#3d4a3e;">'
.. ( #entry.instruments > 0
and ( table.concat( entry.instruments, ", " ) .. " · " ) or "" )
.. table.concat( titles, ", " ) .. "</div>" )
table.insert( line, "</div>" )
table.insert( blocks, table.concat( line, "" ) )
end
end
if #blocks == 0 then
return ""
end
table.insert( blocks, 1, "== " .. heading .. " ==" )
return table.concat( blocks, "\n" )
end
function p.appearances( frame )
return appearances( frame, "free",
"[[PickiPedia:Creative Commons|Creative Commons]] Studio Work" )
end
function p.other( frame )
return appearances( frame, "rest", "Other studio work" )
end
-- The track listing of a record, assembled from the compositions that name it.
--
-- The record page keeps no list of its own. A track is a recording, recordings
-- live on the composition's page, and a page that copied them here would be
-- one more thing to keep in step.
function p.tracks( frame )
local record = subjectOf( frame )
local found = rows( frame, "[[Has record::" .. record .. "]]", {
"?Has track number=number",
"?Has performer=performer",
"?Has instrument=instrument",
}, "song" )
local order, bySong = {}, {}
for _, row in ipairs( found ) do
if not bySong[ row.subject ] then
bySong[ row.subject ] = { number = tonumber( row.b ), players = {}, seen = {} }
table.insert( order, row.subject )
end
local entry = bySong[ row.subject ]
entry.number = entry.number or tonumber( row.b )
if row.c ~= "" and not entry.seen[ row.c ] then
entry.seen[ row.c ] = true
table.insert( entry.players, { name = row.c, instrument = split( row.d )[1] or "" } )
end
end
if #order == 0 then
return ""
end
table.sort( order, function( a, b )
local na, nb = bySong[ a ].number, bySong[ b ].number
if na and nb then return na < nb end
if na then return true end
if nb then return false end
return a < b
end )
local out = {}
for _, song in ipairs( order ) do
local entry = bySong[ song ]
table.sort( entry.players, function( a, b ) return a.name < b.name end )
local shown = {}
for _, player in ipairs( entry.players ) do
table.insert( shown, frame:expandTemplate{
title = "m", args = { player.name, player.instrument } } )
end
local line = { '<div style="padding:.45em .2em; border-bottom:1px solid #d4dacd;">' }
table.insert( line, '<div style="font-weight:600;">'
.. ( entry.number and ( entry.number .. ". " ) or "" )
.. "[[" .. song .. "|" .. bare( song ) .. "]]</div>" )
if #shown > 0 then
table.insert( line, '<div style="font-size:.9em; line-height:1.9; color:#3d4a3e;">'
.. table.concat( shown, " · " ) .. "</div>" )
end
table.insert( line, "</div>" )
table.insert( out, table.concat( line, "" ) )
end
return table.concat( out, "\n" )
end
-- Deprecated: the previous shape, where recordings lived on the record's page
-- and named their track by title.
--
-- Kept only so the three record pages written that way keep rendering until
-- the importer has moved them onto composition pages. Nothing should call it
-- afterwards; see PickiPedia:Compositions for why the data moved.
function p.track( frame )
local args = callArgs( frame )
local track = trim( args.track )
if track == "" then
return '<span class="error">Studio track: no track name given</span>'
end
local number = trim( args.number )
local shown = {}
for _, player in ipairs( players( args ) ) do
local parts = {
"Has track name=" .. escape( track ),
"Has performer=" .. escape( player.name ),
}
if number ~= "" then
table.insert( parts, "Has track number=" .. escape( number ) )
end
for _, instrument in ipairs( split( player.instruments ) ) do
table.insert( parts, "Has instrument=" .. escape( instrument ) )
end
frame:preprocess( "{{#subobject:|" .. table.concat( parts, "|" ) .. "}}" )
local first = split( player.instruments )[1] or ""
local link = frame:expandTemplate{ title = "m", args = { player.name, first } }
if player.instruments ~= "" then
link = link .. ' <span style="color:#6b7566;">' .. player.instruments .. "</span>"
end
table.insert( shown, link )
end
local label = number ~= "" and ( number .. ". " .. track ) or track
return '<div style="padding:.5em .2em; border-bottom:1px solid #d4dacd;">'
.. '<div style="font-weight:600;">' .. label .. "</div>"
.. '<div style="font-size:.95em; line-height:1.9;">'
.. table.concat( shown, " · " ) .. "</div></div>"
end
return p