Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a667846
fix(watchsync): classify Trakt and Simkl rate limits and pace writes
Quick104 Sep 23, 2026
d53738b
fix(watchsync): page every Trakt list read
Quick104 Sep 23, 2026
de0dfde
fix(watchsync): keep the MDBList API key out of error text
Quick104 Sep 23, 2026
7fd6898
fix(watchsync): match Trakt not_found echoes by any shared id
Quick104 Sep 23, 2026
58c1636
feat(watchsync): sync movie and series ratings with watch providers
Quick104 Sep 23, 2026
ea3c3fb
feat(watchsync): sync Trakt movie and show ratings
Quick104 Sep 23, 2026
0aac5d0
chore(deps): require the plugin SDK rating contract
Quick104 Sep 23, 2026
f9fce03
feat(watchsync): sync ratings with watch-sync plugins
Quick104 Sep 23, 2026
09c290c
fix(apiv2): answer provider rate limits with 429 and Retry-After
Quick104 Sep 24, 2026
b96821a
fix(watchsync): read multi-page Trakt listings twice
Quick104 Sep 24, 2026
c38658b
fix(watchsync): keep rating flags off v1 and fence rating imports
Quick104 Sep 24, 2026
8f56199
fix(watchsync): treat plugin rows without a rating as unreadable
Quick104 Sep 24, 2026
7e86836
Merge branch 'fix/watchsync-provider-rate-limits' into fix/watchsync-…
Quick104 Sep 24, 2026
7974d22
Merge branch 'fix/watchsync-mdblist-key-redaction' into fix/watchsync…
Quick104 Sep 24, 2026
c182ac7
Merge branch 'fix/watchsync-trakt-not-found-ids' into feat/watchsync-…
Quick104 Sep 24, 2026
4657805
Merge branch 'fix/watchsync-trakt-pagination' into fix/watchsync-trak…
Quick104 Sep 24, 2026
3162413
Merge branch 'feat/watchsync-rating-sync' into feat/watchsync-plugin-…
Quick104 Sep 24, 2026
97edaf9
fix(watchsync): pace paged Trakt reads within the GET budget
Quick104 Sep 24, 2026
81c69b1
Merge branch 'fix/watchsync-trakt-not-found-ids' into feat/watchsync-…
Quick104 Sep 24, 2026
b2cb506
Merge branch 'fix/watchsync-trakt-pagination' into fix/watchsync-trak…
Quick104 Sep 24, 2026
9617d70
Merge branch 'feat/watchsync-rating-sync' into feat/watchsync-plugin-…
Quick104 Sep 24, 2026
9555767
fix(watchsync): defer limiter refusals and Trakt device-code 429s
Quick104 Sep 24, 2026
09776ed
fix(watchsync): keep error classification when masking the MDBList key
Quick104 Sep 24, 2026
066263c
fix(watchsync): update rating cursors in place and resend ratings cha…
Quick104 Sep 24, 2026
4a32d98
Merge branch 'fix/watchsync-provider-rate-limits' into fix/watchsync-…
Quick104 Sep 24, 2026
b62b2df
Merge branch 'fix/watchsync-mdblist-key-redaction' into fix/watchsync…
Quick104 Sep 24, 2026
82f4cb4
Merge branch 'fix/watchsync-trakt-pagination' into fix/watchsync-trak…
Quick104 Sep 24, 2026
cbe73ef
Merge branch 'fix/watchsync-trakt-not-found-ids' into feat/watchsync-…
Quick104 Sep 24, 2026
3bc0d5a
Merge branch 'feat/watchsync-rating-sync' into feat/watchsync-plugin-…
Quick104 Sep 24, 2026
21eb44c
fix(watchsync): fence agreed ratings on the bound account and resend …
Quick104 Sep 24, 2026
fc32cfc
fix(watchsync): share the Trakt page budget across one account's tokens
Quick104 Sep 24, 2026
0cbd584
Merge branch 'fix/watchsync-trakt-pagination' into fix/watchsync-trak…
Quick104 Sep 24, 2026
e2794c8
Merge branch 'fix/watchsync-trakt-not-found-ids' into feat/watchsync-…
Quick104 Sep 24, 2026
8a30013
Merge branch 'feat/watchsync-rating-sync' into feat/watchsync-plugin-…
Quick104 Sep 24, 2026
6dcf3a1
fix(watchsync): lock the binding for agreed-rating writes and keep la…
Quick104 Sep 24, 2026
7c401af
Merge remote-tracking branch 'origin/main' into fix/watchsync-trakt-p…
Quick104 Sep 24, 2026
a28c9d8
Merge branch 'fix/watchsync-trakt-pagination' into fix/watchsync-trak…
Quick104 Sep 24, 2026
2759f78
Merge branch 'fix/watchsync-trakt-not-found-ids' into feat/watchsync-…
Quick104 Sep 24, 2026
1a2e7ca
Merge branch 'feat/watchsync-rating-sync' into feat/watchsync-plugin-…
Quick104 Sep 24, 2026
9238c03
chore(deps): build against plugin SDK v0.17.0
Quick104 Sep 24, 2026
7af16b7
Merge branch 'main' into feat/watchsync-plugin-ratings
Quick104 Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/architecture/watch-provider-rating-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ mark the profile's recommendations stale once per run.

A provider that rates only some kinds implements `RatingKindFilter`; items of other
kinds are left out of its sync entirely, so they are neither sent nor read as removed.
Plugin providers rate the kinds they list in `supported_media_types`. Removing an
absent rating must answer `APPLIED` or `NO_CHANGE`, so a plugin's `REJECTED` answer to
a rating removal counts as a failure and the removal is retried on the next run. A
complete plugin snapshot with an unreadable rating row covers no kind, because the
row's kind is unknown, and the run records a warning.

A provider that records a rated title as watched implements `RatingExportWatchGate`.
Silo then sends a new rating of that kind only once the profile has a completed play of
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ require (
)

require (
github.com/Silo-Server/silo-plugin-sdk v0.16.1
github.com/Silo-Server/silo-plugin-sdk v0.17.0
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0g
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s=
github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA=
github.com/Silo-Server/silo-plugin-sdk v0.16.1 h1:bctPlzlsr75Z9ARQ86VnYY6UhyQ9YPwgoP+mqSRESPA=
github.com/Silo-Server/silo-plugin-sdk v0.16.1/go.mod h1:abwsCEKuPAAgeAqpNGbwoaut2eQlC/Kj97u89Vvg9qM=
github.com/Silo-Server/silo-plugin-sdk v0.17.0 h1:dHA4UbObMVIzAaLQC4clq1bSWuPaim88yptIRR5/f/M=
github.com/Silo-Server/silo-plugin-sdk v0.17.0/go.mod h1:abwsCEKuPAAgeAqpNGbwoaut2eQlC/Kj97u89Vvg9qM=
github.com/TwiN/go-color v1.4.1 h1:mqG0P/KBgHKVqmtL5ye7K0/Gr4l6hTksPgTgMk3mUzc=
github.com/TwiN/go-color v1.4.1/go.mod h1:WcPf/jtiW95WBIsEeY1Lc/b8aaWoiqQpu5cf8WFxu+s=
github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4=
Expand Down
36 changes: 34 additions & 2 deletions internal/watchsync/plugin_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,13 @@ type PluginProvider struct {
resolveClient WatchSyncPluginClientResolver
resolveConfig WatchSyncPluginConfigResolver
repository PluginCredentialRepository
now func() time.Time
}

const (
watchSyncUnsupportedMovieMediaMessage = "watch sync plugin does not support movie media"
watchSyncUnsupportedEpisodeMediaMessage = "watch sync plugin does not support episode media"
watchSyncUnsupportedSeriesMediaMessage = "watch sync plugin does not support series media"
watchSyncUnsupportedMediaMessage = "watch sync plugin does not support this media type"
watchSyncJSONSchemaNumberType = "number"
watchSyncJSONSchemaBooleanType = "boolean"
Expand Down Expand Up @@ -112,6 +114,7 @@ func NewPluginProvider(options PluginProviderOptions) (*PluginProvider, error) {
resolveClient: options.ResolveClient,
resolveConfig: options.ResolveConfig,
repository: options.Repository,
now: time.Now,
}, nil
}

Expand Down Expand Up @@ -173,6 +176,8 @@ func (p *PluginProvider) Capabilities() Capabilities {
RemoveWatchlist: p.descriptor.GetRemoveWatchlist(),
ProvidesWatchlistOrder: p.descriptor.GetProvidesWatchlistOrder(),
ScrobblePlayback: p.descriptor.GetScrobblePlayback(),
ImportRatings: p.descriptor.GetImportRatings(),
ExportRatings: p.descriptor.GetExportRatings(),
}
}

Expand Down Expand Up @@ -1152,12 +1157,29 @@ func mediaFromIdentity(mediaItemID, kind, title string, year int, imdbID, tmdbID
}
}

// SyncsRatingKind reports whether the plugin rates items of kind, from the
// media types it supports. Without it, the host would send a movie-only plugin
// every series rating on each sync and log the rejection.
func (p *PluginProvider) SyncsRatingKind(kind string) bool {
return p.supportsMedia(watchSyncMediaType(kind))
}

// mediaFromLocalFavorite builds list and rating media. A series item carries
// its own ids, so its SERIES media has them in external_ids and no series_*.
func mediaFromLocalFavorite(item LocalFavorite) *pluginv1.WatchSyncMedia {
return mediaFromIdentity(item.MediaItemID, item.Kind, item.Title, item.Year,
item.IMDbID, item.TMDBID, item.TVDBID, "", 0,
item.SeriesIMDbID, item.SeriesTMDBID, item.SeriesTVDBID, 0, 0)
}

func watchSyncMediaType(kind string) pluginv1.WatchSyncMediaType {
switch strings.ToLower(strings.TrimSpace(kind)) {
case historyimport.KindMovie:
return pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE
case historyimport.KindEpisode:
return pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE
case historyimport.KindSeries:
return pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES
default:
return pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED
}
Expand Down Expand Up @@ -1263,12 +1285,20 @@ func supportedWatchSyncMediaTypes(descriptor *pluginv1.WatchSyncProviderDescript
for _, mediaType := range media {
switch mediaType {
case pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE,
pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE:
pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE,
pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES:
supported[mediaType] = struct{}{}
default:
case pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED:
return nil, fmt.Errorf("advertises unsupported media type %q", mediaType.String())
default:
// A media type added by a newer SDK is ignored rather than
// rejecting the plugin, so a plugin release that opts into a new
// type keeps its existing sync on servers that predate the type.
}
}
if len(supported) == 0 {
return nil, errors.New("advertises no media type this server supports")
}
return supported, nil
}

Expand All @@ -1286,6 +1316,8 @@ func unsupportedWatchSyncMediaMessage(mediaType pluginv1.WatchSyncMediaType) str
return watchSyncUnsupportedMovieMediaMessage
case pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE:
return watchSyncUnsupportedEpisodeMediaMessage
case pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES:
return watchSyncUnsupportedSeriesMediaMessage
default:
return watchSyncUnsupportedMediaMessage
}
Expand Down
Loading
Loading