fix(downloads): support Transmission and aria2 end to end

Fixes #65
This commit is contained in:
ShukeBta
2026-08-10 21:04:04 +08:00
parent c0d743d89d
commit 42ef19b7e5
43 changed files with 2350 additions and 480 deletions
+15 -17
View File
@@ -100,13 +100,11 @@ func (h *DownloadClientHandler) Create(c *gin.Context) {
return
}
// 热插拔:加载新客户端
go func() {
if initErr := h.svc.DownloadMgr.AddClient(ctx, client); initErr != nil {
h.log.Warn("failed to hot-add download client", zap.Error(initErr))
}
}()
_ = h.svc.Downloads.ReloadConfig(ctx)
if err := h.svc.Downloads.ReloadConfig(ctx); err != nil {
h.log.Warn("failed to reload download clients after create", zap.Error(err))
Error(c, http.StatusInternalServerError, ErrInternal, "客户端已保存,但运行时重载失败: "+err.Error())
return
}
Success(c, client)
}
@@ -207,13 +205,11 @@ func (h *DownloadClientHandler) Update(c *gin.Context) {
}
clearLegacyQBitSettingsIfNoDefault(c.Request.Context(), h.svc)
// 热更新适配器
go func() {
if updateErr := h.svc.DownloadMgr.UpdateClient(ctx, client); updateErr != nil {
h.log.Warn("failed to hot-update download client", zap.Error(updateErr))
}
}()
_ = h.svc.Downloads.ReloadConfig(ctx)
if err := h.svc.Downloads.ReloadConfig(ctx); err != nil {
h.log.Warn("failed to reload download clients after update", zap.Error(err))
Error(c, http.StatusInternalServerError, ErrInternal, "客户端已更新,但运行时重载失败: "+err.Error())
return
}
Success(c, client)
}
@@ -235,10 +231,12 @@ func (h *DownloadClientHandler) Delete(c *gin.Context) {
return
}
// 热移除
h.svc.DownloadMgr.RemoveClient(id)
clearLegacyQBitSettingsIfNoDefault(c.Request.Context(), h.svc)
_ = h.svc.Downloads.ReloadConfig(ctx)
if err := h.svc.Downloads.ReloadConfig(ctx); err != nil {
h.log.Warn("failed to reload download clients after delete", zap.Error(err))
Error(c, http.StatusInternalServerError, ErrInternal, "客户端已删除,但运行时重载失败: "+err.Error())
return
}
SuccessWithMessage(c, "已删除", nil)
}
+12 -3
View File
@@ -39,7 +39,10 @@ func createDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
}
// 让真正发起下载的 DownloadService 立刻读到新的 qb 配置,
// 避免保存后还要重启进程才能生效。
_ = svc.Downloads.ReloadConfig(c.Request.Context())
if err := svc.Downloads.ReloadConfig(c.Request.Context()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "client saved but runtime reload failed: " + err.Error()})
return
}
c.JSON(http.StatusCreated, row)
}
}
@@ -56,7 +59,10 @@ func updateDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
_ = svc.Downloads.ReloadConfig(c.Request.Context())
if err := svc.Downloads.ReloadConfig(c.Request.Context()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "client updated but runtime reload failed: " + err.Error()})
return
}
c.JSON(http.StatusOK, row)
}
}
@@ -67,7 +73,10 @@ func deleteDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
_ = svc.Downloads.ReloadConfig(c.Request.Context())
if err := svc.Downloads.ReloadConfig(c.Request.Context()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "client deleted but runtime reload failed: " + err.Error()})
return
}
c.Status(http.StatusNoContent)
}
}
+23 -3
View File
@@ -138,11 +138,26 @@ func visibleLiveTorrents(rows []model.DownloadTask, live []service.QBitTorrent)
}
filtered := make([]service.QBitTorrent, 0, len(live))
for _, torrent := range live {
matchedByID := false
for _, row := range rows {
if strings.TrimSpace(row.ExternalID) != "" && strings.EqualFold(row.ExternalID, torrent.Hash) &&
(strings.TrimSpace(row.DownloadClientID) == "" || row.DownloadClientID == torrent.ClientID) {
filtered = append(filtered, torrent)
matchedByID = true
break
}
}
if matchedByID {
continue
}
torrentTitle := normalizeTitle(torrent.Name)
if torrentTitle == "" {
continue
}
for _, row := range rows {
if strings.TrimSpace(row.DownloadClientID) != "" && strings.TrimSpace(torrent.ClientID) != "" && row.DownloadClientID != torrent.ClientID {
continue
}
rowTitle := normalizeTitle(row.Title)
if rowTitle == "" {
continue
@@ -197,7 +212,7 @@ func deleteDownloadHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
hash := c.Param("hash")
withFiles := c.Query("delete_files") == "true"
if err := svc.Downloads.Delete(c.Request.Context(), hash, withFiles); err != nil {
if err := svc.Downloads.Delete(c.Request.Context(), hash, withFiles, c.Query("client_id")); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
@@ -208,6 +223,7 @@ func deleteDownloadHandler(svc *service.Container) gin.HandlerFunc {
type relocateDownloadReq struct {
Hash string `json:"hash" binding:"required"`
Location string `json:"location" binding:"required"`
ClientID string `json:"client_id"`
}
// relocateDownloadHandler moves a torrent's data to a new directory while
@@ -219,8 +235,12 @@ func relocateDownloadHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := svc.Downloads.RelocateTorrent(c.Request.Context(), req.Hash, req.Location); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
if err := svc.Downloads.RelocateTorrent(c.Request.Context(), req.Hash, req.Location, req.ClientID); err != nil {
status := http.StatusInternalServerError
if errors.Is(err, service.ErrDownloadOperationUnsupported) {
status = http.StatusBadRequest
}
c.JSON(status, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"hash": strings.TrimSpace(req.Hash), "location": strings.TrimSpace(req.Location)})
+2 -12
View File
@@ -11,15 +11,9 @@ import (
"github.com/ShukeBta/MediaStationGo/internal/service"
)
// downloadPauseHandler is a thin alias — the underlying qBittorrent
// service exposes pause via the WebUI; we mark our local row too so
// the React UI shows the right state on next refresh.
func downloadPauseHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
if err := svc.Repo.DB.WithContext(c.Request.Context()).
Model(&model.DownloadTask{}).
Where("id = ?", c.Param("id")).
Update("status", "paused").Error; err != nil {
if err := svc.Downloads.PauseDownloadTask(c.Request.Context(), c.Param("id")); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
@@ -27,13 +21,9 @@ func downloadPauseHandler(svc *service.Container) gin.HandlerFunc {
}
}
// downloadResumeHandler marks the row as queued so the next poll picks it up.
func downloadResumeHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
if err := svc.Repo.DB.WithContext(c.Request.Context()).
Model(&model.DownloadTask{}).
Where("id = ?", c.Param("id")).
Update("status", "queued").Error; err != nil {
if err := svc.Downloads.ResumeDownloadTask(c.Request.Context(), c.Param("id")); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}