// backend\meta.go package main import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" ) // -------------------------- // generated/meta//meta.json // -------------------------- type previewClip struct { StartSeconds float64 `json:"startSeconds"` DurationSeconds float64 `json:"durationSeconds"` } type previewSpriteMeta struct { Path string `json:"path"` // z.B. /api/preview-sprite/ Count int `json:"count"` // Anzahl Frames im Sprite Cols int `json:"cols"` // Spalten im Tile Rows int `json:"rows"` // Zeilen im Tile StepSeconds float64 `json:"stepSeconds"` // Zeitabstand zwischen Frames CellWidth int `json:"cellWidth,omitempty"` CellHeight int `json:"cellHeight,omitempty"` } type videoFileMeta struct { Size int64 `json:"size"` ModUnix int64 `json:"modUnix"` SourceURL string `json:"sourceUrl,omitempty"` } type videoStreamMeta struct { Width int `json:"width,omitempty"` Height int `json:"height,omitempty"` FPS float64 `json:"fps,omitempty"` Resolution string `json:"resolution,omitempty"` } type videoMediaMeta struct { DurationSeconds float64 `json:"durationSeconds"` Video videoStreamMeta `json:"video"` } type previewThumbMeta struct { Path string `json:"path,omitempty"` Exists bool `json:"exists,omitempty"` } type previewMeta struct { Clips []previewClip `json:"clips,omitempty"` Thumb *previewThumbMeta `json:"thumb,omitempty"` Sprite *previewSpriteMeta `json:"sprite,omitempty"` } type analysisMeta struct { AI *aiAnalysisMeta `json:"ai,omitempty"` } type postworkHistoryEntry struct { Queue string `json:"queue"` Phase string `json:"phase"` State string `json:"state"` Label string `json:"label,omitempty"` Position int `json:"position,omitempty"` Waiting int `json:"waiting,omitempty"` Running int `json:"running,omitempty"` MaxParallel int `json:"maxParallel,omitempty"` TS int64 `json:"ts"` } type postworkCompletedMeta struct { Meta bool `json:"meta,omitempty"` Teaser bool `json:"teaser,omitempty"` Thumb bool `json:"thumb,omitempty"` Sprites bool `json:"sprites,omitempty"` Analyze bool `json:"analyze,omitempty"` } type postworkMeta struct { Completed *postworkCompletedMeta `json:"completed,omitempty"` History []postworkHistoryEntry `json:"history,omitempty"` } type videoMeta struct { Version int `json:"version"` UpdatedAtUnix int64 `json:"updatedAtUnix"` File videoFileMeta `json:"file"` Media videoMediaMeta `json:"media"` Preview previewMeta `json:"preview,omitempty"` Analysis analysisMeta `json:"analysis,omitempty"` Postwork postworkMeta `json:"postwork,omitempty"` Validation map[string]any `json:"validation,omitempty"` } type aiSegmentMeta struct { Label string `json:"label"` StartSeconds float64 `json:"startSeconds"` EndSeconds float64 `json:"endSeconds"` DurationSeconds float64 `json:"durationSeconds"` Score float64 `json:"score,omitempty"` AutoSelected bool `json:"autoSelected,omitempty"` } type aiAnalysisMeta struct { Goal string `json:"goal,omitempty"` Mode string `json:"mode,omitempty"` Hits []analyzeHit `json:"hits,omitempty"` Segments []aiSegmentMeta `json:"segments,omitempty"` Rating *aiRatingMeta `json:"rating,omitempty"` AnalyzedAtUnix int64 `json:"analyzedAtUnix,omitempty"` } func readVideoMeta(metaPath string, fi os.FileInfo) (dur float64, w int, h int, fps float64, ok bool) { m, ok := readVideoMetaIfValid(metaPath, fi) if !ok || m == nil { return 0, 0, 0, 0, false } return m.Media.DurationSeconds, m.Media.Video.Width, m.Media.Video.Height, m.Media.Video.FPS, true } func readVideoMetaDuration(metaPath string, fi os.FileInfo) (float64, bool) { m, ok := readVideoMetaIfValid(metaPath, fi) if !ok || m == nil || m.Media.DurationSeconds <= 0 { return 0, false } return m.Media.DurationSeconds, true } func metaJSONPathForAssetID(assetID string) (string, error) { root, err := generatedMetaRoot() if err != nil { return "", err } if strings.TrimSpace(root) == "" { return "", fmt.Errorf("generated/meta root leer") } return filepath.Join(root, assetID, "meta.json"), nil } func ensureVideoMetaForFile(ctx context.Context, fullPath string, fi os.FileInfo, sourceURL string) (*videoMeta, bool) { stem := strings.TrimSuffix(filepath.Base(fullPath), filepath.Ext(fullPath)) assetID := stripHotPrefix(strings.TrimSpace(stem)) if assetID == "" { return nil, false } var err error assetID, err = sanitizeID(assetID) if err != nil || assetID == "" { return nil, false } metaPath, err := metaJSONPathForAssetID(assetID) if err != nil { return nil, false } if m, ok := readVideoMetaIfValid(metaPath, fi); ok { return m, true } if ctx == nil { ctx = context.Background() } cctx, cancel := context.WithTimeout(ctx, 8*time.Second) defer cancel() if durSem != nil { if err := durSem.Acquire(cctx); err != nil { return nil, false } defer durSem.Release() } dur, derr := durationSecondsCached(cctx, fullPath) if derr != nil || dur <= 0 { return nil, false } w, h, fps, perr := probeVideoProps(cctx, fullPath) if perr != nil { w, h, fps = 0, 0, 0 } _ = os.MkdirAll(filepath.Dir(metaPath), 0o755) if err := writeVideoMeta(metaPath, fi, dur, w, h, fps, sourceURL); err != nil { return nil, false } m, ok := readVideoMetaLoose(metaPath) if !ok || m == nil { return nil, false } return m, true } func attachMetaToJobBestEffort(ctx context.Context, job *RecordJob, fullPath string) { if job == nil { return } fullPath = strings.TrimSpace(fullPath) if fullPath == "" { return } fi, err := os.Stat(fullPath) if err != nil || fi == nil || fi.IsDir() { return } if job.SizeBytes <= 0 { job.SizeBytes = fi.Size() } m, ok := ensureVideoMetaForFileBestEffort(ctx, fullPath, job.SourceURL) if !ok || m == nil { return } job.Meta = m if job.DurationSeconds <= 0 && m.Media.DurationSeconds > 0 { job.DurationSeconds = m.Media.DurationSeconds } if job.VideoWidth <= 0 && m.Media.Video.Width > 0 { job.VideoWidth = m.Media.Video.Width } if job.VideoHeight <= 0 && m.Media.Video.Height > 0 { job.VideoHeight = m.Media.Video.Height } if job.FPS <= 0 && m.Media.Video.FPS > 0 { job.FPS = m.Media.Video.FPS } } func ensureVideoMetaForFileBestEffort(ctx context.Context, fullPath string, sourceURL string) (*videoMeta, bool) { fullPath = strings.TrimSpace(fullPath) if fullPath == "" { return nil, false } fi, err := os.Stat(fullPath) if err != nil || fi == nil || fi.IsDir() || fi.Size() <= 0 { return nil, false } if m, ok := ensureVideoMetaForFile(ctx, fullPath, fi, sourceURL); ok && m != nil { return m, true } dur := durationSecondsCacheOnly(fullPath, fi) if dur <= 0 { return nil, false } stem := strings.TrimSuffix(filepath.Base(fullPath), filepath.Ext(fullPath)) assetID := stripHotPrefix(strings.TrimSpace(stem)) if assetID == "" { return nil, false } assetID, err = sanitizeID(assetID) if err != nil || assetID == "" { return nil, false } metaPath, err := metaJSONPathForAssetID(assetID) if err != nil || strings.TrimSpace(metaPath) == "" { return nil, false } _ = os.MkdirAll(filepath.Dir(metaPath), 0o755) _ = writeVideoMetaDuration(metaPath, fi, dur, sourceURL) if m, ok := readVideoMetaIfValid(metaPath, fi); ok && m != nil { return m, true } return nil, false } func readVideoMetaIfValid(metaPath string, fi os.FileInfo) (*videoMeta, bool) { b, err := os.ReadFile(metaPath) if err != nil || len(b) == 0 { return nil, false } var m videoMeta if err := json.Unmarshal(b, &m); err != nil { return nil, false } if m.Version != 3 { return nil, false } if m.File.Size != fi.Size() || m.File.ModUnix != fi.ModTime().Unix() { return nil, false } if m.Media.DurationSeconds <= 0 { return nil, false } return &m, true } func readVideoMetaLoose(metaPath string) (*videoMeta, bool) { b, err := os.ReadFile(metaPath) if err != nil || len(b) == 0 { return nil, false } var m videoMeta if err := json.Unmarshal(b, &m); err != nil { return nil, false } if m.Version == 0 { m.Version = 3 } return &m, true } func mergePreviewSpriteMeta(existing, incoming *previewSpriteMeta) *previewSpriteMeta { if existing == nil && incoming == nil { return nil } if existing == nil { cp := *incoming return &cp } if incoming == nil { cp := *existing return &cp } out := *incoming if strings.TrimSpace(out.Path) == "" { out.Path = existing.Path } if out.Count <= 0 { out.Count = existing.Count } if out.Cols <= 0 { out.Cols = existing.Cols } if out.Rows <= 0 { out.Rows = existing.Rows } if out.StepSeconds <= 0 { out.StepSeconds = existing.StepSeconds } if out.CellWidth <= 0 { out.CellWidth = existing.CellWidth } if out.CellHeight <= 0 { out.CellHeight = existing.CellHeight } return &out } func hasAIAnalysisMeta(ai *aiAnalysisMeta) bool { if ai == nil { return false } if strings.TrimSpace(ai.Goal) != "" { return true } if strings.TrimSpace(ai.Mode) != "" { return true } if len(ai.Hits) > 0 { return true } if len(ai.Segments) > 0 { return true } if ai.AnalyzedAtUnix > 0 { return true } return false } func readVideoMetaSourceURL(metaPath string, fi os.FileInfo) (string, bool) { m, ok := readVideoMetaIfValid(metaPath, fi) if !ok || m == nil { return "", false } u := strings.TrimSpace(m.File.SourceURL) if u == "" { return "", false } return u, true } func writeVideoMeta(metaPath string, fi os.FileInfo, dur float64, w int, h int, fps float64, sourceURL string) error { if strings.TrimSpace(metaPath) == "" || dur <= 0 { return nil } var existing *videoMeta if old, ok := readVideoMetaLoose(metaPath); ok && old != nil { existing = old } m := videoMeta{ Version: 3, UpdatedAtUnix: time.Now().Unix(), File: videoFileMeta{ Size: fi.Size(), ModUnix: fi.ModTime().Unix(), SourceURL: strings.TrimSpace(sourceURL), }, Media: videoMediaMeta{ DurationSeconds: dur, Video: videoStreamMeta{ Width: w, Height: h, FPS: fps, Resolution: formatResolution(w, h), }, }, } if existing != nil { m.Preview = existing.Preview m.Analysis = existing.Analysis m.Postwork = existing.Postwork m.Validation = existing.Validation if m.File.SourceURL == "" { m.File.SourceURL = existing.File.SourceURL } if m.Media.Video.Width <= 0 { m.Media.Video.Width = existing.Media.Video.Width } if m.Media.Video.Height <= 0 { m.Media.Video.Height = existing.Media.Video.Height } if m.Media.Video.FPS <= 0 { m.Media.Video.FPS = existing.Media.Video.FPS } if m.Media.Video.Resolution == "" { m.Media.Video.Resolution = existing.Media.Video.Resolution } } buf, err := json.Marshal(m) if err != nil { return err } buf = append(buf, '\n') return atomicWriteFile(metaPath, buf) } func writeVideoMetaWithPreviewClips(metaPath string, fi os.FileInfo, dur float64, w int, h int, fps float64, sourceURL string, clips []previewClip) error { if strings.TrimSpace(metaPath) == "" || dur <= 0 { return nil } assetID := strings.TrimSpace(filepath.Base(filepath.Dir(metaPath))) var existing *videoMeta if old, ok := readVideoMetaLoose(metaPath); ok && old != nil { existing = old } m := videoMeta{ Version: 3, UpdatedAtUnix: time.Now().Unix(), File: videoFileMeta{ Size: fi.Size(), ModUnix: fi.ModTime().Unix(), SourceURL: strings.TrimSpace(sourceURL), }, Media: videoMediaMeta{ DurationSeconds: dur, Video: videoStreamMeta{ Width: w, Height: h, FPS: fps, Resolution: formatResolution(w, h), }, }, Preview: previewMeta{ Clips: clips, Thumb: buildPreviewThumbMeta(assetID), }, } if existing != nil { m.Analysis = existing.Analysis m.Postwork = existing.Postwork if existing.Preview.Sprite != nil && m.Preview.Sprite == nil { m.Preview.Sprite = existing.Preview.Sprite } if m.Preview.Thumb == nil && existing.Preview.Thumb != nil { m.Preview.Thumb = existing.Preview.Thumb } if m.File.SourceURL == "" { m.File.SourceURL = existing.File.SourceURL } if m.Media.Video.Width <= 0 { m.Media.Video.Width = existing.Media.Video.Width } if m.Media.Video.Height <= 0 { m.Media.Video.Height = existing.Media.Video.Height } if m.Media.Video.FPS <= 0 { m.Media.Video.FPS = existing.Media.Video.FPS } if m.Media.Video.Resolution == "" { m.Media.Video.Resolution = existing.Media.Video.Resolution } } buf, err := json.Marshal(m) if err != nil { return err } buf = append(buf, '\n') return atomicWriteFile(metaPath, buf) } func writeVideoMetaWithPreviewClipsAndSprite( metaPath string, fi os.FileInfo, dur float64, w int, h int, fps float64, sourceURL string, clips []previewClip, sprite *previewSpriteMeta, ) error { if strings.TrimSpace(metaPath) == "" || dur <= 0 { return nil } assetID := strings.TrimSpace(filepath.Base(filepath.Dir(metaPath))) var existing *videoMeta if old, ok := readVideoMetaLoose(metaPath); ok && old != nil { existing = old } m := videoMeta{ Version: 3, UpdatedAtUnix: time.Now().Unix(), File: videoFileMeta{ Size: fi.Size(), ModUnix: fi.ModTime().Unix(), SourceURL: strings.TrimSpace(sourceURL), }, Media: videoMediaMeta{ DurationSeconds: dur, Video: videoStreamMeta{ Width: w, Height: h, FPS: fps, Resolution: formatResolution(w, h), }, }, Preview: previewMeta{ Clips: clips, Thumb: buildPreviewThumbMeta(assetID), Sprite: sprite, }, } if existing != nil { m.Analysis = existing.Analysis m.Postwork = existing.Postwork m.Preview.Sprite = mergePreviewSpriteMeta(existing.Preview.Sprite, m.Preview.Sprite) if m.Preview.Thumb == nil && existing.Preview.Thumb != nil { m.Preview.Thumb = existing.Preview.Thumb } if len(m.Preview.Clips) == 0 && len(existing.Preview.Clips) > 0 { m.Preview.Clips = existing.Preview.Clips } if m.File.SourceURL == "" { m.File.SourceURL = existing.File.SourceURL } if m.Media.Video.Width <= 0 { m.Media.Video.Width = existing.Media.Video.Width } if m.Media.Video.Height <= 0 { m.Media.Video.Height = existing.Media.Video.Height } if m.Media.Video.FPS <= 0 { m.Media.Video.FPS = existing.Media.Video.FPS } if m.Media.Video.Resolution == "" { m.Media.Video.Resolution = existing.Media.Video.Resolution } if m.Media.DurationSeconds <= 0 { m.Media.DurationSeconds = existing.Media.DurationSeconds } } buf, err := json.Marshal(m) if err != nil { return err } buf = append(buf, '\n') return atomicWriteFile(metaPath, buf) } func writeVideoMetaDuration(metaPath string, fi os.FileInfo, dur float64, sourceURL string) error { return writeVideoMeta(metaPath, fi, dur, 0, 0, 0, sourceURL) } func generatedMetaFile(assetID string) (string, error) { assetID = stripHotPrefix(strings.TrimSpace(assetID)) if assetID == "" { return "", fmt.Errorf("empty assetID") } id, err := sanitizeID(assetID) if err != nil || id == "" { return "", fmt.Errorf("invalid assetID: %w", err) } return metaJSONPathForAssetID(id) } func generatedDirForID(id string) (string, error) { id, err := sanitizeID(id) if err != nil { return "", err } root, err := generatedMetaRoot() if err != nil { return "", err } if strings.TrimSpace(root) == "" { return "", fmt.Errorf("generated meta root ist leer") } return filepath.Join(root, id), nil } func ensureGeneratedDir(id string) (string, error) { dir, err := generatedDirForID(id) if err != nil { return "", err } if err := os.MkdirAll(dir, 0o755); err != nil { return "", err } return dir, nil } func formatResolution(w, h int) string { if w <= 0 || h <= 0 { return "" } return fmt.Sprintf("%dx%d", w, h) } func generatedThumbFile(id string) (string, error) { dir, err := generatedDirForID(id) if err != nil { return "", err } return filepath.Join(dir, "preview.jpg"), nil } func generatedPreviewFile(id string) (string, error) { dir, err := generatedDirForID(id) if err != nil { return "", err } return filepath.Join(dir, "preview.mp4"), nil } func generatedPreviewSpriteFile(id string) (string, error) { dir, err := generatedDirForID(id) if err != nil { return "", err } return filepath.Join(dir, "preview-sprite.jpg"), nil } func buildPreviewThumbMeta(assetID string) *previewThumbMeta { assetID = stripHotPrefix(strings.TrimSpace(assetID)) if assetID == "" { return nil } thumbPath, err := generatedThumbFile(assetID) if err != nil || strings.TrimSpace(thumbPath) == "" { return nil } if _, err := os.Stat(thumbPath); err != nil { return nil } return &previewThumbMeta{ Path: "/api/generated/cover?id=" + assetID, Exists: true, } } func ensureGeneratedDirs() error { root, err := generatedMetaRoot() if err != nil { return err } if strings.TrimSpace(root) == "" { return fmt.Errorf("generated meta root ist leer") } return os.MkdirAll(root, 0o755) } func sanitizeID(id string) (string, error) { id = strings.TrimSpace(id) if id == "" { return "", fmt.Errorf("id fehlt") } if strings.ContainsAny(id, `/\`) { return "", fmt.Errorf("ungültige id") } return id, nil } func writeVideoMetaAI( metaPath string, fi os.FileInfo, dur float64, w int, h int, fps float64, sourceURL string, ai *aiAnalysisMeta, ) error { if strings.TrimSpace(metaPath) == "" || dur <= 0 { return nil } var existing *videoMeta if old, ok := readVideoMetaLoose(metaPath); ok && old != nil { existing = old } m := videoMeta{ Version: 3, UpdatedAtUnix: time.Now().Unix(), File: videoFileMeta{ Size: fi.Size(), ModUnix: fi.ModTime().Unix(), SourceURL: strings.TrimSpace(sourceURL), }, Media: videoMediaMeta{ DurationSeconds: dur, Video: videoStreamMeta{ Width: w, Height: h, FPS: fps, Resolution: formatResolution(w, h), }, }, Analysis: analysisMeta{ AI: ai, }, } if existing != nil { m.Preview = existing.Preview m.Postwork = existing.Postwork if !hasAIAnalysisMeta(ai) && hasAIAnalysisMeta(existing.Analysis.AI) { m.Analysis = existing.Analysis } if m.Media.Video.Width <= 0 { m.Media.Video.Width = existing.Media.Video.Width } if m.Media.Video.Height <= 0 { m.Media.Video.Height = existing.Media.Video.Height } if m.Media.Video.FPS <= 0 { m.Media.Video.FPS = existing.Media.Video.FPS } if m.Media.Video.Resolution == "" { m.Media.Video.Resolution = existing.Media.Video.Resolution } if m.File.SourceURL == "" { m.File.SourceURL = existing.File.SourceURL } if m.Media.DurationSeconds <= 0 { m.Media.DurationSeconds = existing.Media.DurationSeconds } } buf, err := json.MarshalIndent(m, "", " ") if err != nil { return err } buf = append(buf, '\n') return atomicWriteFile(metaPath, buf) } func writeVideoAIForFile( ctx context.Context, fullPath string, sourceURL string, ai *aiAnalysisMeta, ) error { fullPath = strings.TrimSpace(fullPath) if fullPath == "" || ai == nil { return nil } fi, err := os.Stat(fullPath) if err != nil || fi == nil || fi.IsDir() || fi.Size() <= 0 { return fmt.Errorf("datei nicht gefunden") } m, ok := ensureVideoMetaForFileBestEffort(ctx, fullPath, sourceURL) if !ok || m == nil { return fmt.Errorf("meta konnte nicht erzeugt werden") } stem := strings.TrimSuffix(filepath.Base(fullPath), filepath.Ext(fullPath)) assetID := stripHotPrefix(strings.TrimSpace(stem)) if assetID == "" { return fmt.Errorf("asset id fehlt") } assetID, err = sanitizeID(assetID) if err != nil || assetID == "" { return fmt.Errorf("asset id ungültig: %w", err) } metaPath, err := metaJSONPathForAssetID(assetID) if err != nil { return err } return writeVideoMetaAI( metaPath, fi, m.Media.DurationSeconds, m.Media.Video.Width, m.Media.Video.Height, m.Media.Video.FPS, sourceURL, ai, ) } func writeVideoMetaPostworkEntry( metaPath string, fi os.FileInfo, entry postworkHistoryEntry, ) error { if strings.TrimSpace(metaPath) == "" { return nil } var existing videoMeta if old, ok := readVideoMetaLoose(metaPath); ok && old != nil { existing = *old } // Datei-Metadaten immer aktualisieren existing.Version = 3 existing.UpdatedAtUnix = time.Now().Unix() existing.File.Size = fi.Size() existing.File.ModUnix = fi.ModTime().Unix() queue := strings.TrimSpace(strings.ToLower(entry.Queue)) phase := strings.TrimSpace(strings.ToLower(entry.Phase)) if queue == "" { queue = "postwork" } if phase == "" { phase = "postwork" } entry.Queue = queue entry.Phase = phase if entry.TS <= 0 { entry.TS = time.Now().Unix() } history := existing.Postwork.History replaced := false for i := range history { if strings.EqualFold(history[i].Queue, entry.Queue) && strings.EqualFold(history[i].Phase, entry.Phase) { // nur ersetzen, wenn neuer oder gleich neu if history[i].TS <= entry.TS { history[i] = entry } replaced = true break } } if !replaced { history = append(history, entry) } existing.Postwork.History = history if existing.Postwork.Completed == nil { existing.Postwork.Completed = &postworkCompletedMeta{} } if strings.EqualFold(entry.State, "done") { switch queue { case "postwork": switch phase { case "postwork", "probe", "remuxing", "moving", "meta": existing.Postwork.Completed.Meta = true case "assets", "teaser": existing.Postwork.Completed.Teaser = true case "thumb": existing.Postwork.Completed.Thumb = true case "sprites", "sprite": existing.Postwork.Completed.Sprites = true } case "enrich": switch phase { case "analyze", "analysis", "ai": existing.Postwork.Completed.Analyze = true } } } buf, err := json.MarshalIndent(existing, "", " ") if err != nil { return err } buf = append(buf, '\n') return atomicWriteFile(metaPath, buf) } func readVideoMetaAI(metaPath string) (map[string]any, bool) { b, err := os.ReadFile(metaPath) if err != nil || len(b) == 0 { return nil, false } var m map[string]any dec := json.NewDecoder(strings.NewReader(string(b))) dec.UseNumber() if err := dec.Decode(&m); err != nil { return nil, false } rawAnalysis, ok := m["analysis"].(map[string]any) if !ok || rawAnalysis == nil { return nil, false } ai, ok := rawAnalysis["ai"].(map[string]any) if !ok || ai == nil { return nil, false } return ai, true } func writePostworkEntryForFile( fullPath string, queue string, phase string, state string, label string, position int, waiting int, running int, maxParallel int, ) error { fullPath = strings.TrimSpace(fullPath) if fullPath == "" { return fmt.Errorf("fullPath fehlt") } fi, err := os.Stat(fullPath) if err != nil || fi == nil || fi.IsDir() { return fmt.Errorf("datei nicht gefunden") } stem := strings.TrimSuffix(filepath.Base(fullPath), filepath.Ext(fullPath)) assetID := stripHotPrefix(strings.TrimSpace(stem)) if assetID == "" { return fmt.Errorf("asset id fehlt") } assetID, err = sanitizeID(assetID) if err != nil { return err } metaPath, err := metaJSONPathForAssetID(assetID) if err != nil { return err } // sorgt dafür, dass meta.json grundsätzlich existiert if _, ok := ensureVideoMetaForFileBestEffort(context.Background(), fullPath, ""); !ok { return fmt.Errorf("meta konnte nicht erzeugt werden") } return writeVideoMetaPostworkEntry(metaPath, fi, postworkHistoryEntry{ Queue: queue, Phase: phase, State: state, Label: label, Position: position, Waiting: waiting, Running: running, MaxParallel: maxParallel, TS: time.Now().Unix(), }) }