package sync import ( "fmt" "path/filepath" "strings" "sub-cli/internal/format/lrc" "sub-cli/internal/format/srt" "sub-cli/internal/format/vtt" "sub-cli/internal/model" ) // SyncLyrics synchronizes the timeline of a source lyrics file with a target lyrics file func SyncLyrics(sourceFile, targetFile string) error { sourceFmt := strings.TrimPrefix(filepath.Ext(sourceFile), ".") targetFmt := strings.TrimPrefix(filepath.Ext(targetFile), ".") // Check for supported format combinations if sourceFmt == "lrc" && targetFmt == "lrc" { return syncLRCFiles(sourceFile, targetFile) } else if sourceFmt == "srt" && targetFmt == "srt" { return syncSRTFiles(sourceFile, targetFile) } else if sourceFmt == "vtt" && targetFmt == "vtt" { return syncVTTFiles(sourceFile, targetFile) } else { return fmt.Errorf("sync only supports files of the same format (lrc-to-lrc, srt-to-srt, or vtt-to-vtt)") } } // syncLRCFiles synchronizes two LRC files func syncLRCFiles(sourceFile, targetFile string) error { source, err := lrc.Parse(sourceFile) if err != nil { return fmt.Errorf("error parsing source file: %w", err) } target, err := lrc.Parse(targetFile) if err != nil { return fmt.Errorf("error parsing target file: %w", err) } // Check if line counts match if len(source.Timeline) != len(target.Content) { fmt.Printf("Warning: Source timeline (%d entries) and target content (%d entries) have different counts. Timeline will be scaled.\n", len(source.Timeline), len(target.Content)) } // Apply timeline from source to target syncedLyrics := syncLRCTimeline(source, target) // Write the synced lyrics to the target file return lrc.Generate(syncedLyrics, targetFile) } // syncSRTFiles synchronizes two SRT files func syncSRTFiles(sourceFile, targetFile string) error { sourceEntries, err := srt.Parse(sourceFile) if err != nil { return fmt.Errorf("error parsing source SRT file: %w", err) } targetEntries, err := srt.Parse(targetFile) if err != nil { return fmt.Errorf("error parsing target SRT file: %w", err) } // Check if entry counts match if len(sourceEntries) != len(targetEntries) { fmt.Printf("Warning: Source (%d entries) and target (%d entries) have different entry counts. Timeline will be adjusted.\n", len(sourceEntries), len(targetEntries)) } // Sync the timelines syncedEntries := syncSRTTimeline(sourceEntries, targetEntries) // Write the synced entries to the target file return srt.Generate(syncedEntries, targetFile) } // syncVTTFiles synchronizes two VTT files func syncVTTFiles(sourceFile, targetFile string) error { sourceSubtitle, err := vtt.Parse(sourceFile) if err != nil { return fmt.Errorf("error parsing source VTT file: %w", err) } targetSubtitle, err := vtt.Parse(targetFile) if err != nil { return fmt.Errorf("error parsing target VTT file: %w", err) } // Check if entry counts match if len(sourceSubtitle.Entries) != len(targetSubtitle.Entries) { fmt.Printf("Warning: Source (%d entries) and target (%d entries) have different entry counts. Timeline will be adjusted.\n", len(sourceSubtitle.Entries), len(targetSubtitle.Entries)) } // Sync the timelines syncedSubtitle := syncVTTTimeline(sourceSubtitle, targetSubtitle) // Write the synced subtitle to the target file return vtt.Generate(syncedSubtitle, targetFile) } // syncLRCTimeline applies the timeline from the source lyrics to the target lyrics func syncLRCTimeline(source, target model.Lyrics) model.Lyrics { result := model.Lyrics{ Metadata: target.Metadata, Content: target.Content, } // Create timeline with same length as target content result.Timeline = make([]model.Timestamp, len(target.Content)) // Use source timeline if available and lengths match if len(source.Timeline) > 0 && len(source.Timeline) == len(target.Content) { copy(result.Timeline, source.Timeline) } else if len(source.Timeline) > 0 { // If lengths don't match, scale timeline using our improved scaleTimeline function result.Timeline = scaleTimeline(source.Timeline, len(target.Content)) } return result } // syncSRTTimeline applies the timing from source SRT entries to target SRT entries func syncSRTTimeline(sourceEntries, targetEntries []model.SRTEntry) []model.SRTEntry { result := make([]model.SRTEntry, len(targetEntries)) // Copy target entries copy(result, targetEntries) // If source and target have the same number of entries, directly apply timings if len(sourceEntries) == len(targetEntries) { for i := range result { result[i].StartTime = sourceEntries[i].StartTime result[i].EndTime = sourceEntries[i].EndTime } } else { // If entry counts differ, scale the timing for i := range result { // Calculate scaled index sourceIdx := 0 if len(sourceEntries) > 1 { sourceIdx = i * (len(sourceEntries) - 1) / (len(targetEntries) - 1) } // Ensure the index is within bounds if sourceIdx >= len(sourceEntries) { sourceIdx = len(sourceEntries) - 1 } // Apply the scaled timing result[i].StartTime = sourceEntries[sourceIdx].StartTime // Calculate end time: if not the last entry, use duration from source if i < len(result)-1 { // If next source entry exists, calculate duration var duration model.Timestamp if sourceIdx+1 < len(sourceEntries) { duration = calculateDuration(sourceEntries[sourceIdx].StartTime, sourceEntries[sourceIdx+1].StartTime) } else { // If no next source entry, use the source's end time (usually a few seconds after start) duration = calculateDuration(sourceEntries[sourceIdx].StartTime, sourceEntries[sourceIdx].EndTime) } // Apply duration to next start time result[i].EndTime = addDuration(result[i].StartTime, duration) } else { // For the last entry, add a fixed duration (e.g., 3 seconds) result[i].EndTime = sourceEntries[sourceIdx].EndTime } } } // Ensure proper sequence numbering for i := range result { result[i].Number = i + 1 } return result } // syncVTTTimeline applies the timing from source VTT subtitle to target VTT subtitle func syncVTTTimeline(source, target model.Subtitle) model.Subtitle { result := model.NewSubtitle() result.Format = "vtt" result.Title = target.Title result.Metadata = target.Metadata result.Styles = target.Styles // Create entries array with same length as target result.Entries = make([]model.SubtitleEntry, len(target.Entries)) // Copy target entries copy(result.Entries, target.Entries) // 如果源字幕为空或目标字幕为空,直接返回复制的目标内容 if len(source.Entries) == 0 || len(target.Entries) == 0 { // 确保索引编号正确 for i := range result.Entries { result.Entries[i].Index = i + 1 } return result } // If source and target have the same number of entries, directly apply timings if len(source.Entries) == len(target.Entries) { for i := range result.Entries { result.Entries[i].StartTime = source.Entries[i].StartTime result.Entries[i].EndTime = source.Entries[i].EndTime } } else { // If entry counts differ, scale the timing similar to SRT sync for i := range result.Entries { // Calculate scaled index sourceIdx := 0 if len(source.Entries) > 1 { sourceIdx = i * (len(source.Entries) - 1) / (len(target.Entries) - 1) } // Ensure the index is within bounds if sourceIdx >= len(source.Entries) { sourceIdx = len(source.Entries) - 1 } // Apply the scaled timing result.Entries[i].StartTime = source.Entries[sourceIdx].StartTime // Calculate end time: if not the last entry, use duration from source if i < len(result.Entries)-1 { // If next source entry exists, calculate duration var duration model.Timestamp if sourceIdx+1 < len(source.Entries) { duration = calculateDuration(source.Entries[sourceIdx].StartTime, source.Entries[sourceIdx+1].StartTime) } else { duration = calculateDuration(source.Entries[sourceIdx].StartTime, source.Entries[sourceIdx].EndTime) } result.Entries[i].EndTime = addDuration(result.Entries[i].StartTime, duration) } else { // For the last entry, use the end time from source result.Entries[i].EndTime = source.Entries[sourceIdx].EndTime } } } // Ensure proper index numbering for i := range result.Entries { result.Entries[i].Index = i + 1 } return result } // scaleTimeline scales a timeline to match a different number of entries func scaleTimeline(timeline []model.Timestamp, targetCount int) []model.Timestamp { if targetCount <= 0 || len(timeline) == 0 { return []model.Timestamp{} } result := make([]model.Timestamp, targetCount) if targetCount == 1 { result[0] = timeline[0] return result } sourceLength := len(timeline) // Handle simple case: same length if targetCount == sourceLength { copy(result, timeline) return result } // Handle case where target is longer than source // We need to interpolate timestamps between source entries for i := 0; i < targetCount; i++ { if sourceLength == 1 { // If source has only one entry, use it for all target entries result[i] = timeline[0] continue } // Calculate a floating-point position in the source timeline floatIndex := float64(i) * float64(sourceLength-1) / float64(targetCount-1) lowerIndex := int(floatIndex) upperIndex := lowerIndex + 1 // Handle boundary case if upperIndex >= sourceLength { upperIndex = sourceLength - 1 lowerIndex = upperIndex - 1 } // If indices are the same, just use the source timestamp if lowerIndex == upperIndex || lowerIndex < 0 { result[i] = timeline[upperIndex] } else { // Calculate the fraction between the lower and upper indices fraction := floatIndex - float64(lowerIndex) // Convert timestamps to milliseconds for interpolation lowerMS := timeline[lowerIndex].Hours*3600000 + timeline[lowerIndex].Minutes*60000 + timeline[lowerIndex].Seconds*1000 + timeline[lowerIndex].Milliseconds upperMS := timeline[upperIndex].Hours*3600000 + timeline[upperIndex].Minutes*60000 + timeline[upperIndex].Seconds*1000 + timeline[upperIndex].Milliseconds // Interpolate resultMS := int(float64(lowerMS) + fraction*float64(upperMS-lowerMS)) // Convert back to timestamp hours := resultMS / 3600000 resultMS %= 3600000 minutes := resultMS / 60000 resultMS %= 60000 seconds := resultMS / 1000 milliseconds := resultMS % 1000 result[i] = model.Timestamp{ Hours: hours, Minutes: minutes, Seconds: seconds, Milliseconds: milliseconds, } } } return result } // calculateDuration calculates the time difference between two timestamps func calculateDuration(start, end model.Timestamp) model.Timestamp { startMillis := start.Hours*3600000 + start.Minutes*60000 + start.Seconds*1000 + start.Milliseconds endMillis := end.Hours*3600000 + end.Minutes*60000 + end.Seconds*1000 + end.Milliseconds durationMillis := endMillis - startMillis if durationMillis < 0 { // Return zero duration if end is before start return model.Timestamp{ Hours: 0, Minutes: 0, Seconds: 0, Milliseconds: 0, } } hours := durationMillis / 3600000 durationMillis %= 3600000 minutes := durationMillis / 60000 durationMillis %= 60000 seconds := durationMillis / 1000 milliseconds := durationMillis % 1000 return model.Timestamp{ Hours: hours, Minutes: minutes, Seconds: seconds, Milliseconds: milliseconds, } } // addDuration adds a duration to a timestamp func addDuration(start, duration model.Timestamp) model.Timestamp { startMillis := start.Hours*3600000 + start.Minutes*60000 + start.Seconds*1000 + start.Milliseconds durationMillis := duration.Hours*3600000 + duration.Minutes*60000 + duration.Seconds*1000 + duration.Milliseconds totalMillis := startMillis + durationMillis hours := totalMillis / 3600000 totalMillis %= 3600000 minutes := totalMillis / 60000 totalMillis %= 60000 seconds := totalMillis / 1000 milliseconds := totalMillis % 1000 return model.Timestamp{ Hours: hours, Minutes: minutes, Seconds: seconds, Milliseconds: milliseconds, } }