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
Move recordings onto composition pages: records and musicians now read them rather than holding them (via update-page on MediaWiki MCP Server) |
A cut is its own thing now: identified by block height and studio, with zero or more appearances on records (via update-page on MediaWiki MCP Server) |
||
| Line 1: | Line 1: | ||
-- Studio | -- Studio cuts: a particular recording of a composition, and where it turns up. | ||
-- | -- | ||
-- | -- Three things, kept apart because they really are three things: | ||
-- | -- | ||
-- | -- the composition the page itself, in Song: (PickiPedia:Compositions) | ||
-- | -- a cut one recording of it, by one lineup, on one day | ||
-- | -- an appearance that cut turning up on a record | ||
-- | |||
-- A cut has zero or more appearances. Zero is ordinary: plenty of things are | |||
-- cut in a studio without ever landing on an album. More than one happens the | |||
-- moment a compilation exists, and the point of giving a cut its own identity | |||
-- is that the two appearances can then say they are the same performance | |||
-- rather than each claiming its own. | |||
-- | |||
-- A cut is identified by block height and studio, which is always | |||
-- disambiguating and does not depend on anybody agreeing what to call a | |||
-- session. | |||
-- | -- | ||
-- 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 15: | Line 23: | ||
local p = {} | local p = {} | ||
-- Parameters of {{Studio | -- Parameters of {{Studio cut}} that describe the session rather than name a | ||
-- | -- player. Anything else is a musician. | ||
local NOT_A_PLAYER = { | local NOT_A_PLAYER = { | ||
cut = true, | |||
blockheight = true, | |||
studio = true, | studio = true, | ||
recorded = true, | recorded = true, | ||
engineer = true, | engineer = true, | ||
record = true, | |||
number = true, | |||
} | } | ||
| Line 39: | Line 49: | ||
end | end | ||
local function callArgs( frame ) | local function callArgs( frame ) | ||
local parent = frame:getParent() | local parent = frame:getParent() | ||
if parent then | if parent then | ||
-- pairs(), not next(): Scribunto's args table is lazy, so next() | |||
-- reports no arguments on a call that has plenty. | |||
for _ in pairs( parent.args ) do | for _ in pairs( parent.args ) do | ||
return parent.args | return parent.args | ||
| Line 56: | Line 62: | ||
-- Everything the parser wraps around a value that is not part of the value: | -- 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 | -- the strip marker, SMW's span markup, and SMW's own [[SMW::off]] switch. | ||
local function plain( text ) | local function plain( text ) | ||
text = mw.text.unstrip( text or "" ) | text = mw.text.unstrip( text or "" ) | ||
| Line 66: | Line 69: | ||
end | end | ||
local function escape( value ) | local function escape( value ) | ||
return ( value:gsub( "|", "{{!}}" ):gsub( "=", "{{=}}" ) ) | return ( value:gsub( "|", "{{!}}" ):gsub( "=", "{{=}}" ) ) | ||
end | end | ||
local function bare( title ) | local function bare( title ) | ||
return ( title:gsub( "^[^:]+:", "" ) ) | return ( title:gsub( "^[^:]+:", "" ) ) | ||
end | |||
local function subobject( frame, parts ) | |||
frame:preprocess( "{{#subobject:|" .. table.concat( parts, "|" ) .. "}}" ) | |||
end | |||
-- What identifies this cut. | |||
-- | |||
-- Block height and studio, per the convention: a block height is a time nobody | |||
-- has to agree on the name of, and two sessions in the same studio at the same | |||
-- height do not happen. | |||
-- | |||
-- Failing that, an explicit label, and failing that the record it was cut for | |||
-- — which is a placeholder, not an identity. Two records naming the same cut | |||
-- that way cannot be told they are the same performance; that is exactly what | |||
-- recording the block height fixes. | |||
local function cutId( args ) | |||
local given = trim( args.cut ) | |||
if given ~= "" then | |||
return given | |||
end | |||
local height, studio = trim( args.blockheight ), trim( args.studio ) | |||
if height ~= "" and studio ~= "" then | |||
return height .. " " .. studio | |||
end | |||
if height ~= "" then | |||
return height | |||
end | |||
return trim( args.record ) | |||
end | end | ||
| Line 87: | Line 117: | ||
end | end | ||
-- | -- One recording of this composition. | ||
function p.cut( frame ) | |||
local args = callArgs( frame ) | |||
local id = cutId( args ) | |||
if id == "" then | |||
return '<span class="error">Studio cut: give it a block height and ' | |||
local | .. 'studio, or a record it was cut for</span>' | ||
if | |||
end | end | ||
local record, number = trim( args.record ), trim( args.number ) | |||
local record = trim( args.record ) | |||
local shown = {} | local shown = {} | ||
for _, player in ipairs( players( args ) ) do | for _, player in ipairs( players( args ) ) do | ||
local parts = { | |||
"Has cut=" .. escape( id ), | |||
"Has performer=" .. escape( player.name ), | |||
} | |||
for _, instrument in ipairs( split( player.instruments ) ) do | |||
table.insert( parts, "Has instrument=" .. escape( instrument ) ) | |||
end | |||
subobject( frame, parts ) | |||
-- The icon | -- The icon is the instrument played on this cut, not whatever the | ||
-- | -- musician's page calls their main one. | ||
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 128: | Line 147: | ||
end | end | ||
table.insert( shown, link ) | table.insert( shown, link ) | ||
end | |||
-- The common case: a cut made for one record says so here, rather than | |||
-- needing a second call to state the obvious. | |||
if record ~= "" then | |||
local parts = { "Has cut=" .. escape( id ), "Has record=" .. escape( record ) } | |||
if number ~= "" then | |||
table.insert( parts, "Has track number=" .. escape( number ) ) | |||
end | |||
subobject( frame, parts ) | |||
end | end | ||
| Line 136: | Line 165: | ||
table.insert( session, field == "engineer" and ( "engineer " .. value ) or value ) | table.insert( session, field == "engineer" and ( "engineer " .. value ) or value ) | ||
end | end | ||
end | |||
if trim( args.blockheight ) ~= "" then | |||
table.insert( session, "block " .. trim( args.blockheight ) ) | |||
end | |||
local heading | |||
if record ~= "" then | |||
heading = "[[" .. record .. "]]" | |||
.. ( number ~= "" and ( ' <span style="font-weight:400; color:#6b7566;">track ' | |||
.. number .. "</span>" ) or "" ) | |||
else | |||
-- A cut with nowhere to appear is not a lesser thing; say what it is. | |||
heading = '<span style="color:#5a6858;">studio cut</span>' | |||
end | end | ||
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;">' .. heading .. "</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 154: | Line 194: | ||
end | end | ||
-- | -- This cut turning up somewhere else: a compilation, a reissue, a single. | ||
function p.appearance( frame ) | |||
local args = callArgs( frame ) | |||
local id, record = trim( args.cut ), trim( args.record ) | |||
if id == "" or record == "" then | |||
return '<span class="error">Cut appears on: needs both a cut and a record</span>' | |||
end | |||
local number = trim( args.number ) | |||
local parts = { "Has cut=" .. escape( id ), "Has record=" .. escape( record ) } | |||
if number ~= "" then | |||
table.insert( parts, "Has track number=" .. escape( number ) ) | |||
end | |||
subobject( frame, parts ) | |||
return '<div style="padding:.3em .2em; font-size:.9em; color:#3d4a3e;">also on [[' | |||
.. record .. "]]" | |||
.. ( number ~= "" and ( ", track " .. number ) or "" ) .. "</div>" | |||
end | |||
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 161: | Line 220: | ||
end | end | ||
local function freelyLicensed( frame, record ) | local function freelyLicensed( frame, record ) | ||
local licence = property( frame, record, "Has license" ) | local licence = property( frame, record, "Has license" ) | ||
| Line 170: | Line 228: | ||
end | end | ||
-- Rows of " | -- Rows of "subject||b||c||d", from Template:Studio appearance row. | ||
local function rows( frame, query, printouts, mainlabel ) | local function rows( frame, query, printouts, mainlabel ) | ||
local args = { query } | local args = { query } | ||
| Line 191: | Line 249: | ||
local a, b, c, d = row:match( "^(.-)||(.-)||(.-)||(.*)$" ) | local a, b, c, d = row:match( "^(.-)||(.-)||(.-)||(.*)$" ) | ||
if a and trim( a ) ~= "" then | if a and trim( a ) ~= "" then | ||
table.insert( out, { | table.insert( out, { | ||
subject = trim( ( a:match( "^([^#]*)" ) or a ) ), | subject = trim( ( a:match( "^([^#]*)" ) or a ) ), | ||
| Line 200: | Line 256: | ||
end | end | ||
return out | return out | ||
end | |||
-- Where a set of cuts appear, asked in one query rather than one per cut. | |||
-- | |||
-- A record with a dozen tracks would otherwise mean a dozen queries; SMW takes | |||
-- alternatives with ||, so it is one. | |||
local function appearancesOf( frame, ids ) | |||
local wanted = {} | |||
for id in pairs( ids ) do | |||
table.insert( wanted, id ) | |||
end | |||
if #wanted == 0 then | |||
return {} | |||
end | |||
table.sort( wanted ) | |||
local found = rows( frame, "[[Has cut::" .. table.concat( wanted, "||" ) .. "]]", { | |||
"?Has cut=cut", | |||
"?Has record=record", | |||
"?Has track number=number", | |||
}, "song" ) | |||
local byCut = {} | |||
for _, row in ipairs( found ) do | |||
if row.c ~= "" then | |||
byCut[ row.b ] = byCut[ row.b ] or {} | |||
table.insert( byCut[ row.b ], { | |||
song = row.subject, record = row.c, number = tonumber( row.d ), | |||
} ) | |||
end | |||
end | |||
return byCut | |||
end | end | ||
| Line 210: | Line 298: | ||
end | end | ||
-- Every | -- Every cut a musician played on, grouped by the record it appears on. | ||
-- | -- | ||
-- @param wanted "free" for records anybody may share, "rest" for the others. | -- @param wanted "free" for records anybody may share, "rest" for the others, | ||
-- "none" for cuts that appear on no record at all. | |||
local function appearances( frame, wanted, heading ) | local function appearances( frame, wanted, heading ) | ||
local who = subjectOf( frame ) | local who = subjectOf( frame ) | ||
local | local mine = rows( frame, "[[Has performer::" .. who .. "]]", { | ||
"?Has | "?Has cut=cut", | ||
"?Has instrument=instrument", | "?Has instrument=instrument", | ||
"?Has performer=performer", | |||
}, "song" ) | }, "song" ) | ||
local ids, instruments, songOf = {}, {}, {} | |||
for _, row in ipairs( mine ) do | |||
if row.b ~= "" then | |||
ids[ row.b ] = true | |||
songOf[ row.b ] = row.subject | |||
for _, one in ipairs( split( row.c ) ) do | |||
instruments[ row.b ] = instruments[ row.b ] or {} | |||
table.insert( instruments[ row.b ], one ) | |||
end | |||
end | |||
end | |||
local where = appearancesOf( frame, ids ) | |||
-- Grouped by record, so a picker's page reads as a list of records rather | |||
-- than a list of sessions. | |||
local order, byRecord = {}, {} | local order, byRecord = {}, {} | ||
for | local loose = {} | ||
local | for id in pairs( ids ) do | ||
if | local places = where[ id ] | ||
if not byRecord[ record ] then | if places and #places > 0 then | ||
for _, place in ipairs( places ) do | |||
if not byRecord[ place.record ] then | |||
byRecord[ place.record ] = { songs = {}, instruments = {}, seen = {} } | |||
table.insert( order, place.record ) | |||
end | |||
local entry = byRecord[ place.record ] | |||
if not entry.seen[ place.song ] then | |||
entry.seen[ place.song ] = true | |||
table.insert( entry.songs, { title = place.song, number = place.number } ) | |||
end | |||
for _, one in ipairs( instruments[ id ] or {} ) do | |||
if not entry.instruments[ one ] then | |||
entry.instruments[ one ] = true | |||
table.insert( entry.instruments, one ) | |||
end | |||
end | end | ||
end | end | ||
else | |||
table.insert( loose, { song = songOf[ id ], id = id, | |||
instruments = instruments[ id ] or {} } ) | |||
end | end | ||
end | end | ||
local blocks = {} | local blocks = {} | ||
local titles = {} | if wanted == "none" then | ||
table.sort( loose, function( a, b ) return a.song < b.song end ) | |||
for _, cut in ipairs( loose ) do | |||
table.insert( blocks, | |||
'<div style="padding:.45em .2em; border-bottom:1px solid #d4dacd;">' | |||
.. '<div style="font-weight:600;">[[' .. cut.song .. "|" | |||
.. bare( cut.song ) .. "]]</div>" | |||
.. '<div style="font-size:.9em; color:#3d4a3e;">' | |||
.. ( #cut.instruments > 0 | |||
and ( table.concat( cut.instruments, ", " ) .. " · " ) or "" ) | |||
.. cut.id .. "</div></div>" ) | |||
end | |||
else | |||
table.sort( order ) | |||
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 | ||
end | end | ||
end | end | ||
| Line 299: | Line 426: | ||
end | end | ||
-- | -- Cuts that are on no record at all — a session for its own sake, a video, a | ||
-- | -- thing that was never finished. Worth saying: it happened, and they played on | ||
-- The | -- it. | ||
function p.unreleased( frame ) | |||
return appearances( frame, "none", "Studio cuts not on a record" ) | |||
end | |||
-- The track listing of a record, assembled from the cuts that appear on it. | |||
function p.tracks( frame ) | function p.tracks( frame ) | ||
local record = subjectOf( frame ) | local record = subjectOf( frame ) | ||
local | local places = rows( frame, "[[Has record::" .. record .. "]]", { | ||
"?Has cut=cut", | |||
"?Has track number=number", | "?Has track number=number", | ||
"?Has | "?Has record=record", | ||
}, "song" ) | }, "song" ) | ||
local order, bySong = {}, {} | local ids, order, bySong = {}, {}, {} | ||
for _, row in ipairs( | for _, row in ipairs( places ) do | ||
if not bySong[ row.subject ] then | if row.b ~= "" then | ||
ids[ row.b ] = true | |||
if not bySong[ row.subject ] then | |||
bySong[ row.subject ] = { number = tonumber( row.c ), cuts = {} } | |||
table.insert( order, row.subject ) | |||
end | |||
local entry = bySong[ row.subject ] | |||
entry.number = entry.number or tonumber( row.c ) | |||
table.insert( entry. | table.insert( entry.cuts, row.b ) | ||
end | end | ||
end | end | ||
| Line 328: | Line 458: | ||
if #order == 0 then | if #order == 0 then | ||
return "" | return "" | ||
end | |||
-- Who played, for every cut on this record, in one more query. | |||
local wanted = {} | |||
for id in pairs( ids ) do | |||
table.insert( wanted, id ) | |||
end | |||
table.sort( wanted ) | |||
local personnel = rows( frame, | |||
"[[Has cut::" .. table.concat( wanted, "||" ) .. "]][[Has performer::+]]", { | |||
"?Has cut=cut", | |||
"?Has performer=performer", | |||
"?Has instrument=instrument", | |||
}, "song" ) | |||
local byCut = {} | |||
for _, row in ipairs( personnel ) do | |||
if row.b ~= "" and row.c ~= "" then | |||
byCut[ row.b ] = byCut[ row.b ] or {} | |||
table.insert( byCut[ row.b ], { name = row.c, instrument = split( row.d )[1] or "" } ) | |||
end | |||
end | end | ||
| Line 341: | Line 492: | ||
for _, song in ipairs( order ) do | for _, song in ipairs( order ) do | ||
local entry = bySong[ song ] | local entry = bySong[ song ] | ||
local shown = {} | local seen, shown = {}, {} | ||
for _, | for _, id in ipairs( entry.cuts ) do | ||
table.insert( shown, frame:expandTemplate{ | for _, player in ipairs( byCut[ id ] or {} ) do | ||
if not seen[ player.name ] then | |||
seen[ player.name ] = true | |||
table.insert( shown, player ) | |||
end | |||
end | |||
end | |||
table.sort( shown, function( a, b ) return a.name < b.name end ) | |||
local links = {} | |||
for _, player in ipairs( shown ) do | |||
table.insert( links, frame:expandTemplate{ | |||
title = "m", args = { player.name, player.instrument } } ) | title = "m", args = { player.name, player.instrument } } ) | ||
end | end | ||
| Line 353: | Line 514: | ||
.. ( entry.number and ( entry.number .. ". " ) or "" ) | .. ( entry.number and ( entry.number .. ". " ) or "" ) | ||
.. "[[" .. song .. "|" .. bare( song ) .. "]]</div>" ) | .. "[[" .. song .. "|" .. bare( song ) .. "]]</div>" ) | ||
if # | if #links > 0 then | ||
table.insert( line, '<div style="font-size:.9em; line-height:1.9; color:#3d4a3e;">' | table.insert( line, '<div style="font-size:.9em; line-height:1.9; color:#3d4a3e;">' | ||
.. table.concat( | .. table.concat( links, " · " ) .. "</div>" ) | ||
end | end | ||
table.insert( line, "</div>" ) | table.insert( line, "</div>" ) | ||
| Line 361: | Line 522: | ||
end | end | ||
return table.concat( out, "\n" ) | return table.concat( out, "\n" ) | ||
end | end | ||
return p | return p | ||
Revision as of 00:16, 19 September 2026
Documentation for this module may be created at Module:Studio sessions/doc
-- Studio cuts: a particular recording of a composition, and where it turns up.
--
-- Three things, kept apart because they really are three things:
--
-- the composition the page itself, in Song: (PickiPedia:Compositions)
-- a cut one recording of it, by one lineup, on one day
-- an appearance that cut turning up on a record
--
-- A cut has zero or more appearances. Zero is ordinary: plenty of things are
-- cut in a studio without ever landing on an album. More than one happens the
-- moment a compilation exists, and the point of giving a cut its own identity
-- is that the two appearances can then say they are the same performance
-- rather than each claiming its own.
--
-- A cut is identified by block height and studio, which is always
-- disambiguating and does not depend on anybody agreeing what to call a
-- session.
--
-- 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 cut}} that describe the session rather than name a
-- player. Anything else is a musician.
local NOT_A_PLAYER = {
cut = true,
blockheight = true,
studio = true,
recorded = true,
engineer = true,
record = true,
number = 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
local function callArgs( frame )
local parent = frame:getParent()
if parent then
-- pairs(), not next(): Scribunto's args table is lazy, so next()
-- reports no arguments on a call that has plenty.
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.
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
local function escape( value )
return ( value:gsub( "|", "{{!}}" ):gsub( "=", "{{=}}" ) )
end
local function bare( title )
return ( title:gsub( "^[^:]+:", "" ) )
end
local function subobject( frame, parts )
frame:preprocess( "{{#subobject:|" .. table.concat( parts, "|" ) .. "}}" )
end
-- What identifies this cut.
--
-- Block height and studio, per the convention: a block height is a time nobody
-- has to agree on the name of, and two sessions in the same studio at the same
-- height do not happen.
--
-- Failing that, an explicit label, and failing that the record it was cut for
-- — which is a placeholder, not an identity. Two records naming the same cut
-- that way cannot be told they are the same performance; that is exactly what
-- recording the block height fixes.
local function cutId( args )
local given = trim( args.cut )
if given ~= "" then
return given
end
local height, studio = trim( args.blockheight ), trim( args.studio )
if height ~= "" and studio ~= "" then
return height .. " " .. studio
end
if height ~= "" then
return height
end
return trim( args.record )
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
-- One recording of this composition.
function p.cut( frame )
local args = callArgs( frame )
local id = cutId( args )
if id == "" then
return '<span class="error">Studio cut: give it a block height and '
.. 'studio, or a record it was cut for</span>'
end
local record, number = trim( args.record ), trim( args.number )
local shown = {}
for _, player in ipairs( players( args ) ) do
local parts = {
"Has cut=" .. escape( id ),
"Has performer=" .. escape( player.name ),
}
for _, instrument in ipairs( split( player.instruments ) ) do
table.insert( parts, "Has instrument=" .. escape( instrument ) )
end
subobject( frame, parts )
-- The icon is the instrument played on this cut, not whatever the
-- musician's page calls their main one.
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
-- The common case: a cut made for one record says so here, rather than
-- needing a second call to state the obvious.
if record ~= "" then
local parts = { "Has cut=" .. escape( id ), "Has record=" .. escape( record ) }
if number ~= "" then
table.insert( parts, "Has track number=" .. escape( number ) )
end
subobject( frame, parts )
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
if trim( args.blockheight ) ~= "" then
table.insert( session, "block " .. trim( args.blockheight ) )
end
local heading
if record ~= "" then
heading = "[[" .. record .. "]]"
.. ( number ~= "" and ( ' <span style="font-weight:400; color:#6b7566;">track '
.. number .. "</span>" ) or "" )
else
-- A cut with nowhere to appear is not a lesser thing; say what it is.
heading = '<span style="color:#5a6858;">studio cut</span>'
end
local out = { '<div style="padding:.5em .2em; border-bottom:1px solid #d4dacd;">' }
table.insert( out, '<div style="font-weight:600;">' .. heading .. "</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
-- This cut turning up somewhere else: a compilation, a reissue, a single.
function p.appearance( frame )
local args = callArgs( frame )
local id, record = trim( args.cut ), trim( args.record )
if id == "" or record == "" then
return '<span class="error">Cut appears on: needs both a cut and a record</span>'
end
local number = trim( args.number )
local parts = { "Has cut=" .. escape( id ), "Has record=" .. escape( record ) }
if number ~= "" then
table.insert( parts, "Has track number=" .. escape( number ) )
end
subobject( frame, parts )
return '<div style="padding:.3em .2em; font-size:.9em; color:#3d4a3e;">also on [['
.. record .. "]]"
.. ( number ~= "" and ( ", track " .. number ) or "" ) .. "</div>"
end
local function property( frame, page, name )
return trim( plain( frame:callParserFunction( "#show", {
page, "?" .. name, "link=none", "default=",
} ) ) )
end
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 "subject||b||c||d", 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
table.insert( out, {
subject = trim( ( a:match( "^([^#]*)" ) or a ) ),
b = trim( b ), c = trim( c ), d = trim( d ),
} )
end
end
return out
end
-- Where a set of cuts appear, asked in one query rather than one per cut.
--
-- A record with a dozen tracks would otherwise mean a dozen queries; SMW takes
-- alternatives with ||, so it is one.
local function appearancesOf( frame, ids )
local wanted = {}
for id in pairs( ids ) do
table.insert( wanted, id )
end
if #wanted == 0 then
return {}
end
table.sort( wanted )
local found = rows( frame, "[[Has cut::" .. table.concat( wanted, "||" ) .. "]]", {
"?Has cut=cut",
"?Has record=record",
"?Has track number=number",
}, "song" )
local byCut = {}
for _, row in ipairs( found ) do
if row.c ~= "" then
byCut[ row.b ] = byCut[ row.b ] or {}
table.insert( byCut[ row.b ], {
song = row.subject, record = row.c, number = tonumber( row.d ),
} )
end
end
return byCut
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 cut a musician played on, grouped by the record it appears on.
--
-- @param wanted "free" for records anybody may share, "rest" for the others,
-- "none" for cuts that appear on no record at all.
local function appearances( frame, wanted, heading )
local who = subjectOf( frame )
local mine = rows( frame, "[[Has performer::" .. who .. "]]", {
"?Has cut=cut",
"?Has instrument=instrument",
"?Has performer=performer",
}, "song" )
local ids, instruments, songOf = {}, {}, {}
for _, row in ipairs( mine ) do
if row.b ~= "" then
ids[ row.b ] = true
songOf[ row.b ] = row.subject
for _, one in ipairs( split( row.c ) ) do
instruments[ row.b ] = instruments[ row.b ] or {}
table.insert( instruments[ row.b ], one )
end
end
end
local where = appearancesOf( frame, ids )
-- Grouped by record, so a picker's page reads as a list of records rather
-- than a list of sessions.
local order, byRecord = {}, {}
local loose = {}
for id in pairs( ids ) do
local places = where[ id ]
if places and #places > 0 then
for _, place in ipairs( places ) do
if not byRecord[ place.record ] then
byRecord[ place.record ] = { songs = {}, instruments = {}, seen = {} }
table.insert( order, place.record )
end
local entry = byRecord[ place.record ]
if not entry.seen[ place.song ] then
entry.seen[ place.song ] = true
table.insert( entry.songs, { title = place.song, number = place.number } )
end
for _, one in ipairs( instruments[ id ] or {} ) do
if not entry.instruments[ one ] then
entry.instruments[ one ] = true
table.insert( entry.instruments, one )
end
end
end
else
table.insert( loose, { song = songOf[ id ], id = id,
instruments = instruments[ id ] or {} } )
end
end
local blocks = {}
if wanted == "none" then
table.sort( loose, function( a, b ) return a.song < b.song end )
for _, cut in ipairs( loose ) do
table.insert( blocks,
'<div style="padding:.45em .2em; border-bottom:1px solid #d4dacd;">'
.. '<div style="font-weight:600;">[[' .. cut.song .. "|"
.. bare( cut.song ) .. "]]</div>"
.. '<div style="font-size:.9em; color:#3d4a3e;">'
.. ( #cut.instruments > 0
and ( table.concat( cut.instruments, ", " ) .. " · " ) or "" )
.. cut.id .. "</div></div>" )
end
else
table.sort( order )
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
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
-- Cuts that are on no record at all — a session for its own sake, a video, a
-- thing that was never finished. Worth saying: it happened, and they played on
-- it.
function p.unreleased( frame )
return appearances( frame, "none", "Studio cuts not on a record" )
end
-- The track listing of a record, assembled from the cuts that appear on it.
function p.tracks( frame )
local record = subjectOf( frame )
local places = rows( frame, "[[Has record::" .. record .. "]]", {
"?Has cut=cut",
"?Has track number=number",
"?Has record=record",
}, "song" )
local ids, order, bySong = {}, {}, {}
for _, row in ipairs( places ) do
if row.b ~= "" then
ids[ row.b ] = true
if not bySong[ row.subject ] then
bySong[ row.subject ] = { number = tonumber( row.c ), cuts = {} }
table.insert( order, row.subject )
end
local entry = bySong[ row.subject ]
entry.number = entry.number or tonumber( row.c )
table.insert( entry.cuts, row.b )
end
end
if #order == 0 then
return ""
end
-- Who played, for every cut on this record, in one more query.
local wanted = {}
for id in pairs( ids ) do
table.insert( wanted, id )
end
table.sort( wanted )
local personnel = rows( frame,
"[[Has cut::" .. table.concat( wanted, "||" ) .. "]][[Has performer::+]]", {
"?Has cut=cut",
"?Has performer=performer",
"?Has instrument=instrument",
}, "song" )
local byCut = {}
for _, row in ipairs( personnel ) do
if row.b ~= "" and row.c ~= "" then
byCut[ row.b ] = byCut[ row.b ] or {}
table.insert( byCut[ row.b ], { name = row.c, instrument = split( row.d )[1] or "" } )
end
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 ]
local seen, shown = {}, {}
for _, id in ipairs( entry.cuts ) do
for _, player in ipairs( byCut[ id ] or {} ) do
if not seen[ player.name ] then
seen[ player.name ] = true
table.insert( shown, player )
end
end
end
table.sort( shown, function( a, b ) return a.name < b.name end )
local links = {}
for _, player in ipairs( shown ) do
table.insert( links, 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 #links > 0 then
table.insert( line, '<div style="font-size:.9em; line-height:1.9; color:#3d4a3e;">'
.. table.concat( links, " · " ) .. "</div>" )
end
table.insert( line, "</div>" )
table.insert( out, table.concat( line, "" ) )
end
return table.concat( out, "\n" )
end
return p