fix: add global adult library visibility controls

This commit is contained in:
ShukeBta
2026-05-30 03:24:08 +08:00
parent ce8ffabba7
commit 99ecae0c44
17 changed files with 208 additions and 37 deletions
+2
View File
@@ -68,6 +68,7 @@ func (s *AuthService) SeedAdmin(ctx context.Context) error {
PasswordHash: hash,
Role: "admin",
Tier: "plus",
HideAdult: true,
ForcePasswordReset: pwd == "admin123",
}
if err := s.repo.User.Create(ctx, user); err != nil {
@@ -112,6 +113,7 @@ func (s *AuthService) Register(ctx context.Context, username, password string) (
PasswordHash: hash,
Role: role,
Tier: "free",
HideAdult: true,
}
if err := s.repo.User.Create(ctx, u); err != nil {
return nil, nil, err
+11
View File
@@ -55,6 +55,17 @@ func TestRegisterRejectsMoreThanTwentyUsers(t *testing.T) {
}
}
func TestRegisterDefaultsAdultLibrariesHidden(t *testing.T) {
_, auth, _, _ := newAuthTestServices(t)
user, _, err := auth.Register(context.Background(), "viewer", "password")
if err != nil {
t.Fatalf("register: %v", err)
}
if !user.HideAdult {
t.Fatal("new users should hide adult libraries by default")
}
}
func TestDefaultPermissionsAreViewerOnly(t *testing.T) {
perms := DefaultPermissions("user-1")
if !perms.CanViewDashboard || !perms.CanPlayMedia || !perms.CanExternalPlayer {
+4 -1
View File
@@ -175,6 +175,9 @@ func TestEmbyHidesAdultLibrariesForUserLock(t *testing.T) {
if err := svc.repo.Library.Create(t.Context(), &adult); err != nil {
t.Fatalf("create adult library: %v", err)
}
if err := svc.repo.Setting.Set(t.Context(), AdultLibraryIDsSettingKey, `["`+adult.ID+`"]`); err != nil {
t.Fatalf("set adult libraries: %v", err)
}
if err := svc.repo.DB.Create(&model.Media{LibraryID: safe.ID, Title: "安全电影", Path: `/media/movies/a.mkv`}).Error; err != nil {
t.Fatalf("create safe media: %v", err)
}
@@ -205,7 +208,7 @@ func newTestEmbyService(t *testing.T) *EmbyService {
if err != nil {
t.Fatalf("open db: %v", err)
}
if err := db.AutoMigrate(&model.Library{}, &model.Series{}, &model.Media{}, &model.Favorite{}, &model.PlaybackHistory{}, &model.User{}); err != nil {
if err := db.AutoMigrate(&model.Library{}, &model.Series{}, &model.Media{}, &model.Favorite{}, &model.PlaybackHistory{}, &model.User{}, &model.Setting{}); err != nil {
t.Fatalf("migrate: %v", err)
}
repos := repository.New(db)
+8
View File
@@ -26,6 +26,7 @@ type MediaService struct {
type MediaVisibility struct {
IncludeNSFW bool
AllowedLibraryIDs []string
HiddenLibraryIDs []string
}
func (v MediaVisibility) Allows(media *model.Media) bool {
@@ -35,6 +36,11 @@ func (v MediaVisibility) Allows(media *model.Media) bool {
if !v.IncludeNSFW && media.NSFW {
return false
}
for _, id := range v.HiddenLibraryIDs {
if id == media.LibraryID {
return false
}
}
if len(v.AllowedLibraryIDs) == 0 {
return true
}
@@ -182,6 +188,7 @@ func (s *MediaService) ListMediaVisible(ctx context.Context, libraryID string, p
return s.repo.Media.ListByLibraryFiltered(ctx, libraryID, (page-1)*pageSize, pageSize, repository.MediaQueryFilter{
IncludeNSFW: visibility.IncludeNSFW,
AllowedLibraryIDs: visibility.AllowedLibraryIDs,
HiddenLibraryIDs: visibility.HiddenLibraryIDs,
})
}
@@ -197,6 +204,7 @@ func (s *MediaService) SearchMediaVisible(ctx context.Context, query string, lim
return s.repo.Media.SearchFiltered(ctx, query, limit, repository.MediaQueryFilter{
IncludeNSFW: visibility.IncludeNSFW,
AllowedLibraryIDs: visibility.AllowedLibraryIDs,
HiddenLibraryIDs: visibility.HiddenLibraryIDs,
})
}
+25 -4
View File
@@ -17,7 +17,7 @@ func TestMediaVisibilityFiltersNSFWAndLibraries(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.Library{}, &model.Media{}); err != nil {
if err := db.AutoMigrate(&model.Library{}, &model.Media{}, &model.Setting{}); err != nil {
t.Fatal(err)
}
repos := repository.New(db)
@@ -31,6 +31,9 @@ func TestMediaVisibilityFiltersNSFWAndLibraries(t *testing.T) {
if err := db.Create(&libB).Error; err != nil {
t.Fatal(err)
}
if err := repos.Setting.Set(t.Context(), AdultLibraryIDsSettingKey, `["`+libB.ID+`"]`); err != nil {
t.Fatal(err)
}
rows := []model.Media{
{LibraryID: libA.ID, Title: "普通电影", Path: "/media/movies/a.mkv"},
{LibraryID: libA.ID, Title: "成人电影", Path: "/media/movies/b.mkv", NSFW: true},
@@ -40,11 +43,15 @@ func TestMediaVisibilityFiltersNSFWAndLibraries(t *testing.T) {
t.Fatal(err)
}
items, err := svc.SearchMediaVisible(t.Context(), "电影", 20, MediaVisibility{IncludeNSFW: false})
hiddenAdultLibraries := AdultLibraryIDs(t.Context(), repos)
items, err := svc.SearchMediaVisible(t.Context(), "电影", 20, MediaVisibility{
IncludeNSFW: false,
HiddenLibraryIDs: hiddenAdultLibraries,
})
if err != nil {
t.Fatal(err)
}
if got := sortedMediaTitles(items); !slices.Equal(got, []string{"普通电影", "限制媒体库电影"}) {
if got := sortedMediaTitles(items); !slices.Equal(got, []string{"普通电影"}) {
t.Fatalf("NSFW-filtered search = %#v", got)
}
@@ -59,13 +66,27 @@ func TestMediaVisibilityFiltersNSFWAndLibraries(t *testing.T) {
t.Fatalf("library-filtered search = %#v", got)
}
listed, total, err := svc.ListMediaVisible(t.Context(), libA.ID, 1, 20, MediaVisibility{IncludeNSFW: false})
listed, total, err := svc.ListMediaVisible(t.Context(), libA.ID, 1, 20, MediaVisibility{
IncludeNSFW: false,
HiddenLibraryIDs: hiddenAdultLibraries,
})
if err != nil {
t.Fatal(err)
}
if total != 1 || len(listed) != 1 || listed[0].Title != "普通电影" {
t.Fatalf("NSFW-filtered list total=%d rows=%#v", total, sortedMediaTitles(listed))
}
listed, total, err = svc.ListMediaVisible(t.Context(), libB.ID, 1, 20, MediaVisibility{
IncludeNSFW: false,
HiddenLibraryIDs: hiddenAdultLibraries,
})
if err != nil {
t.Fatal(err)
}
if total != 0 || len(listed) != 0 {
t.Fatalf("adult library should be hidden total=%d rows=%#v", total, sortedMediaTitles(listed))
}
}
func sortedMediaTitles(rows []model.Media) []string {
+32 -3
View File
@@ -9,20 +9,24 @@ import (
"github.com/ShukeBta/MediaStationGo/internal/repository"
)
const AdultLibraryIDsSettingKey = "adult.library_ids"
// AdultContentEnabled reads the global Adult / NSFW switch.
func AdultContentEnabled(ctx context.Context, repo *repository.Container) bool {
if repo == nil || repo.Setting == nil {
return false
return true
}
value, err := repo.Setting.Get(ctx, "adult.enabled")
if err != nil {
return false
return true
}
switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "true", "yes", "on", "enabled", "启用", "开启":
return true
default:
case "0", "false", "no", "off", "disabled", "禁用", "关闭":
return false
default:
return true
}
}
@@ -45,6 +49,7 @@ func UserDefaultMediaVisibility(ctx context.Context, repo *repository.Container,
if UserHidesAdult(ctx, repo, userID) {
visibility.IncludeNSFW = false
}
visibility.HiddenLibraryIDs = hiddenAdultLibraryIDs(ctx, repo, visibility.IncludeNSFW)
if userID == "" || repo.PlayProfile == nil {
return visibility
}
@@ -58,6 +63,7 @@ func UserDefaultMediaVisibility(ctx context.Context, repo *repository.Container,
}
visibility.IncludeNSFW = visibility.IncludeNSFW && row.AllowAdult
visibility.AllowedLibraryIDs = DecodeAllowedLibraryIDs(row.AllowedLibraryIDs)
visibility.HiddenLibraryIDs = hiddenAdultLibraryIDs(ctx, repo, visibility.IncludeNSFW)
break
}
return visibility
@@ -99,6 +105,11 @@ func LibraryVisibleForUser(ctx context.Context, repo *repository.Container, lib
if visibility.IncludeNSFW {
return true
}
for _, id := range visibility.HiddenLibraryIDs {
if id == lib.ID {
return false
}
}
if LibraryLooksAdult(lib) {
return false
}
@@ -127,3 +138,21 @@ func LibraryLooksAdult(lib model.Library) bool {
}
return false
}
func AdultLibraryIDs(ctx context.Context, repo *repository.Container) []string {
if repo == nil || repo.Setting == nil {
return nil
}
raw, err := repo.Setting.Get(ctx, AdultLibraryIDsSettingKey)
if err != nil {
return nil
}
return DecodeAllowedLibraryIDs(raw)
}
func hiddenAdultLibraryIDs(ctx context.Context, repo *repository.Container, includeNSFW bool) []string {
if includeNSFW {
return nil
}
return AdultLibraryIDs(ctx, repo)
}