|
|
| (6 intermediate revisions by the same user not shown) |
| Line 1: |
Line 1: |
| -- Studio sessions: who played what, on which track, on which record. | | -- Studio cuts: a particular recording of a composition, and where it turns up. |
| -- | | -- |
| -- The data comes from arthel, where it exists because the Rabbithole player | | -- The module itself lives in the repository, at |
| -- needs it to follow a picker from one track to the next. Here it answers the | | -- extensions/PickiPediaContent/lua/pickipedia/studio.lua, and is loaded from |
| -- other direction: given a musician, which records are they on. | | -- there through Scribunto's external library paths. Six hundred lines |
| | -- deciding what every song, record and musician page says is code, and code |
| | -- belongs somewhere that reviews it, remembers why it changed, and can put it |
| | -- back. On a wiki page it had none of those. |
| -- | | -- |
| -- Each track records one subobject per player, because a subobject holds a set | | -- So change it there, not here. An edit to this page would be overridden by |
| -- of values and not a set of pairs -- "Harry Clark" and "mandolin" have to sit | | -- nothing and reviewed by nobody -- it would simply take effect, across |
| -- in the same subobject to stay attached to each other. One per player per | | -- thirty-six pages, which is the problem. |
| -- track is what keeps that true. | |
| -- | | -- |
| -- Note for future editors: do not wrap this header in a --[==[ ]==] block
| | -- The model it implements is documented at PickiPedia:Compositions: a |
| -- comment containing wiki link syntax. Lua reads the inner brackets as nested
| | -- composition is a page in Song:, a cut is one recording of it by one lineup |
| -- long brackets and refuses to load the module.
| | -- on one day, and an appearance is that cut turning up on a record. Templates: |
| | | -- Studio cut, Cut appears on, Record tracks, In the studio. |
| 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, | | -- Tests run without a wiki, in the extension's tests/check-studio.lua. |
| -- 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 | | return require( 'pickipedia.studio' ) |