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() }