支持ass转换
This commit is contained in:
parent
a29d2c057e
commit
eba676b8de
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -89,4 +89,5 @@ fabric.properties
|
|||
# vendor/
|
||||
*.srt
|
||||
*.lrc
|
||||
*.ass
|
||||
/temp
|
||||
|
|
125
ass.go
Normal file
125
ass.go
Normal 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
4
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
|
||||
|
|
4
go.sum
4
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=
|
196
lrctocaptions/flag.go
Normal file
196
lrctocaptions/flag.go
Normal 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)
|
||||
}
|
|
@ -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("")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue