This commit is contained in:
+110
-20
@@ -11,6 +11,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -65,14 +66,14 @@ func (h *Hub) Remove(conn *websocket.Conn) {
|
||||
}
|
||||
|
||||
type PreviewResponse struct {
|
||||
Title string `json:"title"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Title string `json:"title"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
PreviewStreamURL string `json:"previewStreamUrl"`
|
||||
Duration string `json:"duration"`
|
||||
DurationSeconds int `json:"durationSeconds"`
|
||||
StartDefault string `json:"startDefault"`
|
||||
EndDefault string `json:"endDefault"`
|
||||
Qualities []map[string]any `json:"qualities"`
|
||||
Duration string `json:"duration"`
|
||||
DurationSeconds int `json:"durationSeconds"`
|
||||
StartDefault string `json:"startDefault"`
|
||||
EndDefault string `json:"endDefault"`
|
||||
Qualities []map[string]any `json:"qualities"`
|
||||
}
|
||||
|
||||
func RegisterRoutes(router *gin.Engine, app *App) {
|
||||
@@ -141,11 +142,11 @@ func (a *App) uploadFile(c *gin.Context) {
|
||||
|
||||
func (a *App) startDownload(c *gin.Context) {
|
||||
var req struct {
|
||||
URL string `json:"url"`
|
||||
Start string `json:"start"`
|
||||
End string `json:"end"`
|
||||
URL string `json:"url"`
|
||||
Start string `json:"start"`
|
||||
End string `json:"end"`
|
||||
Quality string `json:"quality"`
|
||||
Force bool `json:"force"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -192,7 +193,7 @@ func (a *App) previewDownload(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("python3", a.WorkerScript, "--mode", "probe", "--url", req.URL, "--output", filepath.Join(a.DownloadsDir, "probe.tmp"))
|
||||
cmd := exec.Command("python3", a.WorkerScript, "--mode", "probe", "--url", req.URL)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": strings.TrimSpace(string(output))})
|
||||
@@ -258,34 +259,53 @@ func (a *App) searchMedia(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
results, err := a.SearchService.SearchMedia(req.Query)
|
||||
queryVariants, expandErr := a.GeminiService.ExpandQuery(req.Query)
|
||||
if len(queryVariants) == 0 {
|
||||
queryVariants = []string{req.Query}
|
||||
}
|
||||
|
||||
results, err := a.SearchService.SearchMedia(queryVariants)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(results) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"results": []services.AIRecommendation{}, "warning": "Vertex AI Search returned no renderable results. Check your website indexing fields and thumbnails."})
|
||||
warning := "SearXNG returned no renderable results."
|
||||
if expandErr != nil {
|
||||
warning += " Query expansion fallback was used."
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"results": []services.AIRecommendation{}, "warning": warning})
|
||||
return
|
||||
}
|
||||
|
||||
recommended, err := a.GeminiService.Recommend(req.Query, results)
|
||||
scored := rankSearchResults(req.Query, results)
|
||||
shortlist := scored[:min(len(scored), 10)]
|
||||
recommended, err := a.GeminiService.Recommend(req.Query, shortlist)
|
||||
if err != nil {
|
||||
fallback := make([]services.AIRecommendation, 0, min(4, len(results)))
|
||||
for _, result := range results[:min(4, len(results))] {
|
||||
fallback := make([]services.AIRecommendation, 0, min(20, len(scored)))
|
||||
for _, result := range scored[:min(20, len(scored))] {
|
||||
fallback = append(fallback, services.AIRecommendation{
|
||||
Title: result.Title,
|
||||
Link: result.Link,
|
||||
ThumbnailURL: result.ThumbnailURL,
|
||||
Source: result.Source,
|
||||
Reason: "Gemini recommendation failed, showing raw search result.",
|
||||
Reason: "Keyword-ranked result added without extra Gemini vision tokens.",
|
||||
Recommended: true,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"results": fallback, "warning": err.Error()})
|
||||
warning := err.Error()
|
||||
if expandErr != nil {
|
||||
warning = warning + " Query expansion fallback was used."
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"results": fallback, "warning": warning, "queries": queryVariants})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"results": recommended})
|
||||
response := gin.H{"results": mergeRecommendations(recommended, scored, 20), "queries": queryVariants}
|
||||
if expandErr != nil {
|
||||
response["warning"] = "Gemini query expansion failed, using the original query only."
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func normalizeFilename(name string) string {
|
||||
@@ -321,6 +341,76 @@ func min(a, b int) int {
|
||||
return b
|
||||
}
|
||||
|
||||
func rankSearchResults(query string, results []services.SearchResult) []services.SearchResult {
|
||||
queryTerms := strings.Fields(strings.ToLower(query))
|
||||
type scoredResult struct {
|
||||
item services.SearchResult
|
||||
score int
|
||||
}
|
||||
|
||||
scored := make([]scoredResult, 0, len(results))
|
||||
for _, result := range results {
|
||||
score := 0
|
||||
text := strings.ToLower(result.Title + " " + result.Snippet + " " + result.Source)
|
||||
for _, term := range queryTerms {
|
||||
if strings.Contains(text, term) {
|
||||
score += 3
|
||||
}
|
||||
}
|
||||
if result.ThumbnailURL != "" {
|
||||
score += 2
|
||||
}
|
||||
switch result.Source {
|
||||
case "Google Video":
|
||||
score += 3
|
||||
case "Envato":
|
||||
score += 2
|
||||
case "Artgrid":
|
||||
score += 2
|
||||
}
|
||||
scored = append(scored, scoredResult{item: result, score: score})
|
||||
}
|
||||
|
||||
sort.SliceStable(scored, func(i, j int) bool {
|
||||
return scored[i].score > scored[j].score
|
||||
})
|
||||
|
||||
ranked := make([]services.SearchResult, 0, len(scored))
|
||||
for _, item := range scored {
|
||||
ranked = append(ranked, item.item)
|
||||
}
|
||||
return ranked
|
||||
}
|
||||
|
||||
func mergeRecommendations(recommended []services.AIRecommendation, ranked []services.SearchResult, limit int) []services.AIRecommendation {
|
||||
merged := make([]services.AIRecommendation, 0, min(limit, len(ranked)))
|
||||
seen := map[string]bool{}
|
||||
|
||||
for _, item := range recommended {
|
||||
if item.Link == "" || seen[item.Link] {
|
||||
continue
|
||||
}
|
||||
seen[item.Link] = true
|
||||
merged = append(merged, item)
|
||||
}
|
||||
|
||||
for _, item := range ranked {
|
||||
if len(merged) >= limit || item.Link == "" || seen[item.Link] {
|
||||
continue
|
||||
}
|
||||
seen[item.Link] = true
|
||||
merged = append(merged, services.AIRecommendation{
|
||||
Title: item.Title,
|
||||
Link: item.Link,
|
||||
ThumbnailURL: item.ThumbnailURL,
|
||||
Source: item.Source,
|
||||
Reason: "Keyword-ranked result added without extra Gemini vision tokens.",
|
||||
Recommended: true,
|
||||
})
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func EnsurePaths(downloadsDir, workerScript string) error {
|
||||
if err := os.MkdirAll(downloadsDir, 0o755); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user