diff --git a/javascript/commons/Countdown.js b/javascript/commons/Countdown.js index d23c15fc3c..e7a2ff57c4 100644 --- a/javascript/commons/Countdown.js +++ b/javascript/commons/Countdown.js @@ -49,7 +49,8 @@ liquipedia.countdown = { liquipedia.countdown.setupCountdownsIfSwitchToggleExists(); - // Only run when the window is actually in the front, not in background tabs (on browsers that support it) + // Only run when the window is actually in the front, + // not in background tabs (on browsers that support it) mw.loader.using( 'mediawiki.visibleTimeout' ).then( ( require ) => { liquipedia.countdown.timeoutFunctions = require( 'mediawiki.visibleTimeout' ); liquipedia.countdown.runCountdown(); @@ -96,7 +97,6 @@ liquipedia.countdown = { ); }, setCountdownString: function ( timerObjectNode ) { - const streamsarr = []; const countdownElem = timerObjectNode.querySelector( '.timer-object-countdown' ); let datestr = '', live = 'LIVE'; @@ -143,75 +143,12 @@ liquipedia.countdown = { } else { datestr = ''; // DATE ERROR! } - if ( timerObjectNode.dataset.streamTwitch ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamTwitch2 ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamYoutube ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamAfreeca ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamAfreecatv ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamBilibili ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamBooyah ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamCc163 ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamDailymotion ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamDouyu ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamFacebook ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamHuomao ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamHuya ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamKick ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamLoco ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamMildom ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamNimo ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamTrovo ) { - streamsarr.push( '' ); - } - if ( timerObjectNode.dataset.streamTl ) { - streamsarr.push( '' ); - } let html = '' + datestr + ''; - if ( datestr.length > 0 && streamsarr.length > 0 ) { + if ( datestr.length > 0 && timerObjectNode.dataset.hasstreams === 'true' ) { html += ' - '; } - if ( timerObjectNode.dataset.finished !== 'finished' ) { - html += streamsarr.join( ' ' ); - } countdownElem.innerHTML = html; }, - getStreamName: function ( url ) { - return url.replace( /\s/g, '_' ); - }, timeZoneAbbr: new Map( [ [ 'Acre Time', 'ACT' ], [ 'Afghanistan Time', 'AFT' ], diff --git a/standard/countdown.lua b/standard/countdown.lua new file mode 100644 index 0000000000..bf4210c2f3 --- /dev/null +++ b/standard/countdown.lua @@ -0,0 +1,76 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Countdown +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Arguments = require('Module:Arguments') +local DateExt = require('Module:Date/Ext') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') + +local StreamLinks = Lua.import('Module:Links/Stream') + +local Countdown = {} + +---@param frame Frame +---@return string +function Countdown.create(frame) + return Countdown._create(Arguments.getArgs(frame)) +end + +---@param args table +---@return string +function Countdown._create(args) + if Logic.isEmpty(args.date) and not args.timestamp then + return '' + end + + local wrapper = mw.html.create('span') + :addClass('timer-object') + + if Logic.readBool(args.rawcountdown) then + wrapper:addClass('timer-object-countdown-only') + end + if Logic.readBool(args.rawdatetime) then + wrapper:addClass('timer-object-datetime-only') + end + + -- Timestamp + local timestamp = args.timestamp or DateExt.readTimestampOrNil(args.date) or 'error' + wrapper:attr('data-timestamp', timestamp) + + local streams + if Logic.readBool(args.finished) then + wrapper:attr('data-finished', 'finished') + elseif not Logic.readBool(args.nostreams) then + streams = StreamLinks.buildDisplays(StreamLinks.filterStreams(args)) + end + if streams then + streams = table.concat(streams, ' ') + wrapper:attr('data-hasstreams', 'true') + end + + + if args.text then + wrapper:attr('data-countdown-end-text', args.text) + end + if args.separator then + wrapper:attr('data-separator', args.separator) + end + + wrapper:wikitext(args.date) + + if Logic.isEmpty(streams) then + return tostring(wrapper) + end + + return tostring(mw.html.create() + :node(wrapper) + :wikitext(streams) + ) +end + +return Countdown diff --git a/standard/links_stream.lua b/standard/links_stream.lua index 78463a6f89..98f19a41a8 100644 --- a/standard/links_stream.lua +++ b/standard/links_stream.lua @@ -11,20 +11,21 @@ Module containing utility functions for streaming platforms. ]] local StreamLinks = {} +local Array = require('Module:Array') local Class = require('Module:Class') local Logic = require('Module:Logic') +local Page = require('Module:Page') local String = require('Module:StringUtils') +local Table = require('Module:Table') local Variables = require('Module:Variables') ---[[ -List of streaming platforms supported in Module:Countdown. This is a subset of -the list in Module:Links -]] +local TLNET_STREAM = 'stream' + +--List of streaming platforms supported in Module:Countdown. StreamLinks.countdownPlatformNames = { 'afreeca', - 'afreecatv', 'bilibili', - 'cc163', + 'cc', 'dailymotion', 'douyu', 'facebook', @@ -34,30 +35,26 @@ StreamLinks.countdownPlatformNames = { 'loco', 'mildom', 'nimo', - 'stream', + TLNET_STREAM, 'tl', 'trovo', 'twitch', - 'twitch2', 'youtube', } ---[[ -Lookup table of allowed inputs that use a plattform with a different name -]] -StreamLinks.streamPlatformLookupNames = { - twitch2 = 'twitch', -} +---@param key string +---@return boolean +function StreamLinks.isStream(key) + return Array.any(StreamLinks.countdownPlatformNames, function(platform) + return String.startsWith(key, platform) + end) +end ---Extracts the streaming platform args from an argument table for use in Module:Countdown. ---@param args {[string]: string} ---@return table function StreamLinks.readCountdownStreams(args) - local stream = {} - for _, platformName in ipairs(StreamLinks.countdownPlatformNames) do - stream[platformName] = args[platformName] - end - return stream + return Table.filterByKey(args, StreamLinks.isStream) end ---Resolves the value of a stream given the platform @@ -83,45 +80,130 @@ function StreamLinks.processStreams(forwardedInputArgs) forwardedInputArgs.stream = nil end - for _, platformName in pairs(StreamLinks.countdownPlatformNames) do - local streamValue = Logic.emptyOr( - streams[platformName], - forwardedInputArgs[platformName], - Variables.varDefault(platformName) - ) + streams = Table.merge( + Table.filterByKey(forwardedInputArgs, StreamLinks.isStream), + Table.filterByKey(streams, StreamLinks.isStream) + ) - if String.isNotEmpty(streamValue) then - -- stream has no platform - if platformName ~= 'stream' then - local lookUpPlatform = StreamLinks.streamPlatformLookupNames[platformName] or platformName + local processedStreams = {} + Array.forEach(StreamLinks.countdownPlatformNames, function(platformName) + Table.mergeInto(processedStreams, StreamLinks._processStreamsOfPlatform(streams, platformName)) + end) + + return processedStreams +end - streamValue = StreamLinks.resolve(lookUpPlatform, streamValue) +---@param streamValues {[string]: string} +---@param platformName string +---@return {[string]: string} +function StreamLinks._processStreamsOfPlatform(streamValues, platformName) + local platformStreams = {} + local legacyStreams = {} + local enCounter = 0 + + for key, streamValue in Table.iter.spairs(streamValues) do + if platformName ~= TLNET_STREAM then + streamValue = StreamLinks.resolve(platformName, streamValue) + end + + -- legacy key + if key:match('^' .. platformName .. '%d*$') then + table.insert(legacyStreams, streamValue) + elseif key:match('^' .. platformName .. '_%a+_%d+') then + local streamKey = StreamLinks.StreamKey(key) + if streamKey.languageCode == 'en' then + enCounter = enCounter + 1 end + platformStreams[streamKey:toString()] = streamValue + end + end - local key = StreamLinks.StreamKey(platformName):toString() - streams[key] = streamValue - streams[platformName] = streamValue -- Legacy + for _, streamValue in ipairs(legacyStreams) do + if not Table.includes(platformStreams, streamValue) then + enCounter = enCounter + 1 + local streamKey = StreamLinks.StreamKey(platformName, 'en', enCounter):toString() + platformStreams[streamKey] = streamValue + platformStreams[platformName] = streamValue -- Legacy end end + if Logic.isEmpty(platformStreams) then + platformStreams = {[platformName] = Variables.varDefault(platformName)} + end + + return platformStreams +end + +---@param platform string +---@param streamValue string +---@return string? +function StreamLinks.displaySingle(platform, streamValue) + local icon = '' + if platform == TLNET_STREAM then + return Page.makeExternalLink(icon, 'https://tl.net/video/streams/' .. streamValue) + end + + local streamLink = StreamLinks.resolve(platform, streamValue) + if not streamLink then return nil end + + return Page.makeInternalLink({}, icon, 'Special:Stream/' .. platform .. '/' .. streamValue) +end + +---@param streams {string: string[]} +---@return string[]? +function StreamLinks.buildDisplays(streams) + local displays = {} + Array.forEach(StreamLinks.countdownPlatformNames, function(platform) + Array.forEach(streams[platform] or {}, function(streamValue) + table.insert(displays, StreamLinks.displaySingle(platform, streamValue)) + end) + end) + return Table.isNotEmpty(displays) and displays or nil +end + +---Filter non-english streams if english streams exists +---@param streamsInput {string: string} +---@return {string: string[]} +function StreamLinks.filterStreams(streamsInput) + local hasEnglishStream = Table.any(streamsInput, function(key) + return key:match('_en_') or Table.includes(StreamLinks.countdownPlatformNames, key) + end) + + local streams = {} + for rawHost, stream in Table.iter.spairs(streamsInput) do + if #(mw.text.split(rawHost, '_', true)) == 3 then + local streamKey = StreamLinks.StreamKey(rawHost) + local platform = streamKey.platform + if not streams[platform] then + streams[platform] = {} + end + table.insert(streams[platform], (not hasEnglishStream or streamKey.languageCode == 'en') and stream or nil) + end + end + + Array.forEach(StreamLinks.countdownPlatformNames, function(platformName) + local stream = streamsInput[platformName] + if type(streams[platformName]) == 'table' or String.isEmpty(stream) then return end + streams[platformName] = {stream, streamsInput[platformName .. 2]} + end) + return streams end --- StreamKey Class -- Contains the triplet that makes up a stream key -- [platform, languageCode, index] ----@class StreamKey +---@class StreamKey: BaseClass ---@operator call(...): StreamKey ---@field platform string ---@field languageCode string ---@field index integer ----@field is_a function -StreamLinks.StreamKey = Class.new( +local StreamKey = Class.new( function (self, ...) self:new(...) end ) -local StreamKey = StreamLinks.StreamKey +StreamLinks.StreamKey = StreamKey ---@param tbl string ---@param languageCode string @@ -146,7 +228,9 @@ function StreamKey:new(tbl, languageCode, index) languageCode = 'en' -- Input is a StreamKey in string format elseif #components == 3 then - platform, languageCode, index = unpack(components) + local stringIndex + platform, languageCode, stringIndex = unpack(components) + index = tonumber(stringIndex) --[[@as integer]] end end @@ -158,30 +242,29 @@ function StreamKey:new(tbl, languageCode, index) end ---@param input string ----@return string?, integer? +---@return string, integer function StreamKey:_fromLegacy(input) for _, platform in pairs(StreamLinks.countdownPlatformNames) do - -- The intersection of values in countdownPlatformNames and keys in streamPlatformLookupNames - -- are not valid platforms. E.g. "twitch2" is not a valid platform. - if not StreamLinks.streamPlatformLookupNames[platform] then - -- Check if this platform matches the input - if string.find(input, platform .. '%d-$') then - local index = 1 - -- If the input is longer than the platform, there's an index at the end - -- Eg. In "twitch2", the 2 would the index. - if #input > #platform then - index = tonumber(input:sub(#platform + 1)) --[[@as integer]] - end - return platform, index + if string.find(input, platform .. '%d-$') then + local index = 1 + -- If the input is longer than the platform, there's an index at the end + -- Eg. In "twitch2", the 2 would be the index. + if #input > #platform then + index = tonumber(input:sub(#platform + 1)) or index + assert(index, '"' .. input .. '" is not a supported stream key') end + return platform, index end end + error('"' .. input .. '" is not a supported stream key') end +---@return string function StreamKey:toString() return self.platform .. '_' .. self.languageCode .. '_' .. self.index end +---@return string function StreamKey:toLegacy() -- Return twitch instead of twitch1 if self.index == 1 then @@ -190,6 +273,7 @@ function StreamKey:toLegacy() return self.platform .. self.index end +---@return boolean function StreamKey:_isValid() assert(Logic.isNotEmpty(self.platform), 'StreamKey: Platform is required') assert(Logic.isNotEmpty(self.languageCode), 'StreamKey: Language Code is required') diff --git a/stylesheets/commons/Icons.less b/stylesheets/commons/Icons.less index e058a74dd5..fc0641f872 100644 --- a/stylesheets/commons/Icons.less +++ b/stylesheets/commons/Icons.less @@ -118,6 +118,7 @@ Note: When adding a new icon, please add to .icon-make-image( mixer, "//liquipedia.net/commons/images/8/85/InfoboxIcon_Mixer.png" ); .icon-make-image( music, "//liquipedia.net/commons/images/3/37/InfoboxIcon_Music.png" ); .icon-make-image( niconico, "//liquipedia.net/commons/images/b/bf/InfoboxIcon_Niconico.png" ); + .icon-make-image( nimo, "//liquipedia.net/commons/images/f/f7/InfoboxIcon_NimoTV.png" ); .icon-make-image( nimotv, "//liquipedia.net/commons/images/f/f7/InfoboxIcon_NimoTV.png" ); .icon-make-image( nwc3l, "//liquipedia.net/commons/images/1/1c/InfoboxIcon_NWC3L.png" ); .icon-make-image( octane, "//liquipedia.net/commons/images/d/da/InfoboxIcon_Octane.png" );