Compare commits
10 commits
0c57078a8a
...
9f96b255ae
Author | SHA1 | Date | |
---|---|---|---|
9f96b255ae | |||
28b49b7f78 | |||
|
1556b11d5f | ||
|
82e67b1a32 | ||
|
9d031072ad | ||
|
7d5a8bdf54 | ||
|
00deeaf425 | ||
|
6875b43b78 | ||
|
45e42ce4ff | ||
|
1b1862cb3f |
9 changed files with 579 additions and 139 deletions
68
.forgejo/workflows/ci.yml
Normal file
68
.forgejo/workflows/ci.yml
Normal file
|
@ -0,0 +1,68 @@
|
|||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: docker
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, windows, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
export GOOS=${{ matrix.goos }}
|
||||
export GOARCH=${{ matrix.goarch }}
|
||||
export CGO_ENABLED=0
|
||||
go build -v -o sub-cli-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goos == 'windows' && '.exe' || '' }}
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: forgejo/upload-artifact@v4
|
||||
with:
|
||||
name: sub-cli-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: sub-cli-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goos == 'windows' && '.exe' || '' }}
|
||||
retention-days: 7
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: docker
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: forgejo/download-artifact@v4
|
||||
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: https://github.com/softprops/action-gh-release@v2
|
||||
with:
|
||||
name: Release ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: |
|
||||
sub-cli-linux-amd64/sub-cli-linux-amd64
|
||||
sub-cli-linux-arm64/sub-cli-linux-arm64
|
||||
sub-cli-darwin-amd64/sub-cli-darwin-amd64
|
||||
sub-cli-darwin-arm64/sub-cli-darwin-arm64
|
||||
sub-cli-windows-amd64/sub-cli-windows-amd64.exe
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACTIONS_TOKEN }}
|
|
@ -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
|
||||
|
|
128
convert.go
Normal file
128
convert.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
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)
|
||||
}
|
||||
}
|
2
go.mod
2
go.mod
|
@ -1,3 +1,3 @@
|
|||
module lrc-cli
|
||||
module sub-cli
|
||||
|
||||
go 1.22.6
|
||||
|
|
224
lrc.go
Normal file
224
lrc.go
Normal file
|
@ -0,0 +1,224 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"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 {
|
||||
return Lyrics{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
lyrics := Lyrics{
|
||||
Metadata: make(map[string]string),
|
||||
}
|
||||
timeLineRegex := regexp.MustCompile(`\[((\d+:)?\d+:\d+(\.\d+)?)\]`)
|
||||
tagRegex := regexp.MustCompile(`\[(\w+):(.+)\]`)
|
||||
|
||||
for scanner.Scan() {
|
||||
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)
|
||||
// Content
|
||||
content := timeLineRegex.ReplaceAllString(line, "")
|
||||
lyrics.Content = append(lyrics.Content, strings.TrimSpace(content))
|
||||
} else {
|
||||
// Metadata
|
||||
matches := tagRegex.FindStringSubmatch(line)
|
||||
if len(matches) == 3 {
|
||||
lyrics.Metadata[matches[1]] = strings.TrimSpace(matches[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return Lyrics{}, err
|
||||
}
|
||||
|
||||
return lyrics, nil
|
||||
}
|
||||
|
||||
func saveLyrics(filePath string, lyrics Lyrics) error {
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Write metadata
|
||||
for key, value := range lyrics.Metadata {
|
||||
fmt.Fprintf(file, "[%s: %s]\n", key, value)
|
||||
}
|
||||
|
||||
// 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])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncLyrics(args []string) {
|
||||
if len(args) < 2 {
|
||||
fmt.Println(SYNC_USAGE)
|
||||
return
|
||||
}
|
||||
|
||||
sourceFile := args[0]
|
||||
targetFile := args[1]
|
||||
|
||||
sourceLyrics, err := parseLyrics(sourceFile)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing source lyrics file:", err)
|
||||
return
|
||||
}
|
||||
|
||||
targetLyrics, err := parseLyrics(targetFile)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing target lyrics file:", err)
|
||||
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]
|
||||
}
|
||||
|
||||
// save to target, name it as "<filename>_synced.lrc"
|
||||
targetFileName := strings.TrimSuffix(targetFile, ".lrc") + "_synced.lrc"
|
||||
err = saveLyrics(targetFileName, targetLyrics)
|
||||
if err != nil {
|
||||
fmt.Println("Error saving synced lyrics file:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func convertLyrics(sourceFile, targetFile, targetFmt string) {
|
||||
switch targetFmt {
|
||||
case "txt":
|
||||
lrcToTxt(sourceFile, targetFile)
|
||||
case "srt":
|
||||
lrcToSrt(sourceFile, targetFile)
|
||||
default:
|
||||
fmt.Printf("unsupported target format: %s\n", targetFmt)
|
||||
}
|
||||
}
|
||||
|
||||
func fmtLyrics(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Println("Usage: lyc-cli fmt <source>")
|
||||
return
|
||||
}
|
||||
|
||||
sourceFile := args[0]
|
||||
|
||||
sourceLyrics, err := parseLyrics(sourceFile)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing source lyrics file:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// save to target (source_name_fmt.lrc)
|
||||
targetFile := strings.TrimSuffix(sourceFile, ".lrc") + "_fmt.lrc"
|
||||
err = saveLyrics(targetFile, sourceLyrics)
|
||||
if err != nil {
|
||||
fmt.Println("Error saving formatted lyrics file:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
6
main.go
6
main.go
|
@ -14,6 +14,12 @@ func main() {
|
|||
switch os.Args[1] {
|
||||
case "sync":
|
||||
syncLyrics(os.Args[2:])
|
||||
case "convert":
|
||||
convert(os.Args[2:])
|
||||
case "fmt":
|
||||
fmtLyrics(os.Args[2:])
|
||||
case "version":
|
||||
fmt.Printf("sub-cli version %s\n", VERSION)
|
||||
case "help":
|
||||
fmt.Println(USAGE)
|
||||
default:
|
||||
|
|
33
model.go
33
model.go
|
@ -1,14 +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
|
||||
}
|
||||
|
||||
const USAGE = `Usage: lyc-cli [command] [options]
|
||||
Commands:
|
||||
type SRTEntry struct {
|
||||
Number int
|
||||
StartTime Timestamp
|
||||
EndTime Timestamp
|
||||
Content string
|
||||
}
|
||||
|
||||
const (
|
||||
VERSION = "0.3.0"
|
||||
USAGE = `Usage: sub-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`
|
||||
|
||||
const SYNC_USAGE = `Usage: lyc-cli sync <source> <target>`
|
||||
SYNC_USAGE = `Usage: sub-cli sync <source> <target>`
|
||||
CONVERT_USAGE = `Usage: sub-cli convert <source> <target>
|
||||
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`
|
||||
)
|
||||
|
|
120
srt.go
Normal file
120
srt.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
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,
|
||||
}
|
||||
}
|
131
util.go
131
util.go
|
@ -1,131 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseLyrics(filePath string) (Lyrics, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return Lyrics{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
lyrics := Lyrics{
|
||||
Metadata: make(map[string]string),
|
||||
}
|
||||
timeLineRegex := regexp.MustCompile(`\[((\d+:)?\d+:\d+(\.\d+)?)\]`)
|
||||
tagRegex := regexp.MustCompile(`\[(\w+):(.+)\]`)
|
||||
|
||||
for scanner.Scan() {
|
||||
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)
|
||||
// Content
|
||||
content := timeLineRegex.ReplaceAllString(line, "")
|
||||
lyrics.Content = append(lyrics.Content, strings.TrimSpace(content))
|
||||
} else {
|
||||
// Metadata
|
||||
matches := tagRegex.FindStringSubmatch(line)
|
||||
if len(matches) == 3 {
|
||||
lyrics.Metadata[matches[1]] = strings.TrimSpace(matches[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return Lyrics{}, err
|
||||
}
|
||||
|
||||
return lyrics, nil
|
||||
}
|
||||
|
||||
func syncLyrics(args []string) {
|
||||
if len(args) < 2 {
|
||||
fmt.Println(SYNC_USAGE)
|
||||
return
|
||||
}
|
||||
|
||||
sourceFile := args[0]
|
||||
targetFile := args[1]
|
||||
|
||||
sourceLyrics, err := parseLyrics(sourceFile)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing source lyrics file:", err)
|
||||
return
|
||||
}
|
||||
|
||||
targetLyrics, err := parseLyrics(targetFile)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing target lyrics file:", err)
|
||||
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)
|
||||
}
|
||||
|
||||
for i := 0; i < minLength; i++ {
|
||||
targetLyrics.Timeline[i] = sourceLyrics.Timeline[i]
|
||||
}
|
||||
|
||||
// save to target, name it as "<filename>_synced.lrc"
|
||||
targetFileName := strings.TrimSuffix(targetFile, ".lrc") + "_synced.lrc"
|
||||
err = saveLyrics(targetFileName, targetLyrics)
|
||||
if err != nil {
|
||||
fmt.Println("Error saving synced lyrics file:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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 saveLyrics(filePath string, lyrics Lyrics) error {
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Write metadata
|
||||
for key, value := range lyrics.Metadata {
|
||||
fmt.Fprintf(file, "[%s: %s]\n", key, value)
|
||||
}
|
||||
|
||||
// Write timeline and content
|
||||
for i := 0; i < len(lyrics.Timeline); i++ {
|
||||
fmt.Fprintf(file, "%s %s\n", lyrics.Timeline[i], lyrics.Content[i])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue