initial commit
This commit is contained in:
commit
43107bf5c1
12 changed files with 667 additions and 0 deletions
329
lts.go
Normal file
329
lts.go
Normal file
|
@ -0,0 +1,329 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"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.0" (build 2022.02.18)`
|
||||
)
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
//获取歌词
|
||||
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)
|
||||
}
|
||||
|
||||
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)}
|
||||
}
|
||||
//处理结果文件的文件名
|
||||
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.OpenFile(name, os.O_WRONLY|os.O_CREATE, os.ModePerm)
|
||||
if err != nil {
|
||||
fmt.Printf("保存结果失败:%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
writer := bufio.NewWriter(file)
|
||||
|
||||
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
|
||||
}
|
||||
//临时缓冲区,偶数索引存原文,奇数存译文
|
||||
buf := make([]*SRTContent, size, size)
|
||||
for i, item := range srt.Content {
|
||||
buf[2*i] = item
|
||||
}
|
||||
for i, item := range tranSrt.Content {
|
||||
buf[2*i+1] = item
|
||||
}
|
||||
|
||||
//写入文件
|
||||
for _, item := range buf {
|
||||
if item != nil {
|
||||
item.Index = index
|
||||
_, _ = writer.WriteString(item.String())
|
||||
index++
|
||||
}
|
||||
}
|
||||
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)
|
||||
os.Exit(1)
|
||||
}
|
||||
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()
|
||||
}
|
Reference in a new issue