Compare commits

...

5 commits

Author SHA1 Message Date
CDN18
82e67b1a32
chore: not WIP anymore 2024-09-27 21:04:33 +08:00
CDN18
9d031072ad
chore: bump version 2024-09-27 21:00:46 +08:00
CDN18
7d5a8bdf54
feat: support srt 2024-09-27 20:59:59 +08:00
CDN18
00deeaf425
chore: prepare for srt support + force sync lrc timestamp if timeline length mismatch 2024-09-27 20:39:48 +08:00
CDN18
6875b43b78
refactor: parse Timeline as Timestamp[] 2024-09-27 20:28:22 +08:00
6 changed files with 316 additions and 60 deletions

View file

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

122
convert.go Normal file
View file

@ -0,0 +1,122 @@
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)),
}
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

@ -4,11 +4,77 @@ 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 {
@ -27,9 +93,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 +134,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
@ -91,16 +161,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]
}
@ -114,39 +182,14 @@ 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)
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)
}
}
@ -173,21 +216,9 @@ 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)
}
return fmt.Sprintf("[%02d:%02d.%03d]", ts.Minutes, ts.Seconds, ts.Milliseconds)
}

View file

@ -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":

View file

@ -1,22 +1,39 @@
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
}
type SRTEntry struct {
Number int
StartTime Timestamp
EndTime Timestamp
Content string
}
const (
VERSION = "0.2.0"
VERSION = "0.3.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 <source> <target>`
CONVERT_USAGE = `Usage: lyc-cli convert <source> <target>
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`
)

86
srt.go Normal file
View file

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