Files
ComputerCraft-Music168-Player/MusicLyrics.lua
2025-11-16 19:37:40 +08:00

522 lines
17 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 歌词显示软件
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(...)