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
Strip leftover strip markers and SMW span markup from query results, or the record name is not a name (via update-page on MediaWiki MCP Server) |
Strip SMW's SMW::off switch out of query results, which was becoming part of the first record's name (via update-page on MediaWiki MCP Server) |
||
| Line 53: | Line 53: | ||
-- pairs(), not next(). Scribunto's args table is lazy: values arrive | -- pairs(), not next(). Scribunto's args table is lazy: values arrive | ||
-- through a metatable and the table itself is empty, so next() reports | -- through a metatable and the table itself is empty, so next() reports | ||
-- no arguments on a call that has plenty | -- 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 81: | Line 80: | ||
local function escape( value ) | local function escape( value ) | ||
return ( value:gsub( "|", "{{!}}" ):gsub( "=", "{{=}}" ) ) | return ( value:gsub( "|", "{{!}}" ):gsub( "=", "{{=}}" ) ) | ||
end | |||
-- Everything the parser wraps around a value that is not part of the value. | |||
-- | |||
-- Three layers, each found the hard way: the strip marker the parser hands | |||
-- back, the span markup SMW adds even with link=none, and SMW's own | |||
-- [[SMW::off]] switch, which brackets template-formatted results so a result | |||
-- cannot annotate the page showing it. That last one made the first record's | |||
-- name begin "[[SMW::off]]", so "[[" .. name .. "]]" rendered as a warning | |||
-- about a property label containing "[" rather than as a link. | |||
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 | end | ||
| Line 157: | Line 170: | ||
-- described rather than being repeated on every track. | -- described rather than being repeated on every track. | ||
local function freelyLicensed( frame, record ) | local function freelyLicensed( frame, record ) | ||
local licence = frame:callParserFunction( "#show", { | local licence = plain( frame:callParserFunction( "#show", { | ||
record, "?Has license", "link=none", "default=", | record, "?Has license", "link=none", "default=", | ||
} ) | } ) ) | ||
local lower = licence:lower() | local lower = licence:lower() | ||
local free = lower:find( "cc " ) or lower:find( "creative commons" ) | local free = lower:find( "cc " ) or lower:find( "creative commons" ) | ||
| Line 180: | Line 191: | ||
end | end | ||
local raw = frame:callParserFunction( "#ask", { | local raw = plain( frame:callParserFunction( "#ask", { | ||
"[[Has performer::" .. who .. "]]", | "[[Has performer::" .. who .. "]]", | ||
"?Has track name=track", | "?Has track name=track", | ||
| Line 192: | Line 203: | ||
"searchlabel=", | "searchlabel=", | ||
"limit=200", | "limit=200", | ||
} ) | } ) ) | ||
if trim( raw ) == "" then | if trim( raw ) == "" then | ||
return "" | return "" | ||
Revision as of 21:45, 17 September 2026
Documentation for this module may be created at Module:Studio sessions/doc
-- Studio sessions: who played what, on which track, on which record.
--
-- The data comes from arthel, where it exists because the Rabbithole player
-- needs it to follow a picker from one track to the next. Here it answers the
-- other direction: given a musician, which records are they on.
--
-- Each track records one subobject per player, because a subobject holds a set
-- of values and not a set of pairs -- "Harry Clark" and "mandolin" have to sit
-- in the same subobject to stay attached to each other. One per player per
-- track is what keeps that true.
--
-- 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 track}} that describe the session rather than name a
-- player. Anything else is a musician.
local NOT_A_PLAYER = {
track = true,
number = true,
studio = true,
recorded = true,
engineer = true,
record = 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 like {{Studio track}} is none at all. The
-- template's own parameters are on the parent frame. Reading frame.args
-- directly is why the first version of this answered "no track name given" to
-- a call that plainly gave one.
local function callArgs( frame )
local parent = frame:getParent()
if parent then
-- pairs(), not next(). Scribunto's args table is lazy: values arrive
-- through a metatable and the table itself is empty, 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
-- The players of a track.
--
-- Lua hands named arguments back in an arbitrary order, so they are sorted by
-- name: an order nobody chose is better than one that changes between parses.
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
-- A pipe or an equals sign in a value would end the parameter early, so they
-- are written as the templates that stand for them.
local function escape( value )
return ( value:gsub( "|", "{{!}}" ):gsub( "=", "{{=}}" ) )
end
-- Everything the parser wraps around a value that is not part of the value.
--
-- Three layers, each found the hard way: the strip marker the parser hands
-- back, the span markup SMW adds even with link=none, and SMW's own
-- [[SMW::off]] switch, which brackets template-formatted results so a result
-- cannot annotate the page showing it. That last one made the first record's
-- name begin "[[SMW::off]]", so "[[" .. name .. "]]" rendered as a warning
-- about a property label containing "[" rather than as a link.
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
-- Record one player on one track, as data a query can reach.
--
-- 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: the page looked
-- right and [[Has performer::+]] matched nothing at all. Preprocessing it is
-- what actually stores it.
local function annotate( frame, track, number, player )
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, "|" ) .. "}}" )
end
-- One track of a record: its personnel, recorded and shown.
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 named = players( args )
local shown = {}
for _, player in ipairs( named ) do
annotate( frame, track, 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
-- one track played dobro on that track.
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
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;">' .. label .. "</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
-- Whether a record is under a licence that lets people share it.
--
-- Asked of the record's own page, so the answer lives where the record is
-- described rather than being repeated on every track.
local function freelyLicensed( frame, record )
local licence = plain( frame:callParserFunction( "#show", {
record, "?Has license", "link=none", "default=",
} ) )
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 ), trim( licence )
end
-- Every track a musician appears on, grouped by record.
--
-- Records anybody may share come first and in full; the rest are listed after,
-- more quietly. That ordering is a claim about what this wiki is for: a
-- recording you can pass to somebody is worth more here than one you can only
-- point at.
function p.appearances( frame )
local who = trim( callArgs( frame )[1] or frame.args[1] )
if who == "" then
who = mw.title.getCurrentTitle().text
end
local raw = plain( frame:callParserFunction( "#ask", {
"[[Has performer::" .. who .. "]]",
"?Has track name=track",
"?Has track number=number",
"?Has instrument=instrument",
"mainlabel=record",
"format=plainlist",
"link=none",
"sep=@@",
"template=Studio appearance row",
"searchlabel=",
"limit=200",
} ) )
if trim( raw ) == "" then
return ""
end
-- Rows arrive as "record||track||number||instrument" from
-- Template:Studio appearance row. Grouping happens here rather than in the
-- query because a record with six tracks should read as one line, not six.
local order, byRecord = {}, {}
for _, row in ipairs( mw.text.split( raw, "@@", true ) ) do
local record, track, number, instrument = row:match( "^(.-)||(.-)||(.-)||(.*)$" )
if record and trim( record ) ~= "" then
-- A subobject's subject prints as "4masks#_abc123". The record is
-- the part before the fragment; the fragment is SMW's own id.
record = trim( record:match( "^([^#]*)" ) or record )
if not byRecord[ record ] then
byRecord[ record ] = { tracks = {}, instruments = {}, seen = {} }
table.insert( order, record )
end
local entry = byRecord[ record ]
if trim( track ) ~= "" and not entry.seen[ track ] then
entry.seen[ track ] = true
table.insert( entry.tracks, { title = trim( track ), number = tonumber( number ) } )
end
for _, one in ipairs( split( instrument ) ) do
if not entry.instruments[ one ] then
entry.instruments[ one ] = true
table.insert( entry.instruments, one )
end
end
end
end
local free, paid = {}, {}
for _, record in ipairs( order ) do
local entry = byRecord[ record ]
table.sort( entry.tracks, 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 _, track in ipairs( entry.tracks ) do
table.insert( titles, track.title )
end
local isFree, licence = freelyLicensed( frame, record )
local instruments = table.concat( entry.instruments, ", " )
local line = {
'<div style="padding:.45em .2em; border-bottom:1px solid #d4dacd;">',
'<div style="font-weight:600;">[[' .. record .. "]]",
}
if isFree 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 ~= "" and licence or "free to share" )
.. "</span>" )
end
table.insert( line, "</div>" )
table.insert( line, '<div style="font-size:.9em; color:#3d4a3e;">'
.. ( instruments ~= "" and instruments .. " · " or "" )
.. ( #titles == 1 and titles[1] or ( #titles .. " tracks: " .. table.concat( titles, ", " ) ) )
.. "</div>" )
table.insert( line, "</div>" )
table.insert( isFree and free or paid, table.concat( line, "" ) )
end
local out = { "== In the studio ==" }
for _, block in ipairs( free ) do
table.insert( out, block )
end
if #paid > 0 then
if #free > 0 then
table.insert( out, '<div style="font-size:.72em; letter-spacing:.14em;'
.. ' text-transform:uppercase; color:#6b7566; margin:.8em 0 .2em;">'
.. "Also on</div>" )
end
for _, block in ipairs( paid ) do
table.insert( out, block )
end
end
return table.concat( out, "\n" )
end
return p