From 2ce88069a93a17fc3f55495311dd8ab3fd4ef712 Mon Sep 17 00:00:00 2001 From: 0x1d Date: Tue, 18 Nov 2025 13:36:38 +0100 Subject: [PATCH] fix: stuff --- internal/downloader/downloader.go | 160 +++++++++++++++++++++++++----- 1 file changed, 137 insertions(+), 23 deletions(-) diff --git a/internal/downloader/downloader.go b/internal/downloader/downloader.go index 1191377..b4e885c 100644 --- a/internal/downloader/downloader.go +++ b/internal/downloader/downloader.go @@ -208,29 +208,42 @@ func downloadPlaylist(ctx context.Context, client *http.Client, parsed *url.URL, // Create a client with longer timeout for VOD segments vodClient := &http.Client{Timeout: vodSegmentTimeout} - // Create temp file for concatenated segments - tempTS, err := os.CreateTemp("", "sdl-*.ts") + // Create temp directory for segments + tempDir, err := os.MkdirTemp("", "sdl-segments-*") if err != nil { - return fmt.Errorf("create temp file: %w", err) + return fmt.Errorf("create temp directory: %w", err) } - tempPath := tempTS.Name() - defer os.Remove(tempPath) + defer os.RemoveAll(tempDir) segments := collectSegments(mediaPlaylist) totalSegments := len(segments) fmt.Fprintf(os.Stderr, "Downloading %d segments...\n", totalSegments) - // Download and concatenate segments directly + // Download each segment to a separate file + segmentFiles := make([]string, 0, totalSegments) for i, segment := range segments { - if err := downloadSegment(vodCtx, vodClient, finalURL, segment, tempTS); err != nil { - tempTS.Close() + segmentFile := filepath.Join(tempDir, fmt.Sprintf("segment-%05d.ts", i)) + file, err := os.Create(segmentFile) + if err != nil { + return fmt.Errorf("create segment file %d: %w", i, err) + } + + if err := downloadSegment(vodCtx, vodClient, finalURL, segment, file); err != nil { + file.Close() return fmt.Errorf("download segment %d: %w", i, err) } - // Flush after each segment to ensure data is written - if err := tempTS.Sync(); err != nil { - tempTS.Close() - return fmt.Errorf("sync after segment %d: %w", i, err) + + if err := file.Sync(); err != nil { + file.Close() + return fmt.Errorf("sync segment file %d: %w", i, err) } + + if err := file.Close(); err != nil { + return fmt.Errorf("close segment file %d: %w", i, err) + } + + segmentFiles = append(segmentFiles, segmentFile) + // Show progress: segment number, total, and percentage progress := float64(i+1) / float64(totalSegments) * 100 fmt.Fprintf(os.Stderr, "\rProgress: %d/%d segments (%.1f%%)", i+1, totalSegments, progress) @@ -238,23 +251,31 @@ func downloadPlaylist(ctx context.Context, client *http.Client, parsed *url.URL, } fmt.Fprintf(os.Stderr, "\n") - // Close the file before conversion - if err := tempTS.Close(); err != nil { - return fmt.Errorf("close temp file: %w", err) + // Create concat file for ffmpeg concat protocol (not demuxer) + // The concat protocol format is different: just file paths, one per line + concatFile := filepath.Join(tempDir, "concat.txt") + concatF, err := os.Create(concatFile) + if err != nil { + return fmt.Errorf("create concat file: %w", err) } - // Verify the file exists and has content - stat, err := os.Stat(tempPath) - if err != nil { - return fmt.Errorf("stat temp file: %w", err) + for _, segFile := range segmentFiles { + // Use absolute path for concat protocol + absPath, err := filepath.Abs(segFile) + if err != nil { + concatF.Close() + return fmt.Errorf("get absolute path: %w", err) + } + // Concat protocol format: just the path, one per line + fmt.Fprintf(concatF, "%s\n", absPath) } - if stat.Size() == 0 { - return fmt.Errorf("downloaded file is empty") + if err := concatF.Close(); err != nil { + return fmt.Errorf("close concat file: %w", err) } mp4Name := ensureMP4Extension(name) - fmt.Fprintf(os.Stderr, "Converting to MP4 (%.2f MB)...\n", float64(stat.Size())/1024/1024) - if err := transmuxToMP4(vodCtx, tempPath, mp4Name); err != nil { + fmt.Fprintf(os.Stderr, "Converting to MP4...\n") + if err := transmuxToMP4FromConcatProtocol(vodCtx, concatFile, mp4Name); err != nil { return err } fmt.Fprintf(os.Stderr, "Complete: %s\n", mp4Name) @@ -664,6 +685,99 @@ func inferOutputNameMust(rawURL string) string { return inferOutputName(parsed) } +func transmuxToMP4FromConcatProtocol(ctx context.Context, concatFile, mp4Path string) error { + if _, err := exec.LookPath("ffmpeg"); err != nil { + return errFFmpegMissing + } + + if err := os.MkdirAll(filepath.Dir(mp4Path), 0o755); err != nil { + return fmt.Errorf("ensure output directory: %w", err) + } + + // Use ffmpeg concat protocol (not demuxer) - this is designed for TS files + // Format: concat:file1.ts|file2.ts|file3.ts + // Or we can use the file list with concat protocol + absConcatFile, err := filepath.Abs(concatFile) + if err != nil { + return fmt.Errorf("get absolute path for concat file: %w", err) + } + + // Read the concat file to build the concat protocol string + concatContent, err := os.ReadFile(absConcatFile) + if err != nil { + return fmt.Errorf("read concat file: %w", err) + } + + lines := strings.Split(string(concatContent), "\n") + var segmentPaths []string + for _, line := range lines { + line = strings.TrimSpace(line) + if line != "" { + segmentPaths = append(segmentPaths, line) + } + } + + if len(segmentPaths) == 0 { + return fmt.Errorf("no segments found in concat file") + } + + // Build concat protocol string: concat:file1.ts|file2.ts|... + concatInput := "concat:" + strings.Join(segmentPaths, "|") + + // Use concat protocol with better flags for TS files + cmd := exec.CommandContext(ctx, "ffmpeg", + "-y", + "-i", concatInput, + "-c", "copy", + "-movflags", "+faststart", + mp4Path) + cmd.Stdout = io.Discard + var stderr bytes.Buffer + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + // If copy fails, try with re-encoding + msg := strings.TrimSpace(stderr.String()) + needsReencode := strings.Contains(msg, "could not find corresponding") || + strings.Contains(msg, "error reading header") || + strings.Contains(msg, "Invalid data") || + strings.Contains(msg, "Invalid argument") + + if needsReencode { + fmt.Fprintf(os.Stderr, "Copy mode failed, trying re-encode...\n") + cmd2 := exec.CommandContext(ctx, "ffmpeg", + "-y", + "-i", concatInput, + "-c:v", "libx264", + "-preset", "medium", + "-crf", "23", + "-c:a", "aac", + "-b:a", "128k", + "-movflags", "+faststart", + mp4Path) + cmd2.Stdout = io.Discard + var stderr2 bytes.Buffer + cmd2.Stderr = &stderr2 + if err2 := cmd2.Run(); err2 != nil { + os.Remove(mp4Path) + msg2 := strings.TrimSpace(stderr2.String()) + if msg2 != "" { + return fmt.Errorf("ffmpeg (re-encode): %s", msg2) + } + return fmt.Errorf("ffmpeg (re-encode): %w", err2) + } + return nil + } + os.Remove(mp4Path) + if msg != "" { + return fmt.Errorf("ffmpeg: %s", msg) + } + return fmt.Errorf("ffmpeg: %w", err) + } + + return nil +} + func transmuxToMP4FromConcat(ctx context.Context, concatFile, mp4Path string) error { if _, err := exec.LookPath("ffmpeg"); err != nil { return errFFmpegMissing