diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ca61bbb..5db2549 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -40,15 +40,15 @@ func TestLoadDefaults(t *testing.T) { if cfg.Database.MaxOpenConns != defaultDatabaseMaxOpenConns { t.Fatalf("expected default MaxOpenConns %d, got %d", defaultDatabaseMaxOpenConns, cfg.Database.MaxOpenConns) } - if cfg.Cache.RedisPrefix != "mmtl" { - t.Fatalf("expected default redis prefix, got %q", cfg.Cache.RedisPrefix) - } - if cfg.Cache.MediaTTLSeconds != 15 { - t.Fatalf("expected default media cache ttl 15, got %d", cfg.Cache.MediaTTLSeconds) - } - if cfg.Search.Index != "mmtl_media" { - t.Fatalf("expected default search index, got %q", cfg.Search.Index) - } + if cfg.Cache.RedisPrefix != "mmtl" { + t.Fatalf("expected default redis prefix, got %q", cfg.Cache.RedisPrefix) + } + if cfg.Cache.MediaTTLSeconds != 15 { + t.Fatalf("expected default media cache ttl 15, got %d", cfg.Cache.MediaTTLSeconds) + } + if cfg.Search.Index != "mmtl_media" { + t.Fatalf("expected default search index, got %q", cfg.Search.Index) + } if cfg.Database.MaxIdleConns != defaultDatabaseMaxIdleConns { t.Fatalf("expected default MaxIdleConns %d, got %d", defaultDatabaseMaxIdleConns, cfg.Database.MaxIdleConns) } diff --git a/internal/handler/api_config_handler.go b/internal/handler/api_config_handler.go index f186722..f59888b 100644 --- a/internal/handler/api_config_handler.go +++ b/internal/handler/api_config_handler.go @@ -177,7 +177,7 @@ func (h *ApiConfigHandler) TestApiConfig(c *gin.Context) { _ = h.svc.ApiConfig.UpdateTestResult(c.Request.Context(), provider, result) c.JSON(http.StatusOK, gin.H{ - "code": 0, + "code": 0, "message": "ok", "data": gin.H{ "result": result, diff --git a/internal/handler/danmaku.go b/internal/handler/danmaku.go index 628589e..afd136c 100644 --- a/internal/handler/danmaku.go +++ b/internal/handler/danmaku.go @@ -30,4 +30,4 @@ func getDanmakuConfigHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { c.JSON(http.StatusOK, svc.Danmaku.Config(c.Request.Context())) } -} \ No newline at end of file +} diff --git a/internal/handler/emby_authed_loop_test.go b/internal/handler/emby_authed_loop_test.go index 5b4d390..6d199e2 100644 --- a/internal/handler/emby_authed_loop_test.go +++ b/internal/handler/emby_authed_loop_test.go @@ -144,4 +144,4 @@ func TestEmbyLoginWithAuthorizationHeaderToken(t *testing.T) { if w.Code != http.StatusOK { t.Fatalf("Authorization-token request = %d body=%s", w.Code, w.Body.String()) } -} \ No newline at end of file +} diff --git a/internal/handler/routes_authenticated_test.go b/internal/handler/routes_authenticated_test.go index 2633a6a..8acc5c1 100644 --- a/internal/handler/routes_authenticated_test.go +++ b/internal/handler/routes_authenticated_test.go @@ -23,16 +23,16 @@ func TestAuthenticatedRouteSurfacesAreRegistered(t *testing.T) { routes[route.Method+" "+route.Path] = true } - for _, want := range []string{ - "GET /api/me", - "GET /api/auth/permissions", - "GET /api/libraries", - "GET /api/media", - "GET /api/stream/:id", - "GET /api/storage", - "GET /api/watch-history", - "GET /api/playback/:id/info", - } { + for _, want := range []string{ + "GET /api/me", + "GET /api/auth/permissions", + "GET /api/libraries", + "GET /api/media", + "GET /api/stream/:id", + "GET /api/storage", + "GET /api/watch-history", + "GET /api/playback/:id/info", + } { if !routes[want] { t.Fatalf("%s route is not registered", want) } diff --git a/internal/handler/stats_user.go b/internal/handler/stats_user.go index d2f14c6..ee0b156 100644 --- a/internal/handler/stats_user.go +++ b/internal/handler/stats_user.go @@ -26,9 +26,9 @@ func statsUserHandler(svc *service.Container) gin.HandlerFunc { _ = svc.Repo.DB.Model(&model.PlaybackHistory{}). Where("user_id = ?", uid).Count(&total).Error c.JSON(http.StatusOK, gin.H{ - "user_id": uid, - "watched_ms": watched, - "plays": total, + "user_id": uid, + "watched_ms": watched, + "plays": total, "watched_hours": float64(watched) / 1000.0 / 3600.0, }) } diff --git a/internal/handler/strm.go b/internal/handler/strm.go index 57387e7..cf9e4dd 100644 --- a/internal/handler/strm.go +++ b/internal/handler/strm.go @@ -410,7 +410,11 @@ func cancelPendingDownloadsHandler(svc *service.Container) gin.HandlerFunc { func strmPlayHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { provider := strings.TrimSpace(c.Param("provider")) - result, err := svc.Strm.ResolvePlay(c.Request.Context(), provider, url.Values(c.Request.URL.Query())) + q := url.Values(c.Request.URL.Query()) + if userAgent := c.GetHeader("User-Agent"); userAgent != "" { + q.Set("__ua", userAgent) + } + result, err := svc.Strm.ResolvePlay(c.Request.Context(), provider, q) if err != nil { if errors.Is(err, service.ErrStrmPlayNotFound) { c.Status(http.StatusNotFound) diff --git a/internal/handler/system_extra.go b/internal/handler/system_extra.go index ef6f2e6..0e93cbd 100644 --- a/internal/handler/system_extra.go +++ b/internal/handler/system_extra.go @@ -58,12 +58,12 @@ func schemaHandler(_ *service.Container) gin.HandlerFunc { return func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "groups": []gin.H{ - { - "key": "general", - "label": "常规", - "items": []gin.H{ - {"key": "tmdb.language", "type": "select", "label": "TMDb 元数据语言"}, - {"key": "transcode.enabled", "type": "toggle", "label": "启用转码"}, + { + "key": "general", + "label": "常规", + "items": []gin.H{ + {"key": "tmdb.language", "type": "select", "label": "TMDb 元数据语言"}, + {"key": "transcode.enabled", "type": "toggle", "label": "启用转码"}, {"key": "transcode.hw_accel", "type": "select", "label": "硬件编码器"}, {"key": "transcode.hw_enabled", "type": "toggle", "label": "启用硬件加速"}, {"key": "transcode.max_jobs", "type": "number", "label": "最大并发"}, @@ -120,10 +120,10 @@ func schemaHandler(_ *service.Container) gin.HandlerFunc { {"key": "qbittorrent.password", "type": "text"}, {"key": "qbittorrent.savepath", "type": "text"}, }, -}, - { - "key": "system-update", - "label": "系统更新", + }, + { + "key": "system-update", + "label": "系统更新", "items": []gin.H{ {"key": "system.update.image", "type": "text", "label": "应用镜像"}, {"key": "system.update.compose_dir", "type": "text", "label": "Docker Compose 安装目录"}, diff --git a/internal/handler/task_helpers.go b/internal/handler/task_helpers.go index 6f472b8..88b4f27 100644 --- a/internal/handler/task_helpers.go +++ b/internal/handler/task_helpers.go @@ -7,4 +7,4 @@ func finishHTTPTask(task *service.TaskHandle, err error, stage, message string, return } task.Finish(err, service.TaskUpdate{Stage: stage, Message: message, Metrics: metrics, Details: details}) -} \ No newline at end of file +} diff --git a/internal/middleware/emby_auth.go b/internal/middleware/emby_auth.go index b9130dd..adeb720 100644 --- a/internal/middleware/emby_auth.go +++ b/internal/middleware/emby_auth.go @@ -42,14 +42,14 @@ func EmbyAuthRequired(secret string) gin.HandlerFunc { return []byte(secret), nil }) -if err != nil || !parsed.Valid || claims.UserID == "" { - c.JSON(http.StatusUnauthorized, gin.H{ - "Code": 40101, - "Message": "Invalid token", - }) - c.Abort() - return - } + if err != nil || !parsed.Valid || claims.UserID == "" { + c.JSON(http.StatusUnauthorized, gin.H{ + "Code": 40101, + "Message": "Invalid token", + }) + c.Abort() + return + } c.Set(EmbyCtxUserID, claims.UserID) c.Set(CtxUserID, claims.UserID) diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index ad684de..f32b2de 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -106,13 +106,13 @@ func TestAuthRequiredSyncsAccessTokenCookieFromBearer(t *testing.T) { }, }) - router := gin.New() - router.Use(AuthRequired(secret)) - router.GET("/api/test-auth-cookie", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"ok": true}) - }) + router := gin.New() + router.Use(AuthRequired(secret)) + router.GET("/api/test-auth-cookie", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"ok": true}) + }) - req := httptest.NewRequest(http.MethodGet, "/api/test-auth-cookie", nil) + req := httptest.NewRequest(http.MethodGet, "/api/test-auth-cookie", nil) req.Header.Set("Authorization", "Bearer "+token) w := httptest.NewRecorder() router.ServeHTTP(w, req) diff --git a/internal/model/api_config.go b/internal/model/api_config.go index fd487ee..b382ed6 100644 --- a/internal/model/api_config.go +++ b/internal/model/api_config.go @@ -39,7 +39,7 @@ func PredefinedProviders() []ApiProvider { {ID: "openai", Name: "OpenAI", Description: "GPT 系列模型", HasAPIKey: true, HasBaseURL: true}, {ID: "deepseek", Name: "DeepSeek", Description: "DeepSeek 大模型", HasAPIKey: true, HasBaseURL: true}, {ID: "siliconflow", Name: "SiliconFlow", Description: "AI 模型聚合 API", HasAPIKey: true, HasBaseURL: true}, - {ID: "adult", Name: "Adult / 番号", Description: "JavDB/JavBus 成人内容元数据与 Cookie 凭据", HasAPIKey: true, HasBaseURL: true}, - {ID: "metatube", Name: "MetaTube", Description: "MetaTube Server 番号元数据后端服务", HasAPIKey: true, HasBaseURL: true}, - } + {ID: "adult", Name: "Adult / 番号", Description: "JavDB/JavBus 成人内容元数据与 Cookie 凭据", HasAPIKey: true, HasBaseURL: true}, + {ID: "metatube", Name: "MetaTube", Description: "MetaTube Server 番号元数据后端服务", HasAPIKey: true, HasBaseURL: true}, + } } diff --git a/internal/model/permission.go b/internal/model/permission.go index 99bc59b..dfddde6 100644 --- a/internal/model/permission.go +++ b/internal/model/permission.go @@ -16,7 +16,7 @@ type UserPermission struct { UserID string `gorm:"uniqueIndex;size:36;not null" json:"user_id"` // 默认开启(6项)- Basic - CanViewDashboard bool `gorm:"default:true" json:"can_view_dashboard"` + CanViewDashboard bool `gorm:"default:true" json:"can_view_dashboard"` CanPlayMedia bool `gorm:"default:true" json:"can_play_media"` CanCast bool `gorm:"default:true" json:"can_cast"` CanExternalPlayer bool `gorm:"default:true" json:"can_external_player"` @@ -24,18 +24,18 @@ type UserPermission struct { CanViewHistory bool `gorm:"default:true" json:"can_view_history"` // 默认关闭(13项)- Advanced - CanEditMedia bool `gorm:"default:false" json:"can_edit_media"` - CanRescrape bool `gorm:"default:false" json:"can_rescrape"` - CanUseAI bool `gorm:"default:false" json:"can_use_ai"` - CanCaptureFrames bool `gorm:"default:false" json:"can_capture_frames"` - CanManageDownloads bool `gorm:"default:false" json:"can_manage_downloads"` - CanManageSubscriptions bool `gorm:"default:false" json:"can_manage_subscriptions"` - CanManageSites bool `gorm:"default:false" json:"can_manage_sites"` - CanUseAIAssistant bool `gorm:"default:false" json:"can_use_ai_assistant"` - CanManageUsers bool `gorm:"default:false" json:"can_manage_users"` - CanManageFiles bool `gorm:"default:false" json:"can_manage_files"` - CanManageStrm bool `gorm:"default:false" json:"can_manage_strm"` - CanAccessSettings bool `gorm:"default:false" json:"can_access_settings"` + CanEditMedia bool `gorm:"default:false" json:"can_edit_media"` + CanRescrape bool `gorm:"default:false" json:"can_rescrape"` + CanUseAI bool `gorm:"default:false" json:"can_use_ai"` + CanCaptureFrames bool `gorm:"default:false" json:"can_capture_frames"` + CanManageDownloads bool `gorm:"default:false" json:"can_manage_downloads"` + CanManageSubscriptions bool `gorm:"default:false" json:"can_manage_subscriptions"` + CanManageSites bool `gorm:"default:false" json:"can_manage_sites"` + CanUseAIAssistant bool `gorm:"default:false" json:"can_use_ai_assistant"` + CanManageUsers bool `gorm:"default:false" json:"can_manage_users"` + CanManageFiles bool `gorm:"default:false" json:"can_manage_files"` + CanManageStrm bool `gorm:"default:false" json:"can_manage_strm"` + CanAccessSettings bool `gorm:"default:false" json:"can_access_settings"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -52,26 +52,26 @@ func (p *UserPermission) BeforeCreate(_ *gorm.DB) error { // NewDefaultPermission 创建带有默认权限的 UserPermission。 func NewDefaultPermission(userID string) *UserPermission { return &UserPermission{ - ID: uuid.NewString(), - UserID: userID, - CanViewDashboard: true, - CanPlayMedia: true, - CanCast: true, - CanExternalPlayer: true, - CanFavorite: true, - CanViewHistory: true, - CanEditMedia: false, - CanRescrape: false, - CanUseAI: false, - CanCaptureFrames: false, - CanManageDownloads: false, - CanManageSubscriptions: false, - CanManageSites: false, - CanUseAIAssistant: false, - CanManageUsers: false, - CanManageFiles: false, - CanManageStrm: false, - CanAccessSettings: false, + ID: uuid.NewString(), + UserID: userID, + CanViewDashboard: true, + CanPlayMedia: true, + CanCast: true, + CanExternalPlayer: true, + CanFavorite: true, + CanViewHistory: true, + CanEditMedia: false, + CanRescrape: false, + CanUseAI: false, + CanCaptureFrames: false, + CanManageDownloads: false, + CanManageSubscriptions: false, + CanManageSites: false, + CanUseAIAssistant: false, + CanManageUsers: false, + CanManageFiles: false, + CanManageStrm: false, + CanAccessSettings: false, } } @@ -88,13 +88,13 @@ func (p *UserPermission) PermissionMap() map[string]bool { "can_rescrape": p.CanRescrape, "can_use_ai": p.CanUseAI, "can_capture_frames": p.CanCaptureFrames, - "can_manage_downloads": p.CanManageDownloads, - "can_manage_subscriptions": p.CanManageSubscriptions, + "can_manage_downloads": p.CanManageDownloads, + "can_manage_subscriptions": p.CanManageSubscriptions, "can_manage_sites": p.CanManageSites, - "can_use_ai_assistant": p.CanUseAIAssistant, + "can_use_ai_assistant": p.CanUseAIAssistant, "can_manage_users": p.CanManageUsers, "can_manage_files": p.CanManageFiles, "can_manage_strm": p.CanManageStrm, - "can_access_settings": p.CanAccessSettings, + "can_access_settings": p.CanAccessSettings, } } diff --git a/internal/repository/storage_config_repo.go b/internal/repository/storage_config_repo.go index e6c666e..403d808 100644 --- a/internal/repository/storage_config_repo.go +++ b/internal/repository/storage_config_repo.go @@ -66,7 +66,6 @@ func (r *StorageConfigRepository) Upsert(ctx context.Context, c *model.StorageCo }).Error } - // Delete removes a storage config by ID. func (r *StorageConfigRepository) Delete(ctx context.Context, id string) error { return r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.StorageConfig{}).Error diff --git a/internal/service/cloud/pan115_openapi.go b/internal/service/cloud/pan115_openapi.go index 351eb82..d15e31d 100644 --- a/internal/service/cloud/pan115_openapi.go +++ b/internal/service/cloud/pan115_openapi.go @@ -77,7 +77,11 @@ func (p *openAPI115Provider) List(ctx context.Context, dirID string) ([]FileEntr } func (p *openAPI115Provider) Resolve(ctx context.Context, fileRef string) (*DirectLink, error) { - url, err := p.c.GetDownloadURL(ctx, fileRef) + return p.ResolveWithUA(ctx, fileRef, "") +} + +func (p *openAPI115Provider) ResolveWithUA(ctx context.Context, fileRef, ua string) (*DirectLink, error) { + url, err := p.c.GetDownloadURLWithUA(ctx, fileRef, ua) if err != nil { return nil, err } diff --git a/internal/service/cloud115/client.go b/internal/service/cloud115/client.go index fac3fe1..94ecca8 100644 --- a/internal/service/cloud115/client.go +++ b/internal/service/cloud115/client.go @@ -90,11 +90,15 @@ type RespBase struct { } // doJSON 执行 HTTP 请求并解析为统一响应;带 AccessToken(access=true 时)。 -func (c *OpenClient) doJSON(ctx context.Context, method, rawURL string, form map[string]string, access bool, retries int) (*RespBase, error) { +func (c *OpenClient) doJSON(ctx context.Context, method, rawURL string, form map[string]string, access bool, retries int, uas ...string) (*RespBase, error) { executor := c.executor if executor == nil { executor = GetGlobalExecutor() } + ua := "" + if len(uas) > 0 { + ua = uas[0] + } var lastErr error for attempt := 0; attempt <= retries; attempt++ { @@ -103,7 +107,7 @@ func (c *OpenClient) doJSON(ctx context.Context, method, rawURL string, form map return nil, err } - req, err := c.buildRequest(ctx, method, rawURL, form, access) + req, err := c.buildRequestWithUA(ctx, method, rawURL, form, access, ua) if err != nil { return nil, err } @@ -187,6 +191,10 @@ func (c *OpenClient) doJSON(ctx context.Context, method, rawURL string, form map } func (c *OpenClient) buildRequest(ctx context.Context, method, rawURL string, form map[string]string, access bool) (*http.Request, error) { + return c.buildRequestWithUA(ctx, method, rawURL, form, access, "") +} + +func (c *OpenClient) buildRequestWithUA(ctx context.Context, method, rawURL string, form map[string]string, access bool, ua string) (*http.Request, error) { method = strings.ToUpper(method) var body io.Reader if method == http.MethodPost && len(form) > 0 { @@ -211,7 +219,11 @@ func (c *OpenClient) buildRequest(ctx context.Context, method, rawURL string, fo if err != nil { return nil, err } - req.Header.Set("User-Agent", DefaultUA) + targetUA := DefaultUA + if strings.TrimSpace(ua) != "" { + targetUA = strings.TrimSpace(ua) + } + req.Header.Set("User-Agent", targetUA) if method == http.MethodPost && len(form) > 0 { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } @@ -226,6 +238,11 @@ func (c *OpenClient) doAuthJSON(ctx context.Context, method, rawURL string, form return c.doJSON(ctx, method, rawURL, form, true, retries) } +// doAuthJSONWithUA 带自定义 User-Agent 的业务请求(换取直链等防盗链接口使用)。 +func (c *OpenClient) doAuthJSONWithUA(ctx context.Context, method, rawURL string, form map[string]string, retries int, ua string) (*RespBase, error) { + return c.doJSON(ctx, method, rawURL, form, true, retries, ua) +} + // IsThrottleCode 判断是否为限流错误码。 func IsThrottleCode(code int) bool { return code == RequestMaxLimitCode || code == RequestRateLimitCode diff --git a/internal/service/cloud115/open.go b/internal/service/cloud115/open.go index 5192add..276f151 100644 --- a/internal/service/cloud115/open.go +++ b/internal/service/cloud115/open.go @@ -127,11 +127,17 @@ type downloadURLData struct { // GetDownloadURL 获取下载直链(pickcode)。命中缓存直接返回, // 避免对同一文件反复换取直链触发 115 风控。 func (c *OpenClient) GetDownloadURL(ctx context.Context, pickCode string) (string, error) { - if cached := GetDownloadURLCache(pickCode); cached != "" { + return c.GetDownloadURLWithUA(ctx, pickCode, "") +} + +// GetDownloadURLWithUA 支持按调用方/播放器 User-Agent 换取对应的 115 CDN 直链(用于 115 防盗链白名单校验)。 +func (c *OpenClient) GetDownloadURLWithUA(ctx context.Context, pickCode, ua string) (string, error) { + ua = strings.TrimSpace(ua) + if cached := GetDownloadURLCache(pickCode, ua); cached != "" { return cached, nil } params := map[string]string{"pick_code": pickCode} - resp, err := c.doAuthJSON(ctx, "POST", ProAPIBase+"/open/ufile/downurl", params, 1) + resp, err := c.doAuthJSONWithUA(ctx, "POST", ProAPIBase+"/open/ufile/downurl", params, 1, ua) if err != nil { return "", err } @@ -143,7 +149,7 @@ func (c *OpenClient) GetDownloadURL(ctx context.Context, pickCode string) (strin if first.URL.URL == "" { return "", fmt.Errorf("115: 下载地址为空(文件可能未上传完成或已被删除)") } - SetDownloadURLCache(pickCode, first.URL.URL) + SetDownloadURLCache(pickCode, first.URL.URL, ua) return first.URL.URL, nil } diff --git a/internal/service/cloud115/url_cache.go b/internal/service/cloud115/url_cache.go index 40b488a..383dd29 100644 --- a/internal/service/cloud115/url_cache.go +++ b/internal/service/cloud115/url_cache.go @@ -7,6 +7,7 @@ package cloud115 import ( + "strings" "sync" "time" ) @@ -28,17 +29,30 @@ var ( urlCache = map[string]urlCacheEntry{} ) +// urlCacheKey 构造缓存键名(pickCode + UA,实现直链防盗链按客户端 UA 独立缓存)。 +func urlCacheKey(pickCode, ua string) string { + if ua == "" { + return pickCode + } + return pickCode + "@" + ua +} + // GetDownloadURLCache 返回未过期的缓存直链;不存在或已过期返回空串。 -func GetDownloadURLCache(pickCode string) string { +func GetDownloadURLCache(pickCode string, uas ...string) string { if pickCode == "" { return "" } + ua := "" + if len(uas) > 0 { + ua = uas[0] + } + key := urlCacheKey(pickCode, ua) urlCacheMu.Lock() defer urlCacheMu.Unlock() - entry, ok := urlCache[pickCode] + entry, ok := urlCache[key] if !ok || time.Now().After(entry.expiresAt) { if ok { - delete(urlCache, pickCode) + delete(urlCache, key) } return "" } @@ -46,10 +60,14 @@ func GetDownloadURLCache(pickCode string) string { } // SetDownloadURLCache 写入直链缓存。 -func SetDownloadURLCache(pickCode, url string) { +func SetDownloadURLCache(pickCode string, url string, uas ...string) { if pickCode == "" || url == "" { return } + ua := "" + if len(uas) > 0 { + ua = uas[0] + } urlCacheMu.Lock() defer urlCacheMu.Unlock() if len(urlCache) >= maxCachedURLs { @@ -60,15 +78,20 @@ func SetDownloadURLCache(pickCode, url string) { } } } - urlCache[pickCode] = urlCacheEntry{url: url, expiresAt: time.Now().Add(downloadURLCacheTTL)} + key := urlCacheKey(pickCode, ua) + urlCache[key] = urlCacheEntry{url: url, expiresAt: time.Now().Add(downloadURLCacheTTL)} } -// ClearDownloadURLCache 删除指定 pickcode 的缓存(下载得到非 2xx 时调用)。 +// ClearDownloadURLCache 删除指定 pickcode 的所有缓存(下载得到非 2xx 时调用)。 func ClearDownloadURLCache(pickCode string) { if pickCode == "" { return } urlCacheMu.Lock() defer urlCacheMu.Unlock() - delete(urlCache, pickCode) + for k := range urlCache { + if k == pickCode || strings.HasPrefix(k, pickCode+"@") { + delete(urlCache, k) + } + } } diff --git a/internal/service/strm_play.go b/internal/service/strm_play.go index c6ce701..fdd7239 100644 --- a/internal/service/strm_play.go +++ b/internal/service/strm_play.go @@ -66,7 +66,15 @@ func (s *StrmService) resolveCloudPlay(ctx context.Context, provider string, q u if err != nil { return nil, err } - link, err := p.Resolve(ctx, ref) + var link *cloud.DirectLink + ua := q.Get("__ua") + if uaProvider, ok := p.(interface { + ResolveWithUA(ctx context.Context, fileRef, ua string) (*cloud.DirectLink, error) + }); ok && ua != "" { + link, err = uaProvider.ResolveWithUA(ctx, ref, ua) + } else { + link, err = p.Resolve(ctx, ref) + } if err != nil { return nil, err }