mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 19:36:36 +08:00
7095d9ebec
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.
163 lines
4.8 KiB
Go
163 lines
4.8 KiB
Go
// Package handler — watch history endpoints.
|
|
//
|
|
// The base /history GET / POST routes already exist; these add the three
|
|
// auxiliary surfaces the React WatchHistoryPage needs:
|
|
//
|
|
// GET /api/watch-history paginated list (admin sees every user)
|
|
// GET /api/watch-history/stats aggregate watch time + completion
|
|
// GET /api/watch-history/continue resume rail (incomplete only)
|
|
// DELETE /api/watch-history clear (?media_item_id= optional)
|
|
// DELETE /api/watch-history/:id remove one row
|
|
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
"github.com/ShukeBta/MediaStationGo/internal/service"
|
|
)
|
|
|
|
// historyListHandler returns the caller's history rows joined with the
|
|
// matching media in a single response.
|
|
func historyListHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
uid, _ := c.Get(middleware.CtxUserID)
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
|
|
if limit <= 0 || limit > 500 {
|
|
limit = 50
|
|
}
|
|
items, err := svc.Playback.RecentHistory(c.Request.Context(), toString(uid), limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, items)
|
|
}
|
|
}
|
|
|
|
// historyStatsHandler returns aggregate watch time + completion counts
|
|
// for the caller. Used by the WatchHistoryPage hero card.
|
|
func historyStatsHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
uid, _ := c.Get(middleware.CtxUserID)
|
|
userID := toString(uid)
|
|
|
|
var total int64
|
|
_ = svc.Repo.DB.Model(&model.PlaybackHistory{}).
|
|
Where("user_id = ?", userID).Count(&total).Error
|
|
|
|
var completed int64
|
|
_ = svc.Repo.DB.Model(&model.PlaybackHistory{}).
|
|
Where("user_id = ? AND completed = ?", userID, true).Count(&completed).Error
|
|
|
|
var watchedMs int64
|
|
_ = svc.Repo.DB.Model(&model.PlaybackHistory{}).
|
|
Where("user_id = ?", userID).
|
|
Select("COALESCE(SUM(position_ms), 0)").
|
|
Row().Scan(&watchedMs)
|
|
|
|
var last *time.Time
|
|
row := svc.Repo.DB.Model(&model.PlaybackHistory{}).
|
|
Where("user_id = ?", userID).
|
|
Select("MAX(watched_at)").Row()
|
|
var lastT time.Time
|
|
if err := row.Scan(&lastT); err == nil && !lastT.IsZero() {
|
|
last = &lastT
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"total": total,
|
|
"completed": completed,
|
|
"watched_ms": watchedMs,
|
|
"watched_hours": float64(watchedMs) / 1000.0 / 3600.0,
|
|
"last_watched": last,
|
|
})
|
|
}
|
|
}
|
|
|
|
// historyContinueHandler returns "Continue Watching" rows: incomplete
|
|
// items, most recent first.
|
|
func historyContinueHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
uid, _ := c.Get(middleware.CtxUserID)
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
if limit <= 0 || limit > 50 {
|
|
limit = 10
|
|
}
|
|
var rows []model.PlaybackHistory
|
|
if err := svc.Repo.DB.
|
|
Where("user_id = ? AND completed = ?", toString(uid), false).
|
|
Order("watched_at desc").
|
|
Limit(limit).
|
|
Find(&rows).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
// Hydrate media in one query.
|
|
ids := make([]string, 0, len(rows))
|
|
for _, r := range rows {
|
|
ids = append(ids, r.MediaID)
|
|
}
|
|
var media []model.Media
|
|
if len(ids) > 0 {
|
|
_ = svc.Repo.DB.Where("id IN ?", ids).Find(&media).Error
|
|
}
|
|
mIdx := make(map[string]model.Media, len(media))
|
|
for _, m := range media {
|
|
mIdx[m.ID] = m
|
|
}
|
|
out := make([]gin.H, 0, len(rows))
|
|
for _, r := range rows {
|
|
m, ok := mIdx[r.MediaID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
out = append(out, gin.H{
|
|
"history": r,
|
|
"media": m,
|
|
})
|
|
}
|
|
c.JSON(http.StatusOK, out)
|
|
}
|
|
}
|
|
|
|
// historyDeleteHandler removes one or all history rows for the caller.
|
|
//
|
|
// DELETE /api/watch-history?media_id=xxx → delete just that media's row
|
|
// DELETE /api/watch-history → clear all rows for the user
|
|
func historyDeleteHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
uid, _ := c.Get(middleware.CtxUserID)
|
|
userID := toString(uid)
|
|
mediaID := c.Query("media_id")
|
|
|
|
q := svc.Repo.DB.Where("user_id = ?", userID)
|
|
if mediaID != "" {
|
|
q = q.Where("media_id = ?", mediaID)
|
|
}
|
|
if err := q.Delete(&model.PlaybackHistory{}).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func historyDeleteOneHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
uid, _ := c.Get(middleware.CtxUserID)
|
|
if err := svc.Repo.DB.
|
|
Where("user_id = ? AND id = ?", toString(uid), c.Param("id")).
|
|
Delete(&model.PlaybackHistory{}).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
}
|