This repository has been archived on 2024-10-02. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
ltc/lts.go
2022-02-18 15:23:24 +08:00

329 lines
8.3 KiB
Go
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.

package main
import (
"bufio"
"fmt"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/jessevdk/go-flags"
)
type SRTContent struct {
//序号从1开始
Index int
//开始时间,单位毫秒
Start int
//结束时间,单位毫秒
End int
//歌词内容
Text string
}
type SRT struct {
//歌曲名
Title string
//歌手名 未指定文件名是,文件名格式为:歌曲名-歌手名.srt
Artist string
Content []*SRTContent
}
// Option 运行时传入的选项
type Option struct {
Id string `short:"i" long:"id" description:"歌曲的id网易云和QQ音乐均可。"`
Input string `short:"I" long:"input" description:"需要转换的LRC文件路径。"`
Source string `short:"s" long:"source" description:"当设置id时有效指定从网易云163还是QQ音乐qq上获取歌词。" default:"163" choice:"163" choice:"qq" choice:"QQ"`
Download bool `short:"d" long:"download" description:"只下载歌词,而不进行解析。"`
Mode int `short:"m" long:"mode" default:"1" description:"原文和译文的排列模式,可选值有:[1] [2] [3]" choice:"1" choice:"2" choice:"3"`
Version bool `short:"v" long:"version" description:"获取版本信息"`
Output string `no-flag:""`
}
const (
// VERSION 当前版本
VERSION = `"0.1.0" (build 2022.02.18)`
)
var (
ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36"
client = http.Client{}
opt Option
)
func main() {
args, err := flags.Parse(&opt)
if err != nil {
os.Exit(0)
}
//显示版本信息
if opt.Version {
fmt.Printf("LrcToSrt(lts) version %s\n", VERSION)
os.Exit(0)
}
//获取保存的文件名
if len(args) != 0 {
opt.Output = args[0]
}
//获取歌词
var lyric, tranLyric string
if opt.Id != "" {
if opt.Source != "163" {
lyric, tranLyric = GetQQLyric(opt.Id)
} else {
lyric, tranLyric = Get163Lyric(opt.Id)
}
//下载歌词
if opt.Download {
//对文件名进行处理
o := opt.Output
if o == "" {
o = opt.Id + ".lrc"
} else if !strings.HasSuffix(o, ".lrc") {
o += ".lrc"
}
WriteFile(o, lyric)
if tranLyric != "" {
WriteFile("tran_"+o, tranLyric)
}
fmt.Println("下载歌词完成!")
return
}
} else if opt.Input != "" {
//从文件中获取歌词
if !strings.HasSuffix(opt.Input, ".lrc") {
fmt.Println("Error: 不支持的格式目前只支持lrc歌词文件。")
os.Exit(1)
}
lyric = ReadFile(opt.Input)
if lyric == "" {
fmt.Println("获取歌词失败,文件内容为空。")
os.Exit(1)
}
} else {
fmt.Println("Error: 请指定需要转换的歌词。")
os.Exit(1)
}
lyricSRT, tranLyricSRT := Lrc2Srt(lyric), Lrc2Srt(tranLyric)
SaveSRT(lyricSRT, tranLyricSRT, opt.Output)
}
// SaveSRT 保存数据为SRT文件
func SaveSRT(srt *SRT, tranSrt *SRT, name string) {
if tranSrt == nil {
//没有译文时用一个空的对象的代替减少nil判断
tranSrt = &SRT{Content: make([]*SRTContent, 0)}
}
//处理结果文件的文件名
if name == "" {
title := srt.Title
if title != "" {
//以歌曲名命名
name = fmt.Sprintf("%s.srt", title)
} else if opt.Id != "" {
//以歌曲的id命名
name = fmt.Sprintf("%s.srt", opt.Id)
} else if opt.Input != "" {
//以LRC文件的文件名命名
name = fmt.Sprintf("%s.srt", opt.Input[:len(opt.Input)-4])
} else {
//以当前时间的毫秒值命名
name = fmt.Sprintf("%d.srt", time.Now().Unix())
}
} else if !strings.HasSuffix(name, ".srt") {
name += ".srt"
}
file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
fmt.Printf("保存结果失败:%v\n", err)
os.Exit(1)
}
defer file.Close()
writer := bufio.NewWriter(file)
index := 1
switch opt.Mode {
case 1:
//译文和原文交错排列
_len, _lent, size := len(srt.Content), len(tranSrt.Content), 0
if _len > _lent {
size = 2 * _len
} else {
size = 2 * _lent
}
//临时缓冲区,偶数索引存原文,奇数存译文
buf := make([]*SRTContent, size, size)
for i, item := range srt.Content {
buf[2*i] = item
}
for i, item := range tranSrt.Content {
buf[2*i+1] = item
}
//写入文件
for _, item := range buf {
if item != nil {
item.Index = index
_, _ = writer.WriteString(item.String())
index++
}
}
case 2:
//原文在上,译文在下
for _, item := range srt.Content {
item.Index = index
_, _ = writer.WriteString(item.String())
index++
}
for _, item := range tranSrt.Content {
item.Index = index
_, _ = writer.WriteString(item.String())
index++
}
case 3:
//译文在上,原文在下
for _, item := range tranSrt.Content {
item.Index = index
_, _ = writer.WriteString(item.String())
index++
}
for _, item := range srt.Content {
item.Index = index
_, _ = writer.WriteString(item.String())
index++
}
}
err = writer.Flush()
if err != nil {
fmt.Printf("保存结果失败:%v\n", err)
os.Exit(1)
}
fmt.Printf("转换文件完成,保存结果为:%s\n", name)
}
// Lrc2Srt 将原始个LRC字符串歌词解析SRT对象
func Lrc2Srt(src string) *SRT {
if src == "" {
return nil
}
//标准的LRC文件为一行一句歌词
lyrics := strings.Split(src, "\n")
//标识标签的正则 [ar:A-SOUL]形式
infoRegx := regexp.MustCompile(`^\[([a-z]+):([\s\S]*)]`)
srt := &SRT{Content: make([]*SRTContent, 0, len(lyrics))}
//解析标识信息
for {
if len(lyrics) == 0 {
break
}
l := lyrics[0]
//根据正则表达式进行匹配
info := infoRegx.FindStringSubmatch(l)
//标识信息位于歌词信息前面,当出现未匹配成功时,即可退出循环
if info != nil {
//info 中为匹配成功的字符串和 子组合(正则表达式中括号包裹的部分)
//例如,对于标识信息:[ar:A-SOUL]info中的数据为[[ar:A-SOUL] ar A-SOUL]
key := info[1]
switch key {
case "ar":
//歌手名
if len(info) == 3 {
srt.Artist = info[2]
}
case "ti":
//歌曲名
if len(info) == 3 {
srt.Title = info[2]
}
}
lyrics = lyrics[1:]
} else {
break
}
}
//歌词信息的正则,"[00:10.222]超级的敏感"或“[00:10:222]超级的敏感”或“[00:10]超级的敏感”或“[00:10.22]超级的敏感”或“[00:10:22]超级的敏感”
lyricRegx := regexp.MustCompile(`\[(\d\d):(\d\d)([.:]\d{2,3})?]([\s\S]+)`)
index := 0
for _, l := range lyrics {
content := lyricRegx.FindStringSubmatch(l)
if content != nil {
c := SplitLyric(content[1:])
if c != nil {
if index != 0 {
//前一条字幕的结束时间为当前字幕开始的时间
srt.Content[index-1].End = c.Start
}
srt.Content = append(srt.Content, c)
index++
}
}
}
//最后一条字幕
last := srt.Content[index-1]
//最后一条字幕的结束时间为其开始时间 + 10秒
last.End = last.Start + 10000
return srt
}
// SplitLyric 对分割出来的歌词信息进行解析
func SplitLyric(src []string) *SRTContent {
minute, err := strconv.Atoi(src[0])
second, err := strconv.Atoi(src[1])
if err != nil {
fmt.Printf("错误的时间格式:%s\n", src)
return nil
}
millisecond, content := 0, ""
_len := len(src)
if _len == 3 {
//歌词信息没有毫秒值
content = src[2]
} else if _len == 4 {
content = src[3]
//字符串的第一个字符是 "." 或 ":"
ms := src[2][1:]
millisecond, err = strconv.Atoi(ms)
//QQ音乐歌词文件中毫秒值只有两位需要特殊处理一下
if len(ms) == 2 {
millisecond *= 10
}
if err != nil {
fmt.Printf("错误的时间格式:%s\n", src)
return nil
}
}
srtContent := &SRTContent{}
srtContent.Start = Time2Millisecond(minute, second, millisecond)
srtContent.Text = content
return srtContent
}
//返回SRT文件中一句字幕的字符串表示形式
/**
1
00:00:01,111 --> 00:00:10,111
字幕
*/
func (s *SRTContent) String() string {
builder := strings.Builder{}
builder.WriteString(strconv.Itoa(s.Index))
builder.WriteByte('\n')
sh, sm, ss, sms := Millisecond2Time(s.Start)
eh, em, es, ems := Millisecond2Time(s.End)
builder.WriteString(fmt.Sprintf("%02d:%02d:%02d,%03d --> %02d:%02d:%02d,%03d\n",
sh, sm, ss, sms, eh, em, es, ems))
builder.WriteString(s.Text)
builder.WriteString("\n\n")
return builder.String()
}