Files
SaveAny-Bot/storage/telegram/video_split_test.go
Haopeng Huo e4144e73e6 feat(telegram): split oversized videos into playable parts (#229)
* feat(telegram): split oversized videos losslessly

* fix(telegram): respect Premium upload limits

* fix(telegram): keep split videos in one album
2026-08-05 16:57:46 +08:00

175 lines
4.9 KiB
Go

package telegram
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
func TestInitialSegmentDuration(t *testing.T) {
tests := []struct {
name string
duration float64
fileSize int64
targetSize int64
want float64
}{
{name: "two balanced parts", duration: 120, fileSize: 2200, targetSize: 1900, want: 60},
{name: "three balanced parts", duration: 120, fileSize: 3900, targetSize: 1900, want: 40},
{name: "minimum one second", duration: 1.5, fileSize: 3900, targetSize: 1900, want: 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := initialSegmentDuration(tt.duration, tt.fileSize, tt.targetSize)
if got != tt.want {
t.Fatalf("initialSegmentDuration() = %v, want %v", got, tt.want)
}
})
}
}
func TestVideoPartExtension(t *testing.T) {
tests := map[string]string{
"clip.MOV": ".mov",
"clip.mkv": ".mkv",
"clip.exe": ".mp4",
"clip": ".mp4",
}
for input, want := range tests {
if got := videoPartExtension(input); got != want {
t.Fatalf("videoPartExtension(%q) = %q, want %q", input, got, want)
}
}
}
func TestPartStoragePath(t *testing.T) {
tests := map[string]string{
"clip.mov": "clip.part001.mov",
"123456/clip.mov": "123456/clip.part001.mov",
"/123456/clip.mov": "/123456/clip.part001.mov",
"folder/sub/clip.mov": "folder/sub/clip.part001.mov",
}
for input, want := range tests {
if got := partStoragePath(input, "clip.part001.mov"); got != want {
t.Fatalf("partStoragePath(%q) = %q, want %q", input, got, want)
}
}
}
func TestVideoPartCaption(t *testing.T) {
if got := videoPartCaption(nil, 0); got != nil {
t.Fatalf("caption without source = %q, want nil", *got)
}
source := "original caption"
first := videoPartCaption(&source, 0)
if first == nil || *first != source {
t.Fatalf("first part caption = %v, want %q", first, source)
}
second := videoPartCaption(&source, 1)
if second == nil || *second != "" {
t.Fatalf("second part caption = %v, want an explicit empty caption", second)
}
}
func TestSplitLosslessVideoRetriesOversizedPart(t *testing.T) {
tempDir := t.TempDir()
inputPath := filepath.Join(tempDir, "source.mov")
if err := os.WriteFile(inputPath, []byte("source"), 0o600); err != nil {
t.Fatal(err)
}
outputDir := filepath.Join(tempDir, "parts")
if err := os.Mkdir(outputDir, 0o755); err != nil {
t.Fatal(err)
}
originalRunner := runMediaTool
t.Cleanup(func() { runMediaTool = originalRunner })
ffmpegCalls := 0
runMediaTool = func(ctx context.Context, name string, args ...string) ([]byte, error) {
switch name {
case "ffprobe":
return []byte("120.0\n"), nil
case "ffmpeg":
ffmpegCalls++
pattern := args[len(args)-1]
sizes := []int{2001, 199}
if ffmpegCalls > 1 {
sizes = []int{1100, 1100}
}
for index, size := range sizes {
partPath := strings.Replace(pattern, "%03d", fmt.Sprintf("%03d", index+1), 1)
if err := os.WriteFile(partPath, make([]byte, size), 0o600); err != nil {
return nil, err
}
}
return nil, nil
default:
return nil, fmt.Errorf("unexpected media tool %q", name)
}
}
parts, err := splitLosslessVideo(t.Context(), inputPath, outputDir, "example.mov", 2200, 2000)
if err != nil {
t.Fatalf("splitLosslessVideo() failed: %v", err)
}
if ffmpegCalls != 2 {
t.Fatalf("ffmpeg calls = %d, want 2", ffmpegCalls)
}
if len(parts) != 2 {
t.Fatalf("parts = %d, want 2", len(parts))
}
if parts[0].Name != "example.part001.mov" || parts[1].Name != "example.part002.mov" {
t.Fatalf("unexpected part names: %#v", parts)
}
for _, part := range parts {
if part.Size > 2000 {
t.Fatalf("part %s exceeds limit: %d", part.Name, part.Size)
}
}
}
func TestSplitLosslessVideoRejectsMultipleAlbums(t *testing.T) {
tempDir := t.TempDir()
inputPath := filepath.Join(tempDir, "source.mov")
if err := os.WriteFile(inputPath, []byte("source"), 0o600); err != nil {
t.Fatal(err)
}
outputDir := filepath.Join(tempDir, "parts")
if err := os.Mkdir(outputDir, 0o755); err != nil {
t.Fatal(err)
}
originalRunner := runMediaTool
t.Cleanup(func() { runMediaTool = originalRunner })
runMediaTool = func(ctx context.Context, name string, args ...string) ([]byte, error) {
switch name {
case "ffprobe":
return []byte("120.0\n"), nil
case "ffmpeg":
pattern := args[len(args)-1]
for index := 0; index < maxLosslessVideoParts+1; index++ {
partPath := strings.Replace(pattern, "%03d", fmt.Sprintf("%03d", index+1), 1)
if err := os.WriteFile(partPath, make([]byte, 100), 0o600); err != nil {
return nil, err
}
}
return nil, nil
default:
return nil, fmt.Errorf("unexpected media tool %q", name)
}
}
_, err := splitLosslessVideo(t.Context(), inputPath, outputDir, "example.mov", 2200, 2000)
if err == nil {
t.Fatal("splitLosslessVideo() succeeded with more than one album of parts")
}
if !strings.Contains(err.Error(), "single-album limit") {
t.Fatalf("splitLosslessVideo() error = %q, want single-album limit", err)
}
}