This commit is contained in:
+128
-6
@@ -2,11 +2,14 @@ package handlers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha1"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -24,12 +27,13 @@ import (
|
||||
)
|
||||
|
||||
type App struct {
|
||||
DB *sql.DB
|
||||
DownloadsDir string
|
||||
WorkerScript string
|
||||
SearchService *services.SearchService
|
||||
GeminiService *services.GeminiService
|
||||
Hub *Hub
|
||||
DB *sql.DB
|
||||
DownloadsDir string
|
||||
PreviewCacheDir string
|
||||
WorkerScript string
|
||||
SearchService *services.SearchService
|
||||
GeminiService *services.GeminiService
|
||||
Hub *Hub
|
||||
}
|
||||
|
||||
type Hub struct {
|
||||
@@ -92,6 +96,7 @@ func RegisterRoutes(router *gin.Engine, app *App) {
|
||||
})
|
||||
router.GET("/ws", app.handleWS)
|
||||
router.GET("/api/history/check", app.checkDuplicate)
|
||||
router.GET("/api/preview/stream", app.streamPreview)
|
||||
router.POST("/api/download/preview", app.previewDownload)
|
||||
router.POST("/api/upload", app.uploadFile)
|
||||
router.POST("/api/download", app.startDownload)
|
||||
@@ -134,6 +139,61 @@ func (a *App) checkDuplicate(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"exists": record != nil, "record": record})
|
||||
}
|
||||
|
||||
func (a *App) streamPreview(c *gin.Context) {
|
||||
target := strings.TrimSpace(c.Query("url"))
|
||||
if target == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "url is required"})
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, target, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
req.Header.Set("Referer", inferPreviewReferer(target))
|
||||
|
||||
resp, err := a.SearchService.Client.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 300 {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("preview source returned %d", resp.StatusCode)})
|
||||
return
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if strings.Contains(strings.ToLower(target), ".m3u8") || strings.Contains(strings.ToLower(contentType), "mpegurl") {
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
rewritten := rewriteM3U8Playlist(string(body), target)
|
||||
c.Header("Content-Type", "application/vnd.apple.mpegurl")
|
||||
c.String(http.StatusOK, rewritten)
|
||||
return
|
||||
}
|
||||
|
||||
if isCacheablePreview(target, contentType) {
|
||||
if cachedPath, err := a.cachePreviewResponse(target, contentType, resp.Body); err == nil {
|
||||
c.File(cachedPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if contentType != "" {
|
||||
c.Header("Content-Type", contentType)
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
_, _ = io.Copy(c.Writer, resp.Body)
|
||||
}
|
||||
|
||||
func (a *App) uploadFile(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
@@ -562,6 +622,68 @@ func EnsurePaths(downloadsDir, workerScript string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func inferPreviewReferer(target string) string {
|
||||
lower := strings.ToLower(target)
|
||||
switch {
|
||||
case strings.Contains(lower, "envatousercontent.com"), strings.Contains(lower, "elements.envato.com"):
|
||||
return "https://elements.envato.com/"
|
||||
case strings.Contains(lower, "artgrid"), strings.Contains(lower, "artlist"):
|
||||
return "https://artgrid.io/"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func rewriteM3U8Playlist(body, target string) string {
|
||||
lines := strings.Split(body, "\n")
|
||||
baseURL := target
|
||||
if idx := strings.LastIndex(baseURL, "/"); idx >= 0 {
|
||||
baseURL = baseURL[:idx+1]
|
||||
}
|
||||
for idx, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
resolved := trimmed
|
||||
if !strings.HasPrefix(trimmed, "http://") && !strings.HasPrefix(trimmed, "https://") {
|
||||
resolved = baseURL + strings.TrimPrefix(trimmed, "/")
|
||||
}
|
||||
lines[idx] = "/api/preview/stream?url=" + url.QueryEscape(resolved)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func isCacheablePreview(target, contentType string) bool {
|
||||
lower := strings.ToLower(target + " " + contentType)
|
||||
return strings.Contains(lower, ".mp4") || strings.Contains(lower, "video/mp4")
|
||||
}
|
||||
|
||||
func (a *App) cachePreviewResponse(target, contentType string, body io.Reader) (string, error) {
|
||||
if a.PreviewCacheDir == "" {
|
||||
return "", fmt.Errorf("preview cache dir is not configured")
|
||||
}
|
||||
if err := os.MkdirAll(a.PreviewCacheDir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sum := sha1.Sum([]byte(target))
|
||||
ext := ".bin"
|
||||
if strings.Contains(strings.ToLower(target), ".mp4") || strings.Contains(strings.ToLower(contentType), "video/mp4") {
|
||||
ext = ".mp4"
|
||||
}
|
||||
path := filepath.Join(a.PreviewCacheDir, fmt.Sprintf("%x%s", sum, ext))
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
if _, err := io.Copy(file, body); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func summarizeOutput(prefix string, output []byte, err error) string {
|
||||
trimmed := strings.TrimSpace(string(output))
|
||||
if trimmed == "" && err != nil {
|
||||
|
||||
+4
-3
@@ -31,9 +31,10 @@ func main() {
|
||||
}
|
||||
|
||||
app := &handlers.App{
|
||||
DB: db,
|
||||
DownloadsDir: downloadsDir,
|
||||
WorkerScript: workerScript,
|
||||
DB: db,
|
||||
DownloadsDir: downloadsDir,
|
||||
PreviewCacheDir: filepath.Join(downloadsDir, ".preview-cache"),
|
||||
WorkerScript: workerScript,
|
||||
SearchService: services.NewSearchService(
|
||||
os.Getenv("SEARXNG_BASE_URL"),
|
||||
os.Getenv("SEARXNG_GOOGLE_VIDEO_ENGINE"),
|
||||
|
||||
Reference in New Issue
Block a user