Compare commits
26 commits
Author | SHA1 | Date | |
---|---|---|---|
|
f044d36339 | ||
|
425e354bca | ||
|
b973ecb562 | ||
|
dc2e111c14 | ||
|
7212aa3220 | ||
|
76d1e8215d | ||
|
72607e927c | ||
|
dc8033dd53 | ||
|
eba676b8de | ||
|
a29d2c057e | ||
|
3e2af31678 | ||
|
a4ea096e3d | ||
|
c381852b49 | ||
|
8b57abe8ee | ||
|
8891c44211 | ||
|
d708df2de0 | ||
|
c9b1b38c03 | ||
|
5f49bde3ae | ||
|
d71a05e4b1 | ||
|
9a48cb03af | ||
|
845ec9c5fe | ||
|
f92ddb245b | ||
|
664e04c5ab | ||
|
8e7170f2d2 | ||
|
8fcc3509f5 | ||
|
0694c54460 |
27 changed files with 1710 additions and 386 deletions
29
.github/workflows/go.yml
vendored
Normal file
29
.github/workflows/go.yml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
#编译
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup Go environment
|
||||||
|
uses: actions/setup-go@v3.0.0
|
||||||
|
with:
|
||||||
|
go-version: '>=1.18'
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -run=Test
|
||||||
|
|
||||||
|
- name: Benchmark
|
||||||
|
run: go test -bench=Benchmark -benchmem
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -87,6 +87,7 @@ fabric.properties
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# vendor/
|
||||||
|
*.srt
|
||||||
*.bat
|
*.lrc
|
||||||
|
*.ass
|
||||||
/temp
|
/temp
|
||||||
|
|
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ltc
|
4
.idea/lrc2srt.iml → .idea/ltc.iml
generated
4
.idea/lrc2srt.iml → .idea/ltc.iml
generated
|
@ -2,9 +2,7 @@
|
||||||
<module type="WEB_MODULE" version="4">
|
<module type="WEB_MODULE" version="4">
|
||||||
<component name="Go" enabled="true" />
|
<component name="Go" enabled="true" />
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
8
.idea/misc.xml
generated
Normal file
8
.idea/misc.xml
generated
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SwUserDefinedSpecifications">
|
||||||
|
<option name="specTypeByUrl">
|
||||||
|
<map />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
2
.idea/modules.xml
generated
2
.idea/modules.xml
generated
|
@ -2,7 +2,7 @@
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/lrc2srt.iml" filepath="$PROJECT_DIR$/.idea/lrc2srt.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/ltc.iml" filepath="$PROJECT_DIR$/.idea/ltc.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
57
README.md
57
README.md
|
@ -1,10 +1,14 @@
|
||||||
# LrcToSrt
|
# LrcToCaptons
|
||||||
用于将LRC歌词文件转换成SRT字幕文件
|
|
||||||
|
[](https://github.com/Hami-Lemon/LrcToSrt/actions/workflows/go.yml)
|
||||||
|
|
||||||
|
用于将LRC歌词文件转换成ASS、SRT字幕文件
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
- [x] lrc文件转换成srt文件
|
- [x] lrc文件转换成srt文件
|
||||||
- [x] 从网易云音乐或QQ音乐上获取歌词,并转换成srt文件
|
- [x] lrc文件转换成ass文件
|
||||||
- [x] 从网易云音乐或QQ音乐上下载歌词
|
- [x] 从网易云音乐或QQ音乐上获取歌词,并转换。
|
||||||
|
- [x] 从网易云音乐或QQ音乐上下载歌词。
|
||||||
|
|
||||||
## 下载
|
## 下载
|
||||||
|
|
||||||
|
@ -13,21 +17,25 @@
|
||||||
|
|
||||||
## 开始使用
|
## 开始使用
|
||||||
|
|
||||||
```
|
```text
|
||||||
Usage:
|
LrcToCaptions(ltc) 将LRC歌词文件转换成字幕文件。
|
||||||
D:\ProgrameStudy\lrc2srt\lts.exe [OPTIONS]
|
ltc version: "0.3.4" (build 2022.03.30)
|
||||||
|
|
||||||
Application Options:
|
用法:ltc [options] OutputFile
|
||||||
-i, --id= 歌曲的id,网易云和QQ音乐均可。
|
|
||||||
-I, --input= 需要转换的LRC文件路径。
|
|
||||||
-s, --source=[163|qq|QQ] 当设置id时有效,指定从网易云(163)还是QQ音乐(qq)上获取歌词。
|
|
||||||
(default: 163)
|
|
||||||
-d, --download 只下载歌词,而不进行解析。
|
|
||||||
-m, --mode=[1|2|3] 原文和译文的排列模式,可选值有:[1] [2] [3] (default: 1)
|
|
||||||
-v, --version 获取版本信息
|
|
||||||
|
|
||||||
Help Options:
|
options:
|
||||||
-h, --help Show this help message
|
|
||||||
|
-d 设置该选项时,只下载歌词,而无需转换。
|
||||||
|
-f value
|
||||||
|
转换成的字幕文件格式,可选值:ass(默认),srt
|
||||||
|
-i string
|
||||||
|
歌词来源,可以是歌词对应的歌曲id,也可以是歌词文件
|
||||||
|
-m value
|
||||||
|
设置歌词原文和译文的合并模式,可选值:1(默认),2,3。
|
||||||
|
-s string
|
||||||
|
选择从网易云还是QQ音乐上获取歌词,可选值:163(默认),qq。 (default "163")
|
||||||
|
-v 获取当前程序版本信息。
|
||||||
|
-h 显示帮助信息。
|
||||||
```
|
```
|
||||||
|
|
||||||
### 获取歌曲id
|
### 获取歌曲id
|
||||||
|
@ -142,3 +150,18 @@ lts -i 003FJlVU1rxjv8 -m 2 -s qq "ふわふわ时间.srt"
|
||||||
## 结束时间处理策略
|
## 结束时间处理策略
|
||||||
|
|
||||||
因为在LRC文件中,并不包含一句歌词的结束时间,所以在转换成SRT文件时,处理策略为,**一句歌词的结束时间为下一句歌词的开始时间,最后一句歌词的结束时间为其`开始时间+10秒`**,所以在打轴时,对进入间奏的地方应该手动调整歌词的结束时间。
|
因为在LRC文件中,并不包含一句歌词的结束时间,所以在转换成SRT文件时,处理策略为,**一句歌词的结束时间为下一句歌词的开始时间,最后一句歌词的结束时间为其`开始时间+10秒`**,所以在打轴时,对进入间奏的地方应该手动调整歌词的结束时间。
|
||||||
|
|
||||||
|
## 源码编译
|
||||||
|
|
||||||
|
### 环境需求
|
||||||
|
|
||||||
|
- [Go 1.18+](https://golang.google.cn/dl/)
|
||||||
|
|
||||||
|
### 编译
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Hami-Lemon/ltc.git
|
||||||
|
cd ./ltc/lrctocaptions
|
||||||
|
go build -o ltc.exe .
|
||||||
|
```
|
||||||
|
|
||||||
|
|
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
|
||||||
|
}
|
|
@ -1,11 +1,10 @@
|
||||||
package main
|
package ltc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CloudLyricBase struct {
|
type CloudLyricBase struct {
|
||||||
|
@ -38,15 +37,15 @@ func Get163Lyric(id string) (lyric, tLyric string) {
|
||||||
req, _ := http.NewRequest("GET", api, nil)
|
req, _ := http.NewRequest("GET", api, nil)
|
||||||
//必须设置Referer,否则会请求失败
|
//必须设置Referer,否则会请求失败
|
||||||
req.Header.Add("Referer", "https://music.163.com")
|
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)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("网络错误:%v\n", err)
|
fmt.Printf("网络错误:%v\n", err)
|
||||||
os.Exit(1)
|
panic("网络异常,获取失败。")
|
||||||
}
|
}
|
||||||
if resp == nil || resp.StatusCode != http.StatusOK {
|
if resp == nil || resp.StatusCode != http.StatusOK {
|
||||||
fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode)
|
fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode)
|
||||||
os.Exit(1)
|
panic("获取失败,未能正确获取到数据")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
@ -54,7 +53,7 @@ func Get163Lyric(id string) (lyric, tLyric string) {
|
||||||
err = json.NewDecoder(resp.Body).Decode(&cloudLyric)
|
err = json.NewDecoder(resp.Body).Decode(&cloudLyric)
|
||||||
if cloudLyric.Sgc {
|
if cloudLyric.Sgc {
|
||||||
fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", cloudLyric)
|
fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", cloudLyric)
|
||||||
os.Exit(1)
|
panic("id错误,获取歌词失败")
|
||||||
}
|
}
|
||||||
return cloudLyric.Lrc.Lyric, cloudLyric.TLyric.Lyric
|
return cloudLyric.Lrc.Lyric, cloudLyric.TLyric.Lyric
|
||||||
}
|
}
|
||||||
|
|
22
cloudlyric_test.go
Normal file
22
cloudlyric_test.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package ltc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGet163Lyric(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
}{
|
||||||
|
{"1423123512"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("id=%s", tt.input), func(t *testing.T) {
|
||||||
|
l, lt := Get163Lyric(tt.input)
|
||||||
|
if l == "" || lt == "" {
|
||||||
|
t.Errorf("get cloud lyric faild, id = %s", tt.input)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
16
example/example.txt
Normal file
16
example/example.txt
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
encoding=utf-8
|
||||||
|
|
||||||
|
example#1 从网易云上下载歌词
|
||||||
|
lts -i 1903635166 传说的世界.srt
|
||||||
|
|
||||||
|
example#2 从qq音乐上下载歌词
|
||||||
|
lts -i 003eKeNV0t8IVi -s qq "bad guy.srt"
|
||||||
|
|
||||||
|
example#3 从网易云上下载歌词,不解析
|
||||||
|
lts -i 1903635166 -d 传说的世界.lrc
|
||||||
|
|
||||||
|
example#4 解析已有的lrc文件
|
||||||
|
lts -I 传说的世界.lrc 传说的世界2.srt
|
||||||
|
|
||||||
|
example#5 设置mode为2,原文在上,译文在下
|
||||||
|
lts -i 003eKeNV0t8IVi -s qq -m 2 "bad guy2.srt"
|
268
glist/linkedlist.go
Normal file
268
glist/linkedlist.go
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
package glist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CAPACITY 链表的最大容量
|
||||||
|
CAPACITY uint = math.MaxUint
|
||||||
|
ZERO = uint(0) //uint类型的0
|
||||||
|
)
|
||||||
|
|
||||||
|
// Node 链表中的一个结点
|
||||||
|
type Node[E any] struct {
|
||||||
|
element E //保存的内容
|
||||||
|
prev *Node[E] //前一个结点
|
||||||
|
next *Node[E] //后一个结点
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone 克隆Node,返回的Node的Prev和Next均为nil,Element保持不变
|
||||||
|
func (n *Node[E]) Clone() *Node[E] {
|
||||||
|
node := &Node[E]{
|
||||||
|
element: n.element,
|
||||||
|
prev: nil,
|
||||||
|
next: nil,
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkedList 链表,实现了List
|
||||||
|
type LinkedList[E any] struct {
|
||||||
|
len uint //链表中元素个数
|
||||||
|
first *Node[E] //头指针
|
||||||
|
last *Node[E] //尾指针
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLinkedList 创建一个链表,
|
||||||
|
//列表的最大容量为uint类型的最大值
|
||||||
|
func NewLinkedList[E any]() *LinkedList[E] {
|
||||||
|
return &LinkedList[E]{
|
||||||
|
len: 0,
|
||||||
|
first: nil,
|
||||||
|
last: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) Size() uint {
|
||||||
|
return l.len
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) IsEmpty() bool {
|
||||||
|
return l.len == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) IsNotEmpty() bool {
|
||||||
|
return l.len != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) Append(element E) bool {
|
||||||
|
//超出最大值无法添加
|
||||||
|
if l.len == CAPACITY {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
node := &Node[E]{
|
||||||
|
element: element,
|
||||||
|
prev: nil,
|
||||||
|
next: nil,
|
||||||
|
}
|
||||||
|
//链表为空,头指针指向该结点
|
||||||
|
if l.first == nil {
|
||||||
|
l.first = node
|
||||||
|
l.last = node
|
||||||
|
} else {
|
||||||
|
//链表不为空,添加到尾部
|
||||||
|
node.prev = l.last
|
||||||
|
l.last.next = node
|
||||||
|
l.last = node
|
||||||
|
}
|
||||||
|
l.len++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) Insert(index uint, element E) bool {
|
||||||
|
//当前size已经达到最大值或者索引越界
|
||||||
|
if l.len == CAPACITY || index > l.len {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
node := &Node[E]{
|
||||||
|
element: element,
|
||||||
|
prev: nil,
|
||||||
|
next: nil,
|
||||||
|
}
|
||||||
|
//插入头部
|
||||||
|
if index == 0 {
|
||||||
|
if l.first == nil {
|
||||||
|
//链表为空
|
||||||
|
l.first = node
|
||||||
|
l.last = node
|
||||||
|
} else {
|
||||||
|
//链表不为空
|
||||||
|
node.next = l.first
|
||||||
|
l.first.prev = node
|
||||||
|
l.first = node
|
||||||
|
}
|
||||||
|
} else if index == l.len {
|
||||||
|
//插入尾部
|
||||||
|
l.last.next = node
|
||||||
|
node.prev = l.last
|
||||||
|
l.last = node
|
||||||
|
} else {
|
||||||
|
var prev *Node[E]
|
||||||
|
head := l.first
|
||||||
|
for i := ZERO; i < index; i++ {
|
||||||
|
prev = head
|
||||||
|
head = head.next
|
||||||
|
}
|
||||||
|
node.next = head
|
||||||
|
node.prev = prev
|
||||||
|
prev.next = node
|
||||||
|
head.prev = node
|
||||||
|
}
|
||||||
|
l.len++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) Remove(index uint) bool {
|
||||||
|
if index >= l.len {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
head := l.first
|
||||||
|
var prev *Node[E]
|
||||||
|
for i := ZERO; i < index; i++ {
|
||||||
|
prev = head
|
||||||
|
head = head.next
|
||||||
|
}
|
||||||
|
//删除第一个结点
|
||||||
|
if head == l.first {
|
||||||
|
l.first.next = nil
|
||||||
|
l.first = head.next
|
||||||
|
} else if head == l.last {
|
||||||
|
//删除最后一个结点
|
||||||
|
l.last = prev
|
||||||
|
l.last.next = nil
|
||||||
|
} else {
|
||||||
|
prev.next = head.next
|
||||||
|
head.next.prev = prev
|
||||||
|
}
|
||||||
|
l.len--
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) Get(index uint) *E {
|
||||||
|
if index >= l.len {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
node := l.first
|
||||||
|
for i := ZERO; i < index; i++ {
|
||||||
|
node = node.next
|
||||||
|
}
|
||||||
|
return &(node.element)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) Set(index uint, element E) bool {
|
||||||
|
if index >= l.len {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
node := l.first
|
||||||
|
for i := ZERO; i < index; i++ {
|
||||||
|
node = node.next
|
||||||
|
}
|
||||||
|
node.element = element
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) PushBack(element E) bool {
|
||||||
|
return l.Append(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) PushFront(element E) bool {
|
||||||
|
return l.Insert(0, element)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) PopBack() *E {
|
||||||
|
//链表为空
|
||||||
|
if l.len == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
node := l.last
|
||||||
|
//只有一个元素
|
||||||
|
if l.len == 1 {
|
||||||
|
l.last = nil
|
||||||
|
l.first = nil
|
||||||
|
} else {
|
||||||
|
l.last = node.prev
|
||||||
|
l.last.next = nil
|
||||||
|
}
|
||||||
|
l.len--
|
||||||
|
return &(node.element)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) PopFront() *E {
|
||||||
|
if l.len == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
node := l.first
|
||||||
|
if l.len == 1 {
|
||||||
|
l.first = nil
|
||||||
|
l.last = nil
|
||||||
|
} else {
|
||||||
|
l.first = node.next
|
||||||
|
l.first.prev = nil
|
||||||
|
}
|
||||||
|
l.len--
|
||||||
|
return &(node.element)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) PullBack() *E {
|
||||||
|
if l.len == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &(l.last.element)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedList[E]) PullFront() *E {
|
||||||
|
if l.len == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &(l.first.element)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterator 获取该链表的迭代器
|
||||||
|
func (l *LinkedList[E]) Iterator() Iterator[E] {
|
||||||
|
return &LinkedListIterator[E]{
|
||||||
|
reverse: false,
|
||||||
|
next: l.first,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReverseIterator 获取反向迭代器
|
||||||
|
func (l *LinkedList[E]) ReverseIterator() Iterator[E] {
|
||||||
|
return &LinkedListIterator[E]{
|
||||||
|
reverse: true,
|
||||||
|
next: l.last,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkedListIterator[E any] struct {
|
||||||
|
//是否反向,如果为true,则是从尾部向头部迭代
|
||||||
|
reverse bool
|
||||||
|
next *Node[E]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedListIterator[E]) Has() bool {
|
||||||
|
return l.next != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedListIterator[E]) Next() E {
|
||||||
|
e := l.next
|
||||||
|
if e == nil {
|
||||||
|
panic("iterator is empty.")
|
||||||
|
}
|
||||||
|
if l.reverse {
|
||||||
|
l.next = e.prev
|
||||||
|
} else {
|
||||||
|
l.next = e.next
|
||||||
|
}
|
||||||
|
return e.element
|
||||||
|
}
|
209
glist/linkedlist_test.go
Normal file
209
glist/linkedlist_test.go
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
package glist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkListElement[T comparable](list *LinkedList[T], items []T, t *testing.T) {
|
||||||
|
if list.len != uint(len(items)) {
|
||||||
|
t.Errorf("list len= %d, items len = %d.", list.len, len(items))
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
for it := list.Iterator(); it.Has(); {
|
||||||
|
e := it.Next()
|
||||||
|
if e != items[i] {
|
||||||
|
t.Errorf("index=%d,except:%v, but got:%v", i, items[i], e)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinkedListIterator_Has(t *testing.T) {
|
||||||
|
//空列表
|
||||||
|
t.Run("empty list", func(t *testing.T) {
|
||||||
|
emptyList := NewLinkedList[int]()
|
||||||
|
it := emptyList.Iterator()
|
||||||
|
if it.Has() {
|
||||||
|
t.Errorf("iterator should empty, but not")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//非空列表
|
||||||
|
t.Run("non-empty list", func(t *testing.T) {
|
||||||
|
list := NewLinkedList[int]()
|
||||||
|
list.Append(1)
|
||||||
|
list.Append(2)
|
||||||
|
it := list.Iterator()
|
||||||
|
if !(it.Has()) {
|
||||||
|
t.Errorf("iterator should not empty, but not")
|
||||||
|
}
|
||||||
|
it.Next()
|
||||||
|
it.Next()
|
||||||
|
if it.Has() {
|
||||||
|
t.Errorf("iterator should empty, but not")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinkedListIterator_Next(t *testing.T) {
|
||||||
|
t.Run("empty list", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p == nil {
|
||||||
|
t.Errorf("iterator is empty, should panic.")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
emptyList := NewLinkedList[int]()
|
||||||
|
it := emptyList.Iterator()
|
||||||
|
it.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-empty list", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
t.Errorf("iterator is non-empty, should no panic.")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
list := NewLinkedList[int]()
|
||||||
|
times := rand.Int() % 20 //20次以内
|
||||||
|
for i := times; i >= 0; i-- {
|
||||||
|
list.Append(i)
|
||||||
|
}
|
||||||
|
it := list.Iterator()
|
||||||
|
for i := times; i >= 0; i-- {
|
||||||
|
it.Next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinkedList_Append(t *testing.T) {
|
||||||
|
var sliceWithTenNum [10]int
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
sliceWithTenNum[i] = i
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []int
|
||||||
|
}{
|
||||||
|
{"Append 0 value", []int{}},
|
||||||
|
{"Append 10 value", sliceWithTenNum[:]},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
list := NewLinkedList[int]()
|
||||||
|
for _, v := range tt.input {
|
||||||
|
list.Append(v)
|
||||||
|
}
|
||||||
|
checkListElement(list, tt.input, t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinkedList_Size(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want uint
|
||||||
|
}{
|
||||||
|
{"empty list", 0},
|
||||||
|
{"10 values list", 10},
|
||||||
|
{"20 values list", 20},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
num := tt.want
|
||||||
|
list := NewLinkedList[int]()
|
||||||
|
for i := uint(0); i < num; i++ {
|
||||||
|
list.Append(int(i))
|
||||||
|
}
|
||||||
|
if got := list.Size(); got != tt.want {
|
||||||
|
t.Errorf("Size() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinkedList_IsEmpty(t *testing.T) {
|
||||||
|
nonEmpty := NewLinkedList[int]()
|
||||||
|
nonEmpty.Append(1)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
list *LinkedList[int]
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"empty list", NewLinkedList[int](), true},
|
||||||
|
{"non-empty list", nonEmpty, false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
l := tt.list
|
||||||
|
if got := l.IsEmpty(); got != tt.want {
|
||||||
|
t.Errorf("IsEmpty() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinkedList_Remove(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
listNum int
|
||||||
|
args uint
|
||||||
|
want bool
|
||||||
|
values []int
|
||||||
|
}{
|
||||||
|
{"empty list", 0, 0, false, nil},
|
||||||
|
{"index gather than len", 0, 1, false, nil},
|
||||||
|
{"1 value list:delete index 0", 1, 0, true, []int{}},
|
||||||
|
{"10 value list:delete index 0", 10, 0, true, []int{1, 2, 3, 4, 5, 6, 7, 8, 9}},
|
||||||
|
{"10 value list:delete index 5", 10, 5, true, []int{0, 1, 2, 3, 4, 6, 7, 8, 9}},
|
||||||
|
{"10 value list:delete index 9", 10, 9, true, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
l := NewLinkedList[int]()
|
||||||
|
for i := 0; i < tt.listNum; i++ {
|
||||||
|
l.Append(i)
|
||||||
|
}
|
||||||
|
if got := l.Remove(tt.args); got != tt.want {
|
||||||
|
t.Errorf("Remove() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
if tt.want {
|
||||||
|
checkListElement(l, tt.values, t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinkedList_Get(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
listNum int
|
||||||
|
args uint
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"empty list,index 0", 0, 0, -1},
|
||||||
|
{"empty list,index 1", 0, 1, -1},
|
||||||
|
{"1 value list, index 0", 1, 0, 0},
|
||||||
|
{"10 value list, index 0", 10, 0, 0},
|
||||||
|
{"10 value list, index 5", 10, 5, 5},
|
||||||
|
{"10 value list, index 9", 10, 9, 9},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
l := NewLinkedList[int]()
|
||||||
|
for i := 0; i < tt.listNum; i++ {
|
||||||
|
l.Append(i)
|
||||||
|
}
|
||||||
|
want := new(int)
|
||||||
|
if tt.want == -1 {
|
||||||
|
want = nil
|
||||||
|
} else {
|
||||||
|
*want = tt.want
|
||||||
|
}
|
||||||
|
if got := l.Get(tt.args); !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("Get() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
50
glist/list.go
Normal file
50
glist/list.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package glist
|
||||||
|
|
||||||
|
// List 一个基本的列表
|
||||||
|
type List[E any] interface {
|
||||||
|
// Size 获取列表中数据个数
|
||||||
|
Size() uint
|
||||||
|
//IsEmpty 判断列表是否为空,如果为空,返回true,否则返回false
|
||||||
|
IsEmpty() bool
|
||||||
|
// IsNotEmpty 判断列表是否非空,如果列表不为空,返回true,否则返回false
|
||||||
|
IsNotEmpty() bool
|
||||||
|
// Append 向列表尾部添加一个元素
|
||||||
|
Append(element E) bool
|
||||||
|
// Insert 向列表指定索引处插入一个元素,如果插入成功返回true,否则返回false
|
||||||
|
Insert(index uint, element E) bool
|
||||||
|
// Remove 从列表中移除元素element,如果元素不存在,则返回false
|
||||||
|
Remove(index uint) bool
|
||||||
|
// Get 从列表中获取索引为index元素的指针,索引从0开始,如果索引超出范围则返回nil
|
||||||
|
Get(index uint) *E
|
||||||
|
// Set 改变列表中索引为index的元素的值,如果索引超出范围则返回false
|
||||||
|
Set(index uint, element E) bool
|
||||||
|
// Iterator 获取列表的迭代器
|
||||||
|
Iterator() Iterator[E]
|
||||||
|
// ReverseIterator 反向迭代器,从尾部向前迭代
|
||||||
|
ReverseIterator() Iterator[E]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue 队列
|
||||||
|
type Queue[E any] interface {
|
||||||
|
List[E]
|
||||||
|
// PushBack 队列尾部添加元素,添加成功返回true
|
||||||
|
PushBack(element E) bool
|
||||||
|
// PushFront 队列头部添加元素,添加成功返回true
|
||||||
|
PushFront(element E) bool
|
||||||
|
// PopBack 删除队列尾部的元素,返回被删除的元素的指针,如果队列为空,则返回nil
|
||||||
|
PopBack() *E
|
||||||
|
// PopFront 删除队列头部的元素,返回被删除元素的指针,如果队列为空,返回nil
|
||||||
|
PopFront() *E
|
||||||
|
// PullBack 获取队列尾部的元素的指针,不会删除,如果队列为空,返回nil
|
||||||
|
PullBack() *E
|
||||||
|
// PullFront 获取队列头部的元素的指针,不会删除,如果队列为空,返回nil
|
||||||
|
PullFront() *E
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterator 列表迭代器
|
||||||
|
type Iterator[E any] interface {
|
||||||
|
// Has 是否还有元素
|
||||||
|
Has() bool
|
||||||
|
// Next 获取元素
|
||||||
|
Next() E
|
||||||
|
}
|
8
go.mod
8
go.mod
|
@ -1,7 +1,3 @@
|
||||||
module github.com/hami_lemon/lrc2srt
|
module github.com/Hami-Lemon/ltc
|
||||||
|
|
||||||
go 1.17
|
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=
|
|
128
lrc.go
Normal file
128
lrc.go
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
package ltc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Hami-Lemon/ltc/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: strings.TrimSpace(content), //去掉前后的空格
|
||||||
|
}
|
||||||
|
return lrcNode
|
||||||
|
}
|
68
lrc_test.go
Normal file
68
lrc_test.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package ltc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseLRC(t *testing.T) {
|
||||||
|
lrc := `[ar:artist]
|
||||||
|
[al:album]
|
||||||
|
[ti:title]
|
||||||
|
[by:author]
|
||||||
|
[00:24.83] 天涯的尽头 有谁去过
|
||||||
|
[00:28.53] 山水优雅着 保持沉默
|
||||||
|
[00:32.20] 我们的青春却热闹很多
|
||||||
|
[00:35.38] 而且是谁都 不准偷
|
||||||
|
`
|
||||||
|
content := []string{
|
||||||
|
"天涯的尽头 有谁去过", "山水优雅着 保持沉默", "我们的青春却热闹很多", "而且是谁都 不准偷",
|
||||||
|
}
|
||||||
|
l := ParseLRC(lrc)
|
||||||
|
if l.Artist != "artist" {
|
||||||
|
t.Errorf("LRC Artist=%s, want=%s", l.Artist, "artist")
|
||||||
|
}
|
||||||
|
if l.Album != "album" {
|
||||||
|
t.Errorf("LRC Album=%s, want=%s", l.Album, "album")
|
||||||
|
}
|
||||||
|
if l.Title != "title" {
|
||||||
|
t.Errorf("LRC Title=%s, want=%s", l.Title, "title")
|
||||||
|
}
|
||||||
|
if l.Author != "author" {
|
||||||
|
t.Errorf("LRC Author=%s, want=%s", l.Author, "author")
|
||||||
|
}
|
||||||
|
lrcList := l.LrcList
|
||||||
|
index := 0
|
||||||
|
for it := lrcList.Iterator(); it.Has(); {
|
||||||
|
c := it.Next().content
|
||||||
|
if c != content[index] {
|
||||||
|
t.Errorf("LRCNode Content=%s, want=%s", c, content[index])
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitLyric(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
want *LRCNode
|
||||||
|
}{
|
||||||
|
{"lrc:[00:49.88] 有一些话想 对你说", []string{"00", "49", ".88", " 有一些话想 对你说"},
|
||||||
|
&LRCNode{time: time2Millisecond(0, 49, 880), content: "有一些话想 对你说"}},
|
||||||
|
{"lrc:[00:49:88] 有一些话想 对你说", []string{"00", "49", ":88", " 有一些话想 对你说"},
|
||||||
|
&LRCNode{time: time2Millisecond(0, 49, 880), content: "有一些话想 对你说"}},
|
||||||
|
{"lrc:[00:49.880] 有一些话想 对你说", []string{"00", "49", ".880", " 有一些话想 对你说"},
|
||||||
|
&LRCNode{time: time2Millisecond(0, 49, 880), content: "有一些话想 对你说"}},
|
||||||
|
{"lrc:[00:49] 有一些话想 对你说", []string{"00", "49", " 有一些话想 对你说"},
|
||||||
|
&LRCNode{time: time2Millisecond(0, 49, 0), content: "有一些话想 对你说"}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := SplitLyric(tt.args); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("SplitLyric() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
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)
|
||||||
|
}
|
122
lrctocaptions/ltc.go
Normal file
122
lrctocaptions/ltc.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/Hami-Lemon/ltc"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// VERSION 当前版本
|
||||||
|
VERSION = `"0.3.4" (build 2022.03.30)`
|
||||||
|
VERSION_INFO = "LrcToCaptions(ltc) version: %s\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
parseFlag()
|
||||||
|
//TODO 酷狗的krc精准到字,更利于打轴 https://shansing.com/read/392/
|
||||||
|
//显示版本信息
|
||||||
|
if version.IsSet() {
|
||||||
|
fmt.Printf(VERSION_INFO, VERSION)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//未指定来源
|
||||||
|
if input == "" {
|
||||||
|
fmt.Printf("未指定歌词来源\n")
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
//获取歌词,lyric为原文歌词,tranLyric为译文歌词
|
||||||
|
var lyric, tranLyric string
|
||||||
|
//从文件中获取
|
||||||
|
if checkPath(input) {
|
||||||
|
if !strings.HasSuffix(input, ".lrc") {
|
||||||
|
fmt.Println("Error: 不支持的格式,目前只支持lrc歌词文件。")
|
||||||
|
panic("")
|
||||||
|
}
|
||||||
|
if data, err := ioutil.ReadFile(input); err == nil {
|
||||||
|
if len(data) == 0 {
|
||||||
|
fmt.Println("获取歌词失败,文件内容为空。")
|
||||||
|
panic("")
|
||||||
|
}
|
||||||
|
lyric = string(data)
|
||||||
|
} else {
|
||||||
|
panic("读取文件失败:" + input + err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//从网络上获取
|
||||||
|
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 {
|
||||||
|
//原文和译文合并
|
||||||
|
srt.Merge(srtT, mode.Mode())
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//如果是相对路径,则获取其对应的绝对路径
|
||||||
|
if !filepath.IsAbs(output) {
|
||||||
|
//如果是相对路径,父目录即是当前运行路径
|
||||||
|
dir, er := os.Getwd()
|
||||||
|
if er == nil {
|
||||||
|
output = dir + string(os.PathSeparator) + output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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("")
|
||||||
|
}
|
||||||
|
}
|
329
lts.go
329
lts.go
|
@ -1,329 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
28
qqlyric.go
28
qqlyric.go
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package ltc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
@ -6,15 +6,21 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
从QQ音乐上获取歌词
|
||||||
|
*/
|
||||||
|
|
||||||
|
// QQLyric qq音乐获取歌词接口返回的数据结构
|
||||||
type QQLyric struct {
|
type QQLyric struct {
|
||||||
RetCode int `json:"retcode"`
|
RetCode int `json:"retcode"`
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
SubCode int `json:"subcode"`
|
SubCode int `json:"subcode"`
|
||||||
Lyric string `json:"lyric"`
|
//Lyric 原文歌词
|
||||||
Trans string `json:"trans"`
|
Lyric string `json:"lyric"`
|
||||||
|
//Trans 译文歌词
|
||||||
|
Trans string `json:"trans"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetQQLyric(id string) (lyric, tLyric string) {
|
func GetQQLyric(id string) (lyric, tLyric string) {
|
||||||
|
@ -36,17 +42,17 @@ func GetQQLyric(id string) (lyric, tLyric string) {
|
||||||
req, _ := http.NewRequest("GET", api, nil)
|
req, _ := http.NewRequest("GET", api, nil)
|
||||||
//必须设置Referer,否则会请求失败
|
//必须设置Referer,否则会请求失败
|
||||||
req.Header.Add("Referer", "https://y.qq.com")
|
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")
|
req.Header.Add("accept-encoding", "gzip")
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("网络错误:%v\n", err)
|
fmt.Printf("网络错误:%v\n", err)
|
||||||
os.Exit(1)
|
panic("网络异常,请求失败。")
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp == nil || resp.StatusCode != http.StatusOK {
|
if resp == nil || resp.StatusCode != http.StatusOK {
|
||||||
fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode)
|
fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode)
|
||||||
os.Exit(1)
|
panic("获取失败,未能正确获取到数据")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
@ -56,7 +62,7 @@ func GetQQLyric(id string) (lyric, tLyric string) {
|
||||||
err = json.NewDecoder(reader).Decode(&qqLyric)
|
err = json.NewDecoder(reader).Decode(&qqLyric)
|
||||||
if qqLyric.RetCode != 0 {
|
if qqLyric.RetCode != 0 {
|
||||||
fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", qqLyric)
|
fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", qqLyric)
|
||||||
os.Exit(1)
|
panic("id错误,获取歌词失败。")
|
||||||
}
|
}
|
||||||
return qqLyric.Lyric, qqLyric.Trans
|
return qqLyric.Lyric, qqLyric.Trans
|
||||||
}
|
}
|
||||||
|
|
22
qqlyric_test.go
Normal file
22
qqlyric_test.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package ltc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetQQLyric(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
}{
|
||||||
|
{"0002Jztl3eJKu0"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("id=%s", tt.input), func(t *testing.T) {
|
||||||
|
l, lt := GetQQLyric(tt.input)
|
||||||
|
if l == "" || lt == "" {
|
||||||
|
t.Errorf("get cloud lyric faild, id = %s", tt.input)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
202
srt.go
Normal file
202
srt.go
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
package ltc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"github.com/Hami-Lemon/ltc/glist"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"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: 0,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//可以类比为合并两个有序链表,
|
||||||
|
//算法参考:https://leetcode-cn.com/problems/merge-two-sorted-lists/solution/he-bing-liang-ge-you-xu-lian-biao-by-leetcode-solu/
|
||||||
|
func (s *SRT) mergeStack(other *SRT) {
|
||||||
|
sIt, oIt := s.Content.Iterator(), other.Content.Iterator()
|
||||||
|
//不对原来的链表做修改,合并的信息保存在一个新的链表中
|
||||||
|
merge := glist.NewLinkedList[*SRTContent]()
|
||||||
|
var sNode, oNode *SRTContent
|
||||||
|
//分别获取两个链表的第一个元素
|
||||||
|
if sIt.Has() && oIt.Has() {
|
||||||
|
sNode, oNode = sIt.Next(), oIt.Next()
|
||||||
|
}
|
||||||
|
//开始迭代
|
||||||
|
for sIt.Has() && oIt.Has() {
|
||||||
|
//小于等于,当相等时,s中的元素添加进去
|
||||||
|
if sNode.Start <= oNode.Start {
|
||||||
|
merge.Append(sNode)
|
||||||
|
sNode = sIt.Next()
|
||||||
|
} else {
|
||||||
|
merge.Append(oNode)
|
||||||
|
oNode = oIt.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sNode != nil && oNode != nil {
|
||||||
|
//循环退出时,sNode和oNode指向的元素还没有进行比较,会导致缺少两条数据
|
||||||
|
if sNode.Start <= oNode.Start {
|
||||||
|
merge.Append(sNode)
|
||||||
|
merge.Append(oNode)
|
||||||
|
} else {
|
||||||
|
merge.Append(oNode)
|
||||||
|
merge.Append(sNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//剩下的元素添加到链表中,最多只有一个链表有剩余元素
|
||||||
|
for sIt.Has() {
|
||||||
|
merge.Append(sIt.Next())
|
||||||
|
}
|
||||||
|
for oIt.Has() {
|
||||||
|
merge.Append(oIt.Next())
|
||||||
|
}
|
||||||
|
s.Content = merge
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
for it := other.Content.ReverseIterator(); it.Has(); {
|
||||||
|
s.Content.PushFront(it.Next())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile 将SRT格式的数据写入指定的文件中
|
||||||
|
func (s *SRT) WriteFile(path string) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
//不存在对应文件夹
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
panic("文件夹不存在:" + filepath.Dir(path))
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
index := 1
|
||||||
|
for it := s.Content.Iterator(); it.Has(); {
|
||||||
|
content := it.Next()
|
||||||
|
content.Index = index
|
||||||
|
index++
|
||||||
|
_, err := writer.WriteString(content.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writer.Flush()
|
||||||
|
}
|
75
srt_test.go
Normal file
75
srt_test.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package ltc
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSRTContent_String(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Index int
|
||||||
|
Start int
|
||||||
|
End int
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"srtContent String()", fields{1, 10, 20, "test"},
|
||||||
|
"1\n00:00:00,010 --> 00:00:00,020\ntest\n\n"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := &SRTContent{
|
||||||
|
Index: tt.fields.Index,
|
||||||
|
Start: tt.fields.Start,
|
||||||
|
End: tt.fields.End,
|
||||||
|
Text: tt.fields.Text,
|
||||||
|
}
|
||||||
|
if got := s.String(); got != tt.want {
|
||||||
|
t.Errorf("String() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLrcToSrt(t *testing.T) {
|
||||||
|
lrc := `[ar:artist]
|
||||||
|
[al:album]
|
||||||
|
[ti:title]
|
||||||
|
[by:author]
|
||||||
|
[00:24.83] 天涯的尽头 有谁去过
|
||||||
|
[00:28.53] 山水优雅着 保持沉默
|
||||||
|
[00:32.20] 我们的青春却热闹很多
|
||||||
|
[00:35.38] 而且是谁都 不准偷
|
||||||
|
`
|
||||||
|
content := []string{
|
||||||
|
"天涯的尽头 有谁去过", "山水优雅着 保持沉默", "我们的青春却热闹很多", "而且是谁都 不准偷",
|
||||||
|
}
|
||||||
|
l := ParseLRC(lrc)
|
||||||
|
srt := LrcToSrt(l)
|
||||||
|
if srt.Title != "title" {
|
||||||
|
t.Errorf("SRT Title=%s, want=%s", srt.Title, "title")
|
||||||
|
}
|
||||||
|
if srt.Artist != "artist" {
|
||||||
|
t.Errorf("SRT Artist=%s, want=%s", srt.Artist, "altist")
|
||||||
|
}
|
||||||
|
index := 0
|
||||||
|
for it := srt.Content.Iterator(); it.Has(); {
|
||||||
|
c := it.Next().Text
|
||||||
|
if c != content[index] {
|
||||||
|
t.Errorf("srt Text=%s, want=%s", c, content[index])
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSRT_MergeStack(t *testing.T) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSRT_MergeUp(t *testing.T) {
|
||||||
|
|
||||||
|
}
|
||||||
|
func TestSRT_MergeBottom(t *testing.T) {
|
||||||
|
|
||||||
|
}
|
27
util.go
27
util.go
|
@ -1,13 +1,22 @@
|
||||||
package main
|
package ltc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"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 根据分,秒,毫秒 计算出对应的毫秒值
|
// Time2Millisecond 根据分,秒,毫秒 计算出对应的毫秒值
|
||||||
func Time2Millisecond(m, s, ms int) int {
|
func time2Millisecond(m, s, ms int) int {
|
||||||
t := m*60 + s
|
t := m*60 + s
|
||||||
t *= 1000
|
t *= 1000
|
||||||
t += ms
|
t += ms
|
||||||
|
@ -15,7 +24,7 @@ func Time2Millisecond(m, s, ms int) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Millisecond2Time 根据毫秒值计算出对应的 时,分,秒,毫秒形式的时间值
|
// Millisecond2Time 根据毫秒值计算出对应的 时,分,秒,毫秒形式的时间值
|
||||||
func Millisecond2Time(millisecond int) (h, m, s, ms int) {
|
func millisecond2Time(millisecond int) (h, m, s, ms int) {
|
||||||
ms = millisecond % 1000
|
ms = millisecond % 1000
|
||||||
|
|
||||||
s = millisecond / 1000
|
s = millisecond / 1000
|
||||||
|
@ -36,7 +45,9 @@ func ReadFile(name string) string {
|
||||||
fmt.Printf("打开文件失败:%v\n", err)
|
fmt.Printf("打开文件失败:%v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func(file *os.File) {
|
||||||
|
_ = file.Close()
|
||||||
|
}(file)
|
||||||
data, err := io.ReadAll(file)
|
data, err := io.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("读取文件失败:%v\n", err)
|
fmt.Printf("读取文件失败:%v\n", err)
|
||||||
|
@ -46,12 +57,14 @@ func ReadFile(name string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteFile(name string, data string) {
|
func WriteFile(name string, data string) {
|
||||||
file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, os.ModePerm)
|
file, err := os.Create(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("保存文件失败:%v\n", err)
|
fmt.Printf("创建结果文件失败:%v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func(file *os.File) {
|
||||||
|
_ = file.Close()
|
||||||
|
}(file)
|
||||||
nw, err := file.WriteString(data)
|
nw, err := file.WriteString(data)
|
||||||
if err != nil || nw < len(data) {
|
if err != nil || nw < len(data) {
|
||||||
fmt.Printf("保存文件失败:%v\n", err)
|
fmt.Printf("保存文件失败:%v\n", err)
|
||||||
|
|
80
util_test.go
Normal file
80
util_test.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package ltc
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestMillisecond2Time(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
millisecond int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantH int
|
||||||
|
wantM int
|
||||||
|
wantS int
|
||||||
|
wantMs int
|
||||||
|
}{
|
||||||
|
{"ms_0", args{0}, 0, 0, 0, 0},
|
||||||
|
{"ms_100", args{100}, 0, 0, 0, 100},
|
||||||
|
{"ms_1000", args{1000}, 0, 0, 1, 0},
|
||||||
|
{"ms_1100", args{1100}, 0, 0, 1, 100},
|
||||||
|
{"ms_60000", args{60000}, 0, 1, 0, 0},
|
||||||
|
{"ms_3600000", args{3600000}, 1, 0, 0, 0},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotH, gotM, gotS, gotMs := millisecond2Time(tt.args.millisecond)
|
||||||
|
if gotH != tt.wantH {
|
||||||
|
t.Errorf("Millisecond2Time() gotH = %v, want %v", gotH, tt.wantH)
|
||||||
|
}
|
||||||
|
if gotM != tt.wantM {
|
||||||
|
t.Errorf("Millisecond2Time() gotM = %v, want %v", gotM, tt.wantM)
|
||||||
|
}
|
||||||
|
if gotS != tt.wantS {
|
||||||
|
t.Errorf("Millisecond2Time() gotS = %v, want %v", gotS, tt.wantS)
|
||||||
|
}
|
||||||
|
if gotMs != tt.wantMs {
|
||||||
|
t.Errorf("Millisecond2Time() gotMs = %v, want %v", gotMs, tt.wantMs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTime2Millisecond(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
m int
|
||||||
|
s int
|
||||||
|
ms int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"0:0.0", args{0, 0, 0}, 0},
|
||||||
|
{"0:0.1", args{0, 0, 1}, 1},
|
||||||
|
{"0:0.999", args{0, 0, 999}, 999},
|
||||||
|
{"0:1.0", args{0, 1, 0}, 1000},
|
||||||
|
{"0:1.999", args{0, 1, 999}, 1999},
|
||||||
|
{"1:0.0", args{1, 0, 0}, 60000},
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
t.Errorf("Time2Millisecond() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkTime2Millisecond(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
time2Millisecond(999, 999, 999)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMillisecond2Time(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
millisecond2Time(9999999999)
|
||||||
|
}
|
||||||
|
}
|
Reference in a new issue