mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-01 03:56:38 +08:00
fix: improve discover and series special handling
This commit is contained in:
@@ -40,10 +40,7 @@ var discoverSectionCatalog = []discoverSectionDef{
|
||||
func discoverSectionsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
sections := make([]gin.H, 0, len(discoverSectionCatalog))
|
||||
for _, section := range discoverSectionCatalog {
|
||||
if !discoverProviderEnabled(c.Request.Context(), svc, section.Provider) {
|
||||
continue
|
||||
}
|
||||
for _, section := range enabledDiscoverSections(c.Request.Context(), svc) {
|
||||
sections = append(sections, gin.H{"key": section.Key, "label": section.Label, "provider": section.Provider})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"sections": sections})
|
||||
@@ -56,11 +53,18 @@ func discoverSectionsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
// the page.
|
||||
func discoverFeedHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
keys := strings.Split(c.DefaultQuery("sections", "tmdb_trending_day,tmdb_popular_movie,douban_hot_movie,bangumi_calendar"), ",")
|
||||
rawSections := c.Query("sections")
|
||||
if strings.TrimSpace(rawSections) == "" {
|
||||
rawSections = strings.Join(defaultDiscoverSectionKeys(c.Request.Context(), svc), ",")
|
||||
}
|
||||
keys := strings.Split(rawSections, ",")
|
||||
out := gin.H{}
|
||||
artworkItems := []service.ExternalMediaResult{}
|
||||
for _, raw := range keys {
|
||||
k := strings.TrimSpace(raw)
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
if provider := discoverSectionProvider(k); provider != "" && !discoverProviderEnabled(c.Request.Context(), svc, provider) {
|
||||
out[k] = []service.ExternalMediaResult{}
|
||||
continue
|
||||
@@ -78,6 +82,41 @@ func discoverFeedHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func enabledDiscoverSections(ctx context.Context, svc *service.Container) []discoverSectionDef {
|
||||
sections := make([]discoverSectionDef, 0, len(discoverSectionCatalog))
|
||||
for _, section := range discoverSectionCatalog {
|
||||
if !discoverProviderEnabled(ctx, svc, section.Provider) {
|
||||
continue
|
||||
}
|
||||
sections = append(sections, section)
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
func defaultDiscoverSectionKeys(ctx context.Context, svc *service.Container) []string {
|
||||
preferred := []string{"tmdb_trending_day", "douban_hot_movie", "douban_hot_tv", "bangumi_calendar"}
|
||||
enabled := map[string]struct{}{}
|
||||
for _, section := range enabledDiscoverSections(ctx, svc) {
|
||||
enabled[section.Key] = struct{}{}
|
||||
}
|
||||
out := make([]string, 0, len(preferred))
|
||||
for _, key := range preferred {
|
||||
if _, ok := enabled[key]; ok {
|
||||
out = append(out, key)
|
||||
}
|
||||
}
|
||||
if len(out) > 0 {
|
||||
return out
|
||||
}
|
||||
for _, section := range enabledDiscoverSections(ctx, svc) {
|
||||
out = append(out, section.Key)
|
||||
if len(out) >= 4 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func discoverSectionProvider(key string) string {
|
||||
for _, section := range discoverSectionCatalog {
|
||||
if section.Key == key {
|
||||
|
||||
@@ -35,3 +35,33 @@ func TestDiscoverProviderEnabledHonorsAPIConfigToggle(t *testing.T) {
|
||||
t.Fatal("missing API config should keep discover provider available")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDiscoverSectionKeysSkipDisabledProviders(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.APIConfig{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repos := repository.New(db)
|
||||
apiConfig := service.NewAPIConfigService(zap.NewNop(), repos, service.NewCryptoService("", zap.NewNop()))
|
||||
disabled := false
|
||||
for _, provider := range []string{"douban", "bangumi"} {
|
||||
if _, err := apiConfig.Update(t.Context(), provider, service.APIConfigPatch{Enabled: &disabled}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
svc := &service.Container{APIConfig: apiConfig}
|
||||
|
||||
keys := defaultDiscoverSectionKeys(t.Context(), svc)
|
||||
for _, key := range keys {
|
||||
switch discoverSectionProvider(key) {
|
||||
case "douban", "bangumi":
|
||||
t.Fatalf("disabled provider key %q should not be selected by default; keys=%v", key, keys)
|
||||
}
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
t.Fatal("default keys should keep enabled providers")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ var (
|
||||
patSeasonFolder = regexp.MustCompile(`(?i)(?:^|[^a-z])(?:s|season)\.?\s*(\d{1,2})(?:[^0-9]|$)|第\s*([0-9一二三四五六七八九十百零两]+)\s*季`)
|
||||
patSeasonOnly = regexp.MustCompile(`(?i)(?:^|[\s._-])(?:s|season)\.?\s*\d{1,2}(?:[\s._-]|$)`)
|
||||
patBareEpisode = regexp.MustCompile(`^(?:第\s*)?0?(\d{1,3})(?:\s*(?:v\d+)?)?$`)
|
||||
patSpecialSeason = regexp.MustCompile(`(?i)^(?:s0+|season[\s._-]*0+|specials?|sp|ova|oad|extra|extras|番外|特别篇|特別篇|特典)$`)
|
||||
patSpecialSeason = regexp.MustCompile(`(?i)^(?:s0+|season[\s._-]*0+|special[\s._-]*episodes?|specials?|sp|ovas?|oads?|extras?|bonus(?:es)?|omake|番外篇?|特别篇|特別篇|特典|外传|外傳|总集篇|總集篇)$`)
|
||||
// patCNSeason 匹配中文季/部标记,支持阿拉伯数字与中文数字(如「第二季」「第2部」)。
|
||||
patCNSeason = regexp.MustCompile(`第\s*[0-9一二三四五六七八九十百零两]+\s*[季部]`)
|
||||
)
|
||||
|
||||
@@ -237,7 +237,7 @@ func TestManualSearchReturnsMovieFallbackForTVTypedTMDbSearch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualSearchTMDbNumericIDTriesMovieAndTVNamespaces(t *testing.T) {
|
||||
func TestManualSearchAllProvidersTMDbNumericIDTriesMovieAndTVNamespaces(t *testing.T) {
|
||||
var paths []string
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
paths = append(paths, r.URL.Path)
|
||||
@@ -284,7 +284,7 @@ func TestManualSearchTMDbNumericIDTriesMovieAndTVNamespaces(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
results, err := scraper.ManualSearch(t.Context(), &media, "12345", "tmdb", "movie")
|
||||
results, err := scraper.ManualSearch(t.Context(), &media, "12345", "all", "movie")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ func groupMediaSeriesCards(items []model.Media) []SeriesCard {
|
||||
return cards
|
||||
}
|
||||
|
||||
var episodicPathRE = regexp.MustCompile(`(?i)[\\/](?:电视剧|剧集|国产剧|欧美剧|日韩剧|日剧|韩剧|综艺|纪录片|动漫|番剧|国漫|日番|儿童|tv|series|shows?|season[\s._-]*\d|s\d{1,2}(?:[\s._-]|[\\/])|specials?|sp|ova|oad|extra|extras|特别篇|特別篇|番外|特典)[\\/]`)
|
||||
var episodicPathRE = regexp.MustCompile(`(?i)[\\/](?:电视剧|剧集|国产剧|欧美剧|日韩剧|日剧|韩剧|综艺|纪录片|动漫|番剧|国漫|日番|儿童|tv|series|shows?|season[\s._-]*\d|s\d{1,2}(?:[\s._-]|[\\/])|special[\s._-]*episodes?|specials?|sp|ovas?|oads?|extras?|bonus(?:es)?|omake|特别篇|特別篇|番外篇?|特典|外传|外傳|总集篇|總集篇)[\\/]`)
|
||||
|
||||
func mediaSeriesKey(media model.Media) string {
|
||||
return compactSeriesKey(mediaSeriesRawKey(media))
|
||||
@@ -173,7 +173,7 @@ var (
|
||||
seriesIDRE = regexp.MustCompile(`(?i)\s*\[(?:tmdb|tmdbid)[=-]\d+\]\s*`)
|
||||
seriesBraceRE = regexp.MustCompile(`(?i)\s*\{(?:tmdb|tmdbid|douban|bangumi|bgm|thetvdb|tvdb)[\s:=#-]*[a-z0-9_-]+\}\s*`)
|
||||
seriesSpacerRE = regexp.MustCompile(`[\s._-]+`)
|
||||
seriesSpecialSuffixRE = regexp.MustCompile(`(?i)(?:\s+(?:specials?|sp|ova|oad|extra|extras)|\s*(?:特别篇|特別篇|番外|特典|外传|外傳))$`)
|
||||
seriesSpecialSuffixRE = regexp.MustCompile(`(?i)(?:\s+(?:s0+|season 0+|special episodes?|specials?|sp|ovas?|oads?|extras?|bonus(?:es)?|omake)|\s*(?:特别篇|特別篇|番外篇?|特典|外传|外傳|总集篇|總集篇))$`)
|
||||
)
|
||||
|
||||
func normalizeSeriesTitle(value string) string {
|
||||
|
||||
@@ -61,3 +61,41 @@ func TestMediaSeriesKeyCollapsesSpecialTitleSuffix(t *testing.T) {
|
||||
t.Fatalf("chinese special key=%q, want main key=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaSeriesKeyCollapsesSeasonZeroAndSpecialAliases(t *testing.T) {
|
||||
main := model.Media{
|
||||
LibraryID: "lib-anime",
|
||||
Path: `cloud://openlist/动漫/日番/宝可梦 (1997) {tmdb-60572}/Season 1/宝可梦.S01E01.mkv`,
|
||||
SeasonNum: 1,
|
||||
EpisodeNum: 1,
|
||||
}
|
||||
seasonZero := model.Media{
|
||||
LibraryID: "lib-anime",
|
||||
Path: `cloud://openlist/动漫/日番/宝可梦 (1997) {tmdb-60572}/Season 0/宝可梦.S00E34.mkv`,
|
||||
SeasonNum: 0,
|
||||
EpisodeNum: 34,
|
||||
}
|
||||
specialEpisode := model.Media{
|
||||
LibraryID: "lib-anime",
|
||||
Path: `cloud://openlist/动漫/日番/宝可梦 Special Episode/宝可梦.SP01.mkv`,
|
||||
SeasonNum: 0,
|
||||
EpisodeNum: 1,
|
||||
}
|
||||
extraEpisode := model.Media{
|
||||
LibraryID: "lib-anime",
|
||||
Path: `cloud://openlist/动漫/日番/宝可梦 番外篇/宝可梦.SP02.mkv`,
|
||||
SeasonNum: 0,
|
||||
EpisodeNum: 2,
|
||||
}
|
||||
|
||||
want := mediaSeriesKey(main)
|
||||
for name, item := range map[string]model.Media{
|
||||
"season zero": seasonZero,
|
||||
"special episode": specialEpisode,
|
||||
"番外篇": extraEpisode,
|
||||
} {
|
||||
if got := mediaSeriesKey(item); got != want {
|
||||
t.Fatalf("%s key=%q, want main key=%q", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,10 +78,10 @@ export function isEpisodeLike(media: Media): boolean {
|
||||
// 剧集类目录名(电视剧/动漫及其二级分类)。媒体路径落在这些目录下时, 即便
|
||||
// 季集号未识别出来, 也应按剧集对待, 跳转到 /library 分类视图而非 /media 单页。
|
||||
const EPISODIC_PATH_RE =
|
||||
/[\\/](?:电视剧|剧集|国产剧|欧美剧|日韩剧|日剧|韩剧|综艺|纪录片|动漫|番剧|国漫|日番|儿童|tv|series|shows?|season[\s._-]*\d|s\d{1,2}(?:[\s._-]|[\\/])|specials?|sp|ova|oad|extra|extras|特别篇|特別篇|番外|特典)[\\/]/i
|
||||
/[\\/](?:电视剧|剧集|国产剧|欧美剧|日韩剧|日剧|韩剧|综艺|纪录片|动漫|番剧|国漫|日番|儿童|tv|series|shows?|season[\s._-]*\d|s\d{1,2}(?:[\s._-]|[\\/])|special[\s._-]*episodes?|specials?|sp|ovas?|oads?|extras?|bonus(?:es)?|omake|特别篇|特別篇|番外篇?|特典|外传|外傳|总集篇|總集篇)[\\/]/i
|
||||
|
||||
const SEASON_FOLDER_RE =
|
||||
/^(?:s\d{1,2}|season[\s._-]*\d{1,2}|第\s*\d{1,2}\s*季|specials?|sp|ova|oad|extra|extras|特别篇|特別篇|番外|特典)$/i
|
||||
/^(?:s\d{1,2}|season[\s._-]*\d{1,2}|第\s*[0-9一二三四五六七八九十百零两]+\s*季|special[\s._-]*episodes?|specials?|sp|ovas?|oads?|extras?|bonus(?:es)?|omake|特别篇|特別篇|番外篇?|特典|外传|外傳|总集篇|總集篇)$/i
|
||||
|
||||
function pathLooksEpisodic(media: Media): boolean {
|
||||
const path = (media.path || media.display_library_path || media.library_path || '')
|
||||
@@ -114,7 +114,7 @@ function normalizeTitle(value?: string): string {
|
||||
}
|
||||
|
||||
const SERIES_SPECIAL_SUFFIX_RE =
|
||||
/(?:\s+(?:specials?|sp|ova|oad|extra|extras)|\s*(?:特别篇|特別篇|番外|特典|外传|外傳))$/i
|
||||
/(?:\s+(?:s0+|season 0+|special episodes?|specials?|sp|ovas?|oads?|extras?|bonus(?:es)?|omake)|\s*(?:特别篇|特別篇|番外篇?|特典|外传|外傳|总集篇|總集篇))$/i
|
||||
|
||||
function normalizePathSeriesTitle(value?: string): string {
|
||||
const title = normalizeTitle(value)
|
||||
|
||||
Reference in New Issue
Block a user