From 6875b43b78e721a2457b4e434fe4e4dedc0fee47 Mon Sep 17 00:00:00 2001 From: CDN18 Date: Fri, 27 Sep 2024 20:28:22 +0800 Subject: [PATCH 1/5] refactor: parse Timeline as Timestamp[] --- model.go | 9 +++++- util.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/model.go b/model.go index c9a7c94..db66230 100644 --- a/model.go +++ b/model.go @@ -1,8 +1,15 @@ package main +type Timestamp struct { + Hours int + Minutes int + Seconds int + Milliseconds int +} + type Lyrics struct { Metadata map[string]string - Timeline []string + Timeline []Timestamp Content []string } diff --git a/util.go b/util.go index aef69b1..9e0efc4 100644 --- a/util.go +++ b/util.go @@ -6,9 +6,76 @@ import ( "os" "path/filepath" "regexp" + "strconv" "strings" ) +func parseTimestamp(timeStr string) (Timestamp, error) { + // 移除方括号 + timeStr = strings.Trim(timeStr, "[]") + + parts := strings.Split(timeStr, ":") + var hours, minutes, seconds, milliseconds int + var err error + + switch len(parts) { + case 2: // 分钟:秒.毫秒 + minutes, err = strconv.Atoi(parts[0]) + if err != nil { + return Timestamp{}, err + } + secParts := strings.Split(parts[1], ".") + seconds, err = strconv.Atoi(secParts[0]) + if err != nil { + return Timestamp{}, err + } + if len(secParts) > 1 { + milliseconds, err = strconv.Atoi(secParts[1]) + if err != nil { + return Timestamp{}, err + } + // 根据毫秒的位数调整 + switch len(secParts[1]) { + case 1: + milliseconds *= 100 + case 2: + milliseconds *= 10 + } + } + case 3: // 小时:分钟:秒.毫秒 + hours, err = strconv.Atoi(parts[0]) + if err != nil { + return Timestamp{}, err + } + minutes, err = strconv.Atoi(parts[1]) + if err != nil { + return Timestamp{}, err + } + secParts := strings.Split(parts[2], ".") + seconds, err = strconv.Atoi(secParts[0]) + if err != nil { + return Timestamp{}, err + } + if len(secParts) > 1 { + milliseconds, err = strconv.Atoi(secParts[1]) + if err != nil { + return Timestamp{}, err + } + // 根据毫秒的位数调整 + switch len(secParts[1]) { + case 1: + milliseconds *= 100 + case 2: + milliseconds *= 10 + } + } + default: + return Timestamp{}, fmt.Errorf("无效的时间戳格式") + } + + return Timestamp{Hours: hours, Minutes: minutes, Seconds: seconds, Milliseconds: milliseconds}, nil +} + func parseLyrics(filePath string) (Lyrics, error) { file, err := os.Open(filePath) if err != nil { @@ -27,9 +94,13 @@ func parseLyrics(filePath string) (Lyrics, error) { line := scanner.Text() if strings.HasPrefix(line, "[") && strings.Contains(line, "]") { if timeLineRegex.MatchString(line) { - // Timeline Tag - time := timeLineRegex.FindString(line) - lyrics.Timeline = append(lyrics.Timeline, time) + // Timeline + timeStr := timeLineRegex.FindString(line) + timestamp, err := parseTimestamp(timeStr) + if err != nil { + return Lyrics{}, err + } + lyrics.Timeline = append(lyrics.Timeline, timestamp) // Content content := timeLineRegex.ReplaceAllString(line, "") lyrics.Content = append(lyrics.Content, strings.TrimSpace(content)) @@ -64,7 +135,7 @@ func saveLyrics(filePath string, lyrics Lyrics) error { // Write timeline and content for i := 0; i < len(lyrics.Timeline); i++ { - fmt.Fprintf(file, "%s %s\n", lyrics.Timeline[i], lyrics.Content[i]) + fmt.Fprintf(file, "%s %s\n", timestampToString(lyrics.Timeline[i]), lyrics.Content[i]) } return nil @@ -191,3 +262,10 @@ func lrcToTxt(sourceFile, targetFile string) { fmt.Fprintln(file, content) } } + +func timestampToString(ts Timestamp) string { + if ts.Hours > 0 { + return fmt.Sprintf("[%02d:%02d:%02d.%03d]", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds) + } + return fmt.Sprintf("[%02d:%02d.%03d]", ts.Minutes, ts.Seconds, ts.Milliseconds) +} From 00deeaf425e5c6624a981b2927ce94efc935d2d0 Mon Sep 17 00:00:00 2001 From: CDN18 Date: Fri, 27 Sep 2024 20:39:48 +0800 Subject: [PATCH 2/5] chore: prepare for srt support + force sync lrc timestamp if timeline length mismatch --- convert.go | 25 +++++++++++++++++++++++++ util.go => lrc.go | 44 +++----------------------------------------- 2 files changed, 28 insertions(+), 41 deletions(-) create mode 100644 convert.go rename util.go => lrc.go (84%) diff --git a/convert.go b/convert.go new file mode 100644 index 0000000..55c0e31 --- /dev/null +++ b/convert.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "os" +) + +func lrcToTxt(sourceFile, targetFile string) { + sourceLyrics, err := parseLyrics(sourceFile) + if err != nil { + fmt.Println("Error parsing source lyrics file:", err) + return + } + + file, err := os.Create(targetFile) + if err != nil { + fmt.Println("Error creating target file:", err) + return + } + defer file.Close() + + for _, content := range sourceLyrics.Content { + fmt.Fprintln(file, content) + } +} diff --git a/util.go b/lrc.go similarity index 84% rename from util.go rename to lrc.go index 9e0efc4..072e968 100644 --- a/util.go +++ b/lrc.go @@ -162,16 +162,14 @@ func syncLyrics(args []string) { return } - // Sync timeline - if len(sourceLyrics.Timeline) != len(targetLyrics.Timeline) { - fmt.Println("Warning: Timeline length mismatch") - return - } minLength := len(sourceLyrics.Timeline) if len(targetLyrics.Timeline) < minLength { minLength = len(targetLyrics.Timeline) + fmt.Printf("Warning: Timeline length mismatch. Source: %d lines, Target: %d lines. Will sync the first %d lines.\n", + len(sourceLyrics.Timeline), len(targetLyrics.Timeline), minLength) } + // Sync the timeline for i := 0; i < minLength; i++ { targetLyrics.Timeline[i] = sourceLyrics.Timeline[i] } @@ -185,23 +183,6 @@ func syncLyrics(args []string) { } } -// func printLyricsInfo(lyrics Lyrics) { -// fmt.Println("Metadata:") -// for key, value := range lyrics.Metadata { -// fmt.Printf("%s: %s\n", key, value) -// } - -// fmt.Println("\nTimeline:") -// for _, time := range lyrics.Timeline { -// fmt.Println(time) -// } - -// fmt.Println("\nLyrics Content:") -// for _, content := range lyrics.Content { -// fmt.Println(content) -// } -// } - func convertLyrics(args []string) { if len(args) < 2 { fmt.Println(CONVERT_USAGE) @@ -244,25 +225,6 @@ func fmtLyrics(args []string) { } } -func lrcToTxt(sourceFile, targetFile string) { - sourceLyrics, err := parseLyrics(sourceFile) - if err != nil { - fmt.Println("Error parsing source lyrics file:", err) - return - } - - file, err := os.Create(targetFile) - if err != nil { - fmt.Println("Error creating target file:", err) - return - } - defer file.Close() - - for _, content := range sourceLyrics.Content { - fmt.Fprintln(file, content) - } -} - func timestampToString(ts Timestamp) string { if ts.Hours > 0 { return fmt.Sprintf("[%02d:%02d:%02d.%03d]", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds) From 7d5a8bdf54cf953cf2c88e1b5bf5d13b31de67ad Mon Sep 17 00:00:00 2001 From: CDN18 Date: Fri, 27 Sep 2024 20:59:59 +0800 Subject: [PATCH 3/5] feat: support srt --- convert.go | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ lrc.go | 29 ++++++---------- main.go | 2 +- model.go | 12 ++++++- srt.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 205 insertions(+), 21 deletions(-) create mode 100644 srt.go diff --git a/convert.go b/convert.go index 55c0e31..d03527d 100644 --- a/convert.go +++ b/convert.go @@ -3,8 +3,32 @@ package main import ( "fmt" "os" + "path/filepath" + "strings" ) +func convert(args []string) { + if len(args) < 2 { + fmt.Println(CONVERT_USAGE) + return + } + + sourceFile := args[0] + targetFile := args[1] + + sourceFmt := strings.TrimPrefix(filepath.Ext(sourceFile), ".") + targetFmt := strings.TrimPrefix(filepath.Ext(targetFile), ".") + + switch sourceFmt { + case "lrc": + convertLyrics(sourceFile, targetFile, targetFmt) + case "srt": + convertSRT(sourceFile, targetFile, targetFmt) + default: + fmt.Printf("unsupported source file format: %s\n", sourceFmt) + } +} + func lrcToTxt(sourceFile, targetFile string) { sourceLyrics, err := parseLyrics(sourceFile) if err != nil { @@ -23,3 +47,76 @@ func lrcToTxt(sourceFile, targetFile string) { fmt.Fprintln(file, content) } } + +func lrcToSrt(sourceFile, targetFile string) { + sourceLyrics, err := parseLyrics(sourceFile) + if err != nil { + fmt.Println("Error parsing source lyrics file:", err) + return + } + + file, err := os.Create(targetFile) + if err != nil { + fmt.Println("Error creating target file:", err) + return + } + defer file.Close() + + for i, content := range sourceLyrics.Content { + startTime := sourceLyrics.Timeline[i] + var endTime Timestamp + if i < len(sourceLyrics.Timeline)-1 { + endTime = sourceLyrics.Timeline[i+1] + } else { + endTime = addSeconds(startTime, 3) + } + + fmt.Fprintf(file, "%d\n", i+1) + fmt.Fprintf(file, "%s --> %s\n", formatSRTTimestamp(startTime), formatSRTTimestamp(endTime)) + fmt.Fprintf(file, "%s\n\n", content) + } +} + +func srtToLrc(sourceFile, targetFile string) { + srtEntries, err := parseSRT(sourceFile) + if err != nil { + fmt.Println("Error parsing source SRT file:", err) + return + } + + lyrics := Lyrics{ + Metadata: make(map[string]string), + Timeline: make([]Timestamp, len(srtEntries)), + Content: make([]string, len(srtEntries)), + } + + for i, entry := range srtEntries { + lyrics.Timeline[i] = entry.StartTime + lyrics.Content[i] = entry.Content + } + + err = saveLyrics(targetFile, lyrics) + if err != nil { + fmt.Println("Error saving LRC file:", err) + return + } +} + +func srtToTxt(sourceFile, targetFile string) { + srtEntries, err := parseSRT(sourceFile) + if err != nil { + fmt.Println("Error parsing source SRT file:", err) + return + } + + file, err := os.Create(targetFile) + if err != nil { + fmt.Println("Error creating target file:", err) + return + } + defer file.Close() + + for _, entry := range srtEntries { + fmt.Fprintln(file, entry.Content) + } +} diff --git a/lrc.go b/lrc.go index 072e968..955eaec 100644 --- a/lrc.go +++ b/lrc.go @@ -4,14 +4,13 @@ import ( "bufio" "fmt" "os" - "path/filepath" "regexp" "strconv" "strings" ) func parseTimestamp(timeStr string) (Timestamp, error) { - // 移除方括号 + // remove brackets timeStr = strings.Trim(timeStr, "[]") parts := strings.Split(timeStr, ":") @@ -19,7 +18,7 @@ func parseTimestamp(timeStr string) (Timestamp, error) { var err error switch len(parts) { - case 2: // 分钟:秒.毫秒 + case 2: // minutes:seconds.milliseconds minutes, err = strconv.Atoi(parts[0]) if err != nil { return Timestamp{}, err @@ -34,7 +33,7 @@ func parseTimestamp(timeStr string) (Timestamp, error) { if err != nil { return Timestamp{}, err } - // 根据毫秒的位数调整 + // adjust milliseconds based on the number of digits switch len(secParts[1]) { case 1: milliseconds *= 100 @@ -42,7 +41,7 @@ func parseTimestamp(timeStr string) (Timestamp, error) { milliseconds *= 10 } } - case 3: // 小时:分钟:秒.毫秒 + case 3: // hours:minutes:seconds.milliseconds hours, err = strconv.Atoi(parts[0]) if err != nil { return Timestamp{}, err @@ -61,7 +60,7 @@ func parseTimestamp(timeStr string) (Timestamp, error) { if err != nil { return Timestamp{}, err } - // 根据毫秒的位数调整 + // adjust milliseconds based on the number of digits switch len(secParts[1]) { case 1: milliseconds *= 100 @@ -70,7 +69,7 @@ func parseTimestamp(timeStr string) (Timestamp, error) { } } default: - return Timestamp{}, fmt.Errorf("无效的时间戳格式") + return Timestamp{}, fmt.Errorf("invalid timestamp format") } return Timestamp{Hours: hours, Minutes: minutes, Seconds: seconds, Milliseconds: milliseconds}, nil @@ -183,22 +182,14 @@ func syncLyrics(args []string) { } } -func convertLyrics(args []string) { - if len(args) < 2 { - fmt.Println(CONVERT_USAGE) - return - } - - sourceFile := args[0] - targetFile := args[1] - - targetFmt := strings.TrimPrefix(filepath.Ext(targetFile), ".") - +func convertLyrics(sourceFile, targetFile, targetFmt string) { switch targetFmt { case "txt": lrcToTxt(sourceFile, targetFile) + case "srt": + lrcToSrt(sourceFile, targetFile) default: - fmt.Println("Unsupported target format:", targetFmt) + fmt.Printf("unsupported target format: %s\n", targetFmt) } } diff --git a/main.go b/main.go index 05f0f94..ff6e52f 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ func main() { case "sync": syncLyrics(os.Args[2:]) case "convert": - convertLyrics(os.Args[2:]) + convert(os.Args[2:]) case "fmt": fmtLyrics(os.Args[2:]) case "version": diff --git a/model.go b/model.go index db66230..f968eb6 100644 --- a/model.go +++ b/model.go @@ -13,17 +13,27 @@ type Lyrics struct { Content []string } +type SRTEntry struct { + Number int + StartTime Timestamp + EndTime Timestamp + Content string +} + const ( VERSION = "0.2.0" USAGE = `Usage: lyc-cli [command] [options] Commands: sync Synchronize timeline of two lyrics files convert Convert lyrics file to another format + fmt Format lyrics file help Show help` SYNC_USAGE = `Usage: lyc-cli sync ` CONVERT_USAGE = `Usage: lyc-cli convert Note: Target format is determined by file extension. Supported formats: - .txt Plain text format(No meta/timeline tags)` + .txt Plain text format(No meta/timeline tags, only support as target format) + .srt SubRip Subtitle format + .lrc LRC format` ) diff --git a/srt.go b/srt.go new file mode 100644 index 0000000..6700b44 --- /dev/null +++ b/srt.go @@ -0,0 +1,86 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + "time" +) + +func parseSRT(filePath string) ([]SRTEntry, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + var entries []SRTEntry + var currentEntry SRTEntry + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + if currentEntry.Number != 0 { + entries = append(entries, currentEntry) + currentEntry = SRTEntry{} + } + continue + } + + if currentEntry.Number == 0 { + currentEntry.Number, _ = strconv.Atoi(line) + } else if currentEntry.StartTime.Hours == 0 { + times := strings.Split(line, " --> ") + currentEntry.StartTime = parseSRTTimestamp(times[0]) + currentEntry.EndTime = parseSRTTimestamp(times[1]) + } else { + currentEntry.Content += line + "\n" + } + } + + if currentEntry.Number != 0 { + entries = append(entries, currentEntry) + } + + return entries, scanner.Err() +} + +func convertSRT(sourceFile, targetFile, targetFmt string) { + switch targetFmt { + case "txt": + srtToTxt(sourceFile, targetFile) + case "lrc": + srtToLrc(sourceFile, targetFile) + default: + fmt.Printf("unsupported target format: %s\n", targetFmt) + } +} + +func formatSRTTimestamp(ts Timestamp) string { + return fmt.Sprintf("%02d:%02d:%02d,%03d", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds) +} + +func parseSRTTimestamp(timeStr string) Timestamp { + t, _ := time.Parse("15:04:05,000", timeStr) + return Timestamp{ + Hours: t.Hour(), + Minutes: t.Minute(), + Seconds: t.Second(), + Milliseconds: t.Nanosecond() / 1e6, + } +} + +// basically for the last line of lrc +func addSeconds(ts Timestamp, seconds int) Timestamp { + t := time.Date(0, 1, 1, ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds*1e6, time.UTC) + t = t.Add(time.Duration(seconds) * time.Second) + return Timestamp{ + Hours: t.Hour(), + Minutes: t.Minute(), + Seconds: t.Second(), + Milliseconds: t.Nanosecond() / 1e6, + } +} From 9d031072add432b2a39b529994203d9c042b0e9a Mon Sep 17 00:00:00 2001 From: CDN18 Date: Fri, 27 Sep 2024 21:00:46 +0800 Subject: [PATCH 4/5] chore: bump version --- model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model.go b/model.go index f968eb6..476df14 100644 --- a/model.go +++ b/model.go @@ -21,7 +21,7 @@ type SRTEntry struct { } const ( - VERSION = "0.2.0" + VERSION = "0.3.0" USAGE = `Usage: lyc-cli [command] [options] Commands: sync Synchronize timeline of two lyrics files From 82e67b1a3220eca28a1cfb5240fe0aa23e0f7a2b Mon Sep 17 00:00:00 2001 From: CDN18 Date: Fri, 27 Sep 2024 21:04:33 +0800 Subject: [PATCH 5/5] chore: not WIP anymore --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 00f775d..6661292 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # lrc-cli -[WIP] A CLI tool for LRC. +A CLI tool for LRC. See [releases](https://git.owu.one/starset-mirror/lrc-cli/releases) for binaries. ## Usage