package api import ( "context" "encoding/json" "fmt" "log/slog" "net/http" "strconv" "strings" "time" "github.com/TopherMayor/unified-media-manager/internal/service" "github.com/labstack/echo/v4" ) func searchReleases(svc *service.SearchService) echo.HandlerFunc { return func(c echo.Context) error { ctx, cancel := context.WithTimeout(c.Request().Context(), 30*time.Second) defer cancel() query := c.QueryParam("query") if query == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "query parameter is required"}) } mediaType := c.QueryParam("media_type") var indexerIDs []int64 if ids := c.QueryParam("indexer_ids"); ids != "" { for _, idStr := range strings.Split(ids, ",") { idStr = strings.TrimSpace(idStr) if id, err := strconv.ParseInt(idStr, 10, 64); err == nil { indexerIDs = append(indexerIDs, id) } } } req := service.SearchRequest{ Query: query, MediaType: mediaType, IndexerIDs: indexerIDs, } results, err := svc.Search(ctx, req) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]interface{}{ "data": results, "total": len(results), }) } } func grabRelease(svc *service.SearchService, dcSvc *service.DownloadClientService, queueSvc *service.QueueService, safetySvc *service.SafetyService, activitySvc *service.ActivityService) echo.HandlerFunc { return func(c echo.Context) error { ctx, cancel := context.WithTimeout(c.Request().Context(), 30*time.Second) defer cancel() var req service.GrabRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"}) } if req.DownloadURL == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "download_url is required"}) } if req.Title == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "title is required"}) } if req.MediaID == 0 { return c.JSON(http.StatusBadRequest, map[string]string{"error": "media_id is required"}) } // Safety check: block dangerous file extensions before download if safetySvc != nil { block := safetySvc.Check(req.Title, req.DownloadURL) if block != nil { if activitySvc != nil { activitySvc.LogAsync(service.LogEntry{ EventType: "safety_block", MediaID: &req.MediaID, MediaType: &req.MediaType, Title: req.Title, Description: &block.Reason, Data: json.RawMessage(fmt.Sprintf(`{"extension":"%s","indexer":"%s"}`, block.MatchedExtension, req.IndexerName)), }) } return c.JSON(http.StatusUnprocessableEntity, map[string]interface{}{ "error": block.Reason, "blocked": true, }) } } result, err := svc.Grab(ctx, req, dcSvc) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } qualityJSON, _ := json.Marshal(req.Quality) _, err = queueSvc.CreateQueueEntry(ctx, service.CreateQueueEntryRequest{ MediaID: req.MediaID, MediaType: req.MediaType, ReleaseTitle: req.Title, Indexer: req.IndexerName, DownloadClient: result.ClientName, Quality: qualityJSON, Protocol: result.Protocol, DownloadID: result.DownloadID, }) if err != nil { slog.Error("failed to create queue entry", "error", err) } // Log successful grab activity if activitySvc != nil { activitySvc.LogAsync(service.LogEntry{ EventType: "grab", MediaID: &req.MediaID, MediaType: &req.MediaType, Title: fmt.Sprintf("Grabbed %s", req.Title), Description: &req.IndexerName, Data: json.RawMessage(fmt.Sprintf(`{"release":"%s","client":"%s","protocol":"%s"}`, req.Title, result.ClientName, result.Protocol)), }) } return c.JSON(http.StatusCreated, map[string]interface{}{ "queue_id": result.QueueID, "download_id": result.DownloadID, "client": result.ClientName, "protocol": result.Protocol, }) } }