mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 13:56:37 +08:00
优化
This commit is contained in:
@@ -880,6 +880,9 @@ func (r *EmbyRemoteService) RemoteLatestForDisplay(ctx context.Context, mount *m
|
|||||||
// RemoteLatestSeries 拉取剧集库最近更新的 Series。部分 Emby 服务不支持
|
// RemoteLatestSeries 拉取剧集库最近更新的 Series。部分 Emby 服务不支持
|
||||||
// DateLastContentAdded 或过滤 Series,此时回退到 Latest 并把 Episode 归并到
|
// DateLastContentAdded 或过滤 Series,此时回退到 Latest 并把 Episode 归并到
|
||||||
// 对应 Series。
|
// 对应 Series。
|
||||||
|
//
|
||||||
|
// 使用 Recursive=true 并跳过 anime/ 等中间容器,与 RemoteSeriesCards /
|
||||||
|
// Emby 客户端列剧集方式一致。
|
||||||
func (r *EmbyRemoteService) RemoteLatestSeries(ctx context.Context, mount *model.EmbyMount, acct *model.StrmAccount, remoteViewID string, limit int) ([]map[string]any, error) {
|
func (r *EmbyRemoteService) RemoteLatestSeries(ctx context.Context, mount *model.EmbyMount, acct *model.StrmAccount, remoteViewID string, limit int) ([]map[string]any, error) {
|
||||||
if limit <= 0 || limit > 100 {
|
if limit <= 0 || limit > 100 {
|
||||||
limit = 20
|
limit = 20
|
||||||
@@ -891,17 +894,32 @@ func (r *EmbyRemoteService) RemoteLatestSeries(ctx context.Context, mount *model
|
|||||||
q := url.Values{}
|
q := url.Values{}
|
||||||
q.Set("ParentId", remoteViewID)
|
q.Set("ParentId", remoteViewID)
|
||||||
q.Set("IncludeItemTypes", "Series")
|
q.Set("IncludeItemTypes", "Series")
|
||||||
q.Set("Recursive", "false")
|
q.Set("Recursive", "true")
|
||||||
q.Set("SortBy", "DateLastContentAdded")
|
q.Set("SortBy", "DateLastContentAdded")
|
||||||
q.Set("SortOrder", "Descending")
|
q.Set("SortOrder", "Descending")
|
||||||
q.Set("Limit", strconv.Itoa(limit))
|
// 多取一些以便滤掉中间容器后仍够 limit。
|
||||||
|
q.Set("Limit", strconv.Itoa(limit*4))
|
||||||
q.Set("Fields", "Overview,Genres,ProviderIds,Path,RecursiveItemCount,SeriesPrimaryImage,DateCreated,DateLastMediaAdded,PremiereDate,ProductionYear,CommunityRating,CriticRating")
|
q.Set("Fields", "Overview,Genres,ProviderIds,Path,RecursiveItemCount,SeriesPrimaryImage,DateCreated,DateLastMediaAdded,PremiereDate,ProductionYear,CommunityRating,CriticRating")
|
||||||
var body struct {
|
var body struct {
|
||||||
Items []map[string]any `json:"Items"`
|
Items []map[string]any `json:"Items"`
|
||||||
}
|
}
|
||||||
if err := r.doGet(ctx, acct, cfg, "/Users/"+url.PathEscape(r.remoteUserID(cfg))+"/Items", q, &body); err == nil && len(body.Items) > 0 {
|
if err := r.doGet(ctx, acct, cfg, "/Users/"+url.PathEscape(r.remoteUserID(cfg))+"/Items", q, &body); err == nil && len(body.Items) > 0 {
|
||||||
RewriteEmbyRemoteIDs(body.Items, mount.ID)
|
filtered := make([]map[string]any, 0, limit)
|
||||||
return body.Items, nil
|
for _, it := range body.Items {
|
||||||
|
name := remoteItemString(it, "Name")
|
||||||
|
path := remoteItemString(it, "Path")
|
||||||
|
if remoteSeriesItemLooksLikeContainer(name, path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, it)
|
||||||
|
if len(filtered) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(filtered) > 0 {
|
||||||
|
RewriteEmbyRemoteIDs(filtered, mount.ID)
|
||||||
|
return filtered, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items, err := r.RemoteLatest(ctx, mount, acct, remoteViewID, limit)
|
items, err := r.RemoteLatest(ctx, mount, acct, remoteViewID, limit)
|
||||||
|
|||||||
@@ -516,17 +516,17 @@ const remoteSeriesPageSize = 200
|
|||||||
|
|
||||||
// RemoteSeriesCards 远程剧集库的系列卡片(ChildCount 作为集数)。
|
// RemoteSeriesCards 远程剧集库的系列卡片(ChildCount 作为集数)。
|
||||||
//
|
//
|
||||||
// 远程 Emby 的 Series DTO 不会返回 DateLastMediaAdded 字段(即使请求 Fields
|
// 与 Emby 客户端一致:IncludeItemTypes=Series + Recursive=true,按
|
||||||
// 也缺失),但其服务端排序支持 SortBy=DateLastContentAdded——即客户端"上次
|
// DateLastContentAdded 倒序分页拉全库剧集。多媒体根下的 anime/ 等中间容器
|
||||||
// 添加集日期"排序。因此这里直接按该键倒序分页拉全量,返回的卡片顺序与对方
|
// 若偶发出现在结果里则过滤掉;LastAddedAt 仅在远程提供 DateLastMediaAdded
|
||||||
// Emby 客户端选择"上次添加集日期"完全一致;LastAddedAt 在远程提供字段时
|
// 时填充。
|
||||||
// 才填充,否则保持 nil(前端对无该值的卡片维持服务器顺序,不再回退加入日期)。
|
|
||||||
func (r *EmbyRemoteService) RemoteSeriesCards(ctx context.Context, mount *model.EmbyMount, acct *model.StrmAccount, remoteViewID string) ([]SeriesCard, error) {
|
func (r *EmbyRemoteService) RemoteSeriesCards(ctx context.Context, mount *model.EmbyMount, acct *model.StrmAccount, remoteViewID string) ([]SeriesCard, error) {
|
||||||
cfg, err := r.remoteConfigWithToken(ctx, acct)
|
cfg, err := r.remoteConfigWithToken(ctx, acct)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cacheKey := r.remoteCacheKey("series-cards", acct.ID, mount.ID, remoteViewID)
|
// v3:Recursive=true(对齐 Emby 客户端),与旧直属/下探缓存区分。
|
||||||
|
cacheKey := r.remoteCacheKey("series-cards-v3", acct.ID, mount.ID, remoteViewID)
|
||||||
var cached []SeriesCard
|
var cached []SeriesCard
|
||||||
if r.cache != nil && r.cache.GetJSON(ctx, cacheKey, &cached) {
|
if r.cache != nil && r.cache.GetJSON(ctx, cacheKey, &cached) {
|
||||||
return cached, nil
|
return cached, nil
|
||||||
@@ -534,7 +534,7 @@ func (r *EmbyRemoteService) RemoteSeriesCards(ctx context.Context, mount *model.
|
|||||||
q := url.Values{}
|
q := url.Values{}
|
||||||
q.Set("ParentId", remoteViewID)
|
q.Set("ParentId", remoteViewID)
|
||||||
q.Set("IncludeItemTypes", "Series")
|
q.Set("IncludeItemTypes", "Series")
|
||||||
q.Set("Recursive", "false")
|
q.Set("Recursive", "true")
|
||||||
q.Set("SortBy", "DateLastContentAdded")
|
q.Set("SortBy", "DateLastContentAdded")
|
||||||
q.Set("SortOrder", "Descending")
|
q.Set("SortOrder", "Descending")
|
||||||
q.Set("Limit", strconv.Itoa(remoteSeriesPageSize))
|
q.Set("Limit", strconv.Itoa(remoteSeriesPageSize))
|
||||||
@@ -554,9 +554,13 @@ func (r *EmbyRemoteService) RemoteSeriesCards(ctx context.Context, mount *model.
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
for _, it := range body.Items {
|
for _, it := range body.Items {
|
||||||
|
name := remoteItemString(it, "Name")
|
||||||
|
path := remoteItemString(it, "Path")
|
||||||
|
if remoteSeriesItemLooksLikeContainer(name, path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
RewriteEmbyRemoteIDs(it, mount.ID)
|
RewriteEmbyRemoteIDs(it, mount.ID)
|
||||||
m := r.MapRemoteItemToMedia(ctx, mount, acct, cfg, it)
|
m := r.MapRemoteItemToMedia(ctx, mount, acct, cfg, it)
|
||||||
// 集数优先用递归条目数(ChildCount 只算直属 Season 文件夹数)。
|
|
||||||
count := remoteItemInt(it, "RecursiveItemCount")
|
count := remoteItemInt(it, "RecursiveItemCount")
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
count = remoteItemInt(it, "ChildCount")
|
count = remoteItemInt(it, "ChildCount")
|
||||||
@@ -577,7 +581,7 @@ func (r *EmbyRemoteService) RemoteSeriesCards(ctx context.Context, mount *model.
|
|||||||
LastAddedAt: lastAdded,
|
LastAddedAt: lastAdded,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if int64(len(cards)) >= body.TotalRecordCount || len(body.Items) < remoteSeriesPageSize {
|
if int64(startIndex+len(body.Items)) >= body.TotalRecordCount || len(body.Items) < remoteSeriesPageSize {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -587,6 +591,14 @@ func (r *EmbyRemoteService) RemoteSeriesCards(ctx context.Context, mount *model.
|
|||||||
return cards, nil
|
return cards, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func remoteSeriesItemLooksLikeContainer(name, path string) bool {
|
||||||
|
if isEmbyGenericContainer(name) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
base := pathBaseSlash(strings.TrimRight(strings.ReplaceAll(path, "\\", "/"), "/"))
|
||||||
|
return base != "" && isEmbyGenericContainer(base)
|
||||||
|
}
|
||||||
|
|
||||||
// RemoteLatestCards 远程库最新条目(首页预览卡片),映射 SeriesCard。
|
// RemoteLatestCards 远程库最新条目(首页预览卡片),映射 SeriesCard。
|
||||||
func (r *EmbyRemoteService) RemoteLatestCards(ctx context.Context, mount *model.EmbyMount, acct *model.StrmAccount, remoteViewID string, limit int) ([]SeriesCard, error) {
|
func (r *EmbyRemoteService) RemoteLatestCards(ctx context.Context, mount *model.EmbyMount, acct *model.StrmAccount, remoteViewID string, limit int) ([]SeriesCard, error) {
|
||||||
cfg, err := r.remoteConfigWithToken(ctx, acct)
|
cfg, err := r.remoteConfigWithToken(ctx, acct)
|
||||||
|
|||||||
@@ -121,10 +121,15 @@ func TestRemoteSeriesCardsAutoAuthOnFirstBrowse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if r.URL.Path == "/emby/Users/real-user-guid/Items" {
|
if r.URL.Path == "/emby/Users/real-user-guid/Items" {
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
if q.Get("ParentId") != "view-1" || q.Get("IncludeItemTypes") != "Series" {
|
if q.Get("IncludeItemTypes") != "Series" || q.Get("ParentId") != "view-1" {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if q.Get("Recursive") != "true" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, _ = w.Write([]byte("expected Recursive=true"))
|
||||||
|
return
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"TotalRecordCount": 1,
|
"TotalRecordCount": 1,
|
||||||
@@ -231,6 +236,11 @@ func TestRemoteSeriesCardsResolveUserIDFromAPIKey(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.URL.Path == "/emby/Users/real-user-guid/Items" {
|
if r.URL.Path == "/emby/Users/real-user-guid/Items" {
|
||||||
|
q := r.URL.Query()
|
||||||
|
if q.Get("ParentId") != "view-2" || q.Get("Recursive") != "true" {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"TotalRecordCount": 1,
|
"TotalRecordCount": 1,
|
||||||
@@ -674,6 +684,11 @@ func TestRemoteSeriesCardsFetchAllPages(t *testing.T) {
|
|||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
|
if query.Get("Recursive") != "true" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, _ = w.Write([]byte("expected Recursive=true"))
|
||||||
|
return
|
||||||
|
}
|
||||||
startIndex, _ := strconv.Atoi(query.Get("StartIndex"))
|
startIndex, _ := strconv.Atoi(query.Get("StartIndex"))
|
||||||
limit, _ := strconv.Atoi(query.Get("Limit"))
|
limit, _ := strconv.Atoi(query.Get("Limit"))
|
||||||
requests.Add(1)
|
requests.Add(1)
|
||||||
@@ -740,6 +755,84 @@ func TestRemoteSeriesCardsFetchAllPages(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 与 Emby 客户端一致:Recursive=true 拉全库 Series,并过滤 anime 等中间容器。
|
||||||
|
func TestRemoteSeriesCardsRecursiveFiltersAnimeContainers(t *testing.T) {
|
||||||
|
var sawRecursive atomic.Bool
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if strings.TrimPrefix(r.URL.Path, "/emby") != "/Users/remote-user/Items" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := r.URL.Query()
|
||||||
|
if q.Get("ParentId") != "view-2023" || q.Get("IncludeItemTypes") != "Series" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if q.Get("Recursive") == "true" {
|
||||||
|
sawRecursive.Store(true)
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"TotalRecordCount": 5,
|
||||||
|
"Items": []map[string]any{
|
||||||
|
{"Id": "c0", "Name": "anime", "Type": "Series", "Path": "https://cdn/0/anime/", "RecursiveItemCount": 3086},
|
||||||
|
{"Id": "c1", "Name": "anime", "Type": "Series", "Path": "https://cdn/1/anime/", "RecursiveItemCount": 567},
|
||||||
|
{"Id": "s-a", "Name": "数码宝贝 BEATBREAK", "Type": "Series", "Path": "https://cdn/0/anime/digimon", "RecursiveItemCount": 24},
|
||||||
|
{"Id": "s-b", "Name": "活死喵之夜", "Type": "Series", "Path": "https://cdn/0/anime/nyaight", "RecursiveItemCount": 12},
|
||||||
|
{"Id": "s-c", "Name": "药屋少女的呢喃", "Type": "Series", "Path": "https://cdn/1/anime/kusuriya", "RecursiveItemCount": 51},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
db := newServiceTestDB(t, &model.StrmAccount{}, &model.EmbyMount{})
|
||||||
|
repos := repository.New(db)
|
||||||
|
svc := NewEmbyRemoteService(&config.Config{}, zap.NewNop(), repos, NewCryptoService("", zap.NewNop()))
|
||||||
|
|
||||||
|
rawConfig, _ := json.Marshal(map[string]string{
|
||||||
|
"url": server.URL,
|
||||||
|
"token": "fake-token",
|
||||||
|
"remote_user_id": "remote-user",
|
||||||
|
})
|
||||||
|
acct := &model.StrmAccount{
|
||||||
|
Base: model.Base{ID: "acct-recursive"},
|
||||||
|
Name: "nijigem",
|
||||||
|
Provider: model.StrmProviderEmbyRemote,
|
||||||
|
Config: string(rawConfig),
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
if err := repos.StrmAccount.Create(t.Context(), acct); err != nil {
|
||||||
|
t.Fatalf("create account: %v", err)
|
||||||
|
}
|
||||||
|
mount := &model.EmbyMount{
|
||||||
|
Base: model.Base{ID: "mount-recursive"},
|
||||||
|
AccountID: acct.ID,
|
||||||
|
RemoteViewID: "view-2023",
|
||||||
|
RemoteViewName: "2023前 动漫",
|
||||||
|
CollectionType: "tvshows",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
if err := repos.EmbyMount.Create(t.Context(), mount); err != nil {
|
||||||
|
t.Fatalf("create mount: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cards, err := svc.RemoteSeriesCards(t.Context(), mount, acct, mount.RemoteViewID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("RemoteSeriesCards failed: %v", err)
|
||||||
|
}
|
||||||
|
if !sawRecursive.Load() {
|
||||||
|
t.Fatal("expected Recursive=true on remote Items request")
|
||||||
|
}
|
||||||
|
if len(cards) != 3 {
|
||||||
|
t.Fatalf("cards = %d, want 3 real series after filtering anime containers", len(cards))
|
||||||
|
}
|
||||||
|
for _, card := range cards {
|
||||||
|
if strings.EqualFold(card.Rep.Title, "anime") {
|
||||||
|
t.Fatalf("container title %q should have been filtered", card.Rep.Title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 首页「最新条目」卡片 key 必须与媒体库剧集列表的 key 一致,否则点击后
|
// 首页「最新条目」卡片 key 必须与媒体库剧集列表的 key 一致,否则点击后
|
||||||
// 媒体库页找不到目标剧集,只能退回整库列表。
|
// 媒体库页找不到目标剧集,只能退回整库列表。
|
||||||
func TestRemoteLatestCardsKeyMatchesSeriesList(t *testing.T) {
|
func TestRemoteLatestCardsKeyMatchesSeriesList(t *testing.T) {
|
||||||
@@ -751,6 +844,14 @@ func TestRemoteLatestCardsKeyMatchesSeriesList(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
seriesQueryCalls.Add(1)
|
seriesQueryCalls.Add(1)
|
||||||
|
parentID := r.URL.Query().Get("ParentId")
|
||||||
|
if parentID == "series-latest" {
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"Items": []map[string]any{},
|
||||||
|
"TotalRecordCount": 0,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"Items": []map[string]any{
|
"Items": []map[string]any{
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user