Skip to content

Commit

Permalink
feat: List and launch AppleTV Apps
Browse files Browse the repository at this point in the history
Now supports listing installed apps with `listApps()` and launching them with `launchApp(id)`
  • Loading branch information
sebbo2002 committed Mar 21, 2023
1 parent 4e6ed8c commit 680d1de
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 1 deletion.
41 changes: 40 additions & 1 deletion src/lib/device.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

import {
NodePyATVApp,
NodePyATVDeviceOptions,
NodePyATVDeviceState,
NodePyATVExecutableType,
Expand Down Expand Up @@ -378,7 +379,36 @@ export default class NodePyATVDevice implements EventEmitter{
return state.appId;
}

private async _pressKey(key: NodePyATVInternalKeys, executableType: NodePyATVExecutableType) {
/**
* Returns the list of installed apps on the Apple TV. Probably requires `companionCredentials`,
* see https://pyatv.dev/documentation/atvremote/#apps for more details.
*/
async listApps(): Promise<NodePyATVApp[]> {
const id = addRequestId();
const parameters = getParamters(this.options);

const result = await request(id, NodePyATVExecutableType.atvremote, [...parameters, 'app_list'], this.options);
if(typeof result !== 'string' || !result.startsWith('App: ')) {
throw new Error('Unexpected atvremote response: ' + result);
}

removeRequestId(id);
const regex = /(.+) \(([^\)]+)\)$/i;
const items = result.substring(5, ).split(', App: ').map(i => {
const m = i.match(regex);
if (m !== null) {
return {
id: m[2],
name: m[1],
launch: () => this.launchApp(m[2])
};
}
}) as Array<NodePyATVApp | undefined>;

return items.filter(Boolean) as NodePyATVApp[];
}

private async _pressKey(key: NodePyATVInternalKeys | string, executableType: NodePyATVExecutableType) {
const id = addRequestId();
const parameters = getParamters(this.options);

Expand Down Expand Up @@ -611,6 +641,15 @@ export default class NodePyATVDevice implements EventEmitter{
await this._pressKey(NodePyATVInternalKeys.turnOn, NodePyATVExecutableType.atvremote);
}

/**
* Launch an application. Probably requires `companionCredentials`, see
* https://pyatv.dev/documentation/atvremote/#apps for more details.
* @param id App identifier, e.g. `com.netflix.Netflix`
*/
async launchApp(id: string): Promise<void> {
await this._pressKey('launch_app=' + id, NodePyATVExecutableType.atvremote);
}

/**
* Add an event listener. Will start the event subscription with the
* Apple TV as long as there are listeners for any event registered.
Expand Down
6 changes: 6 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,9 @@ export interface NodePyATVState {
appId: string | null,
powerState: NodePyATVPowerState | null
}

export interface NodePyATVApp {
id: string;
name: string;
launch: () => Promise<void>;
}
42 changes: 42 additions & 0 deletions test/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,35 @@ describe('NodePyATVDevice', function () {
});
});

describe('listApps()', function () {
it('should work', async function () {
const device = new NodePyATVDevice({
name: 'My Testdevice',
host: '192.168.178.2',
spawn: createFakeSpawn(cp => {
cp.end(
'App: Fitness (com.apple.Fitness), App: Podcasts (com.apple.podcasts), ' +
'App: Filme (com.apple.TVMovies), App: Prime Video (com.amazon.aiv.AIVApp), ' +
'App: TV (com.apple.TVWatchList), App: Fotos (com.apple.TVPhotos), App: App Store ' +
'(com.apple.TVAppStore), App: Arcade (com.apple.Arcade), App: TV-Sendungen (com.apple.TVShows), ' +
'App: Suchen (com.apple.TVSearch), App: Live TV (de.couchfunk.WM2014), App: RTL+ ' +
'(com.rtlinteractive.tvnow), App: Computer (com.apple.TVHomeSharing), App: ARTE ' +
'(tv.arte.plus7), App: YouTube (com.google.ios.youtube), App: ARD Mediathek ' +
'(de.swr.avp.ard.tablet), App: Disney+ (com.disney.disneyplus), App: Plex (com.plexapp.plex), ' +
'App: Joyn (de.prosiebensat1digital.seventv), App: Einstellungen (com.apple.TVSettings), ' +
'App: ZDFmediathek (de.zdf.mediathek.universal), App: Crossy Road (com.hipsterwhale.crossy), ' +
'App: Netflix (com.netflix.Netflix), App: Infuse (com.firecore.infuse), ' +
'App: Musik (com.apple.TVMusic)');
})
});

const result = await device.listApps();
assert.strictEqual(result.length, 25);
assert.strictEqual(result[0].id, 'com.apple.Fitness');
assert.strictEqual(result[0].name, 'Fitness');
});
});

describe('pressKey()', function () {
it('should work with valid key', async function () {
const device = new NodePyATVDevice({
Expand Down Expand Up @@ -781,4 +810,17 @@ describe('NodePyATVDevice', function () {
});
});

describe('launchApp()', function () {
it('should work', async function () {
const device = new NodePyATVDevice({
name: 'My Testdevice',
host: '192.168.178.2',
spawn: createFakeSpawn(cp => {
cp.end('');
})
});

await device.launchApp('com.apple.TVShows');
});
});
});

0 comments on commit 680d1de

Please sign in to comment.