@constructorfleet/extension-watch-history
v1.0.1
Published
Reference Seerr extension: records what each user has watched.
Readme
Watch History — reference Seerr extension
Records what each user has watched, and shows them their own history in a panel.
Its real job is to be the honest end-to-end exercise of the extension SDK. Every capability a manifest can declare is declared here and actually used, so if the SDK is missing something an extension needs, this is where it shows up:
| Capability | Where it is used |
| --- | --- |
| store (entity) | src/entity/WatchEvent.ts, one ext_watch-history_event table |
| store.kv | the sync job's cursor, read back by the panel |
| users: read | hasPermission for view_all; the job's orphan check |
| media: read | resolving a tmdbId when recording a watch |
| requests: read | the /unwatched route |
| settings: read | applicationTitle in the notification |
| jobs | sync, on the manifest's cron schedule |
| events | request.available (writes rows), media.available (logged) |
| routes | GET/POST /history, GET /unwatched |
| permissions | view_own (default) and view_all (requiresCore: MANAGE_USERS) |
| notifications | milestone |
| panel | dist/panel.js, sidebar ClockIcon |
Building
pnpm build # both halves; see below for why there are two
pnpm typecheckTwo tsconfigs, and this is not incidental:
tsconfig.json→ CommonJS. The hostrequire()s the entry point, and theexport =insrc/index.tsonly means anything under CJS emit.tsconfig.panel.json→ ES2022 modules. The panel is loaded by a browserimport().module: ES2022is chosen sotscemits barefrom "react"and leaves it alone — those specifiers are exactly what the host's import map rewrites to its React. A bundler that inlined React would give the panel a second copy, which renders correctly and then dies on the first hook.
Installing
# from a checkout
pnpm build
cp -r . "${CONFIG_DIRECTORY:-config}/extensions/watch-history"
# then restart Seerr and enable it under Settings → ExtensionsRestarting is required, not a formality: TypeORM cannot register an entity after
DataSource.initialize(), so an extension contributing a table is only picked up
at boot.
Three things worth reading the comments for
These are the parts that surprised the author, and each is explained where it happens rather than here:
watchedAtgoes through the SDK'sDbAwareColumn, and the migration throughresolveColumnType— sqlite and Postgres disagree about date types, so a baretype: 'datetime'works on a sqlite dev box and fails on a Postgres deployment. Both halves must make the same decision or the migrated schema and the entity metadata disagree; the decorator covers the entity and the string form covers the raw SQL. Seesrc/entity/WatchEvent.tsandsrc/migration/.- The manifest exists twice —
seerr-extension.json(what the host reads) andsrc/manifest.ts(whatdefineExtensionnarrows from). JSON imports widentruetoboolean, which silently defeats the narrowing entirely. Seesrc/manifest.ts; a test in the Seerr repo asserts the two agree. userId/mediaIdare plain columns, not relations — a foreign key from an extension table into a core one makes uninstalling a schema problem for core rather than aDROP TABLE. The cost is orphan rows, which thesyncjob prunes.
