Sync from /srv/compose/unified-media-manager

This commit is contained in:
Christopher Mayor
2026-04-24 10:45:19 -07:00
commit 7dbd00e537
132 changed files with 25394 additions and 0 deletions

197
internal/worker/rss_sync.go Normal file
View File

@@ -0,0 +1,197 @@
package worker
import (
"context"
"log/slog"
"time"
"github.com/TopherMayor/unified-media-manager/internal/config"
"github.com/TopherMayor/unified-media-manager/internal/db"
"github.com/TopherMayor/unified-media-manager/internal/service"
)
type RSSSyncWorker struct {
database *db.DB
mediaSvc *service.MediaService
searchSvc *service.SearchService
dcSvc *service.DownloadClientService
qualitySvc *service.QualityService
cfg *config.Config
}
func NewRSSSyncWorker(database *db.DB, mediaSvc *service.MediaService, searchSvc *service.SearchService, dcSvc *service.DownloadClientService, qualitySvc *service.QualityService, cfg *config.Config) *RSSSyncWorker {
return &RSSSyncWorker{
database: database,
mediaSvc: mediaSvc,
searchSvc: searchSvc,
dcSvc: dcSvc,
qualitySvc: qualitySvc,
cfg: cfg,
}
}
func (w *RSSSyncWorker) Name() string {
return "rss_sync"
}
func (w *RSSSyncWorker) CronExpr() string {
return w.cfg.WorkerRSSSyncInterval
}
func (w *RSSSyncWorker) Run(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
searched := 0
grabs := 0
errors := 0
seen := make(map[int64]bool)
missing, _, err := w.mediaSvc.SearchMissing(ctx, service.MediaFilters{PageSize: 100})
if err != nil {
slog.Error("failed to search missing media", "error", err)
errors++
}
for i := range missing {
seen[missing[i].ID] = true
}
upgrades, _, err := w.mediaSvc.SearchUpgrades(ctx, service.MediaFilters{PageSize: 100})
if err != nil {
slog.Error("failed to search upgrade media", "error", err)
errors++
}
for i := range upgrades {
seen[upgrades[i].ID] = true
}
var allItems []service.Media
for _, m := range missing {
allItems = append(allItems, m)
}
for _, m := range upgrades {
if !seen[m.ID] {
allItems = append(allItems, m)
seen[m.ID] = true
}
}
for _, item := range allItems {
var hasPending bool
err := w.database.Pool.QueryRow(ctx,
`SELECT EXISTS(SELECT 1 FROM download_queue WHERE media_id = $1 AND status IN ('pending', 'downloading'))`,
item.ID).Scan(&hasPending)
if err != nil {
slog.Error("failed to check pending queue", "media_id", item.ID, "error", err)
continue
}
if hasPending {
continue
}
_, err = w.database.Pool.Exec(ctx,
"UPDATE media SET last_search_at = NOW() WHERE id = $1", item.ID)
if err != nil {
slog.Error("failed to update last_search_at", "media_id", item.ID, "error", err)
}
results, err := w.searchSvc.Search(ctx, service.SearchRequest{
Query: item.Title,
MediaType: item.MediaType,
})
if err != nil {
slog.Error("failed to search indexers", "media_id", item.ID, "title", item.Title, "error", err)
errors++
searched++
continue
}
searched++
if len(results) == 0 {
continue
}
var filtered []service.SearchResult
for _, r := range results {
var blocked bool
err := w.database.Pool.QueryRow(ctx,
"SELECT EXISTS(SELECT 1 FROM blocklist WHERE release_title = $1)", r.Title).Scan(&blocked)
if err != nil {
slog.Error("failed to check blocklist", "error", err)
continue
}
if blocked {
continue
}
if item.QualityProfileID != nil {
profile, err := w.qualitySvc.GetByID(ctx, *item.QualityProfileID)
if err != nil {
slog.Error("failed to get quality profile", "profile_id", *item.QualityProfileID, "error", err)
filtered = append(filtered, r)
continue
}
if r.QualityTier != nil {
allowed := false
for _, name := range profile.AllowedQualities {
if name == r.QualityTier.Name {
allowed = true
break
}
}
if !allowed {
continue
}
}
}
filtered = append(filtered, r)
}
if len(filtered) == 0 {
continue
}
best := filtered[0]
for _, r := range filtered[1:] {
bestRank := 0
if best.QualityTier != nil {
bestRank = best.QualityTier.Rank
}
rank := 0
if r.QualityTier != nil {
rank = r.QualityTier.Rank
}
if rank > bestRank || (rank == bestRank && r.Size > best.Size) {
best = r
}
}
_, err = w.searchSvc.Grab(ctx, service.GrabRequest{
DownloadURL: best.DownloadURL,
Title: best.Title,
MediaType: item.MediaType,
Quality: best.Quality,
IndexerName: best.IndexerName,
MediaID: item.ID,
}, w.dcSvc)
if err != nil {
slog.Error("failed to auto-grab release", "media_id", item.ID, "release", best.Title, "error", err)
errors++
continue
}
status := "searching"
if err := w.mediaSvc.Update(ctx, item.ID, item.MediaType, service.UpdateMediaRequest{Status: &status}); err != nil {
slog.Error("failed to update media status", "media_id", item.ID, "error", err)
}
slog.Info("auto-grabbed release", "media", item.Title, "release", best.Title, "quality", best.QualityTier.Name)
grabs++
}
slog.Info("rss sync completed", "searched", searched, "grabs", grabs, "errors", errors)
return nil
}