···1313| Netflix | `netflix.com/watch/...` |
1414| YouTube | `youtube.com/watch?v=...`, `youtu.be/...`, `youtube.com/embed/...`, `youtube.com/live/...` |
15151616-Adding a service is a self-contained change: drop a `ServicePlugin` under
1616+Adding a service starts in `packages/shared/src/services.ts`, which owns the
1717+service ID, display metadata, URL parsing, canonical watch URL builder, and
1818+extension match patterns. Then add the extension-only DOM integration under
1719`apps/extension/src/utils/services/<id>.ts`, add a one-line
1818-`runServiceContentScript(MY_SERVICE)` entrypoint, register the plugin in
1919-`SERVICE_PLUGINS`, and append its origin to `host_permissions` in
2020-`apps/extension/wxt.config.ts`.
2020+`runServiceContentScript(MY_SERVICE)` entrypoint, and register the plugin in
2121+`SERVICE_PLUGINS`.
21222223## Commands
2324
-1
apps/extension/src/utils/services/dom-video.ts
···1818 let refreshFrame: number | null = null;
1919 let stopped = false;
20202121-2221 const readContext = (): ServiceContentContext | null => {
2322 const mediaId = plugin.parseUrl(window.location.href)?.mediaId;
2423 if (!activeVideo || !mediaId) return null;
···11-import type { ServiceId } from '@open-watch-party/shared';
11+import {
22+ getServiceDefinitionByUrl,
33+ type ServiceDescriptor,
44+ type ServiceId,
55+} from '@open-watch-party/shared';
2637import { NETFLIX_SERVICE } from './netflix';
48import { YOUTUBE_SERVICE } from './youtube';
55-import type { ServiceDescriptor, ServicePlugin } from './types';
99+import type { ServicePlugin } from './types';
610711/**
812 * Every service the extension knows about. Order drives popup rendering.
913 *
1014 * Adding a service:
1111- * 1. Add a `ServiceId` to `packages/shared/src/protocol.ts`.
1212- * 2. Create a `ServicePlugin` at `apps/extension/src/utils/services/<id>.ts`.
1515+ * 1. Add its shared definition to `packages/shared/src/services.ts`.
1616+ * 2. Create the extension DOM integration at `apps/extension/src/utils/services/<id>.ts`.
1317 * 3. Add a one-line entrypoint at `src/entrypoints/<id>.content.ts` via
1418 * `runServiceContentScript(MY_SERVICE)`.
1515- * 4. Append the plugin below and add its origin(s) to `host_permissions`
1616- * in `wxt.config.ts`.
1919+ * 4. Append the plugin below.
1720 */
1821export const SERVICE_PLUGINS: readonly ServicePlugin[] = [NETFLIX_SERVICE, YOUTUBE_SERVICE];
1922···3639export function findPluginByUrl(
3740 url: string | null | undefined,
3841): { plugin: ServicePlugin; isWatchPage: boolean } | null {
3939- if (!url) return null;
4040- for (const plugin of SERVICE_PLUGINS) {
4141- const parsedUrl = plugin.parseUrl(url);
4242- if (parsedUrl) {
4343- return { plugin, isWatchPage: Boolean(parsedUrl.mediaId) };
4444- }
4545- }
4646- return null;
4242+ const serviceMatch = getServiceDefinitionByUrl(url);
4343+ if (!serviceMatch) return null;
4444+4545+ const plugin = getPlugin(serviceMatch.service.id);
4646+ return plugin ? { plugin, isWatchPage: serviceMatch.isWatchPage } : null;
4747}
+4-25
apps/extension/src/utils/services/types.ts
···11-import type { ServiceId } from '@open-watch-party/shared';
22-33-/** Presentation metadata rendered by the popup UI. */
44-export interface ServiceDescriptor {
55- readonly id: ServiceId;
66- readonly label: string;
77- readonly accent: string;
88- readonly accentContrast: string;
99- readonly glyph: string;
1010- readonly watchPathHint: string;
1111-}
11+import type { SupportedServiceDefinition } from '@open-watch-party/shared';
122133/**
1414- * Self-contained service integration. A plugin bundles the service metadata,
1515- * URL classifier, DOM selectors, and the one optional playback override needed
1616- * to drive a service from a content script.
44+ * Service integration plus the extension-only DOM hooks needed by a content script.
175 */
1818-export interface ServicePlugin {
1919- readonly id: ServiceId;
2020- readonly descriptor: ServiceDescriptor;
2121- /** WXT-style match patterns; consumed at build time for the manifest. */
2222- readonly contentMatches: readonly string[];
66+export type ServicePlugin = SupportedServiceDefinition & {
237 /** Shown when a watch URL matched but the `<video>` element isn't ready. */
248 readonly playerNotReadyMessage: string;
2525- /**
2626- * Returns null for URLs outside the service. A non-null result without
2727- * `mediaId` means the URL belongs to the service but is not a watch page.
2828- */
2929- parseUrl(url: string): { mediaId?: string } | null;
309 getVideo(): HTMLVideoElement | null;
3110 getMediaTitle(): string;
3232-}
1111+};