Files
silo-server/internal/historyimport/plex_provider.go
T
QuickandClaude Fable 5 9ead29bfcc fix(historyimport): make Plex watchlist import actually work
The browser Plex OAuth flow only sent the PMS server access token, which
the discover API rejects (401), so the watchlist step always failed with
a buried warning. The web client now forwards the plex.tv account token
via a new additive plex_account_token field.

The discover watchlist listing also ignores includeGuids, so items
arrived without external ids and could only exact-title/year match.
FetchWatchlist now resolves ids per item from the discover metadata
endpoint, decoding both Metadata- and Video-keyed containers, and
degrades to a title/year fallback warning instead of dropping items.

Part of #245

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 13:11:12 -04:00

125 lines
3.7 KiB
Go

package historyimport
import (
"context"
"fmt"
"log/slog"
)
type PlexServerProvider struct {
client *PlexClient
baseURL string
token string
accountToken string
}
func NewPlexServerProvider(client *PlexClient, baseURL, token string) *PlexServerProvider {
return &PlexServerProvider{client: client, baseURL: baseURL, token: token}
}
// WithAccountToken enables account-level fetches (the watchlist). Empty
// disables them: server-token-only imports still work, minus the watchlist.
func (p *PlexServerProvider) WithAccountToken(token string) *PlexServerProvider {
p.accountToken = token
return p
}
func (p *PlexServerProvider) Fetch(ctx context.Context) ([]Record, []string, error) {
sections, err := p.client.FetchLibrarySections(ctx, p.baseURL, p.token)
if err != nil {
return nil, nil, err
}
var allItems []PlexItem
var warnings []string
for _, section := range sections {
switch section.Type {
case "movie":
items, err := p.client.FetchWatchedItems(ctx, p.baseURL, p.token, section.Key, 1)
if err != nil {
warnings = append(warnings, fmt.Sprintf("failed to fetch movies from section %q: %v", section.Title, err))
continue
}
allItems = append(allItems, items...)
case "show":
items, err := p.client.FetchWatchedItems(ctx, p.baseURL, p.token, section.Key, 4)
if err != nil {
warnings = append(warnings, fmt.Sprintf("failed to fetch episodes from section %q: %v", section.Title, err))
continue
}
allItems = append(allItems, items...)
}
}
onDeck, err := p.client.FetchOnDeck(ctx, p.baseURL, p.token)
if err != nil {
warnings = append(warnings, fmt.Sprintf("failed to fetch on-deck items: %v", err))
} else {
allItems = append(allItems, onDeck...)
}
seriesMeta := p.fetchSeriesMetadata(ctx, allItems, &warnings)
merged := make(map[string]Record, len(allItems))
for _, item := range allItems {
record := NormalizePlexItem(item, seriesMeta[item.GrandparentRatingKey])
existing, ok := merged[record.ExternalID]
if !ok {
merged[record.ExternalID] = record
continue
}
merged[record.ExternalID] = mergeRecords(existing, record)
}
records := make([]Record, 0, len(merged))
for _, record := range merged {
records = append(records, record)
}
// The account watchlist rides along with the history import. Best
// effort: a watchlist fetch failure downgrades to a warning so the
// watch-history import still completes (issue #245).
if p.accountToken != "" {
items, watchlistWarnings, err := p.client.FetchWatchlist(ctx, p.accountToken)
if err != nil {
warnings = append(warnings, fmt.Sprintf("watchlist fetch failed: %v", err))
} else {
warnings = append(warnings, watchlistWarnings...)
for _, item := range items {
records = append(records, NormalizePlexWatchlistItem(item))
}
}
}
return records, warnings, nil
}
func (p *PlexServerProvider) fetchSeriesMetadata(ctx context.Context, items []PlexItem, warnings *[]string) map[string]*PlexItem {
seen := make(map[string]struct{})
var seriesKeys []string
for _, item := range items {
if item.Type != "episode" || item.GrandparentRatingKey == "" {
continue
}
if _, ok := seen[item.GrandparentRatingKey]; ok {
continue
}
seen[item.GrandparentRatingKey] = struct{}{}
seriesKeys = append(seriesKeys, item.GrandparentRatingKey)
}
result := make(map[string]*PlexItem, len(seriesKeys))
for _, key := range seriesKeys {
meta, err := p.client.FetchMetadata(ctx, p.baseURL, p.token, key)
if err != nil {
slog.Warn("plex history import: failed to fetch series metadata", "rating_key", key, "error", err)
*warnings = append(*warnings, fmt.Sprintf("failed to fetch series metadata for %s: %v", key, err))
continue
}
if meta != nil {
result[key] = meta
}
}
return result
}