package ass import ( "bufio" "fmt" "os" "path/filepath" "regexp" "strconv" "strings" "sub-cli/internal/model" ) // 常量定义 const ( ASSHeader = "[Script Info]" ASSStylesHeader = "[V4+ Styles]" ASSEventsHeader = "[Events]" DefaultFormat = "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text" ) // Parse 解析ASS文件为ASSFile结构 func Parse(filePath string) (model.ASSFile, error) { file, err := os.Open(filePath) if err != nil { return model.ASSFile{}, fmt.Errorf("打开ASS文件失败: %w", err) } defer file.Close() result := model.NewASSFile() scanner := bufio.NewScanner(file) // 当前解析的区块 currentSection := "" var styleFormat, eventFormat []string for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, ";") { // 跳过空行和注释行 continue } // 检查章节标题 if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { currentSection = line continue } switch currentSection { case ASSHeader: // 解析脚本信息 if strings.Contains(line, ":") { parts := strings.SplitN(line, ":", 2) key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) result.ScriptInfo[key] = value } case ASSStylesHeader: // 解析样式格式行和样式定义 if strings.HasPrefix(line, "Format:") { formatStr := strings.TrimPrefix(line, "Format:") styleFormat = parseFormatLine(formatStr) } else if strings.HasPrefix(line, "Style:") { styleValues := parseStyleLine(line) if len(styleFormat) > 0 && len(styleValues) > 0 { style := model.ASSStyle{ Name: styleValues[0], // 第一个值通常是样式名称 Properties: make(map[string]string), } // 将原始格式行保存下来 style.Properties["Format"] = "Name, " + strings.Join(styleFormat[1:], ", ") style.Properties["Style"] = strings.Join(styleValues, ", ") // 解析各个样式属性 for i := 0; i < len(styleFormat) && i < len(styleValues); i++ { style.Properties[styleFormat[i]] = styleValues[i] } result.Styles = append(result.Styles, style) } } case ASSEventsHeader: // 解析事件格式行和对话行 if strings.HasPrefix(line, "Format:") { formatStr := strings.TrimPrefix(line, "Format:") eventFormat = parseFormatLine(formatStr) } else if len(eventFormat) > 0 && (strings.HasPrefix(line, "Dialogue:") || strings.HasPrefix(line, "Comment:")) { eventType := "Dialogue" if strings.HasPrefix(line, "Comment:") { eventType = "Comment" line = strings.TrimPrefix(line, "Comment:") } else { line = strings.TrimPrefix(line, "Dialogue:") } values := parseEventLine(line) if len(values) >= len(eventFormat) { event := model.NewASSEvent() event.Type = eventType // 填充事件属性 for i, format := range eventFormat { value := values[i] switch strings.TrimSpace(format) { case "Layer": layer, _ := strconv.Atoi(value) event.Layer = layer case "Start": event.StartTime = parseASSTimestamp(value) case "End": event.EndTime = parseASSTimestamp(value) case "Style": event.Style = value case "Name": event.Name = value case "MarginL": marginL, _ := strconv.Atoi(value) event.MarginL = marginL case "MarginR": marginR, _ := strconv.Atoi(value) event.MarginR = marginR case "MarginV": marginV, _ := strconv.Atoi(value) event.MarginV = marginV case "Effect": event.Effect = value case "Text": // 文本可能包含逗号,所以需要特殊处理 textStartIndex := strings.Index(line, value) if textStartIndex >= 0 { event.Text = line[textStartIndex:] } else { event.Text = value } } } result.Events = append(result.Events, event) } } } } if err := scanner.Err(); err != nil { return model.ASSFile{}, fmt.Errorf("读取ASS文件失败: %w", err) } return result, nil } // Generate 生成ASS文件 func Generate(assFile model.ASSFile, filePath string) error { // 确保目录存在 dir := filepath.Dir(filePath) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("创建目录失败: %w", err) } file, err := os.Create(filePath) if err != nil { return fmt.Errorf("创建ASS文件失败: %w", err) } defer file.Close() writer := bufio.NewWriter(file) // 写入脚本信息 writer.WriteString(ASSHeader + "\n") for key, value := range assFile.ScriptInfo { writer.WriteString(fmt.Sprintf("%s: %s\n", key, value)) } writer.WriteString("\n") // 写入样式信息 writer.WriteString(ASSStylesHeader + "\n") if len(assFile.Styles) > 0 { // 获取样式格式 format := "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding" if style := assFile.Styles[0]; style.Properties["Format"] != "" { format = "Format: " + style.Properties["Format"] } writer.WriteString(format + "\n") // 写入各个样式 for _, style := range assFile.Styles { if style.Properties["Style"] != "" { writer.WriteString("Style: " + style.Properties["Style"] + "\n") } else { // 手动构造样式行 writer.WriteString(fmt.Sprintf("Style: %s,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\n", style.Name)) } } } else { // 写入默认样式 writer.WriteString("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n") writer.WriteString("Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\n") } writer.WriteString("\n") // 写入事件信息 writer.WriteString(ASSEventsHeader + "\n") writer.WriteString(DefaultFormat + "\n") // 写入各个对话行 for _, event := range assFile.Events { startTime := formatASSTimestamp(event.StartTime) endTime := formatASSTimestamp(event.EndTime) line := fmt.Sprintf("%s: %d,%s,%s,%s,%s,%d,%d,%d,%s,%s\n", event.Type, event.Layer, startTime, endTime, event.Style, event.Name, event.MarginL, event.MarginR, event.MarginV, event.Effect, event.Text) writer.WriteString(line) } return writer.Flush() } // Format 格式化ASS文件 func Format(filePath string) error { // 解析文件 assFile, err := Parse(filePath) if err != nil { return err } // 重新生成文件 return Generate(assFile, filePath) } // ConvertToSubtitle 将ASS文件转换为通用字幕格式 func ConvertToSubtitle(filePath string) (model.Subtitle, error) { assFile, err := Parse(filePath) if err != nil { return model.Subtitle{}, err } subtitle := model.NewSubtitle() subtitle.Format = "ass" // 复制脚本信息到元数据 for key, value := range assFile.ScriptInfo { subtitle.Metadata[key] = value } // 复制样式信息到FormatData styleMap := make(map[string]model.ASSStyle) for _, style := range assFile.Styles { styleMap[style.Name] = style } subtitle.FormatData["styles"] = styleMap // 转换事件到字幕条目 for i, event := range assFile.Events { entry := model.NewSubtitleEntry() entry.Index = i + 1 entry.StartTime = event.StartTime entry.EndTime = event.EndTime entry.Text = event.Text // 保存ASS特定属性到FormatData eventData := make(map[string]interface{}) eventData["type"] = event.Type eventData["layer"] = event.Layer eventData["style"] = event.Style eventData["name"] = event.Name eventData["marginL"] = event.MarginL eventData["marginR"] = event.MarginR eventData["marginV"] = event.MarginV eventData["effect"] = event.Effect entry.FormatData["ass"] = eventData // 设置基本样式属性 if style, ok := styleMap[event.Style]; ok { if bold, exists := style.Properties["Bold"]; exists && bold == "1" { entry.Styles["bold"] = "true" } if italic, exists := style.Properties["Italic"]; exists && italic == "1" { entry.Styles["italic"] = "true" } if underline, exists := style.Properties["Underline"]; exists && underline == "1" { entry.Styles["underline"] = "true" } } subtitle.Entries = append(subtitle.Entries, entry) } return subtitle, nil } // ConvertFromSubtitle 将通用字幕格式转换为ASS文件 func ConvertFromSubtitle(subtitle model.Subtitle, filePath string) error { assFile := model.NewASSFile() // 复制元数据到脚本信息 for key, value := range subtitle.Metadata { assFile.ScriptInfo[key] = value } // 添加标题(如果有) if subtitle.Title != "" { assFile.ScriptInfo["Title"] = subtitle.Title } // 从FormatData恢复样式(如果有) if styles, ok := subtitle.FormatData["styles"].(map[string]model.ASSStyle); ok { for _, style := range styles { assFile.Styles = append(assFile.Styles, style) } } // 转换字幕条目到ASS事件 for _, entry := range subtitle.Entries { event := model.NewASSEvent() event.StartTime = entry.StartTime event.EndTime = entry.EndTime event.Text = entry.Text // 从FormatData恢复ASS特定属性(如果有) if assData, ok := entry.FormatData["ass"].(map[string]interface{}); ok { if eventType, ok := assData["type"].(string); ok { event.Type = eventType } if layer, ok := assData["layer"].(int); ok { event.Layer = layer } if style, ok := assData["style"].(string); ok { event.Style = style } if name, ok := assData["name"].(string); ok { event.Name = name } if marginL, ok := assData["marginL"].(int); ok { event.MarginL = marginL } if marginR, ok := assData["marginR"].(int); ok { event.MarginR = marginR } if marginV, ok := assData["marginV"].(int); ok { event.MarginV = marginV } if effect, ok := assData["effect"].(string); ok { event.Effect = effect } } else { // 根据基本样式设置ASS样式 if _, ok := entry.Styles["bold"]; ok { // 创建一个加粗样式(如果尚未存在) styleName := "Bold" found := false for _, style := range assFile.Styles { if style.Name == styleName { found = true break } } if !found { boldStyle := model.ASSStyle{ Name: styleName, Properties: map[string]string{ "Bold": "1", }, } assFile.Styles = append(assFile.Styles, boldStyle) } event.Style = styleName } if _, ok := entry.Styles["italic"]; ok { // 创建一个斜体样式(如果尚未存在) styleName := "Italic" found := false for _, style := range assFile.Styles { if style.Name == styleName { found = true break } } if !found { italicStyle := model.ASSStyle{ Name: styleName, Properties: map[string]string{ "Italic": "1", }, } assFile.Styles = append(assFile.Styles, italicStyle) } event.Style = styleName } if _, ok := entry.Styles["underline"]; ok { // 创建一个下划线样式(如果尚未存在) styleName := "Underline" found := false for _, style := range assFile.Styles { if style.Name == styleName { found = true break } } if !found { underlineStyle := model.ASSStyle{ Name: styleName, Properties: map[string]string{ "Underline": "1", }, } assFile.Styles = append(assFile.Styles, underlineStyle) } event.Style = styleName } } assFile.Events = append(assFile.Events, event) } // 生成ASS文件 return Generate(assFile, filePath) } // 辅助函数 // parseFormatLine 解析格式行中的各个字段 func parseFormatLine(formatStr string) []string { fields := strings.Split(formatStr, ",") result := make([]string, 0, len(fields)) for _, field := range fields { result = append(result, strings.TrimSpace(field)) } return result } // parseStyleLine 解析样式行 func parseStyleLine(line string) []string { // 去掉"Style:"前缀 styleStr := strings.TrimPrefix(line, "Style:") return splitCSV(styleStr) } // parseEventLine 解析事件行 func parseEventLine(line string) []string { return splitCSV(line) } // splitCSV 拆分CSV格式的字符串,但保留Text字段中的逗号 func splitCSV(line string) []string { var result []string inText := false current := "" for _, char := range line { if char == ',' && !inText { result = append(result, strings.TrimSpace(current)) current = "" } else { current += string(char) // 这是个简化处理,实际ASS格式更复杂 // 当处理到足够数量的字段后,剩余部分都当作Text字段 if len(result) >= 9 { inText = true } } } if current != "" { result = append(result, strings.TrimSpace(current)) } return result } // parseASSTimestamp 解析ASS格式的时间戳 (h:mm:ss.cc) func parseASSTimestamp(timeStr string) model.Timestamp { // 匹配 h:mm:ss.cc 格式 re := regexp.MustCompile(`(\d+):(\d+):(\d+)\.(\d+)`) matches := re.FindStringSubmatch(timeStr) if len(matches) == 5 { hours, _ := strconv.Atoi(matches[1]) minutes, _ := strconv.Atoi(matches[2]) seconds, _ := strconv.Atoi(matches[3]) // ASS使用厘秒(1/100秒),需要转换为毫秒 centiseconds, _ := strconv.Atoi(matches[4]) milliseconds := centiseconds * 10 return model.Timestamp{ Hours: hours, Minutes: minutes, Seconds: seconds, Milliseconds: milliseconds, } } // 返回零时间戳,如果解析失败 return model.Timestamp{} } // formatASSTimestamp 格式化为ASS格式的时间戳 (h:mm:ss.cc) func formatASSTimestamp(timestamp model.Timestamp) string { // ASS使用厘秒(1/100秒) centiseconds := timestamp.Milliseconds / 10 return fmt.Sprintf("%d:%02d:%02d.%02d", timestamp.Hours, timestamp.Minutes, timestamp.Seconds, centiseconds) }