mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-04 12:36:37 +08:00
feat: backup, notifier, organizer, douban, watch-history, poster-wall
Complete the port of the remaining MediaStation features:
Backend services:
- service/backup.go: SQLite hot backup via VACUUM INTO, list, delete,
restore. Backup files stored under {data_dir}/backups/ with timestamps.
- service/notifier.go: Multi-channel push notifications (Telegram /
Bark / WeChat Server酱 / Webhook). Configuration via settings table
keys notify.telegram.*, notify.bark.*, etc.
- service/organizer.go: Auto-rename + move media files into library
directory structure. Movie: {Title} ({Year})/{Title} ({Year}).ext,
TV: {Title}/Season XX/{Title} - SxxExx.ext. Both per-media and
per-library batch endpoints.
- service/douban.go: Douban (豆瓣) metadata provider using the
unofficial subject_suggest API. Returns Chinese titles + posters.
Requires douban_cookie in secrets config.
Handlers:
- handler/backup.go: GET/POST/DELETE /admin/backups, POST restore.
- handler/organizer.go: POST /admin/media/:id/organize,
POST /admin/libraries/:id/organize.
- handler/notify.go: POST /admin/notify/test.
Frontend pages:
- WatchHistoryPage.tsx: full paginated history with progress bars,
resume buttons, poster thumbnails and timestamps.
- PosterWallPage.tsx: dense 8-column poster grid of all media
(matches the original MediaStation PosterWallView.vue).
Sidebar additions: 观看历史 + 海报墙 links in the main nav group.
Verified: go build, go vet, go test all pass; tsc -b && vite build
emits 30 lazy chunks; main bundle 254 KB / 85 KB gzipped.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
// Package service — multi-channel push notifications.
|
||||
//
|
||||
// NotifierService dispatches structured messages to one or more channels
|
||||
// configured in the system settings table:
|
||||
//
|
||||
// notify.telegram.bot_token + notify.telegram.chat_id
|
||||
// notify.bark.server + notify.bark.key
|
||||
// notify.wechat.sendkey
|
||||
// notify.webhook.url + notify.webhook.method
|
||||
//
|
||||
// Notifications are triggered by the subscription poller, the download
|
||||
// poller, the scan / scrape completions, and any future event worth
|
||||
// surfacing to the operator's phone.
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
||||
)
|
||||
|
||||
// NotifierService dispatches push notifications.
|
||||
type NotifierService struct {
|
||||
log *zap.Logger
|
||||
repo *repository.Container
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewNotifierService is the constructor.
|
||||
func NewNotifierService(log *zap.Logger, repo *repository.Container) *NotifierService {
|
||||
return &NotifierService{
|
||||
log: log,
|
||||
repo: repo,
|
||||
client: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// Send dispatches a notification to every configured channel. Failures
|
||||
// are logged but do not propagate — notifications are best-effort.
|
||||
func (n *NotifierService) Send(ctx context.Context, title, body, eventType string) {
|
||||
n.sendTelegram(ctx, title, body)
|
||||
n.sendBark(ctx, title, body)
|
||||
n.sendWechat(ctx, title, body)
|
||||
n.sendWebhook(ctx, title, body, eventType)
|
||||
}
|
||||
|
||||
func (n *NotifierService) get(ctx context.Context, key string) string {
|
||||
v, _ := n.repo.Setting.Get(ctx, key)
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
|
||||
func (n *NotifierService) sendTelegram(ctx context.Context, title, body string) {
|
||||
token := n.get(ctx, "notify.telegram.bot_token")
|
||||
chatID := n.get(ctx, "notify.telegram.chat_id")
|
||||
if token == "" || chatID == "" {
|
||||
return
|
||||
}
|
||||
text := fmt.Sprintf("<b>%s</b>\n\n%s", escapeHTML(title), escapeHTML(body))
|
||||
u := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", token)
|
||||
form := url.Values{}
|
||||
form.Set("chat_id", chatID)
|
||||
form.Set("text", text)
|
||||
form.Set("parse_mode", "HTML")
|
||||
resp, err := n.client.PostForm(u, form)
|
||||
if err != nil {
|
||||
n.log.Debug("telegram notify failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
func (n *NotifierService) sendBark(ctx context.Context, title, body string) {
|
||||
server := n.get(ctx, "notify.bark.server")
|
||||
key := n.get(ctx, "notify.bark.key")
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
if server == "" {
|
||||
server = "https://api.day.app"
|
||||
}
|
||||
u := fmt.Sprintf("%s/%s/%s/%s",
|
||||
strings.TrimRight(server, "/"),
|
||||
url.PathEscape(key),
|
||||
url.PathEscape(title),
|
||||
url.PathEscape(body),
|
||||
)
|
||||
resp, err := n.client.Get(u)
|
||||
if err != nil {
|
||||
n.log.Debug("bark notify failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
func (n *NotifierService) sendWechat(ctx context.Context, title, body string) {
|
||||
sendkey := n.get(ctx, "notify.wechat.sendkey")
|
||||
if sendkey == "" {
|
||||
return
|
||||
}
|
||||
u := fmt.Sprintf("https://sctapi.ftqq.com/%s.send", sendkey)
|
||||
form := url.Values{}
|
||||
form.Set("title", title)
|
||||
form.Set("desp", body)
|
||||
resp, err := n.client.PostForm(u, form)
|
||||
if err != nil {
|
||||
n.log.Debug("wechat notify failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
func (n *NotifierService) sendWebhook(ctx context.Context, title, body, eventType string) {
|
||||
webhookURL := n.get(ctx, "notify.webhook.url")
|
||||
if webhookURL == "" {
|
||||
return
|
||||
}
|
||||
payload := fmt.Sprintf(`{"title":%q,"content":%q,"type":%q}`, title, body, eventType)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, webhookURL,
|
||||
strings.NewReader(payload))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := n.client.Do(req)
|
||||
if err != nil {
|
||||
n.log.Debug("webhook notify failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
func escapeHTML(s string) string {
|
||||
r := strings.NewReplacer("&", "&", "<", "<", ">", ">")
|
||||
return r.Replace(s)
|
||||
}
|
||||
Reference in New Issue
Block a user