package lrc import ( "bufio" "fmt" "os" "regexp" "strconv" "strings" "sub-cli/internal/model" ) // Parse parses an LRC file and returns a Lyrics struct func Parse(filePath string) (model.Lyrics, error) { lyrics := model.Lyrics{ Metadata: make(map[string]string), } file, err := os.Open(filePath) if err != nil { return lyrics, err } defer file.Close() scanner := bufio.NewScanner(file) metadataRegex := regexp.MustCompile(`\[([\w:]+):(.*?)\]`) timestampRegex := regexp.MustCompile(`\[(\d+:\d+(?:\.\d+)?)\]`) for scanner.Scan() { line := scanner.Text() // Extract metadata metadataMatches := metadataRegex.FindAllStringSubmatch(line, -1) for _, match := range metadataMatches { if len(match) >= 3 { key := match[1] value := match[2] lyrics.Metadata[key] = value } } // Extract timestamp and content timestampMatches := timestampRegex.FindAllStringSubmatch(line, -1) if len(timestampMatches) > 0 { var timestamps []model.Timestamp lineContent := line for _, match := range timestampMatches { if len(match) >= 2 { timestamp, err := ParseTimestamp(match[1]) if err == nil { timestamps = append(timestamps, timestamp) } lineContent = strings.Replace(lineContent, match[0], "", 1) } } lineContent = strings.TrimSpace(lineContent) if lineContent != "" { for range timestamps { lyrics.Timeline = append(lyrics.Timeline, timestamps...) lyrics.Content = append(lyrics.Content, lineContent) } } } } return lyrics, nil } // ParseTimestamp parses an LRC timestamp string into a Timestamp struct func ParseTimestamp(timeStr string) (model.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 model.Timestamp{}, err } secParts := strings.Split(parts[1], ".") seconds, err = strconv.Atoi(secParts[0]) if err != nil { return model.Timestamp{}, err } if len(secParts) > 1 { milliseconds, err = strconv.Atoi(secParts[1]) if err != nil { return model.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 model.Timestamp{}, err } minutes, err = strconv.Atoi(parts[1]) if err != nil { return model.Timestamp{}, err } secParts := strings.Split(parts[2], ".") seconds, err = strconv.Atoi(secParts[0]) if err != nil { return model.Timestamp{}, err } if len(secParts) > 1 { milliseconds, err = strconv.Atoi(secParts[1]) if err != nil { return model.Timestamp{}, err } // adjust milliseconds based on the number of digits switch len(secParts[1]) { case 1: milliseconds *= 100 case 2: milliseconds *= 10 } } default: return model.Timestamp{}, fmt.Errorf("invalid timestamp format: %s", timeStr) } return model.Timestamp{ Hours: hours, Minutes: minutes, Seconds: seconds, Milliseconds: milliseconds, }, nil } // Generate generates an LRC file from a Lyrics struct func Generate(lyrics model.Lyrics, filePath string) 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 content with timestamps for i, content := range lyrics.Content { if i < len(lyrics.Timeline) { timestamp := lyrics.Timeline[i] fmt.Fprintf(file, "[%02d:%02d.%03d]%s\n", timestamp.Minutes, timestamp.Seconds, timestamp.Milliseconds, content) } else { fmt.Fprintln(file, content) } } return nil } // Format formats an LRC file func Format(filePath string) error { lyrics, err := Parse(filePath) if err != nil { return err } return Generate(lyrics, filePath) }