{"_id":"@constructorfleet/extension-watch-stats","_rev":"2-c38711da80744d9cbdbf4507cb0cd3c1","name":"@constructorfleet/extension-watch-stats","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@constructorfleet/extension-watch-stats","version":"1.0.0","license":"MIT","_id":"@constructorfleet/extension-watch-stats@1.0.0","maintainers":[{"name":"teagan42","email":"that@teagantotally.rocks"}],"homepage":"https://github.com/constructorfleet/seerr-extensions#readme","bugs":{"url":"https://github.com/constructorfleet/seerr-extensions/issues"},"dist":{"shasum":"a312b34471970825dcf263bc4f028903f1ec5b3f","tarball":"https://registry.npmjs.org/@constructorfleet/extension-watch-stats/-/extension-watch-stats-1.0.0.tgz","fileCount":19,"integrity":"sha512-IjMOsFqdB4Cdjt+8arHJx9GI1N3ff58jJ6w+GJLQE7z9a61UpGGGcjqgv+SM5+hXU5LfvOFW0eBZGOZ0/X5KIw==","signatures":[{"sig":"MEYCIQDG7yTyOJ9tUO50CwuY7DqH3owBO8mlcdV031ylg7kWOwIhANQONzxXyxohVFQX2ro8RbMM6UoW+s/NzM63+tILaINv","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":80814},"main":"dist/index.js","_from":"file:constructorfleet-extension-watch-stats-1.0.0.tgz","scripts":{"build":"tsc --project tsconfig.json && tsc --project tsconfig.panel.json","typecheck":"tsc --project tsconfig.json --noEmit && tsc --project tsconfig.panel.json --noEmit"},"_npmUser":{"name":"teagan42","email":"that@teagantotally.rocks"},"_resolved":"/tmp/beabc0ac4172f5e29fef15ae37fadf5d/constructorfleet-extension-watch-stats-1.0.0.tgz","_integrity":"sha512-IjMOsFqdB4Cdjt+8arHJx9GI1N3ff58jJ6w+GJLQE7z9a61UpGGGcjqgv+SM5+hXU5LfvOFW0eBZGOZ0/X5KIw==","repository":{"url":"git+https://github.com/constructorfleet/seerr-extensions.git","type":"git","directory":"watch-stats"},"_npmVersion":"10.9.8","description":"Example Seerr extension: brings play counts from Tautulli or Tracearr into Seerr.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"@constructorfleet/extension-sdk":"1.0.0"},"publishConfig":{"registry":"https://npm.pkg.github.com"},"_hasShrinkwrap":false,"devDependencies":{"swr":"2.2.5","zod":"4.3.6","react":"18.3.1","typeorm":"0.3.29","typescript":"5.4.5","@types/node":"22.19.0","@types/react":"18.3.12","@constructorfleet/extension-ui":"1.0.0"},"peerDependencies":{"zod":"^4.3.6","typeorm":"^0.3.29"},"_npmOperationalInternal":{"tmp":"tmp/extension-watch-stats_1.0.0_1786099892611_0.2524401859227621","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@constructorfleet/extension-watch-stats","version":"1.0.1","description":"Example Seerr extension: brings play counts from Tautulli or Tracearr into Seerr.","license":"MIT","main":"dist/index.js","peerDependencies":{"typeorm":"^0.3.29","zod":"^4.3.6"},"dependencies":{"@constructorfleet/extension-sdk":"1.0.1"},"repository":{"type":"git","url":"git+https://github.com/constructorfleet/seerr-extensions.git","directory":"watch-stats"},"publishConfig":{"registry":"https://npm.pkg.github.com"},"devDependencies":{"typescript":"5.4.5","@types/node":"22.19.0","typeorm":"0.3.29","zod":"4.3.6","@constructorfleet/extension-ui":"1.0.1","react":"18.3.1","@types/react":"18.3.12","swr":"2.2.5"},"scripts":{"build":"tsc --project tsconfig.json && tsc --project tsconfig.panel.json","typecheck":"tsc --project tsconfig.json --noEmit && tsc --project tsconfig.panel.json --noEmit"},"_id":"@constructorfleet/extension-watch-stats@1.0.1","bugs":{"url":"https://github.com/constructorfleet/seerr-extensions/issues"},"homepage":"https://github.com/constructorfleet/seerr-extensions#readme","_integrity":"sha512-VkIsPg5g04iu8BhMO24B4mq+pM1JAb4cbEy/GJwKTKJYyPfEw7mGgGu78kMwzYDk0V4dljbTyKKQfHJBQkdWRw==","_resolved":"/tmp/cf5f82ae7599013b8628a2003cb06510/constructorfleet-extension-watch-stats-1.0.1.tgz","_from":"file:constructorfleet-extension-watch-stats-1.0.1.tgz","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-VkIsPg5g04iu8BhMO24B4mq+pM1JAb4cbEy/GJwKTKJYyPfEw7mGgGu78kMwzYDk0V4dljbTyKKQfHJBQkdWRw==","shasum":"408ae82ecf9aa71d1c910307cb49ec9d4cbf948b","tarball":"https://registry.npmjs.org/@constructorfleet/extension-watch-stats/-/extension-watch-stats-1.0.1.tgz","fileCount":19,"unpackedSize":80814,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGKiu9QuyW+vUpJ3CO5Ztllg2jWx4IZ68CMXtMG/FrPaAiEA/FKSDqUBNAXUm+a9LBGvhskh7pjXLZHELy6EjY9e8UY="}]},"_npmUser":{"name":"teagan42","email":"that@teagantotally.rocks"},"directories":{},"maintainers":[{"name":"teagan42","email":"that@teagantotally.rocks"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/extension-watch-stats_1.0.1_1786102041382_0.8874839584317622"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-07T10:51:32.439Z","modified":"2026-08-07T11:27:21.705Z","1.0.0":"2026-08-07T10:51:32.769Z","1.0.1":"2026-08-07T11:27:21.536Z"},"bugs":{"url":"https://github.com/constructorfleet/seerr-extensions/issues"},"license":"MIT","homepage":"https://github.com/constructorfleet/seerr-extensions#readme","repository":{"type":"git","url":"git+https://github.com/constructorfleet/seerr-extensions.git","directory":"watch-stats"},"description":"Example Seerr extension: brings play counts from Tautulli or Tracearr into Seerr.","maintainers":[{"name":"teagan42","email":"that@teagantotally.rocks"}],"readme":"# Watch Stats — an extension that integrates external services\n\nBrings user viewing statistics into Seerr: what each person has watched, what is\ntrending on the server, and what to watch next based on both.\n\nWhere [`watch-history`](../watch-history) exists to exercise every capability the\nSDK has, this one answers a narrower question: **what does an extension look like\nwhen the data lives somewhere else?** It reads plays from either\n[Tautulli](https://tautulli.com) or [Tracearr](https://github.com/connorgallopo/Tracearr),\nand the contrast between those two halves is the whole point.\n\n| | Tautulli | Tracearr |\n| --- | --- | --- |\n| Does core know about it? | Yes — Settings → Services | No |\n| Who holds the credential? | Seerr | this extension |\n| How the extension reaches it | `sdk.tautulli` | its own `fetch` |\n| Identifies a title by | Plex rating key | TMDB id |\n| Reports watch time in | seconds (converted by the host) | milliseconds |\n| Adapter | `src/sources/tautulli.ts`, ~90 lines | `src/sources/tracearr.ts`, ~175 |\n\nThe size difference is the argument for host capabilities. `sdk.tautulli` is\nread-only and manifest-gated (`requires: { tautulli: 'read' }`) and hands over\nrecords already normalized, so the Tautulli adapter contains no hostname, no port,\nno API key and no HTTP call. The Tracearr adapter has to do all of it — auth,\ncursor pagination, a page cap, response validation, and deciding what an\nunreachable server means.\n\n**`sdk.tautulli` is the one capability whose declaration does not guarantee\npresence.** The operator must *also* have configured Tautulli in Seerr's settings,\nso the member is optional and `src/index.ts` checks for it. That is why an\nunconfigured source is a state with a sentence attached (`sourceProblem`, rendered\nby the panel) rather than an error.\n\n## Configuring\n\nUnder Settings → Extensions → Watch Stats:\n\n| Setting | Notes |\n| --- | --- |\n| `source` | `tautulli` or `tracearr` |\n| `tracearr_url` | base URL, trailing slash tolerated. Only for `tracearr` |\n| `tracearr_token` | a `trr_pub_…` token from Tracearr's Settings → General |\n| `trend_days` | the window plays are counted over, 1–90 (default 7) |\n\nChoosing `tautulli` needs nothing here — the connection is core's, under\nSettings → Services.\n\nThe manifest declares **no `requires.http` allowlist**, deliberately. Both\nservices live at an address the operator chooses, so there is no hostname the\nauthor could list; a plausible-looking placeholder would make the one file whose\njob is to describe truthfully what an extension touches into a lie.\n\n## Building and installing\n\n```\npnpm build      # both tsconfigs — CJS entry point, ESM panel\npnpm typecheck\ncp -r . \"${CONFIG_DIRECTORY:-config}/extensions/watch-stats\"\n# then restart Seerr and enable it under Settings → Extensions\n```\n\nRestarting is required: TypeORM cannot register an entity after\n`DataSource.initialize()`, so the `ext_watch-stats_play` table is only picked\nup at boot. The two-tsconfig split is explained in `watch-history`'s README and\nmatters for the same reasons here.\n\n## Four things worth reading the comments for\n\n1. **The stored table is an aggregate, not a play log.** Neither source exposes a\n   stable per-play id, so there is nothing to deduplicate an incremental sync\n   against. `sync` therefore recomputes the window and replaces the table inside\n   one transaction — which makes running it twice indistinguishable from running\n   it once, and makes a failed sync leave the previous numbers in place rather\n   than emptying the panel. See `src/entity/PlayStat.ts` and `sync` in\n   `src/index.ts`.\n2. **A source's identifiers are not core's.** A play arrives keyed by a rating key\n   *or* a tmdbId, and by a *media-server* user id. `sdk.media.findByRatingKey` /\n   `findByTmdbId` and the `plexId → user.id` map are what turn those into the only\n   durable keys worth storing. A play that resolves to neither is dropped, not\n   stored under a guess — someone watching without a Seerr account, or a title\n   added to the library outside Seerr, are both ordinary states.\n3. **Episodes count against the series.** Tautulli reports episode plays\n   individually; core's `media` row is the series. Both adapters fold `episode`\n   into `tv` and key on the series, so twelve episodes are twelve plays of one\n   title rather than twelve titles core has no row for.\n4. **The panel imports `@constructorfleet/extension-ui` instead of writing Tailwind.** This\n   is the only example that does, and it is the right way. `tailwind.config.js`\n   scans `src/pages/**` and `src/components/**`, so a class appearing *only* in a\n   runtime-loaded panel bundle is never compiled — the element renders unstyled\n   with no console error and no log line. Importing core's own components sidesteps\n   the whole class of bug; see the header of `src/panel.tsx`.\n\n## Where the tests are\n\n`server/lib/extensions/watchStats.test.ts`, in the Seerr repo — an integration\ntest that builds this example with its own `tsc`, installs the output into a\ntemporary directory, and drives it through the real loader. It fakes Tautulli at\ncore's `TautulliAPI` prototype and Tracearr at `globalThis.fetch`, then asserts the\njoins, the idempotency, the permission gate and the manifest agreement above.\n","readmeFilename":"README.md"}