+202
-134
Diff
round #0
+5
-4
README.md
+5
-4
README.md
···
13
13
| Netflix | `netflix.com/watch/...` |
14
14
| YouTube | `youtube.com/watch?v=...`, `youtu.be/...`, `youtube.com/embed/...`, `youtube.com/live/...` |
15
15
16
-
Adding a service is a self-contained change: drop a `ServicePlugin` under
16
+
Adding a service starts in `packages/shared/src/services.ts`, which owns the
17
+
service ID, display metadata, URL parsing, canonical watch URL builder, and
18
+
extension match patterns. Then add the extension-only DOM integration under
17
19
`apps/extension/src/utils/services/<id>.ts`, add a one-line
18
-
`runServiceContentScript(MY_SERVICE)` entrypoint, register the plugin in
19
-
`SERVICE_PLUGINS`, and append its origin to `host_permissions` in
20
-
`apps/extension/wxt.config.ts`.
20
+
`runServiceContentScript(MY_SERVICE)` entrypoint, and register the plugin in
21
+
`SERVICE_PLUGINS`.
21
22
22
23
## Commands
23
24
-1
apps/extension/src/utils/services/dom-video.ts
-1
apps/extension/src/utils/services/dom-video.ts
+5
-26
apps/extension/src/utils/services/netflix.ts
+5
-26
apps/extension/src/utils/services/netflix.ts
···
1
+
import { getServiceDefinition } from '@open-watch-party/shared';
1
2
import type { ServicePlugin } from './types';
2
3
3
-
const NETFLIX_HOST_RE = /(^|\.)netflix\.com$/;
4
4
const NETFLIX_TITLE_SUFFIX = /\s*-\s*Netflix$/i;
5
+
const NETFLIX_DEFINITION = getServiceDefinition('netflix');
5
6
6
-
function parseNetflix(rawUrl: string): URL | null {
7
-
try {
8
-
const url = new URL(rawUrl);
9
-
return NETFLIX_HOST_RE.test(url.hostname) ? url : null;
10
-
} catch {
11
-
return null;
12
-
}
13
-
}
14
-
15
-
function extractNetflixMediaId(url: URL): string | undefined {
16
-
return url.pathname.match(/^\/watch\/(\d+)/)?.[1];
7
+
if (!NETFLIX_DEFINITION) {
8
+
throw new Error('Netflix service definition is missing.');
17
9
}
18
10
19
11
export const NETFLIX_SERVICE: ServicePlugin = {
20
-
id: 'netflix',
21
-
descriptor: {
22
-
id: 'netflix',
23
-
label: 'Netflix',
24
-
accent: '#e50914',
25
-
accentContrast: '#ffffff',
26
-
glyph: 'N',
27
-
watchPathHint: 'netflix.com/watch/…',
28
-
},
29
-
contentMatches: ['*://*.netflix.com/*'],
12
+
...NETFLIX_DEFINITION,
30
13
playerNotReadyMessage: 'Netflix player is still loading.',
31
-
parseUrl: (url) => {
32
-
const parsed = parseNetflix(url);
33
-
return parsed ? { mediaId: extractNetflixMediaId(parsed) } : null;
34
-
},
35
14
getVideo: () => document.querySelector<HTMLVideoElement>('video'),
36
15
getMediaTitle: () => document.title.replace(NETFLIX_TITLE_SUFFIX, '').trim() || 'Netflix',
37
16
};
+14
-14
apps/extension/src/utils/services/registry.ts
+14
-14
apps/extension/src/utils/services/registry.ts
···
1
-
import type { ServiceId } from '@open-watch-party/shared';
1
+
import {
2
+
getServiceDefinitionByUrl,
3
+
type ServiceDescriptor,
4
+
type ServiceId,
5
+
} from '@open-watch-party/shared';
2
6
3
7
import { NETFLIX_SERVICE } from './netflix';
4
8
import { YOUTUBE_SERVICE } from './youtube';
5
-
import type { ServiceDescriptor, ServicePlugin } from './types';
9
+
import type { ServicePlugin } from './types';
6
10
7
11
/**
8
12
* Every service the extension knows about. Order drives popup rendering.
9
13
*
10
14
* Adding a service:
11
-
* 1. Add a `ServiceId` to `packages/shared/src/protocol.ts`.
12
-
* 2. Create a `ServicePlugin` at `apps/extension/src/utils/services/<id>.ts`.
15
+
* 1. Add its shared definition to `packages/shared/src/services.ts`.
16
+
* 2. Create the extension DOM integration at `apps/extension/src/utils/services/<id>.ts`.
13
17
* 3. Add a one-line entrypoint at `src/entrypoints/<id>.content.ts` via
14
18
* `runServiceContentScript(MY_SERVICE)`.
15
-
* 4. Append the plugin below and add its origin(s) to `host_permissions`
16
-
* in `wxt.config.ts`.
19
+
* 4. Append the plugin below.
17
20
*/
18
21
export const SERVICE_PLUGINS: readonly ServicePlugin[] = [NETFLIX_SERVICE, YOUTUBE_SERVICE];
19
22
···
36
39
export function findPluginByUrl(
37
40
url: string | null | undefined,
38
41
): { plugin: ServicePlugin; isWatchPage: boolean } | null {
39
-
if (!url) return null;
40
-
for (const plugin of SERVICE_PLUGINS) {
41
-
const parsedUrl = plugin.parseUrl(url);
42
-
if (parsedUrl) {
43
-
return { plugin, isWatchPage: Boolean(parsedUrl.mediaId) };
44
-
}
45
-
}
46
-
return null;
42
+
const serviceMatch = getServiceDefinitionByUrl(url);
43
+
if (!serviceMatch) return null;
44
+
45
+
const plugin = getPlugin(serviceMatch.service.id);
46
+
return plugin ? { plugin, isWatchPage: serviceMatch.isWatchPage } : null;
47
47
}
+4
-25
apps/extension/src/utils/services/types.ts
+4
-25
apps/extension/src/utils/services/types.ts
···
1
-
import type { ServiceId } from '@open-watch-party/shared';
2
-
3
-
/** Presentation metadata rendered by the popup UI. */
4
-
export interface ServiceDescriptor {
5
-
readonly id: ServiceId;
6
-
readonly label: string;
7
-
readonly accent: string;
8
-
readonly accentContrast: string;
9
-
readonly glyph: string;
10
-
readonly watchPathHint: string;
11
-
}
1
+
import type { SupportedServiceDefinition } from '@open-watch-party/shared';
12
2
13
3
/**
14
-
* Self-contained service integration. A plugin bundles the service metadata,
15
-
* URL classifier, DOM selectors, and the one optional playback override needed
16
-
* to drive a service from a content script.
4
+
* Service integration plus the extension-only DOM hooks needed by a content script.
17
5
*/
18
-
export interface ServicePlugin {
19
-
readonly id: ServiceId;
20
-
readonly descriptor: ServiceDescriptor;
21
-
/** WXT-style match patterns; consumed at build time for the manifest. */
22
-
readonly contentMatches: readonly string[];
6
+
export type ServicePlugin = SupportedServiceDefinition & {
23
7
/** Shown when a watch URL matched but the `<video>` element isn't ready. */
24
8
readonly playerNotReadyMessage: string;
25
-
/**
26
-
* Returns null for URLs outside the service. A non-null result without
27
-
* `mediaId` means the URL belongs to the service but is not a watch page.
28
-
*/
29
-
parseUrl(url: string): { mediaId?: string } | null;
30
9
getVideo(): HTMLVideoElement | null;
31
10
getMediaTitle(): string;
32
-
}
11
+
};
+5
-44
apps/extension/src/utils/services/youtube.ts
+5
-44
apps/extension/src/utils/services/youtube.ts
···
1
+
import { getServiceDefinition } from '@open-watch-party/shared';
1
2
import type { ServicePlugin } from './types';
2
3
3
-
const YOUTUBE_HOST_RE = /(^|\.)(youtube\.com|youtu\.be|youtube-nocookie\.com)$/;
4
4
const YOUTUBE_TITLE_SUFFIX = /\s*-\s*YouTube$/i;
5
+
const YOUTUBE_DEFINITION = getServiceDefinition('youtube');
5
6
6
-
function parseYoutube(rawUrl: string): URL | null {
7
-
try {
8
-
const url = new URL(rawUrl);
9
-
return YOUTUBE_HOST_RE.test(url.hostname) ? url : null;
10
-
} catch {
11
-
return null;
12
-
}
13
-
}
14
-
15
-
function extractYoutubeMediaId(url: URL): string | undefined {
16
-
const host = url.hostname;
17
-
18
-
if (/(^|\.)youtube\.com$/.test(host)) {
19
-
if (url.pathname === '/watch') {
20
-
return url.searchParams.get('v') ?? undefined;
21
-
}
22
-
return url.pathname.match(/^\/(?:embed|live)\/([^/?#]+)/)?.[1] ?? undefined;
23
-
}
24
-
25
-
if (/(^|\.)youtube-nocookie\.com$/.test(host)) {
26
-
return url.pathname.match(/^\/embed\/([^/?#]+)/)?.[1] ?? undefined;
27
-
}
28
-
29
-
if (host === 'youtu.be') {
30
-
const id = url.pathname.replace(/^\//, '').split('/')[0];
31
-
return id || undefined;
32
-
}
33
-
34
-
return undefined;
7
+
if (!YOUTUBE_DEFINITION) {
8
+
throw new Error('YouTube service definition is missing.');
35
9
}
36
10
37
11
export const YOUTUBE_SERVICE: ServicePlugin = {
38
-
id: 'youtube',
39
-
descriptor: {
40
-
id: 'youtube',
41
-
label: 'YouTube',
42
-
accent: '#ff0033',
43
-
accentContrast: '#ffffff',
44
-
glyph: 'Y',
45
-
watchPathHint: 'youtube.com/watch?v=…',
46
-
},
47
-
contentMatches: ['*://*.youtube.com/*', '*://youtu.be/*', '*://*.youtube-nocookie.com/*'],
12
+
...YOUTUBE_DEFINITION,
48
13
playerNotReadyMessage: 'YouTube player is still loading.',
49
-
parseUrl: (url) => {
50
-
const parsed = parseYoutube(url);
51
-
return parsed ? { mediaId: extractYoutubeMediaId(parsed) } : null;
52
-
},
53
14
getVideo: () =>
54
15
document.querySelector<HTMLVideoElement>('#movie_player video, video.html5-main-video, video'),
55
16
getMediaTitle: () => document.title.replace(YOUTUBE_TITLE_SUFFIX, '').trim(),
+2
-6
apps/extension/wxt.config.ts
+2
-6
apps/extension/wxt.config.ts
···
1
1
import { defineConfig } from 'wxt';
2
2
import tailwindcss from '@tailwindcss/vite';
3
+
import { SUPPORTED_SERVICE_CONTENT_MATCHES } from '@open-watch-party/shared';
3
4
4
5
const LOCAL_SERVER_URL = 'http://localhost:8787';
5
6
···
19
20
'wss://*',
20
21
];
21
22
22
-
const hostPermissions = [
23
-
'*://*.netflix.com/*',
24
-
'*://*.youtube.com/*',
25
-
'*://youtu.be/*',
26
-
'*://*.youtube-nocookie.com/*',
27
-
];
23
+
const hostPermissions = [...SUPPORTED_SERVICE_CONTENT_MATCHES];
28
24
29
25
export default defineConfig({
30
26
srcDir: 'src',
+2
-1
apps/server/src/socket.ts
+2
-1
apps/server/src/socket.ts
···
507
507
const key = memberKey(roomCode, memberId);
508
508
const priorSocketId = state.activeSocketByMember.get(key);
509
509
const allowPlaybackUpdate =
510
-
state.sessionsBySocket.get(socketId)?.allowPlaybackUpdate ?? createPlaybackUpdateTokenConsumer();
510
+
state.sessionsBySocket.get(socketId)?.allowPlaybackUpdate ??
511
+
createPlaybackUpdateTokenConsumer();
511
512
512
513
if (priorSocketId && priorSocketId !== socketId) {
513
514
log.info(
History
3 rounds
0 comments
ruszabarov.tngl.sh
submitted
#2
1 commit
expand
collapse
centralize service definition
merge conflicts detected
expand
collapse
expand
collapse
- README.md:13
- apps/extension/src/components/popup/Lobby.svelte:59
- apps/extension/src/entrypoints/netflix.content.ts:1
- apps/extension/src/entrypoints/youtube.content.ts:1
- apps/extension/src/utils/active-tab.ts:40
- apps/extension/src/utils/background/controlled-tab-service.ts:19
- apps/extension/src/utils/services/dom-video.ts:1
- apps/extension/src/utils/services/netflix.ts:1
- apps/extension/src/utils/services/registry.ts:1
- apps/extension/src/utils/services/types.ts:1
- apps/extension/src/utils/services/youtube.ts:1
- apps/extension/wxt.config.ts:1
- apps/server/src/socket.ts:507
- packages/shared/src/protocol.ts:1
- packages/shared/src/room.test.ts:1
- packages/shared/src/room.ts:11
- packages/shared/src/services.ts:1
expand 0 comments
ruszabarov.tngl.sh
submitted
#1
1 commit
expand
collapse
centralize service definition
expand 0 comments
ruszabarov.tngl.sh
submitted
#0
1 commit
expand
collapse
centralize service definition