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 }