mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-08-22 00:32:26 +08:00
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
This commit is contained in:
174
storage/telegram/video_split_test.go
Normal file
174
storage/telegram/video_split_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user