mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-02 04:16:38 +08:00
bug处理
This commit is contained in:
@@ -62,6 +62,7 @@ func registerAdminStrmRoutes(admin *gin.RouterGroup, svc *service.Container) {
|
||||
admin.DELETE("/strm/accounts/:id", deleteStrmAccountHandler(svc))
|
||||
admin.POST("/strm/accounts/:id/test", testStrmAccountHandler(svc))
|
||||
admin.GET("/strm/accounts/:id/list", listStrmRemoteDirHandler(svc))
|
||||
admin.GET("/strm/accounts/:id/resolve", resolveStrmRemoteDirHandler(svc))
|
||||
admin.GET("/strm/115/sources", listStrm115SourcesHandler(svc))
|
||||
admin.POST("/strm/accounts/:id/oauth/start", startStrm115OAuthHandler(svc))
|
||||
admin.POST("/strm/accounts/:id/oauth/poll", pollStrm115OAuthHandler(svc))
|
||||
|
||||
+62
-36
@@ -3,6 +3,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -172,6 +173,19 @@ func listStrmRemoteDirHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// resolveStrmRemoteDirHandler 按远端目录引用(115 为目录 ID)反查完整展示路径。
|
||||
func resolveStrmRemoteDirHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
dir := strings.TrimSpace(c.Query("dir"))
|
||||
path, err := svc.Strm.ResolveRemoteDirPath(c.Request.Context(), c.Param("id"), dir)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"path": path})
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 全局设置 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func getStrmSettingsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
@@ -203,24 +217,25 @@ func updateStrmSettingsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
// ─── 同步目录 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
type strmSyncPathReq struct {
|
||||
Name string `json:"name"`
|
||||
AccountID string `json:"account_id"`
|
||||
Provider string `json:"provider"`
|
||||
RemotePath string `json:"remote_path"`
|
||||
LocalPath string `json:"local_path"`
|
||||
StrmBaseURL string `json:"strm_base_url"`
|
||||
VideoExt string `json:"video_ext"`
|
||||
MetaExt string `json:"meta_ext"`
|
||||
ExcludeName string `json:"exclude_name"`
|
||||
MinVideoSizeMB int64 `json:"min_video_size_mb"`
|
||||
AddPath int `json:"add_path"`
|
||||
DownloadMeta *bool `json:"download_meta"`
|
||||
UploadMeta *bool `json:"upload_meta"`
|
||||
DeleteDir *bool `json:"delete_dir"`
|
||||
Cron string `json:"cron"`
|
||||
EnableCron *bool `json:"enable_cron"`
|
||||
SyncMode string `json:"sync_mode"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Name string `json:"name"`
|
||||
AccountID string `json:"account_id"`
|
||||
Provider string `json:"provider"`
|
||||
RemotePath string `json:"remote_path"`
|
||||
RemoteDisplayPath string `json:"remote_display_path"`
|
||||
LocalPath string `json:"local_path"`
|
||||
StrmBaseURL string `json:"strm_base_url"`
|
||||
VideoExt string `json:"video_ext"`
|
||||
MetaExt string `json:"meta_ext"`
|
||||
ExcludeName string `json:"exclude_name"`
|
||||
MinVideoSizeMB int64 `json:"min_video_size_mb"`
|
||||
AddPath int `json:"add_path"`
|
||||
DownloadMeta *bool `json:"download_meta"`
|
||||
UploadMeta *bool `json:"upload_meta"`
|
||||
DeleteDir *bool `json:"delete_dir"`
|
||||
Cron string `json:"cron"`
|
||||
EnableCron *bool `json:"enable_cron"`
|
||||
SyncMode string `json:"sync_mode"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type strmSyncPathView struct {
|
||||
@@ -240,6 +255,16 @@ func strmSyncPathViews(svc *service.Container, c *gin.Context, paths []model.Str
|
||||
view.AccountEnabled = acct.Enabled
|
||||
}
|
||||
}
|
||||
// 历史 115 数据若尚未记录展示路径,尝试反查一次并回写数据库自愈
|
||||
if p.Provider == model.StrmProvider115 && strings.TrimSpace(p.RemoteDisplayPath) == "" && strings.TrimSpace(p.RemotePath) != "" && p.AccountID != "" {
|
||||
resolveCtx, cancel := context.WithTimeout(c.Request.Context(), 3*time.Second)
|
||||
if fullPath, err := svc.Strm.ResolveRemoteDirPath(resolveCtx, p.AccountID, p.RemotePath); err == nil && fullPath != "" {
|
||||
view.RemoteDisplayPath = fullPath
|
||||
p.RemoteDisplayPath = fullPath
|
||||
_ = svc.Repo.StrmSyncPath.Update(context.Background(), &p)
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
out = append(out, view)
|
||||
}
|
||||
return out
|
||||
@@ -663,24 +688,25 @@ func strmPlayHandler(svc *service.Container) gin.HandlerFunc {
|
||||
// strmSyncPathFromReq 组装同步目录模型(缺省值交给服务层处理)。
|
||||
func strmSyncPathFromReq(req strmSyncPathReq) *model.StrmSyncPath {
|
||||
return &model.StrmSyncPath{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
AccountID: strings.TrimSpace(req.AccountID),
|
||||
Provider: strings.TrimSpace(req.Provider),
|
||||
RemotePath: strings.TrimSpace(req.RemotePath),
|
||||
LocalPath: strings.TrimSpace(req.LocalPath),
|
||||
StrmBaseURL: strings.TrimSpace(req.StrmBaseURL),
|
||||
VideoExt: req.VideoExt,
|
||||
MetaExt: req.MetaExt,
|
||||
ExcludeName: req.ExcludeName,
|
||||
MinVideoSizeMB: req.MinVideoSizeMB,
|
||||
AddPath: req.AddPath,
|
||||
DownloadMeta: boolValue(req.DownloadMeta, true),
|
||||
UploadMeta: boolValue(req.UploadMeta, false),
|
||||
DeleteDir: boolValue(req.DeleteDir, false),
|
||||
Cron: strings.TrimSpace(req.Cron),
|
||||
EnableCron: boolValue(req.EnableCron, false),
|
||||
SyncMode: strings.TrimSpace(req.SyncMode),
|
||||
Enabled: boolValue(req.Enabled, true),
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
AccountID: strings.TrimSpace(req.AccountID),
|
||||
Provider: strings.TrimSpace(req.Provider),
|
||||
RemotePath: strings.TrimSpace(req.RemotePath),
|
||||
RemoteDisplayPath: strings.TrimSpace(req.RemoteDisplayPath),
|
||||
LocalPath: strings.TrimSpace(req.LocalPath),
|
||||
StrmBaseURL: strings.TrimSpace(req.StrmBaseURL),
|
||||
VideoExt: req.VideoExt,
|
||||
MetaExt: req.MetaExt,
|
||||
ExcludeName: req.ExcludeName,
|
||||
MinVideoSizeMB: req.MinVideoSizeMB,
|
||||
AddPath: req.AddPath,
|
||||
DownloadMeta: boolValue(req.DownloadMeta, true),
|
||||
UploadMeta: boolValue(req.UploadMeta, false),
|
||||
DeleteDir: boolValue(req.DeleteDir, false),
|
||||
Cron: strings.TrimSpace(req.Cron),
|
||||
EnableCron: boolValue(req.EnableCron, false),
|
||||
SyncMode: strings.TrimSpace(req.SyncMode),
|
||||
Enabled: boolValue(req.Enabled, true),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,11 @@ type StrmSyncPath struct {
|
||||
AccountID string `gorm:"size:36;index" json:"account_id"` // StrmAccount.ID;local 为空
|
||||
Provider string `gorm:"size:32" json:"provider"` // StrmProvider*(冗余,便于列表展示)
|
||||
RemotePath string `gorm:"size:1024" json:"remote_path"` // 远端目录:115=目录ID,OpenList/CD2=路径,local=源目录
|
||||
LocalPath string `gorm:"size:1024" json:"local_path"` // STRM/元数据本地输出目录
|
||||
// RemoteDisplayPath 是远端目录的完整展示路径(如 /电影/剧集)。115 的
|
||||
// RemotePath 是目录 ID,用户无法辨认,浏览选择或按 ID 反查时把人类可读
|
||||
// 路径存到这里;路径型网盘(CD2/OpenList)与 local 留空(RemotePath 即路径)。
|
||||
RemoteDisplayPath string `gorm:"size:1024" json:"remote_display_path"`
|
||||
LocalPath string `gorm:"size:1024" json:"local_path"` // STRM/元数据本地输出目录
|
||||
// STRM 链接配置(空值继承全局 strm.* 设置)
|
||||
StrmBaseURL string `gorm:"size:512" json:"strm_base_url"` // 覆盖 strm.base_url
|
||||
VideoExt string `gorm:"size:512" json:"video_ext"` // 逗号分隔,覆盖 strm.video_ext
|
||||
|
||||
@@ -95,28 +95,29 @@ func (r *StrmSyncPathRepository) List(ctx context.Context) ([]model.StrmSyncPath
|
||||
func (r *StrmSyncPathRepository) Update(ctx context.Context, p *model.StrmSyncPath) error {
|
||||
return withSQLiteBusyRetry(ctx, func() error {
|
||||
return r.db.WithContext(ctx).Model(&model.StrmSyncPath{}).Where("id = ?", p.ID).Updates(map[string]any{
|
||||
"name": p.Name,
|
||||
"account_id": p.AccountID,
|
||||
"provider": p.Provider,
|
||||
"remote_path": p.RemotePath,
|
||||
"local_path": p.LocalPath,
|
||||
"strm_base_url": p.StrmBaseURL,
|
||||
"video_ext": p.VideoExt,
|
||||
"meta_ext": p.MetaExt,
|
||||
"exclude_name": p.ExcludeName,
|
||||
"min_video_size_mb": p.MinVideoSizeMB,
|
||||
"add_path": p.AddPath,
|
||||
"download_meta": p.DownloadMeta,
|
||||
"upload_meta": p.UploadMeta,
|
||||
"delete_dir": p.DeleteDir,
|
||||
"cron": p.Cron,
|
||||
"enable_cron": p.EnableCron,
|
||||
"sync_mode": p.SyncMode,
|
||||
"enabled": p.Enabled,
|
||||
"last_sync_at": p.LastSyncAt,
|
||||
"last_sync_status": p.LastSyncStatus,
|
||||
"last_sync_message": p.LastSyncMessage,
|
||||
"updated_at": time.Now(),
|
||||
"name": p.Name,
|
||||
"account_id": p.AccountID,
|
||||
"provider": p.Provider,
|
||||
"remote_path": p.RemotePath,
|
||||
"remote_display_path": p.RemoteDisplayPath,
|
||||
"local_path": p.LocalPath,
|
||||
"strm_base_url": p.StrmBaseURL,
|
||||
"video_ext": p.VideoExt,
|
||||
"meta_ext": p.MetaExt,
|
||||
"exclude_name": p.ExcludeName,
|
||||
"min_video_size_mb": p.MinVideoSizeMB,
|
||||
"add_path": p.AddPath,
|
||||
"download_meta": p.DownloadMeta,
|
||||
"upload_meta": p.UploadMeta,
|
||||
"delete_dir": p.DeleteDir,
|
||||
"cron": p.Cron,
|
||||
"enable_cron": p.EnableCron,
|
||||
"sync_mode": p.SyncMode,
|
||||
"enabled": p.Enabled,
|
||||
"last_sync_at": p.LastSyncAt,
|
||||
"last_sync_status": p.LastSyncStatus,
|
||||
"last_sync_message": p.LastSyncMessage,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -37,10 +37,10 @@ type strmSyncState struct {
|
||||
syncType string
|
||||
|
||||
mu sync.Mutex
|
||||
processed int // 已处理文件计数(用于定期落库进度)
|
||||
lastProgressFlush time.Time // 上次进度落库时间
|
||||
seenVideo map[string]bool // "v:"+去掉扩展名的相对路径 → 远端存在该视频
|
||||
seenMeta map[string]bool // "m:"+相对路径 → 远端存在该元数据
|
||||
processed int // 已处理文件计数(用于定期落库进度)
|
||||
lastProgressFlush time.Time // 上次进度落库时间
|
||||
seenVideo map[string]bool // "v:"+去掉扩展名的相对路径 → 远端存在该视频
|
||||
seenMeta map[string]bool // "m:"+相对路径 → 远端存在该元数据
|
||||
remoteMeta map[string]int64 // 远端元数据大小(上传比对用)
|
||||
remoteMetaRef map[string]string // "m:"+相对路径 → 远端元数据文件引用(115 文件 ID,覆盖上传前删除旧文件用)
|
||||
remoteMetaSha1 map[string]string // "m:"+相对路径 → 远端元数据内容 SHA1(115 列表返回;上传/下载精确比对用,其他网盘为空)
|
||||
@@ -184,6 +184,66 @@ func (s *StrmService) ListRemoteDir(ctx context.Context, accountID, dir string)
|
||||
return provider.List(ctx, dir)
|
||||
}
|
||||
|
||||
// ResolveRemoteDirPath 解析远端目录的完整展示路径。115 的目录以 ID 存储,
|
||||
// 用户无法辨认,这里按 ID 反查 115 返回的祖先链拼出人类可读路径;路径型
|
||||
// 网盘(CD2/OpenList)与本地目录的 remote_path 本身就是路径,原样返回。
|
||||
func (s *StrmService) ResolveRemoteDirPath(ctx context.Context, accountID, dir string) (string, error) {
|
||||
acct, err := s.repo.StrmAccount.FindByID(ctx, accountID)
|
||||
if err != nil || acct == nil {
|
||||
return "", errNotFoundOr(err, "网盘账号不存在")
|
||||
}
|
||||
dir = strings.TrimSpace(dir)
|
||||
if dir == "" {
|
||||
return "", nil
|
||||
}
|
||||
if acct.Provider != model.StrmProvider115 {
|
||||
return dir, nil
|
||||
}
|
||||
if dir == "0" {
|
||||
return "", nil
|
||||
}
|
||||
provider, err := s.providerFor(ctx, acct)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
oc, ok := provider.(interface{ OpenClient() *cloud115.OpenClient })
|
||||
if !ok {
|
||||
return "", fmt.Errorf("115: 客户端初始化失败")
|
||||
}
|
||||
detail, err := oc.OpenClient().GetFsDetailByCid(ctx, dir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("115: 解析目录路径失败:%w", err)
|
||||
}
|
||||
return cloud115FullPath(detail), nil
|
||||
}
|
||||
|
||||
// cloud115FullPath 把 115 目录详情的祖先链拼成以 / 开头的完整路径。
|
||||
func cloud115FullPath(detail *cloud115.RemoteFileDetail) string {
|
||||
if detail == nil {
|
||||
return ""
|
||||
}
|
||||
segments := make([]string, 0, len(detail.Paths)+1)
|
||||
hasSelf := false
|
||||
for _, p := range detail.Paths {
|
||||
if p.FileId == "0" || p.FileId == "" {
|
||||
continue
|
||||
}
|
||||
if p.FileId == detail.FileId {
|
||||
hasSelf = true
|
||||
}
|
||||
if name := strings.TrimSpace(p.Name); name != "" {
|
||||
segments = append(segments, name)
|
||||
}
|
||||
}
|
||||
if !hasSelf && strings.TrimSpace(detail.FileName) != "" {
|
||||
segments = append(segments, strings.TrimSpace(detail.FileName))
|
||||
}
|
||||
if len(segments) == 0 {
|
||||
return ""
|
||||
}
|
||||
return "/" + strings.Join(segments, "/")
|
||||
}
|
||||
|
||||
// runSync 执行同步主体;结束时更新记录与目录状态。
|
||||
func (s *StrmService) runSync(ctx context.Context, p *model.StrmSyncPath, rec *model.StrmSyncRecord, cancel context.CancelFunc) {
|
||||
defer s.clearRunning(p.ID, cancel)
|
||||
@@ -819,9 +879,9 @@ func (st *strmSyncState) walk115Flat(open115 *cloud115.OpenClient) error {
|
||||
}
|
||||
fileCh := make(chan strmFileTask)
|
||||
var (
|
||||
procWg sync.WaitGroup
|
||||
procWg sync.WaitGroup
|
||||
procErrMu sync.Mutex
|
||||
procErr error
|
||||
procErr error
|
||||
)
|
||||
for i := 0; i < strmProcessWorkers; i++ {
|
||||
procWg.Add(1)
|
||||
|
||||
@@ -1161,3 +1161,48 @@ func TestWalk115FlatConcurrentProcessing(t *testing.T) {
|
||||
t.Fatalf("incremental sync should skip all, new = %d skipped = %d", st2.rec.NewStrm, st2.rec.Skipped)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloud115FullPath(t *testing.T) {
|
||||
paths := func(ids, names []string) []struct {
|
||||
FileId string `json:"file_id"`
|
||||
Name string `json:"file_name"`
|
||||
} {
|
||||
out := make([]struct {
|
||||
FileId string `json:"file_id"`
|
||||
Name string `json:"file_name"`
|
||||
}, 0, len(ids))
|
||||
for i := range ids {
|
||||
out = append(out, struct {
|
||||
FileId string `json:"file_id"`
|
||||
Name string `json:"file_name"`
|
||||
}{FileId: ids[i], Name: names[i]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// 祖先链已包含当前目录自身(115 常规返回)
|
||||
d := &cloud115.RemoteFileDetail{
|
||||
FileId: "330",
|
||||
FileName: "剧集",
|
||||
Paths: paths([]string{"0", "100", "330"}, []string{"", "电影", "剧集"}),
|
||||
}
|
||||
if got := cloud115FullPath(d); got != "/电影/剧集" {
|
||||
t.Fatalf("full path = %q, want /电影/剧集", got)
|
||||
}
|
||||
|
||||
// 祖先链不含当前目录自身,用 FileName 兜底
|
||||
d = &cloud115.RemoteFileDetail{
|
||||
FileId: "330",
|
||||
FileName: "剧集",
|
||||
Paths: paths([]string{"0", "100"}, []string{"", "电影"}),
|
||||
}
|
||||
if got := cloud115FullPath(d); got != "/电影/剧集" {
|
||||
t.Fatalf("full path = %q, want /电影/剧集", got)
|
||||
}
|
||||
|
||||
// 根目录:无有效祖先段
|
||||
d = &cloud115.RemoteFileDetail{FileId: "0"}
|
||||
if got := cloud115FullPath(d); got != "" {
|
||||
t.Fatalf("root full path = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user