Skip to content

feat(tracking): add webhook media trackers, with Yamtrack - #304

Draft
sean-wils wants to merge 5 commits into
lostb1t:mainfrom
sean-wils:refactor/webhook-media-tracker
Draft

feat(tracking): add webhook media trackers, with Yamtrack#304
sean-wils wants to merge 5 commits into
lostb1t:mainfrom
sean-wils:refactor/webhook-media-tracker

Conversation

@sean-wils

@sean-wils sean-wils commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Towards #207.

Several trackers are fed through Jellyfin's webhook plugin and differ only in the body they send. webhook_media_tracker is the shared half: build the notification, post it, classify the reply. A provider supplies a WebhookFormat and nothing else.

Yamtrack is the first on it. Scrob follows in its own PR once this lands.

FYI connecting a tracker has no API yet, that'll be a separate PR, so this does nothing for users until that lands.

@sean-wils sean-wils changed the title feat(tracking): add Scrob as a second webhook media tracker feat(tracking): add Yamtrack and Scrob as webhook media trackers Aug 21, 2026
@sean-wils
sean-wils marked this pull request as draft August 21, 2026 23:23
@sean-wils
sean-wils force-pushed the refactor/webhook-media-tracker branch from 6f75add to c5f53c2 Compare August 22, 2026 10:19
@sean-wils sean-wils changed the title feat(tracking): add Yamtrack and Scrob as webhook media trackers feat(tracking): add webhook media trackers, with Yamtrack Aug 22, 2026
Several trackers are fed through Jellyfin's webhook plugin and differ
only in the body they want and how they read a response.
`webhook_media_tracker` is the half they share: building the
notification, posting it, and classifying the reply into retryable,
reauth or permanent. A provider supplies a `WebhookFormat` and nothing
else.

Yamtrack is the first one on it. It keys an episode on the episode's
own ids, which is why `WebhookItem` carries both those and the series'.

A tracker may hold its credential in the url, as a path segment or a
query parameter, and a transport error prints the url it failed on.
That string becomes `last_error` and `GET /users/{id}/mediatrackers`
hands it back, so the url is dropped before the message is built.
TMDB's season listing carries no `external_ids`, so an episode reaches a
tracker with nothing of its own. Yamtrack matches an episode on its own
tvdb or imdb id, never the series', so it depends on
`complete_episode_ids` for every episode it can place.

Nothing in this file reaches that code, and a break there costs no test
and no error: the webhook still takes the delivery and Yamtrack drops
the episode on the floor.
@sean-wils
sean-wils force-pushed the refactor/webhook-media-tracker branch from c5f53c2 to debed03 Compare August 26, 2026 22:13
Yamtrack looks an episode up by its own tvdb or imdb id and reads
nothing else. `is_matchable` passes an episode whose only ids are the
series', which this provider never consults, so one that resolved to
nothing was posted anyway, answered 200, and recorded as delivered.

Found on a season-0 special, where TMDB carries no external ids for the
episode and completion leaves it with none: the webhook went out with an
empty `ProviderIds` and the watch was lost with nothing to show for it.
30% of the episodes in the library it was found on carry no tmdb id of
their own, so they all rest on that lookup succeeding.
@sean-wils

Copy link
Copy Markdown
Contributor Author

Tested against a real Yamtrack instance:

  1. episodes and movies both scrobble
  2. an abandoned film records as in progress
  3. an episode TMDB can't fill in fails visibly instead of reporting success

I'll look into fixing the unresolved episodes in a separate PR.

@sean-wils
sean-wils marked this pull request as ready for review August 27, 2026 00:58
@lostb1t

lostb1t commented Aug 27, 2026

Copy link
Copy Markdown
Owner

i don't think webhooks and trackers are the same thing tho. wanted to tackle webhooks acfer this

do these trackers dont have an api?

@sean-wils

sean-wils commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

They do, but they don't follow a standard and would rely on us maintaining every self hosted tracking service under the sun in isolation if they change anything. They all share the same jellyfin webhook plugin functionality so in my mind it makes way more sense to me to just follow the standard defined where at all possible.

@sean-wils

Copy link
Copy Markdown
Contributor Author

If you''d rather tackle webhooks properly maybe let's close this for now and revisit once it's ready.

The guard refused an episode whose ids were empty, but Yamtrack reads an
episode's own tvdb or imdb id and never looks at tmdb on its TV path. An
episode carrying only a tmdb id has something for `provider_ids` to send
and still identifies nothing, so it passed the check, took a 200, and
was recorded as delivered.

That is the shape most of a real library is in: TMDB gives an episode
its own tmdb id and no `external_ids` at all, so the case the guard was
written for was the case it let through.
@lostb1t

lostb1t commented Aug 27, 2026

Copy link
Copy Markdown
Owner

i didnt gave it much thought but seeing your pr you provide the webhooks/body which makes sense

tho yeah i need to have a proper look again because webhooks in its core are generic and used outside of trackers

The shared half sat at `addons/webhook_media_tracker.rs`, next to the
addons rather than inside the capability it serves, which read as a claim
on webhooks generally. It is neither: no operator-configured URL, no body
a user can edit, just the payload dialect these trackers' receivers parse.
Moving it to `media_tracker/jellyfin_webhook_body.rs` puts it where its
only caller lives and names it after what it builds.

Yamtrack's API doc line goes with it: their write API is merged to a
feature branch, not to a release, so "no released write API" is the
accurate claim.
@sean-wils

Copy link
Copy Markdown
Contributor Author

Yeah, agreed they should be generic. I think the split is:

  • a webhook is operator-level: any URL, any body
  • a tracker connection is per-user, holds a credential, surfaces a reauth prompt when it goes stale, has a per-provider event filter, and the body is fixed so the user can't get it wrong

Yamtrack wants the second one, so I'd keep it on the tracker trait even once generic webhooks exist.

Renamed it to media_tracker/jellyfin_webhook_body.rs so it reads as the body and not as a claim on the area. Have a look whenever.

I'll keep this in draft until you'd have a think about it. 👍

@sean-wils
sean-wils marked this pull request as draft August 27, 2026 12:33
@lostb1t

lostb1t commented Aug 27, 2026

Copy link
Copy Markdown
Owner

posted an idea on discord

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants