Module:YouTubeSubscribers

Documentation for this module may be created at Module:YouTubeSubscribers/doc

POINT_IN_TIME_PID = "P585"
YT_CHAN_ID_PID= "P2397"
SUB_COUNT_PID = "P8687"

local p = {} 

-- taken from https://en.wikipedia.org/wiki/Module:Wd
function parseDate(dateStr, precision)
	precision = precision or "d"

	local i, j, index, ptr
	local parts = {nil, nil, nil}

	if dateStr == nil then
		return parts[1], parts[2], parts[3]  -- year, month, day
	end

	-- 'T' for snak values, '/' for outputs with '/Julian' attached
	i, j = dateStr:find("[T/]")

	if i then
		dateStr = dateStr:sub(1, i-1)
	end

	local from = 1

	if dateStr:sub(1,1) == "-" then
		-- this is a negative number, look further ahead
		from = 2
	end

	index = 1
	ptr = 1

	i, j = dateStr:find("-", from)

	if i then
		-- year
		parts[index] = tonumber(mw.ustring.gsub(dateStr:sub(ptr, i-1), "^%+(.+)$", "%1"), 10)  -- remove '+' sign (explicitly give base 10 to prevent error)

		if parts[index] == -0 then
			parts[index] = tonumber("0")  -- for some reason, 'parts[index] = 0' may actually store '-0', so parse from string instead
		end

		if precision == "y" then
			-- we're done
			return parts[1], parts[2], parts[3]  -- year, month, day
		end

		index = index + 1
		ptr = i + 1

		i, j = dateStr:find("-", ptr)

		if i then
			-- month
			parts[index] = tonumber(dateStr:sub(ptr, i-1), 10)

			if precision == "m" then
				-- we're done
				return parts[1], parts[2], parts[3]  -- year, month, day
			end

			index = index + 1
			ptr = i + 1
		end
	end

	if dateStr:sub(ptr) ~= "" then
		-- day if we have month, month if we have year, or year
		parts[index] = tonumber(dateStr:sub(ptr), 10)
	end

	return parts[1], parts[2], parts[3]  -- year, month, day
end

-- taken from https://en.wikipedia.org/wiki/Module:Wd
local function datePrecedesDate(aY, aM, aD, bY, bM, bD)
	if aY == nil or bY == nil then
		return nil
	end
	aM = aM or 1
	aD = aD or 1
	bM = bM or 1
	bD = bD or 1

	if aY < bY then
		return true
	elseif aY > bY then
		return false
	elseif aM < bM then
		return true
	elseif aM > bM then
		return false
	elseif aD < bD then
		return true
	end

	return false
end

function getClaimDate(claim)
	if claim['qualifiers'] and claim['qualifiers'][POINT_IN_TIME_PID] then 
		local pointsInTime = claim['qualifiers'][POINT_IN_TIME_PID]
		if #pointsInTime ~= 1 then
			-- be conservative in what we accept
			error("Encountered a statement with zero or multiple point in time (P85) qualifiers. Please add or remove point in time information so each statement has exactly one")
		end
		local pointInTime = pointsInTime[1]
		if pointInTime and pointInTime['datavalue'] and pointInTime['datavalue']['value'] and pointInTime['datavalue']['value']['time'] then
			return parseDate(pointInTime['datavalue']['value']['time'])
		end
	end
	return nil
end

-- for a given list of statements find the newest one with a matching qual
function newestMatchingStatement(statements, qual, targetQualValue)
	local newestStatement = nil
	local newestStatementYr = nil
	local newestStatementMo = nil
	local newestStatementDay = nil
    for k, v in pairs(statements) do
    	if v['rank'] ~= "deprecated" and v['qualifiers'] and v['qualifiers'][qual] then
    		local quals = v['qualifiers'][qual]
    		-- should only have one instance of the qualifier on a statement
    		if #quals == 1 then
    			local qual = quals[1]
    			if qual['datavalue'] and qual['datavalue']['value'] then
    				local qualValue = qual['datavalue']['value']
    				if qualValue == targetQualValue then
	    				local targetYr, targetMo, targetDay = getClaimDate(v)
	    				if targetYr then
	    					local older = datePrecedesDate(targetYr, targetMo, targetDay, newestStatementYr, newestStatementMo, newestStatementDay)
	    					if older == nil or not older then
	    						newestStatementYr, newestStatementMo, newestStatementDay = targetYr, targetMo, targetDay
	    						newestStatement = v
	    					end
	    				end
    				end
    			end
    		end
    	end
    end
	return newestStatement
end

-- for a given property and qualifier pair returns the newest statement that matches
function newestMatching(e, prop, qual, targetQualValue)
	-- first check the best statements
	local statements = e:getBestStatements(prop)
	local newestStatement = newestMatchingStatement(statements, qual, targetQualValue)
	if newestStatement then
		return newestStatement
	end
	-- try again with all statements if nothing so far
	statements = e:getAllStatements(prop)
	newestStatement = newestMatchingStatement(statements, qual, targetQualValue)
	if newestStatement then
		return newestStatement
	end
	return nil
end

function getEntity ( frame )
	local qid = nil
	if frame.args then
		qid = frame.args["qid"]
	end
	if not qid then
		qid = mw.wikibase.getEntityIdForCurrentPage()
	end
	assert(qid, "No qid found for page. Please make a Wikidata item for this article")
	local e = mw.wikibase.getEntity(qid)
	assert(e, "No such item found: " .. qid)
	return e
end

-- find the channel ID we are going to be getting the sub counts for
function getBestYtChanId(e) 
	local chanIds = e:getBestStatements(YT_CHAN_ID_PID)
	if #chanIds == 1 then
		local chan = chanIds[1]
		if chan and chan["mainsnak"] and chan["mainsnak"]["datavalue"] and chan["mainsnak"]["datavalue"]["value"] then
			return chan["mainsnak"]["datavalue"]["value"]
		end
	end
	return nil
end

function returnError(frame, eMessage)
	return frame:expandTemplate{ title = 'error', args = { eMessage } } .. "[[Category:Pages with YouTubeSubscribers module errors]]"
end

-- the date of the current YT subscriber count
function p.date( frame )
	local e = getEntity(frame)
	local chanId = getBestYtChanId(e)
	assert(chanId, "Could not find a single best YouTube channel ID for this item. Add a YouTube channel ID or set the rank of one channel ID to be preferred")
	local s = newestMatching(e, SUB_COUNT_PID, YT_CHAN_ID_PID, chanId)
	if s then
		local yt_year, yt_month, yt_day = getClaimDate(s)
		if not yt_year then
			return nil
		end
		local dateString = yt_year .. "|"
		-- construct YYYY|mm|dd date string
		if yt_month and yt_month ~= 0 then
			dateString = dateString .. yt_month .. "|"
			-- truncate the day of month
			--if yt_day and yt_day ~= 0 then
			--	dateString = dateString .. yt_day
			--end
		end
		return frame:expandTemplate{title="Format date", args = {yt_year, yt_month, yd_day}}
	end
	error("Could not find a date for YouTube subscriber information. Is there a social media followers statement (P8687) qualified with good values for P585 and P2397?")
end

function p.dateNice( frame )
	local status, obj = pcall(p.date, frame)
	if status then
		return obj
	else 
		return returnError(frame, obj)
	end
end

-- the most up to date number of subscribers
function p.subCount( frame )
	local e = getEntity(frame)
	local subCount = nil
	local chanId = getBestYtChanId(e)
	if chanId then
		local s = newestMatching(e, SUB_COUNT_PID, YT_CHAN_ID_PID, chanId)
		if s and s["mainsnak"] and s['mainsnak']["datavalue"] and s['mainsnak']["datavalue"]["value"] and s['mainsnak']["datavalue"]['value']['amount'] then
			subCount = s['mainsnak']["datavalue"]['value']['amount']
		end
	else 
		error("Could not find a single best YouTube channel ID for this item. Add a YouTube channel ID or set the rank of one channel ID to be preferred")
	end
    if subCount then
    	return tonumber(subCount)
    else
    	error("Found an associated YouTube channel ID but could not find a most recent value for social media followers (i.e. P8687 qualified with P585 and P2397)")
    end
end

function p.subCountNice( frame )
	local status, obj = pcall(p.subCount, frame)
	if status then
		return frame:expandTemplate{title="Format price", args = {obj}}
	else 
		return returnError(frame, obj)
	end
end

return p