Compare commits

..

No commits in common. "main" and "v0.2.0" have entirely different histories.
main ... v0.2.0

6 changed files with 60 additions and 356 deletions

View file

@ -1,6 +1,6 @@
# lrc-cli # 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. See [releases](https://git.owu.one/starset-mirror/lrc-cli/releases) for binaries.
## Usage ## Usage

View file

@ -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)
}
}

View file

@ -15,7 +15,7 @@ func main() {
case "sync": case "sync":
syncLyrics(os.Args[2:]) syncLyrics(os.Args[2:])
case "convert": case "convert":
convert(os.Args[2:]) convertLyrics(os.Args[2:])
case "fmt": case "fmt":
fmtLyrics(os.Args[2:]) fmtLyrics(os.Args[2:])
case "version": case "version":

View file

@ -1,39 +1,22 @@
package main package main
type Timestamp struct {
Hours int
Minutes int
Seconds int
Milliseconds int
}
type Lyrics struct { type Lyrics struct {
Metadata map[string]string Metadata map[string]string
Timeline []Timestamp Timeline []string
Content []string Content []string
} }
type SRTEntry struct {
Number int
StartTime Timestamp
EndTime Timestamp
Content string
}
const ( const (
VERSION = "0.3.0" VERSION = "0.2.0"
USAGE = `Usage: lyc-cli [command] [options] USAGE = `Usage: lyc-cli [command] [options]
Commands: Commands:
sync Synchronize timeline of two lyrics files sync Synchronize timeline of two lyrics files
convert Convert lyrics file to another format convert Convert lyrics file to another format
fmt Format lyrics file
help Show help` help Show help`
SYNC_USAGE = `Usage: lyc-cli sync <source> <target>` SYNC_USAGE = `Usage: lyc-cli sync <source> <target>`
CONVERT_USAGE = `Usage: lyc-cli convert <source> <target> CONVERT_USAGE = `Usage: lyc-cli convert <source> <target>
Note: Note:
Target format is determined by file extension. Supported formats: Target format is determined by file extension. Supported formats:
.txt Plain text format(No meta/timeline tags, only support as target format) .txt Plain text format(No meta/timeline tags)`
.srt SubRip Subtitle format
.lrc LRC format`
) )

120
srt.go
View file

@ -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,
}
}

View file

@ -4,77 +4,11 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"os" "os"
"path/filepath"
"regexp" "regexp"
"strconv"
"strings" "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) { func parseLyrics(filePath string) (Lyrics, error) {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
@ -93,13 +27,9 @@ func parseLyrics(filePath string) (Lyrics, error) {
line := scanner.Text() line := scanner.Text()
if strings.HasPrefix(line, "[") && strings.Contains(line, "]") { if strings.HasPrefix(line, "[") && strings.Contains(line, "]") {
if timeLineRegex.MatchString(line) { if timeLineRegex.MatchString(line) {
// Timeline // Timeline Tag
timeStr := timeLineRegex.FindString(line) time := timeLineRegex.FindString(line)
timestamp, err := parseTimestamp(timeStr) lyrics.Timeline = append(lyrics.Timeline, time)
if err != nil {
return Lyrics{}, err
}
lyrics.Timeline = append(lyrics.Timeline, timestamp)
// Content // Content
content := timeLineRegex.ReplaceAllString(line, "") content := timeLineRegex.ReplaceAllString(line, "")
lyrics.Content = append(lyrics.Content, strings.TrimSpace(content)) lyrics.Content = append(lyrics.Content, strings.TrimSpace(content))
@ -134,7 +64,7 @@ func saveLyrics(filePath string, lyrics Lyrics) error {
// Write timeline and content // Write timeline and content
for i := 0; i < len(lyrics.Timeline); i++ { 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 return nil
@ -161,14 +91,16 @@ func syncLyrics(args []string) {
return return
} }
// Sync timeline
if len(sourceLyrics.Timeline) != len(targetLyrics.Timeline) {
fmt.Println("Warning: Timeline length mismatch")
return
}
minLength := len(sourceLyrics.Timeline) minLength := len(sourceLyrics.Timeline)
if len(targetLyrics.Timeline) < minLength { if len(targetLyrics.Timeline) < minLength {
minLength = len(targetLyrics.Timeline) 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++ { for i := 0; i < minLength; i++ {
targetLyrics.Timeline[i] = sourceLyrics.Timeline[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 { switch targetFmt {
case "txt": case "txt":
lrcToTxt(sourceFile, targetFile) lrcToTxt(sourceFile, targetFile)
case "srt":
lrcToSrt(sourceFile, targetFile)
default: 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 { func lrcToTxt(sourceFile, targetFile string) {
if ts.Hours > 0 { sourceLyrics, err := parseLyrics(sourceFile)
return fmt.Sprintf("[%02d:%02d:%02d.%03d]", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds) 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)
} }