diff --git a/.gitignore b/.gitignore index d6d8712..5bb3f1e 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,6 @@ fabric.properties # Dependency directories (remove the comment below to include it) # vendor/ - -*.bat +/example/*.srt +/example/*.lrc /temp diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..f6cf966 --- /dev/null +++ b/build.bat @@ -0,0 +1,3 @@ +echo off +@REM forceposix 表示在windows上参数也为linux风格,即以“-”开头 +go build -tags="forceposix" -ldflags "-s -w" -o lts.exe . diff --git a/example/example.txt b/example/example.txt new file mode 100644 index 0000000..264a271 --- /dev/null +++ b/example/example.txt @@ -0,0 +1,16 @@ +encoding=utf-8 + +example#1 从网易云上下载歌词 +lts -i 1903635166 传说的世界.srt + +example#2 从qq音乐上下载歌词 +lts -i 003eKeNV0t8IVi -s qq "bad guy.srt" + +example#3 从网易云上下载歌词,不解析 +lts -i 1903635166 -d 传说的世界.lrc + +example#4 解析已有的lrc文件 +lts -I 传说的世界.lrc 传说的世界2.srt + +example#5 设置mode为2,原文在上,译文在下 +lts -i 003eKeNV0t8IVi -s qq -m 2 "bad guy2.srt" diff --git a/lts.go b/lts.go index 59de013..0dcb32e 100644 --- a/lts.go +++ b/lts.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "regexp" + "sort" "strconv" "strings" "time" @@ -44,7 +45,7 @@ type Option struct { const ( // VERSION 当前版本 - VERSION = `"0.1.0" (build 2022.02.18)` + VERSION = `"0.1.1" (build 2022.03.05)` ) var ( @@ -68,7 +69,7 @@ func main() { opt.Output = args[0] } - //获取歌词 + //获取歌词,lyric为原文歌词,tranLyric为译文歌词 var lyric, tranLyric string if opt.Id != "" { if opt.Source != "163" { @@ -107,7 +108,7 @@ func main() { fmt.Println("Error: 请指定需要转换的歌词。") os.Exit(1) } - + //原文和译文作为两条歌词流信息分开保存,但最终生成的srt文件会同时包含两个信息 lyricSRT, tranLyricSRT := Lrc2Srt(lyric), Lrc2Srt(tranLyric) SaveSRT(lyricSRT, tranLyricSRT, opt.Output) } @@ -117,6 +118,8 @@ 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 == "" { @@ -138,40 +141,45 @@ func SaveSRT(srt *SRT, tranSrt *SRT, name string) { name += ".srt" } - file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, os.ModePerm) + file, err := os.Create(name) if err != nil { - fmt.Printf("保存结果失败:%v\n", err) + fmt.Printf("创建结果文件失败:%v\n", err) os.Exit(1) } - defer file.Close() - writer := bufio.NewWriter(file) + 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: - //译文和原文交错排列 - _len, _lent, size := len(srt.Content), len(tranSrt.Content), 0 - if _len > _lent { - size = 2 * _len - } else { - size = 2 * _lent + //将两个歌词合并成一个新的数组 + size := len(srt.Content) + len(tranSrt.Content) + temp := make([]*SRTContent, size, size) + i := 0 + for _, v := range srt.Content { + temp[i] = v + i++ } - //临时缓冲区,偶数索引存原文,奇数存译文 - buf := make([]*SRTContent, size, size) - for i, item := range srt.Content { - buf[2*i] = item + for _, v := range tranSrt.Content { + temp[i] = v + i++ } - for i, item := range tranSrt.Content { - buf[2*i+1] = item - } - + //按开始时间进行排序,使用SliceStable确保一句歌词的原文在译文之前 + sort.SliceStable(temp, func(i, j int) bool { + return temp[i].Start < temp[j].Start + }) //写入文件 - for _, item := range buf { - if item != nil { - item.Index = index - _, _ = writer.WriteString(item.String()) - index++ - } + for i, v := range temp { + v.Index = i + 1 + _, _ = writer.WriteString(v.String()) } case 2: //原文在上,译文在下 @@ -202,9 +210,9 @@ func SaveSRT(srt *SRT, tranSrt *SRT, name string) { err = writer.Flush() if err != nil { fmt.Printf("保存结果失败:%v\n", err) - os.Exit(1) + } else { + fmt.Printf("转换文件完成,保存结果为:%s\n", name) } - fmt.Printf("转换文件完成,保存结果为:%s\n", name) } // Lrc2Srt 将原始个LRC字符串歌词解析SRT对象 diff --git a/lts_test.go b/lts_test.go new file mode 100644 index 0000000..c1762f9 --- /dev/null +++ b/lts_test.go @@ -0,0 +1,25 @@ +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 89cef12..a764c74 100644 --- a/qqlyric.go +++ b/qqlyric.go @@ -9,12 +9,19 @@ import ( "os" ) +/** +从QQ音乐上获取歌词 +*/ + +// QQLyric qq音乐获取歌词接口返回的数据结构 type QQLyric struct { - RetCode int `json:"retcode"` - Code int `json:"code"` - SubCode int `json:"subcode"` - Lyric string `json:"lyric"` - Trans string `json:"trans"` + RetCode int `json:"retcode"` + Code int `json:"code"` + SubCode int `json:"subcode"` + //Lyric 原文歌词 + Lyric string `json:"lyric"` + //Trans 译文歌词 + Trans string `json:"trans"` } func GetQQLyric(id string) (lyric, tLyric string) { diff --git a/util.go b/util.go index 89a67da..8d99dd5 100644 --- a/util.go +++ b/util.go @@ -36,7 +36,9 @@ func ReadFile(name string) string { fmt.Printf("打开文件失败:%v\n", err) os.Exit(1) } - defer file.Close() + defer func(file *os.File) { + _ = file.Close() + }(file) data, err := io.ReadAll(file) if err != nil { fmt.Printf("读取文件失败:%v\n", err) @@ -46,12 +48,14 @@ func ReadFile(name string) string { } func WriteFile(name string, data string) { - file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, os.ModePerm) + file, err := os.Create(name) if err != nil { - fmt.Printf("保存文件失败:%v\n", err) + fmt.Printf("创建结果文件失败:%v\n", err) os.Exit(1) } - defer file.Close() + defer func(file *os.File) { + _ = file.Close() + }(file) nw, err := file.WriteString(data) if err != nil || nw < len(data) { fmt.Printf("保存文件失败:%v\n", err) diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..4690dda --- /dev/null +++ b/util_test.go @@ -0,0 +1,80 @@ +package main + +import "testing" + +func TestMillisecond2Time(t *testing.T) { + type args struct { + millisecond int + } + tests := []struct { + name string + args args + wantH int + wantM int + wantS int + wantMs int + }{ + {"ms_0", args{0}, 0, 0, 0, 0}, + {"ms_100", args{100}, 0, 0, 0, 100}, + {"ms_1000", args{1000}, 0, 0, 1, 0}, + {"ms_1100", args{1100}, 0, 0, 1, 100}, + {"ms_60000", args{60000}, 0, 1, 0, 0}, + {"ms_3600000", args{3600000}, 1, 0, 0, 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotH, gotM, gotS, gotMs := Millisecond2Time(tt.args.millisecond) + if gotH != tt.wantH { + t.Errorf("Millisecond2Time() gotH = %v, want %v", gotH, tt.wantH) + } + if gotM != tt.wantM { + t.Errorf("Millisecond2Time() gotM = %v, want %v", gotM, tt.wantM) + } + if gotS != tt.wantS { + t.Errorf("Millisecond2Time() gotS = %v, want %v", gotS, tt.wantS) + } + if gotMs != tt.wantMs { + t.Errorf("Millisecond2Time() gotMs = %v, want %v", gotMs, tt.wantMs) + } + }) + } +} + +func TestTime2Millisecond(t *testing.T) { + type args struct { + m int + s int + ms int + } + tests := []struct { + name string + args args + want int + }{ + {"0:0.0", args{0, 0, 0}, 0}, + {"0:0.1", args{0, 0, 1}, 1}, + {"0:0.999", args{0, 0, 999}, 999}, + {"0:1.0", args{0, 1, 0}, 1000}, + {"0:1.999", args{0, 1, 999}, 1999}, + {"1:0.0", args{1, 0, 0}, 60000}, + } + 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 { + t.Errorf("Time2Millisecond() = %v, want %v", got, tt.want) + } + }) + } +} + +func BenchmarkTime2Millisecond(b *testing.B) { + for i := 0; i < b.N; i++ { + Time2Millisecond(999, 999, 999) + } +} + +func BenchmarkMillisecond2Time(b *testing.B) { + for i := 0; i < b.N; i++ { + Millisecond2Time(9999999999) + } +}