diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index aef47b9..863c15a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -13,14 +13,25 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup Go environment - uses: actions/setup-go@v3.0.0 + - name: Set up Go + uses: actions/setup-go@v2 with: - go-version: '>=1.18' + go-version: 1.17 check-latest: true - name: Build run: go build -v ./... + #执行测试 + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up GO + uses: actions/setup-go@v2 + with: + go-version: 1.17 + check-latest: true - name: Test run: go test -run=Test diff --git a/.gitignore b/.gitignore index 53ceaab..5bb3f1e 100644 --- a/.gitignore +++ b/.gitignore @@ -87,7 +87,6 @@ fabric.properties # Dependency directories (remove the comment below to include it) # vendor/ -*.srt -*.lrc -*.ass +/example/*.srt +/example/*.lrc /temp diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 4fcc028..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -ltc \ No newline at end of file diff --git a/.idea/ltc.iml b/.idea/lrc2srt.iml similarity index 71% rename from .idea/ltc.iml rename to .idea/lrc2srt.iml index 5e764c4..a41de17 100644 --- a/.idea/ltc.iml +++ b/.idea/lrc2srt.iml @@ -2,7 +2,9 @@ - + + + diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 283b9b4..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index d70defd..19b0b7d 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 26733ff..a59a074 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ -# LrcToCaptons +# LrcToSrt [![Build](https://github.com/Hami-Lemon/LrcToSrt/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/Hami-Lemon/LrcToSrt/actions/workflows/go.yml) -用于将LRC歌词文件转换成ASS、SRT字幕文件 +用于将LRC歌词文件转换成SRT字幕文件 ## 功能 - [x] lrc文件转换成srt文件 -- [x] lrc文件转换成ass文件 -- [x] 从网易云音乐或QQ音乐上获取歌词,并转换。 -- [x] 从网易云音乐或QQ音乐上下载歌词。 +- [x] 从网易云音乐或QQ音乐上获取歌词,并转换成srt文件 +- [x] 从网易云音乐或QQ音乐上下载歌词 ## 下载 @@ -17,25 +16,21 @@ ## 开始使用 -```text -LrcToCaptions(ltc) 将LRC歌词文件转换成字幕文件。 -ltc version: "0.3.4" (build 2022.03.30) +``` +Usage: + D:\ProgrameStudy\lrc2srt\lts.exe [OPTIONS] -用法:ltc [options] OutputFile +Application Options: + -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 获取版本信息 -options: - - -d 设置该选项时,只下载歌词,而无需转换。 - -f value - 转换成的字幕文件格式,可选值:ass(默认),srt - -i string - 歌词来源,可以是歌词对应的歌曲id,也可以是歌词文件 - -m value - 设置歌词原文和译文的合并模式,可选值:1(默认),2,3。 - -s string - 选择从网易云还是QQ音乐上获取歌词,可选值:163(默认),qq。 (default "163") - -v 获取当前程序版本信息。 - -h 显示帮助信息。 +Help Options: + -h, --help Show this help message ``` ### 获取歌曲id @@ -150,18 +145,3 @@ lts -i 003FJlVU1rxjv8 -m 2 -s qq "ふわふわ时间.srt" ## 结束时间处理策略 因为在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 . -``` - diff --git a/ass.go b/ass.go deleted file mode 100644 index c130679..0000000 --- a/ass.go +++ /dev/null @@ -1,125 +0,0 @@ -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 -} diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..f6cf966 --- /dev/null +++ b/build.bat @@ -0,0 +1,3 @@ +echo off +@REM forceposix 表示在windows上参数也为linux风格,即以“-”开头 +go build -tags="forceposix" -ldflags "-s -w" -o lts.exe . diff --git a/cloudlyric.go b/cloudlyric.go index e46127c..baac27a 100644 --- a/cloudlyric.go +++ b/cloudlyric.go @@ -1,10 +1,11 @@ -package ltc +package main import ( "encoding/json" "fmt" "net/http" "net/url" + "os" ) type CloudLyricBase struct { @@ -37,15 +38,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", CHROME_UA) + req.Header.Add("User-Agent", ChromeUA) resp, err := client.Do(req) if err != nil { fmt.Printf("网络错误:%v\n", err) - panic("网络异常,获取失败。") + os.Exit(1) } if resp == nil || resp.StatusCode != http.StatusOK { fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode) - panic("获取失败,未能正确获取到数据") + os.Exit(1) } defer resp.Body.Close() @@ -53,7 +54,7 @@ func Get163Lyric(id string) (lyric, tLyric string) { err = json.NewDecoder(resp.Body).Decode(&cloudLyric) if cloudLyric.Sgc { fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", cloudLyric) - panic("id错误,获取歌词失败") + os.Exit(1) } return cloudLyric.Lrc.Lyric, cloudLyric.TLyric.Lyric } diff --git a/cloudlyric_test.go b/cloudlyric_test.go deleted file mode 100644 index 0d82d25..0000000 --- a/cloudlyric_test.go +++ /dev/null @@ -1,22 +0,0 @@ -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) - } - }) - } -} diff --git a/glist/linkedlist.go b/glist/linkedlist.go deleted file mode 100644 index b4c121a..0000000 --- a/glist/linkedlist.go +++ /dev/null @@ -1,268 +0,0 @@ -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 -} diff --git a/glist/linkedlist_test.go b/glist/linkedlist_test.go deleted file mode 100644 index f9558c5..0000000 --- a/glist/linkedlist_test.go +++ /dev/null @@ -1,209 +0,0 @@ -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) - } - }) - } -} diff --git a/glist/list.go b/glist/list.go deleted file mode 100644 index ca26569..0000000 --- a/glist/list.go +++ /dev/null @@ -1,50 +0,0 @@ -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 -} diff --git a/go.mod b/go.mod index d2e7b41..77c5a2a 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ -module github.com/Hami-Lemon/ltc +module github.com/hami_lemon/lrc2srt -go 1.18 +go 1.17 + +require github.com/jessevdk/go-flags v1.5.0 + +require golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect diff --git a/go.sum b/go.sum index e69de29..df31363 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,4 @@ +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= diff --git a/lrc.go b/lrc.go deleted file mode 100644 index 049d304..0000000 --- a/lrc.go +++ /dev/null @@ -1,128 +0,0 @@ -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 -} diff --git a/lrc_test.go b/lrc_test.go deleted file mode 100644 index 2104117..0000000 --- a/lrc_test.go +++ /dev/null @@ -1,68 +0,0 @@ -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) - } - }) - } -} diff --git a/lrctocaptions/flag.go b/lrctocaptions/flag.go deleted file mode 100644 index 7e93535..0000000 --- a/lrctocaptions/flag.go +++ /dev/null @@ -1,196 +0,0 @@ -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) -} diff --git a/lrctocaptions/ltc.go b/lrctocaptions/ltc.go deleted file mode 100644 index ee61fff..0000000 --- a/lrctocaptions/ltc.go +++ /dev/null @@ -1,122 +0,0 @@ -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("") - } -} diff --git a/lts.go b/lts.go new file mode 100644 index 0000000..0dcb32e --- /dev/null +++ b/lts.go @@ -0,0 +1,337 @@ +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() +} diff --git a/lts_test.go b/lts_test.go new file mode 100644 index 0000000..c1762f9 --- /dev/null +++ b/lts_test.go @@ -0,0 +1,25 @@ +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) + } +} diff --git a/qqlyric.go b/qqlyric.go index 4fe3ad6..a764c74 100644 --- a/qqlyric.go +++ b/qqlyric.go @@ -1,4 +1,4 @@ -package ltc +package main import ( "compress/gzip" @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "os" ) /** @@ -42,17 +43,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", CHROME_UA) + req.Header.Add("User-Agent", ChromeUA) req.Header.Add("accept-encoding", "gzip") resp, err := client.Do(req) if err != nil { fmt.Printf("网络错误:%v\n", err) - panic("网络异常,请求失败。") + os.Exit(1) } if resp == nil || resp.StatusCode != http.StatusOK { fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode) - panic("获取失败,未能正确获取到数据") + os.Exit(1) } defer resp.Body.Close() @@ -62,7 +63,7 @@ func GetQQLyric(id string) (lyric, tLyric string) { err = json.NewDecoder(reader).Decode(&qqLyric) if qqLyric.RetCode != 0 { fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", qqLyric) - panic("id错误,获取歌词失败。") + os.Exit(1) } return qqLyric.Lyric, qqLyric.Trans } diff --git a/qqlyric_test.go b/qqlyric_test.go deleted file mode 100644 index d339d6e..0000000 --- a/qqlyric_test.go +++ /dev/null @@ -1,22 +0,0 @@ -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) - } - }) - } -} diff --git a/srt.go b/srt.go deleted file mode 100644 index e7e361b..0000000 --- a/srt.go +++ /dev/null @@ -1,202 +0,0 @@ -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() -} diff --git a/srt_test.go b/srt_test.go deleted file mode 100644 index 921af88..0000000 --- a/srt_test.go +++ /dev/null @@ -1,75 +0,0 @@ -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) { - -} diff --git a/util.go b/util.go index 43d026e..8d99dd5 100644 --- a/util.go +++ b/util.go @@ -1,22 +1,13 @@ -package ltc +package main 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 @@ -24,7 +15,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 diff --git a/util_test.go b/util_test.go index 12c5b16..4690dda 100644 --- a/util_test.go +++ b/util_test.go @@ -1,4 +1,4 @@ -package ltc +package main 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) } }