Files
MeBox/internal/service/adult_scraper_test.go
T
ShukeBta b44c7f860b fix(media): 修复emby电影库混入剧集/特别篇识别/剧集合集 + 新增手动编辑元数据
emby 兼容层(hills/yamby 等客户端):
- 电影库不再混入剧集:filterLikelyEpisodicPathsFromMovieQuery 按路径
  特征(Season/S0x/Specials/电视剧/日番/国漫等)剔除误入电影库的剧集
- itemPayload 改用 mediaShouldBeEpisode:有季集号且(属剧集库或路径像剧集)
  才标记为 Episode,修正电影库里剧集被当成 Movie

特别篇识别:
- episode_parser 新增 patSpecialSeason,识别 S0/Season 0/Specials/SP/OVA/
  OAD/番外/特别篇/特典 文件夹 → 第0季;seasonFromParents 返回 found 标志,
  区分"明确第0季"与"未识别默认第1季"
- emby seasonDir 正则同步支持 Specials/SP/OVA/特别篇 等

剧集合集:
- groupSeries 抽出 isSeriesCard,卡片链接按是否剧集判定,修复剧集卡片
  跳转;每集分离的根因(季集号/分组key)随入库识别修复一并改善

手动编辑元数据(自采集视频无TMDB信息时可自定义封面/标题/简介/分类):
- 新增 MediaService.UpdateMetadata + PATCH /media/:id/metadata(需admin)
- 前端 MetadataEditDialog 接入 LibraryPage 与 MediaDetailPage

其他:scanner/scraper/nfo/local+cloud_metadata/adult 元数据识别配套改进;
.gitignore 忽略本地 .dev-cache/.dev-data 运行产物。
2026-06-19 12:23:56 +08:00

152 lines
4.7 KiB
Go

package service
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/glebarez/sqlite"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/repository"
)
func TestNormalizeAdultCode(t *testing.T) {
cases := map[string]string{
"SSIS001.mp4": "SSIS-001",
"fc2-ppv-1234567.mkv": "FC2-PPV-1234567",
"heyzo_1234.mp4": "HEYZO-1234",
"120118_001-carib.mp4": "120118-001",
"movie.1080p.x264.mkv": "",
}
for in, want := range cases {
if got := normalizeAdultCode(in); got != want {
t.Fatalf("normalizeAdultCode(%q) = %q, want %q", in, got, want)
}
}
}
func TestParseAdultDetailHTML(t *testing.T) {
html := `<html>
<h2 class="title"><strong>SSIS-001 测试标题</strong></h2>
<img class="video-cover" src="/covers/ssis001.jpg">
<a class="sample-box" href="/samples/1.jpg"></a>
<span class="score"><span class="value">4.7</span></span>
<div>日期 2024-05-01</div>
</html>`
got := parseAdultDetailHTML(html, "SSIS-001", "javdb", "https://javdb.com/v/abc")
if got == nil {
t.Fatal("parseAdultDetailHTML returned nil")
}
if got.Title != "测试标题" || got.OriginalName != "SSIS-001" || !got.NSFW {
t.Fatalf("unexpected metadata: %+v", got)
}
if got.PosterURL != "https://javdb.com/covers/ssis001.jpg" || got.BackdropURL != "https://javdb.com/samples/1.jpg" {
t.Fatalf("unexpected artwork: %+v", got)
}
if got.Year != 2024 {
t.Fatalf("year = %d, want 2024", got.Year)
}
}
func TestParseAdultDetailHTMLDerivesDMMPoster(t *testing.T) {
html := `<html>
<h3>NACR-833 测试标题</h3>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/h_237nacr00833/h_237nacr00833jp-1.jpg"></a>
</html>`
got := parseAdultDetailHTML(html, "NACR-833", "javbus", "https://www.javbus.com/NACR-833")
if got == nil {
t.Fatal("parseAdultDetailHTML returned nil")
}
if got.PosterURL != "https://pics.dmm.co.jp/digital/video/h_237nacr00833/h_237nacr00833pl.jpg" {
t.Fatalf("PosterURL = %q", got.PosterURL)
}
}
func TestAdultProviderUsesConfiguredMultipleSources(t *testing.T) {
bad := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "temporary failure", http.StatusInternalServerError)
}))
defer bad.Close()
good := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/search":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(`<a class="box" href="/v/ssis001"><strong>SSIS-001 多源入口</strong></a>`))
case "/v/ssis001":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(`<h2 class="title"><strong>SSIS-001 多源命中标题</strong></h2><img class="video-cover" src="/cover.jpg">`))
default:
http.NotFound(w, r)
}
}))
defer good.Close()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.APIConfig{}); err != nil {
t.Fatal(err)
}
apiConfig := NewAPIConfigService(zap.NewNop(), repository.New(db), NewCryptoService("", zap.NewNop()))
baseURL := bad.URL + "\n" + good.URL
if _, err := apiConfig.Update(context.Background(), "adult", APIConfigPatch{BaseURL: &baseURL}); err != nil {
t.Fatal(err)
}
provider := NewAdultProvider(zap.NewNop(), apiConfig)
match, err := provider.Search(context.Background(), "SSIS-001")
if err != nil {
t.Fatal(err)
}
if match == nil || match.Title != "多源命中标题" || match.OriginalName != "SSIS-001" || !match.NSFW {
t.Fatalf("multi-source adult match = %+v", match)
}
}
func TestAdultSourceKindRecognizesJavBusMirrors(t *testing.T) {
cases := map[string]string{
"https://javdb.com": "javdb",
"https://javbus.sbs": "javbus",
"https://www.javbus.com": "javbus",
"https://www.cdnbus.cyou": "javbus",
"https://www.javsee.cyou": "javbus",
"https://www.busjav.cyou": "javbus",
"www.cdnbus.cyou": "javbus",
"https://example.invalid": "javdb",
}
for in, want := range cases {
if got := adultSourceKind(in); got != want {
t.Fatalf("adultSourceKind(%q) = %q, want %q", in, got, want)
}
}
}
func TestAdultProviderDefaultBases(t *testing.T) {
provider := &AdultProvider{}
got := provider.resolveBases(context.Background())
want := []string{
"https://javdb.com",
"https://javbus.sbs",
"https://www.javbus.com",
"https://www.cdnbus.cyou",
"https://www.javsee.cyou",
"https://www.busjav.cyou",
}
if len(got) != len(want) {
t.Fatalf("resolveBases len = %d, want %d: %v", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("resolveBases[%d] = %q, want %q", i, got[i], want[i])
}
}
}