From 5f49bde3ae4b2c79cedb1dc5c31a764417bfa29a Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Mon, 28 Mar 2022 17:38:09 +0800 Subject: [PATCH 01/16] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=B3=9B=E5=9E=8B=E7=9A=84=E9=93=BE=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- glist/linkedlist.go | 251 +++++++++++++++++++++++++++++++++++++++ glist/linkedlist_test.go | 209 ++++++++++++++++++++++++++++++++ glist/list.go | 48 ++++++++ 3 files changed, 508 insertions(+) create mode 100644 glist/linkedlist.go create mode 100644 glist/linkedlist_test.go create mode 100644 glist/list.go diff --git a/glist/linkedlist.go b/glist/linkedlist.go new file mode 100644 index 0000000..fdacafe --- /dev/null +++ b/glist/linkedlist.go @@ -0,0 +1,251 @@ +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]{next: l.First} +} + +type LinkedListIterator[E any] struct { + 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.") + } + l.next = e.Next + return e.Element +} diff --git a/glist/linkedlist_test.go b/glist/linkedlist_test.go new file mode 100644 index 0000000..b05b50b --- /dev/null +++ b/glist/linkedlist_test.go @@ -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) + } + }) + } +} diff --git a/glist/list.go b/glist/list.go new file mode 100644 index 0000000..0f1da8d --- /dev/null +++ b/glist/list.go @@ -0,0 +1,48 @@ +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] +} + +// 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 +} From c9b1b38c0355f5db6a8711e21202d595860702b9 Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Mon, 28 Mar 2022 18:26:40 +0800 Subject: [PATCH 02/16] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=A1=B9=E7=9B=AE,?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BA=86=E9=A1=B9=E7=9B=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- cloudlyric.go | 11 +- go.mod | 4 +- lrc.go | 128 +++++++++++++++++++ lts.go | 337 -------------------------------------------------- lts/main.go | 124 +++++++++++++++++++ lts_test.go | 25 ---- qqlyric.go | 11 +- srt.go | 173 ++++++++++++++++++++++++++ util.go | 15 ++- util_test.go | 10 +- 11 files changed, 456 insertions(+), 386 deletions(-) create mode 100644 lrc.go delete mode 100644 lts.go create mode 100644 lts/main.go delete mode 100644 lts_test.go create mode 100644 srt.go diff --git a/.gitignore b/.gitignore index 5bb3f1e..96d2cfd 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,6 @@ fabric.properties # Dependency directories (remove the comment below to include it) # vendor/ -/example/*.srt -/example/*.lrc +*.srt +*.lrc /temp diff --git a/cloudlyric.go b/cloudlyric.go index baac27a..0cd1fac 100644 --- a/cloudlyric.go +++ b/cloudlyric.go @@ -1,11 +1,10 @@ -package main +package lrc2srt import ( "encoding/json" "fmt" "net/http" "net/url" - "os" ) type CloudLyricBase struct { @@ -38,15 +37,15 @@ func Get163Lyric(id string) (lyric, tLyric string) { req, _ := http.NewRequest("GET", api, nil) //必须设置Referer,否则会请求失败 req.Header.Add("Referer", "https://music.163.com") - req.Header.Add("User-Agent", ChromeUA) + req.Header.Add("User-Agent", CHROME_UA) resp, err := client.Do(req) if err != nil { fmt.Printf("网络错误:%v\n", err) - os.Exit(1) + panic("网络异常,获取失败。") } if resp == nil || resp.StatusCode != http.StatusOK { fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode) - os.Exit(1) + panic("获取失败,未能正确获取到数据") } defer resp.Body.Close() @@ -54,7 +53,7 @@ func Get163Lyric(id string) (lyric, tLyric string) { err = json.NewDecoder(resp.Body).Decode(&cloudLyric) if cloudLyric.Sgc { fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", cloudLyric) - os.Exit(1) + panic("id错误,获取歌词失败") } return cloudLyric.Lrc.Lyric, cloudLyric.TLyric.Lyric } diff --git a/go.mod b/go.mod index 77c5a2a..20b43fd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module github.com/hami_lemon/lrc2srt +module github.com/Hami-Lemon/lrc2srt -go 1.17 +go 1.18 require github.com/jessevdk/go-flags v1.5.0 diff --git a/lrc.go b/lrc.go new file mode 100644 index 0000000..30257b3 --- /dev/null +++ b/lrc.go @@ -0,0 +1,128 @@ +package lrc2srt + +import ( + "regexp" + "strconv" + "strings" + + "github.com/Hami-Lemon/lrc2srt/glist" +) + +type LRCNode struct { + //歌词出现的时间,单位毫秒 + time int + //歌词内容 + content string +} + +type LRC struct { + //歌曲名 + Title string + //歌手名 + Artist string + //专辑名 + Album string + //歌词作者 + Author string + //歌词列表 + LrcList glist.Queue[*LRCNode] +} + +func ParseLRC(src string) *LRC { + if src == "" { + return nil + } + //标准的LRC文件为一行一句歌词 + lyrics := strings.Split(src, "\n") + //标识标签的正则 [ar:A-SOUL]形式 + infoRegx := regexp.MustCompile(`^\[([a-z]+):([\s\S]*)]`) + + lrc := &LRC{LrcList: glist.NewLinkedList[*LRCNode]()} + //解析标识信息 + for { + if len(lyrics) == 0 { + break + } + l := lyrics[0] + //根据正则表达式进行匹配 + info := infoRegx.FindStringSubmatch(l) + //标识信息位于歌词信息前面,当出现未匹配成功时,即可退出循环 + if info != nil { + //info 中为匹配成功的字符串和 子组合(正则表达式中括号包裹的部分) + //例如,对于标识信息:[ar:A-SOUL],info中的数据为[[ar:A-SOUL], ar, A-SOUL] + key := info[1] + switch key { + case "ar": + //歌手名 + if len(info) == 3 { + lrc.Artist = info[2] + } + case "ti": + //歌曲名 + if len(info) == 3 { + lrc.Title = info[2] + } + case "al": + //专辑名 + if len(info) == 3 { + lrc.Album = info[2] + } + case "by": + //歌词作者 + if len(info) == 3 { + lrc.Author = info[2] + } + } + lyrics = lyrics[1:] + } else { + break + } + } + //歌词信息的正则,"[00:10.222]超级的敏感"或“[00:10:222]超级的敏感”或“[00:10]超级的敏感”或“[00:10.22]超级的敏感”或“[00:10:22]超级的敏感” + lyricRegx := regexp.MustCompile(`\[(\d\d):(\d\d)([.:]\d{2,3})?]([\s\S]+)`) + for _, l := range lyrics { + content := lyricRegx.FindStringSubmatch(l) + if content != nil { + node := SplitLyric(content[1:]) + if node != nil { + lrc.LrcList.PushBack(node) + } + } + } + return lrc +} + +// SplitLyric 对分割出来的歌词信息进行解析 +func SplitLyric(src []string) *LRCNode { + minute, err := strconv.Atoi(src[0]) + second, err := strconv.Atoi(src[1]) + if err != nil { + panic("错误的时间格式:" + strings.Join(src, " ")) + return nil + } + millisecond, content := 0, "" + + _len := len(src) + if _len == 3 { + //歌词信息没有毫秒值 + content = src[2] + } else if _len == 4 { + content = src[3] + //毫秒字符串的第一个字符是 "." 或 ":",需要去掉 + ms := src[2][1:] + millisecond, err = strconv.Atoi(ms) + //QQ音乐歌词文件中,毫秒值只有两位,需要特殊处理一下 + if len(ms) == 2 { + millisecond *= 10 + } + if err != nil { + panic("错误的时间格式:" + strings.Join(src, " ")) + return nil + } + } + lrcNode := &LRCNode{ + time: time2Millisecond(minute, second, millisecond), + content: content, + } + return lrcNode +} diff --git a/lts.go b/lts.go deleted file mode 100644 index 0dcb32e..0000000 --- a/lts.go +++ /dev/null @@ -1,337 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "net/http" - "os" - "regexp" - "sort" - "strconv" - "strings" - "time" - - "github.com/jessevdk/go-flags" -) - -type SRTContent struct { - //序号,从1开始 - Index int - //开始时间,单位毫秒 - Start int - //结束时间,单位毫秒 - End int - //歌词内容 - Text string -} -type SRT struct { - //歌曲名 - Title string - //歌手名 未指定文件名是,文件名格式为:歌曲名-歌手名.srt - Artist string - Content []*SRTContent -} - -// Option 运行时传入的选项 -type Option struct { - Id string `short:"i" long:"id" description:"歌曲的id,网易云和QQ音乐均可。"` - Input string `short:"I" long:"input" description:"需要转换的LRC文件路径。"` - Source string `short:"s" long:"source" description:"当设置id时有效,指定从网易云(163)还是QQ音乐(qq)上获取歌词。" default:"163" choice:"163" choice:"qq" choice:"QQ"` - Download bool `short:"d" long:"download" description:"只下载歌词,而不进行解析。"` - Mode int `short:"m" long:"mode" default:"1" description:"原文和译文的排列模式,可选值有:[1] [2] [3]" choice:"1" choice:"2" choice:"3"` - Version bool `short:"v" long:"version" description:"获取版本信息"` - Output string `no-flag:""` -} - -const ( - // VERSION 当前版本 - VERSION = `"0.1.1" (build 2022.03.05)` -) - -var ( - ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36" - client = http.Client{} - opt Option -) - -func main() { - args, err := flags.Parse(&opt) - if err != nil { - os.Exit(0) - } - //显示版本信息 - if opt.Version { - fmt.Printf("LrcToSrt(lts) version %s\n", VERSION) - os.Exit(0) - } - //获取保存的文件名 - if len(args) != 0 { - opt.Output = args[0] - } - - //获取歌词,lyric为原文歌词,tranLyric为译文歌词 - var lyric, tranLyric string - if opt.Id != "" { - if opt.Source != "163" { - lyric, tranLyric = GetQQLyric(opt.Id) - } else { - lyric, tranLyric = Get163Lyric(opt.Id) - } - //下载歌词 - if opt.Download { - //对文件名进行处理 - o := opt.Output - if o == "" { - o = opt.Id + ".lrc" - } else if !strings.HasSuffix(o, ".lrc") { - o += ".lrc" - } - WriteFile(o, lyric) - if tranLyric != "" { - WriteFile("tran_"+o, tranLyric) - } - fmt.Println("下载歌词完成!") - return - } - } else if opt.Input != "" { - //从文件中获取歌词 - if !strings.HasSuffix(opt.Input, ".lrc") { - fmt.Println("Error: 不支持的格式,目前只支持lrc歌词文件。") - os.Exit(1) - } - lyric = ReadFile(opt.Input) - if lyric == "" { - fmt.Println("获取歌词失败,文件内容为空。") - os.Exit(1) - } - } else { - fmt.Println("Error: 请指定需要转换的歌词。") - os.Exit(1) - } - //原文和译文作为两条歌词流信息分开保存,但最终生成的srt文件会同时包含两个信息 - lyricSRT, tranLyricSRT := Lrc2Srt(lyric), Lrc2Srt(tranLyric) - SaveSRT(lyricSRT, tranLyricSRT, opt.Output) -} - -// SaveSRT 保存数据为SRT文件 -func SaveSRT(srt *SRT, tranSrt *SRT, name string) { - if tranSrt == nil { - //没有译文时,用一个空的对象的代替,减少nil判断 - tranSrt = &SRT{Content: make([]*SRTContent, 0)} - //因为没有译文,所以mode选项无效,设为2之后,后面不用做多余判断 - opt.Mode = 2 - } - //处理结果文件的文件名 - if name == "" { - title := srt.Title - if title != "" { - //以歌曲名命名 - name = fmt.Sprintf("%s.srt", title) - } else if opt.Id != "" { - //以歌曲的id命名 - name = fmt.Sprintf("%s.srt", opt.Id) - } else if opt.Input != "" { - //以LRC文件的文件名命名 - name = fmt.Sprintf("%s.srt", opt.Input[:len(opt.Input)-4]) - } else { - //以当前时间的毫秒值命名 - name = fmt.Sprintf("%d.srt", time.Now().Unix()) - } - } else if !strings.HasSuffix(name, ".srt") { - name += ".srt" - } - - file, err := os.Create(name) - if err != nil { - fmt.Printf("创建结果文件失败:%v\n", err) - os.Exit(1) - } - defer func(file *os.File) { - _ = file.Close() - }(file) - //6KB的缓存区,大部分歌词生成的SRT文件均在4-6kb左右 - writer := bufio.NewWriterSize(file, 1024*6) - - /* - 原文和译文歌词的排列方式,因为原文歌词中可能包含一些非歌词信息, - 例如作词者,作曲者等,而在译文歌词中却可能不包含这些 - */ - //srt的序号 - index := 1 - switch opt.Mode { - case 1: - //将两个歌词合并成一个新的数组 - size := len(srt.Content) + len(tranSrt.Content) - temp := make([]*SRTContent, size, size) - i := 0 - for _, v := range srt.Content { - temp[i] = v - i++ - } - for _, v := range tranSrt.Content { - temp[i] = v - i++ - } - //按开始时间进行排序,使用SliceStable确保一句歌词的原文在译文之前 - sort.SliceStable(temp, func(i, j int) bool { - return temp[i].Start < temp[j].Start - }) - //写入文件 - for i, v := range temp { - v.Index = i + 1 - _, _ = writer.WriteString(v.String()) - } - case 2: - //原文在上,译文在下 - for _, item := range srt.Content { - item.Index = index - _, _ = writer.WriteString(item.String()) - index++ - } - - for _, item := range tranSrt.Content { - item.Index = index - _, _ = writer.WriteString(item.String()) - index++ - } - case 3: - //译文在上,原文在下 - for _, item := range tranSrt.Content { - item.Index = index - _, _ = writer.WriteString(item.String()) - index++ - } - for _, item := range srt.Content { - item.Index = index - _, _ = writer.WriteString(item.String()) - index++ - } - } - err = writer.Flush() - if err != nil { - fmt.Printf("保存结果失败:%v\n", err) - } else { - fmt.Printf("转换文件完成,保存结果为:%s\n", name) - } -} - -// Lrc2Srt 将原始个LRC字符串歌词解析SRT对象 -func Lrc2Srt(src string) *SRT { - if src == "" { - return nil - } - //标准的LRC文件为一行一句歌词 - lyrics := strings.Split(src, "\n") - //标识标签的正则 [ar:A-SOUL]形式 - infoRegx := regexp.MustCompile(`^\[([a-z]+):([\s\S]*)]`) - - srt := &SRT{Content: make([]*SRTContent, 0, len(lyrics))} - //解析标识信息 - for { - if len(lyrics) == 0 { - break - } - l := lyrics[0] - //根据正则表达式进行匹配 - info := infoRegx.FindStringSubmatch(l) - //标识信息位于歌词信息前面,当出现未匹配成功时,即可退出循环 - if info != nil { - //info 中为匹配成功的字符串和 子组合(正则表达式中括号包裹的部分) - //例如,对于标识信息:[ar:A-SOUL],info中的数据为[[ar:A-SOUL] ar A-SOUL] - key := info[1] - switch key { - case "ar": - //歌手名 - if len(info) == 3 { - srt.Artist = info[2] - } - case "ti": - //歌曲名 - if len(info) == 3 { - srt.Title = info[2] - } - } - lyrics = lyrics[1:] - } else { - break - } - } - //歌词信息的正则,"[00:10.222]超级的敏感"或“[00:10:222]超级的敏感”或“[00:10]超级的敏感”或“[00:10.22]超级的敏感”或“[00:10:22]超级的敏感” - lyricRegx := regexp.MustCompile(`\[(\d\d):(\d\d)([.:]\d{2,3})?]([\s\S]+)`) - index := 0 - for _, l := range lyrics { - content := lyricRegx.FindStringSubmatch(l) - if content != nil { - c := SplitLyric(content[1:]) - if c != nil { - if index != 0 { - //前一条字幕的结束时间为当前字幕开始的时间 - srt.Content[index-1].End = c.Start - } - srt.Content = append(srt.Content, c) - index++ - } - } - } - //最后一条字幕 - last := srt.Content[index-1] - //最后一条字幕的结束时间为其开始时间 + 10秒 - last.End = last.Start + 10000 - return srt -} - -// SplitLyric 对分割出来的歌词信息进行解析 -func SplitLyric(src []string) *SRTContent { - minute, err := strconv.Atoi(src[0]) - second, err := strconv.Atoi(src[1]) - if err != nil { - fmt.Printf("错误的时间格式:%s\n", src) - return nil - } - millisecond, content := 0, "" - - _len := len(src) - if _len == 3 { - //歌词信息没有毫秒值 - content = src[2] - } else if _len == 4 { - content = src[3] - //字符串的第一个字符是 "." 或 ":" - ms := src[2][1:] - millisecond, err = strconv.Atoi(ms) - //QQ音乐歌词文件中,毫秒值只有两位,需要特殊处理一下 - if len(ms) == 2 { - millisecond *= 10 - } - if err != nil { - fmt.Printf("错误的时间格式:%s\n", src) - return nil - } - } - - srtContent := &SRTContent{} - srtContent.Start = Time2Millisecond(minute, second, millisecond) - srtContent.Text = content - return srtContent -} - -//返回SRT文件中,一句字幕的字符串表示形式 -/** -1 -00:00:01,111 --> 00:00:10,111 -字幕 - -*/ -func (s *SRTContent) String() string { - builder := strings.Builder{} - builder.WriteString(strconv.Itoa(s.Index)) - builder.WriteByte('\n') - sh, sm, ss, sms := Millisecond2Time(s.Start) - eh, em, es, ems := Millisecond2Time(s.End) - builder.WriteString(fmt.Sprintf("%02d:%02d:%02d,%03d --> %02d:%02d:%02d,%03d\n", - sh, sm, ss, sms, eh, em, es, ems)) - builder.WriteString(s.Text) - builder.WriteString("\n\n") - - return builder.String() -} diff --git a/lts/main.go b/lts/main.go new file mode 100644 index 0000000..f17c1bd --- /dev/null +++ b/lts/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "fmt" + "os" + "strings" + "time" + + l2s "github.com/Hami-Lemon/lrc2srt" + "github.com/jessevdk/go-flags" +) + +type Option struct { + Id string `short:"i" long:"id" description:"歌曲的id,网易云和QQ音乐均可。"` + Input string `short:"I" long:"input" description:"需要转换的LRC文件路径。"` + Source string `short:"s" long:"source" description:"当设置id时有效,指定从网易云(163)还是QQ音乐(qq)上获取歌词。" default:"163" choice:"163" choice:"qq" choice:"QQ"` + Download bool `short:"d" long:"download" description:"只下载歌词,而不进行解析。"` + Mode int `short:"m" long:"mode" default:"1" description:"原文和译文的排列模式,可选值有:[1] [2] [3]" choice:"1" choice:"2" choice:"3"` + Version bool `short:"v" long:"version" description:"获取版本信息"` + Output string `no-flag:""` +} + +const ( + // VERSION 当前版本 + VERSION = `"0.2.0" (build 2022.03.28)` +) + +var ( + opt Option +) + +func main() { + //TODO 支持转ass文件 + args, err := flags.Parse(&opt) + if err != nil { + os.Exit(0) + } + //显示版本信息 + if opt.Version { + fmt.Printf("LrcToSrt(lts) version %s\n", VERSION) + return + } + //获取保存的文件名 + if len(args) != 0 { + opt.Output = args[0] + } + + //获取歌词,lyric为原文歌词,tranLyric为译文歌词 + var lyric, tranLyric string + if opt.Id != "" { + if opt.Source != "163" { + lyric, tranLyric = l2s.GetQQLyric(opt.Id) + } else { + lyric, tranLyric = l2s.Get163Lyric(opt.Id) + } + //下载歌词 + if opt.Download { + //对文件名进行处理 + o := opt.Output + if o == "" { + o = opt.Id + ".lrc" + } else if !strings.HasSuffix(o, ".lrc") { + o += ".lrc" + } + l2s.WriteFile(o, lyric) + if tranLyric != "" { + l2s.WriteFile("tran_"+o, tranLyric) + } + fmt.Println("下载歌词完成!") + return + } + } else if opt.Input != "" { + //从文件中获取歌词 + if !strings.HasSuffix(opt.Input, ".lrc") { + fmt.Println("Error: 不支持的格式,目前只支持lrc歌词文件。") + os.Exit(1) + } + lyric = l2s.ReadFile(opt.Input) + if lyric == "" { + fmt.Println("获取歌词失败,文件内容为空。") + os.Exit(1) + } + } else { + fmt.Println("Error: 请指定需要转换的歌词。") + os.Exit(1) + } + lrc, lrcT := l2s.ParseLRC(lyric), l2s.ParseLRC(tranLyric) + srt, srtT := l2s.LrcToSrt(lrc), l2s.LrcToSrt(lrcT) + if srtT != nil { + var mode l2s.SRTMergeMode + switch opt.Mode { + case 1: + mode = l2s.SRT_MERGE_MODE_STACK + case 2: + mode = l2s.SRT_MERGE_MODE_UP + case 3: + mode = l2s.SRT_MERGE_MODE_BOTTOM + } + srt.Merge(srtT, mode) + } + name := opt.Output + if name == "" { + title := srt.Title + if title != "" { + //以歌曲名命名 + name = fmt.Sprintf("%s.srt", title) + } else if opt.Id != "" { + //以歌曲的id命名 + name = fmt.Sprintf("%s.srt", opt.Id) + } else if opt.Input != "" { + //以LRC文件的文件名命名 + name = fmt.Sprintf("%s.srt", opt.Input[:len(opt.Input)-4]) + } else { + //以当前时间的毫秒值命名 + name = fmt.Sprintf("%d.srt", time.Now().Unix()) + } + } else if !strings.HasSuffix(name, ".srt") { + name += ".srt" + } + if err = srt.WriteFile(name); err != nil { + fmt.Println("出现错误,保存失败") + panic(err.Error()) + } +} diff --git a/lts_test.go b/lts_test.go deleted file mode 100644 index c1762f9..0000000 --- a/lts_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import "testing" - -func BenchmarkSRTContent_String(b *testing.B) { - srt := SRTContent{ - Index: 10, - Start: 100, - End: 200, - Text: "言语 不起作用,想看到 具体行动", - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = srt.String() - } -} - -func BenchmarkLrc2Srt(b *testing.B) { - id := "28891491" - lyric, lyricT := Get163Lyric(id) - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, _ = Lrc2Srt(lyric), Lrc2Srt(lyricT) - } -} diff --git a/qqlyric.go b/qqlyric.go index a764c74..a3acfba 100644 --- a/qqlyric.go +++ b/qqlyric.go @@ -1,4 +1,4 @@ -package main +package lrc2srt import ( "compress/gzip" @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "net/url" - "os" ) /** @@ -43,17 +42,17 @@ func GetQQLyric(id string) (lyric, tLyric string) { req, _ := http.NewRequest("GET", api, nil) //必须设置Referer,否则会请求失败 req.Header.Add("Referer", "https://y.qq.com") - req.Header.Add("User-Agent", ChromeUA) + req.Header.Add("User-Agent", CHROME_UA) req.Header.Add("accept-encoding", "gzip") resp, err := client.Do(req) if err != nil { fmt.Printf("网络错误:%v\n", err) - os.Exit(1) + panic("网络异常,请求失败。") } if resp == nil || resp.StatusCode != http.StatusOK { fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode) - os.Exit(1) + panic("获取失败,未能正确获取到数据") } defer resp.Body.Close() @@ -63,7 +62,7 @@ func GetQQLyric(id string) (lyric, tLyric string) { err = json.NewDecoder(reader).Decode(&qqLyric) if qqLyric.RetCode != 0 { fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", qqLyric) - os.Exit(1) + panic("id错误,获取歌词失败。") } return qqLyric.Lyric, qqLyric.Trans } diff --git a/srt.go b/srt.go new file mode 100644 index 0000000..ba3c61a --- /dev/null +++ b/srt.go @@ -0,0 +1,173 @@ +package lrc2srt + +import ( + "bufio" + "fmt" + "github.com/Hami-Lemon/lrc2srt/glist" + "io" + "os" + "sort" + "strconv" + "strings" +) + +type SRTMergeMode int + +const ( + SRT_MERGE_MODE_STACK SRTMergeMode = iota + SRT_MERGE_MODE_UP + SRT_MERGE_MODE_BOTTOM +) + +type SRTContent struct { + //序号,从1开始 + Index int + //开始时间,单位毫秒 + Start int + //结束时间,单位毫秒 + End int + //歌词内容 + Text string +} + +/** +1 +00:00:01,111 --> 00:00:10,111 +字幕 + +*/ +//返回SRT文件中,一句字幕的字符串表示形式 +func (s *SRTContent) String() string { + builder := strings.Builder{} + builder.WriteString(strconv.Itoa(s.Index)) + builder.WriteByte('\n') + sh, sm, ss, sms := millisecond2Time(s.Start) + eh, em, es, ems := millisecond2Time(s.End) + builder.WriteString(fmt.Sprintf("%02d:%02d:%02d,%03d --> %02d:%02d:%02d,%03d\n", + sh, sm, ss, sms, eh, em, es, ems)) + builder.WriteString(s.Text) + builder.WriteString("\n\n") + return builder.String() +} + +type SRT struct { + //歌曲名 + Title string + //歌手名 未指定文件名是,文件名格式为:歌曲名-歌手名.srt + Artist string + Content glist.Queue[*SRTContent] +} + +// LrcToSrt LRC对象转换成SRT对象 +func LrcToSrt(lrc *LRC) *SRT { + if lrc == nil { + return nil + } + srt := &SRT{ + Title: lrc.Title, + Artist: lrc.Artist, + Content: glist.NewLinkedList[*SRTContent](), + } + index := 1 + //上一条srt信息 + var prevSRT *SRTContent + for it := lrc.LrcList.Iterator(); it.Has(); { + lrcNode := it.Next() + srtContent := &SRTContent{ + Index: index, + Start: lrcNode.time, + Text: lrcNode.content, + } + if index != 1 { + //上一条歌词的结束时间设置为当前歌词的开始时间 + prevSRT.End = srtContent.Start + } + srt.Content.PushBack(srtContent) + index++ + prevSRT = srtContent + } + //最后一条歌词 + if prevSRT != nil { + //结束时间是为其 开始时间+10 秒 + prevSRT.End = prevSRT.Start + 1000 + } + return srt +} + +// Merge 将另一个srt信息合并到当前srt中,有三种合并模式 +//1. SRT_MERGE_MODE_STACK: 按照开始时间对两个srt信息进行排序,交错合并 +//2. SRT_MERGE_MODE_UP: 当前srt信息排列在上,另一个排列在下,即 other 追加到后面 +//3. SRT_MERGE_MODE_BOTTOM: 当前srt信息排列在下,另一个排列在上,即 other 添加到前面 +func (s *SRT) Merge(other *SRT, mode SRTMergeMode) { + switch mode { + case SRT_MERGE_MODE_STACK: + s.mergeStack(other) + case SRT_MERGE_MODE_UP: + s.mergeUp(other) + case SRT_MERGE_MODE_BOTTOM: + s.mergeBottom(other) + } +} + +//todo 改进算法,现在的算法太慢了 +func (s *SRT) mergeStack(other *SRT) { + size := s.Content.Size() + other.Content.Size() + temp := make([]*SRTContent, size, size) + index := 0 + for it := s.Content.Iterator(); it.Has(); { + temp[index] = it.Next() + index++ + } + for it := other.Content.Iterator(); it.Has(); { + temp[index] = it.Next() + index++ + } + sort.SliceStable(temp, func(i, j int) bool { + return temp[i].Start < temp[j].Start + }) + list := glist.NewLinkedList[*SRTContent]() + for _, v := range temp { + list.Append(v) + } + s.Content = list +} + +func (s *SRT) mergeUp(other *SRT) { + if other.Content.IsNotEmpty() { + for it := other.Content.Iterator(); it.Has(); { + s.Content.Append(it.Next()) + } + } +} + +func (s *SRT) mergeBottom(other *SRT) { + oq := other.Content + if oq.IsNotEmpty() { + s.Content.PushFront(*(oq.PullBack())) + } +} + +// WriteFile 将SRT格式的数据写入指定的文件中 +func (s *SRT) WriteFile(path string) error { + f, err := os.Create(path) + if err != nil { + return err + } + err = s.Write(f) + err = f.Close() + return err +} + +// Write 将SRT格式的数据写入dst中 +func (s *SRT) Write(dst io.Writer) error { + //6KB的缓冲 + bufSize := 1024 * 6 + writer := bufio.NewWriterSize(dst, bufSize) + for it := s.Content.Iterator(); it.Has(); { + _, err := writer.WriteString(it.Next().String()) + if err != nil { + return err + } + } + return writer.Flush() +} diff --git a/util.go b/util.go index 8d99dd5..8f6a720 100644 --- a/util.go +++ b/util.go @@ -1,13 +1,22 @@ -package main +package lrc2srt import ( "fmt" "io" + "net/http" "os" ) +const ( + CHROME_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36" +) + +var ( + client = http.Client{} +) + // Time2Millisecond 根据分,秒,毫秒 计算出对应的毫秒值 -func Time2Millisecond(m, s, ms int) int { +func time2Millisecond(m, s, ms int) int { t := m*60 + s t *= 1000 t += ms @@ -15,7 +24,7 @@ func Time2Millisecond(m, s, ms int) int { } // Millisecond2Time 根据毫秒值计算出对应的 时,分,秒,毫秒形式的时间值 -func Millisecond2Time(millisecond int) (h, m, s, ms int) { +func millisecond2Time(millisecond int) (h, m, s, ms int) { ms = millisecond % 1000 s = millisecond / 1000 diff --git a/util_test.go b/util_test.go index 4690dda..c052392 100644 --- a/util_test.go +++ b/util_test.go @@ -1,4 +1,4 @@ -package main +package lrc2srt import "testing" @@ -23,7 +23,7 @@ func TestMillisecond2Time(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotH, gotM, gotS, gotMs := Millisecond2Time(tt.args.millisecond) + gotH, gotM, gotS, gotMs := millisecond2Time(tt.args.millisecond) if gotH != tt.wantH { t.Errorf("Millisecond2Time() gotH = %v, want %v", gotH, tt.wantH) } @@ -60,7 +60,7 @@ func TestTime2Millisecond(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := Time2Millisecond(tt.args.m, tt.args.s, tt.args.ms); got != tt.want { + if got := time2Millisecond(tt.args.m, tt.args.s, tt.args.ms); got != tt.want { t.Errorf("Time2Millisecond() = %v, want %v", got, tt.want) } }) @@ -69,12 +69,12 @@ func TestTime2Millisecond(t *testing.T) { func BenchmarkTime2Millisecond(b *testing.B) { for i := 0; i < b.N; i++ { - Time2Millisecond(999, 999, 999) + time2Millisecond(999, 999, 999) } } func BenchmarkMillisecond2Time(b *testing.B) { for i := 0; i < b.N; i++ { - Millisecond2Time(9999999999) + millisecond2Time(9999999999) } } From d708df2de08e8dacb61d4d71ce0a8c8ea2daebea Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Mon, 28 Mar 2022 18:28:44 +0800 Subject: [PATCH 03/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5bb3f1e..96d2cfd 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,6 @@ fabric.properties # Dependency directories (remove the comment below to include it) # vendor/ -/example/*.srt -/example/*.lrc +*.srt +*.lrc /temp From 8b57abe8ee85816ced38cefc8d5b0decf8dae896 Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Mon, 28 Mar 2022 20:04:51 +0800 Subject: [PATCH 04/16] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=AE=97=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- srt.go | 54 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/srt.go b/srt.go index ba3c61a..e9c5a2d 100644 --- a/srt.go +++ b/srt.go @@ -6,7 +6,6 @@ import ( "github.com/Hami-Lemon/lrc2srt/glist" "io" "os" - "sort" "strconv" "strings" ) @@ -109,27 +108,44 @@ func (s *SRT) Merge(other *SRT, mode SRTMergeMode) { } } -//todo 改进算法,现在的算法太慢了 +//可以类比为合并两个有序链表, +//算法参考: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) { - size := s.Content.Size() + other.Content.Size() - temp := make([]*SRTContent, size, size) - index := 0 - for it := s.Content.Iterator(); it.Has(); { - temp[index] = it.Next() - index++ + ls, _ := s.Content.(*glist.LinkedList[*SRTContent]) + lo, _ := other.Content.(*glist.LinkedList[*SRTContent]) + lhs, lho := ls.First, lo.First + //临时的空结点 + preHead := &glist.Node[*SRTContent]{} + prev := preHead + + for lhs != nil && lho != nil { + //副本结点,从而不改变other对象中的内容 + oCopy := lho.Clone() + + if lhs.Element.Start <= oCopy.Element.Start { + prev.Next = lhs + lhs.Prev = prev + lhs = lhs.Next + } else { + prev.Next = oCopy + oCopy.Prev = prev + lho = lho.Next + } + prev = prev.Next } - for it := other.Content.Iterator(); it.Has(); { - temp[index] = it.Next() - index++ + if lhs == nil { + //如果剩下的内容是other中的,则依次迭代复制到s中 + for n := lho; n != nil; n = n.Next { + c := n.Clone() + prev.Next = c + c.Prev = prev + prev = prev.Next + } + } else { + prev.Next = lhs } - sort.SliceStable(temp, func(i, j int) bool { - return temp[i].Start < temp[j].Start - }) - list := glist.NewLinkedList[*SRTContent]() - for _, v := range temp { - list.Append(v) - } - s.Content = list + ls.First = preHead.Next + ls.First.Prev = nil } func (s *SRT) mergeUp(other *SRT) { From c381852b49b01be09a9bc598f4f80ddfa1aeb384 Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Mon, 28 Mar 2022 20:37:37 +0800 Subject: [PATCH 05/16] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dsrt=E5=BA=8F=E5=8F=B7?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E4=BB=A5=E5=8F=8A=E6=A8=A1=E5=BC=8F3?= =?UTF-8?q?=E6=97=A0=E8=AF=91=E6=96=87=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- glist/linkedlist.go | 23 ++++++++++++++++++++--- glist/list.go | 2 ++ srt.go | 14 ++++++++++---- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/glist/linkedlist.go b/glist/linkedlist.go index fdacafe..77424f8 100644 --- a/glist/linkedlist.go +++ b/glist/linkedlist.go @@ -230,11 +230,24 @@ func (l *LinkedList[E]) PullFront() *E { // Iterator 获取该链表的迭代器 func (l *LinkedList[E]) Iterator() Iterator[E] { - return &LinkedListIterator[E]{next: l.First} + 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 { - next *Node[E] + //是否反向,如果为true,则是从尾部向头部迭代 + reverse bool + next *Node[E] } func (l *LinkedListIterator[E]) Has() bool { @@ -246,6 +259,10 @@ func (l *LinkedListIterator[E]) Next() E { if e == nil { panic("iterator is empty.") } - l.next = e.Next + if l.reverse { + l.next = e.Prev + } else { + l.next = e.Next + } return e.Element } diff --git a/glist/list.go b/glist/list.go index 0f1da8d..ca26569 100644 --- a/glist/list.go +++ b/glist/list.go @@ -20,6 +20,8 @@ type List[E any] interface { Set(index uint, element E) bool // Iterator 获取列表的迭代器 Iterator() Iterator[E] + // ReverseIterator 反向迭代器,从尾部向前迭代 + ReverseIterator() Iterator[E] } // Queue 队列 diff --git a/srt.go b/srt.go index e9c5a2d..8142b3f 100644 --- a/srt.go +++ b/srt.go @@ -19,7 +19,7 @@ const ( ) type SRTContent struct { - //序号,从1开始 + //序号,从1开始,只在写入文件的时候设置这个属性 Index int //开始时间,单位毫秒 Start int @@ -73,7 +73,7 @@ func LrcToSrt(lrc *LRC) *SRT { for it := lrc.LrcList.Iterator(); it.Has(); { lrcNode := it.Next() srtContent := &SRTContent{ - Index: index, + Index: 0, Start: lrcNode.time, Text: lrcNode.content, } @@ -159,7 +159,9 @@ func (s *SRT) mergeUp(other *SRT) { func (s *SRT) mergeBottom(other *SRT) { oq := other.Content if oq.IsNotEmpty() { - s.Content.PushFront(*(oq.PullBack())) + for it := other.Content.ReverseIterator(); it.Has(); { + s.Content.PushFront(it.Next()) + } } } @@ -179,8 +181,12 @@ 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(); { - _, err := writer.WriteString(it.Next().String()) + content := it.Next() + content.Index = index + index++ + _, err := writer.WriteString(content.String()) if err != nil { return err } From a4ea096e3dbaf0e77f5768092ac64fd2b524b4ce Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Mon, 28 Mar 2022 20:40:24 +0800 Subject: [PATCH 06/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lts/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lts/main.go b/lts/main.go index f17c1bd..de1e42f 100644 --- a/lts/main.go +++ b/lts/main.go @@ -22,7 +22,7 @@ type Option struct { const ( // VERSION 当前版本 - VERSION = `"0.2.0" (build 2022.03.28)` + VERSION = `"0.2.3" (build 2022.03.28)` ) var ( From 3e2af31678ade3cb58cfb93978f56c46e212a175 Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Tue, 29 Mar 2022 11:16:04 +0800 Subject: [PATCH 07/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cloudlyric_test.go | 22 +++++ glist/linkedlist.go | 188 +++++++++++++++++++-------------------- glist/linkedlist_test.go | 6 +- lrc.go | 2 +- lrc_test.go | 68 ++++++++++++++ lts/main.go | 15 +++- qqlyric_test.go | 22 +++++ srt.go | 67 +++++++------- srt_test.go | 75 ++++++++++++++++ 9 files changed, 335 insertions(+), 130 deletions(-) create mode 100644 cloudlyric_test.go create mode 100644 lrc_test.go create mode 100644 qqlyric_test.go create mode 100644 srt_test.go diff --git a/cloudlyric_test.go b/cloudlyric_test.go new file mode 100644 index 0000000..8d69e2d --- /dev/null +++ b/cloudlyric_test.go @@ -0,0 +1,22 @@ +package lrc2srt + +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 index 77424f8..b4c121a 100644 --- a/glist/linkedlist.go +++ b/glist/linkedlist.go @@ -12,163 +12,163 @@ const ( // Node 链表中的一个结点 type Node[E any] struct { - Element E //保存的内容 - Prev *Node[E] //前一个结点 - Next *Node[E] //后一个结点 + 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, + element: n.element, + prev: nil, + next: nil, } return node } // LinkedList 链表,实现了List type LinkedList[E any] struct { - Len uint //链表中元素个数 - First *Node[E] //头指针 - Last *Node[E] //尾指针 + 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, + len: 0, + first: nil, + last: nil, } } func (l *LinkedList[E]) Size() uint { - return l.Len + return l.len } func (l *LinkedList[E]) IsEmpty() bool { - return l.Len == 0 + return l.len == 0 } func (l *LinkedList[E]) IsNotEmpty() bool { - return l.Len != 0 + return l.len != 0 } func (l *LinkedList[E]) Append(element E) bool { //超出最大值无法添加 - if l.Len == CAPACITY { + if l.len == CAPACITY { return false } node := &Node[E]{ - Element: element, - Prev: nil, - Next: nil, + element: element, + prev: nil, + next: nil, } //链表为空,头指针指向该结点 - if l.First == nil { - l.First = node - l.Last = node + if l.first == nil { + l.first = node + l.last = node } else { //链表不为空,添加到尾部 - node.Prev = l.Last - l.Last.Next = node - l.Last = node + node.prev = l.last + l.last.next = node + l.last = node } - l.Len++ + l.len++ return true } func (l *LinkedList[E]) Insert(index uint, element E) bool { //当前size已经达到最大值或者索引越界 - if l.Len == CAPACITY || index > l.Len { + if l.len == CAPACITY || index > l.len { return false } node := &Node[E]{ - Element: element, - Prev: nil, - Next: nil, + element: element, + prev: nil, + next: nil, } //插入头部 if index == 0 { - if l.First == nil { + if l.first == nil { //链表为空 - l.First = node - l.Last = node + l.first = node + l.last = node } else { //链表不为空 - node.Next = l.First - l.First.Prev = node - l.First = node + node.next = l.first + l.first.prev = node + l.first = node } - } else if index == l.Len { + } else if index == l.len { //插入尾部 - l.Last.Next = node - node.Prev = l.Last - l.Last = node + l.last.next = node + node.prev = l.last + l.last = node } else { var prev *Node[E] - head := l.First + head := l.first for i := ZERO; i < index; i++ { prev = head - head = head.Next + head = head.next } - node.Next = head - node.Prev = prev - prev.Next = node - head.Prev = node + node.next = head + node.prev = prev + prev.next = node + head.prev = node } - l.Len++ + l.len++ return true } func (l *LinkedList[E]) Remove(index uint) bool { - if index >= l.Len { + if index >= l.len { return false } - head := l.First + head := l.first var prev *Node[E] for i := ZERO; i < index; i++ { prev = head - head = head.Next + head = head.next } //删除第一个结点 - if head == l.First { - l.First.Next = nil - l.First = head.Next - } else if head == l.Last { + if head == l.first { + l.first.next = nil + l.first = head.next + } else if head == l.last { //删除最后一个结点 - l.Last = prev - l.Last.Next = nil + l.last = prev + l.last.next = nil } else { - prev.Next = head.Next - head.Next.Prev = prev + prev.next = head.next + head.next.prev = prev } - l.Len-- + l.len-- return true } func (l *LinkedList[E]) Get(index uint) *E { - if index >= l.Len { + if index >= l.len { return nil } - node := l.First + node := l.first for i := ZERO; i < index; i++ { - node = node.Next + node = node.next } - return &(node.Element) + return &(node.element) } func (l *LinkedList[E]) Set(index uint, element E) bool { - if index >= l.Len { + if index >= l.len { return false } - node := l.First + node := l.first for i := ZERO; i < index; i++ { - node = node.Next + node = node.next } - node.Element = element + node.element = element return true } @@ -182,57 +182,57 @@ func (l *LinkedList[E]) PushFront(element E) bool { func (l *LinkedList[E]) PopBack() *E { //链表为空 - if l.Len == 0 { + if l.len == 0 { return nil } - node := l.Last + node := l.last //只有一个元素 - if l.Len == 1 { - l.Last = nil - l.First = nil + if l.len == 1 { + l.last = nil + l.first = nil } else { - l.Last = node.Prev - l.Last.Next = nil + l.last = node.prev + l.last.next = nil } - l.Len-- - return &(node.Element) + l.len-- + return &(node.element) } func (l *LinkedList[E]) PopFront() *E { - if l.Len == 0 { + if l.len == 0 { return nil } - node := l.First - if l.Len == 1 { - l.First = nil - l.Last = nil + node := l.first + if l.len == 1 { + l.first = nil + l.last = nil } else { - l.First = node.Next - l.First.Prev = nil + l.first = node.next + l.first.prev = nil } - l.Len-- - return &(node.Element) + l.len-- + return &(node.element) } func (l *LinkedList[E]) PullBack() *E { - if l.Len == 0 { + if l.len == 0 { return nil } - return &(l.Last.Element) + return &(l.last.element) } func (l *LinkedList[E]) PullFront() *E { - if l.Len == 0 { + if l.len == 0 { return nil } - return &(l.First.Element) + return &(l.first.element) } // Iterator 获取该链表的迭代器 func (l *LinkedList[E]) Iterator() Iterator[E] { return &LinkedListIterator[E]{ reverse: false, - next: l.First, + next: l.first, } } @@ -240,7 +240,7 @@ func (l *LinkedList[E]) Iterator() Iterator[E] { func (l *LinkedList[E]) ReverseIterator() Iterator[E] { return &LinkedListIterator[E]{ reverse: true, - next: l.Last, + next: l.last, } } @@ -260,9 +260,9 @@ func (l *LinkedListIterator[E]) Next() E { panic("iterator is empty.") } if l.reverse { - l.next = e.Prev + l.next = e.prev } else { - l.next = e.Next + l.next = e.next } - return e.Element + return e.element } diff --git a/glist/linkedlist_test.go b/glist/linkedlist_test.go index b05b50b..f9558c5 100644 --- a/glist/linkedlist_test.go +++ b/glist/linkedlist_test.go @@ -7,8 +7,8 @@ import ( ) 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)) + 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(); { @@ -153,7 +153,7 @@ func TestLinkedList_Remove(t *testing.T) { values []int }{ {"empty list", 0, 0, false, nil}, - {"index gather than Len", 0, 1, 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}}, diff --git a/lrc.go b/lrc.go index 30257b3..a4c803c 100644 --- a/lrc.go +++ b/lrc.go @@ -122,7 +122,7 @@ func SplitLyric(src []string) *LRCNode { } lrcNode := &LRCNode{ time: time2Millisecond(minute, second, millisecond), - content: content, + content: strings.TrimSpace(content), //去掉前后的空格 } return lrcNode } diff --git a/lrc_test.go b/lrc_test.go new file mode 100644 index 0000000..10a71b5 --- /dev/null +++ b/lrc_test.go @@ -0,0 +1,68 @@ +package lrc2srt + +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/lts/main.go b/lts/main.go index de1e42f..d5fafb3 100644 --- a/lts/main.go +++ b/lts/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "path/filepath" "strings" "time" @@ -22,7 +23,7 @@ type Option struct { const ( // VERSION 当前版本 - VERSION = `"0.2.3" (build 2022.03.28)` + VERSION = `"0.2.4" (build 2022.03.29)` ) var ( @@ -31,13 +32,14 @@ var ( func main() { //TODO 支持转ass文件 + //TODO 酷狗的krc支持逐字,更利于打轴 https://shansing.com/read/392/ args, err := flags.Parse(&opt) if err != nil { os.Exit(0) } //显示版本信息 if opt.Version { - fmt.Printf("LrcToSrt(lts) version %s\n", VERSION) + fmt.Printf("LrcToSrt(lts) version: %s\n", VERSION) return } //获取保存的文件名 @@ -121,4 +123,13 @@ func main() { fmt.Println("出现错误,保存失败") panic(err.Error()) } + //如果是相对路径,则获取其对应的绝对路径 + if !filepath.IsAbs(name) { + //如果是相对路径,父目录即是当前运行路径 + dir, er := os.Getwd() + if er == nil { + name = dir + name + } + } + fmt.Printf("保存结果为:%s\n", name) } diff --git a/qqlyric_test.go b/qqlyric_test.go new file mode 100644 index 0000000..18dc81c --- /dev/null +++ b/qqlyric_test.go @@ -0,0 +1,22 @@ +package lrc2srt + +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 index 8142b3f..d61b485 100644 --- a/srt.go +++ b/srt.go @@ -6,6 +6,7 @@ import ( "github.com/Hami-Lemon/lrc2srt/glist" "io" "os" + "path/filepath" "strconv" "strings" ) @@ -111,41 +112,43 @@ func (s *SRT) Merge(other *SRT, mode SRTMergeMode) { //可以类比为合并两个有序链表, //算法参考: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) { - ls, _ := s.Content.(*glist.LinkedList[*SRTContent]) - lo, _ := other.Content.(*glist.LinkedList[*SRTContent]) - lhs, lho := ls.First, lo.First - //临时的空结点 - preHead := &glist.Node[*SRTContent]{} - prev := preHead - - for lhs != nil && lho != nil { - //副本结点,从而不改变other对象中的内容 - oCopy := lho.Clone() - - if lhs.Element.Start <= oCopy.Element.Start { - prev.Next = lhs - lhs.Prev = prev - lhs = lhs.Next + 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 { - prev.Next = oCopy - oCopy.Prev = prev - lho = lho.Next + merge.Append(oNode) + oNode = oIt.Next() } - prev = prev.Next } - if lhs == nil { - //如果剩下的内容是other中的,则依次迭代复制到s中 - for n := lho; n != nil; n = n.Next { - c := n.Clone() - prev.Next = c - c.Prev = prev - prev = prev.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) } - } else { - prev.Next = lhs } - ls.First = preHead.Next - ls.First.Prev = nil + //剩下的元素添加到链表中,最多只有一个链表有剩余元素 + for sIt.Has() { + merge.Append(sIt.Next()) + } + for oIt.Has() { + merge.Append(oIt.Next()) + } + s.Content = merge } func (s *SRT) mergeUp(other *SRT) { @@ -169,6 +172,10 @@ func (s *SRT) mergeBottom(other *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) diff --git a/srt_test.go b/srt_test.go new file mode 100644 index 0000000..370665d --- /dev/null +++ b/srt_test.go @@ -0,0 +1,75 @@ +package lrc2srt + +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) { + +} From a29d2c057e5c4d9cd9baad9f6cea9f93d88ccf39 Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Tue, 29 Mar 2022 19:46:22 +0800 Subject: [PATCH 08/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=90=8D:=20lrc2lts=20=3D>=20ltc=20(LrcToCaptions)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.name | 1 + .idea/{lrc2srt.iml => ltc.iml} | 0 .idea/modules.xml | 2 +- cloudlyric.go | 2 +- cloudlyric_test.go | 2 +- go.mod | 2 +- lrc.go | 4 ++-- lrc_test.go | 2 +- build.bat => lrctocaptions/build.bat | 2 +- lts/main.go => lrctocaptions/ltc.go | 28 ++++++++++++++-------------- qqlyric.go | 2 +- qqlyric_test.go | 2 +- srt.go | 4 ++-- srt_test.go | 2 +- util.go | 2 +- util_test.go | 2 +- 16 files changed, 30 insertions(+), 29 deletions(-) create mode 100644 .idea/.name rename .idea/{lrc2srt.iml => ltc.iml} (100%) rename build.bat => lrctocaptions/build.bat (60%) rename lts/main.go => lrctocaptions/ltc.go (84%) diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..4fcc028 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +ltc \ No newline at end of file diff --git a/.idea/lrc2srt.iml b/.idea/ltc.iml similarity index 100% rename from .idea/lrc2srt.iml rename to .idea/ltc.iml diff --git a/.idea/modules.xml b/.idea/modules.xml index 19b0b7d..d70defd 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/cloudlyric.go b/cloudlyric.go index 0cd1fac..e46127c 100644 --- a/cloudlyric.go +++ b/cloudlyric.go @@ -1,4 +1,4 @@ -package lrc2srt +package ltc import ( "encoding/json" diff --git a/cloudlyric_test.go b/cloudlyric_test.go index 8d69e2d..0d82d25 100644 --- a/cloudlyric_test.go +++ b/cloudlyric_test.go @@ -1,4 +1,4 @@ -package lrc2srt +package ltc import ( "fmt" diff --git a/go.mod b/go.mod index 20b43fd..d11e0e1 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/Hami-Lemon/lrc2srt +module github.com/Hami-Lemon/ltc go 1.18 diff --git a/lrc.go b/lrc.go index a4c803c..049d304 100644 --- a/lrc.go +++ b/lrc.go @@ -1,11 +1,11 @@ -package lrc2srt +package ltc import ( "regexp" "strconv" "strings" - "github.com/Hami-Lemon/lrc2srt/glist" + "github.com/Hami-Lemon/ltc/glist" ) type LRCNode struct { diff --git a/lrc_test.go b/lrc_test.go index 10a71b5..2104117 100644 --- a/lrc_test.go +++ b/lrc_test.go @@ -1,4 +1,4 @@ -package lrc2srt +package ltc import ( "reflect" diff --git a/build.bat b/lrctocaptions/build.bat similarity index 60% rename from build.bat rename to lrctocaptions/build.bat index f6cf966..f62eda1 100644 --- a/build.bat +++ b/lrctocaptions/build.bat @@ -1,3 +1,3 @@ echo off @REM forceposix 表示在windows上参数也为linux风格,即以“-”开头 -go build -tags="forceposix" -ldflags "-s -w" -o lts.exe . +go build -tags="forceposix" -ldflags "-s -w" -o ltc.exe . diff --git a/lts/main.go b/lrctocaptions/ltc.go similarity index 84% rename from lts/main.go rename to lrctocaptions/ltc.go index d5fafb3..0136ba9 100644 --- a/lts/main.go +++ b/lrctocaptions/ltc.go @@ -7,7 +7,7 @@ import ( "strings" "time" - l2s "github.com/Hami-Lemon/lrc2srt" + "github.com/Hami-Lemon/ltc" "github.com/jessevdk/go-flags" ) @@ -39,7 +39,7 @@ func main() { } //显示版本信息 if opt.Version { - fmt.Printf("LrcToSrt(lts) version: %s\n", VERSION) + fmt.Printf("LrcToCaptions(ltc) version: %s\n", VERSION) return } //获取保存的文件名 @@ -51,9 +51,9 @@ func main() { var lyric, tranLyric string if opt.Id != "" { if opt.Source != "163" { - lyric, tranLyric = l2s.GetQQLyric(opt.Id) + lyric, tranLyric = ltc.GetQQLyric(opt.Id) } else { - lyric, tranLyric = l2s.Get163Lyric(opt.Id) + lyric, tranLyric = ltc.Get163Lyric(opt.Id) } //下载歌词 if opt.Download { @@ -64,9 +64,9 @@ func main() { } else if !strings.HasSuffix(o, ".lrc") { o += ".lrc" } - l2s.WriteFile(o, lyric) + ltc.WriteFile(o, lyric) if tranLyric != "" { - l2s.WriteFile("tran_"+o, tranLyric) + ltc.WriteFile("tran_"+o, tranLyric) } fmt.Println("下载歌词完成!") return @@ -77,7 +77,7 @@ func main() { fmt.Println("Error: 不支持的格式,目前只支持lrc歌词文件。") os.Exit(1) } - lyric = l2s.ReadFile(opt.Input) + lyric = ltc.ReadFile(opt.Input) if lyric == "" { fmt.Println("获取歌词失败,文件内容为空。") os.Exit(1) @@ -86,17 +86,17 @@ func main() { fmt.Println("Error: 请指定需要转换的歌词。") os.Exit(1) } - lrc, lrcT := l2s.ParseLRC(lyric), l2s.ParseLRC(tranLyric) - srt, srtT := l2s.LrcToSrt(lrc), l2s.LrcToSrt(lrcT) + lrc, lrcT := ltc.ParseLRC(lyric), ltc.ParseLRC(tranLyric) + srt, srtT := ltc.LrcToSrt(lrc), ltc.LrcToSrt(lrcT) if srtT != nil { - var mode l2s.SRTMergeMode + var mode ltc.SRTMergeMode switch opt.Mode { case 1: - mode = l2s.SRT_MERGE_MODE_STACK + mode = ltc.SRT_MERGE_MODE_STACK case 2: - mode = l2s.SRT_MERGE_MODE_UP + mode = ltc.SRT_MERGE_MODE_UP case 3: - mode = l2s.SRT_MERGE_MODE_BOTTOM + mode = ltc.SRT_MERGE_MODE_BOTTOM } srt.Merge(srtT, mode) } @@ -128,7 +128,7 @@ func main() { //如果是相对路径,父目录即是当前运行路径 dir, er := os.Getwd() if er == nil { - name = dir + name + name = dir + string(os.PathSeparator) + name } } fmt.Printf("保存结果为:%s\n", name) diff --git a/qqlyric.go b/qqlyric.go index a3acfba..4fe3ad6 100644 --- a/qqlyric.go +++ b/qqlyric.go @@ -1,4 +1,4 @@ -package lrc2srt +package ltc import ( "compress/gzip" diff --git a/qqlyric_test.go b/qqlyric_test.go index 18dc81c..d339d6e 100644 --- a/qqlyric_test.go +++ b/qqlyric_test.go @@ -1,4 +1,4 @@ -package lrc2srt +package ltc import ( "fmt" diff --git a/srt.go b/srt.go index d61b485..e7e361b 100644 --- a/srt.go +++ b/srt.go @@ -1,9 +1,9 @@ -package lrc2srt +package ltc import ( "bufio" "fmt" - "github.com/Hami-Lemon/lrc2srt/glist" + "github.com/Hami-Lemon/ltc/glist" "io" "os" "path/filepath" diff --git a/srt_test.go b/srt_test.go index 370665d..921af88 100644 --- a/srt_test.go +++ b/srt_test.go @@ -1,4 +1,4 @@ -package lrc2srt +package ltc import "testing" diff --git a/util.go b/util.go index 8f6a720..43d026e 100644 --- a/util.go +++ b/util.go @@ -1,4 +1,4 @@ -package lrc2srt +package ltc import ( "fmt" diff --git a/util_test.go b/util_test.go index c052392..12c5b16 100644 --- a/util_test.go +++ b/util_test.go @@ -1,4 +1,4 @@ -package lrc2srt +package ltc import "testing" From eba676b8ded2472a619b7d5fe5b27b29797c5750 Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Wed, 30 Mar 2022 10:41:28 +0800 Subject: [PATCH 09/16] =?UTF-8?q?=E6=94=AF=E6=8C=81ass=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + ass.go | 125 +++++++++++++++++++++++++++ go.mod | 4 - go.sum | 4 - lrctocaptions/flag.go | 196 ++++++++++++++++++++++++++++++++++++++++++ lrctocaptions/ltc.go | 181 ++++++++++++++++++-------------------- 6 files changed, 406 insertions(+), 105 deletions(-) create mode 100644 ass.go create mode 100644 lrctocaptions/flag.go diff --git a/.gitignore b/.gitignore index 96d2cfd..53ceaab 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,5 @@ fabric.properties # vendor/ *.srt *.lrc +*.ass /temp diff --git a/ass.go b/ass.go new file mode 100644 index 0000000..c130679 --- /dev/null +++ b/ass.go @@ -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 +} diff --git a/go.mod b/go.mod index d11e0e1..d2e7b41 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,3 @@ module github.com/Hami-Lemon/ltc go 1.18 - -require github.com/jessevdk/go-flags v1.5.0 - -require golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect diff --git a/go.sum b/go.sum index df31363..e69de29 100644 --- a/go.sum +++ b/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= diff --git a/lrctocaptions/flag.go b/lrctocaptions/flag.go new file mode 100644 index 0000000..7e93535 --- /dev/null +++ b/lrctocaptions/flag.go @@ -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) +} diff --git a/lrctocaptions/ltc.go b/lrctocaptions/ltc.go index 0136ba9..ee61fff 100644 --- a/lrctocaptions/ltc.go +++ b/lrctocaptions/ltc.go @@ -1,135 +1,122 @@ package main import ( + "bufio" + "flag" "fmt" + "github.com/Hami-Lemon/ltc" + "io/ioutil" "os" "path/filepath" "strings" - "time" - - "github.com/Hami-Lemon/ltc" - "github.com/jessevdk/go-flags" ) -type Option struct { - Id string `short:"i" long:"id" description:"歌曲的id,网易云和QQ音乐均可。"` - Input string `short:"I" long:"input" description:"需要转换的LRC文件路径。"` - Source string `short:"s" long:"source" description:"当设置id时有效,指定从网易云(163)还是QQ音乐(qq)上获取歌词。" default:"163" choice:"163" choice:"qq" choice:"QQ"` - Download bool `short:"d" long:"download" description:"只下载歌词,而不进行解析。"` - Mode int `short:"m" long:"mode" default:"1" description:"原文和译文的排列模式,可选值有:[1] [2] [3]" choice:"1" choice:"2" choice:"3"` - Version bool `short:"v" long:"version" description:"获取版本信息"` - Output string `no-flag:""` -} - const ( // VERSION 当前版本 - VERSION = `"0.2.4" (build 2022.03.29)` -) - -var ( - opt Option + VERSION = `"0.3.4" (build 2022.03.30)` + VERSION_INFO = "LrcToCaptions(ltc) version: %s\n" ) func main() { - //TODO 支持转ass文件 - //TODO 酷狗的krc支持逐字,更利于打轴 https://shansing.com/read/392/ - args, err := flags.Parse(&opt) - if err != nil { - os.Exit(0) - } + parseFlag() + //TODO 酷狗的krc精准到字,更利于打轴 https://shansing.com/read/392/ //显示版本信息 - if opt.Version { - fmt.Printf("LrcToCaptions(ltc) version: %s\n", VERSION) + if version.IsSet() { + fmt.Printf(VERSION_INFO, VERSION) return } - //获取保存的文件名 - if len(args) != 0 { - opt.Output = args[0] + //未指定来源 + if input == "" { + fmt.Printf("未指定歌词来源\n") + flag.Usage() + os.Exit(0) } - //获取歌词,lyric为原文歌词,tranLyric为译文歌词 var lyric, tranLyric string - if opt.Id != "" { - if opt.Source != "163" { - lyric, tranLyric = ltc.GetQQLyric(opt.Id) - } else { - lyric, tranLyric = ltc.Get163Lyric(opt.Id) - } - //下载歌词 - if opt.Download { - //对文件名进行处理 - o := opt.Output - if o == "" { - o = opt.Id + ".lrc" - } else if !strings.HasSuffix(o, ".lrc") { - o += ".lrc" - } - ltc.WriteFile(o, lyric) - if tranLyric != "" { - ltc.WriteFile("tran_"+o, tranLyric) - } - fmt.Println("下载歌词完成!") - return - } - } else if opt.Input != "" { - //从文件中获取歌词 - if !strings.HasSuffix(opt.Input, ".lrc") { + //从文件中获取 + if checkPath(input) { + if !strings.HasSuffix(input, ".lrc") { fmt.Println("Error: 不支持的格式,目前只支持lrc歌词文件。") - os.Exit(1) + panic("") } - lyric = ltc.ReadFile(opt.Input) - if lyric == "" { - fmt.Println("获取歌词失败,文件内容为空。") - os.Exit(1) + if data, err := ioutil.ReadFile(input); err == nil { + if len(data) == 0 { + fmt.Println("获取歌词失败,文件内容为空。") + panic("") + } + lyric = string(data) + } else { + panic("读取文件失败:" + input + err.Error()) } } else { - fmt.Println("Error: 请指定需要转换的歌词。") - os.Exit(1) + //从网络上获取 + if source != "163" { + lyric, tranLyric = ltc.GetQQLyric(input) + } else { + lyric, tranLyric = ltc.Get163Lyric(input) + } } + + //下载歌词 + if download.IsSet() { + //对文件名进行处理 + o := output + if o == "" { + o = input + ".lrc" + } else if !strings.HasSuffix(o, ".lrc") { + o += ".lrc" + } + writeFile(o, lyric) + if tranLyric != "" { + writeFile("tran_"+o, tranLyric) + } + fmt.Println("下载歌词完成!") + return + } + lrc, lrcT := ltc.ParseLRC(lyric), ltc.ParseLRC(tranLyric) + //先转换成srt srt, srtT := ltc.LrcToSrt(lrc), ltc.LrcToSrt(lrcT) if srtT != nil { - var mode ltc.SRTMergeMode - switch opt.Mode { - case 1: - mode = ltc.SRT_MERGE_MODE_STACK - case 2: - mode = ltc.SRT_MERGE_MODE_UP - case 3: - mode = ltc.SRT_MERGE_MODE_BOTTOM - } - srt.Merge(srtT, mode) + //原文和译文合并 + srt.Merge(srtT, mode.Mode()) } - name := opt.Output - if name == "" { - title := srt.Title - if title != "" { - //以歌曲名命名 - name = fmt.Sprintf("%s.srt", title) - } else if opt.Id != "" { - //以歌曲的id命名 - name = fmt.Sprintf("%s.srt", opt.Id) - } else if opt.Input != "" { - //以LRC文件的文件名命名 - name = fmt.Sprintf("%s.srt", opt.Input[:len(opt.Input)-4]) - } else { - //以当前时间的毫秒值命名 - name = fmt.Sprintf("%d.srt", time.Now().Unix()) + switch format.Value() { + case FORMAT_SRT: + if err := srt.WriteFile(output); err != nil { + fmt.Println("出现错误,保存失败") + panic(err.Error()) + } + case FORMAT_ASS: + ass := ltc.SrtToAss(srt) + if err := ass.WriteFile(output); err != nil { + fmt.Println("出现错误,保存失败") + panic(err.Error()) } - } else if !strings.HasSuffix(name, ".srt") { - name += ".srt" - } - if err = srt.WriteFile(name); err != nil { - fmt.Println("出现错误,保存失败") - panic(err.Error()) } //如果是相对路径,则获取其对应的绝对路径 - if !filepath.IsAbs(name) { + if !filepath.IsAbs(output) { //如果是相对路径,父目录即是当前运行路径 dir, er := os.Getwd() if er == nil { - name = dir + string(os.PathSeparator) + name + output = dir + string(os.PathSeparator) + output } } - fmt.Printf("保存结果为:%s\n", name) + fmt.Printf("保存结果为:%s\n", output) +} + +func writeFile(file string, content string) { + f, err := os.Create(file) + if err != nil { + fmt.Printf("创建结果文件失败:%v\n", err) + panic("") + } + defer f.Close() + writer := bufio.NewWriter(f) + _, err = writer.WriteString(content) + err = writer.Flush() + if err != nil { + fmt.Printf("保存文件失败:%v\n", err) + panic("") + } } From dc8033dd53173454217e6ec8fe283381ff8c7d92 Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Wed, 30 Mar 2022 10:43:09 +0800 Subject: [PATCH 10/16] =?UTF-8?q?=E5=88=A0=E9=99=A4build.bat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lrctocaptions/build.bat | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 lrctocaptions/build.bat diff --git a/lrctocaptions/build.bat b/lrctocaptions/build.bat deleted file mode 100644 index f62eda1..0000000 --- a/lrctocaptions/build.bat +++ /dev/null @@ -1,3 +0,0 @@ -echo off -@REM forceposix 表示在windows上参数也为linux风格,即以“-”开头 -go build -tags="forceposix" -ldflags "-s -w" -o ltc.exe . From 72607e927c266bdbd66f92959d9c2103837ac05d Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Wed, 30 Mar 2022 10:54:32 +0800 Subject: [PATCH 11/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9github=20action?= =?UTF-8?q?=E7=9A=84go=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/go.yml | 5 ++--- .idea/misc.xml | 8 ++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .idea/misc.xml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 863c15a..a02909e 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -13,10 +13,9 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Go - uses: actions/setup-go@v2 + - name: Setup Go environment + uses: actions/setup-go@v3.0.0 with: - go-version: 1.17 check-latest: true - name: Build diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..283b9b4 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file From 76d1e8215de1ab30fdf5b8995d64563d53901f92 Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Wed, 30 Mar 2022 10:58:03 +0800 Subject: [PATCH 12/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9github=20action?= =?UTF-8?q?=E7=9A=84go=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/go.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a02909e..70053e8 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,6 +16,7 @@ jobs: - name: Setup Go environment uses: actions/setup-go@v3.0.0 with: + go-version: '>=1.18' check-latest: true - name: Build From 7212aa32208fb6b7fa78714b4d83ce7f2c314526 Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Wed, 30 Mar 2022 11:01:30 +0800 Subject: [PATCH 13/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9github=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/go.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 70053e8..aef47b9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -21,17 +21,6 @@ jobs: - 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 From dc2e111c1491907c00f583b938bf9c8ca933abb9 Mon Sep 17 00:00:00 2001 From: Hami Lemon Date: Mon, 4 Apr 2022 13:35:29 +0800 Subject: [PATCH 14/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/ltc.iml | 4 +--- README.md | 37 +++++++++++++++++++++---------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/.idea/ltc.iml b/.idea/ltc.iml index a41de17..5e764c4 100644 --- a/.idea/ltc.iml +++ b/.idea/ltc.iml @@ -2,9 +2,7 @@ - - - + diff --git a/README.md b/README.md index a59a074..f790b96 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,13 @@ [![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歌词文件转换成SRT字幕文件 +用于将LRC歌词文件转换成ASS、SRT字幕文件 ## 功能 - [x] lrc文件转换成srt文件 -- [x] 从网易云音乐或QQ音乐上获取歌词,并转换成srt文件 -- [x] 从网易云音乐或QQ音乐上下载歌词 +- [x] lrc文件转换成ass文件 +- [x] 从网易云音乐或QQ音乐上获取歌词,并转换。 +- [x] 从网易云音乐或QQ音乐上下载歌词。 ## 下载 @@ -16,21 +17,25 @@ ## 开始使用 -``` -Usage: - D:\ProgrameStudy\lrc2srt\lts.exe [OPTIONS] +```text +LrcToCaptions(ltc) 将LRC歌词文件转换成字幕文件。 +ltc version: "0.3.4" (build 2022.03.30) -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 获取版本信息 +用法:ltc [options] OutputFile -Help Options: - -h, --help Show this help message +options: + + -d 设置该选项时,只下载歌词,而无需转换。 + -f value + 转换成的字幕文件格式,可选值:ass(默认),srt + -i string + 歌词来源,可以是歌词对应的歌曲id,也可以是歌词文件 + -m value + 设置歌词原文和译文的合并模式,可选值:1(默认),2,3。 + -s string + 选择从网易云还是QQ音乐上获取歌词,可选值:163(默认),qq。 (default "163") + -v 获取当前程序版本信息。 + -h 显示帮助信息。 ``` ### 获取歌曲id From b973ecb5624e0757eb3d82da66f95b3966def31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=88=E4=B9=90=E4=B8=8D=E7=94=9C=E4=B8=8D=E8=A6=81?= =?UTF-8?q?=E9=92=B1?= Date: Mon, 4 Apr 2022 13:36:32 +0800 Subject: [PATCH 15/16] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f790b96..d9444ed 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# LrcToSrt +# LrcToCaptons [![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) From 425e354bca69a6d4875330bf927f4c293d397dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=88=E4=B9=90=E4=B8=8D=E7=94=9C=E4=B8=8D=E8=A6=81?= =?UTF-8?q?=E9=92=B1?= Date: Thu, 2 Jun 2022 15:46:25 +0800 Subject: [PATCH 16/16] update README --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index f790b96..b3fce7e 100644 --- a/README.md +++ b/README.md @@ -150,3 +150,18 @@ 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 . +``` +