diff --git a/.gitignore b/.gitignore index 96d2cfd..53ceaab 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,5 @@ fabric.properties # vendor/ *.srt *.lrc +*.ass /temp diff --git a/ass.go b/ass.go new file mode 100644 index 0000000..c130679 --- /dev/null +++ b/ass.go @@ -0,0 +1,125 @@ +package ltc + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/Hami-Lemon/ltc/glist" +) + +type ASSNode struct { + Start int //开始时间 + End int //结束时间 + Dialogue string //内容 +} + +func (a *ASSNode) String() string { + builder := strings.Builder{} + sh, sm, ss, sms := millisecond2Time(a.Start) + eh, em, es, ems := millisecond2Time(a.End) + builder.WriteString("Dialogue: 0,") + sms /= 10 + ems /= 10 + builder.WriteString(fmt.Sprintf("%d:%02d:%02d.%02d,%d:%02d:%02d.%02d,Default,,", + sh, sm, ss, sms, eh, em, es, ems)) + builder.WriteString("0000,0000,0000,,") + builder.WriteString(a.Dialogue) + return builder.String() +} + +type ASS struct { + Content glist.Queue[*ASSNode] +} + +func LrcToAss(lrc *LRC) *ASS { + return SrtToAss(LrcToSrt(lrc)) +} + +func SrtToAss(srt *SRT) *ASS { + if srt == nil { + return nil + } + ass := &ASS{ + Content: glist.NewLinkedList[*ASSNode](), + } + for it := srt.Content.Iterator(); it.Has(); { + s := it.Next() + node := &ASSNode{ + Start: s.Start, + End: s.End, + Dialogue: s.Text, + } + ass.Content.PushBack(node) + } + return ass +} + +func (a *ASS) WriteFile(path string) error { + f, err := os.Create(path) + if err != nil { + //不存在对应文件夹 + if os.IsNotExist(err) { + panic("文件夹不存在:" + filepath.Dir(path)) + } + return err + } + err = a.Write(f) + err = f.Close() + return err +} + +func (a *ASS) Write(dst io.Writer) error { + if err := writeScriptInfo(dst); err != nil { + return err + } + if err := writeStyles(dst); err != nil { + return err + } + if err := writeEventHeader(dst); err != nil { + return err + } + for it := a.Content.Iterator(); it.Has(); { + temp := it.Next() + r := temp.String() + _, err := fmt.Fprintf(dst, "%s\n", r) + if err != nil { + return err + } + } + return nil +} + +func writeScriptInfo(dst io.Writer) error { + text := `[Script Info] +Title: LRC ASS file +ScriptType: v4.00+ +PlayResX: 1920 +PlayResY: 1080 +Collisions: Reverse +WrapStyle: 2 + +` + _, err := dst.Write([]byte(text)) + return err +} + +func writeStyles(dst io.Writer) error { + text := `[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,黑体,36,&H00FFFFFF,&H00FFFFFF,&H00000000,&00FFFFFF,-1,0,0,0,100,100,0,0,1,0,1,2,0,0,0,1 + +` + _, err := dst.Write([]byte(text)) + return err +} + +func writeEventHeader(dst io.Writer) error { + text := `[Events] +Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text +` + _, err := dst.Write([]byte(text)) + return err +} diff --git a/go.mod b/go.mod index d11e0e1..d2e7b41 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,3 @@ module github.com/Hami-Lemon/ltc go 1.18 - -require github.com/jessevdk/go-flags v1.5.0 - -require golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect diff --git a/go.sum b/go.sum index df31363..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +0,0 @@ -github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/lrctocaptions/flag.go b/lrctocaptions/flag.go new file mode 100644 index 0000000..7e93535 --- /dev/null +++ b/lrctocaptions/flag.go @@ -0,0 +1,196 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "github.com/Hami-Lemon/ltc" + "os" + "strconv" + "strings" +) + +var ( + input string //输入,可以是歌词对应的歌曲id,也可以是文件名 + source string //歌词来源,默认163,可选163(网易云音乐),QQ或qq(QQ音乐),后续支持:kg(酷狗音乐) + download boolFlag //是否只下载歌词,当输入是歌曲id且设置该选项时,只下载歌词而不进行处理 + mode modeFlag //如果存在译文时的合并模式 + version boolFlag //当前程序版本信息,设置该选项时只输出版本信息 + format formatFlag //字幕格式,可选: ass,srt,默认为ass + output string //保存的文件名 +) + +//检查路径path是否有效 +func checkPath(path string) bool { + if _, err := os.Stat(path); err == nil { + return true + } else { + return false + } +} + +func parseFlag() { + flag.StringVar(&input, "i", "", "歌词来源,可以是歌词对应的歌曲id,也可以是歌词文件") + flag.StringVar(&source, "s", "163", "选择从网易云还是QQ音乐上获取歌词,可选值:163(默认),qq。") + flag.Var(&download, "d", "设置该选项时,只下载歌词,而无需转换。") + flag.Var(&mode, "m", "设置歌词原文和译文的合并模式,可选值:1(默认),2,3。") + flag.Var(&version, "v", "获取当前程序版本信息。") + flag.Var(&format, "f", "转换成的字幕文件格式,可选值:ass(默认),srt") + flag.Usage = func() { + fmt.Printf("LrcToCaptions(ltc) 将LRC歌词文件转换成字幕文件。\n") + fmt.Printf("ltc version: %s\n\n", VERSION) + fmt.Printf("用法:ltc [options] OutputFile\n\n") + fmt.Printf("options:\n\n") + flag.PrintDefaults() + fmt.Println("") + } + flag.Parse() + if other := flag.Args(); len(other) != 0 { + output = other[0] + } + outputProcess() +} + +func outputProcess() { + //处理结果文件名 + if output == "" { + //和输入源同名 + dot := strings.LastIndex(input, ".") + if dot == -1 { + output = input + } else { + output = input[:dot] + } + } + //后缀名处理 + suffix := func(o, s string) string { + if !strings.HasSuffix(o, s) { + return o + s + } + return o + } + if download.IsSet() { + output = suffix(output, ".lrc") + } else { + switch format.Value() { + case FORMAT_SRT: + output = suffix(output, ".srt") + case FORMAT_ASS: + output = suffix(output, ".ass") + } + } +} + +// boolFlag bool值类型的参数 +//实现flags包中的boolFlag接口,设置bool值时不要传具体的值 +//即: -flag 等价与 -flag=true +type boolFlag bool + +func (b *boolFlag) String() string { + if b == nil { + return "false" + } + return strconv.FormatBool(bool(*b)) +} + +func (b *boolFlag) Set(value string) error { + if f, err := strconv.ParseBool(value); err != nil { + return err + } else { + *b = boolFlag(f) + return nil + } +} + +func (b *boolFlag) IsBoolFlag() bool { + return true +} + +func (b *boolFlag) IsSet() bool { + return bool(*b) +} + +//歌词合并模式的选项 +type modeFlag ltc.SRTMergeMode + +func (m *modeFlag) String() string { + if m == nil { + return "STACK_MODE" + } + switch ltc.SRTMergeMode(*m) { + case ltc.SRT_MERGE_MODE_STACK: + return "STACK_MODE" + case ltc.SRT_MERGE_MODE_UP: + return "UP_MODE" + case ltc.SRT_MERGE_MODE_BOTTOM: + return "BOTTOM_MODE" + default: + return "STACK_MODE" + } +} + +func (m *modeFlag) Set(value string) error { + if value == "" { + *m = modeFlag(ltc.SRT_MERGE_MODE_STACK) + } + v := strings.ToLower(value) + switch v { + case "1", "stack": + *m = modeFlag(ltc.SRT_MERGE_MODE_STACK) + case "2", "up": + *m = modeFlag(ltc.SRT_MERGE_MODE_UP) + case "3", "bottom": + *m = modeFlag(ltc.SRT_MERGE_MODE_BOTTOM) + default: + return errors.New("invalid mode value:" + v + " only support 1, 2, 3") + } + return nil +} + +func (m *modeFlag) Mode() ltc.SRTMergeMode { + return ltc.SRTMergeMode(*m) +} + +// Format 字幕文件的格式 +type Format int + +const ( + FORMAT_ASS Format = iota + FORMAT_SRT +) + +type formatFlag Format + +func (f *formatFlag) String() string { + if f == nil { + return "" + } + ft := Format(*f) + switch ft { + case FORMAT_SRT: + return "srt" + case FORMAT_ASS: + return "ass" + } + return "" +} + +func (f *formatFlag) Set(value string) error { + if value == "" { + *f = formatFlag(FORMAT_ASS) + } + v := strings.ToLower(value) + switch v { + case "srt": + *f = formatFlag(FORMAT_SRT) + case "ass": + *f = formatFlag(FORMAT_ASS) + default: + return errors.New("invalid format value:" + value) + } + return nil +} + +func (f *formatFlag) Value() Format { + return Format(*f) +} diff --git a/lrctocaptions/ltc.go b/lrctocaptions/ltc.go index 0136ba9..ee61fff 100644 --- a/lrctocaptions/ltc.go +++ b/lrctocaptions/ltc.go @@ -1,135 +1,122 @@ package main import ( + "bufio" + "flag" "fmt" + "github.com/Hami-Lemon/ltc" + "io/ioutil" "os" "path/filepath" "strings" - "time" - - "github.com/Hami-Lemon/ltc" - "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.4" (build 2022.03.29)` -) - -var ( - opt Option + VERSION = `"0.3.4" (build 2022.03.30)` + VERSION_INFO = "LrcToCaptions(ltc) version: %s\n" ) func main() { - //TODO 支持转ass文件 - //TODO 酷狗的krc支持逐字,更利于打轴 https://shansing.com/read/392/ - args, err := flags.Parse(&opt) - if err != nil { - os.Exit(0) - } + parseFlag() + //TODO 酷狗的krc精准到字,更利于打轴 https://shansing.com/read/392/ //显示版本信息 - if opt.Version { - fmt.Printf("LrcToCaptions(ltc) version: %s\n", VERSION) + if version.IsSet() { + fmt.Printf(VERSION_INFO, VERSION) return } - //获取保存的文件名 - if len(args) != 0 { - opt.Output = args[0] + //未指定来源 + if input == "" { + fmt.Printf("未指定歌词来源\n") + flag.Usage() + os.Exit(0) } - //获取歌词,lyric为原文歌词,tranLyric为译文歌词 var lyric, tranLyric string - if opt.Id != "" { - if opt.Source != "163" { - lyric, tranLyric = ltc.GetQQLyric(opt.Id) - } else { - lyric, tranLyric = ltc.Get163Lyric(opt.Id) - } - //下载歌词 - if opt.Download { - //对文件名进行处理 - o := opt.Output - if o == "" { - o = opt.Id + ".lrc" - } else if !strings.HasSuffix(o, ".lrc") { - o += ".lrc" - } - ltc.WriteFile(o, lyric) - if tranLyric != "" { - ltc.WriteFile("tran_"+o, tranLyric) - } - fmt.Println("下载歌词完成!") - return - } - } else if opt.Input != "" { - //从文件中获取歌词 - if !strings.HasSuffix(opt.Input, ".lrc") { + //从文件中获取 + if checkPath(input) { + if !strings.HasSuffix(input, ".lrc") { fmt.Println("Error: 不支持的格式,目前只支持lrc歌词文件。") - os.Exit(1) + panic("") } - lyric = ltc.ReadFile(opt.Input) - if lyric == "" { - fmt.Println("获取歌词失败,文件内容为空。") - os.Exit(1) + if data, err := ioutil.ReadFile(input); err == nil { + if len(data) == 0 { + fmt.Println("获取歌词失败,文件内容为空。") + panic("") + } + lyric = string(data) + } else { + panic("读取文件失败:" + input + err.Error()) } } else { - fmt.Println("Error: 请指定需要转换的歌词。") - os.Exit(1) + //从网络上获取 + if source != "163" { + lyric, tranLyric = ltc.GetQQLyric(input) + } else { + lyric, tranLyric = ltc.Get163Lyric(input) + } } + + //下载歌词 + if download.IsSet() { + //对文件名进行处理 + o := output + if o == "" { + o = input + ".lrc" + } else if !strings.HasSuffix(o, ".lrc") { + o += ".lrc" + } + writeFile(o, lyric) + if tranLyric != "" { + writeFile("tran_"+o, tranLyric) + } + fmt.Println("下载歌词完成!") + return + } + lrc, lrcT := ltc.ParseLRC(lyric), ltc.ParseLRC(tranLyric) + //先转换成srt srt, srtT := ltc.LrcToSrt(lrc), ltc.LrcToSrt(lrcT) if srtT != nil { - var mode ltc.SRTMergeMode - switch opt.Mode { - case 1: - mode = ltc.SRT_MERGE_MODE_STACK - case 2: - mode = ltc.SRT_MERGE_MODE_UP - case 3: - mode = ltc.SRT_MERGE_MODE_BOTTOM - } - srt.Merge(srtT, mode) + //原文和译文合并 + srt.Merge(srtT, mode.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()) + switch format.Value() { + case FORMAT_SRT: + if err := srt.WriteFile(output); err != nil { + fmt.Println("出现错误,保存失败") + panic(err.Error()) + } + case FORMAT_ASS: + ass := ltc.SrtToAss(srt) + if err := ass.WriteFile(output); err != nil { + fmt.Println("出现错误,保存失败") + panic(err.Error()) } - } else if !strings.HasSuffix(name, ".srt") { - name += ".srt" - } - if err = srt.WriteFile(name); err != nil { - fmt.Println("出现错误,保存失败") - panic(err.Error()) } //如果是相对路径,则获取其对应的绝对路径 - if !filepath.IsAbs(name) { + if !filepath.IsAbs(output) { //如果是相对路径,父目录即是当前运行路径 dir, er := os.Getwd() if er == nil { - name = dir + string(os.PathSeparator) + name + output = dir + string(os.PathSeparator) + output } } - fmt.Printf("保存结果为:%s\n", name) + fmt.Printf("保存结果为:%s\n", output) +} + +func writeFile(file string, content string) { + f, err := os.Create(file) + if err != nil { + fmt.Printf("创建结果文件失败:%v\n", err) + panic("") + } + defer f.Close() + writer := bufio.NewWriter(f) + _, err = writer.WriteString(content) + err = writer.Flush() + if err != nil { + fmt.Printf("保存文件失败:%v\n", err) + panic("") + } }