mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-08-06 13:03:19 +08:00
* feat(telegram): split oversized videos losslessly * fix(telegram): respect Premium upload limits * fix(telegram): keep split videos in one album
175 lines
4.9 KiB
Go
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)
|
|
}
|
|
}
|