Skip to content

Extension events

Implement IEventExtension when an extension needs to react after Cove successfully processes a top-level library mutation. Cove delivers these notifications in process after the mutation has persisted. They are invalidation signals: read the entity again when you need its current state instead of treating the event as a complete change record.

EventExtensionBase and FullExtensionBase route handlers by the canonical names in this reference. Implementations that consume ExtensionEvent directly should route on CanonicalEventType. EventType remains available as the original wire name for compatibility.

Every extension event contains the following values:

PropertyMeaning
CanonicalEventTypeStable noun.verb name to use for new handlers.
EventTypeCompatibility wire name. It normally matches CanonicalEventType; legacy audio and text events are the exceptions listed below.
EntityTypeLowercase entity kind such as video, performer, or audio.
EntityIdCove database identifier of the affected entity.
DataOptional event-specific data. Ordinary lifecycle events omit it.

The SDK helpers OnCreated(entityType, ...), OnUpdated(entityType, ...), and OnDeleted(entityType, ...) construct the canonical lifecycle names. On(eventType, ...) and OnEvent(eventType, ...) register any other canonical name.

Extensions receive an ExtensionEvent object in process rather than an HTTP request. The examples below show the same property values in JSON form.

A typical lifecycle notification has no event-specific data:

{
"eventType": "video.updated",
"canonicalEventType": "video.updated",
"entityType": "video",
"entityId": 42,
"data": null
}

Create and delete notifications have the same shape; only the event names change to video.created or video.deleted. The entity kind and identifier change for the other lifecycle events.

Legacy audio and text notifications demonstrate why new handlers should use CanonicalEventType:

{
"eventType": "audioupdated",
"canonicalEventType": "audio.updated",
"entityType": "audio",
"entityId": 42,
"data": null
}

A rating notification includes the user, normalized rating aspect, and new value under data.entity:

{
"eventType": "rating.updated",
"canonicalEventType": "rating.updated",
"entityType": "video",
"entityId": 42,
"data": {
"entity": {
"userId": 7,
"aspect": "overall",
"value": 4
}
}
}

Clearing that rating produces rating.deleted and a null value:

{
"eventType": "rating.deleted",
"canonicalEventType": "rating.deleted",
"entityType": "video",
"entityId": 42,
"data": {
"entity": {
"userId": 7,
"aspect": "overall",
"value": null
}
}
}

Cove publishes one lifecycle event for each top-level entity successfully processed by a supported create, update, or delete mutation. An accepted update can publish even when the submitted values already match persisted state. A bulk request publishes one event per entity found and successfully processed; missing identifiers, duplicate identifiers, rejected requests, and skipped filesystem operations do not produce events.

EntityCreatedUpdatedDeletedCompatibility wire name
Videovideo.createdvideo.updatedvideo.deletedSame as canonical
Performerperformer.createdperformer.updatedperformer.deletedSame as canonical
Studiostudio.createdstudio.updatedstudio.deletedSame as canonical
Tagtag.createdtag.updatedtag.deletedSame as canonical
Gallerygallery.createdgallery.updatedgallery.deletedSame as canonical
Imageimage.createdimage.updatedimage.deletedSame as canonical
Groupgroup.createdgroup.updatedgroup.deletedSame as canonical
Audioaudio.createdaudio.updatedaudio.deletedaudiocreated, audioupdated, audiodeleted
Texttext.createdtext.updatedtext.deletedtextcreated, textupdated, textdeleted

The compatibility spellings for audio and text predate this documented contract. Cove keeps them in EventType so existing direct implementations continue to work; the SDK routes their CanonicalEventType through the normal lifecycle helpers.

Create, update, and delete include the corresponding single-entity API operations and library scan intake. Update also covers scraper and metadata-server application, successful metadata-server batch-tag items, merges into a surviving target, file ownership or path changes, gallery membership and chapter changes, and group query, item, order, snapshot, and subgroup changes. A merge publishes an update for the surviving target and a delete for each source that was removed.

Ratings have their own lifecycle because they are scoped to the current user and rating aspect:

Canonical nameWhen Cove publishes it
rating.createdThe current user adds a rating for an aspect that did not have one.
rating.updatedThe current user changes an existing rating aspect.
rating.deletedThe current user clears an existing rating aspect.

EntityType and EntityId identify the rated entity. Data["entity"] contains userId, the normalized aspect, and value; the deletion value is null. Clearing an aspect that has no rating is a no-op and does not publish an event.

Extension events are best-effort process-local notifications. Cove does not persist, replay, retry, or globally order them. The host queues each event independently after publication so an extension handler does not block the mutation request. Multiple handlers or events—including events from one bulk request—can therefore run concurrently. A host shutdown or process failure can lose an event that has already been published but not dispatched.

Handlers should be idempotent, tolerate a missing entity after an update notification, and use EntityId to fetch current state when needed. Do not use events as an audit log or as the only copy of durable extension data.

Only EntityEvent reaches IEventExtension. Internal JobEvent and system CoveEvent values—including scan progress and server lifecycle values—are not part of the extension event contract.

Use a narrow SDK base when event routing is the extension’s main responsibility:

public sealed class LibraryObserver : EventExtensionBase
{
public override string Id => "example.library-observer";
public override string Name => "Library observer";
public override string Version => "1.0.0";
protected override void RegisterHandlers()
{
OnCreated("video", HandleVideoCreatedAsync);
OnUpdated("audio", HandleAudioUpdatedAsync);
On("rating.deleted", HandleRatingDeletedAsync);
}
private static Task HandleVideoCreatedAsync(ExtensionEvent evt, CancellationToken ct) =>
Task.CompletedTask;
private static Task HandleAudioUpdatedAsync(ExtensionEvent evt, CancellationToken ct) =>
Task.CompletedTask;
private static Task HandleRatingDeletedAsync(ExtensionEvent evt, CancellationToken ct) =>
Task.CompletedTask;
}

Use FullExtensionBase.DefineEventHandlers() when the same package also contributes jobs, APIs, data, or UI. A direct IEventExtension implementation receives every extension event and owns its own filtering.