homebridge-stream-triggers 0.1.0 → 1.0.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/lib/atv.d.ts +22 -0
- package/lib/atv.d.ts.map +1 -0
- package/lib/atv.js +86 -0
- package/lib/atv.js.map +1 -0
- package/lib/exec.d.ts +14 -0
- package/lib/exec.d.ts.map +1 -0
- package/lib/exec.js +35 -0
- package/lib/exec.js.map +1 -0
- package/lib/launcher.d.ts +22 -0
- package/lib/launcher.d.ts.map +1 -0
- package/lib/launcher.js +56 -0
- package/lib/launcher.js.map +1 -0
- package/lib/platform.d.ts +2 -0
- package/lib/platform.d.ts.map +1 -1
- package/lib/platform.js +17 -3
- package/lib/platform.js.map +1 -1
- package/lib/ytdlp.d.ts +30 -0
- package/lib/ytdlp.d.ts.map +1 -0
- package/lib/ytdlp.js +118 -0
- package/lib/ytdlp.js.map +1 -0
- package/package.json +1 -1
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
|
package/lib/atv.d.ts.map
ADDED
|
@@ -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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"}
|
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
|
package/lib/exec.js.map
ADDED
|
@@ -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"}
|
package/lib/launcher.js
ADDED
|
@@ -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,6 +7,8 @@ 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);
|
|
11
13
|
/** Called by Homebridge for each accessory restored from cache at startup. */
|
|
12
14
|
configureAccessory(accessory: PlatformAccessory): void;
|
package/lib/platform.d.ts.map
CHANGED
|
@@ -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;
|
|
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;AAYpB,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;IA6B1B,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;CAiBtB"}
|
package/lib/platform.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { AppleTv } from "./atv.js";
|
|
1
2
|
import { configSchema, displayNameFor, } from "./config.js";
|
|
3
|
+
import { StreamLauncher } from "./launcher.js";
|
|
2
4
|
import { PLATFORM_NAME, PLUGIN_NAME } from "./settings.js";
|
|
5
|
+
import { YtDlp } from "./ytdlp.js";
|
|
3
6
|
export class StreamTriggersPlatform {
|
|
4
7
|
log;
|
|
5
8
|
api;
|
|
@@ -8,6 +11,8 @@ export class StreamTriggersPlatform {
|
|
|
8
11
|
config;
|
|
9
12
|
accessories = new Map();
|
|
10
13
|
launchesInFlight = new Set();
|
|
14
|
+
ytDlp;
|
|
15
|
+
launcher;
|
|
11
16
|
constructor(log, rawConfig, api) {
|
|
12
17
|
this.log = log;
|
|
13
18
|
this.api = api;
|
|
@@ -21,6 +26,8 @@ export class StreamTriggersPlatform {
|
|
|
21
26
|
this.log.error(`Invalid config, running with no channels: ${parsed.error.message}`);
|
|
22
27
|
this.config = configSchema.parse({});
|
|
23
28
|
}
|
|
29
|
+
this.ytDlp = new YtDlp(log, api.user.storagePath(), this.config.ytDlpPath);
|
|
30
|
+
this.launcher = new StreamLauncher(log, new AppleTv(log, this.config), this.ytDlp);
|
|
24
31
|
api.on("didFinishLaunching", () => {
|
|
25
32
|
try {
|
|
26
33
|
this.syncAccessories();
|
|
@@ -28,6 +35,8 @@ export class StreamTriggersPlatform {
|
|
|
28
35
|
catch (error) {
|
|
29
36
|
this.log.error(`Failed to sync accessories: ${String(error)}`);
|
|
30
37
|
}
|
|
38
|
+
// Non-blocking: download/refresh the managed yt-dlp binary in the background.
|
|
39
|
+
void this.ytDlp.ensureFresh();
|
|
31
40
|
});
|
|
32
41
|
}
|
|
33
42
|
/** Called by Homebridge for each accessory restored from cache at startup. */
|
|
@@ -99,9 +108,14 @@ export class StreamTriggersPlatform {
|
|
|
99
108
|
return;
|
|
100
109
|
}
|
|
101
110
|
this.launchesInFlight.add(channel.key);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
this.launcher
|
|
112
|
+
.launch(channel)
|
|
113
|
+
.catch((error) => {
|
|
114
|
+
this.log.error(`[${channel.key}] Launch rejected unexpectedly: ${String(error)}`);
|
|
115
|
+
})
|
|
116
|
+
.finally(() => {
|
|
117
|
+
this.launchesInFlight.delete(channel.key);
|
|
118
|
+
});
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
//# sourceMappingURL=platform.js.map
|
package/lib/platform.js.map
CHANGED
|
@@ -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;
|
|
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
|
package/lib/ytdlp.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ytdlp.js","sourceRoot":"","sources":["../src/ytdlp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAErD,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEhF,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAChC,MAAM,mBAAmB,GAAG,OAAO,CAAC;AACpC,MAAM,UAAU,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE5C,gFAAgF;AAChF,MAAM,UAAU,cAAc,CAC5B,WAA4B,OAAO,CAAC,QAAQ,EAC5C,OAA4B,OAAO,CAAC,IAAI;IAExC,IAAI,QAAQ,KAAK,OAAO,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,cAAc,CAAC;IAClE,IAAI,QAAQ,KAAK,OAAO,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,sBAAsB,CAAC;IAC5E,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,cAAc,CAAC;IACjD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,OAAO,CAAC,OAAe,EAAE,QAAgB,IAAI,CAAC,GAAG,EAAE;IACjE,OAAO,KAAK,GAAG,OAAO,GAAG,UAAU,CAAC;AACtC,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,KAAK;IAKG;IAEA;IANH,WAAW,CAAS;IAC5B,eAAe,CAA4B;IAEnD,YACmB,GAAY,EAC7B,WAAmB,EACF,YAAgC;QAFhC,QAAG,GAAH,GAAG,CAAS;QAEZ,iBAAY,GAAZ,YAAY,CAAoB;QAEjD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,iBAAiB,EAAE,QAAQ,CAAC,CAAC;IACzE,CAAC;IAED,8EAA8E;IAC9E,WAAW;QACT,IAAI,IAAI,CAAC,YAAY;YAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAChD,IAAI,CAAC,eAAe,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YACnD,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;QACnC,CAAC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,eAAe,CAAC;IAC9B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,kBAAkB,CAAC,GAAW,EAAE,MAAc;QAClD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QAE9B,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,MAAM,sBAAsB,GAAG,EAAE,CAAC,CAAC;QACrD,MAAM,MAAM,GAAG,MAAM,UAAU,CAC7B,MAAM,EACN,CAAC,SAAS,EAAE,IAAI,EAAE,eAAe,EAAE,GAAG,CAAC,EACvC,gBAAgB,CACjB,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,IAAI,oBAAoB,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,MAAM,6BAA6B,MAAM,EAAE,CAAC,CAAC;gBAC/D,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC;YAC1B,CAAC;iBAAM,CAAC;gBACN,4DAA4D;gBAC5D,IAAI,CAAC,GAAG,CAAC,IAAI,CACX,GAAG,MAAM,sBAAsB,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAClE,CAAC;YACJ,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QACjE,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,kCAAkC,CAAC,CAAC;YAC3D,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,yEAAyE;IACjE,KAAK,CAAC,UAAU,CAAC,MAAc;QACrC,IAAI,IAAI,CAAC,YAAY;YAAE,OAAO,IAAI,CAAC,YAAY,CAAC;QAChD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;QAC7D,IAAI,IAAI,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC,WAAW,CAAC;QACrC,IAAI,CAAC,GAAG,CAAC,KAAK,CACZ,GAAG,MAAM,+BAA+B,IAAI,CAAC,WAAW,wCAAwC,CACjG,CAAC;QACF,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC;IACnB,CAAC;IAEO,KAAK,CAAC,OAAO;QACnB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;QAC7D,IAAI,IAAI,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,2BAA2B,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;YAC/D,OAAO;QACT,CAAC;QAED,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QACzD,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC;YACnB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,8BAA8B,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,QAAQ;QACpB,MAAM,KAAK,GAAG,cAAc,EAAE,CAAC;QAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CAAC,uBAAuB,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7E,CAAC;QACD,MAAM,GAAG,GAAG,6DAA6D,KAAK,EAAE,CAAC;QACjF,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,GAAG,EAAE,CAAC,CAAC;QAEpC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAChC,QAAQ,EAAE,QAAQ;YAClB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,mBAAmB,CAAC;SACjD,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC;QACzE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;QAEvD,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpE,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,WAAW,MAAM,CAAC;QAC1C,MAAM,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACnD,MAAM,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QAC3C,IAAI,CAAC,GAAG,CAAC,IAAI,CACX,sBAAsB,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,WAAW,EAAE,CAC1F,CAAC;IACJ,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-stream-triggers",
|
|
3
3
|
"displayName": "Homebridge Stream Triggers",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.0.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",
|