125 lines
4.0 KiB
Go
125 lines
4.0 KiB
Go
package app
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"image/jpeg"
|
||
"io"
|
||
"net/http"
|
||
"os/exec"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
func isHongguoImageHost(host string) bool {
|
||
host = strings.ToLower(host)
|
||
return host == "hongguoduanju.com" || host == "www.hongguoduanju.com" ||
|
||
strings.HasSuffix(host, ".byteimg.com") || strings.HasSuffix(host, ".fqnovelpic.com")
|
||
}
|
||
|
||
func (app *UIApp) loadCoverImage(ctx context.Context, remoteURL string, decode func([]byte, string) []byte) ([]byte, error) {
|
||
source, _ := ctx.Value(coverSourceKey{}).(string)
|
||
referer := sourceCoverReferer(source, remoteURL)
|
||
return app.coverImages.load(ctx, remoteURL+"\x00"+referer, func(ctx context.Context) ([]byte, error) {
|
||
ctx = context.WithValue(ctx, coverRequestKey{}, true)
|
||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, remoteURL, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !validImageURL(request.URL) {
|
||
return nil, errors.New("封面地址不受支持")
|
||
}
|
||
request.Header.Set("Referer", referer)
|
||
request.Header.Set("User-Agent", userAgent)
|
||
request.Header.Set("Accept", "image/webp,image/jpeg,image/png,image/gif,*/*;q=0.5")
|
||
client := *app.downloader.client
|
||
client.CheckRedirect = func(request *http.Request, via []*http.Request) error {
|
||
if len(via) >= 5 || !validImageURL(request.URL) {
|
||
return errors.New("封面重定向地址不受支持")
|
||
}
|
||
request.Header.Set("Referer", referer)
|
||
return nil
|
||
}
|
||
response, err := client.Do(request)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer response.Body.Close()
|
||
if response.StatusCode != http.StatusOK {
|
||
return nil, fmt.Errorf("封面上游 HTTP %d", response.StatusCode)
|
||
}
|
||
if response.ContentLength > maxCoverBytes {
|
||
return nil, errors.New("封面文件过大")
|
||
}
|
||
data, err := io.ReadAll(io.LimitReader(response.Body, maxCoverBytes+1))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(data) > maxCoverBytes {
|
||
return nil, errors.New("封面文件过大")
|
||
}
|
||
if decode != nil {
|
||
data = decode(data, remoteURL)
|
||
}
|
||
if isKnownImage(data) {
|
||
return data, nil
|
||
}
|
||
if isHEICImage(data) {
|
||
return app.downloader.convertHEICCover(ctx, data)
|
||
}
|
||
return nil, errors.New("封面内容无效,请检查代理或站点是否需要验证")
|
||
})
|
||
}
|
||
|
||
type coverOutput struct {
|
||
buffer bytes.Buffer
|
||
}
|
||
|
||
func (output *coverOutput) Write(data []byte) (int, error) {
|
||
if len(data) > (4<<20)-output.buffer.Len() {
|
||
return 0, errors.New("封面转换结果过大")
|
||
}
|
||
return output.buffer.Write(data)
|
||
}
|
||
|
||
func (output *coverOutput) Bytes() []byte { return output.buffer.Bytes() }
|
||
|
||
func (downloader *Downloader) convertHEICCover(ctx context.Context, data []byte) ([]byte, error) {
|
||
image, err := extractHEICImage(data)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
ffmpeg, err := downloader.ensureFFmpeg(ctx)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("封面转换需要 FFmpeg:%w", err)
|
||
}
|
||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||
defer cancel()
|
||
filters := append(image.filters, "scale=w='min(800,iw)':h='min(800,ih)':force_original_aspect_ratio=decrease")
|
||
command := exec.CommandContext(ctx, ffmpeg, "-hide_banner", "-loglevel", "error", "-nostdin",
|
||
"-max_alloc", "67108864", "-protocol_whitelist", "pipe", "-threads", "1", "-f", "hevc", "-i", "pipe:0",
|
||
"-frames:v", "1", "-an", "-sn", "-vf", strings.Join(filters, ","), "-filter_threads", "1",
|
||
"-threads", "1", "-c:v", "mjpeg", "-pix_fmt", "yuvj420p", "-q:v", "3", "-f", "image2pipe", "pipe:1")
|
||
command.Stdin = bytes.NewReader(image.data)
|
||
var output coverOutput
|
||
stderr := &cappedStringWriter{limit: 4096}
|
||
command.Stdout, command.Stderr = &output, stderr
|
||
if err := command.Run(); err != nil {
|
||
if ctx.Err() != nil {
|
||
return nil, ctx.Err()
|
||
}
|
||
detail := strings.TrimSpace(stderr.String())
|
||
if detail == "" {
|
||
detail = err.Error()
|
||
}
|
||
return nil, fmt.Errorf("HEIC 封面转换失败:%s", detail)
|
||
}
|
||
config, err := jpeg.DecodeConfig(bytes.NewReader(output.Bytes()))
|
||
if err != nil || config.Width <= 0 || config.Height <= 0 || config.Width > 800 || config.Height > 800 {
|
||
return nil, errors.New("HEIC 封面转换结果无效")
|
||
}
|
||
return output.Bytes(), nil
|
||
}
|