mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
778bf77437
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.
143 lines
3.8 KiB
Go
143 lines
3.8 KiB
Go
// 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)
|
|
}
|