Module:SocialMediaStats
| This module is rated as beta. It is considered ready for widespread use, but as it is still relatively new, it should be applied with some caution to ensure results are as expected. |
point in time (P585) (see uses)
YouTube channel ID (P2397) (see uses)
YouTube handle (P11245) (see uses)
social media followers (P8687) (see uses)
number of viewers/listeners (P5436) (see uses)
This module fetches social media stats count from a page's Wikidata entity. That data should be automatically updated on Wikidata's side.
Usage
[edit]Currently there are three methods YTsubscribers, YTviews and YTdate. They can be used with a YouTube channel ID like below:
{{#invoke:SocialMediaStats|YTsubscribers|youtube_id=YouTube channel ID}}
{{#invoke:SocialMediaStats|YTviews|youtube_id=YouTube channel ID}}
{{#invoke:SocialMediaStats|YTdate|youtube_id=YouTube channel ID}}
Or used with a YouTube handle like below:
{{#invoke:SocialMediaStats|YTsubscribers|youtube_handle=YouTube handle}}
{{#invoke:SocialMediaStats|YTviews|youtube_handle=YouTube handle}}
{{#invoke:SocialMediaStats|YTdate|youtube_handle=YouTube handle}}
You can use up to three of these and you can mix and match them as in:
{{#invoke:SocialMediaStats|YTsubscribers|youtube_handle=YouTube handle|youtube_id2=YouTube channel ID}}
They can be also used with a QID like below to specify where to load the data from but you generally won't need this.
{{#invoke:SocialMediaStats|YTsubscribers|qid=Wikidata entity ID|youtube_handle=YouTube handle }}
Error tracking category
[edit]
-- scribunto module to get YouTube channel statistics from Wikidata for social media personality infoboxes
require ('strict')
local autoDate = require("Module:Auto date formatter")
local POINT_IN_TIME_PID = "P585"
local YT_CHAN_ID_PID = "P2397"
local YT_HANDLE_PID = "P11245"
local SUB_COUNT_PID = "P8687"
local VIEW_COUNT_PID = "P5436"
local p = {}
-- taken from https://en.wikipedia.org/wiki/Module:Wd
local 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
local 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 qualifier
local 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
local 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
local function getValidStatements(e, prop)
-- call getAllStatements and filter out deprecated ones
local allStatements = e:getAllStatements(prop)
local validStatements = {}
for _, statement in pairs(allStatements) do
if statement['rank'] ~= "deprecated" then
table.insert(validStatements, statement)
end
end
return validStatements
end
local function getEntity(frame)
local qid = nil
if frame.args then
qid = frame.args["qid"]
end
if not qid or mw.text.trim(qid) == "" then
qid = mw.wikibase.getEntityIdForCurrentPage()
end
if not qid then
local e = nil
return e
end
local e = mw.wikibase.getEntity(qid)
assert(e, "No such item found: " .. qid)
return e
end
-- Convert YouTube handle to channel ID if needed
local function normalizeChannelId(channelParam)
if not channelParam then
return nil
end
if channelParam:sub(1, 1) == "@" then
return channelParam:sub(2)
else
return channelParam
end
end
-- Get all YouTube channel IDs from the entity
local function getAllYtChannelIds(e)
local channelIds = {}
local chanIdStatements = getValidStatements(e, YT_CHAN_ID_PID)
for _, statement in pairs(chanIdStatements) do
if statement and
statement["mainsnak"] and
statement["mainsnak"]["datavalue"] and
statement["mainsnak"]["datavalue"]["value"]
then
table.insert(channelIds, statement["mainsnak"]["datavalue"]["value"])
end
end
return channelIds
end
local function getHandlesToChannelIds(e)
-- get a mapping of handles to channel IDs and vice versa
local mapping = {}
mapping["handles"] = {}
mapping["channelIds"] = {}
local chanIdStatements = getValidStatements(e, YT_CHAN_ID_PID)
-- Iterate over each channel ID statement and find associated handles as qualifiers
for _, chanStatement in pairs(chanIdStatements) do
local channelId = nil
if chanStatement and
chanStatement["mainsnak"] and
chanStatement["mainsnak"]["datavalue"] and
chanStatement["mainsnak"]["datavalue"]["value"]
then
channelId = chanStatement["mainsnak"]["datavalue"]["value"]
end
-- Now look for handle qualifiers on this statement
if chanStatement['qualifiers'] then
local handleQuals = chanStatement['qualifiers'][YT_HANDLE_PID]
if handleQuals then
for _, handleQual in pairs(handleQuals) do
if handleQual['datavalue'] and handleQual['datavalue']['value'] then
local handleValue = handleQual['datavalue']['value']
local lowerHandle = handleValue:lower()
mapping["handles"][lowerHandle] = channelId
mapping["channelIds"][channelId] = handleValue
end
end
end
end
end
local handleStatements = getValidStatements(e, YT_HANDLE_PID)
-- Iterate over each handle statement and find associated channel IDs as qualifiers
for _, handleStatement in pairs(handleStatements) do
local handleValue = nil
if handleStatement and
handleStatement["mainsnak"] and
handleStatement["mainsnak"]["datavalue"] and
handleStatement["mainsnak"]["datavalue"]["value"]
then
handleValue = handleStatement["mainsnak"]["datavalue"]["value"]
end
-- Now look for channel ID qualifiers on this statement
if handleStatement['qualifiers'] then
local chanIdQuals = handleStatement['qualifiers'][YT_CHAN_ID_PID]
if chanIdQuals then
for _, chanIdQual in pairs(chanIdQuals) do
if chanIdQual['datavalue'] and chanIdQual['datavalue']['value'] then
local channelId = chanIdQual['datavalue']['value']
local lowerHandle = handleValue:lower()
mapping["handles"][lowerHandle] = channelId
mapping["channelIds"][channelId] = handleValue
end
end
end
end
end
return mapping
end
-- Find the best matching channel ID for a given parameter
local function findMatchingChannelId(e, channelParam)
if not channelParam then
return nil
end
local normalizedParam = normalizeChannelId(channelParam)
local allChannelIds = getAllYtChannelIds(e)
-- First try exact match
for _, channelId in pairs(allChannelIds) do
if channelId == normalizedParam or channelId == channelParam then
return channelId
end
end
-- If no exact match then we assume it's a handle and look for it
-- first check if it starts with UC
local handleToChannelId = getHandlesToChannelIds(e)
if handleToChannelId["handles"][normalizedParam:lower()] then
return handleToChannelId["handles"][normalizedParam:lower()]
end
return nil
end
local function returnError(frame, eMessage)
return frame:expandTemplate{ title = 'error', args = { eMessage } } .. "[[Category:Pages with SocialMediaStats module errors]]"
end
-- Get the statistic value from a statement
local function getStatisticValue(statement)
if statement and
statement["mainsnak"] and
statement['mainsnak']["datavalue"] and
statement['mainsnak']["datavalue"]["value"] and
statement['mainsnak']["datavalue"]['value']['amount']
then
return tonumber(statement['mainsnak']["datavalue"]['value']['amount'])
end
return nil
end
-- Get formatted date from a statement
local function getFormattedDate(frame, statement)
if statement then
local yt_year, yt_month, yt_day = getClaimDate(statement)
if yt_year then
return autoDate._access_archive_format(frame:expandTemplate{title="Format date", args = {yt_year, yt_month, yt_day}})
end
end
return nil
end
-- Get subscriber count for a channel
local function getSubscriberCount(e, channelId)
local statement = newestMatching(e, SUB_COUNT_PID, YT_CHAN_ID_PID, channelId)
return getStatisticValue(statement)
end
-- Get view count for a channel
local function getViewCount(e, channelId)
local statement = newestMatching(e, VIEW_COUNT_PID, YT_CHAN_ID_PID, channelId)
return getStatisticValue(statement)
end
-- Get the date for statistics (assumes subscriber and view counts have same date)
local function getStatsDate(frame, e, channelId)
local statement = newestMatching(e, SUB_COUNT_PID, YT_CHAN_ID_PID, channelId)
return getFormattedDate(frame, statement)
end
local function passedArgs(frame)
-- iterate over frame.args and check if any non-qid and non-number args are present
for k, v in pairs(frame.args) do
if k ~= "qid" and type(k) ~= "number" and tonumber(k) == nil then
return true
end
end
return false
end
-- Main function to get subscriber counts for up to 3 channels
function p.YTsubscribersInt(frame)
if not passedArgs(frame) then
return ""
end
local e = getEntity(frame)
if not e then
return ""
end
local results = {}
local singleResult = nil
local hasData = false
local handleMapping = getHandlesToChannelIds(e)
-- Check each of the 3 possible channels
for i = 1, 3 do
local handleParam = "youtube_handle" .. (i == 1 and "" or tostring(i))
local idParam = "youtube_id" .. (i == 1 and "" or tostring(i))
local channelParam = frame.args[handleParam]
if channelParam == nil or channelParam == "" then
channelParam = frame.args[idParam]
end
if channelParam then
local channelId = findMatchingChannelId(e, channelParam)
if channelId then
local subCount = getSubscriberCount(e, channelId)
if subCount and subCount > 0 then
local formattedCount = frame:expandTemplate{title="Format price", args = {subCount}}
local channelName = channelParam:gsub("^@", "")
if handleMapping["channelIds"][channelName] then
channelName = handleMapping["channelIds"][channelName]
end
table.insert(results, formattedCount .. " (" .. channelName .. ")")
singleResult = formattedCount
hasData = true
end
end
end
end
if not hasData then
local params = ""
for k, v in pairs(frame.args) do
params = params .. k .. "=" .. v .. "; "
end
return returnError(frame, "No subscriber data found for " .. e:getId() .. " with the provided parameters: " .. params)
end
-- If only one result, return it directly
if #results == 1 and singleResult ~= nil then
return singleResult
end
-- Multiple results, use {{ubl}}
return frame:expandTemplate{title="ubl", args = results}
end
-- Main function to get view counts for up to 3 channels
function p.YTviewsInt(frame)
if not passedArgs(frame) then
return ""
end
local e = getEntity(frame)
if not e then
return ""
end
local results = {}
local singleResult = nil
local hasData = false
local handleMapping = getHandlesToChannelIds(e)
-- Check each of the 3 possible channels
for i = 1, 3 do
local handleParam = "youtube_handle" .. (i == 1 and "" or tostring(i))
local idParam = "youtube_id" .. (i == 1 and "" or tostring(i))
local channelParam = frame.args[handleParam]
if channelParam == nil or channelParam == "" then
channelParam = frame.args[idParam]
end
if channelParam then
local channelId = findMatchingChannelId(e, channelParam)
if channelId then
local viewCount = getViewCount(e, channelId)
if viewCount and viewCount > 0 then
local formattedCount = frame:expandTemplate{title="Format price", args = {viewCount}}
local channelName = channelParam:gsub("^@", "") -- Remove @ if present for display
if handleMapping["channelIds"][channelName] then
channelName = handleMapping["channelIds"][channelName]
end
table.insert(results, formattedCount .. " (" .. channelName .. ")")
singleResult = formattedCount
hasData = true
end
end
end
end
if not hasData then
return ""
end
-- If only one result, return it directly
if #results == 1 and singleResult ~= nil then
return singleResult
end
-- Multiple results, use {{ubl}}
return frame:expandTemplate{title="ubl", args = results}
end
-- Function to get the date of statistics
function p.YTdateInt(frame)
local e = getEntity(frame)
if not e then
return ""
end
-- Try to get date from any available channel
for i = 1, 3 do
local handleParam = "youtube_handle" .. (i == 1 and "" or tostring(i))
local idParam = "youtube_id" .. (i == 1 and "" or tostring(i))
local channelParam = frame.args[handleParam]
if channelParam == nil or channelParam == "" then
channelParam = frame.args[idParam]
end
if channelParam then
local channelId = findMatchingChannelId(e, channelParam)
if channelId then
local date = getStatsDate(frame, e, channelId)
if date then
return date
end
end
end
end
return ""
end
-- Safe wrapper functions
function p.YTsubscribers(frame)
local status, obj = pcall(p.YTsubscribersInt, frame)
if status then
return obj
else
return returnError(frame, obj)
end
end
function p.YTviews(frame)
local status, obj = pcall(p.YTviewsInt, frame)
if status then
return obj
else
return returnError(frame, obj)
end
end
function p.YTdate(frame)
local status, obj = pcall(p.YTdateInt, frame)
if status then
return obj
else
return returnError(frame, obj)
end
end
return p
--[[
-- useful for debugger testing
local f = mw.getCurrentFrame()
local args = {}
args['qid'] = 'Q111862397'
args['youtube_handle'] = 'LinusTechTips'
f['args'] = args
p.YTsubscribersInt(f)
p.YTviewsInt(f)
local e = mw.wikibase.getEntity('Q57618112')
print(mw.dumpObject(getHandlesToChannelIds(e)))
--]]