支持ass转换

This commit is contained in:
Hami Lemon 2022-03-30 10:41:28 +08:00
parent a29d2c057e
commit eba676b8de
6 changed files with 406 additions and 105 deletions

1
.gitignore vendored
View file

@ -89,4 +89,5 @@ fabric.properties
# vendor/
*.srt
*.lrc
*.ass
/temp

125
ass.go Normal file
View file

@ -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
}

4
go.mod
View file

@ -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

4
go.sum
View file

@ -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=

196
lrctocaptions/flag.go Normal file
View file

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

View file

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