Add download preview flow and search fallback
Some checks failed
build-push / docker (push) Has been cancelled

This commit is contained in:
AI Assistant
2026-03-12 16:01:19 +09:00
parent 5d0ba114ee
commit e136650790
5 changed files with 312 additions and 48 deletions

View File

@@ -64,12 +64,23 @@ func (h *Hub) Remove(conn *websocket.Conn) {
_ = conn.Close()
}
type PreviewResponse struct {
Title string `json:"title"`
Thumbnail string `json:"thumbnail"`
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) {
router.GET("/healthz", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
router.GET("/ws", app.handleWS)
router.GET("/api/history/check", app.checkDuplicate)
router.POST("/api/download/preview", app.previewDownload)
router.POST("/api/upload", app.uploadFile)
router.POST("/api/download", app.startDownload)
router.POST("/api/search", app.searchMedia)
@@ -132,6 +143,7 @@ func (a *App) startDownload(c *gin.Context) {
URL string `json:"url"`
Start string `json:"start"`
End string `json:"end"`
Quality string `json:"quality"`
Force bool `json:"force"`
}
if err := c.ShouldBindJSON(&req); err != nil {
@@ -157,13 +169,46 @@ func (a *App) startDownload(c *gin.Context) {
return
}
go a.runDownload(recordID, req.URL, req.Start, req.End, outputPath)
quality := strings.TrimSpace(req.Quality)
if quality == "" {
quality = "best"
}
go a.runDownload(recordID, req.URL, req.Start, req.End, quality, outputPath)
c.JSON(http.StatusAccepted, gin.H{"message": "download started", "recordId": recordID})
}
func (a *App) runDownload(recordID int64, url, start, end, outputPath string) {
func (a *App) previewDownload(c *gin.Context) {
var req struct {
URL string `json:"url"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if strings.TrimSpace(req.URL) == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "url is required"})
return
}
cmd := exec.Command("python3", a.WorkerScript, "--mode", "probe", "--url", req.URL, "--output", filepath.Join(a.DownloadsDir, "probe.tmp"))
output, err := cmd.CombinedOutput()
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": strings.TrimSpace(string(output))})
return
}
var preview PreviewResponse
if err := json.Unmarshal(output, &preview); err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "invalid probe response"})
return
}
c.JSON(http.StatusOK, preview)
}
func (a *App) runDownload(recordID int64, url, start, end, quality, outputPath string) {
a.Hub.Broadcast("progress", gin.H{"type": "download", "status": "queued", "progress": 0, "url": url})
cmd := exec.Command("python3", a.WorkerScript, "--url", url, "--start", start, "--end", end, "--output", outputPath)
cmd := exec.Command("python3", a.WorkerScript, "--url", url, "--start", start, "--end", end, "--quality", quality, "--output", outputPath)
stdout, err := cmd.StdoutPipe()
if err != nil {
a.Hub.Broadcast("progress", gin.H{"type": "download", "status": "error", "progress": 0, "message": err.Error()})
@@ -187,6 +232,9 @@ func (a *App) runDownload(recordID int64, url, start, end, outputPath string) {
a.Hub.Broadcast("progress", msg)
}
}
if err := scanner.Err(); err != nil {
a.Hub.Broadcast("progress", gin.H{"type": "download", "status": "error", "progress": 100, "message": err.Error()})
}
status := "completed"
if err := cmd.Wait(); err != nil {

View File

@@ -3,6 +3,7 @@ package services
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
@@ -45,61 +46,120 @@ func (s *SearchService) SearchMedia(query string) ([]SearchResult, error) {
values.Set("key", s.APIKey)
values.Set("cx", s.CX)
values.Set("q", fullQuery)
values.Set("searchType", "image")
values.Set("num", "10")
values.Set("safe", "off")
results := make([]SearchResult, 0, 30)
seen := map[string]bool{}
for _, start := range []string{"1", "11", "21"} {
values.Set("start", start)
endpoint := "https://www.googleapis.com/customsearch/v1?" + values.Encode()
resp, err := s.Client.Get(endpoint)
pageResults, err := s.fetchPage(values, start, true)
if err != nil {
return nil, err
pageResults, err = s.fetchPage(values, start, false)
if err != nil {
return nil, err
}
}
if resp.StatusCode >= 300 {
resp.Body.Close()
return nil, fmt.Errorf("google cse returned status %d", resp.StatusCode)
}
var payload struct {
Items []struct {
Title string `json:"title"`
Link string `json:"link"`
DisplayLink string `json:"displayLink"`
Snippet string `json:"snippet"`
Image struct {
ThumbnailLink string `json:"thumbnailLink"`
} `json:"image"`
} `json:"items"`
}
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
resp.Body.Close()
return nil, err
}
resp.Body.Close()
for _, item := range payload.Items {
if item.Link == "" || seen[item.Link] {
for _, item := range pageResults {
if item.Link == "" || item.ThumbnailURL == "" || seen[item.Link] {
continue
}
seen[item.Link] = true
results = append(results, SearchResult{
Title: item.Title,
Link: item.Link,
DisplayLink: item.DisplayLink,
Snippet: item.Snippet,
ThumbnailURL: item.Image.ThumbnailLink,
Source: inferSource(item.DisplayLink),
})
results = append(results, item)
}
}
return results, nil
}
func (s *SearchService) fetchPage(values url.Values, start string, imageSearch bool) ([]SearchResult, error) {
pageValues := url.Values{}
for key, items := range values {
for _, item := range items {
pageValues.Add(key, item)
}
}
pageValues.Set("start", start)
if imageSearch {
pageValues.Set("searchType", "image")
}
endpoint := "https://www.googleapis.com/customsearch/v1?" + pageValues.Encode()
resp, err := s.Client.Get(endpoint)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
data, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
return nil, fmt.Errorf("google cse returned status %d: %s", resp.StatusCode, strings.TrimSpace(string(data)))
}
var payload struct {
Items []struct {
Title string `json:"title"`
Link string `json:"link"`
DisplayLink string `json:"displayLink"`
Snippet string `json:"snippet"`
Image struct {
ThumbnailLink string `json:"thumbnailLink"`
} `json:"image"`
Pagemap struct {
CSEImage []struct {
Src string `json:"src"`
} `json:"cse_image"`
CSEThumbnail []struct {
Src string `json:"src"`
} `json:"cse_thumbnail"`
Metatags []map[string]string `json:"metatags"`
} `json:"pagemap"`
} `json:"items"`
}
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
results := make([]SearchResult, 0, len(payload.Items))
for _, item := range payload.Items {
thumb := item.Image.ThumbnailLink
if thumb == "" {
thumb = extractThumbnail(item.Pagemap)
}
results = append(results, SearchResult{
Title: item.Title,
Link: item.Link,
DisplayLink: item.DisplayLink,
Snippet: item.Snippet,
ThumbnailURL: thumb,
Source: inferSource(item.DisplayLink),
})
}
return results, nil
}
func extractThumbnail(pagemap struct {
CSEImage []struct{ Src string "json:\"src\"" } "json:\"cse_image\""
CSEThumbnail []struct{ Src string "json:\"src\"" } "json:\"cse_thumbnail\""
Metatags []map[string]string "json:\"metatags\""
}) string {
if len(pagemap.CSEThumbnail) > 0 && pagemap.CSEThumbnail[0].Src != "" {
return pagemap.CSEThumbnail[0].Src
}
if len(pagemap.CSEImage) > 0 && pagemap.CSEImage[0].Src != "" {
return pagemap.CSEImage[0].Src
}
for _, tag := range pagemap.Metatags {
if value := tag["og:image"]; value != "" {
return value
}
if value := tag["twitter:image"]; value != "" {
return value
}
}
return ""
}
func inferSource(displayLink string) string {
switch {
case strings.Contains(displayLink, "youtube"):