From c9b1b38c0355f5db6a8711e21202d595860702b9 Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Mon, 28 Mar 2022 18:26:40 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=A1=B9=E7=9B=AE,=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E4=BA=86=E9=A1=B9=E7=9B=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- cloudlyric.go | 11 +- go.mod | 4 +- lrc.go | 128 +++++++++++++++++++ lts.go | 337 -------------------------------------------------- lts/main.go | 124 +++++++++++++++++++ lts_test.go | 25 ---- qqlyric.go | 11 +- srt.go | 173 ++++++++++++++++++++++++++ util.go | 15 ++- util_test.go | 10 +- 11 files changed, 456 insertions(+), 386 deletions(-) create mode 100644 lrc.go delete mode 100644 lts.go create mode 100644 lts/main.go delete mode 100644 lts_test.go create mode 100644 srt.go diff --git a/.gitignore b/.gitignore index 5bb3f1e..96d2cfd 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,6 @@ fabric.properties # Dependency directories (remove the comment below to include it) # vendor/ -/example/*.srt -/example/*.lrc +*.srt +*.lrc /temp diff --git a/cloudlyric.go b/cloudlyric.go index baac27a..0cd1fac 100644 --- a/cloudlyric.go +++ b/cloudlyric.go @@ -1,11 +1,10 @@ -package main +package lrc2srt import ( "encoding/json" "fmt" "net/http" "net/url" - "os" ) type CloudLyricBase struct { @@ -38,15 +37,15 @@ func Get163Lyric(id string) (lyric, tLyric string) { req, _ := http.NewRequest("GET", api, nil) //必须设置Referer,否则会请求失败 req.Header.Add("Referer", "https://music.163.com") - req.Header.Add("User-Agent", ChromeUA) + req.Header.Add("User-Agent", CHROME_UA) resp, err := client.Do(req) if err != nil { fmt.Printf("网络错误:%v\n", err) - os.Exit(1) + panic("网络异常,获取失败。") } if resp == nil || resp.StatusCode != http.StatusOK { fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode) - os.Exit(1) + panic("获取失败,未能正确获取到数据") } defer resp.Body.Close() @@ -54,7 +53,7 @@ func Get163Lyric(id string) (lyric, tLyric string) { err = json.NewDecoder(resp.Body).Decode(&cloudLyric) if cloudLyric.Sgc { fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", cloudLyric) - os.Exit(1) + panic("id错误,获取歌词失败") } return cloudLyric.Lrc.Lyric, cloudLyric.TLyric.Lyric } diff --git a/go.mod b/go.mod index 77c5a2a..20b43fd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module github.com/hami_lemon/lrc2srt +module github.com/Hami-Lemon/lrc2srt -go 1.17 +go 1.18 require github.com/jessevdk/go-flags v1.5.0 diff --git a/lrc.go b/lrc.go new file mode 100644 index 0000000..30257b3 --- /dev/null +++ b/lrc.go @@ -0,0 +1,128 @@ +package lrc2srt + +import ( + "regexp" + "strconv" + "strings" + + "github.com/Hami-Lemon/lrc2srt/glist" +) + +type LRCNode struct { + //歌词出现的时间,单位毫秒 + time int + //歌词内容 + content string +} + +type LRC struct { + //歌曲名 + Title string + //歌手名 + Artist string + //专辑名 + Album string + //歌词作者 + Author string + //歌词列表 + LrcList glist.Queue[*LRCNode] +} + +func ParseLRC(src string) *LRC { + if src == "" { + return nil + } + //标准的LRC文件为一行一句歌词 + lyrics := strings.Split(src, "\n") + //标识标签的正则 [ar:A-SOUL]形式 + infoRegx := regexp.MustCompile(`^\[([a-z]+):([\s\S]*)]`) + + lrc := &LRC{LrcList: glist.NewLinkedList[*LRCNode]()} + //解析标识信息 + 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 { + lrc.Artist = info[2] + } + case "ti": + //歌曲名 + if len(info) == 3 { + lrc.Title = info[2] + } + case "al": + //专辑名 + if len(info) == 3 { + lrc.Album = info[2] + } + case "by": + //歌词作者 + if len(info) == 3 { + lrc.Author = 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]+)`) + for _, l := range lyrics { + content := lyricRegx.FindStringSubmatch(l) + if content != nil { + node := SplitLyric(content[1:]) + if node != nil { + lrc.LrcList.PushBack(node) + } + } + } + return lrc +} + +// SplitLyric 对分割出来的歌词信息进行解析 +func SplitLyric(src []string) *LRCNode { + minute, err := strconv.Atoi(src[0]) + second, err := strconv.Atoi(src[1]) + if err != nil { + panic("错误的时间格式:" + strings.Join(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 { + panic("错误的时间格式:" + strings.Join(src, " ")) + return nil + } + } + lrcNode := &LRCNode{ + time: time2Millisecond(minute, second, millisecond), + content: content, + } + return lrcNode +} diff --git a/lts.go b/lts.go deleted file mode 100644 index 0dcb32e..0000000 --- a/lts.go +++ /dev/null @@ -1,337 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "net/http" - "os" - "regexp" - "sort" - "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.1" (build 2022.03.05)` -) - -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] - } - - //获取歌词,lyric为原文歌词,tranLyric为译文歌词 - 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) - } - //原文和译文作为两条歌词流信息分开保存,但最终生成的srt文件会同时包含两个信息 - 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)} - //因为没有译文,所以mode选项无效,设为2之后,后面不用做多余判断 - opt.Mode = 2 - } - //处理结果文件的文件名 - 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.Create(name) - if err != nil { - fmt.Printf("创建结果文件失败:%v\n", err) - os.Exit(1) - } - defer func(file *os.File) { - _ = file.Close() - }(file) - //6KB的缓存区,大部分歌词生成的SRT文件均在4-6kb左右 - writer := bufio.NewWriterSize(file, 1024*6) - - /* - 原文和译文歌词的排列方式,因为原文歌词中可能包含一些非歌词信息, - 例如作词者,作曲者等,而在译文歌词中却可能不包含这些 - */ - //srt的序号 - index := 1 - switch opt.Mode { - case 1: - //将两个歌词合并成一个新的数组 - size := len(srt.Content) + len(tranSrt.Content) - temp := make([]*SRTContent, size, size) - i := 0 - for _, v := range srt.Content { - temp[i] = v - i++ - } - for _, v := range tranSrt.Content { - temp[i] = v - i++ - } - //按开始时间进行排序,使用SliceStable确保一句歌词的原文在译文之前 - sort.SliceStable(temp, func(i, j int) bool { - return temp[i].Start < temp[j].Start - }) - //写入文件 - for i, v := range temp { - v.Index = i + 1 - _, _ = writer.WriteString(v.String()) - } - 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) - } else { - 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() -} diff --git a/lts/main.go b/lts/main.go new file mode 100644 index 0000000..f17c1bd --- /dev/null +++ b/lts/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "fmt" + "os" + "strings" + "time" + + l2s "github.com/Hami-Lemon/lrc2srt" + "github.com/jessevdk/go-flags" +) + +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.2.0" (build 2022.03.28)` +) + +var ( + opt Option +) + +func main() { + //TODO 支持转ass文件 + args, err := flags.Parse(&opt) + if err != nil { + os.Exit(0) + } + //显示版本信息 + if opt.Version { + fmt.Printf("LrcToSrt(lts) version %s\n", VERSION) + return + } + //获取保存的文件名 + if len(args) != 0 { + opt.Output = args[0] + } + + //获取歌词,lyric为原文歌词,tranLyric为译文歌词 + var lyric, tranLyric string + if opt.Id != "" { + if opt.Source != "163" { + lyric, tranLyric = l2s.GetQQLyric(opt.Id) + } else { + lyric, tranLyric = l2s.Get163Lyric(opt.Id) + } + //下载歌词 + if opt.Download { + //对文件名进行处理 + o := opt.Output + if o == "" { + o = opt.Id + ".lrc" + } else if !strings.HasSuffix(o, ".lrc") { + o += ".lrc" + } + l2s.WriteFile(o, lyric) + if tranLyric != "" { + l2s.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 = l2s.ReadFile(opt.Input) + if lyric == "" { + fmt.Println("获取歌词失败,文件内容为空。") + os.Exit(1) + } + } else { + fmt.Println("Error: 请指定需要转换的歌词。") + os.Exit(1) + } + lrc, lrcT := l2s.ParseLRC(lyric), l2s.ParseLRC(tranLyric) + srt, srtT := l2s.LrcToSrt(lrc), l2s.LrcToSrt(lrcT) + if srtT != nil { + var mode l2s.SRTMergeMode + switch opt.Mode { + case 1: + mode = l2s.SRT_MERGE_MODE_STACK + case 2: + mode = l2s.SRT_MERGE_MODE_UP + case 3: + mode = l2s.SRT_MERGE_MODE_BOTTOM + } + srt.Merge(srtT, mode) + } + name := opt.Output + 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" + } + if err = srt.WriteFile(name); err != nil { + fmt.Println("出现错误,保存失败") + panic(err.Error()) + } +} diff --git a/lts_test.go b/lts_test.go deleted file mode 100644 index c1762f9..0000000 --- a/lts_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import "testing" - -func BenchmarkSRTContent_String(b *testing.B) { - srt := SRTContent{ - Index: 10, - Start: 100, - End: 200, - Text: "言语 不起作用,想看到 具体行动", - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = srt.String() - } -} - -func BenchmarkLrc2Srt(b *testing.B) { - id := "28891491" - lyric, lyricT := Get163Lyric(id) - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, _ = Lrc2Srt(lyric), Lrc2Srt(lyricT) - } -} diff --git a/qqlyric.go b/qqlyric.go index a764c74..a3acfba 100644 --- a/qqlyric.go +++ b/qqlyric.go @@ -1,4 +1,4 @@ -package main +package lrc2srt import ( "compress/gzip" @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "net/url" - "os" ) /** @@ -43,17 +42,17 @@ func GetQQLyric(id string) (lyric, tLyric string) { req, _ := http.NewRequest("GET", api, nil) //必须设置Referer,否则会请求失败 req.Header.Add("Referer", "https://y.qq.com") - req.Header.Add("User-Agent", ChromeUA) + req.Header.Add("User-Agent", CHROME_UA) req.Header.Add("accept-encoding", "gzip") resp, err := client.Do(req) if err != nil { fmt.Printf("网络错误:%v\n", err) - os.Exit(1) + panic("网络异常,请求失败。") } if resp == nil || resp.StatusCode != http.StatusOK { fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode) - os.Exit(1) + panic("获取失败,未能正确获取到数据") } defer resp.Body.Close() @@ -63,7 +62,7 @@ func GetQQLyric(id string) (lyric, tLyric string) { err = json.NewDecoder(reader).Decode(&qqLyric) if qqLyric.RetCode != 0 { fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", qqLyric) - os.Exit(1) + panic("id错误,获取歌词失败。") } return qqLyric.Lyric, qqLyric.Trans } diff --git a/srt.go b/srt.go new file mode 100644 index 0000000..ba3c61a --- /dev/null +++ b/srt.go @@ -0,0 +1,173 @@ +package lrc2srt + +import ( + "bufio" + "fmt" + "github.com/Hami-Lemon/lrc2srt/glist" + "io" + "os" + "sort" + "strconv" + "strings" +) + +type SRTMergeMode int + +const ( + SRT_MERGE_MODE_STACK SRTMergeMode = iota + SRT_MERGE_MODE_UP + SRT_MERGE_MODE_BOTTOM +) + +type SRTContent struct { + //序号,从1开始 + Index int + //开始时间,单位毫秒 + Start int + //结束时间,单位毫秒 + End int + //歌词内容 + Text string +} + +/** +1 +00:00:01,111 --> 00:00:10,111 +字幕 + +*/ +//返回SRT文件中,一句字幕的字符串表示形式 +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() +} + +type SRT struct { + //歌曲名 + Title string + //歌手名 未指定文件名是,文件名格式为:歌曲名-歌手名.srt + Artist string + Content glist.Queue[*SRTContent] +} + +// LrcToSrt LRC对象转换成SRT对象 +func LrcToSrt(lrc *LRC) *SRT { + if lrc == nil { + return nil + } + srt := &SRT{ + Title: lrc.Title, + Artist: lrc.Artist, + Content: glist.NewLinkedList[*SRTContent](), + } + index := 1 + //上一条srt信息 + var prevSRT *SRTContent + for it := lrc.LrcList.Iterator(); it.Has(); { + lrcNode := it.Next() + srtContent := &SRTContent{ + Index: index, + Start: lrcNode.time, + Text: lrcNode.content, + } + if index != 1 { + //上一条歌词的结束时间设置为当前歌词的开始时间 + prevSRT.End = srtContent.Start + } + srt.Content.PushBack(srtContent) + index++ + prevSRT = srtContent + } + //最后一条歌词 + if prevSRT != nil { + //结束时间是为其 开始时间+10 秒 + prevSRT.End = prevSRT.Start + 1000 + } + return srt +} + +// Merge 将另一个srt信息合并到当前srt中,有三种合并模式 +//1. SRT_MERGE_MODE_STACK: 按照开始时间对两个srt信息进行排序,交错合并 +//2. SRT_MERGE_MODE_UP: 当前srt信息排列在上,另一个排列在下,即 other 追加到后面 +//3. SRT_MERGE_MODE_BOTTOM: 当前srt信息排列在下,另一个排列在上,即 other 添加到前面 +func (s *SRT) Merge(other *SRT, mode SRTMergeMode) { + switch mode { + case SRT_MERGE_MODE_STACK: + s.mergeStack(other) + case SRT_MERGE_MODE_UP: + s.mergeUp(other) + case SRT_MERGE_MODE_BOTTOM: + s.mergeBottom(other) + } +} + +//todo 改进算法,现在的算法太慢了 +func (s *SRT) mergeStack(other *SRT) { + size := s.Content.Size() + other.Content.Size() + temp := make([]*SRTContent, size, size) + index := 0 + for it := s.Content.Iterator(); it.Has(); { + temp[index] = it.Next() + index++ + } + for it := other.Content.Iterator(); it.Has(); { + temp[index] = it.Next() + index++ + } + sort.SliceStable(temp, func(i, j int) bool { + return temp[i].Start < temp[j].Start + }) + list := glist.NewLinkedList[*SRTContent]() + for _, v := range temp { + list.Append(v) + } + s.Content = list +} + +func (s *SRT) mergeUp(other *SRT) { + if other.Content.IsNotEmpty() { + for it := other.Content.Iterator(); it.Has(); { + s.Content.Append(it.Next()) + } + } +} + +func (s *SRT) mergeBottom(other *SRT) { + oq := other.Content + if oq.IsNotEmpty() { + s.Content.PushFront(*(oq.PullBack())) + } +} + +// WriteFile 将SRT格式的数据写入指定的文件中 +func (s *SRT) WriteFile(path string) error { + f, err := os.Create(path) + if err != nil { + return err + } + err = s.Write(f) + err = f.Close() + return err +} + +// Write 将SRT格式的数据写入dst中 +func (s *SRT) Write(dst io.Writer) error { + //6KB的缓冲 + bufSize := 1024 * 6 + writer := bufio.NewWriterSize(dst, bufSize) + for it := s.Content.Iterator(); it.Has(); { + _, err := writer.WriteString(it.Next().String()) + if err != nil { + return err + } + } + return writer.Flush() +} diff --git a/util.go b/util.go index 8d99dd5..8f6a720 100644 --- a/util.go +++ b/util.go @@ -1,13 +1,22 @@ -package main +package lrc2srt import ( "fmt" "io" + "net/http" "os" ) +const ( + CHROME_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36" +) + +var ( + client = http.Client{} +) + // Time2Millisecond 根据分,秒,毫秒 计算出对应的毫秒值 -func Time2Millisecond(m, s, ms int) int { +func time2Millisecond(m, s, ms int) int { t := m*60 + s t *= 1000 t += ms @@ -15,7 +24,7 @@ func Time2Millisecond(m, s, ms int) int { } // Millisecond2Time 根据毫秒值计算出对应的 时,分,秒,毫秒形式的时间值 -func Millisecond2Time(millisecond int) (h, m, s, ms int) { +func millisecond2Time(millisecond int) (h, m, s, ms int) { ms = millisecond % 1000 s = millisecond / 1000 diff --git a/util_test.go b/util_test.go index 4690dda..c052392 100644 --- a/util_test.go +++ b/util_test.go @@ -1,4 +1,4 @@ -package main +package lrc2srt import "testing" @@ -23,7 +23,7 @@ func TestMillisecond2Time(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotH, gotM, gotS, gotMs := Millisecond2Time(tt.args.millisecond) + gotH, gotM, gotS, gotMs := millisecond2Time(tt.args.millisecond) if gotH != tt.wantH { t.Errorf("Millisecond2Time() gotH = %v, want %v", gotH, tt.wantH) } @@ -60,7 +60,7 @@ func TestTime2Millisecond(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := Time2Millisecond(tt.args.m, tt.args.s, tt.args.ms); got != tt.want { + if got := time2Millisecond(tt.args.m, tt.args.s, tt.args.ms); got != tt.want { t.Errorf("Time2Millisecond() = %v, want %v", got, tt.want) } }) @@ -69,12 +69,12 @@ func TestTime2Millisecond(t *testing.T) { func BenchmarkTime2Millisecond(b *testing.B) { for i := 0; i < b.N; i++ { - Time2Millisecond(999, 999, 999) + time2Millisecond(999, 999, 999) } } func BenchmarkMillisecond2Time(b *testing.B) { for i := 0; i < b.N; i++ { - Millisecond2Time(9999999999) + millisecond2Time(9999999999) } }