处理无法播放的问题

处理无法播放的问题
This commit is contained in:
truewhile
2026-08-24 21:06:44 +08:00
parent 97d162569d
commit 43fdfe8202
19 changed files with 168 additions and 107 deletions
+9 -9
View File
@@ -40,15 +40,15 @@ func TestLoadDefaults(t *testing.T) {
if cfg.Database.MaxOpenConns != defaultDatabaseMaxOpenConns { if cfg.Database.MaxOpenConns != defaultDatabaseMaxOpenConns {
t.Fatalf("expected default MaxOpenConns %d, got %d", defaultDatabaseMaxOpenConns, cfg.Database.MaxOpenConns) t.Fatalf("expected default MaxOpenConns %d, got %d", defaultDatabaseMaxOpenConns, cfg.Database.MaxOpenConns)
} }
if cfg.Cache.RedisPrefix != "mmtl" { if cfg.Cache.RedisPrefix != "mmtl" {
t.Fatalf("expected default redis prefix, got %q", cfg.Cache.RedisPrefix) t.Fatalf("expected default redis prefix, got %q", cfg.Cache.RedisPrefix)
} }
if cfg.Cache.MediaTTLSeconds != 15 { if cfg.Cache.MediaTTLSeconds != 15 {
t.Fatalf("expected default media cache ttl 15, got %d", cfg.Cache.MediaTTLSeconds) t.Fatalf("expected default media cache ttl 15, got %d", cfg.Cache.MediaTTLSeconds)
} }
if cfg.Search.Index != "mmtl_media" { if cfg.Search.Index != "mmtl_media" {
t.Fatalf("expected default search index, got %q", cfg.Search.Index) t.Fatalf("expected default search index, got %q", cfg.Search.Index)
} }
if cfg.Database.MaxIdleConns != defaultDatabaseMaxIdleConns { if cfg.Database.MaxIdleConns != defaultDatabaseMaxIdleConns {
t.Fatalf("expected default MaxIdleConns %d, got %d", defaultDatabaseMaxIdleConns, cfg.Database.MaxIdleConns) t.Fatalf("expected default MaxIdleConns %d, got %d", defaultDatabaseMaxIdleConns, cfg.Database.MaxIdleConns)
} }
+1 -1
View File
@@ -177,7 +177,7 @@ func (h *ApiConfigHandler) TestApiConfig(c *gin.Context) {
_ = h.svc.ApiConfig.UpdateTestResult(c.Request.Context(), provider, result) _ = h.svc.ApiConfig.UpdateTestResult(c.Request.Context(), provider, result)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"code": 0, "code": 0,
"message": "ok", "message": "ok",
"data": gin.H{ "data": gin.H{
"result": result, "result": result,
+1 -1
View File
@@ -30,4 +30,4 @@ func getDanmakuConfigHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
c.JSON(http.StatusOK, svc.Danmaku.Config(c.Request.Context())) c.JSON(http.StatusOK, svc.Danmaku.Config(c.Request.Context()))
} }
} }
+1 -1
View File
@@ -144,4 +144,4 @@ func TestEmbyLoginWithAuthorizationHeaderToken(t *testing.T) {
if w.Code != http.StatusOK { if w.Code != http.StatusOK {
t.Fatalf("Authorization-token request = %d body=%s", w.Code, w.Body.String()) t.Fatalf("Authorization-token request = %d body=%s", w.Code, w.Body.String())
} }
} }
+10 -10
View File
@@ -23,16 +23,16 @@ func TestAuthenticatedRouteSurfacesAreRegistered(t *testing.T) {
routes[route.Method+" "+route.Path] = true routes[route.Method+" "+route.Path] = true
} }
for _, want := range []string{ for _, want := range []string{
"GET /api/me", "GET /api/me",
"GET /api/auth/permissions", "GET /api/auth/permissions",
"GET /api/libraries", "GET /api/libraries",
"GET /api/media", "GET /api/media",
"GET /api/stream/:id", "GET /api/stream/:id",
"GET /api/storage", "GET /api/storage",
"GET /api/watch-history", "GET /api/watch-history",
"GET /api/playback/:id/info", "GET /api/playback/:id/info",
} { } {
if !routes[want] { if !routes[want] {
t.Fatalf("%s route is not registered", want) t.Fatalf("%s route is not registered", want)
} }
+3 -3
View File
@@ -26,9 +26,9 @@ func statsUserHandler(svc *service.Container) gin.HandlerFunc {
_ = svc.Repo.DB.Model(&model.PlaybackHistory{}). _ = svc.Repo.DB.Model(&model.PlaybackHistory{}).
Where("user_id = ?", uid).Count(&total).Error Where("user_id = ?", uid).Count(&total).Error
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"user_id": uid, "user_id": uid,
"watched_ms": watched, "watched_ms": watched,
"plays": total, "plays": total,
"watched_hours": float64(watched) / 1000.0 / 3600.0, "watched_hours": float64(watched) / 1000.0 / 3600.0,
}) })
} }
+5 -1
View File
@@ -410,7 +410,11 @@ func cancelPendingDownloadsHandler(svc *service.Container) gin.HandlerFunc {
func strmPlayHandler(svc *service.Container) gin.HandlerFunc { func strmPlayHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
provider := strings.TrimSpace(c.Param("provider")) 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 err != nil {
if errors.Is(err, service.ErrStrmPlayNotFound) { if errors.Is(err, service.ErrStrmPlayNotFound) {
c.Status(http.StatusNotFound) c.Status(http.StatusNotFound)
+10 -10
View File
@@ -58,12 +58,12 @@ func schemaHandler(_ *service.Container) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"groups": []gin.H{ "groups": []gin.H{
{ {
"key": "general", "key": "general",
"label": "常规", "label": "常规",
"items": []gin.H{ "items": []gin.H{
{"key": "tmdb.language", "type": "select", "label": "TMDb 元数据语言"}, {"key": "tmdb.language", "type": "select", "label": "TMDb 元数据语言"},
{"key": "transcode.enabled", "type": "toggle", "label": "启用转码"}, {"key": "transcode.enabled", "type": "toggle", "label": "启用转码"},
{"key": "transcode.hw_accel", "type": "select", "label": "硬件编码器"}, {"key": "transcode.hw_accel", "type": "select", "label": "硬件编码器"},
{"key": "transcode.hw_enabled", "type": "toggle", "label": "启用硬件加速"}, {"key": "transcode.hw_enabled", "type": "toggle", "label": "启用硬件加速"},
{"key": "transcode.max_jobs", "type": "number", "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.password", "type": "text"},
{"key": "qbittorrent.savepath", "type": "text"}, {"key": "qbittorrent.savepath", "type": "text"},
}, },
}, },
{ {
"key": "system-update", "key": "system-update",
"label": "系统更新", "label": "系统更新",
"items": []gin.H{ "items": []gin.H{
{"key": "system.update.image", "type": "text", "label": "应用镜像"}, {"key": "system.update.image", "type": "text", "label": "应用镜像"},
{"key": "system.update.compose_dir", "type": "text", "label": "Docker Compose 安装目录"}, {"key": "system.update.compose_dir", "type": "text", "label": "Docker Compose 安装目录"},
+1 -1
View File
@@ -7,4 +7,4 @@ func finishHTTPTask(task *service.TaskHandle, err error, stage, message string,
return return
} }
task.Finish(err, service.TaskUpdate{Stage: stage, Message: message, Metrics: metrics, Details: details}) task.Finish(err, service.TaskUpdate{Stage: stage, Message: message, Metrics: metrics, Details: details})
} }
+8 -8
View File
@@ -42,14 +42,14 @@ func EmbyAuthRequired(secret string) gin.HandlerFunc {
return []byte(secret), nil return []byte(secret), nil
}) })
if err != nil || !parsed.Valid || claims.UserID == "" { if err != nil || !parsed.Valid || claims.UserID == "" {
c.JSON(http.StatusUnauthorized, gin.H{ c.JSON(http.StatusUnauthorized, gin.H{
"Code": 40101, "Code": 40101,
"Message": "Invalid token", "Message": "Invalid token",
}) })
c.Abort() c.Abort()
return return
} }
c.Set(EmbyCtxUserID, claims.UserID) c.Set(EmbyCtxUserID, claims.UserID)
c.Set(CtxUserID, claims.UserID) c.Set(CtxUserID, claims.UserID)
+6 -6
View File
@@ -106,13 +106,13 @@ func TestAuthRequiredSyncsAccessTokenCookieFromBearer(t *testing.T) {
}, },
}) })
router := gin.New() router := gin.New()
router.Use(AuthRequired(secret)) router.Use(AuthRequired(secret))
router.GET("/api/test-auth-cookie", func(c *gin.Context) { router.GET("/api/test-auth-cookie", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true}) 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) req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
+3 -3
View File
@@ -39,7 +39,7 @@ func PredefinedProviders() []ApiProvider {
{ID: "openai", Name: "OpenAI", Description: "GPT 系列模型", HasAPIKey: true, HasBaseURL: true}, {ID: "openai", Name: "OpenAI", Description: "GPT 系列模型", HasAPIKey: true, HasBaseURL: true},
{ID: "deepseek", Name: "DeepSeek", Description: "DeepSeek 大模型", 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: "siliconflow", Name: "SiliconFlow", Description: "AI 模型聚合 API", HasAPIKey: true, HasBaseURL: true},
{ID: "adult", Name: "Adult / 番号", Description: "JavDB/JavBus 成人内容元数据与 Cookie 凭据", 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: "metatube", Name: "MetaTube", Description: "MetaTube Server 番号元数据后端服务", HasAPIKey: true, HasBaseURL: true},
} }
} }
+37 -37
View File
@@ -16,7 +16,7 @@ type UserPermission struct {
UserID string `gorm:"uniqueIndex;size:36;not null" json:"user_id"` UserID string `gorm:"uniqueIndex;size:36;not null" json:"user_id"`
// 默认开启(6项)- Basic // 默认开启(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"` CanPlayMedia bool `gorm:"default:true" json:"can_play_media"`
CanCast bool `gorm:"default:true" json:"can_cast"` CanCast bool `gorm:"default:true" json:"can_cast"`
CanExternalPlayer bool `gorm:"default:true" json:"can_external_player"` 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"` CanViewHistory bool `gorm:"default:true" json:"can_view_history"`
// 默认关闭(13项)- Advanced // 默认关闭(13项)- Advanced
CanEditMedia bool `gorm:"default:false" json:"can_edit_media"` CanEditMedia bool `gorm:"default:false" json:"can_edit_media"`
CanRescrape bool `gorm:"default:false" json:"can_rescrape"` CanRescrape bool `gorm:"default:false" json:"can_rescrape"`
CanUseAI bool `gorm:"default:false" json:"can_use_ai"` CanUseAI bool `gorm:"default:false" json:"can_use_ai"`
CanCaptureFrames bool `gorm:"default:false" json:"can_capture_frames"` CanCaptureFrames bool `gorm:"default:false" json:"can_capture_frames"`
CanManageDownloads bool `gorm:"default:false" json:"can_manage_downloads"` CanManageDownloads bool `gorm:"default:false" json:"can_manage_downloads"`
CanManageSubscriptions bool `gorm:"default:false" json:"can_manage_subscriptions"` CanManageSubscriptions bool `gorm:"default:false" json:"can_manage_subscriptions"`
CanManageSites bool `gorm:"default:false" json:"can_manage_sites"` CanManageSites bool `gorm:"default:false" json:"can_manage_sites"`
CanUseAIAssistant bool `gorm:"default:false" json:"can_use_ai_assistant"` CanUseAIAssistant bool `gorm:"default:false" json:"can_use_ai_assistant"`
CanManageUsers bool `gorm:"default:false" json:"can_manage_users"` CanManageUsers bool `gorm:"default:false" json:"can_manage_users"`
CanManageFiles bool `gorm:"default:false" json:"can_manage_files"` CanManageFiles bool `gorm:"default:false" json:"can_manage_files"`
CanManageStrm bool `gorm:"default:false" json:"can_manage_strm"` CanManageStrm bool `gorm:"default:false" json:"can_manage_strm"`
CanAccessSettings bool `gorm:"default:false" json:"can_access_settings"` CanAccessSettings bool `gorm:"default:false" json:"can_access_settings"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -52,26 +52,26 @@ func (p *UserPermission) BeforeCreate(_ *gorm.DB) error {
// NewDefaultPermission 创建带有默认权限的 UserPermission。 // NewDefaultPermission 创建带有默认权限的 UserPermission。
func NewDefaultPermission(userID string) *UserPermission { func NewDefaultPermission(userID string) *UserPermission {
return &UserPermission{ return &UserPermission{
ID: uuid.NewString(), ID: uuid.NewString(),
UserID: userID, UserID: userID,
CanViewDashboard: true, CanViewDashboard: true,
CanPlayMedia: true, CanPlayMedia: true,
CanCast: true, CanCast: true,
CanExternalPlayer: true, CanExternalPlayer: true,
CanFavorite: true, CanFavorite: true,
CanViewHistory: true, CanViewHistory: true,
CanEditMedia: false, CanEditMedia: false,
CanRescrape: false, CanRescrape: false,
CanUseAI: false, CanUseAI: false,
CanCaptureFrames: false, CanCaptureFrames: false,
CanManageDownloads: false, CanManageDownloads: false,
CanManageSubscriptions: false, CanManageSubscriptions: false,
CanManageSites: false, CanManageSites: false,
CanUseAIAssistant: false, CanUseAIAssistant: false,
CanManageUsers: false, CanManageUsers: false,
CanManageFiles: false, CanManageFiles: false,
CanManageStrm: false, CanManageStrm: false,
CanAccessSettings: false, CanAccessSettings: false,
} }
} }
@@ -88,13 +88,13 @@ func (p *UserPermission) PermissionMap() map[string]bool {
"can_rescrape": p.CanRescrape, "can_rescrape": p.CanRescrape,
"can_use_ai": p.CanUseAI, "can_use_ai": p.CanUseAI,
"can_capture_frames": p.CanCaptureFrames, "can_capture_frames": p.CanCaptureFrames,
"can_manage_downloads": p.CanManageDownloads, "can_manage_downloads": p.CanManageDownloads,
"can_manage_subscriptions": p.CanManageSubscriptions, "can_manage_subscriptions": p.CanManageSubscriptions,
"can_manage_sites": p.CanManageSites, "can_manage_sites": p.CanManageSites,
"can_use_ai_assistant": p.CanUseAIAssistant, "can_use_ai_assistant": p.CanUseAIAssistant,
"can_manage_users": p.CanManageUsers, "can_manage_users": p.CanManageUsers,
"can_manage_files": p.CanManageFiles, "can_manage_files": p.CanManageFiles,
"can_manage_strm": p.CanManageStrm, "can_manage_strm": p.CanManageStrm,
"can_access_settings": p.CanAccessSettings, "can_access_settings": p.CanAccessSettings,
} }
} }
@@ -66,7 +66,6 @@ func (r *StorageConfigRepository) Upsert(ctx context.Context, c *model.StorageCo
}).Error }).Error
} }
// Delete removes a storage config by ID. // Delete removes a storage config by ID.
func (r *StorageConfigRepository) Delete(ctx context.Context, id string) error { func (r *StorageConfigRepository) Delete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.StorageConfig{}).Error return r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.StorageConfig{}).Error
+5 -1
View File
@@ -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) { 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 { if err != nil {
return nil, err return nil, err
} }
+20 -3
View File
@@ -90,11 +90,15 @@ type RespBase struct {
} }
// doJSON 执行 HTTP 请求并解析为统一响应;带 AccessToken(access=true 时)。 // 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 executor := c.executor
if executor == nil { if executor == nil {
executor = GetGlobalExecutor() executor = GetGlobalExecutor()
} }
ua := ""
if len(uas) > 0 {
ua = uas[0]
}
var lastErr error var lastErr error
for attempt := 0; attempt <= retries; attempt++ { 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 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 { if err != nil {
return nil, err 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) { 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) method = strings.ToUpper(method)
var body io.Reader var body io.Reader
if method == http.MethodPost && len(form) > 0 { 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 { if err != nil {
return nil, err 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 { if method == http.MethodPost && len(form) > 0 {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 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) 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 判断是否为限流错误码。 // IsThrottleCode 判断是否为限流错误码。
func IsThrottleCode(code int) bool { func IsThrottleCode(code int) bool {
return code == RequestMaxLimitCode || code == RequestRateLimitCode return code == RequestMaxLimitCode || code == RequestRateLimitCode
+9 -3
View File
@@ -127,11 +127,17 @@ type downloadURLData struct {
// GetDownloadURL 获取下载直链(pickcode)。命中缓存直接返回, // GetDownloadURL 获取下载直链(pickcode)。命中缓存直接返回,
// 避免对同一文件反复换取直链触发 115 风控。 // 避免对同一文件反复换取直链触发 115 风控。
func (c *OpenClient) GetDownloadURL(ctx context.Context, pickCode string) (string, error) { 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 return cached, nil
} }
params := map[string]string{"pick_code": pickCode} 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 { if err != nil {
return "", err return "", err
} }
@@ -143,7 +149,7 @@ func (c *OpenClient) GetDownloadURL(ctx context.Context, pickCode string) (strin
if first.URL.URL == "" { if first.URL.URL == "" {
return "", fmt.Errorf("115: 下载地址为空(文件可能未上传完成或已被删除)") return "", fmt.Errorf("115: 下载地址为空(文件可能未上传完成或已被删除)")
} }
SetDownloadURLCache(pickCode, first.URL.URL) SetDownloadURLCache(pickCode, first.URL.URL, ua)
return first.URL.URL, nil return first.URL.URL, nil
} }
+30 -7
View File
@@ -7,6 +7,7 @@
package cloud115 package cloud115
import ( import (
"strings"
"sync" "sync"
"time" "time"
) )
@@ -28,17 +29,30 @@ var (
urlCache = map[string]urlCacheEntry{} urlCache = map[string]urlCacheEntry{}
) )
// urlCacheKey 构造缓存键名(pickCode + UA,实现直链防盗链按客户端 UA 独立缓存)。
func urlCacheKey(pickCode, ua string) string {
if ua == "" {
return pickCode
}
return pickCode + "@" + ua
}
// GetDownloadURLCache 返回未过期的缓存直链;不存在或已过期返回空串。 // GetDownloadURLCache 返回未过期的缓存直链;不存在或已过期返回空串。
func GetDownloadURLCache(pickCode string) string { func GetDownloadURLCache(pickCode string, uas ...string) string {
if pickCode == "" { if pickCode == "" {
return "" return ""
} }
ua := ""
if len(uas) > 0 {
ua = uas[0]
}
key := urlCacheKey(pickCode, ua)
urlCacheMu.Lock() urlCacheMu.Lock()
defer urlCacheMu.Unlock() defer urlCacheMu.Unlock()
entry, ok := urlCache[pickCode] entry, ok := urlCache[key]
if !ok || time.Now().After(entry.expiresAt) { if !ok || time.Now().After(entry.expiresAt) {
if ok { if ok {
delete(urlCache, pickCode) delete(urlCache, key)
} }
return "" return ""
} }
@@ -46,10 +60,14 @@ func GetDownloadURLCache(pickCode string) string {
} }
// SetDownloadURLCache 写入直链缓存。 // SetDownloadURLCache 写入直链缓存。
func SetDownloadURLCache(pickCode, url string) { func SetDownloadURLCache(pickCode string, url string, uas ...string) {
if pickCode == "" || url == "" { if pickCode == "" || url == "" {
return return
} }
ua := ""
if len(uas) > 0 {
ua = uas[0]
}
urlCacheMu.Lock() urlCacheMu.Lock()
defer urlCacheMu.Unlock() defer urlCacheMu.Unlock()
if len(urlCache) >= maxCachedURLs { 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) { func ClearDownloadURLCache(pickCode string) {
if pickCode == "" { if pickCode == "" {
return return
} }
urlCacheMu.Lock() urlCacheMu.Lock()
defer urlCacheMu.Unlock() defer urlCacheMu.Unlock()
delete(urlCache, pickCode) for k := range urlCache {
if k == pickCode || strings.HasPrefix(k, pickCode+"@") {
delete(urlCache, k)
}
}
} }
+9 -1
View File
@@ -66,7 +66,15 @@ func (s *StrmService) resolveCloudPlay(ctx context.Context, provider string, q u
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }