Module:VOCALOID Songbox/new
来自Vocawiki
更多操作
模块文档
原设计 by User:Amero
{{VOCALOID Songbox}}的临时fork,待格式整改完成之后合并入原模板。
示例
{{VOCALOID_Songbox/new
|image = 春风.jpg
|颜色 = #F5DF41;color:#ED3439
|演唱 = [[初音未来]] & '''john'''
|歌曲名称 = {{lj|春嵐}}<br>春日风暴
|P主 = [[john]]
|nnd_id = sm36053074
|投稿时间 = 2019年12月7日 <!--未填写站点_date参数时将回退到该参数-->
|yt_id = pUH9vCsvq08
|bb_id = BV1MqKXz8Epm
|bb_date = 2025年6月30日
|其他资料 = 仍然保留了其他资料,但不会自动生成文本
|再生 = 将被忽略
|nocate = 1 <!--不添加日期分类-->
}}
投稿平台数小于3时的样式
特殊情况
如果歌曲有多个版本且不宜使用{{tabs}},或是存在删稿、非公开等情况,可以使用 {{VOCALOID Songbox/card}} ,其参数如下:
{{VOCALOID Songbox/card
|nnd <!--投稿平台,命名与 Songbox 本体的参数一致,目前包括 nnd/yt/bb/ac/wyy-->
|sm964511 <!--稿件 id-->
|2007年9月1日 <!--投稿时间,格式要求与本体一致-->
|初版 <!--显示的版本名称,可省略,省略时显示默认的平台名称-->
|再生 = 11,375 <!--用于删稿、非公开时手动填写再生数-->
|class = <!--附加样式类-->
}}
目前可用的附加 class 包括:deleted, one-column, two-columns。
示例
使用 class = deleted 标记视频已删除,目前会使卡片中的标题变灰并加删除线、播放量加下划线,如果填写了再生参数则还会在再生数之后添加(最终记录)。
{{VOCALOID_Songbox/new
|image = 初音未来V2盒装图像.png
|颜色 = #39C5BB
|演唱 = [[初音未来]]
|歌曲名称 = {{lj|鳥の詩}}<br>鸟之诗
|P主 = {{lj|でんげん}}
|投稿 =
{{VOCALOID Songbox/card|nnd|sm964511|2007年9月1日|初版|再生=11,375|class=deleted}}
{{VOCALOID Songbox/card|nnd|sm973051|2007年9月2日|第一次修正版|再生=100,689|class=deleted}}
{{VOCALOID Songbox/card|nnd|sm1014311|2007年9月8日|第二次修正版|再生=21,975|class=deleted}}
{{VOCALOID Songbox/card|nnd|sm1051563|2007年9月13日|第三次修正版}}
{{VOCALOID Songbox/card|nnd|sm1175168|2007年9月30日|完整版}}
{{VOCALOID Songbox/card|nnd|sm1279356|2007年10月14日|音质提高版}}
}}
鳥の詩
鸟之诗演唱
P主
でんげん
投稿
如果出于逻辑或美观需求需要强制卡片以更少的列数显示,可以在第一个card中使用 two-columns 或 one-column:
{{VOCALOID_Songbox/new
|image = Six Greetings.jpeg
|演唱 = [[初音未来]]
|歌曲名称 = Six Greetings
|P主 = {{lj|[[きくお]]}}
|投稿 =
{{VOCALOID Songbox/card|nnd|so21551768|2013/8/9|Niconico Short ver|class=two-columns}}
{{VOCALOID Songbox/card|yt|CWtHv3Lih_0|2013/8/9|YouTube Short ver}}
{{VOCALOID Songbox/card|nnd|so21659640|2013/8/23|Niconico Long ver}}
{{VOCALOID Songbox/card|yt|3ejUVQGSKq0|2013/8/23|YouTube Long ver}}
}}
local strings = require('Module:Strings')
local get = require('Module:Get')
local acandy = require('Module:ACandy')
local a, Fragment, Raw = acandy.a, acandy.Fragment, acandy.Raw
local div, span = a.div, a.span
local trim = mw.text.trim
---@alias Image {
--- file: string,
--- caption: string?,
--- size: string?,
---}
---@alias Date {
--- year: number,
--- month: number,
--- day: number,
---}
---@alias Platform { id: string, date: Date }
---@alias Platforms {
--- nico: Platform?,
--- bili: Platform?,
--- acfun: Platform?,
--- ncm: Platform?,
--- ytb: Platform?,
---}
---@alias Style {
--- general: string?,
--- [1]: string?,
--- [2]: string?,
--- [3]: string?,
---}
---@param str string?
---@return string?
local function clean(str)
if not str then return nil end
str = trim(str)
return str ~= '' and str or nil
end
---@param s1 string?
---@param s2 string?
---@return string?
local function merge(s1, s2)
local merged = trim((s1 or '')..(s2 or ''))
return merged ~= '' and merged or nil
end
local function raw_or_empty(v)
return v ~= nil and Raw(v) or ''
end
local c = {} -- components
---@param props {
--- image: Image?,
--- tabs: string?,
--- title: string?,
--- uploader: string?,
--- producer: string?,
--- note: string?,
--- singer: string?,
--- albums: string?,
--- platforms: Platforms,
--- submissions: string?,
--- colors: Style,
--- text_styles: Style,
---}
function c.SongBox(props, frame)
local colors = props.colors
local text_styles = props.text_styles
local function THeader(nth, content)
local th_style = colors[nth] or colors.general
if th_style then
th_style = 'background:'..th_style
else
th_style = 'background: #66CDAA; color: #FFF;'
end
return div['songbox_th'] {
style = th_style,
span {
style = 'display: flex; align-items: center;'..(text_styles[nth] or text_styles.general or ''),
content,
},
}
end
return div['songbox'] {
props.image and c.Image(props.image) or '',
raw_or_empty(props.tabs),
c.Title(props.title),
c.Note(props),
THeader(1, '演唱'),
div['songbox_td'] / raw_or_empty(props.singer),
THeader(2, props.uploader and 'UP主' or 'P主'),
div['songbox_td'] / raw_or_empty(props.uploader or props.producer),
THeader(3, props.albums and '收录专辑' or '投稿'),
div['songbox_td'] /
(props.albums
and Raw(props.albums)
or div['songbox_submission_container'] /
{ c.Submissions(props.platforms), raw_or_empty(props.submissions) }
),
}
end
---@param props Image
function c.Image(props)
local frame = mw.getCurrentFrame()
return Raw(frame:expandTemplate { title = '氛围图片', args = props })
end
---@param title string?
function c.Title(title)
if not title then
return div['songbox_titles unknown']
end
local titles = strings.split(title, '%s*<br%s*/?>%s*')
local main_title = table.remove(titles, 1)
return div['songbox_titles'] {
div['songbox_main-title'] / Raw(main_title),
Raw(table.concat(titles, '<br>')),
}
end
---@param props {note: string?}
function c.Note(props)
return props.note and div['songbox_note'] {
raw_or_empty(props.note),
} or ''
end
local platform_data = get(
{ key = 'nico', url = 'https://www.nicovideo.jp/watch/%s', name = 'niconico' },
{ key = 'bili', url = 'https://www.bilibili.com/video/%s', name = 'bilibili' },
{ key = 'acfun', url = 'http://www.acfun.cn/v/%s', name = 'AcFun' },
{ key = 'ncm', url = 'https://music.163.com/#/song?id=%s', name = '网易云' },
{ key = 'ytb', url = 'https://www.youtube.com/watch?v=%s', name = 'YouTube' })
---@param props Platforms
function c.Submissions(props)
local submissions = Fragment()
for platform in platform_data:filter(function (x) return props[x.key] end) do
---@cast platform { key: string, url: string, name: string }
local data = props[platform.key] ---@cast data Platform
submissions:insert(c.Card(platform, data))
end
return submissions
end
---@param platform { key: string, url: string, name: string }
---@param data Platform
function c.Card(platform, data, overrides)
overrides = overrides or {}
local frame = mw.getCurrentFrame()
local card = div[('songbox_submission %s %s'):format(platform.key, overrides.class or '')] {
div['submission_link']
/ (data.id and Raw(('[%s -{}-]'):format(platform.url:format(data.id)))),
div['submission_platform'] / Raw(overrides.title or platform.name),
div['submission_info'] {
div['submission_date']
/ Raw(('%s-%s-%s'):format(data.date.year, data.date.month, data.date.day)),
div['submission_playcount'] /
(overrides.playcount
and span['override'] / {
Raw(overrides.playcount),
overrides.playcount_note and span['override-note']
/ Raw(('(%s)'):format(overrides.playcount_note)),
}
or Raw(frame:expandTemplate {
title = platform.name .. 'Count',
args = { id = data.id },
}))
},
mw.title.getCurrentTitle()--[[@cast -?]]:inNamespace(0)
and Raw(
('[[Category:%s年投稿至%s的歌曲]]'):format(data.date.year, platform.name)
.. ('[[Category:%s月%s日投稿至%s的歌曲]]'):format(data.date.month, data.date.day, platform.name)
) or nil,
}
if mw.title.getCurrentTitle()--[[@cast -?]]:inNamespace(0) then
card['data-json'] = mw.text.jsonEncode({
id = data.id,
date = data.date,
platform = { name = platform.name }
})
end
return card;
end
local p = {
components = c,
}
local function get_by_aliases(t, ...)
local n = select('#', ...)
for i = 1, n do
local key = select(i, ...)
local item = t[key]
if item ~= nil then
return item
end
end
return nil
end
---@param date_str string?
---@return Date?
local function parse_date(date_str)
if not date_str then return nil end
local parts = strings.split(date_str, '%s*[-/年月日]%s*', true)
local year = tonumber(parts[1])
local month = tonumber(parts[2])
local day = tonumber(parts[3])
if year and month and day then
return { year = year, month = month, day = day }
else
return nil
end
end
--[[
image
圖片信息 + 图片信息
圖片大小 > 图片大小
tabs
歌曲名称
投稿时间 + 其他资料
演唱
UP主 > P主
收录专辑 > 各平台id&date + 链接
颜色
颜色1
颜色2
颜色3
文字样式
文字样式1
文字样式2
文字样式3
]]
function p.from_args(uncleaned_args)
local args = {} ---@type table<string, string | nil>
for k, v in pairs(uncleaned_args) do
args[k] = clean(v)
end
function platform_from_arg(prefix)
local id = args[prefix..'_id']
local date = parse_date(args[prefix..'_date']
or get_by_aliases(args, '投稿时间', '投稿時間'))
if not id or not date then return nil end -- TODO: 报错
return {
id = id,
date = date,
}
end
local props = {
image = args.image and {
file = args.image,
caption = merge(args['圖片信息'], args['图片信息']),
size = get_by_aliases(args, '圖片大小', '图片大小'),
},
tabs = args.tabs,
title = args['歌曲名称'],
uploader = args['UP主'],
producer = args['P主'],
note = args['其他资料'],
singer = args['演唱'],
albums = args['收录专辑'],
platforms = {
nico = platform_from_arg('nnd'),
bili = platform_from_arg('bb'),
acfun = platform_from_arg('ac'),
ncm = platform_from_arg('wyy'),
ytb = platform_from_arg('yt'),
},
submissions = args['投稿'],
colors = {
general = args['颜色'],
args['颜色1'],
args['颜色2'],
args['颜色3'],
},
text_styles = {
general = args['文字样式'],
args['文字样式1'],
args['文字样式2'],
args['文字样式3'],
}
}
return c.SongBox(props)
end
function p.from_frame(frame)
return p.from_args(frame.args)
end
function p.from_parent(frame)
local parent = frame:getParent()
return p.from_frame(parent)
end
function p.card_from_args(uncleaned_args)
local args = {} ---@type table<string, string | nil>
for k, v in pairs(uncleaned_args) do
args[k] = clean(v)
end
local conversion = {
nnd = "nico",
bb = "bili",
yt = "ytb",
wyy = "ncm",
ac = "acfun"
}
local key = conversion[args[1]] or args[1]
local data = {
id = args[2],
date = parse_date(args[3])
}
local overrides = {
title = args[4],
playcount = get_by_aliases(args, '再生', 'count'),
playcount_note = get_by_aliases(args, '再生提示', 'countnote'),
class = args['class']
}
if overrides.class and overrides.class:find("deleted")
and not overrides.playcount_note then
overrides.playcount_note = "最终记录"
end
if overrides.playcount_note == "nil" then
overrides.playcount_note = nil
end
for platform in platform_data:filter(function (x) return x.key == key end) do
return c.Card(platform, data, overrides)
end
end
function p.card_from_frame(frame)
return p.card_from_args(frame.args)
end
function p.card_from_parent(frame)
local parent = frame:getParent()
return p.card_from_frame(parent)
end
return p