diff --git a/README.md b/README.md index 6661292..00f775d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # lrc-cli -A CLI tool for LRC. +[WIP] A CLI tool for LRC. See [releases](https://git.owu.one/starset-mirror/lrc-cli/releases) for binaries. ## Usage diff --git a/convert.go b/convert.go deleted file mode 100644 index 510eace..0000000 --- a/convert.go +++ /dev/null @@ -1,128 +0,0 @@ -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 { - 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 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)), - } - - // Add default metadata - title := strings.TrimSuffix(filepath.Base(targetFile), filepath.Ext(targetFile)) - lyrics.Metadata["ti"] = title - lyrics.Metadata["ar"] = "" - lyrics.Metadata["al"] = "" - - 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/main.go b/main.go index ff6e52f..05f0f94 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ func main() { case "sync": syncLyrics(os.Args[2:]) case "convert": - convert(os.Args[2:]) + convertLyrics(os.Args[2:]) case "fmt": fmtLyrics(os.Args[2:]) case "version": diff --git a/model.go b/model.go index 476df14..c9a7c94 100644 --- a/model.go +++ b/model.go @@ -1,39 +1,22 @@ package main -type Timestamp struct { - Hours int - Minutes int - Seconds int - Milliseconds int -} - type Lyrics struct { Metadata map[string]string - Timeline []Timestamp + Timeline []string Content []string } -type SRTEntry struct { - Number int - StartTime Timestamp - EndTime Timestamp - Content string -} - const ( - VERSION = "0.3.0" + 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, only support as target format) - .srt SubRip Subtitle format - .lrc LRC format` + .txt Plain text format(No meta/timeline tags)` ) diff --git a/srt.go b/srt.go deleted file mode 100644 index 58cd75f..0000000 --- a/srt.go +++ /dev/null @@ -1,120 +0,0 @@ -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 - var isContent bool - var contentBuffer strings.Builder - - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - - if line == "" { - if currentEntry.Number != 0 { - currentEntry.Content = contentBuffer.String() - entries = append(entries, currentEntry) - currentEntry = SRTEntry{} - isContent = false - contentBuffer.Reset() - } - continue - } - - if currentEntry.Number == 0 { - currentEntry.Number, _ = strconv.Atoi(line) - } else if isEntryTimeStampUnset(currentEntry) { - times := strings.Split(line, " --> ") - if len(times) == 2 { - currentEntry.StartTime = parseSRTTimestamp(times[0]) - currentEntry.EndTime = parseSRTTimestamp(times[1]) - isContent = true - } - } else if isContent { - if contentBuffer.Len() > 0 { - contentBuffer.WriteString("\n") - } - contentBuffer.WriteString(line) - } - } - - if currentEntry.Number != 0 { - currentEntry.Content = contentBuffer.String() - entries = append(entries, currentEntry) - } - - return entries, scanner.Err() -} - -func isEntryTimeStampUnset(currentEntry SRTEntry) bool { - return currentEntry.StartTime.Hours == 0 && currentEntry.StartTime.Minutes == 0 && - currentEntry.StartTime.Seconds == 0 && currentEntry.StartTime.Milliseconds == 0 && - currentEntry.EndTime.Hours == 0 && currentEntry.EndTime.Minutes == 0 && - currentEntry.EndTime.Seconds == 0 && currentEntry.EndTime.Milliseconds == 0 -} - -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 { - parts := strings.Split(timeStr, ",") - if len(parts) != 2 { - return Timestamp{} - } - - timeParts := strings.Split(parts[0], ":") - if len(timeParts) != 3 { - return Timestamp{} - } - - hours, _ := strconv.Atoi(timeParts[0]) - minutes, _ := strconv.Atoi(timeParts[1]) - seconds, _ := strconv.Atoi(timeParts[2]) - milliseconds, _ := strconv.Atoi(parts[1]) - - return Timestamp{ - Hours: hours, - Minutes: minutes, - Seconds: seconds, - Milliseconds: milliseconds, - } -} - -// 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, - } -} diff --git a/lrc.go b/util.go similarity index 52% rename from lrc.go rename to util.go index 955eaec..aef69b1 100644 --- a/lrc.go +++ b/util.go @@ -4,77 +4,11 @@ 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, ":") - var hours, minutes, seconds, milliseconds int - var err error - - switch len(parts) { - case 2: // minutes:seconds.milliseconds - 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 - } - // adjust milliseconds based on the number of digits - switch len(secParts[1]) { - case 1: - milliseconds *= 100 - case 2: - milliseconds *= 10 - } - } - case 3: // hours:minutes:seconds.milliseconds - 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 - } - // adjust milliseconds based on the number of digits - switch len(secParts[1]) { - case 1: - milliseconds *= 100 - case 2: - milliseconds *= 10 - } - } - default: - return Timestamp{}, fmt.Errorf("invalid timestamp format") - } - - 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 { @@ -93,13 +27,9 @@ func parseLyrics(filePath string) (Lyrics, error) { line := scanner.Text() if strings.HasPrefix(line, "[") && strings.Contains(line, "]") { if timeLineRegex.MatchString(line) { - // Timeline - timeStr := timeLineRegex.FindString(line) - timestamp, err := parseTimestamp(timeStr) - if err != nil { - return Lyrics{}, err - } - lyrics.Timeline = append(lyrics.Timeline, timestamp) + // Timeline Tag + time := timeLineRegex.FindString(line) + lyrics.Timeline = append(lyrics.Timeline, time) // Content content := timeLineRegex.ReplaceAllString(line, "") lyrics.Content = append(lyrics.Content, strings.TrimSpace(content)) @@ -134,7 +64,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", timestampToString(lyrics.Timeline[i]), lyrics.Content[i]) + fmt.Fprintf(file, "%s %s\n", lyrics.Timeline[i], lyrics.Content[i]) } return nil @@ -161,14 +91,16 @@ 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] } @@ -182,14 +114,39 @@ func syncLyrics(args []string) { } } -func convertLyrics(sourceFile, targetFile, targetFmt 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) + return + } + + sourceFile := args[0] + targetFile := args[1] + + targetFmt := strings.TrimPrefix(filepath.Ext(targetFile), ".") + switch targetFmt { case "txt": lrcToTxt(sourceFile, targetFile) - case "srt": - lrcToSrt(sourceFile, targetFile) default: - fmt.Printf("unsupported target format: %s\n", targetFmt) + fmt.Println("Unsupported target format:", targetFmt) } } @@ -216,9 +173,21 @@ func fmtLyrics(args []string) { } } -func timestampToString(ts Timestamp) string { - if ts.Hours > 0 { - return fmt.Sprintf("[%02d:%02d:%02d.%03d]", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds) +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) } - return fmt.Sprintf("[%02d:%02d.%03d]", ts.Minutes, ts.Seconds, ts.Milliseconds) }