mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-08 22:36:37 +08:00
feat: Emby 兼容层完整实现 + 多模块功能增强
## Emby/Jellyfin 兼容层 (emby_compat.go / emby.go) - 新增 SystemInfoPublic、FindUser、Items、Item、LatestItems、ResumeItems - 新增 SetFavorite、MarkPlayed、RecordProgress 用户播放状态同步 - 新增 itemPayload、mediaSource、mediaStreams 媒体信息组装 - 双前缀路由 /emby/* 和 / 根路径,兼容 Infuse/Yamby/Senplayer/Kodi - 新增 Ping、SystemEndpoint、AuthByName 端点 - emby.go 扩展对应 handler 函数 ## 站点适配器 (site_adapter.go / site.go) - SiteConfig 扩展 UserAgent/Timeout/Extra/FlareSolverrURL 字段 - doRequest() GET 请求支持 FlareSolverr 代理绕过 Cloudflare/WAF - MTeam api_key 认证改为 Authorization: Bearer 格式 - Search() 重构为 sync.WaitGroup 并发执行,提升多站搜索性能 - siteModelToConfig() 改为 SiteService 方法,按 BrowserEmulation 填充 FlareSolverrURL ## 图片代理 (image_proxy.go) - 重构图片代理服务,支持更多来源和缓存策略 ## 下载管理 (downloads.go / download_clients.go / qbittorrent.go) - 下载任务增强:状态管理、进度追踪优化 - qBittorrent 客户端连接稳定性改进 ## 刮削与数据库 (scraper.go / tmdb.go / repository.go) - 刮削器增强 TMDB 集成,补全元数据字段 - repository 扩展查询方法 ## 前端 (web/src/) - HomePage: 首页布局重构,按媒体库分组展示,系列聚合优化 - DiscoverPage: 发现页增强,错误处理改进(API key 缺失/网络错误分离) - PosterWallPage: 海报墙优化,系列聚合展示 - MediaCard: 媒体卡片优化 - PlayerPage: 播放器改进 - 新增 utils/groupSeries.ts: 系列聚合工具函数 - .gitignore: 添加 .tmp_* 临时文件排除规则
This commit is contained in:
@@ -81,8 +81,16 @@ func (s *SiteService) Delete(ctx context.Context, id string) error {
|
||||
|
||||
// TestConnection tries to reach the site's base URL with the configured
|
||||
// credentials and reports success/failure.
|
||||
// Now uses helper.TestSiteConnectivity with browser-like headers
|
||||
// and optional FlareSolverr support.
|
||||
//
|
||||
// 测试逻辑(与参考项目 ShukeBta/MediaStation 对齐):
|
||||
//
|
||||
// 1. 优先调用对应站点适配器的 Authenticate(),让 PT 站点(M-Team / UNIT3D /
|
||||
// Gazelle 等)使用各自的开放 API 验证,而不是去拉首页 HTML——后者通常
|
||||
// 被 Cloudflare 直接 403 但 API 能正常访问。
|
||||
// 2. 适配器不可用或站点类型未知时,回退到 helper.TestSiteConnectivity 的
|
||||
// 通用浏览器头 GET 方案。
|
||||
// 3. helper.TestSiteConnectivity 在全局 FlareSolverr 启用且站点开启了
|
||||
// BrowserEmulation 时,会自动走 FlareSolverr。
|
||||
func (s *SiteService) TestConnection(ctx context.Context, id string) (bool, string, error) {
|
||||
site, err := s.FindByID(ctx, id)
|
||||
if err != nil || site == nil {
|
||||
@@ -94,25 +102,58 @@ func (s *SiteService) TestConnection(ctx context.Context, id string) (bool, stri
|
||||
if timeout <= 0 {
|
||||
timeout = 15
|
||||
}
|
||||
|
||||
// Use configured FlareSolverr URL (may be empty if not configured)
|
||||
flareSolverrURL := s.flareSolverrURL
|
||||
|
||||
// ── Path 1: site-aware adapter Authenticate ────────────────────────
|
||||
// custom_rss 没有真适配器,跳过;其它类型先尝试针对性认证端点。
|
||||
if adapter := NewSiteAdapter(site); adapter != nil && site.Type != "" && site.Type != "custom_rss" {
|
||||
cfg := s.siteModelToConfig(site)
|
||||
actx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
if authErr := adapter.Authenticate(actx, cfg); authErr == nil {
|
||||
now := time.Now()
|
||||
_ = s.repo.DB.WithContext(ctx).Model(&model.Site{}).Where("id = ?", id).
|
||||
Updates(map[string]any{
|
||||
"login_status": "ok",
|
||||
"last_error": "",
|
||||
"last_check_at": &now,
|
||||
}).Error
|
||||
return true, "连接成功", nil
|
||||
} else {
|
||||
s.log.Warn("site adapter authenticate failed, falling back to generic test",
|
||||
zap.String("site", site.Name),
|
||||
zap.String("type", site.Type),
|
||||
zap.Error(authErr))
|
||||
// 回退到通用 GET 测试 — 给 Cookie/RSS 类站点一个机会
|
||||
}
|
||||
}
|
||||
|
||||
// ── Path 2: generic GET with browser headers / FlareSolverr ───────
|
||||
ok, msg, err := helper.TestSiteConnectivity(site, flareSolverrURL, timeout, s.log)
|
||||
if err != nil {
|
||||
now := time.Now()
|
||||
_ = s.repo.DB.WithContext(ctx).Model(&model.Site{}).Where("id = ?", id).
|
||||
Updates(map[string]any{"last_error": err.Error(), "last_check_at": &now}).Error
|
||||
Updates(map[string]any{
|
||||
"login_status": "fail",
|
||||
"last_error": err.Error(),
|
||||
"last_check_at": &now,
|
||||
}).Error
|
||||
return false, err.Error(), nil
|
||||
}
|
||||
|
||||
loginStatus := "ok"
|
||||
storedError := ""
|
||||
if !ok {
|
||||
loginStatus = "fail"
|
||||
storedError = msg
|
||||
}
|
||||
now := time.Now()
|
||||
_ = s.repo.DB.WithContext(ctx).Model(&model.Site{}).Where("id = ?", id).
|
||||
Updates(map[string]any{"last_error": loginStatus, "last_check_at": &now}).Error
|
||||
Updates(map[string]any{
|
||||
"login_status": loginStatus,
|
||||
"last_error": storedError,
|
||||
"last_check_at": &now,
|
||||
}).Error
|
||||
return ok, msg, nil
|
||||
}
|
||||
|
||||
@@ -172,8 +213,11 @@ func (s *SiteService) Search(ctx context.Context, keyword string) ([]SearchResul
|
||||
|
||||
result, err := adapter.Search(ctxWithTimeout, cfg, keyword, 1)
|
||||
if err != nil {
|
||||
s.log.Debug("site search failed",
|
||||
zap.String("site", site.Name), zap.Error(err))
|
||||
s.log.Warn("site search failed",
|
||||
zap.String("site", site.Name),
|
||||
zap.String("type", site.Type),
|
||||
zap.String("url", site.URL),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
if result == nil {
|
||||
@@ -249,5 +293,6 @@ func (svc *SiteService) siteModelToConfig(s *model.Site) SiteConfig {
|
||||
Timeout: timeout,
|
||||
Extra: extra,
|
||||
FlareSolverrURL: flareSolverrURL,
|
||||
UseProxy: s.UseProxy,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user