package handler import ( "net/http" "strconv" "caorushizi.cn/mediago/internal/api/dto" "caorushizi.cn/mediago/internal/api/sse" "caorushizi.cn/mediago/internal/i18n" "caorushizi.cn/mediago/internal/logger" "caorushizi.cn/mediago/internal/service" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // DownloadHandler handles database-persisted download task endpoints. type DownloadHandler struct { svc *service.DownloadTaskService conf ConfigStore hub *sse.Hub } // NewDownloadHandler creates a DownloadHandler. func NewDownloadHandler(svc *service.DownloadTaskService, conf ConfigStore, hub *sse.Hub) *DownloadHandler { return &DownloadHandler{svc: svc, conf: conf, hub: hub} } // Create adds download tasks (supports batch creation). func (h *DownloadHandler) Create(c *gin.Context) { var req dto.AddDownloadBatchReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: err.Error()}) return } inputs := make([]*service.AddDownloadTaskInput, 0, len(req.Tasks)) for _, t := range req.Tasks { inputs = append(inputs, &service.AddDownloadTaskInput{ Name: t.Name, Type: t.Type, URL: t.URL, Headers: t.Headers, Folder: t.Folder, }) } videos, err := h.svc.AddDownloadTasks(inputs) if err != nil { logger.Error("Failed to add download tasks", zap.Error(err)) c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Success: false, Code: http.StatusInternalServerError, Message: err.Error()}) return } // Auto-start downloads if requested if req.StartDownload { localPath, _ := h.conf.Get("local").(string) deleteSegments, _ := h.conf.Get("deleteSegments").(bool) for _, v := range videos { if err := h.svc.StartDownload(v.ID, localPath, deleteSegments); err != nil { logger.Warn("auto-start download failed", zap.Int64("id", v.ID), zap.Error(err)) } } } // Broadcast creation so every connected renderer (main window, overlay // dialog, external clients) can update its own badge/state without // relying on cross-WebContents IPC. if h.hub != nil && len(videos) > 0 { ids := make([]int64, 0, len(videos)) for _, v := range videos { ids = append(ids, v.ID) } h.hub.Broadcast("download-create", map[string]interface{}{ "ids": ids, "count": len(ids), }) } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgOK), Data: videos}) } // List retrieves a paginated list of download tasks. func (h *DownloadHandler) List(c *gin.Context) { var req dto.DownloadPaginationReq if err := c.ShouldBindQuery(&req); err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: err.Error()}) return } localPath := c.Query("localPath") result, err := h.svc.GetDownloadTasks(req.Current, req.PageSize, req.Filter, localPath) if err != nil { logger.Error("Failed to get download tasks", zap.Error(err)) c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Success: false, Code: http.StatusInternalServerError, Message: err.Error()}) return } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgOK), Data: result}) } // Get retrieves a single download task. func (h *DownloadHandler) Get(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: i18n.T(c, i18n.MsgInvalidID)}) return } video, err := h.svc.FindByIDOrFail(id) if err != nil { c.JSON(http.StatusNotFound, dto.ErrorResponse{Success: false, Code: http.StatusNotFound, Message: err.Error()}) return } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgOK), Data: video}) } // Edit edits a download task. func (h *DownloadHandler) Edit(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: i18n.T(c, i18n.MsgInvalidID)}) return } var req dto.EditDownloadReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: err.Error()}) return } data := make(map[string]interface{}) if req.Name != nil { data["name"] = *req.Name } if req.URL != nil { data["url"] = *req.URL } if req.Headers != nil { data["headers"] = *req.Headers } if req.Folder != nil { data["folder"] = *req.Folder } video, err := h.svc.EditDownloadTask(id, data) if err != nil { logger.Error("Failed to edit download task", zap.Error(err)) c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Success: false, Code: http.StatusInternalServerError, Message: err.Error()}) return } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgOK), Data: video}) } // Delete removes a download task. func (h *DownloadHandler) Delete(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: i18n.T(c, i18n.MsgInvalidID)}) return } if err := h.svc.DeleteDownloadTask(id); err != nil { logger.Error("Failed to delete download task", zap.Error(err)) c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Success: false, Code: http.StatusInternalServerError, Message: err.Error()}) return } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgDeleted)}) } // Start begins downloading a task. func (h *DownloadHandler) Start(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: i18n.T(c, i18n.MsgInvalidID)}) return } var req dto.StartDownloadReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: err.Error()}) return } // Sync the client-provided localPath into the runtime configuration if req.LocalPath != "" { if err := h.conf.Set("local", req.LocalPath); err != nil { logger.Warn("Failed to sync localPath to config", zap.Error(err)) } } if err := h.svc.StartDownload(id, req.LocalPath, req.DeleteSegments); err != nil { logger.Error("Failed to start download", zap.Error(err)) c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Success: false, Code: http.StatusInternalServerError, Message: err.Error()}) return } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgDownloadStarted)}) } // Stop stops a download. func (h *DownloadHandler) Stop(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: i18n.T(c, i18n.MsgInvalidID)}) return } if err := h.svc.StopDownload(id); err != nil { logger.Error("Failed to stop download", zap.Error(err)) c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Success: false, Code: http.StatusInternalServerError, Message: err.Error()}) return } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgDownloadStopped)}) } // Logs retrieves the download log. func (h *DownloadHandler) Logs(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: i18n.T(c, i18n.MsgInvalidID)}) return } content, err := h.svc.GetDownloadLog(id) if err != nil { c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Success: false, Code: http.StatusInternalServerError, Message: err.Error()}) return } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgOK), Data: map[string]interface{}{ "id": id, "log": content, }}) } // Folders retrieves the folder list. func (h *DownloadHandler) Folders(c *gin.Context) { folders, err := h.svc.GetTaskFolders() if err != nil { logger.Error("Failed to get task folders", zap.Error(err)) c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Success: false, Code: http.StatusInternalServerError, Message: err.Error()}) return } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgOK), Data: folders}) } // Export exports the download list. func (h *DownloadHandler) Export(c *gin.Context) { text, err := h.svc.ExportDownloadList() if err != nil { logger.Error("Failed to export download list", zap.Error(err)) c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Success: false, Code: http.StatusInternalServerError, Message: err.Error()}) return } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgOK), Data: text}) } // UpdateStatus bulk-updates task statuses. func (h *DownloadHandler) UpdateStatus(c *gin.Context) { var req dto.UpdateStatusReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: err.Error()}) return } if err := h.svc.SetStatus(req.IDs, req.Status); err != nil { logger.Error("Failed to update status", zap.Error(err)) c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Success: false, Code: http.StatusInternalServerError, Message: err.Error()}) return } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgStatusUpdated)}) } // UpdateIsLive updates the live-stream flag. func (h *DownloadHandler) UpdateIsLive(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: i18n.T(c, i18n.MsgInvalidID)}) return } var req dto.UpdateIsLiveReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, dto.ErrorResponse{Success: false, Code: http.StatusBadRequest, Message: err.Error()}) return } video, err := h.svc.SetIsLive(id, req.IsLive) if err != nil { logger.Error("Failed to update isLive", zap.Error(err)) c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Success: false, Code: http.StatusInternalServerError, Message: err.Error()}) return } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgOK), Data: video}) } // Active retrieves active tasks. func (h *DownloadHandler) Active(c *gin.Context) { tasks, err := h.svc.FindActiveTasks() if err != nil { logger.Error("Failed to find active tasks", zap.Error(err)) c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Success: false, Code: http.StatusInternalServerError, Message: err.Error()}) return } c.JSON(http.StatusOK, dto.SuccessResponse{Success: true, Code: http.StatusOK, Message: i18n.T(c, i18n.MsgOK), Data: tasks}) }