145 lines
3.8 KiB
Go
145 lines
3.8 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strings"
|
||
"sync"
|
||
|
||
"api-server/internal/scraper"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// ExtractHandler 视频提取 API 处理器
|
||
type ExtractHandler struct {
|
||
session *scraper.Session
|
||
}
|
||
|
||
// NewExtractHandler 创建提取处理器
|
||
func NewExtractHandler() *ExtractHandler {
|
||
return &ExtractHandler{
|
||
session: scraper.NewSession(),
|
||
}
|
||
}
|
||
|
||
// ExtractRequest 提取请求
|
||
type ExtractRequest struct {
|
||
URL string `json:"url" binding:"required" example:"https://h1014.sol148.com/view_video.php?viewkey=xxx"`
|
||
}
|
||
|
||
// BatchExtractRequest 批量提取请求
|
||
type BatchExtractRequest struct {
|
||
URLs []string `json:"urls" binding:"required"`
|
||
Concurrency int `json:"concurrency" example:"3"`
|
||
}
|
||
|
||
// RegisterRoutes 注册路由
|
||
func (h *ExtractHandler) RegisterRoutes(r *gin.RouterGroup) {
|
||
r.POST("/extract", h.ExtractSingle)
|
||
r.POST("/extract/batch", h.ExtractBatch)
|
||
}
|
||
|
||
// ExtractSingle 提取单个视频源
|
||
// @Summary 提取视频源
|
||
// @Description 从视频播放页 URL 提取正片源地址与元信息(标题、时长、热度、作者等)
|
||
// @Description 原理:strencode2 → unescape → 正则提取 <source> 中 .mp4 链接
|
||
// @Tags extract
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param body body ExtractRequest true "视频播放页 URL"
|
||
// @Success 200 {object} map[string]interface{}
|
||
// @Failure 400 {object} map[string]interface{}
|
||
// @Failure 500 {object} map[string]interface{}
|
||
// @Router /extract [post]
|
||
func (h *ExtractHandler) ExtractSingle(c *gin.Context) {
|
||
var req struct {
|
||
URL string `json:"url" binding:"required"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请提供视频 URL"})
|
||
return
|
||
}
|
||
|
||
info, err := scraper.ExtractVideo(h.session, req.URL)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"code": 1,
|
||
"msg": "提取失败: " + err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": info})
|
||
}
|
||
|
||
// ExtractBatch 批量提取视频源
|
||
// @Summary 批量提取视频源
|
||
// @Description 并发提取多个视频播放页的源地址,支持自定义并发度(默认3,最大10)
|
||
// @Tags extract
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param body body BatchExtractRequest true "URL 列表 + 并发度"
|
||
// @Success 200 {object} map[string]interface{}
|
||
// @Failure 400 {object} map[string]interface{}
|
||
// @Router /extract/batch [post]
|
||
func (h *ExtractHandler) ExtractBatch(c *gin.Context) {
|
||
var req struct {
|
||
URLs []string `json:"urls" binding:"required"`
|
||
Concurrency int `json:"concurrency"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请提供 URL 列表"})
|
||
return
|
||
}
|
||
|
||
if req.Concurrency <= 0 {
|
||
req.Concurrency = 3
|
||
}
|
||
if req.Concurrency > 10 {
|
||
req.Concurrency = 10
|
||
}
|
||
|
||
type result struct {
|
||
URL string `json:"url"`
|
||
Data *scraper.VideoInfo `json:"data,omitempty"`
|
||
Err string `json:"error,omitempty"`
|
||
}
|
||
|
||
results := make([]result, len(req.URLs))
|
||
sem := make(chan struct{}, req.Concurrency)
|
||
var wg sync.WaitGroup
|
||
|
||
for i, u := range req.URLs {
|
||
wg.Add(1)
|
||
go func(idx int, urlStr string) {
|
||
defer wg.Done()
|
||
sem <- struct{}{}
|
||
defer func() { <-sem }()
|
||
|
||
s := scraper.NewSession()
|
||
info, err := scraper.ExtractVideo(s, strings.TrimSpace(urlStr))
|
||
if err != nil {
|
||
results[idx] = result{URL: urlStr, Err: err.Error()}
|
||
} else {
|
||
results[idx] = result{URL: urlStr, Data: info}
|
||
}
|
||
}(i, u)
|
||
}
|
||
|
||
wg.Wait()
|
||
|
||
success := 0
|
||
for _, r := range results {
|
||
if r.Err == "" {
|
||
success++
|
||
}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"code": 0,
|
||
"total": len(req.URLs),
|
||
"success": success,
|
||
"data": results,
|
||
})
|
||
}
|