105 lines
3.1 KiB
Go
105 lines
3.1 KiB
Go
package logic
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
infmedia "spark-server/internal/infrastructure/media"
|
||
pkgmedia "spark-server/internal/pkg/media"
|
||
)
|
||
|
||
const maxDownloadBytes = 80 << 20 // refuse absurd remote blobs before compress
|
||
|
||
// MediaCompressor downloads remote media and compresses to App-aligned specs before objectstore write.
|
||
// Square bot RSS worker should call FetchImage / FetchVideo instead of raw HTTP+Put.
|
||
type MediaCompressor struct {
|
||
Opt infmedia.Options
|
||
Client *http.Client
|
||
}
|
||
|
||
// NewMediaCompressor builds a compressor; ffmpegPath may be empty (defaults to "ffmpeg").
|
||
func NewMediaCompressor(lim pkgmedia.Limits, jpegQuality, videoCRF int, ffmpegPath string) *MediaCompressor {
|
||
return &MediaCompressor{
|
||
Opt: infmedia.OptionsFromLimits(lim, jpegQuality, videoCRF, ffmpegPath),
|
||
Client: &http.Client{
|
||
Timeout: 60 * time.Second,
|
||
},
|
||
}
|
||
}
|
||
|
||
// FetchImage GETs url and compresses to ≤1080p JPEG/PNG.
|
||
func (c *MediaCompressor) FetchImage(ctx context.Context, url string) (*infmedia.BotAsset, error) {
|
||
raw, err := c.download(ctx, url)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return infmedia.PrepareBotImage(raw, c.Opt)
|
||
}
|
||
|
||
// FetchAvatar GETs url and compresses to ≤200×200.
|
||
func (c *MediaCompressor) FetchAvatar(ctx context.Context, url string) (*infmedia.BotAsset, error) {
|
||
raw, err := c.download(ctx, url)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return infmedia.PrepareBotAvatar(raw, c.Opt)
|
||
}
|
||
|
||
// FetchVideo GETs url and compresses to ≤720p (requires ffmpeg).
|
||
func (c *MediaCompressor) FetchVideo(ctx context.Context, url string) (*infmedia.BotAsset, error) {
|
||
raw, err := c.download(ctx, url)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return infmedia.PrepareBotVideo(ctx, raw, c.Opt)
|
||
}
|
||
|
||
// CompressBytesImage compresses already-downloaded image bytes (e.g. RSS enclosure already fetched).
|
||
func (c *MediaCompressor) CompressBytesImage(src []byte) (*infmedia.BotAsset, error) {
|
||
return infmedia.PrepareBotImage(src, c.Opt)
|
||
}
|
||
|
||
// CompressBytesVideo compresses already-downloaded video bytes.
|
||
func (c *MediaCompressor) CompressBytesVideo(ctx context.Context, src []byte) (*infmedia.BotAsset, error) {
|
||
return infmedia.PrepareBotVideo(ctx, src, c.Opt)
|
||
}
|
||
|
||
func (c *MediaCompressor) download(ctx context.Context, url string) ([]byte, error) {
|
||
url = strings.TrimSpace(url)
|
||
if url == "" {
|
||
return nil, fmt.Errorf("squarebot media: empty url")
|
||
}
|
||
client := c.Client
|
||
if client == nil {
|
||
client = http.DefaultClient
|
||
}
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.Header.Set("User-Agent", "SparkIM-SquareBot/1.0")
|
||
res, err := client.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer func() { _ = res.Body.Close() }()
|
||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||
return nil, fmt.Errorf("squarebot media: http %d", res.StatusCode)
|
||
}
|
||
data, err := io.ReadAll(io.LimitReader(res.Body, maxDownloadBytes+1))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(data) > maxDownloadBytes {
|
||
return nil, fmt.Errorf("squarebot media: download too large")
|
||
}
|
||
if len(data) == 0 {
|
||
return nil, fmt.Errorf("squarebot media: empty body")
|
||
}
|
||
return data, nil
|
||
}
|