mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-01 03:56:38 +08:00
7bd6bcfb1b
Backend
- service/bangumi.go: Bangumi (bgm.tv) scraper for anime libraries.
- service/episode_parser.go: SxxExx / NxE / EPxx / 第NN集 parser with
unit tests; consumed by the scanner for tv/anime libraries.
- service/scraper.go: provider chain orchestrator picks Bangumi for
anime libraries (TMDb fallback), TMDb for everything else; emits
'no_match' rows so we don't retry forever; AnyEnabled() for the
scanner kick.
- service/scanner.go: writes season/episode numbers for tv/anime libs
and reports a 'probed' counter alongside 'added'.
- service/transcoder.go (existing): unchanged, stays per-media.
- service/subtitle.go: discovers external subtitles next to the source
file (.srt/.vtt/.ass/.ssa) and converts SRT/ASS to WebVTT on the fly.
- service/qbittorrent.go: thread-safe qBittorrent v2 Web UI client
(login / add / list / delete) with cookie-jar reuse.
- service/downloads.go: persists download tasks, reads runtime
qbittorrent.* settings, polls /torrents/info every 5 s and pushes
the result to WS subscribers ('download' topic).
- service/subscription.go: 10-minute RSS poller with regex filter,
GUID dedup persisted in the settings table, and 'subscription' WS
events on enqueue.
- service/watcher.go: fsnotify watcher with 5 s coalescing debouncer
that triggers per-library rescans on create/rename/remove.
- service/stats.go: dashboard snapshot — totals, recently added,
gopsutil-driven CPU/mem/disk readings.
- service/profile.go: non-credential profile patch + admin role mutator.
- service/audit.go: best-effort writer for the access_logs table.
- service/service.go: container wires every new service; Boot() spins
up watcher / downloads poller / subscription scheduler; Close()
tears them down on graceful shutdown.
Handlers
- new files: downloads.go, subscriptions.go, subtitles.go, series.go,
stats.go, profile.go, util.go.
- handler.go: registers PATCH /me, /libraries/:id/seasons,
/media/:id/subtitles, /subtitles/:id, /downloads*, /subscriptions*,
/stats, /admin/users/:id/role.
- auth.go / media.go: write audit rows for login + library CRUD and
refresh the watcher when libraries change.
Frontend
- api: new helpers for downloads, subscriptions, profile, series, stats,
subtitles; library helper gained scrape().
- hooks/useWebSocket.ts: shared connection with 3 s reconnect.
- components/GlobalEvents.tsx: surfaces scan / scrape / subscription
completion as toasts (mounted at app root).
- pages: Library now switches to a season-grouped layout for tv/anime
libraries; Player attaches WebVTT <track> elements; new pages for
Downloads (live torrent table), Subscriptions, Profile, Stats.
- components/Layout.tsx + App.tsx: sidebar groups (媒体库 / 自动化 /
账号 / 管理) and routes for the new pages; /stats and /admin remain
admin-only.
- types/index.ts: new types — Subscription, DownloadTask, QBitTorrent,
Hardware, StatsSnapshot.
Verified: go build, go vet, go test (incl. ParseEpisode + srtToVTT +
stripASSTags) all pass; tsc -b && vite build emits 17 route chunks plus
the deferred hls chunk; main bundle 247 KB / 83 KB gzipped.
149 lines
3.9 KiB
Go
149 lines
3.9 KiB
Go
// Package service — download manager.
|
|
//
|
|
// DownloadService persists user-initiated downloads, dispatches them to
|
|
// the configured client (currently qBittorrent) and pushes live progress
|
|
// to the WS hub so the React UI can render a live table.
|
|
//
|
|
// Settings consumed (system Setting table):
|
|
//
|
|
// qbittorrent.url e.g. http://127.0.0.1:8080
|
|
// qbittorrent.username qBittorrent WebUI user
|
|
// qbittorrent.password qBittorrent WebUI password
|
|
// qbittorrent.savepath optional default save dir
|
|
//
|
|
// Settings can be updated at runtime via the admin UI; ReloadConfig()
|
|
// re-reads them and re-authenticates.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
|
)
|
|
|
|
// DownloadService is the single download orchestrator.
|
|
type DownloadService struct {
|
|
log *zap.Logger
|
|
repo *repository.Container
|
|
hub *Hub
|
|
qb *QBitClient
|
|
|
|
mu sync.Mutex
|
|
stopCh chan struct{}
|
|
pollOnce sync.Once
|
|
}
|
|
|
|
// NewDownloadService is the constructor.
|
|
func NewDownloadService(log *zap.Logger, repo *repository.Container, hub *Hub) *DownloadService {
|
|
return &DownloadService{
|
|
log: log,
|
|
repo: repo,
|
|
hub: hub,
|
|
qb: NewQBitClient(log, QBitConfig{}),
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start kicks off the background poller (idempotent).
|
|
func (d *DownloadService) Start(ctx context.Context) {
|
|
d.pollOnce.Do(func() {
|
|
_ = d.ReloadConfig(ctx)
|
|
go d.poll(ctx)
|
|
})
|
|
}
|
|
|
|
// Stop terminates the poller.
|
|
func (d *DownloadService) Stop() {
|
|
close(d.stopCh)
|
|
}
|
|
|
|
// ReloadConfig rebuilds the qBittorrent client from the system settings.
|
|
func (d *DownloadService) ReloadConfig(ctx context.Context) error {
|
|
cfg := QBitConfig{}
|
|
for _, key := range []struct{ from, into *string }{} {
|
|
_ = key
|
|
}
|
|
get := func(k string) string {
|
|
v, _ := d.repo.Setting.Get(ctx, k)
|
|
return v
|
|
}
|
|
cfg.BaseURL = get("qbittorrent.url")
|
|
cfg.Username = get("qbittorrent.username")
|
|
cfg.Password = get("qbittorrent.password")
|
|
d.qb.Configure(cfg)
|
|
return nil
|
|
}
|
|
|
|
// AddDownload accepts a magnet URL / HTTP URL and persists a tracking row.
|
|
func (d *DownloadService) AddDownload(ctx context.Context, userID, urlStr, savePath string) (*model.DownloadTask, error) {
|
|
if urlStr == "" {
|
|
return nil, errors.New("empty url")
|
|
}
|
|
if savePath == "" {
|
|
savePath, _ = d.repo.Setting.Get(ctx, "qbittorrent.savepath")
|
|
}
|
|
if err := d.qb.AddTorrent(ctx, urlStr, savePath); err != nil {
|
|
return nil, err
|
|
}
|
|
t := &model.DownloadTask{
|
|
UserID: userID,
|
|
Source: "qbittorrent",
|
|
URL: urlStr,
|
|
SavePath: savePath,
|
|
Status: "queued",
|
|
}
|
|
if err := d.repo.Download.Create(ctx, t); err != nil {
|
|
return nil, err
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// List returns every persisted download task augmented with live data
|
|
// from qBittorrent when available.
|
|
func (d *DownloadService) List(ctx context.Context) ([]model.DownloadTask, []QBitTorrent, error) {
|
|
rows, err := d.repo.Download.List(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
live, err := d.qb.List(ctx, "")
|
|
if err != nil {
|
|
// Network failure shouldn't break the page — return rows with no
|
|
// live data and let the UI render the persisted snapshot.
|
|
d.log.Debug("qbittorrent list failed", zap.Error(err))
|
|
return rows, nil, nil
|
|
}
|
|
return rows, live, nil
|
|
}
|
|
|
|
// Delete removes a torrent (and optionally its files) from qBittorrent.
|
|
func (d *DownloadService) Delete(ctx context.Context, hash string, withFiles bool) error {
|
|
return d.qb.Delete(ctx, hash, withFiles)
|
|
}
|
|
|
|
// poll fans out qBittorrent /torrents/info every 5 s as WS events. The
|
|
// payload is opaque to the client; the React store merges by hash.
|
|
func (d *DownloadService) poll(ctx context.Context) {
|
|
t := time.NewTicker(5 * time.Second)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-d.stopCh:
|
|
return
|
|
case <-t.C:
|
|
}
|
|
live, err := d.qb.List(ctx, "")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
d.hub.Publish("download", map[string]any{"torrents": live})
|
|
}
|
|
}
|