重构项目,调整了项目结构
This commit is contained in:
parent
5f49bde3ae
commit
c9b1b38c03
11 changed files with 456 additions and 386 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -87,6 +87,6 @@ fabric.properties
|
|||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
/example/*.srt
|
||||
/example/*.lrc
|
||||
*.srt
|
||||
*.lrc
|
||||
/temp
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
package main
|
||||
package lrc2srt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
type CloudLyricBase struct {
|
||||
|
@ -38,15 +37,15 @@ func Get163Lyric(id string) (lyric, tLyric string) {
|
|||
req, _ := http.NewRequest("GET", api, nil)
|
||||
//必须设置Referer,否则会请求失败
|
||||
req.Header.Add("Referer", "https://music.163.com")
|
||||
req.Header.Add("User-Agent", ChromeUA)
|
||||
req.Header.Add("User-Agent", CHROME_UA)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("网络错误:%v\n", err)
|
||||
os.Exit(1)
|
||||
panic("网络异常,获取失败。")
|
||||
}
|
||||
if resp == nil || resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode)
|
||||
os.Exit(1)
|
||||
panic("获取失败,未能正确获取到数据")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
@ -54,7 +53,7 @@ func Get163Lyric(id string) (lyric, tLyric string) {
|
|||
err = json.NewDecoder(resp.Body).Decode(&cloudLyric)
|
||||
if cloudLyric.Sgc {
|
||||
fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", cloudLyric)
|
||||
os.Exit(1)
|
||||
panic("id错误,获取歌词失败")
|
||||
}
|
||||
return cloudLyric.Lrc.Lyric, cloudLyric.TLyric.Lyric
|
||||
}
|
||||
|
|
4
go.mod
4
go.mod
|
@ -1,6 +1,6 @@
|
|||
module github.com/hami_lemon/lrc2srt
|
||||
module github.com/Hami-Lemon/lrc2srt
|
||||
|
||||
go 1.17
|
||||
go 1.18
|
||||
|
||||
require github.com/jessevdk/go-flags v1.5.0
|
||||
|
||||
|
|
128
lrc.go
Normal file
128
lrc.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
package lrc2srt
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Hami-Lemon/lrc2srt/glist"
|
||||
)
|
||||
|
||||
type LRCNode struct {
|
||||
//歌词出现的时间,单位毫秒
|
||||
time int
|
||||
//歌词内容
|
||||
content string
|
||||
}
|
||||
|
||||
type LRC struct {
|
||||
//歌曲名
|
||||
Title string
|
||||
//歌手名
|
||||
Artist string
|
||||
//专辑名
|
||||
Album string
|
||||
//歌词作者
|
||||
Author string
|
||||
//歌词列表
|
||||
LrcList glist.Queue[*LRCNode]
|
||||
}
|
||||
|
||||
func ParseLRC(src string) *LRC {
|
||||
if src == "" {
|
||||
return nil
|
||||
}
|
||||
//标准的LRC文件为一行一句歌词
|
||||
lyrics := strings.Split(src, "\n")
|
||||
//标识标签的正则 [ar:A-SOUL]形式
|
||||
infoRegx := regexp.MustCompile(`^\[([a-z]+):([\s\S]*)]`)
|
||||
|
||||
lrc := &LRC{LrcList: glist.NewLinkedList[*LRCNode]()}
|
||||
//解析标识信息
|
||||
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 {
|
||||
lrc.Artist = info[2]
|
||||
}
|
||||
case "ti":
|
||||
//歌曲名
|
||||
if len(info) == 3 {
|
||||
lrc.Title = info[2]
|
||||
}
|
||||
case "al":
|
||||
//专辑名
|
||||
if len(info) == 3 {
|
||||
lrc.Album = info[2]
|
||||
}
|
||||
case "by":
|
||||
//歌词作者
|
||||
if len(info) == 3 {
|
||||
lrc.Author = 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]+)`)
|
||||
for _, l := range lyrics {
|
||||
content := lyricRegx.FindStringSubmatch(l)
|
||||
if content != nil {
|
||||
node := SplitLyric(content[1:])
|
||||
if node != nil {
|
||||
lrc.LrcList.PushBack(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
return lrc
|
||||
}
|
||||
|
||||
// SplitLyric 对分割出来的歌词信息进行解析
|
||||
func SplitLyric(src []string) *LRCNode {
|
||||
minute, err := strconv.Atoi(src[0])
|
||||
second, err := strconv.Atoi(src[1])
|
||||
if err != nil {
|
||||
panic("错误的时间格式:" + strings.Join(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 {
|
||||
panic("错误的时间格式:" + strings.Join(src, " "))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
lrcNode := &LRCNode{
|
||||
time: time2Millisecond(minute, second, millisecond),
|
||||
content: content,
|
||||
}
|
||||
return lrcNode
|
||||
}
|
337
lts.go
337
lts.go
|
@ -1,337 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"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.1" (build 2022.03.05)`
|
||||
)
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
//获取歌词,lyric为原文歌词,tranLyric为译文歌词
|
||||
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)
|
||||
}
|
||||
//原文和译文作为两条歌词流信息分开保存,但最终生成的srt文件会同时包含两个信息
|
||||
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)}
|
||||
//因为没有译文,所以mode选项无效,设为2之后,后面不用做多余判断
|
||||
opt.Mode = 2
|
||||
}
|
||||
//处理结果文件的文件名
|
||||
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.Create(name)
|
||||
if err != nil {
|
||||
fmt.Printf("创建结果文件失败:%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
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:
|
||||
//将两个歌词合并成一个新的数组
|
||||
size := len(srt.Content) + len(tranSrt.Content)
|
||||
temp := make([]*SRTContent, size, size)
|
||||
i := 0
|
||||
for _, v := range srt.Content {
|
||||
temp[i] = v
|
||||
i++
|
||||
}
|
||||
for _, v := range tranSrt.Content {
|
||||
temp[i] = v
|
||||
i++
|
||||
}
|
||||
//按开始时间进行排序,使用SliceStable确保一句歌词的原文在译文之前
|
||||
sort.SliceStable(temp, func(i, j int) bool {
|
||||
return temp[i].Start < temp[j].Start
|
||||
})
|
||||
//写入文件
|
||||
for i, v := range temp {
|
||||
v.Index = i + 1
|
||||
_, _ = writer.WriteString(v.String())
|
||||
}
|
||||
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)
|
||||
} else {
|
||||
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()
|
||||
}
|
124
lts/main.go
Normal file
124
lts/main.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
l2s "github.com/Hami-Lemon/lrc2srt"
|
||||
"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.0" (build 2022.03.28)`
|
||||
)
|
||||
|
||||
var (
|
||||
opt Option
|
||||
)
|
||||
|
||||
func main() {
|
||||
//TODO 支持转ass文件
|
||||
args, err := flags.Parse(&opt)
|
||||
if err != nil {
|
||||
os.Exit(0)
|
||||
}
|
||||
//显示版本信息
|
||||
if opt.Version {
|
||||
fmt.Printf("LrcToSrt(lts) version %s\n", VERSION)
|
||||
return
|
||||
}
|
||||
//获取保存的文件名
|
||||
if len(args) != 0 {
|
||||
opt.Output = args[0]
|
||||
}
|
||||
|
||||
//获取歌词,lyric为原文歌词,tranLyric为译文歌词
|
||||
var lyric, tranLyric string
|
||||
if opt.Id != "" {
|
||||
if opt.Source != "163" {
|
||||
lyric, tranLyric = l2s.GetQQLyric(opt.Id)
|
||||
} else {
|
||||
lyric, tranLyric = l2s.Get163Lyric(opt.Id)
|
||||
}
|
||||
//下载歌词
|
||||
if opt.Download {
|
||||
//对文件名进行处理
|
||||
o := opt.Output
|
||||
if o == "" {
|
||||
o = opt.Id + ".lrc"
|
||||
} else if !strings.HasSuffix(o, ".lrc") {
|
||||
o += ".lrc"
|
||||
}
|
||||
l2s.WriteFile(o, lyric)
|
||||
if tranLyric != "" {
|
||||
l2s.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 = l2s.ReadFile(opt.Input)
|
||||
if lyric == "" {
|
||||
fmt.Println("获取歌词失败,文件内容为空。")
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Error: 请指定需要转换的歌词。")
|
||||
os.Exit(1)
|
||||
}
|
||||
lrc, lrcT := l2s.ParseLRC(lyric), l2s.ParseLRC(tranLyric)
|
||||
srt, srtT := l2s.LrcToSrt(lrc), l2s.LrcToSrt(lrcT)
|
||||
if srtT != nil {
|
||||
var mode l2s.SRTMergeMode
|
||||
switch opt.Mode {
|
||||
case 1:
|
||||
mode = l2s.SRT_MERGE_MODE_STACK
|
||||
case 2:
|
||||
mode = l2s.SRT_MERGE_MODE_UP
|
||||
case 3:
|
||||
mode = l2s.SRT_MERGE_MODE_BOTTOM
|
||||
}
|
||||
srt.Merge(srtT, 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())
|
||||
}
|
||||
} else if !strings.HasSuffix(name, ".srt") {
|
||||
name += ".srt"
|
||||
}
|
||||
if err = srt.WriteFile(name); err != nil {
|
||||
fmt.Println("出现错误,保存失败")
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
25
lts_test.go
25
lts_test.go
|
@ -1,25 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
11
qqlyric.go
11
qqlyric.go
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package lrc2srt
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -43,17 +42,17 @@ func GetQQLyric(id string) (lyric, tLyric string) {
|
|||
req, _ := http.NewRequest("GET", api, nil)
|
||||
//必须设置Referer,否则会请求失败
|
||||
req.Header.Add("Referer", "https://y.qq.com")
|
||||
req.Header.Add("User-Agent", ChromeUA)
|
||||
req.Header.Add("User-Agent", CHROME_UA)
|
||||
req.Header.Add("accept-encoding", "gzip")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("网络错误:%v\n", err)
|
||||
os.Exit(1)
|
||||
panic("网络异常,请求失败。")
|
||||
}
|
||||
|
||||
if resp == nil || resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode)
|
||||
os.Exit(1)
|
||||
panic("获取失败,未能正确获取到数据")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
@ -63,7 +62,7 @@ func GetQQLyric(id string) (lyric, tLyric string) {
|
|||
err = json.NewDecoder(reader).Decode(&qqLyric)
|
||||
if qqLyric.RetCode != 0 {
|
||||
fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", qqLyric)
|
||||
os.Exit(1)
|
||||
panic("id错误,获取歌词失败。")
|
||||
}
|
||||
return qqLyric.Lyric, qqLyric.Trans
|
||||
}
|
||||
|
|
173
srt.go
Normal file
173
srt.go
Normal file
|
@ -0,0 +1,173 @@
|
|||
package lrc2srt
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/Hami-Lemon/lrc2srt/glist"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SRTMergeMode int
|
||||
|
||||
const (
|
||||
SRT_MERGE_MODE_STACK SRTMergeMode = iota
|
||||
SRT_MERGE_MODE_UP
|
||||
SRT_MERGE_MODE_BOTTOM
|
||||
)
|
||||
|
||||
type SRTContent struct {
|
||||
//序号,从1开始
|
||||
Index int
|
||||
//开始时间,单位毫秒
|
||||
Start int
|
||||
//结束时间,单位毫秒
|
||||
End int
|
||||
//歌词内容
|
||||
Text string
|
||||
}
|
||||
|
||||
/**
|
||||
1
|
||||
00:00:01,111 --> 00:00:10,111
|
||||
字幕
|
||||
|
||||
*/
|
||||
//返回SRT文件中,一句字幕的字符串表示形式
|
||||
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()
|
||||
}
|
||||
|
||||
type SRT struct {
|
||||
//歌曲名
|
||||
Title string
|
||||
//歌手名 未指定文件名是,文件名格式为:歌曲名-歌手名.srt
|
||||
Artist string
|
||||
Content glist.Queue[*SRTContent]
|
||||
}
|
||||
|
||||
// LrcToSrt LRC对象转换成SRT对象
|
||||
func LrcToSrt(lrc *LRC) *SRT {
|
||||
if lrc == nil {
|
||||
return nil
|
||||
}
|
||||
srt := &SRT{
|
||||
Title: lrc.Title,
|
||||
Artist: lrc.Artist,
|
||||
Content: glist.NewLinkedList[*SRTContent](),
|
||||
}
|
||||
index := 1
|
||||
//上一条srt信息
|
||||
var prevSRT *SRTContent
|
||||
for it := lrc.LrcList.Iterator(); it.Has(); {
|
||||
lrcNode := it.Next()
|
||||
srtContent := &SRTContent{
|
||||
Index: index,
|
||||
Start: lrcNode.time,
|
||||
Text: lrcNode.content,
|
||||
}
|
||||
if index != 1 {
|
||||
//上一条歌词的结束时间设置为当前歌词的开始时间
|
||||
prevSRT.End = srtContent.Start
|
||||
}
|
||||
srt.Content.PushBack(srtContent)
|
||||
index++
|
||||
prevSRT = srtContent
|
||||
}
|
||||
//最后一条歌词
|
||||
if prevSRT != nil {
|
||||
//结束时间是为其 开始时间+10 秒
|
||||
prevSRT.End = prevSRT.Start + 1000
|
||||
}
|
||||
return srt
|
||||
}
|
||||
|
||||
// Merge 将另一个srt信息合并到当前srt中,有三种合并模式
|
||||
//1. SRT_MERGE_MODE_STACK: 按照开始时间对两个srt信息进行排序,交错合并
|
||||
//2. SRT_MERGE_MODE_UP: 当前srt信息排列在上,另一个排列在下,即 other 追加到后面
|
||||
//3. SRT_MERGE_MODE_BOTTOM: 当前srt信息排列在下,另一个排列在上,即 other 添加到前面
|
||||
func (s *SRT) Merge(other *SRT, mode SRTMergeMode) {
|
||||
switch mode {
|
||||
case SRT_MERGE_MODE_STACK:
|
||||
s.mergeStack(other)
|
||||
case SRT_MERGE_MODE_UP:
|
||||
s.mergeUp(other)
|
||||
case SRT_MERGE_MODE_BOTTOM:
|
||||
s.mergeBottom(other)
|
||||
}
|
||||
}
|
||||
|
||||
//todo 改进算法,现在的算法太慢了
|
||||
func (s *SRT) mergeStack(other *SRT) {
|
||||
size := s.Content.Size() + other.Content.Size()
|
||||
temp := make([]*SRTContent, size, size)
|
||||
index := 0
|
||||
for it := s.Content.Iterator(); it.Has(); {
|
||||
temp[index] = it.Next()
|
||||
index++
|
||||
}
|
||||
for it := other.Content.Iterator(); it.Has(); {
|
||||
temp[index] = it.Next()
|
||||
index++
|
||||
}
|
||||
sort.SliceStable(temp, func(i, j int) bool {
|
||||
return temp[i].Start < temp[j].Start
|
||||
})
|
||||
list := glist.NewLinkedList[*SRTContent]()
|
||||
for _, v := range temp {
|
||||
list.Append(v)
|
||||
}
|
||||
s.Content = list
|
||||
}
|
||||
|
||||
func (s *SRT) mergeUp(other *SRT) {
|
||||
if other.Content.IsNotEmpty() {
|
||||
for it := other.Content.Iterator(); it.Has(); {
|
||||
s.Content.Append(it.Next())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SRT) mergeBottom(other *SRT) {
|
||||
oq := other.Content
|
||||
if oq.IsNotEmpty() {
|
||||
s.Content.PushFront(*(oq.PullBack()))
|
||||
}
|
||||
}
|
||||
|
||||
// WriteFile 将SRT格式的数据写入指定的文件中
|
||||
func (s *SRT) WriteFile(path string) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.Write(f)
|
||||
err = f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// Write 将SRT格式的数据写入dst中
|
||||
func (s *SRT) Write(dst io.Writer) error {
|
||||
//6KB的缓冲
|
||||
bufSize := 1024 * 6
|
||||
writer := bufio.NewWriterSize(dst, bufSize)
|
||||
for it := s.Content.Iterator(); it.Has(); {
|
||||
_, err := writer.WriteString(it.Next().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return writer.Flush()
|
||||
}
|
15
util.go
15
util.go
|
@ -1,13 +1,22 @@
|
|||
package main
|
||||
package lrc2srt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
CHROME_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36"
|
||||
)
|
||||
|
||||
var (
|
||||
client = http.Client{}
|
||||
)
|
||||
|
||||
// Time2Millisecond 根据分,秒,毫秒 计算出对应的毫秒值
|
||||
func Time2Millisecond(m, s, ms int) int {
|
||||
func time2Millisecond(m, s, ms int) int {
|
||||
t := m*60 + s
|
||||
t *= 1000
|
||||
t += ms
|
||||
|
@ -15,7 +24,7 @@ func Time2Millisecond(m, s, ms int) int {
|
|||
}
|
||||
|
||||
// Millisecond2Time 根据毫秒值计算出对应的 时,分,秒,毫秒形式的时间值
|
||||
func Millisecond2Time(millisecond int) (h, m, s, ms int) {
|
||||
func millisecond2Time(millisecond int) (h, m, s, ms int) {
|
||||
ms = millisecond % 1000
|
||||
|
||||
s = millisecond / 1000
|
||||
|
|
10
util_test.go
10
util_test.go
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package lrc2srt
|
||||
|
||||
import "testing"
|
||||
|
||||
|
@ -23,7 +23,7 @@ func TestMillisecond2Time(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotH, gotM, gotS, gotMs := Millisecond2Time(tt.args.millisecond)
|
||||
gotH, gotM, gotS, gotMs := millisecond2Time(tt.args.millisecond)
|
||||
if gotH != tt.wantH {
|
||||
t.Errorf("Millisecond2Time() gotH = %v, want %v", gotH, tt.wantH)
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ func TestTime2Millisecond(t *testing.T) {
|
|||
}
|
||||
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 {
|
||||
if got := time2Millisecond(tt.args.m, tt.args.s, tt.args.ms); got != tt.want {
|
||||
t.Errorf("Time2Millisecond() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
|
@ -69,12 +69,12 @@ func TestTime2Millisecond(t *testing.T) {
|
|||
|
||||
func BenchmarkTime2Millisecond(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Time2Millisecond(999, 999, 999)
|
||||
time2Millisecond(999, 999, 999)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMillisecond2Time(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Millisecond2Time(9999999999)
|
||||
millisecond2Time(9999999999)
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue