homebridge-stream-triggers 0.1.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,15 +30,30 @@ fire-and-forget in the background. Repeat flips while a launch is in flight are
30
30
 
31
31
  ## Configuration
32
32
 
33
+ Zero-config by default: the channel list is fetched from
34
+ [omni-notify](https://github.com/micthiesen/omni-notify)'s `/api/trigger-channels`
35
+ endpoint (one channel per monitored streamer, YouTube preferred over Twitch, Kick-only
36
+ streamers skipped), so adding a streamer there is all it takes.
37
+
38
+ ```json
39
+ {
40
+ "platform": "StreamTriggers"
41
+ }
42
+ ```
43
+
44
+ The list re-syncs hourly (`channelsRefreshInterval`), so new streamers appear without a
45
+ restart. If omni-notify is unreachable at startup, previously-known switches keep working
46
+ from the accessory cache (the channel is stored in each accessory's context) and fetching
47
+ retries every minute until it succeeds.
48
+
49
+ A static list can be set instead (mainly for testing; it disables fetching entirely):
50
+
33
51
  ```json
34
52
  {
35
53
  "platform": "StreamTriggers",
36
54
  "channels": [
37
- { "key": "destiny", "type": "youtube", "url": "https://www.youtube.com/@destiny/live" },
38
- { "key": "hutch", "type": "youtube", "url": "https://www.youtube.com/@Hutch/live" },
39
- { "key": "wilburgur", "type": "youtube", "url": "https://www.youtube.com/@Wilburgur/live" },
40
- { "key": "vinesauce", "type": "twitch" },
41
- { "key": "jerma", "type": "twitch" }
55
+ { "key": "destiny", "type": "youtube", "url": "https://www.youtube.com/@destiny/live" },
56
+ { "key": "jerma", "type": "twitch" }
42
57
  ]
43
58
  }
44
59
  ```
@@ -56,6 +71,8 @@ fire-and-forget in the background. Repeat flips while a launch is in flight are
56
71
 
57
72
  | Field | Default | Description |
58
73
  | --- | --- | --- |
74
+ | `channelsUrl` | `http://omni.boris/api/trigger-channels` | Endpoint serving the channel list; used when `channels` is empty. |
75
+ | `channelsRefreshInterval` | `3600000` (1h) | How often (ms) to re-fetch the list and re-sync switches. `0` disables background refresh. |
59
76
  | `credentialsDir` | `/var/lib/homebridge/appletv-enhanced` | Where homebridge-appletv-enhanced stores Apple TV pairings. |
60
77
  | `appleTvId` | auto | Apple TV identifier. Auto-discovered as the single subdirectory of `credentialsDir` containing a `credentials.txt`; set explicitly if there are multiple. |
61
78
  | `atvremotePath` | `<credentialsDir>/.venv/bin/atvremote` path | `atvremote` binary from appletv-enhanced's venv (that plugin keeps pyatv updated). |
@@ -10,8 +10,21 @@
10
10
  "type": "string",
11
11
  "default": "Stream Triggers"
12
12
  },
13
+ "channelsUrl": {
14
+ "title": "Channels URL",
15
+ "description": "Endpoint serving the channel list (omni-notify's /api/trigger-channels). Used when the static channel list below is empty.",
16
+ "type": "string",
17
+ "default": "http://omni.boris/api/trigger-channels"
18
+ },
19
+ "channelsRefreshInterval": {
20
+ "title": "Channel Refresh Interval (ms)",
21
+ "description": "How often to re-fetch the channel list and re-sync switches. 0 disables background refresh.",
22
+ "type": "number",
23
+ "default": 3600000
24
+ },
13
25
  "channels": {
14
- "title": "Channels",
26
+ "title": "Channels (static override)",
27
+ "description": "Optional static channel list. When non-empty, the Channels URL is never fetched.",
15
28
  "type": "array",
16
29
  "items": {
17
30
  "type": "object",
package/lib/atv.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { Logging } from "homebridge";
2
+ import type { StreamTriggersConfig } from "./config.js";
3
+ /**
4
+ * Thin wrapper around the atvremote binary from homebridge-appletv-enhanced's venv,
5
+ * reusing that plugin's Companion pairing credentials.
6
+ */
7
+ export declare class AppleTv {
8
+ private readonly log;
9
+ private readonly config;
10
+ private discoveredId;
11
+ constructor(log: Logging, config: Pick<StreamTriggersConfig, "appleTvId" | "atvremotePath" | "credentialsDir">);
12
+ /** Run one atvremote command (e.g. "turn_on"). Logs failures; never throws. */
13
+ run(command: string, prefix: string): Promise<boolean>;
14
+ /**
15
+ * Explicit appleTvId, or auto-discovery: the single subdirectory of credentialsDir
16
+ * containing a credentials.txt. Logs an error (never crashes) on zero or multiple.
17
+ */
18
+ resolveId(prefix: string): Promise<string | undefined>;
19
+ /** Companion credentials = first line of <credentialsDir>/<id>/credentials.txt. */
20
+ private readCredentials;
21
+ }
22
+ //# sourceMappingURL=atv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atv.d.ts","sourceRoot":"","sources":["../src/atv.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAKxD;;;GAGG;AACH,qBAAa,OAAO;IAIhB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJzB,OAAO,CAAC,YAAY,CAAqB;gBAGtB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,IAAI,CAC3B,oBAAoB,EACpB,WAAW,GAAG,eAAe,GAAG,gBAAgB,CACjD;IAGH,+EAA+E;IACzE,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqB5D;;;OAGG;IACG,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAsC5D,mFAAmF;YACrE,eAAe;CAiB9B"}
package/lib/atv.js ADDED
@@ -0,0 +1,86 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { tryCatch } from "@micthiesen/mitools/async";
4
+ import { describeExecError, runCommand } from "./exec.js";
5
+ const ATVREMOTE_TIMEOUT_MS = 30_000;
6
+ /**
7
+ * Thin wrapper around the atvremote binary from homebridge-appletv-enhanced's venv,
8
+ * reusing that plugin's Companion pairing credentials.
9
+ */
10
+ export class AppleTv {
11
+ log;
12
+ config;
13
+ discoveredId;
14
+ constructor(log, config) {
15
+ this.log = log;
16
+ this.config = config;
17
+ }
18
+ /** Run one atvremote command (e.g. "turn_on"). Logs failures; never throws. */
19
+ async run(command, prefix) {
20
+ const id = await this.resolveId(prefix);
21
+ if (!id)
22
+ return false;
23
+ const credentials = await this.readCredentials(id, prefix);
24
+ if (!credentials)
25
+ return false;
26
+ this.log.debug(`${prefix} atvremote ${command}`);
27
+ const result = await runCommand(this.config.atvremotePath, ["--id", id, "--companion-credentials", credentials, command], ATVREMOTE_TIMEOUT_MS);
28
+ if (!result.ok) {
29
+ this.log.error(`${prefix} atvremote ${command} failed: ${describeExecError(result.error)}`);
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+ /**
35
+ * Explicit appleTvId, or auto-discovery: the single subdirectory of credentialsDir
36
+ * containing a credentials.txt. Logs an error (never crashes) on zero or multiple.
37
+ */
38
+ async resolveId(prefix) {
39
+ if (this.config.appleTvId)
40
+ return this.config.appleTvId;
41
+ if (this.discoveredId)
42
+ return this.discoveredId;
43
+ const dir = this.config.credentialsDir;
44
+ const candidates = await tryCatch(async () => {
45
+ const entries = await fs.readdir(dir, { withFileTypes: true });
46
+ const found = [];
47
+ for (const entry of entries) {
48
+ if (!entry.isDirectory())
49
+ continue;
50
+ const hasCredentials = await tryCatch(() => fs.access(path.join(dir, entry.name, "credentials.txt")));
51
+ if (hasCredentials.ok)
52
+ found.push(entry.name);
53
+ }
54
+ return found;
55
+ });
56
+ if (!candidates.ok) {
57
+ this.log.error(`${prefix} Cannot read credentials dir ${dir}: ${candidates.error.message}`);
58
+ return undefined;
59
+ }
60
+ if (candidates.value.length !== 1) {
61
+ this.log.error(`${prefix} Expected exactly one Apple TV pairing in ${dir}, found ` +
62
+ `${candidates.value.length} (${candidates.value.join(", ") || "none"}). ` +
63
+ `Set "appleTvId" explicitly.`);
64
+ return undefined;
65
+ }
66
+ this.discoveredId = candidates.value[0];
67
+ this.log.info(`${prefix} Auto-discovered Apple TV id ${this.discoveredId}`);
68
+ return this.discoveredId;
69
+ }
70
+ /** Companion credentials = first line of <credentialsDir>/<id>/credentials.txt. */
71
+ async readCredentials(id, prefix) {
72
+ const file = path.join(this.config.credentialsDir, id, "credentials.txt");
73
+ const result = await tryCatch(() => fs.readFile(file, "utf8"));
74
+ if (!result.ok) {
75
+ this.log.error(`${prefix} Cannot read ${file}: ${result.error.message}`);
76
+ return undefined;
77
+ }
78
+ const credentials = result.value.split("\n")[0]?.trim();
79
+ if (!credentials) {
80
+ this.log.error(`${prefix} ${file} is empty`);
81
+ return undefined;
82
+ }
83
+ return credentials;
84
+ }
85
+ }
86
+ //# sourceMappingURL=atv.js.map
package/lib/atv.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atv.js","sourceRoot":"","sources":["../src/atv.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { Logging } from "homebridge";
2
+ import { type ChannelConfig } from "./config.js";
3
+ /**
4
+ * Fetch the channel list from omni-notify's /api/trigger-channels endpoint.
5
+ * Returns undefined on any failure (logged); callers fall back to the
6
+ * accessory cache so previously-known switches keep working.
7
+ */
8
+ export declare function fetchChannels(url: string, log: Logging): Promise<ChannelConfig[] | undefined>;
9
+ //# sourceMappingURL=channels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channels.d.ts","sourceRoot":"","sources":["../src/channels.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,aAAa,CAAC;AAKhE;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,aAAa,EAAE,GAAG,SAAS,CAAC,CAatC"}
@@ -0,0 +1,26 @@
1
+ import { tryCatch } from "@micthiesen/mitools/async";
2
+ import { z } from "zod";
3
+ import { channelSchema } from "./config.js";
4
+ const responseSchema = z.object({ channels: z.array(channelSchema) });
5
+ const FETCH_TIMEOUT_MS = 10_000;
6
+ /**
7
+ * Fetch the channel list from omni-notify's /api/trigger-channels endpoint.
8
+ * Returns undefined on any failure (logged); callers fall back to the
9
+ * accessory cache so previously-known switches keep working.
10
+ */
11
+ export async function fetchChannels(url, log) {
12
+ const result = await tryCatch(async () => {
13
+ const response = await fetch(url, {
14
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
15
+ });
16
+ if (!response.ok)
17
+ throw new Error(`HTTP ${response.status}`);
18
+ return responseSchema.parse(await response.json());
19
+ });
20
+ if (!result.ok) {
21
+ log.error(`Failed to fetch channels from ${url}: ${result.error.message}`);
22
+ return undefined;
23
+ }
24
+ return result.value.channels;
25
+ }
26
+ //# sourceMappingURL=channels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channels.js","sourceRoot":"","sources":["../src/channels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAErD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAsB,aAAa,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;AACtE,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,GAAW,EACX,GAAY;IAEZ,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,KAAK,IAAI,EAAE;QACvC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAChC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC;SAC9C,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7D,OAAO,cAAc,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,GAAG,CAAC,KAAK,CAAC,iCAAiC,GAAG,KAAK,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3E,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC;AAC/B,CAAC"}
package/lib/config.d.ts CHANGED
@@ -19,6 +19,8 @@ export declare const configSchema: z.ZodObject<{
19
19
  }>;
20
20
  url: z.ZodOptional<z.ZodString>;
21
21
  }, z.core.$strip>>>;
22
+ channelsUrl: z.ZodDefault<z.ZodString>;
23
+ channelsRefreshInterval: z.ZodDefault<z.ZodNumber>;
22
24
  credentialsDir: z.ZodDefault<z.ZodString>;
23
25
  appleTvId: z.ZodOptional<z.ZodString>;
24
26
  atvremotePath: z.ZodDefault<z.ZodString>;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,aAAa;;;;;;;;iBAKxB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;iBAUvB,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAEhE,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,CAE9D;AAED,oDAAoD;AACpD,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAG7D"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,aAAa;;;;;;;;iBAKxB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;iBAcvB,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAEhE,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,CAE9D;AAED,oDAAoD;AACpD,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAG7D"}
package/lib/config.js CHANGED
@@ -6,7 +6,11 @@ export const channelSchema = z.object({
6
6
  url: z.string().min(1).optional(),
7
7
  });
8
8
  export const configSchema = z.object({
9
+ /** Static override; when empty the list is fetched from channelsUrl. */
9
10
  channels: z.array(channelSchema).default([]),
11
+ channelsUrl: z.string().default("http://omni.boris/api/trigger-channels"),
12
+ /** How often to re-fetch the channel list and re-sync switches. 0 disables. */
13
+ channelsRefreshInterval: z.number().int().nonnegative().default(3_600_000),
10
14
  credentialsDir: z.string().default("/var/lib/homebridge/appletv-enhanced"),
11
15
  appleTvId: z.string().optional(),
12
16
  atvremotePath: z
package/lib/config.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACtB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACzC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACnC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC5C,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC;IAC1E,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,aAAa,EAAE,CAAC;SACb,MAAM,EAAE;SACR,OAAO,CAAC,0DAA0D,CAAC;IACtE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC;IACtC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;CACtD,CAAC,CAAC;AAIH,+EAA+E;AAC/E,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,OAAO,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACjC,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,cAAc,CAAC,OAAsB;IACnD,IAAI,OAAO,CAAC,WAAW;QAAE,OAAO,OAAO,CAAC,WAAW,CAAC;IACpD,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpE,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACtB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACzC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACnC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,wEAAwE;IACxE,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC5C,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,wCAAwC,CAAC;IACzE,+EAA+E;IAC/E,uBAAuB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC;IAC1E,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC;IAC1E,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,aAAa,EAAE,CAAC;SACb,MAAM,EAAE;SACR,OAAO,CAAC,0DAA0D,CAAC;IACtE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC;IACtC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;CACtD,CAAC,CAAC;AAIH,+EAA+E;AAC/E,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,OAAO,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACjC,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,cAAc,CAAC,OAAsB;IACnD,IAAI,OAAO,CAAC,WAAW;QAAE,OAAO,OAAO,CAAC,WAAW,CAAC;IACpD,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpE,CAAC"}
package/lib/exec.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { type Result } from "@micthiesen/mitools/async";
2
+ export interface ExecOutput {
3
+ stdout: string;
4
+ stderr: string;
5
+ }
6
+ /** Run a binary with a hard timeout, returning a Result instead of throwing. */
7
+ export declare function runCommand(file: string, args: string[], timeoutMs: number): Promise<Result<ExecOutput>>;
8
+ /** Human-readable one-liner for a failed subprocess. */
9
+ export declare function describeExecError(error: Error): string;
10
+ /** True when the subprocess failed because the binary itself is missing. */
11
+ export declare function isMissingBinaryError(error: Error): boolean;
12
+ /** Exit code of a failed subprocess, if it ran and exited non-zero. */
13
+ export declare function exitCodeOf(error: Error): number | undefined;
14
+ //# sourceMappingURL=exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../src/exec.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,MAAM,EAAY,MAAM,2BAA2B,CAAC;AAIlE,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,gFAAgF;AAChF,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAQ7B;AAED,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAStD;AAED,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAE1D;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAG3D"}
package/lib/exec.js ADDED
@@ -0,0 +1,35 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { tryCatch } from "@micthiesen/mitools/async";
4
+ const execFileAsync = promisify(execFile);
5
+ /** Run a binary with a hard timeout, returning a Result instead of throwing. */
6
+ export async function runCommand(file, args, timeoutMs) {
7
+ return tryCatch(async () => {
8
+ const { stdout, stderr } = await execFileAsync(file, args, {
9
+ timeout: timeoutMs,
10
+ maxBuffer: 16 * 1024 * 1024,
11
+ });
12
+ return { stdout, stderr };
13
+ });
14
+ }
15
+ /** Human-readable one-liner for a failed subprocess. */
16
+ export function describeExecError(error) {
17
+ const execError = error;
18
+ if (execError.code === "ENOENT")
19
+ return "binary not found (ENOENT)";
20
+ if (execError.killed)
21
+ return `timed out and was killed (${execError.signal ?? "signal"})`;
22
+ const stderr = execError.stderr?.trim().split("\n").at(-1);
23
+ const exit = typeof execError.code === "number" ? `exit ${execError.code}` : error.message;
24
+ return stderr ? `${exit}: ${stderr}` : exit;
25
+ }
26
+ /** True when the subprocess failed because the binary itself is missing. */
27
+ export function isMissingBinaryError(error) {
28
+ return error.code === "ENOENT";
29
+ }
30
+ /** Exit code of a failed subprocess, if it ran and exited non-zero. */
31
+ export function exitCodeOf(error) {
32
+ const code = error.code;
33
+ return typeof code === "number" ? code : undefined;
34
+ }
35
+ //# sourceMappingURL=exec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.js","sourceRoot":"","sources":["../src/exec.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAe,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAElE,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAO1C,gFAAgF;AAChF,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,IAAc,EACd,SAAiB;IAEjB,OAAO,QAAQ,CAAC,KAAK,IAAI,EAAE;QACzB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE;YACzD,OAAO,EAAE,SAAS;YAClB,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;SAC5B,CAAC,CAAC;QACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,iBAAiB,CAAC,KAAY;IAC5C,MAAM,SAAS,GAAG,KAAgD,CAAC;IACnE,IAAI,SAAS,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,2BAA2B,CAAC;IACpE,IAAI,SAAS,CAAC,MAAM;QAClB,OAAO,6BAA6B,SAAS,CAAC,MAAM,IAAI,QAAQ,GAAG,CAAC;IACtE,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,MAAM,IAAI,GACR,OAAO,SAAS,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;IAChF,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9C,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,oBAAoB,CAAC,KAAY;IAC/C,OAAQ,KAA2B,CAAC,IAAI,KAAK,QAAQ,CAAC;AACxD,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,UAAU,CAAC,KAAY;IACrC,MAAM,IAAI,GAAI,KAA2B,CAAC,IAAI,CAAC;IAC/C,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AACrD,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { Logging } from "homebridge";
2
+ import type { AppleTv } from "./atv.js";
3
+ import type { ChannelConfig } from "./config.js";
4
+ import type { YtDlp } from "./ytdlp.js";
5
+ /**
6
+ * The launch flow: wake -> app_list prime -> resolve -> launch_app.
7
+ *
8
+ * The app_list prime is REQUIRED: after an Apple TV reboot (e.g. a tvOS auto-update),
9
+ * tvOS silently drops app-launch requests with a Companion protocol timeout until some
10
+ * client has requested the app list once (documented pyatv FAQ behavior). Priming on
11
+ * every launch keeps the system self-healing across tvOS updates.
12
+ */
13
+ export declare class StreamLauncher {
14
+ private readonly log;
15
+ private readonly atv;
16
+ private readonly ytDlp;
17
+ constructor(log: Logging, atv: AppleTv, ytDlp: YtDlp);
18
+ /** Run the full launch flow for a channel. Logs every step; never throws. */
19
+ launch(channel: ChannelConfig): Promise<void>;
20
+ private resolveUri;
21
+ }
22
+ //# sourceMappingURL=launcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launcher.d.ts","sourceRoot":"","sources":["../src/launcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC;;;;;;;GAOG;AACH,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAFL,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,KAAK;IAG/B,6EAA6E;IACvE,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;YAqBrC,UAAU;CAiBzB"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The launch flow: wake -> app_list prime -> resolve -> launch_app.
3
+ *
4
+ * The app_list prime is REQUIRED: after an Apple TV reboot (e.g. a tvOS auto-update),
5
+ * tvOS silently drops app-launch requests with a Companion protocol timeout until some
6
+ * client has requested the app list once (documented pyatv FAQ behavior). Priming on
7
+ * every launch keeps the system self-healing across tvOS updates.
8
+ */
9
+ export class StreamLauncher {
10
+ log;
11
+ atv;
12
+ ytDlp;
13
+ constructor(log, atv, ytDlp) {
14
+ this.log = log;
15
+ this.atv = atv;
16
+ this.ytDlp = ytDlp;
17
+ }
18
+ /** Run the full launch flow for a channel. Logs every step; never throws. */
19
+ async launch(channel) {
20
+ const prefix = `[${channel.key}]`;
21
+ try {
22
+ this.log.info(`${prefix} Launch requested (${channel.type})`);
23
+ if (!(await this.atv.run("turn_on", prefix)))
24
+ return;
25
+ this.log.info(`${prefix} Apple TV awake`);
26
+ if (!(await this.atv.run("app_list", prefix)))
27
+ return;
28
+ this.log.info(`${prefix} App list primed`);
29
+ const uri = await this.resolveUri(channel, prefix);
30
+ if (!uri)
31
+ return;
32
+ if (!(await this.atv.run(`launch_app=${uri}`, prefix)))
33
+ return;
34
+ this.log.info(`${prefix} Launched ${uri}`);
35
+ }
36
+ catch (error) {
37
+ this.log.error(`${prefix} Launch failed unexpectedly: ${String(error)}`);
38
+ }
39
+ }
40
+ async resolveUri(channel, prefix) {
41
+ if (channel.type === "twitch") {
42
+ // The tvOS Twitch app has no deep links; opening the app is the intended behavior.
43
+ return "tv.twitch";
44
+ }
45
+ if (!channel.url) {
46
+ this.log.error(`${prefix} YouTube channel is missing "url"`);
47
+ return undefined;
48
+ }
49
+ const videoId = await this.ytDlp.resolveLiveVideoId(channel.url, prefix);
50
+ if (!videoId)
51
+ return undefined; // not live or yt-dlp unavailable; already logged
52
+ this.log.info(`${prefix} Live video id: ${videoId}`);
53
+ return `youtube://www.youtube.com/watch?v=${videoId}`;
54
+ }
55
+ }
56
+ //# sourceMappingURL=launcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launcher.js","sourceRoot":"","sources":["../src/launcher.ts"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AACH,MAAM,OAAO,cAAc;IAEN;IACA;IACA;IAHnB,YACmB,GAAY,EACZ,GAAY,EACZ,KAAY;QAFZ,QAAG,GAAH,GAAG,CAAS;QACZ,QAAG,GAAH,GAAG,CAAS;QACZ,UAAK,GAAL,KAAK,CAAO;IAC5B,CAAC;IAEJ,6EAA6E;IAC7E,KAAK,CAAC,MAAM,CAAC,OAAsB;QACjC,MAAM,MAAM,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,CAAC;QAClC,IAAI,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,sBAAsB,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;YAE9D,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;gBAAE,OAAO;YACrD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,iBAAiB,CAAC,CAAC;YAE1C,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;gBAAE,OAAO;YACtD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,kBAAkB,CAAC,CAAC;YAE3C,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YACnD,IAAI,CAAC,GAAG;gBAAE,OAAO;YAEjB,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,cAAc,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;gBAAE,OAAO;YAC/D,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,aAAa,GAAG,EAAE,CAAC,CAAC;QAC7C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,MAAM,gCAAgC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,UAAU,CACtB,OAAsB,EACtB,MAAc;QAEd,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC9B,mFAAmF;YACnF,OAAO,WAAW,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;YACjB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,MAAM,mCAAmC,CAAC,CAAC;YAC7D,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACzE,IAAI,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC,CAAC,iDAAiD;QACjF,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,mBAAmB,OAAO,EAAE,CAAC,CAAC;QACrD,OAAO,qCAAqC,OAAO,EAAE,CAAC;IACxD,CAAC;CACF"}
package/lib/platform.d.ts CHANGED
@@ -7,11 +7,26 @@ export declare class StreamTriggersPlatform implements DynamicPlatformPlugin {
7
7
  private readonly config;
8
8
  private readonly accessories;
9
9
  private readonly launchesInFlight;
10
+ private readonly ytDlp;
11
+ private readonly launcher;
10
12
  constructor(log: Logging, rawConfig: PlatformConfig, api: API);
13
+ private startup;
14
+ /**
15
+ * Background re-sync so channel changes in omni-notify appear without a
16
+ * Homebridge restart. Failures retry sooner (covers omni-notify still
17
+ * booting after a host reboot) but never touch the current accessories.
18
+ */
19
+ private scheduleRefresh;
11
20
  /** Called by Homebridge for each accessory restored from cache at startup. */
12
21
  configureAccessory(accessory: PlatformAccessory): void;
13
- /** Register switches for configured channels; unregister ones no longer configured. */
22
+ /** Register switches for the given channels; unregister ones no longer present. */
14
23
  private syncAccessories;
24
+ /**
25
+ * Fallback when the channel list is unreachable at startup: wire switches
26
+ * from the channel stored in each cached accessory's context, so everything
27
+ * previously known keeps working. Nothing is registered or unregistered.
28
+ */
29
+ private wireCachedAccessories;
15
30
  private setUpSwitch;
16
31
  /** Fire-and-forget launch; never throws, never awaited by the set handler. */
17
32
  private handleTrigger;
@@ -1 +1 @@
1
- {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,GAAG,EACH,cAAc,EACd,qBAAqB,EACrB,OAAO,EACP,iBAAiB,EACjB,cAAc,EACd,OAAO,EACR,MAAM,YAAY,CAAC;AASpB,qBAAa,sBAAuB,YAAW,qBAAqB;aAShD,GAAG,EAAE,OAAO;aAEZ,GAAG,EAAE,GAAG;IAV1B,SAAgB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxC,SAAgB,cAAc,EAAE,OAAO,cAAc,CAAC;IAEtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwC;IACpE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;gBAGpC,GAAG,EAAE,OAAO,EAC5B,SAAS,EAAE,cAAc,EACT,GAAG,EAAE,GAAG;IAwB1B,8EAA8E;IAC9E,kBAAkB,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI;IAKtD,uFAAuF;IACvF,OAAO,CAAC,eAAe;IAoCvB,OAAO,CAAC,WAAW;IA2BnB,8EAA8E;IAC9E,OAAO,CAAC,aAAa;CAUtB"}
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,GAAG,EACH,cAAc,EACd,qBAAqB,EACrB,OAAO,EACP,iBAAiB,EACjB,cAAc,EACd,OAAO,EACR,MAAM,YAAY,CAAC;AAgBpB,qBAAa,sBAAuB,YAAW,qBAAqB;aAWhD,GAAG,EAAE,OAAO;aAEZ,GAAG,EAAE,GAAG;IAZ1B,SAAgB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxC,SAAgB,cAAc,EAAE,OAAO,cAAc,CAAC;IAEtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwC;IACpE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;gBAGxB,GAAG,EAAE,OAAO,EAC5B,SAAS,EAAE,cAAc,EACT,GAAG,EAAE,GAAG;YAuBZ,OAAO;IAuBrB;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAkBvB,8EAA8E;IAC9E,kBAAkB,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI;IAKtD,mFAAmF;IACnF,OAAO,CAAC,eAAe;IAsCvB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAiB7B,OAAO,CAAC,WAAW;IA2BnB,8EAA8E;IAC9E,OAAO,CAAC,aAAa;CAiBtB"}
package/lib/platform.js CHANGED
@@ -1,5 +1,10 @@
1
- import { configSchema, displayNameFor, } from "./config.js";
1
+ import { AppleTv } from "./atv.js";
2
+ import { fetchChannels } from "./channels.js";
3
+ import { channelSchema, configSchema, displayNameFor, } from "./config.js";
4
+ import { StreamLauncher } from "./launcher.js";
2
5
  import { PLATFORM_NAME, PLUGIN_NAME } from "./settings.js";
6
+ import { YtDlp } from "./ytdlp.js";
7
+ const REFRESH_RETRY_MS = 60_000;
3
8
  export class StreamTriggersPlatform {
4
9
  log;
5
10
  api;
@@ -8,6 +13,8 @@ export class StreamTriggersPlatform {
8
13
  config;
9
14
  accessories = new Map();
10
15
  launchesInFlight = new Set();
16
+ ytDlp;
17
+ launcher;
11
18
  constructor(log, rawConfig, api) {
12
19
  this.log = log;
13
20
  this.api = api;
@@ -21,24 +28,68 @@ export class StreamTriggersPlatform {
21
28
  this.log.error(`Invalid config, running with no channels: ${parsed.error.message}`);
22
29
  this.config = configSchema.parse({});
23
30
  }
31
+ this.ytDlp = new YtDlp(log, api.user.storagePath(), this.config.ytDlpPath);
32
+ this.launcher = new StreamLauncher(log, new AppleTv(log, this.config), this.ytDlp);
24
33
  api.on("didFinishLaunching", () => {
34
+ void this.startup();
35
+ });
36
+ }
37
+ async startup() {
38
+ try {
39
+ if (this.config.channels.length > 0) {
40
+ // Static config override (mainly for testing); no fetching involved.
41
+ this.syncAccessories(this.config.channels);
42
+ }
43
+ else {
44
+ const channels = await fetchChannels(this.config.channelsUrl, this.log);
45
+ if (channels) {
46
+ this.syncAccessories(channels);
47
+ }
48
+ else {
49
+ this.wireCachedAccessories();
50
+ }
51
+ this.scheduleRefresh(channels ? this.config.channelsRefreshInterval : REFRESH_RETRY_MS);
52
+ }
53
+ }
54
+ catch (error) {
55
+ this.log.error(`Startup failed: ${String(error)}`);
56
+ }
57
+ // Non-blocking: download/refresh the managed yt-dlp binary in the background.
58
+ void this.ytDlp.ensureFresh();
59
+ }
60
+ /**
61
+ * Background re-sync so channel changes in omni-notify appear without a
62
+ * Homebridge restart. Failures retry sooner (covers omni-notify still
63
+ * booting after a host reboot) but never touch the current accessories.
64
+ */
65
+ scheduleRefresh(delayMs) {
66
+ if (this.config.channelsRefreshInterval <= 0)
67
+ return;
68
+ const timer = setTimeout(async () => {
69
+ let nextDelayMs = REFRESH_RETRY_MS;
25
70
  try {
26
- this.syncAccessories();
71
+ const channels = await fetchChannels(this.config.channelsUrl, this.log);
72
+ if (channels) {
73
+ this.syncAccessories(channels);
74
+ nextDelayMs = this.config.channelsRefreshInterval;
75
+ }
27
76
  }
28
77
  catch (error) {
29
- this.log.error(`Failed to sync accessories: ${String(error)}`);
78
+ this.log.error(`Channel refresh failed: ${String(error)}`);
30
79
  }
31
- });
80
+ this.scheduleRefresh(nextDelayMs);
81
+ }, delayMs);
82
+ timer.unref?.();
32
83
  }
33
84
  /** Called by Homebridge for each accessory restored from cache at startup. */
34
85
  configureAccessory(accessory) {
35
86
  this.log.info(`Restoring cached accessory: ${accessory.displayName}`);
36
87
  this.accessories.set(accessory.UUID, accessory);
37
88
  }
38
- /** Register switches for configured channels; unregister ones no longer configured. */
39
- syncAccessories() {
89
+ /** Register switches for the given channels; unregister ones no longer present. */
90
+ syncAccessories(channels) {
40
91
  const configuredUuids = new Set();
41
- for (const channel of this.config.channels) {
92
+ for (const channel of channels) {
42
93
  if (channel.type === "youtube" && !channel.url) {
43
94
  this.log.error(`[${channel.key}] YouTube channel is missing "url"; skipping`);
44
95
  continue;
@@ -49,12 +100,14 @@ export class StreamTriggersPlatform {
49
100
  const existing = this.accessories.get(uuid);
50
101
  if (existing) {
51
102
  existing.displayName = name;
103
+ existing.context.channel = channel;
52
104
  this.setUpSwitch(existing, channel, name);
53
105
  this.api.updatePlatformAccessories([existing]);
54
- this.log.info(`[${channel.key}] Updated switch "${name}"`);
106
+ this.log.debug(`[${channel.key}] Updated switch "${name}"`);
55
107
  }
56
108
  else {
57
109
  const accessory = new this.api.platformAccessory(name, uuid);
110
+ accessory.context.channel = channel;
58
111
  this.setUpSwitch(accessory, channel, name);
59
112
  this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
60
113
  this.accessories.set(uuid, accessory);
@@ -69,6 +122,25 @@ export class StreamTriggersPlatform {
69
122
  this.log.info(`Unregistered switch "${accessory.displayName}" (channel removed)`);
70
123
  }
71
124
  }
125
+ /**
126
+ * Fallback when the channel list is unreachable at startup: wire switches
127
+ * from the channel stored in each cached accessory's context, so everything
128
+ * previously known keeps working. Nothing is registered or unregistered.
129
+ */
130
+ wireCachedAccessories() {
131
+ let wired = 0;
132
+ for (const accessory of this.accessories.values()) {
133
+ const channel = channelSchema.safeParse(accessory.context.channel);
134
+ if (!channel.success) {
135
+ this.log.warn(`Cached accessory "${accessory.displayName}" has no stored channel; ` +
136
+ `it stays inert until the channel list is reachable`);
137
+ continue;
138
+ }
139
+ this.setUpSwitch(accessory, channel.data, accessory.displayName);
140
+ wired++;
141
+ }
142
+ this.log.warn(`Channel list unavailable; running with ${wired} cached switch(es)`);
143
+ }
72
144
  setUpSwitch(accessory, channel, name) {
73
145
  const service = accessory.getService(this.Service.Switch) ??
74
146
  accessory.addService(this.Service.Switch);
@@ -99,9 +171,14 @@ export class StreamTriggersPlatform {
99
171
  return;
100
172
  }
101
173
  this.launchesInFlight.add(channel.key);
102
- // TODO(build-out): wake Apple TV, prime app_list, resolve stream, launch_app.
103
- this.log.info(`[${channel.key}] Triggered (launch flow not implemented yet)`);
104
- this.launchesInFlight.delete(channel.key);
174
+ this.launcher
175
+ .launch(channel)
176
+ .catch((error) => {
177
+ this.log.error(`[${channel.key}] Launch rejected unexpectedly: ${String(error)}`);
178
+ })
179
+ .finally(() => {
180
+ this.launchesInFlight.delete(channel.key);
181
+ });
105
182
  }
106
183
  }
107
184
  //# sourceMappingURL=platform.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,YAAY,EACZ,cAAc,GAEf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE3D,MAAM,OAAO,sBAAsB;IASf;IAEA;IAVF,OAAO,CAAiB;IACxB,cAAc,CAAwB;IAErC,MAAM,CAAuB;IAC7B,WAAW,GAAG,IAAI,GAAG,EAA6B,CAAC;IACnD,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;IAEtD,YACkB,GAAY,EAC5B,SAAyB,EACT,GAAQ;QAFR,QAAG,GAAH,GAAG,CAAS;QAEZ,QAAG,GAAH,GAAG,CAAK;QAExB,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,cAAc,GAAG,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC;QAE7C,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC;QAC5B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,GAAG,CAAC,KAAK,CACZ,6CAA6C,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CACpE,CAAC;YACF,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACvC,CAAC;QAED,GAAG,CAAC,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE;YAChC,IAAI,CAAC;gBACH,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,+BAA+B,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACjE,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,8EAA8E;IAC9E,kBAAkB,CAAC,SAA4B;QAC7C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,+BAA+B,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC;QACtE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAClD,CAAC;IAED,uFAAuF;IAC/E,eAAe;QACrB,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;QAE1C,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC3C,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;gBAC/C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,GAAG,8CAA8C,CAAC,CAAC;gBAC9E,SAAS;YACX,CAAC;YAED,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,WAAW,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;YACzE,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC1B,MAAM,IAAI,GAAG,GAAG,cAAc,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YAE/D,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC5C,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,WAAW,GAAG,IAAI,CAAC;gBAC5B,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;gBAC1C,IAAI,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC/C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,GAAG,qBAAqB,IAAI,GAAG,CAAC,CAAC;YAC7D,CAAC;iBAAM,CAAC;gBACN,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;gBAC7D,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;gBAC3C,IAAI,CAAC,GAAG,CAAC,2BAA2B,CAAC,WAAW,EAAE,aAAa,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;gBAC9E,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;gBACtC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,GAAG,wBAAwB,IAAI,GAAG,CAAC,CAAC;YAChE,CAAC;QACH,CAAC;QAED,KAAK,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACjD,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,SAAS;YACxC,IAAI,CAAC,GAAG,CAAC,6BAA6B,CAAC,WAAW,EAAE,aAAa,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;YAChF,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,wBAAwB,SAAS,CAAC,WAAW,qBAAqB,CAAC,CAAC;QACpF,CAAC;IACH,CAAC;IAEO,WAAW,CACjB,SAA4B,EAC5B,OAAsB,EACtB,IAAY;QAEZ,MAAM,OAAO,GACX,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;YACzC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC5C,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAE1D,MAAM,EAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;QAC7D,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;QACtB,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YACjB,IAAI,CAAC;gBACH,IAAI,CAAC,KAAK;oBAAE,OAAO,CAAC,0CAA0C;gBAC9D,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YAC9B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,GAAG,6BAA6B,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC9E,CAAC;oBAAS,CAAC;gBACT,0DAA0D;gBAC1D,UAAU,CAAC,GAAG,EAAE;oBACd,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;gBACxB,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,8EAA8E;IACtE,aAAa,CAAC,OAAsB;QAC1C,IAAI,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,GAAG,sCAAsC,CAAC,CAAC;YACrE,OAAO;QACT,CAAC;QACD,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACvC,8EAA8E;QAC9E,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,GAAG,+CAA+C,CAAC,CAAC;QAC9E,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC5C,CAAC;CACF"}
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
package/lib/ytdlp.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { Logging } from "homebridge";
2
+ /** GitHub release asset name for the standalone yt-dlp binary, per platform. */
3
+ export declare function ytDlpAssetName(platform?: NodeJS.Platform, arch?: NodeJS.Architecture): string | undefined;
4
+ /** A binary older than this is considered stale and gets re-downloaded. */
5
+ export declare function isStale(mtimeMs: number, nowMs?: number): boolean;
6
+ /**
7
+ * Self-managed yt-dlp binary: downloaded into Homebridge's storage dir and refreshed
8
+ * when older than 30 days (stale copies break against YouTube within months).
9
+ * An explicit ytDlpPath override disables all self-management.
10
+ */
11
+ export declare class YtDlp {
12
+ private readonly log;
13
+ private readonly overridePath;
14
+ readonly managedPath: string;
15
+ private refreshInFlight;
16
+ constructor(log: Logging, storagePath: string, overridePath: string | undefined);
17
+ /** Download/refresh the managed binary if missing or stale. Never rejects. */
18
+ ensureFresh(): Promise<void>;
19
+ /**
20
+ * Resolve the currently-live video id for a channel live page.
21
+ * Returns undefined when the channel is not live or yt-dlp is unavailable
22
+ * (both logged; neither throws).
23
+ */
24
+ resolveLiveVideoId(url: string, prefix: string): Promise<string | undefined>;
25
+ /** Path to a usable binary, or undefined (logged) if none exists yet. */
26
+ private binaryPath;
27
+ private refresh;
28
+ private download;
29
+ }
30
+ //# sourceMappingURL=ytdlp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ytdlp.d.ts","sourceRoot":"","sources":["../src/ytdlp.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAO1C,gFAAgF;AAChF,wBAAgB,cAAc,CAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,IAAI,GAAE,MAAM,CAAC,YAA2B,GACvC,MAAM,GAAG,SAAS,CAKpB;AAED,2EAA2E;AAC3E,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAmB,GAAG,OAAO,CAE5E;AAED;;;;GAIG;AACH,qBAAa,KAAK;IAKd,OAAO,CAAC,QAAQ,CAAC,GAAG;IAEpB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAN/B,SAAgB,WAAW,EAAE,MAAM,CAAC;IACpC,OAAO,CAAC,eAAe,CAA4B;gBAGhC,GAAG,EAAE,OAAO,EAC7B,WAAW,EAAE,MAAM,EACF,YAAY,EAAE,MAAM,GAAG,SAAS;IAKnD,8EAA8E;IAC9E,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ5B;;;;OAIG;IACG,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IA+BlF,yEAAyE;YAC3D,UAAU;YAWV,OAAO;YAaP,QAAQ;CAuBvB"}
package/lib/ytdlp.js ADDED
@@ -0,0 +1,118 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { tryCatch } from "@micthiesen/mitools/async";
4
+ import { describeExecError, isMissingBinaryError, runCommand } from "./exec.js";
5
+ const YTDLP_TIMEOUT_MS = 60_000;
6
+ const DOWNLOAD_TIMEOUT_MS = 180_000;
7
+ const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
8
+ /** GitHub release asset name for the standalone yt-dlp binary, per platform. */
9
+ export function ytDlpAssetName(platform = process.platform, arch = process.arch) {
10
+ if (platform === "linux" && arch === "x64")
11
+ return "yt-dlp_linux";
12
+ if (platform === "linux" && arch === "arm64")
13
+ return "yt-dlp_linux_aarch64";
14
+ if (platform === "darwin")
15
+ return "yt-dlp_macos";
16
+ return undefined;
17
+ }
18
+ /** A binary older than this is considered stale and gets re-downloaded. */
19
+ export function isStale(mtimeMs, nowMs = Date.now()) {
20
+ return nowMs - mtimeMs > MAX_AGE_MS;
21
+ }
22
+ /**
23
+ * Self-managed yt-dlp binary: downloaded into Homebridge's storage dir and refreshed
24
+ * when older than 30 days (stale copies break against YouTube within months).
25
+ * An explicit ytDlpPath override disables all self-management.
26
+ */
27
+ export class YtDlp {
28
+ log;
29
+ overridePath;
30
+ managedPath;
31
+ refreshInFlight;
32
+ constructor(log, storagePath, overridePath) {
33
+ this.log = log;
34
+ this.overridePath = overridePath;
35
+ this.managedPath = path.join(storagePath, "stream-triggers", "yt-dlp");
36
+ }
37
+ /** Download/refresh the managed binary if missing or stale. Never rejects. */
38
+ ensureFresh() {
39
+ if (this.overridePath)
40
+ return Promise.resolve();
41
+ this.refreshInFlight ??= this.refresh().finally(() => {
42
+ this.refreshInFlight = undefined;
43
+ });
44
+ return this.refreshInFlight;
45
+ }
46
+ /**
47
+ * Resolve the currently-live video id for a channel live page.
48
+ * Returns undefined when the channel is not live or yt-dlp is unavailable
49
+ * (both logged; neither throws).
50
+ */
51
+ async resolveLiveVideoId(url, prefix) {
52
+ const binary = await this.binaryPath(prefix);
53
+ if (!binary)
54
+ return undefined;
55
+ this.log.debug(`${prefix} yt-dlp --print id ${url}`);
56
+ const result = await runCommand(binary, ["--print", "id", "--no-warnings", url], YTDLP_TIMEOUT_MS);
57
+ if (!result.ok) {
58
+ if (isMissingBinaryError(result.error)) {
59
+ this.log.error(`${prefix} yt-dlp binary missing at ${binary}`);
60
+ void this.ensureFresh();
61
+ }
62
+ else {
63
+ // Non-zero exit = channel not live. Expected, not an error.
64
+ this.log.info(`${prefix} Not live (yt-dlp: ${describeExecError(result.error)})`);
65
+ }
66
+ return undefined;
67
+ }
68
+ const id = result.value.stdout.trim().split("\n").at(-1)?.trim();
69
+ if (!id) {
70
+ this.log.info(`${prefix} Not live (yt-dlp printed no id)`);
71
+ return undefined;
72
+ }
73
+ return id;
74
+ }
75
+ /** Path to a usable binary, or undefined (logged) if none exists yet. */
76
+ async binaryPath(prefix) {
77
+ if (this.overridePath)
78
+ return this.overridePath;
79
+ const stat = await tryCatch(() => fs.stat(this.managedPath));
80
+ if (stat.ok)
81
+ return this.managedPath;
82
+ this.log.error(`${prefix} yt-dlp not downloaded yet (${this.managedPath}); retrying download in the background`);
83
+ void this.ensureFresh();
84
+ return undefined;
85
+ }
86
+ async refresh() {
87
+ const stat = await tryCatch(() => fs.stat(this.managedPath));
88
+ if (stat.ok && !isStale(stat.value.mtimeMs)) {
89
+ this.log.debug(`yt-dlp binary is fresh (${this.managedPath})`);
90
+ return;
91
+ }
92
+ const downloaded = await tryCatch(() => this.download());
93
+ if (!downloaded.ok) {
94
+ this.log.error(`Failed to download yt-dlp: ${downloaded.error.message}`);
95
+ }
96
+ }
97
+ async download() {
98
+ const asset = ytDlpAssetName();
99
+ if (!asset) {
100
+ throw new Error(`No yt-dlp asset for ${process.platform}/${process.arch}`);
101
+ }
102
+ const url = `https://github.com/yt-dlp/yt-dlp/releases/latest/download/${asset}`;
103
+ this.log.info(`Downloading ${url}`);
104
+ const response = await fetch(url, {
105
+ redirect: "follow",
106
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
107
+ });
108
+ if (!response.ok)
109
+ throw new Error(`HTTP ${response.status} from ${url}`);
110
+ const body = Buffer.from(await response.arrayBuffer());
111
+ await fs.mkdir(path.dirname(this.managedPath), { recursive: true });
112
+ const tmpPath = `${this.managedPath}.tmp`;
113
+ await fs.writeFile(tmpPath, body, { mode: 0o755 });
114
+ await fs.rename(tmpPath, this.managedPath);
115
+ this.log.info(`Downloaded yt-dlp (${(body.length / 1024 / 1024).toFixed(1)} MB) to ${this.managedPath}`);
116
+ }
117
+ }
118
+ //# sourceMappingURL=ytdlp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ytdlp.js","sourceRoot":"","sources":["../src/ytdlp.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-stream-triggers",
3
3
  "displayName": "Homebridge Stream Triggers",
4
- "version": "0.1.0",
4
+ "version": "1.1.0",
5
5
  "description": "HomeKit switches that launch live streams (YouTube/Twitch) on an Apple TV via atvremote and yt-dlp.",
6
6
  "keywords": [
7
7
  "homebridge-plugin",