mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-05 04:56:37 +08:00
feat: port remaining Vue UI surfaces with full backend support
Backend additions:
- New GORM models: NotifyChannel, PlayProfile (with PIN hashing).
- NotifyChannelService: multi-channel CRUD + unified dispatcher
supporting Telegram / Bark / WeChat / Webhook with optional
per-channel event filtering.
- PlayProfileService: per-user 'viewing personas' with
content-rating gates, library scoping, PIN protection, and
player defaults (autoplay, skip-intro, audio/subtitle prefs).
- New endpoints:
GET /admin/notify/channels list
POST /admin/notify/channels create
PUT /admin/notify/channels/:id update
DELETE /admin/notify/channels/:id delete
POST /admin/notify/channels/:id/test send test
GET /play-profiles list (admin: ?all=true)
POST /play-profiles create
PUT /play-profiles/:id update
DELETE /play-profiles/:id delete
GET /media/recent home page rail
GET /media/stats library composition
GET /watch-history list
GET /watch-history/stats aggregate
GET /watch-history/continue continue-watching rail
DELETE /watch-history[?media_id=] clear
DELETE /watch-history/:id remove one
GET /discover/sections available rails
GET /discover/feed?sections=a,b multi-section TMDb fetch
GET /system/info name/version/runtime
GET /system/status uptime/cpu/mem/disk
GET /system/scheduler read-only scheduler view
GET /stats/overview counts only
GET /stats/trend daily plays
GET /stats/top-content most played
GET /stats/libraries per-library size
GET /stats/monitor live hardware
Frontend additions:
- New API helpers: notify_channels, play_profiles, history,
stats_extra, media_extra, discover_extra, system.
- ProfileManagementPage (/play-profiles): full CRUD form with
PIN management, library scoping, language preferences.
- NotifyChannelsPage (/notify-channels, admin): typed config
forms per channel type + per-channel test action.
- SettingsPage (/settings, admin): grouped key/value editor
covering General, Organize/Scrape, Adult, qBittorrent.
- Layout sidebar links and App.tsx routes wired for all three.
This commit is contained in:
@@ -30,23 +30,27 @@ type Container struct {
|
||||
Subscription *SubscriptionRepository
|
||||
Setting *SettingRepository
|
||||
Log *AccessLogRepository
|
||||
NotifyChannel *NotifyChannelRepository
|
||||
PlayProfile *PlayProfileRepository
|
||||
}
|
||||
|
||||
// New wires every repository to a single *gorm.DB.
|
||||
func New(db *gorm.DB) *Container {
|
||||
return &Container{
|
||||
DB: db,
|
||||
User: &UserRepository{db: db},
|
||||
Library: &LibraryRepository{db: db},
|
||||
Media: &MediaRepository{db: db},
|
||||
Series: &SeriesRepository{db: db},
|
||||
History: &HistoryRepository{db: db},
|
||||
Favorite: &FavoriteRepository{db: db},
|
||||
Playlist: &PlaylistRepository{db: db},
|
||||
Download: &DownloadRepository{db: db},
|
||||
Subscription: &SubscriptionRepository{db: db},
|
||||
Setting: &SettingRepository{db: db},
|
||||
Log: &AccessLogRepository{db: db},
|
||||
DB: db,
|
||||
User: &UserRepository{db: db},
|
||||
Library: &LibraryRepository{db: db},
|
||||
Media: &MediaRepository{db: db},
|
||||
Series: &SeriesRepository{db: db},
|
||||
History: &HistoryRepository{db: db},
|
||||
Favorite: &FavoriteRepository{db: db},
|
||||
Playlist: &PlaylistRepository{db: db},
|
||||
Download: &DownloadRepository{db: db},
|
||||
Subscription: &SubscriptionRepository{db: db},
|
||||
Setting: &SettingRepository{db: db},
|
||||
Log: &AccessLogRepository{db: db},
|
||||
NotifyChannel: &NotifyChannelRepository{db: db},
|
||||
PlayProfile: &PlayProfileRepository{db: db},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,3 +393,111 @@ func (r *AccessLogRepository) Recent(ctx context.Context, limit int) ([]model.Ac
|
||||
err := r.db.WithContext(ctx).Order("created_at desc").Limit(limit).Find(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
|
||||
// ─── Notify Channel ──────────────────────────────────────────────────────────
|
||||
|
||||
// NotifyChannelRepository persists model.NotifyChannel records.
|
||||
type NotifyChannelRepository struct{ db *gorm.DB }
|
||||
|
||||
// Create inserts a new notify channel.
|
||||
func (r *NotifyChannelRepository) Create(ctx context.Context, n *model.NotifyChannel) error {
|
||||
return r.db.WithContext(ctx).Create(n).Error
|
||||
}
|
||||
|
||||
// FindByID returns the channel or (nil, nil).
|
||||
func (r *NotifyChannelRepository) FindByID(ctx context.Context, id string) (*model.NotifyChannel, error) {
|
||||
var n model.NotifyChannel
|
||||
err := r.db.WithContext(ctx).Where("id = ?", id).First(&n).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
// List returns every channel ordered by creation time desc.
|
||||
func (r *NotifyChannelRepository) List(ctx context.Context) ([]model.NotifyChannel, error) {
|
||||
var rows []model.NotifyChannel
|
||||
err := r.db.WithContext(ctx).Order("created_at desc").Find(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// ListEnabled is the variant the dispatcher uses; honours Enabled flag.
|
||||
func (r *NotifyChannelRepository) ListEnabled(ctx context.Context) ([]model.NotifyChannel, error) {
|
||||
var rows []model.NotifyChannel
|
||||
err := r.db.WithContext(ctx).Where("enabled = ?", true).
|
||||
Order("created_at desc").Find(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// Update applies a partial patch addressed by ID. The map keys must use
|
||||
// snake_case GORM column names.
|
||||
func (r *NotifyChannelRepository) Update(ctx context.Context, id string, patch map[string]any) error {
|
||||
return r.db.WithContext(ctx).Model(&model.NotifyChannel{}).
|
||||
Where("id = ?", id).Updates(patch).Error
|
||||
}
|
||||
|
||||
// Delete soft-deletes a channel.
|
||||
func (r *NotifyChannelRepository) Delete(ctx context.Context, id string) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.NotifyChannel{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
// ─── Play Profile ────────────────────────────────────────────────────────────
|
||||
|
||||
// PlayProfileRepository persists model.PlayProfile records.
|
||||
type PlayProfileRepository struct{ db *gorm.DB }
|
||||
|
||||
// Create inserts a new play profile.
|
||||
func (r *PlayProfileRepository) Create(ctx context.Context, p *model.PlayProfile) error {
|
||||
return r.db.WithContext(ctx).Create(p).Error
|
||||
}
|
||||
|
||||
// FindByID returns the profile or (nil, nil).
|
||||
func (r *PlayProfileRepository) FindByID(ctx context.Context, id string) (*model.PlayProfile, error) {
|
||||
var p model.PlayProfile
|
||||
err := r.db.WithContext(ctx).Where("id = ?", id).First(&p).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// ListByUser returns every profile owned by a user.
|
||||
func (r *PlayProfileRepository) ListByUser(ctx context.Context, userID string) ([]model.PlayProfile, error) {
|
||||
var rows []model.PlayProfile
|
||||
err := r.db.WithContext(ctx).Where("user_id = ?", userID).
|
||||
Order("is_default desc, created_at asc").Find(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// List returns every profile across users (admin view).
|
||||
func (r *PlayProfileRepository) List(ctx context.Context) ([]model.PlayProfile, error) {
|
||||
var rows []model.PlayProfile
|
||||
err := r.db.WithContext(ctx).
|
||||
Order("user_id asc, is_default desc, created_at asc").Find(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// ClearDefaultsFor flips all is_default flags to false for the given
|
||||
// user; called inside the same transaction that promotes a new default.
|
||||
func (r *PlayProfileRepository) ClearDefaultsFor(ctx context.Context, userID string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.PlayProfile{}).
|
||||
Where("user_id = ?", userID).Update("is_default", false).Error
|
||||
}
|
||||
|
||||
// Update applies a partial patch addressed by ID.
|
||||
func (r *PlayProfileRepository) Update(ctx context.Context, id string, patch map[string]any) error {
|
||||
return r.db.WithContext(ctx).Model(&model.PlayProfile{}).
|
||||
Where("id = ?", id).Updates(patch).Error
|
||||
}
|
||||
|
||||
// Delete soft-deletes a profile.
|
||||
func (r *PlayProfileRepository) Delete(ctx context.Context, id string) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.PlayProfile{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user