mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
379 lines
13 KiB
Go
379 lines
13 KiB
Go
package service
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/truewhile/MeBox/internal/config"
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
"github.com/truewhile/MeBox/internal/repository"
|
|
)
|
|
|
|
func TestScanLibraryUsesLocalMetadata(t *testing.T) {
|
|
root := t.TempDir()
|
|
seasonDir := filepath.Join(root, "Show", "Season 02")
|
|
if err := os.MkdirAll(seasonDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mediaPath := filepath.Join(seasonDir, "Show - EP03.mkv")
|
|
if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "Show", "tvshow.nfo"), []byte(`<tvshow><title>本地剧名</title><year>2025</year></tvshow>`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(nfoPath(mediaPath), []byte(`<episodedetails><title>本地第三集</title><season>2</season><episode>3</episode></episodedetails>`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
lib := model.Library{Name: "TV", Path: root, Type: "tv", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
|
|
res, err := scanner.ScanLibrary(t.Context(), lib.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.LocalMetadata != 1 {
|
|
t.Fatalf("LocalMetadata = %d, want 1", res.LocalMetadata)
|
|
}
|
|
if res.Added != 1 || res.Updated != 0 {
|
|
t.Fatalf("scan counts added=%d updated=%d, want 1/0", res.Added, res.Updated)
|
|
}
|
|
var media model.Media
|
|
if err := db.First(&media, "path = ?", mediaPath).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 单集名(本地第三集)不应写入 OriginalName(整剧原名,合集分组键)。
|
|
// tvshow.nfo 未提供 originaltitle, 故 OriginalName 应为空。
|
|
if media.Title != "本地剧名" || media.OriginalName != "" || media.SeasonNum != 2 || media.EpisodeNum != 3 || media.ScrapeStatus != "matched" {
|
|
t.Fatalf("unexpected scanned media: %+v", media)
|
|
}
|
|
if media.EpisodeTitle != "本地第三集" {
|
|
t.Fatalf("episode_title = %q, want 本地第三集", media.EpisodeTitle)
|
|
}
|
|
|
|
res, err = scanner.ScanLibrary(t.Context(), lib.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Added != 0 || res.Updated != 0 || res.Skipped != 1 {
|
|
t.Fatalf("repeat scan counts added=%d updated=%d skipped=%d, want 0/0/1", res.Added, res.Updated, res.Skipped)
|
|
}
|
|
}
|
|
|
|
func TestScanAnimeTheatricalFolderUsesMovieNFO(t *testing.T) {
|
|
root := t.TempDir()
|
|
showDir := filepath.Join(root, "摇曳露营△ (2018)")
|
|
movieDir := filepath.Join(showDir, "摇曳露营△ 剧场版 (2022)")
|
|
if err := os.MkdirAll(movieDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(showDir, "tvshow.nfo"), []byte(
|
|
`<tvshow><title>错误的剧集标题</title><tmdbid>76075</tmdbid><year>2018</year></tvshow>`,
|
|
), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mediaPath := filepath.Join(movieDir, "Eiga.Yurukyan.2022.Bluray.mkv")
|
|
if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(nfoPath(mediaPath), []byte(
|
|
`<movie><title>摇曳露营△ 剧场版</title><tmdbid>566466</tmdbid><year>2022</year></movie>`,
|
|
), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
lib := model.Library{Name: "动漫", Path: root, Type: "anime", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
|
|
if _, err := scanner.ScanLibrary(t.Context(), lib.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var media model.Media
|
|
if err := db.First(&media, "path = ?", mediaPath).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if media.Title != "摇曳露营△ 剧场版" || media.TMDbID != 566466 || media.Year != 2022 {
|
|
t.Fatalf("theatrical movie metadata = title %q tmdb %d year %d", media.Title, media.TMDbID, media.Year)
|
|
}
|
|
}
|
|
|
|
func TestScanLibraryDoesNotMarkArtworkOnlyAsMatched(t *testing.T) {
|
|
root := t.TempDir()
|
|
mediaPath := filepath.Join(root, "SSIS-001-CD1.mp4")
|
|
if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
poster := filepath.Join(root, "SSIS-001-poster.jpg")
|
|
if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
lib := model.Library{Name: "Adult", Path: root, Type: "movie", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
|
|
res, err := scanner.ScanLibrary(t.Context(), lib.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.LocalMetadata != 1 {
|
|
t.Fatalf("LocalMetadata = %d, want 1", res.LocalMetadata)
|
|
}
|
|
var media model.Media
|
|
if err := db.First(&media, "path = ?", mediaPath).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if media.PosterURL != poster {
|
|
t.Fatalf("poster_url = %q, want %q", media.PosterURL, poster)
|
|
}
|
|
if media.ScrapeStatus == "matched" {
|
|
t.Fatalf("artwork-only media should remain enrichable, got status %q", media.ScrapeStatus)
|
|
}
|
|
}
|
|
|
|
func TestScanLibraryRefreshesArtworkOnlyMetadata(t *testing.T) {
|
|
root := t.TempDir()
|
|
mediaPath := filepath.Join(root, "Movie.mkv")
|
|
if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldPoster := filepath.Join(root, "Movie-thumb.jpg")
|
|
if err := os.WriteFile(oldPoster, []byte("jpg"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
lib := model.Library{Name: "Movies", Path: root, Type: "movie", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
|
|
if _, err := scanner.ScanLibrary(t.Context(), lib.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
newPoster := filepath.Join(root, "poster.jpg")
|
|
if err := os.WriteFile(newPoster, []byte("jpg"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := scanner.ScanLibrary(t.Context(), lib.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var media model.Media
|
|
if err := db.First(&media, "path = ?", mediaPath).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if media.PosterURL != newPoster {
|
|
t.Fatalf("poster_url = %q, want refreshed local poster %q", media.PosterURL, newPoster)
|
|
}
|
|
if media.ScrapeStatus == "matched" {
|
|
t.Fatalf("artwork-only refresh should keep media enrichable, got %q", media.ScrapeStatus)
|
|
}
|
|
}
|
|
|
|
func TestScanLibraryParsesEpisodesForMovieTypedLibrary(t *testing.T) {
|
|
root := t.TempDir()
|
|
seasonDir := filepath.Join(root, "哈哈哈哈哈 (2020)", "Season 06")
|
|
if err := os.MkdirAll(seasonDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mediaPath := filepath.Join(seasonDir, "哈哈哈哈哈 - S06E17 - 第 17 集.mkv")
|
|
if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
lib := model.Library{Name: "综艺", Path: root, Type: "movie", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
|
|
if _, err := scanner.ScanLibrary(t.Context(), lib.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var media model.Media
|
|
if err := db.First(&media, "path = ?", mediaPath).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if media.SeasonNum != 6 || media.EpisodeNum != 17 {
|
|
t.Fatalf("season/episode = %d/%d, want 6/17", media.SeasonNum, media.EpisodeNum)
|
|
}
|
|
}
|
|
|
|
func TestScanLibraryRefreshesStaleNoMatchDerivedMetadata(t *testing.T) {
|
|
root := t.TempDir()
|
|
seasonDir := filepath.Join(root, "Hntv Spring Festival Gala S01e (2026)", "Season 1")
|
|
if err := os.MkdirAll(seasonDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mediaPath := filepath.Join(seasonDir, "Hntv Spring Festival Gala S01e - S01E202-DD5.QHstudIo.6.4K - 第 202 集.ts")
|
|
if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
lib := model.Library{Name: "综艺", Path: root, Type: "tv", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.DB.Create(&model.Media{
|
|
LibraryID: lib.ID,
|
|
Title: "hntv spring festival gala s01e",
|
|
Path: mediaPath,
|
|
SizeBytes: 1,
|
|
ScrapeStatus: "no_match",
|
|
}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
|
|
res, err := scanner.ScanLibrary(t.Context(), lib.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Updated != 1 || res.Skipped != 0 {
|
|
t.Fatalf("scan counts updated=%d skipped=%d, want 1/0", res.Updated, res.Skipped)
|
|
}
|
|
var media model.Media
|
|
if err := db.First(&media, "path = ?", mediaPath).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if media.Title != "Hntv Spring Festival Gala" || media.SeasonNum != 1 || media.EpisodeNum != 202 || media.ScrapeStatus != "pending" {
|
|
t.Fatalf("stale no_match row was not refreshed: title=%q s=%d e=%d status=%q", media.Title, media.SeasonNum, media.EpisodeNum, media.ScrapeStatus)
|
|
}
|
|
}
|
|
|
|
func TestScanLibraryPrefersEpisodeArtworkOverSeriesFanart(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
episodeArt string
|
|
}{
|
|
{name: "thumb suffix", episodeArt: "Show - S01E01-thumb.jpg"},
|
|
{name: "video stem", episodeArt: "Show - S01E01.jpg"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := t.TempDir()
|
|
seasonDir := filepath.Join(root, "Show", "Season 01")
|
|
if err := os.MkdirAll(seasonDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mediaPath := filepath.Join(seasonDir, "Show - S01E01.mkv")
|
|
if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(nfoPath(mediaPath), []byte(
|
|
`<episodedetails><title>第一集</title><season>1</season><episode>1</episode></episodedetails>`,
|
|
), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
episodeArt := filepath.Join(seasonDir, tc.episodeArt)
|
|
if err := os.WriteFile(episodeArt, []byte("episode still"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seriesFanart := filepath.Join(root, "Show", "fanart.jpg")
|
|
if err := os.WriteFile(seriesFanart, []byte("series backdrop"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
lib := model.Library{Name: "TV", Path: root, Type: "tv", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
existing := model.Media{
|
|
LibraryID: lib.ID,
|
|
Title: "Show",
|
|
Path: mediaPath,
|
|
SizeBytes: 1,
|
|
SeasonNum: 1,
|
|
EpisodeNum: 1,
|
|
BackdropURL: "https://image.example/episode-still.jpg",
|
|
ScrapeStatus: "matched",
|
|
}
|
|
if err := repos.DB.Create(&existing).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
|
|
if _, err := scanner.ScanLibrary(t.Context(), lib.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var media model.Media
|
|
if err := db.First(&media, "id = ?", existing.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if media.BackdropURL != episodeArt {
|
|
t.Fatalf("backdrop_url = %q, want episode artwork %q (series fanart %q must not win)", media.BackdropURL, episodeArt, seriesFanart)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScanLibraryPrunesMissingMedia(t *testing.T) {
|
|
root := t.TempDir()
|
|
mediaPath := filepath.Join(root, "Show.S02E03.mkv")
|
|
if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
lib := model.Library{Name: "TV", Path: root, Type: "tv", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stale := model.Media{
|
|
LibraryID: lib.ID,
|
|
Title: "Show",
|
|
Path: filepath.Join(root, "old", "Show.S02E03.mkv"),
|
|
SizeBytes: 123,
|
|
ScrapeStatus: "pending",
|
|
}
|
|
if err := db.Create(&stale).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
|
|
res, err := scanner.ScanLibrary(t.Context(), lib.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Removed != 1 {
|
|
t.Fatalf("Removed = %d, want 1", res.Removed)
|
|
}
|
|
var count int64
|
|
if err := db.Model(&model.Media{}).Where("library_id = ?", lib.ID).Count(&count).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("media count = %d, want 1", count)
|
|
}
|
|
}
|