feat(api): add homepage video settings endpoints
This commit is contained in:
parent
e7001750a3
commit
0180e6ace5
@ -331,6 +331,7 @@ func RegisterRoutes(r *gin.Engine, opts ...Option) {
|
||||
authGroup.GET("/mfa/status", h.mfaStatus)
|
||||
authGroup.GET("/sync/config", h.syncConfigSnapshot)
|
||||
authGroup.POST("/sync/ack", h.syncConfigAck)
|
||||
authGroup.GET("/homepage-video", h.getHomepageVideoPublic)
|
||||
|
||||
// Sandbox binding read endpoint.
|
||||
// Used by the Console Guest/Demo experience. Must be readable either via a
|
||||
@ -366,6 +367,8 @@ func RegisterRoutes(r *gin.Engine, opts ...Option) {
|
||||
|
||||
authProtected.GET("/admin/settings", h.getAdminSettings)
|
||||
authProtected.POST("/admin/settings", h.updateAdminSettings)
|
||||
authProtected.GET("/admin/homepage-video", h.getHomepageVideoSettings)
|
||||
authProtected.PUT("/admin/homepage-video", h.updateHomepageVideoSettings)
|
||||
|
||||
// Backward-compatible auth-scoped admin routes consumed by the dashboard BFF.
|
||||
authProtected.GET("/admin/users/metrics", h.adminUsersMetrics)
|
||||
|
||||
114
api/homepage_video.go
Normal file
114
api/homepage_video.go
Normal file
@ -0,0 +1,114 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"account/internal/service"
|
||||
)
|
||||
|
||||
type homepageVideoEntryPayload struct {
|
||||
Domain string `json:"domain,omitempty"`
|
||||
VideoURL string `json:"videoUrl"`
|
||||
PosterURL string `json:"posterUrl"`
|
||||
}
|
||||
|
||||
func toHomepageVideoEntryPayload(entry service.HomepageVideoEntry) homepageVideoEntryPayload {
|
||||
return homepageVideoEntryPayload{
|
||||
Domain: strings.TrimSpace(entry.DomainKey),
|
||||
VideoURL: strings.TrimSpace(entry.VideoURL),
|
||||
PosterURL: strings.TrimSpace(entry.PosterURL),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) getHomepageVideoPublic(c *gin.Context) {
|
||||
entry, err := service.ResolveHomepageVideoEntry(c.Request.Context(), h.resolveTenantHost(c))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"resolved": toHomepageVideoEntryPayload(entry),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) getHomepageVideoSettings(c *gin.Context) {
|
||||
if _, ok := h.requireAdminPermission(c, permissionAdminSettingsRead); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := service.GetHomepageVideoSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
overrides := make([]homepageVideoEntryPayload, 0, len(settings.Overrides))
|
||||
for _, item := range settings.Overrides {
|
||||
overrides = append(overrides, toHomepageVideoEntryPayload(item))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"defaultEntry": toHomepageVideoEntryPayload(settings.DefaultEntry),
|
||||
"overrides": overrides,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) updateHomepageVideoSettings(c *gin.Context) {
|
||||
adminUser, ok := h.requireAdminPermission(c, permissionAdminSettingsWrite)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if h.isReadOnlyAccount(adminUser) {
|
||||
respondError(c, http.StatusForbidden, "read_only_account", "demo account is read-only")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
DefaultEntry homepageVideoEntryPayload `json:"defaultEntry"`
|
||||
Overrides []homepageVideoEntryPayload `json:"overrides"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
overrides := make([]service.HomepageVideoEntry, 0, len(req.Overrides))
|
||||
for _, item := range req.Overrides {
|
||||
overrides = append(overrides, service.HomepageVideoEntry{
|
||||
DomainKey: item.Domain,
|
||||
VideoURL: item.VideoURL,
|
||||
PosterURL: item.PosterURL,
|
||||
})
|
||||
}
|
||||
|
||||
settings, err := service.SaveHomepageVideoSettings(c.Request.Context(), service.HomepageVideoSettings{
|
||||
DefaultEntry: service.HomepageVideoEntry{
|
||||
VideoURL: req.DefaultEntry.VideoURL,
|
||||
PosterURL: req.DefaultEntry.PosterURL,
|
||||
},
|
||||
Overrides: overrides,
|
||||
}, adminUser.Email)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if errors.Is(err, service.ErrServiceDBNotInitialized) {
|
||||
status = http.StatusServiceUnavailable
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
responseOverrides := make([]homepageVideoEntryPayload, 0, len(settings.Overrides))
|
||||
for _, item := range settings.Overrides {
|
||||
responseOverrides = append(responseOverrides, toHomepageVideoEntryPayload(item))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"defaultEntry": toHomepageVideoEntryPayload(settings.DefaultEntry),
|
||||
"overrides": responseOverrides,
|
||||
})
|
||||
}
|
||||
193
api/homepage_video_test.go
Normal file
193
api/homepage_video_test.go
Normal file
@ -0,0 +1,193 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"account/internal/model"
|
||||
"account/internal/service"
|
||||
"account/internal/store"
|
||||
)
|
||||
|
||||
type homepageVideoTestEnv struct {
|
||||
router *gin.Engine
|
||||
adminToken string
|
||||
userToken string
|
||||
}
|
||||
|
||||
func setupHomepageVideoTestRouter(t *testing.T) homepageVideoTestEnv {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.AdminSetting{}, &model.HomepageVideoSetting{}); err != nil {
|
||||
t.Fatalf("auto migrate: %v", err)
|
||||
}
|
||||
service.SetDB(db)
|
||||
t.Cleanup(func() {
|
||||
service.SetDB(nil)
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.Close()
|
||||
})
|
||||
|
||||
memoryStore := store.NewMemoryStore()
|
||||
ctx := context.Background()
|
||||
|
||||
createUser := func(name, email, password, role string, level int) string {
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
t.Fatalf("hash password: %v", err)
|
||||
}
|
||||
user := &store.User{
|
||||
Name: name,
|
||||
Email: email,
|
||||
PasswordHash: string(hashed),
|
||||
Role: role,
|
||||
Level: level,
|
||||
EmailVerified: true,
|
||||
}
|
||||
if err := memoryStore.CreateUser(ctx, user); err != nil {
|
||||
t.Fatalf("create user: %v", err)
|
||||
}
|
||||
return password
|
||||
}
|
||||
|
||||
adminPassword := createUser("admin", "admin@example.com", "AdminPass123!", store.RoleAdmin, store.LevelAdmin)
|
||||
userPassword := createUser("user", "user@example.com", "UserPass123!", store.RoleUser, store.LevelUser)
|
||||
|
||||
router := gin.New()
|
||||
RegisterRoutes(router, WithStore(memoryStore), WithEmailVerification(false))
|
||||
|
||||
login := func(email, password string) string {
|
||||
payload := map[string]string{
|
||||
"email": email,
|
||||
"password": password,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/login", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("login failed for %s: %d %s", email, resp.Code, resp.Body.String())
|
||||
}
|
||||
var result struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Fatalf("decode login response: %v", err)
|
||||
}
|
||||
return result.Token
|
||||
}
|
||||
|
||||
return homepageVideoTestEnv{
|
||||
router: router,
|
||||
adminToken: login("admin@example.com", adminPassword),
|
||||
userToken: login("user@example.com", userPassword),
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomepageVideoPublicDefaults(t *testing.T) {
|
||||
env := setupHomepageVideoTestRouter(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/auth/homepage-video", nil)
|
||||
req.Header.Set("X-Forwarded-Host", "cn-www.svc.plus")
|
||||
resp := httptest.NewRecorder()
|
||||
env.router.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d (%s)", resp.Code, resp.Body.String())
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Resolved struct {
|
||||
Domain string `json:"domain"`
|
||||
VideoURL string `json:"videoUrl"`
|
||||
} `json:"resolved"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if payload.Resolved.Domain != "cn-www.svc.plus" {
|
||||
t.Fatalf("expected cn override, got %q", payload.Resolved.Domain)
|
||||
}
|
||||
if payload.Resolved.VideoURL == "" {
|
||||
t.Fatalf("expected resolved video url")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomepageVideoAdminReadWrite(t *testing.T) {
|
||||
env := setupHomepageVideoTestRouter(t)
|
||||
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"defaultEntry": map[string]string{
|
||||
"videoUrl": "https://www.youtube.com/watch?v=test-main",
|
||||
"posterUrl": "https://cdn.svc.plus/default-poster.png",
|
||||
},
|
||||
"overrides": []map[string]string{
|
||||
{
|
||||
"domain": "demo.svc.plus",
|
||||
"videoUrl": "https://www.bilibili.com/video/BV1demo",
|
||||
"posterUrl": "https://cdn.svc.plus/demo-poster.png",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/auth/admin/homepage-video", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
resp := httptest.NewRecorder()
|
||||
env.router.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d (%s)", resp.Code, resp.Body.String())
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/auth/admin/homepage-video", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
resp = httptest.NewRecorder()
|
||||
env.router.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d (%s)", resp.Code, resp.Body.String())
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
DefaultEntry struct {
|
||||
VideoURL string `json:"videoUrl"`
|
||||
} `json:"defaultEntry"`
|
||||
Overrides []struct {
|
||||
Domain string `json:"domain"`
|
||||
} `json:"overrides"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if payload.DefaultEntry.VideoURL != "https://www.youtube.com/watch?v=test-main" {
|
||||
t.Fatalf("unexpected default video url: %q", payload.DefaultEntry.VideoURL)
|
||||
}
|
||||
if len(payload.Overrides) != 1 || payload.Overrides[0].Domain != "demo.svc.plus" {
|
||||
t.Fatalf("unexpected overrides payload: %+v", payload.Overrides)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomepageVideoAdminUnauthorized(t *testing.T) {
|
||||
env := setupHomepageVideoTestRouter(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/auth/admin/homepage-video", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.userToken)
|
||||
resp := httptest.NewRecorder()
|
||||
env.router.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected status 403, got %d", resp.Code)
|
||||
}
|
||||
}
|
||||
@ -1293,6 +1293,7 @@ func openAdminSettingsDB(cfg config.Store) (*gorm.DB, func(context.Context) erro
|
||||
|
||||
if err := db.AutoMigrate(
|
||||
&model.AdminSetting{},
|
||||
&model.HomepageVideoSetting{},
|
||||
&model.SandboxBinding{},
|
||||
&model.Tenant{},
|
||||
&model.TenantDomain{},
|
||||
|
||||
29
internal/model/homepage_video_setting.go
Normal file
29
internal/model/homepage_video_setting.go
Normal file
@ -0,0 +1,29 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// HomepageVideoSetting stores the homepage product demo media config.
|
||||
type HomepageVideoSetting struct {
|
||||
UUID string `gorm:"column:uuid;type:uuid;primaryKey"`
|
||||
DomainKey string `gorm:"column:domain_key;type:text;not null;uniqueIndex:idx_homepage_video_domain_key"`
|
||||
VideoURL string `gorm:"column:video_url;type:text;not null"`
|
||||
PosterURL string `gorm:"column:poster_url;type:text;not null;default:''"`
|
||||
UpdatedBy string `gorm:"column:updated_by;type:text;not null;default:''"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;autoUpdateTime"`
|
||||
}
|
||||
|
||||
func (HomepageVideoSetting) TableName() string { return "homepage_video_settings" }
|
||||
|
||||
func (setting *HomepageVideoSetting) BeforeCreate(tx *gorm.DB) error {
|
||||
if strings.TrimSpace(setting.UUID) == "" {
|
||||
setting.UUID = uuid.NewString()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
188
internal/service/homepage_video_settings.go
Normal file
188
internal/service/homepage_video_settings.go
Normal file
@ -0,0 +1,188 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"account/internal/model"
|
||||
"account/internal/store"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const homepageVideoDefaultDomainKey = "__default__"
|
||||
|
||||
var errHomepageVideoSettingsDBNotInitialized = errors.New("homepage video settings db not initialized")
|
||||
|
||||
type HomepageVideoEntry struct {
|
||||
DomainKey string
|
||||
VideoURL string
|
||||
PosterURL string
|
||||
}
|
||||
|
||||
type HomepageVideoSettings struct {
|
||||
DefaultEntry HomepageVideoEntry
|
||||
Overrides []HomepageVideoEntry
|
||||
}
|
||||
|
||||
func defaultHomepageVideoSettings() HomepageVideoSettings {
|
||||
return HomepageVideoSettings{
|
||||
DefaultEntry: HomepageVideoEntry{
|
||||
VideoURL: "https://www.youtube.com/watch?v=UW6DY6HQnmo",
|
||||
},
|
||||
Overrides: []HomepageVideoEntry{
|
||||
{
|
||||
DomainKey: "cn-www.svc.plus",
|
||||
VideoURL: "https://www.bilibili.com/video/BV12DwazxEkL/?spm_id_from=333.1387.homepage.video_card.click&vd_source=e14d146f9a815c7d11e1a1fc23565ffd",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func GetHomepageVideoSettings(ctx context.Context) (HomepageVideoSettings, error) {
|
||||
database := currentDB()
|
||||
if database == nil {
|
||||
return defaultHomepageVideoSettings(), nil
|
||||
}
|
||||
|
||||
var rows []model.HomepageVideoSetting
|
||||
if err := database.WithContext(ctx).Order("domain_key ASC").Find(&rows).Error; err != nil {
|
||||
return HomepageVideoSettings{}, err
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return defaultHomepageVideoSettings(), nil
|
||||
}
|
||||
|
||||
result := HomepageVideoSettings{}
|
||||
for _, row := range rows {
|
||||
entry := HomepageVideoEntry{
|
||||
DomainKey: normalizeHomepageVideoDomainKey(row.DomainKey),
|
||||
VideoURL: strings.TrimSpace(row.VideoURL),
|
||||
PosterURL: strings.TrimSpace(row.PosterURL),
|
||||
}
|
||||
if row.DomainKey == homepageVideoDefaultDomainKey {
|
||||
result.DefaultEntry = entry
|
||||
continue
|
||||
}
|
||||
result.Overrides = append(result.Overrides, entry)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(result.DefaultEntry.VideoURL) == "" {
|
||||
result.DefaultEntry = defaultHomepageVideoSettings().DefaultEntry
|
||||
}
|
||||
slices.SortFunc(result.Overrides, func(left, right HomepageVideoEntry) int {
|
||||
return strings.Compare(left.DomainKey, right.DomainKey)
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func SaveHomepageVideoSettings(
|
||||
ctx context.Context,
|
||||
settings HomepageVideoSettings,
|
||||
updatedBy string,
|
||||
) (HomepageVideoSettings, error) {
|
||||
database := currentDB()
|
||||
if database == nil {
|
||||
return HomepageVideoSettings{}, errHomepageVideoSettingsDBNotInitialized
|
||||
}
|
||||
|
||||
normalized, err := normalizeHomepageVideoSettings(settings)
|
||||
if err != nil {
|
||||
return HomepageVideoSettings{}, err
|
||||
}
|
||||
|
||||
rows := make([]model.HomepageVideoSetting, 0, 1+len(normalized.Overrides))
|
||||
rows = append(rows, model.HomepageVideoSetting{
|
||||
DomainKey: homepageVideoDefaultDomainKey,
|
||||
VideoURL: normalized.DefaultEntry.VideoURL,
|
||||
PosterURL: normalized.DefaultEntry.PosterURL,
|
||||
UpdatedBy: strings.TrimSpace(updatedBy),
|
||||
})
|
||||
|
||||
for _, item := range normalized.Overrides {
|
||||
rows = append(rows, model.HomepageVideoSetting{
|
||||
DomainKey: item.DomainKey,
|
||||
VideoURL: item.VideoURL,
|
||||
PosterURL: item.PosterURL,
|
||||
UpdatedBy: strings.TrimSpace(updatedBy),
|
||||
})
|
||||
}
|
||||
|
||||
if err := database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.HomepageVideoSetting{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&rows).Error
|
||||
}); err != nil {
|
||||
return HomepageVideoSettings{}, err
|
||||
}
|
||||
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func ResolveHomepageVideoEntry(ctx context.Context, host string) (HomepageVideoEntry, error) {
|
||||
settings, err := GetHomepageVideoSettings(ctx)
|
||||
if err != nil {
|
||||
return HomepageVideoEntry{}, err
|
||||
}
|
||||
|
||||
normalizedHost := store.NormalizeHostname(host)
|
||||
for _, item := range settings.Overrides {
|
||||
if item.DomainKey == normalizedHost {
|
||||
return item, nil
|
||||
}
|
||||
}
|
||||
return settings.DefaultEntry, nil
|
||||
}
|
||||
|
||||
func normalizeHomepageVideoSettings(settings HomepageVideoSettings) (HomepageVideoSettings, error) {
|
||||
defaultVideoURL := strings.TrimSpace(settings.DefaultEntry.VideoURL)
|
||||
if defaultVideoURL == "" {
|
||||
return HomepageVideoSettings{}, errors.New("default videoUrl is required")
|
||||
}
|
||||
|
||||
normalized := HomepageVideoSettings{
|
||||
DefaultEntry: HomepageVideoEntry{
|
||||
VideoURL: defaultVideoURL,
|
||||
PosterURL: strings.TrimSpace(settings.DefaultEntry.PosterURL),
|
||||
},
|
||||
}
|
||||
|
||||
seen := map[string]struct{}{}
|
||||
for _, item := range settings.Overrides {
|
||||
domainKey := normalizeHomepageVideoDomainKey(item.DomainKey)
|
||||
if domainKey == "" {
|
||||
return HomepageVideoSettings{}, errors.New("override domain is required")
|
||||
}
|
||||
if _, exists := seen[domainKey]; exists {
|
||||
return HomepageVideoSettings{}, errors.New("override domain must be unique")
|
||||
}
|
||||
videoURL := strings.TrimSpace(item.VideoURL)
|
||||
if videoURL == "" {
|
||||
return HomepageVideoSettings{}, errors.New("override videoUrl is required")
|
||||
}
|
||||
|
||||
seen[domainKey] = struct{}{}
|
||||
normalized.Overrides = append(normalized.Overrides, HomepageVideoEntry{
|
||||
DomainKey: domainKey,
|
||||
VideoURL: videoURL,
|
||||
PosterURL: strings.TrimSpace(item.PosterURL),
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(normalized.Overrides, func(left, right HomepageVideoEntry) int {
|
||||
return strings.Compare(left.DomainKey, right.DomainKey)
|
||||
})
|
||||
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func normalizeHomepageVideoDomainKey(value string) string {
|
||||
normalized := store.NormalizeHostname(value)
|
||||
if normalized == homepageVideoDefaultDomainKey {
|
||||
return ""
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user