feat: improve subscription and download cards

This commit is contained in:
ShukeBta
2026-05-28 23:27:34 +08:00
parent f457d0050e
commit c1b5dac784
20 changed files with 1239 additions and 204 deletions
+112 -12
View File
@@ -4,17 +4,24 @@ package handler
import (
"context"
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/middleware"
"github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/service"
)
type addDownloadReq struct {
URL string `json:"url" binding:"required"`
SavePath string `json:"save_path"`
URL string `json:"url" binding:"required"`
SavePath string `json:"save_path"`
Title string `json:"title"`
PosterURL string `json:"poster_url"`
BackdropURL string `json:"backdrop_url"`
Overview string `json:"overview"`
}
// resolvePTDownloadURL 把站点搜索结果里的"详情/获取签名"URL 解析成 qb 能直接
@@ -22,10 +29,10 @@ type addDownloadReq struct {
//
// 链路:
//
// 1. 拿 URL 的 host,到 sites 表里找 base_url 同源的站点。
// 2. 如果站点的 type 是已知 PT 框架(mteam/nexusphp/unit3d/...),
// 就用对应适配器的 GetDownloadURL,传入从 URL 里 parse 出来的 id。
// 3. 任一步失败都直接返回原 URL,让 qb 自己去拉(保持向后兼容)。
// 1. 拿 URL 的 host,到 sites 表里找 base_url 同源的站点。
// 2. 如果站点的 type 是已知 PT 框架(mteam/nexusphp/unit3d/...),
// 就用对应适配器的 GetDownloadURL,传入从 URL 里 parse 出来的 id。
// 3. 任一步失败都直接返回原 URL,让 qb 自己去拉(保持向后兼容)。
//
// 这一步存在的意义:M-Team 等站点的搜索结果里 download_url 是
// /api/torrent/genDlToken?id=xxx,需要带 x-api-key 才能调用,qb 自己
@@ -39,8 +46,8 @@ func resolvePTDownloadURL(ctx context.Context, svc *service.Container, raw strin
return raw
}
log.Info("resolved PT download URL",
zap.String("from", raw),
zap.String("to", resolved))
zap.String("from", redactDownloadURL(raw)),
zap.String("to", redactDownloadURL(resolved)))
return resolved
}
@@ -54,12 +61,22 @@ func addDownloadHandler(svc *service.Container) gin.HandlerFunc {
uid, _ := c.Get(middleware.CtxUserID)
// 把站点搜索 URL 转换成真实可下载 URL(M-Team 走 genDlToken 等)。
realURL := resolvePTDownloadURL(c.Request.Context(), svc, req.URL, svc.Log)
t, err := svc.Downloads.AddDownload(c.Request.Context(), uid.(string), realURL, req.SavePath)
fallbackTitle := req.Title
if strings.TrimSpace(fallbackTitle) == "" {
fallbackTitle = realURL
}
meta := enrichDownloadTaskMeta(c.Request.Context(), svc, service.DownloadTaskMeta{
Title: req.Title,
PosterURL: req.PosterURL,
BackdropURL: req.BackdropURL,
Overview: req.Overview,
}, fallbackTitle, "")
t, err := svc.Downloads.AddDownloadWithMeta(c.Request.Context(), uid.(string), realURL, req.SavePath, meta)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
svc.Audit.Record(c.Request.Context(), uid.(string), "download.add", realURL, c.ClientIP(), "")
svc.Audit.Record(c.Request.Context(), uid.(string), "download.add", redactDownloadURL(realURL), c.ClientIP(), "")
c.JSON(http.StatusOK, t)
}
}
@@ -71,13 +88,96 @@ func listDownloadsHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
rows = visibleDownloadRows(c, rows)
enrichAndPersistDownloadRows(c.Request.Context(), svc, rows)
if !isAdminRequest(c) {
live = visibleLiveTorrents(rows, live)
}
taskViews, torrentViews := service.DownloadViews(rows, live)
enrichDownloadTorrentViews(c.Request.Context(), svc, torrentViews)
c.JSON(http.StatusOK, gin.H{
"tasks": rows,
"torrents": live,
"tasks": taskViews,
"torrents": torrentViews,
})
}
}
func visibleDownloadRows(c *gin.Context, rows []model.DownloadTask) []model.DownloadTask {
if isAdminRequest(c) {
return rows
}
uid, _ := c.Get(middleware.CtxUserID)
userID, _ := uid.(string)
filtered := make([]model.DownloadTask, 0, len(rows))
for _, row := range rows {
if row.UserID == userID {
filtered = append(filtered, row)
}
}
return filtered
}
func visibleLiveTorrents(rows []model.DownloadTask, live []service.QBitTorrent) []service.QBitTorrent {
if len(rows) == 0 || len(live) == 0 {
return nil
}
filtered := make([]service.QBitTorrent, 0, len(live))
for _, torrent := range live {
torrentTitle := normalizeTitle(torrent.Name)
if torrentTitle == "" {
continue
}
for _, row := range rows {
rowTitle := normalizeTitle(row.Title)
if rowTitle == "" {
continue
}
if strings.Contains(torrentTitle, rowTitle) || strings.Contains(rowTitle, torrentTitle) {
filtered = append(filtered, torrent)
break
}
}
}
return filtered
}
func isAdminRequest(c *gin.Context) bool {
role, _ := c.Get(middleware.CtxUserRole)
return role == "admin"
}
func normalizeTitle(title string) string {
title = strings.ToLower(title)
var b strings.Builder
for _, r := range title {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r > 127 {
b.WriteRune(r)
}
}
return b.String()
}
func redactDownloadURL(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
if strings.HasPrefix(strings.ToLower(raw), "magnet:") {
return "magnet:?xt=***"
}
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
return "[redacted-download-url]"
}
u.RawQuery = ""
u.Fragment = ""
base := u.String()
if base == "" {
return u.Scheme + "://" + u.Host
}
return base
}
func deleteDownloadHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
hash := c.Param("hash")