522 lines
17 KiB
Lua
522 lines
17 KiB
Lua
-- 歌词显示软件
|
||
local function loadRemoteFont(url)
|
||
local response = http.get(url)
|
||
if not response then
|
||
error("无法连接到字体服务器")
|
||
end
|
||
|
||
if response.getResponseCode() ~= 200 then
|
||
error("字体服务器返回错误: " .. response.getResponseCode())
|
||
end
|
||
|
||
local content = response.readAll()
|
||
response.close()
|
||
|
||
local sandbox = {}
|
||
local chunk, err = load(content, "=remoteFont", "t", sandbox)
|
||
if not chunk then
|
||
error("加载字体失败: " .. err)
|
||
end
|
||
|
||
local success, result = pcall(chunk)
|
||
if not success then
|
||
error("执行字体脚本失败: " .. result)
|
||
end
|
||
|
||
return sandbox.font or sandbox[1] or result
|
||
end
|
||
|
||
-- 显示单个字符的函数
|
||
local function displayChar(charMap, x, y, textColor, backgroundColor)
|
||
local origTextColor = term.getTextColor()
|
||
local origBackgroundColor = term.getBackgroundColor()
|
||
|
||
term.setTextColor(textColor)
|
||
term.setBackgroundColor(backgroundColor)
|
||
|
||
for row = 1, #charMap do
|
||
term.setCursorPos(x, y + row - 1)
|
||
local line = charMap[row]
|
||
|
||
for col = 1, #line do
|
||
local byte = string.byte(line, col)
|
||
|
||
if byte < 128 then
|
||
term.setTextColor(backgroundColor)
|
||
term.setBackgroundColor(textColor)
|
||
term.write(string.char(byte + 128))
|
||
else
|
||
term.setTextColor(textColor)
|
||
term.setBackgroundColor(backgroundColor)
|
||
term.write(string.char(byte))
|
||
end
|
||
end
|
||
end
|
||
|
||
term.setTextColor(origTextColor)
|
||
term.setBackgroundColor(origBackgroundColor)
|
||
end
|
||
|
||
-- 显示UTF-8字符串的函数
|
||
local function displayUtf8String(str, font, x, y, textColor, backgroundColor)
|
||
local function utf8codes(str)
|
||
local i = 1
|
||
return function()
|
||
if i > #str then return end
|
||
|
||
local b1 = string.byte(str, i)
|
||
i = i + 1
|
||
|
||
if b1 < 0x80 then
|
||
return b1
|
||
elseif b1 >= 0xC0 and b1 < 0xE0 then
|
||
local b2 = string.byte(str, i) or 0
|
||
i = i + 1
|
||
return (b1 - 0xC0) * 64 + (b2 - 0x80)
|
||
elseif b1 >= 0xE0 and b1 < 0xF0 then
|
||
local b2 = string.byte(str, i) or 0
|
||
i = i + 1
|
||
local b3 = string.byte(str, i) or 0
|
||
i = i + 1
|
||
return (b1 - 0xE0) * 4096 + (b2 - 0x80) * 64 + (b3 - 0x80)
|
||
else
|
||
return 32
|
||
end
|
||
end
|
||
end
|
||
|
||
local cursorX = x
|
||
|
||
for code in utf8codes(str) do
|
||
local charMap = font[code]
|
||
if not charMap then
|
||
charMap = font[32] or {{"\x80"}}
|
||
end
|
||
|
||
displayChar(charMap, cursorX, y, textColor, backgroundColor)
|
||
cursorX = cursorX + #charMap[1]
|
||
end
|
||
end
|
||
|
||
-- 解析歌词时间
|
||
local function parseTime(timeStr)
|
||
local minutes, seconds, milliseconds = timeStr:match("(%d+):(%d+)%.(%d+)")
|
||
if minutes and seconds and milliseconds then
|
||
return tonumber(minutes) * 60 + tonumber(seconds) + tonumber(milliseconds) / 1000
|
||
end
|
||
return 0
|
||
end
|
||
|
||
-- 解析歌词内容
|
||
local function parseLyrics(lyricStr)
|
||
local lyrics = {}
|
||
if not lyricStr or lyricStr == "" then return lyrics end
|
||
|
||
for line in lyricStr:gmatch("[^\r\n]+") do
|
||
if line:match("%[.+%]") then
|
||
local timeStr = line:match("%[(.+)%]")
|
||
local text = line:match("%].*$")
|
||
if text then text = text:sub(2) else text = "" end
|
||
|
||
-- 过滤掉作词、作曲等元信息
|
||
if timeStr and not timeStr:match("by:") and not timeStr:match("offset:") and not text:match("作词") and not text:match("作曲") then
|
||
local time = parseTime(timeStr)
|
||
table.insert(lyrics, {time = time, text = text})
|
||
end
|
||
end
|
||
end
|
||
|
||
-- 按时间排序
|
||
table.sort(lyrics, function(a, b) return a.time < b.time end)
|
||
return lyrics
|
||
end
|
||
|
||
-- 计算字符串宽度
|
||
local function getStringWidth(str, font)
|
||
local width = 0
|
||
local function utf8codes(s)
|
||
local i = 1
|
||
return function()
|
||
if i > #s then return end
|
||
local b1 = string.byte(s, i)
|
||
i = i + 1
|
||
if b1 < 0x80 then
|
||
return b1
|
||
elseif b1 >= 0xC0 and b1 < 0xE0 then
|
||
local b2 = string.byte(s, i) or 0
|
||
i = i + 1
|
||
return (b1 - 0xC0) * 64 + (b2 - 0x80)
|
||
elseif b1 >= 0xE0 and b1 < 0xF0 then
|
||
local b2 = string.byte(s, i) or 0
|
||
i = i + 1
|
||
local b3 = string.byte(s, i) or 0
|
||
i = i + 1
|
||
return (b1 - 0xE0) * 4096 + (b2 - 0x80) * 64 + (b3 - 0x80)
|
||
else
|
||
return 32
|
||
end
|
||
end
|
||
end
|
||
|
||
for code in utf8codes(str) do
|
||
local charMap = font[code] or font[32]
|
||
width = width + #charMap[1]
|
||
end
|
||
return width
|
||
end
|
||
|
||
-- 获取当前应该显示的歌词索引
|
||
local function getCurrentLyricIndex(lyricPairs, currentTime)
|
||
local currentIndex = 1
|
||
for i = 1, #lyricPairs do
|
||
if lyricPairs[i].time <= currentTime then
|
||
currentIndex = i
|
||
else
|
||
break
|
||
end
|
||
end
|
||
return currentIndex
|
||
end
|
||
|
||
-- 解析颜色参数
|
||
local function parseColor(colorStr)
|
||
local colorsMap = {
|
||
white = colors.white,
|
||
orange = colors.orange,
|
||
magenta = colors.magenta,
|
||
lightBlue = colors.lightBlue,
|
||
yellow = colors.yellow,
|
||
lime = colors.lime,
|
||
pink = colors.pink,
|
||
gray = colors.gray,
|
||
lightGray = colors.lightGray,
|
||
cyan = colors.cyan,
|
||
purple = colors.purple,
|
||
blue = colors.blue,
|
||
brown = colors.brown,
|
||
green = colors.green,
|
||
red = colors.red,
|
||
black = colors.black
|
||
}
|
||
|
||
return colorsMap[colorStr] or colors.white
|
||
end
|
||
|
||
-- 主显示函数
|
||
local function displayLyrics(url, notzh, fontType, colorsConfig)
|
||
-- 选择字体
|
||
local fontUrl
|
||
if fontType == "12px" then
|
||
fontUrl = "https://git.liulikeji.cn/xingluo/ComputerCraft-Utf8/raw/branch/main/fonts/fusion-pixel-12px-proportional-zh_hans.lua"
|
||
else
|
||
fontUrl = "https://git.liulikeji.cn/xingluo/ComputerCraft-Utf8/raw/branch/main/fonts/fusion-pixel-8px-proportional-zh_hans.lua"
|
||
end
|
||
|
||
local font = loadRemoteFont(fontUrl)
|
||
|
||
-- 获取歌词数据
|
||
local response = http.get(url)
|
||
if not response then
|
||
print("1 无法连接到服务器 URL: "..url)
|
||
return
|
||
end
|
||
|
||
if response.getResponseCode() ~= 200 then
|
||
print("服务器返回错误: " .. response.getResponseCode())
|
||
response.close()
|
||
return
|
||
end
|
||
|
||
local content = response.readAll()
|
||
response.close()
|
||
|
||
-- 解析JSON数据
|
||
local data = textutils.unserialiseJSON(content)
|
||
if not data or not data.lrc or not data.lrc.lyric then
|
||
print("2 无法解析歌词数据 URL: "..url)
|
||
return
|
||
end
|
||
|
||
-- 解析歌词
|
||
local lyrics = parseLyrics(data.lrc.lyric)
|
||
local tlyrics = {}
|
||
if not notzh and data.tlyric and data.tlyric.lyric then
|
||
tlyrics = parseLyrics(data.tlyric.lyric)
|
||
end
|
||
|
||
if #lyrics == 0 and #tlyrics == 0 then
|
||
print("3 没有找到有效歌词 URL: "..url)
|
||
print(textutils.serialiseJSON(data))
|
||
return
|
||
end
|
||
|
||
-- 构建时间索引映射
|
||
local timeIndexMap = {}
|
||
for i, lyric in ipairs(lyrics) do
|
||
local timeKey = string.format("%.3f", lyric.time)
|
||
timeIndexMap[timeKey] = {original = i}
|
||
end
|
||
|
||
for i, lyric in ipairs(tlyrics) do
|
||
local timeKey = string.format("%.3f", lyric.time)
|
||
if timeIndexMap[timeKey] then
|
||
timeIndexMap[timeKey].translation = i
|
||
else
|
||
timeIndexMap[timeKey] = {translation = i}
|
||
end
|
||
end
|
||
|
||
-- 构建歌词对数组
|
||
local lyricPairs = {}
|
||
local allTimes = {}
|
||
for timeKey, _ in pairs(timeIndexMap) do
|
||
table.insert(allTimes, tonumber(timeKey))
|
||
end
|
||
table.sort(allTimes)
|
||
|
||
for _, time in ipairs(allTimes) do
|
||
local timeKey = string.format("%.3f", time)
|
||
local indices = timeIndexMap[timeKey]
|
||
|
||
local originalText = ""
|
||
local translationText = ""
|
||
|
||
if indices.original then
|
||
originalText = lyrics[indices.original].text or ""
|
||
end
|
||
|
||
if indices.translation then
|
||
translationText = tlyrics[indices.translation].text or ""
|
||
end
|
||
|
||
table.insert(lyricPairs, {
|
||
time = time,
|
||
original = originalText,
|
||
translation = translationText
|
||
})
|
||
end
|
||
|
||
if #lyricPairs == 0 then
|
||
print("没有找到匹配的歌词对 URL: "..url)
|
||
return
|
||
end
|
||
|
||
-- 计算显示参数
|
||
local screenWidth, screenHeight = term.getSize()
|
||
local fontHeight = #font[32] -- 使用空格字符获取字体高度
|
||
|
||
-- 检查是否有翻译歌词
|
||
local hasTranslation = false
|
||
if not notzh then
|
||
for _, pair in ipairs(lyricPairs) do
|
||
if pair.translation and pair.translation ~= "" then
|
||
hasTranslation = true
|
||
break
|
||
end
|
||
end
|
||
end
|
||
|
||
-- 根据是否显示翻译来计算行高
|
||
local lyricPairHeight
|
||
if notzh or not hasTranslation then
|
||
lyricPairHeight = fontHeight + 1 -- 只有原文的高度
|
||
else
|
||
lyricPairHeight = fontHeight * 2 + 1 -- 原文+翻译的高度
|
||
end
|
||
|
||
local maxLyricPairs = math.floor(screenHeight / lyricPairHeight)
|
||
local visibleLyricPairs = math.max(1, maxLyricPairs) -- 确保至少显示1对
|
||
|
||
-- 显示循环
|
||
while true do
|
||
-- 获取当前播放时间
|
||
local currentTime = _G.getPlay - 1 or 0 -- 直接获取_G.getPlay的值
|
||
|
||
-- 获取当前歌词索引
|
||
local currentIndex = getCurrentLyricIndex(lyricPairs, currentTime)
|
||
term.setBackgroundColor(colorsConfig.background)
|
||
-- 清屏
|
||
term.clear()
|
||
|
||
-- 计算显示范围 - 真正的居中显示策略
|
||
local startPairIndex, endPairIndex
|
||
|
||
if visibleLyricPairs < 3 then
|
||
-- 当可显示歌词少于3个时,优先显示当前和下一个歌词
|
||
startPairIndex = currentIndex
|
||
endPairIndex = math.min(#lyricPairs, startPairIndex + visibleLyricPairs - 1)
|
||
|
||
-- 如果显示空间还有剩余,尝试向前补充显示前面的歌词
|
||
if endPairIndex - startPairIndex + 1 < visibleLyricPairs then
|
||
local remainingSpace = visibleLyricPairs - (endPairIndex - startPairIndex + 1)
|
||
startPairIndex = math.max(1, startPairIndex - remainingSpace)
|
||
endPairIndex = math.min(#lyricPairs, startPairIndex + visibleLyricPairs - 1)
|
||
end
|
||
else
|
||
-- 当可显示歌词3个或更多时,采用真正的居中显示策略
|
||
-- 计算当前歌词上方和下方应该显示的歌词数量
|
||
local aboveCount, belowCount
|
||
if visibleLyricPairs % 2 == 1 then
|
||
-- 奇数个显示位置:上下数量相等
|
||
local halfCount = math.floor(visibleLyricPairs / 2)
|
||
aboveCount = halfCount
|
||
belowCount = halfCount
|
||
else
|
||
-- 偶数个显示位置:上面少一个,下面多一个(如你要求的4个和5个)
|
||
aboveCount = math.floor(visibleLyricPairs / 2) - 1
|
||
belowCount = math.floor(visibleLyricPairs / 2)
|
||
end
|
||
|
||
-- 计算起始和结束索引
|
||
startPairIndex = math.max(1, currentIndex - aboveCount)
|
||
endPairIndex = math.min(#lyricPairs, currentIndex + belowCount)
|
||
|
||
-- 边界调整:如果前面不够显示,向后补充
|
||
if currentIndex - startPairIndex < aboveCount then
|
||
local needMore = aboveCount - (currentIndex - startPairIndex)
|
||
endPairIndex = math.min(#lyricPairs, endPairIndex + needMore)
|
||
end
|
||
|
||
-- 边界调整:如果后面不够显示,向前补充
|
||
if endPairIndex - currentIndex < belowCount then
|
||
local needMore = belowCount - (endPairIndex - currentIndex)
|
||
startPairIndex = math.max(1, startPairIndex - needMore)
|
||
end
|
||
|
||
-- 最终调整确保显示足够的行数
|
||
if endPairIndex - startPairIndex + 1 < visibleLyricPairs then
|
||
if startPairIndex > 1 then
|
||
startPairIndex = math.max(1, endPairIndex - visibleLyricPairs + 1)
|
||
else
|
||
endPairIndex = math.min(#lyricPairs, startPairIndex + visibleLyricPairs - 1)
|
||
end
|
||
end
|
||
end
|
||
|
||
|
||
local startY = 2
|
||
|
||
-- 显示歌词对
|
||
for i = startPairIndex, endPairIndex do
|
||
local lyricPair = lyricPairs[i]
|
||
|
||
-- 计算Y坐标 - 根据是否显示翻译动态计算
|
||
local pairY
|
||
if notzh or not hasTranslation then
|
||
-- 不显示翻译时,每行只占一个字体高度
|
||
pairY = startY + (i - startPairIndex) * (fontHeight + 1)
|
||
else
|
||
-- 显示翻译时,每对歌词占两个字体高度
|
||
pairY = startY + (i - startPairIndex) * (fontHeight * 2 + 1)
|
||
end
|
||
|
||
-- 设置颜色
|
||
local textColor = colorsConfig.normalMain
|
||
local translationColor = colorsConfig.normalSub
|
||
local bgColor = colorsConfig.background
|
||
|
||
if i == currentIndex then
|
||
textColor = colorsConfig.selectedMain
|
||
translationColor = colorsConfig.selectedSub
|
||
bgColor = colorsConfig.selectedBackground
|
||
end
|
||
|
||
-- 显示原文(第一行)
|
||
if lyricPair.original and lyricPair.original ~= "" then
|
||
local x = math.floor((screenWidth - getStringWidth(lyricPair.original, font)) / 2)
|
||
displayUtf8String(lyricPair.original, font, x, pairY, textColor, bgColor)
|
||
end
|
||
|
||
-- 显示翻译(第二行)- 只有当notzh为false且有翻译时才显示
|
||
if not notzh and hasTranslation and lyricPair.translation and lyricPair.translation ~= "" then
|
||
local x = math.floor((screenWidth - getStringWidth(lyricPair.translation, font)) / 2)
|
||
displayUtf8String(lyricPair.translation, font, x, pairY + fontHeight, translationColor, bgColor)
|
||
end
|
||
end
|
||
|
||
sleep(0.5)
|
||
end
|
||
|
||
term.clear()
|
||
term.setCursorPos(1, 1)
|
||
print("歌词显示已退出")
|
||
end
|
||
|
||
|
||
-- 解析命令行参数
|
||
local function parseArgs(args)
|
||
local url = nil
|
||
local notzh = false
|
||
local fontType = "8px" -- 默认8px
|
||
|
||
-- 默认颜色配置
|
||
local colorsConfig = {
|
||
background = colors.black, -- 背景颜色
|
||
normalMain = colors.gray, -- 歌词主颜色
|
||
normalSub = colors.gray, -- 歌词辅颜色
|
||
selectedMain = colors.white, -- 歌词选中主颜色
|
||
selectedSub = colors.lightGray, -- 选中辅颜色
|
||
selectedBackground = colors.black -- 选中背景色
|
||
}
|
||
|
||
for i = 1, #args do
|
||
local arg = args[i]
|
||
|
||
-- 处理颜色参数
|
||
if arg:match("--normabg=") then
|
||
local colorName = arg:sub(6)
|
||
colorsConfig.background = parseColor(colorName)
|
||
elseif arg:match("--normalmain=") then
|
||
local colorName = arg:sub(14)
|
||
colorsConfig.normalMain = parseColor(colorName)
|
||
elseif arg:match("--normalsub=") then
|
||
local colorName = arg:sub(13)
|
||
colorsConfig.normalSub = parseColor(colorName)
|
||
elseif arg:match("--selectedmain=") then
|
||
local colorName = arg:sub(16)
|
||
colorsConfig.selectedMain = parseColor(colorName)
|
||
elseif arg:match("--selectedsub=") then
|
||
local colorName = arg:sub(15)
|
||
colorsConfig.selectedSub = parseColor(colorName)
|
||
elseif arg:match("--selectedbg=") then
|
||
local colorName = arg:sub(14)
|
||
colorsConfig.selectedBackground = parseColor(colorName)
|
||
elseif arg == "--notzh" then
|
||
notzh = true
|
||
elseif arg == "--8px" then
|
||
fontType = "8px"
|
||
elseif arg == "--12px" then
|
||
fontType = "12px"
|
||
elseif not url and arg:sub(1, 4) == "http" then
|
||
url = arg
|
||
end
|
||
end
|
||
|
||
return url, notzh, fontType, colorsConfig
|
||
end
|
||
|
||
-- 主程序
|
||
local function main(...)
|
||
local args = {...}
|
||
local url, notzh, fontType, colorsConfig = parseArgs(args)
|
||
|
||
-- 如果没有通过参数提供URL,则要求用户输入
|
||
if not url then
|
||
term.clear()
|
||
term.setCursorPos(1, 1)
|
||
write("请输入歌词URL: ")
|
||
url = read()
|
||
|
||
if not url or url == "" then
|
||
print("URL不能为空")
|
||
return
|
||
end
|
||
end
|
||
|
||
displayLyrics(url, notzh, fontType, colorsConfig)
|
||
end
|
||
|
||
-- 运行主程序
|
||
main(...)
|
||
|