README.md
README.md
This file has not been changed.
+11
-9
apps/extension/src/utils/services/dom-video.ts
+11
-9
apps/extension/src/utils/services/dom-video.ts
···
1
-
import type { PlaybackUpdateDraft } from '@open-watch-party/shared';
1
+
import type { PlaybackUpdateDraft, ServiceId } from '@open-watch-party/shared';
2
2
import { defineContentScript } from 'wxt/utils/define-content-script';
3
3
4
4
import type { ServiceContentContext } from '../protocol/extension';
5
5
import { onMessage, sendMessage } from '../protocol/messaging';
6
-
import type { ServicePlugin } from './types';
6
+
import { SERVICE_PLUGIN_BY_ID } from './registry';
7
7
8
8
const VIDEO_EVENTS = ['play', 'pause', 'seeked', 'loadedmetadata', 'ended'] as const;
9
9
const SEEK_THRESHOLD_SEC = 1.5;
10
10
11
-
export function runServiceContentScript(plugin: ServicePlugin) {
11
+
export function runServiceContentScript(serviceId: ServiceId) {
12
+
const plugin = SERVICE_PLUGIN_BY_ID[serviceId];
13
+
12
14
return defineContentScript({
13
15
matches: [...plugin.contentMatches],
14
16
main() {
···
19
21
let stopped = false;
20
22
21
23
const readContext = (): ServiceContentContext | null => {
22
-
const mediaId = plugin.parseUrl(window.location.href)?.mediaId;
23
-
if (!activeVideo || !mediaId) return null;
24
+
const mediaId = plugin.extractMediaId(new URL(window.location.href));
25
+
if (!activeVideo || mediaId === null) return null;
24
26
25
27
return {
26
-
serviceId: plugin.id,
28
+
serviceId,
27
29
href: window.location.href,
28
30
title: document.title,
29
31
mediaTitle: plugin.getMediaTitle(),
···
32
34
};
33
35
34
36
const readPlayback = (): PlaybackUpdateDraft | null => {
35
-
const mediaId = plugin.parseUrl(window.location.href)?.mediaId;
36
-
if (!activeVideo || !mediaId) return null;
37
+
const mediaId = plugin.extractMediaId(new URL(window.location.href));
38
+
if (!activeVideo || mediaId === null) return null;
37
39
38
40
return {
39
-
serviceId: plugin.id,
41
+
serviceId,
40
42
mediaId,
41
43
title: plugin.getMediaTitle(),
42
44
positionSec: Number(activeVideo.currentTime.toFixed(3)),
+2
-6
apps/extension/src/utils/services/netflix.ts
+2
-6
apps/extension/src/utils/services/netflix.ts
···
1
-
import { getServiceDefinition } from '@open-watch-party/shared';
1
+
import { SERVICE_DEFINITION_BY_ID } from '@open-watch-party/shared';
2
2
import type { ServicePlugin } from './types';
3
3
4
4
const NETFLIX_TITLE_SUFFIX = /\s*-\s*Netflix$/i;
5
-
const NETFLIX_DEFINITION = getServiceDefinition('netflix');
6
-
7
-
if (!NETFLIX_DEFINITION) {
8
-
throw new Error('Netflix service definition is missing.');
9
-
}
5
+
const NETFLIX_DEFINITION = SERVICE_DEFINITION_BY_ID.netflix;
10
6
11
7
export const NETFLIX_SERVICE: ServicePlugin = {
12
8
...NETFLIX_DEFINITION,
+31
-16
apps/extension/src/utils/services/registry.ts
+31
-16
apps/extension/src/utils/services/registry.ts
···
1
-
import {
2
-
getServiceDefinitionByUrl,
3
-
type ServiceDescriptor,
4
-
type ServiceId,
5
-
} from '@open-watch-party/shared';
1
+
import { findServiceDefinitionByUrl, type ServiceId } from '@open-watch-party/shared';
6
2
7
3
import { NETFLIX_SERVICE } from './netflix';
8
4
import { YOUTUBE_SERVICE } from './youtube';
···
15
11
* 1. Add its shared definition to `packages/shared/src/services.ts`.
16
12
* 2. Create the extension DOM integration at `apps/extension/src/utils/services/<id>.ts`.
17
13
* 3. Add a one-line entrypoint at `src/entrypoints/<id>.content.ts` via
18
-
* `runServiceContentScript(MY_SERVICE)`.
14
+
* `runServiceContentScript('my-service-id')`.
19
15
* 4. Append the plugin below.
20
16
*/
21
-
export const SERVICE_PLUGINS: readonly ServicePlugin[] = [NETFLIX_SERVICE, YOUTUBE_SERVICE];
17
+
export const SERVICE_PLUGIN_BY_ID = {
18
+
netflix: NETFLIX_SERVICE,
19
+
youtube: YOUTUBE_SERVICE,
20
+
} satisfies Record<ServiceId, ServicePlugin>;
22
21
23
-
export const SUPPORTED_SERVICE_DESCRIPTORS: readonly ServiceDescriptor[] = SERVICE_PLUGINS.map(
24
-
(p) => p.descriptor,
25
-
);
22
+
export const SERVICE_PLUGINS = Object.values(SERVICE_PLUGIN_BY_ID);
23
+
24
+
type ServicePluginDescriptor = ServicePlugin['descriptor'];
25
+
26
+
export const SUPPORTED_SERVICE_DESCRIPTORS: readonly ServicePluginDescriptor[] =
27
+
SERVICE_PLUGINS.map((p) => p.descriptor);
26
28
27
29
export function getPlugin(id: ServiceId | null | undefined): ServicePlugin | null {
28
-
return SERVICE_PLUGINS.find((p) => p.id === id) ?? null;
30
+
return id ? SERVICE_PLUGIN_BY_ID[id] : null;
29
31
}
30
32
31
-
export function getServiceDescriptor(id: ServiceId | null | undefined): ServiceDescriptor | null {
33
+
export function getServiceDescriptor(
34
+
id: ServiceId | null | undefined,
35
+
): ServicePluginDescriptor | null {
32
36
return getPlugin(id)?.descriptor ?? null;
33
37
}
34
38
···
38
42
*/
39
43
export function findPluginByUrl(
40
44
url: string | null | undefined,
41
-
): { plugin: ServicePlugin; isWatchPage: boolean } | null {
42
-
const serviceMatch = getServiceDefinitionByUrl(url);
45
+
): { serviceId: ServiceId; plugin: ServicePlugin; isWatchPage: boolean } | null {
46
+
if (!url) return null;
47
+
48
+
let parsedUrl: URL;
49
+
try {
50
+
parsedUrl = new URL(url);
51
+
} catch {
52
+
return null;
53
+
}
54
+
55
+
const serviceMatch = findServiceDefinitionByUrl(parsedUrl);
43
56
if (!serviceMatch) return null;
44
57
45
-
const plugin = getPlugin(serviceMatch.service.id);
46
-
return plugin ? { plugin, isWatchPage: serviceMatch.isWatchPage } : null;
58
+
const plugin = getPlugin(serviceMatch.serviceId);
59
+
return plugin
60
+
? { serviceId: serviceMatch.serviceId, plugin, isWatchPage: serviceMatch.isWatchPage }
61
+
: null;
47
62
}
+2
-2
apps/extension/src/utils/services/types.ts
+2
-2
apps/extension/src/utils/services/types.ts
···
1
-
import type { SupportedServiceDefinition } from '@open-watch-party/shared';
1
+
import type { ServiceDefinition } from '@open-watch-party/shared';
2
2
3
3
/**
4
4
* Service integration plus the extension-only DOM hooks needed by a content script.
5
5
*/
6
-
export type ServicePlugin = SupportedServiceDefinition & {
6
+
export type ServicePlugin = ServiceDefinition & {
7
7
/** Shown when a watch URL matched but the `<video>` element isn't ready. */
8
8
readonly playerNotReadyMessage: string;
9
9
getVideo(): HTMLVideoElement | null;
+2
-6
apps/extension/src/utils/services/youtube.ts
+2
-6
apps/extension/src/utils/services/youtube.ts
···
1
-
import { getServiceDefinition } from '@open-watch-party/shared';
1
+
import { SERVICE_DEFINITION_BY_ID } from '@open-watch-party/shared';
2
2
import type { ServicePlugin } from './types';
3
3
4
4
const YOUTUBE_TITLE_SUFFIX = /\s*-\s*YouTube$/i;
5
-
const YOUTUBE_DEFINITION = getServiceDefinition('youtube');
6
-
7
-
if (!YOUTUBE_DEFINITION) {
8
-
throw new Error('YouTube service definition is missing.');
9
-
}
5
+
const YOUTUBE_DEFINITION = SERVICE_DEFINITION_BY_ID.youtube;
10
6
11
7
export const YOUTUBE_SERVICE: ServicePlugin = {
12
8
...YOUTUBE_DEFINITION,
apps/extension/wxt.config.ts
apps/extension/wxt.config.ts
This file has not been changed.
apps/server/src/socket.ts
apps/server/src/socket.ts
This file has not been changed.
+1
-1
apps/extension/src/components/popup/Lobby.svelte
+1
-1
apps/extension/src/components/popup/Lobby.svelte
···
59
59
60
60
<section class="flex flex-col gap-3">
61
61
<div class="flex items-start gap-3">
62
-
<ServiceBadge serviceId={activeDescriptor?.id ?? null} />
62
+
<ServiceBadge serviceId={activeTab.activeServiceId} />
63
63
<div class="min-w-0 space-y-1">
64
64
<p class="m-0 text-sm font-semibold leading-5">
65
65
{title}
+1
-2
apps/extension/src/entrypoints/netflix.content.ts
+1
-2
apps/extension/src/entrypoints/netflix.content.ts
+1
-2
apps/extension/src/entrypoints/youtube.content.ts
+1
-2
apps/extension/src/entrypoints/youtube.content.ts
+1
-1
apps/extension/src/utils/active-tab.ts
+1
-1
apps/extension/src/utils/active-tab.ts
+10
-3
apps/extension/src/utils/background/controlled-tab-service.ts
+10
-3
apps/extension/src/utils/background/controlled-tab-service.ts
···
19
19
playback: PlaybackUpdateDraft;
20
20
}
21
21
22
+
function isPluginUrl(plugin: { matchesUrl(url: URL): boolean }, rawUrl: string): boolean {
23
+
try {
24
+
return plugin.matchesUrl(new URL(rawUrl));
25
+
} catch {
26
+
return false;
27
+
}
28
+
}
29
+
22
30
export class ControlledTabService {
23
31
constructor(
24
32
private readonly state: BackgroundState,
···
35
43
if (tabId === controlledTab?.tabId && tab.url) {
36
44
const session = selectSession(this.state);
37
45
const sessionPlugin = session ? getPlugin(session.serviceId) : null;
38
-
if (sessionPlugin && !sessionPlugin.parseUrl(tab.url)) {
46
+
if (sessionPlugin && !isPluginUrl(sessionPlugin, tab.url)) {
39
47
this.state.lastWarning = `The controlled tab left ${sessionPlugin.descriptor.label}.`;
40
48
syncBackgroundState(this.state);
41
49
}
···
164
172
throw new Error('This tab is not on a supported streaming service.');
165
173
}
166
174
167
-
const parsedContextUrl = plugin.parseUrl(context.href);
168
-
if (!parsedContextUrl?.mediaId) {
175
+
if (plugin.extractMediaId(new URL(context.href)) === null) {
169
176
throw new Error(`${plugin.descriptor.label} tab is not on a supported watch page.`);
170
177
}
171
178
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