mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
2b99f5f108
Mounted Emby libraries were always appended to web/Emby library lists and detail/play routes without checking allowed_library_ids, so restricted users could still see and open them. Filter remotes with the same visibility policy as local libraries across list/detail/series/stream and Emby Views/Items/search/playback, and label mounts in the admin ACL UI. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: truewhile <truewhile@users.noreply.github.com>
239 lines
6.6 KiB
Go
239 lines
6.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
"github.com/truewhile/MeBox/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 true
|
|
}
|
|
value, err := repo.Setting.Get(ctx, "adult.enabled")
|
|
if err != nil {
|
|
return true
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case "1", "true", "yes", "on", "enabled", "启用", "开启":
|
|
return true
|
|
case "0", "false", "no", "off", "disabled", "禁用", "关闭":
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
// UserHidesAdult reports whether a user's own lock overrides all profiles.
|
|
func UserHidesAdult(ctx context.Context, repo *repository.Container, userID string) bool {
|
|
if strings.TrimSpace(userID) == "" || repo == nil || repo.User == nil {
|
|
return false
|
|
}
|
|
user, err := repo.User.FindByID(ctx, userID)
|
|
return err == nil && user != nil && user.HideAdult
|
|
}
|
|
|
|
// UserDefaultMediaVisibility is the visibility policy used by clients that
|
|
// cannot pass a web play-profile token, notably Emby/Jellyfin-compatible apps.
|
|
func UserDefaultMediaVisibility(ctx context.Context, repo *repository.Container, userID string) MediaVisibility {
|
|
visibility := MediaVisibility{IncludeNSFW: AdultContentEnabled(ctx, repo)}
|
|
if repo == nil {
|
|
return visibility
|
|
}
|
|
if UserHidesAdult(ctx, repo, userID) {
|
|
visibility.IncludeNSFW = false
|
|
}
|
|
visibility.HiddenLibraryIDs = hiddenAdultLibraryIDs(ctx, repo, visibility.IncludeNSFW)
|
|
if userID == "" {
|
|
return visibility
|
|
}
|
|
|
|
if repo.User != nil {
|
|
user, err := repo.User.FindByID(ctx, userID)
|
|
if err == nil && user != nil && user.Role != "admin" {
|
|
if userAllowed := user.DecodeAllowedLibraryIDs(); len(userAllowed) > 0 {
|
|
visibility.AllowedLibraryIDs = userAllowed
|
|
}
|
|
}
|
|
}
|
|
|
|
if repo.PlayProfile == nil {
|
|
return visibility
|
|
}
|
|
rows, err := repo.PlayProfile.ListByUser(ctx, userID)
|
|
if err != nil {
|
|
return visibility
|
|
}
|
|
for _, row := range rows {
|
|
if !row.IsDefault {
|
|
continue
|
|
}
|
|
visibility.IncludeNSFW = visibility.IncludeNSFW && row.AllowAdult
|
|
profileAllowed := DecodeAllowedLibraryIDs(row.AllowedLibraryIDs)
|
|
if len(profileAllowed) > 0 {
|
|
if len(visibility.AllowedLibraryIDs) > 0 {
|
|
visibility.AllowedLibraryIDs = IntersectStrings(visibility.AllowedLibraryIDs, profileAllowed)
|
|
} else {
|
|
visibility.AllowedLibraryIDs = profileAllowed
|
|
}
|
|
}
|
|
visibility.HiddenLibraryIDs = hiddenAdultLibraryIDs(ctx, repo, visibility.IncludeNSFW)
|
|
break
|
|
}
|
|
return visibility
|
|
}
|
|
|
|
// IntersectStrings 计算两个字符串切片的交集。
|
|
func IntersectStrings(a, b []string) []string {
|
|
if len(a) == 0 {
|
|
return b
|
|
}
|
|
if len(b) == 0 {
|
|
return a
|
|
}
|
|
set := make(map[string]struct{}, len(b))
|
|
for _, s := range b {
|
|
set[s] = struct{}{}
|
|
}
|
|
var out []string
|
|
for _, s := range a {
|
|
if _, ok := set[s]; ok {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return []string{"__no_access__"}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// DecodeAllowedLibraryIDs normalises a PlayProfile allowed-library JSON string.
|
|
func DecodeAllowedLibraryIDs(raw string) []string {
|
|
if strings.TrimSpace(raw) == "" {
|
|
return nil
|
|
}
|
|
var ids []string
|
|
if err := json.Unmarshal([]byte(raw), &ids); err != nil {
|
|
return nil
|
|
}
|
|
out := ids[:0]
|
|
for _, id := range ids {
|
|
if strings.TrimSpace(id) != "" {
|
|
out = append(out, strings.TrimSpace(id))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// LibraryIDAllowed reports whether libraryID is permitted by the allow-list.
|
|
// An empty AllowedLibraryIDs means unrestricted access.
|
|
func LibraryIDAllowed(visibility MediaVisibility, libraryID string) bool {
|
|
if len(visibility.AllowedLibraryIDs) == 0 {
|
|
return true
|
|
}
|
|
for _, id := range visibility.AllowedLibraryIDs {
|
|
if id == libraryID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// EmbyMountLibraryID is the web/Emby library id for a mounted remote view.
|
|
func EmbyMountLibraryID(mount *model.EmbyMount) string {
|
|
if mount == nil {
|
|
return ""
|
|
}
|
|
return EncodeEmbyRemoteID(mount.ID, mount.RemoteViewID)
|
|
}
|
|
|
|
// EmbyMountLibraryAllowed reports whether a mounted Emby library is allowed for
|
|
// the given visibility policy.
|
|
func EmbyMountLibraryAllowed(visibility MediaVisibility, mount *model.EmbyMount) bool {
|
|
return LibraryIDAllowed(visibility, EmbyMountLibraryID(mount))
|
|
}
|
|
|
|
// LibraryVisibleForUser applies profile library limits and adult-directory
|
|
// hiding to a library card/folder.
|
|
func LibraryVisibleForUser(ctx context.Context, repo *repository.Container, lib model.Library, visibility MediaVisibility) bool {
|
|
if !LibraryIDAllowed(visibility, lib.ID) {
|
|
return false
|
|
}
|
|
if visibility.IncludeNSFW {
|
|
return true
|
|
}
|
|
hiddenLibraryIDs := visibility.HiddenLibraryIDs
|
|
configuredAdultLibraryIDs := AdultLibraryIDs(ctx, repo)
|
|
hasConfiguredAdultLibraries := len(hiddenLibraryIDs) > 0 || len(configuredAdultLibraryIDs) > 0
|
|
if len(hiddenLibraryIDs) == 0 {
|
|
hiddenLibraryIDs = configuredAdultLibraryIDs
|
|
}
|
|
for _, id := range hiddenLibraryIDs {
|
|
if id == lib.ID {
|
|
return false
|
|
}
|
|
}
|
|
if hasConfiguredAdultLibraries {
|
|
return true
|
|
}
|
|
if LibraryLooksAdult(lib) {
|
|
return false
|
|
}
|
|
if repo != nil && repo.DB != nil {
|
|
var totalCount int64
|
|
_ = repo.DB.WithContext(ctx).Model(&model.Media{}).
|
|
Where("library_id = ?", lib.ID).
|
|
Count(&totalCount).Error
|
|
if totalCount > 0 {
|
|
var nsfwCount int64
|
|
_ = repo.DB.WithContext(ctx).Model(&model.Media{}).
|
|
Where("library_id = ? AND nsfw = ?", lib.ID, true).
|
|
Count(&nsfwCount).Error
|
|
// 仅当整库媒体全部为成人内容(纯成人库)时才隐藏整库;
|
|
// 含有普通内容的混合媒体库保持库本身可见,具体 NSFW 条目在媒体列表内过滤。
|
|
if nsfwCount == totalCount {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// LibraryLooksAdult catches adult-only roots even before all rows are scraped.
|
|
func LibraryLooksAdult(lib model.Library) bool {
|
|
text := strings.ToLower(strings.TrimSpace(lib.Name + " " + lib.Path + " " + lib.Type))
|
|
if text == "" {
|
|
return false
|
|
}
|
|
for _, token := range []string{"成人", "限制级", "nsfw", "adult", "jav", "javdb", "javbus", "9kg", "里番", "番号"} {
|
|
if strings.Contains(text, token) {
|
|
return true
|
|
}
|
|
}
|
|
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)
|
|
}
|