Jump to content

Module:Cast and crew

From Wikisource

require('strict')

local p = {}

local getArgs = require('Module:Arguments').getArgs
local error_message = require('Module:Error')['error']

local function getLink(args)
    -- Get the Wikidata item ID from the frame arguments
    local wikidataItem = args[1]
    if not wikidataItem then
        return error_message({'[[Module:Cast and crew]] error: no Wikidata item provided'})
    end
    
    -- Query Wikidata for sitelinks
    local entity = mw.wikibase.getEntity(wikidataItem)
    if not entity then
        return error_message({'[[Module:Cast and crew]] error: invalid or nonexistent Wikidata item'})
    end
    
    local label = entity:getLabel('en') or wikidataItem
    
    -- Check for an English Wikisource link
    local enwikisourceLink = entity:getSitelink('enwikisource')
    if enwikisourceLink then
    	return '[[' .. enwikisourceLink .. '|' .. label .. ']]'
    end
    
    -- Check for an English Wikipedia link
    local enwikiLink = entity:getSitelink('enwiki')
    if enwikiLink then
    	return '[[w:' .. enwikiLink .. '|' .. label .. ']]'
    end
    
    -- Fallback to a direct Wikidata link with label
    return '[[d:' .. wikidataItem .. '|' .. label .. ']]'
end

function p._getCastList(args)
    local wikidataItem = args[1]
    if not wikidataItem then
        return error_message({'[[Module:Cast and crew]] error: no Wikidata item provided'})
    end
    
    -- Query Wikidata for cast member claims (P161)
    local entity = mw.wikibase.getEntity(wikidataItem)
    if not entity or not entity.claims then
        return ''
    end
    
    local castMembers = entity.claims['P161']
	if not castMembers then
	    castMembers = entity.claims['P725'] -- voice actors
	end
	if not castMembers then
		return ''
	end
	
    local leadingActors = {}
    local otherCastMembers = {}
    local uncreditedActors = {}
    
	for _, castMember in ipairs(castMembers) do
		local castMemberQID = (castMember.mainsnak.datavalue and castMember.mainsnak.datavalue.value.id) or nil
		local roleClaims = (castMember.qualifiers and castMember.qualifiers['P453']) or nil
		local characteristicClaims = (castMember.qualifiers and castMember.qualifiers['P1552']) or nil
		
		local role = ''
		local uncredited = false
		local isLeadingActor = false
		
		-- Get the role if available
        if roleClaims and roleClaims[1].datavalue and roleClaims[1].datavalue.value then
            local roleQID = roleClaims[1].datavalue.value.id
            if roleQID == 'Q18086706' then
            	role = 'Self'
            elseif roleQID then
            	role = getLink({roleQID})
            end
        end
        
        -- Check for characteristics (leading actor, uncredited)
        if characteristicClaims then
            for _, characteristic in ipairs(characteristicClaims) do
                local characteristicQID
                if characteristic and characteristic.datavalue and characteristic.datavalue.value then
                	characteristicQID = characteristic.datavalue.value.id
                end
                if characteristicQID == 'Q1765879' then  -- Leading actor
                    isLeadingActor = true
                elseif characteristicQID == 'Q16582801' or characteristicQID == 'Q122392315' then  -- Uncredited
                    uncredited = true
                end
            end
        end
        
        -- Generate link for the cast member
        local castMemberLink = getLink({castMemberQID})
        if uncredited then
            castMemberLink = castMemberLink .. ' (uncredited)'
        end
        
        -- generate table cells
        local castMemberCells = '<tr><td>' .. role .. '</td><td>' .. castMemberLink .. '</td></tr>'
        
        -- Sort leading actors, other cast members, and uncredited actors
        if isLeadingActor then
            table.insert(leadingActors, castMemberCells)
        elseif uncredited then
            table.insert(uncreditedActors, castMemberCells)
        else
            table.insert(otherCastMembers, castMemberCells)
        end
    end
    
    -- Combine leading actors, other cast members, and uncredited actors
    local castList = table.concat(leadingActors) .. table.concat(otherCastMembers) .. table.concat(uncreditedActors)
    
    -- Return the formatted table if there are cast members
    if castList ~= '' then
    	return '<tr><th colspan="2">Cast</th></tr><tr><th>Role</th><th>Actor</th></tr>' .. castList
    else
        return ''
    end
end

function p.getCastList(frame)
	return p._getCastList(getArgs(frame))
end

function p._getCrewList(args)
    local properties = {
        {"Production company", "P272"},
        {"Distributor", "P750"},
        {"Director", "P57"},
        {"Producer", "P162"},
        {"Screenwriter", "P58"},
        {"Cinematographer", "P344"},
        {"Editor", "P1040"},
        {"Composer", "P86"},
        {"Animator", "P6942"},
        {"Production designer", "P2554"},
        {"Costume designer", "P2515"},
        {"Storyboard artist", "P3275"},
    }
    local deathYearNeeded = {
        ["Director"] = true,
        ["Producer"] = true,
        ["Screenwriter"] = true,
        ["Animator"] = true,
        ["Cinematographer"] = true,
        ["Composer"] = true
    }
    
    local wikidataItem = args[1]
    if not wikidataItem then
        return error_message({'[[Module:Cast and crew]] error: no Wikidata item provided'})
    end
    
    local entity = mw.wikibase.getEntity(wikidataItem)
    if not entity or not entity.claims then
        return ''
    end
    
    local crewTableRows = {}
    local latestDeathYear = 0
    local deathYears = {}
    
    local releaseDateClaim = entity.claims['P577'] and entity.claims['P577'][1]
    local releaseDate = releaseDateClaim and releaseDateClaim.mainsnak.datavalue.value.time
    local releaseYear = releaseDate and releaseDate:match('+([0-9]+)')
    
    local isSilentFilm = false
    if releaseYear and tonumber(releaseYear) >= 1926 then
        local instanceOfClaims = entity.claims['P31'] or {}
        local genreClaims = entity.claims['P136'] or {}
        for _, claim in ipairs(instanceOfClaims) do
            if not isSilentFilm and claim and claim.mainsnak and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value and claim.mainsnak.datavalue.value.id == 'Q226730' then
                isSilentFilm = true
                break
            end
        end
        if not isSilentFilm then
            for _, claim in ipairs(genreClaims) do
                if not isSilentFilm and claim and claim.mainsnak and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value and claim.mainsnak.datavalue.value.id == 'Q226730' then
                    isSilentFilm = true
                    break
                end
            end
        end
    end
    
    for _, prop in ipairs(properties) do
        local role, propertyId = unpack(prop)
        local crewMembers = entity.claims[propertyId]
        
        if crewMembers then
            local crewMemberLinks = {}
            for _, crewMember in ipairs(crewMembers) do
            	if crewMember and crewMember.mainsnak and crewMember.mainsnak.datavalue and crewMember.mainsnak.datavalue.value then
	                local crewMemberQID = crewMember.mainsnak.datavalue.value.id
	                local crewMemberLink = getLink({crewMemberQID})
	                
	                if deathYearNeeded[role] and not deathYears[crewMemberQID] then
	                    local deathDateClaims = mw.wikibase.getBestStatements(crewMemberQID, 'P570')
	                    if deathDateClaims and deathDateClaims[1] and deathDateClaims[1].mainsnak and deathDateClaims[1].mainsnak.datavalue and deathDateClaims[1].mainsnak.datavalue.value then
	                        local deathDate = deathDateClaims[1].mainsnak.datavalue.value['time']
	                        if deathDate then
		                        local deathYear = deathDate:match('+([0-9]+)')
		                        if deathYear then
		                            crewMemberLink = crewMemberLink .. ' (d. ' .. deathYear .. ')'
		                            deathYears[crewMemberQID] = deathYear
		                            latestDeathYear = math.max(latestDeathYear, tonumber(deathYear))
		                        end
	                        end
	                    end
	                end
	                
                	table.insert(crewMemberLinks, crewMemberLink)
                end
            end
            
            if #crewMemberLinks > 0 then
                table.insert(crewTableRows, '<tr><td>' .. role .. '</td><td>' .. table.concat(crewMemberLinks, ', ') .. '</td></tr>')
            end
        end
    end
    
	if #crewTableRows == 0 then
        return ''
	end
	
	table.insert(crewTableRows, 1, '<tr><th colspan="2">Crew</th></tr>')
	
    if latestDeathYear > 0 then
        local currentYear = tonumber(os.date('*t').year)
        local pmaYears = currentYear - latestDeathYear - 1
        local footnoteText = string.format('Based on available information, the latest crew member that is relevant to international copyright laws died in %d, meaning that this film may be in the public domain in countries and jurisdictions with %d years p.m.a. or less, as well as in the United States.', latestDeathYear, pmaYears)
        table.insert(crewTableRows, '<tr><td colspan="2" class="footnoteText">' .. footnoteText .. '</td></tr>')
    end
    
    return table.concat(crewTableRows)
end

function p.getCrewList(frame)
	return p._getCrewList(getArgs(frame))
end

function p._generateCastAndCrew(args)
	-- Try to get the current Wikidata item associated with the page
	local currentPageQID = args[1] or mw.wikibase.getEntityIdForCurrentPage()
	if not currentPageQID then
		return ''
	end
	
	local castListContent = p._getCastList({currentPageQID})
	local crewListContent = p._getCrewList({currentPageQID})
	
	if crewListContent == '' and castListContent == '' then
	    return ''
	end
	
	return tostring(mw.html.create('table')
		:addClass('wikitable mw-collapsible mw-collapsed wst-cast-and-crew')
		:wikitext('<caption>Cast and Crew&#32;</caption>' .. castListContent .. crewListContent)
	)
end

function p.generateCastAndCrew(frame)
	return p._generateCastAndCrew(getArgs(frame))
end

return p