UNPKG

@gibme/tablo.tv

Version:

API interface for interacting with a Tablo TV device

465 lines 20.1 kB
"use strict"; // Copyright (c) 2025, Brandon Lehmann <brandonlehmann@gmail.com> // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Tablo = void 0; const tablo_api_1 = __importDefault(require("./tablo_api")); const lighthouse_1 = __importDefault(require("./lighthouse")); const memory_1 = __importDefault(require("@gibme/cache/memory")); /** * See https://jessedp.github.io/tablo-api-docs/#tablo-api-introduction * for an extensive list of device endpoints * * Note: this implementation is currently incomplete and is unlikely to have all endpoints implemented. */ class Tablo extends tablo_api_1.default { constructor() { super(...arguments); this.cache = new memory_1.default({ stdTTL: 10 * 60 * 1000 }); this.session_channels = new Map(); } /** * Attempts to discover the Tablo devices on the network from which this API is made. * @param timeout */ static discover() { return __awaiter(this, arguments, void 0, function* (timeout = 2000) { return lighthouse_1.default.listAvailableDevices(timeout); }); } /** * Returns the currently available airings * * Note: This method contains a loop that results in the method taking a bit of time to complete, * you may specify a progress callback to help report the progress to the caller. * * Repeated calls to this method are cached for approximately 10 minutes. * * @param all if true, will return all airings, otherwise will only return airings that are currently playing. * @param timeout * @param force_refresh if set to true, will force a refresh of the cache. * @param progress_callback */ airings() { return __awaiter(this, arguments, void 0, function* (all = false, timeout = this.timeout, force_refresh = false, progress_callback) { var _a, _b; const progress = (total, received) => { if (progress_callback) { progress_callback(total, received); } }; const { now, start } = this.currentHour; try { const result = !force_refresh ? (_a = yield this.cache.get('airings')) !== null && _a !== void 0 ? _a : [] : []; if (result.length === 0) { let airings = (_b = yield this.get('/guide/airings', undefined, timeout)) !== null && _b !== void 0 ? _b : []; const total = airings.length; progress(total, result.length); while (airings.length > 0) { const batch = airings.slice(0, 50); airings = airings.slice(50); const data = yield this.batch(batch, timeout); result.push(...Object.entries(data) .map(([, response]) => { return { show_title: response.airing_details.show_title, start_time: new Date(response.airing_details.datetime), end_time: new Date(this.calculate_endtime(response.airing_details.datetime, response.airing_details.duration)), duration: response.airing_details.duration, episode: Object.assign(Object.assign({}, response.episode), { orig_air_date: new Date(response.episode.orig_air_date) }), channel: Object.assign({}, response.airing_details.channel.channel) }; })); progress(total, result.length); } yield this.cache.set('airings', result); } else { progress(result.length, result.length); } return result.filter(airing => { if (all) { return true; } const start_time = new Date(airing.start_time).getTime(); const end_time = new Date(airing.end_time).getTime(); return start_time >= start && start_time < now && end_time > now; }).sort((a, b) => { return (a.channel.major + (a.channel.minor * 0.1)) - (b.channel.major + (b.channel.minor * 0.1)); }); } catch (error) { return []; } }); } /** * Retrieves account subscription information from the device * @param timeout */ accountSubscription() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { try { const response = yield this.get('/account/subscription', undefined, timeout); if (response) { return Object.assign(Object.assign({}, response), { subscriptions: response.subscriptions.map(subscription => { return Object.assign(Object.assign({}, subscription), { expires: subscription.expires ? new Date(subscription.expires) : null }); }) }); } } catch (_a) { } }); } /** * Retrieves the capabilities of the device. * @param timeout */ capabilities() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { var _a; try { const response = yield this.get('/server/capabilities', undefined, timeout); return (_a = response === null || response === void 0 ? void 0 : response.capabilities) !== null && _a !== void 0 ? _a : []; } catch (_b) { return []; } }); } channel(channel_id_1) { return __awaiter(this, arguments, void 0, function* (channel_id, timeout = this.timeout) { const channels = yield this.channels(timeout); return channels.find(elem => elem.channel_identifier === channel_id); }); } /** * Returns a list of the available channels on the device. * @param timeout */ channels() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { var _a; try { const channels = (_a = yield this.get('/guide/channels')) !== null && _a !== void 0 ? _a : []; if (channels.length === 0) { return []; } const channel_data = yield this.batch(channels, timeout); return Object.entries(channel_data) .map(([, response]) => { return Object.assign({}, response.channel); }) .sort((a, b) => { return (a.major + (a.minor * 0.1)) - (b.major + (b.minor * 0.1)); }); } catch (_b) { return []; } }); } /** * Retrieves information regarding the latest (or a specified) channel scan. * * @param scan_idx if not specified, will pull the latest channel scan information * @param timeout */ channelScanInfo(scan_idx_1) { return __awaiter(this, arguments, void 0, function* (scan_idx, timeout = this.timeout) { try { if (!scan_idx) { const response = yield this.get('/channels/info', undefined, timeout); if (response === null || response === void 0 ? void 0 : response.committed_scan) { const [, , , scan_idx] = response.committed_scan.split('/'); return this.channelScanInfo(scan_idx, timeout); } } else { const response = yield this.get(`/channels/scans/${scan_idx}`); if (response) { return Object.assign(Object.assign({}, response), { datetime: new Date(response.datetime) }); } } } catch (_a) { } }); } /** * Deletes/stops an existing watch (streaming) session * @param tokenOrPlayerSession * @param timeout */ deleteSession(tokenOrPlayerSession_1) { return __awaiter(this, arguments, void 0, function* (tokenOrPlayerSession, timeout = this.timeout) { const token = typeof tokenOrPlayerSession === 'string' ? tokenOrPlayerSession : tokenOrPlayerSession.token; try { const success = yield this.delete(`/player/sessions/${token}`, { lh: undefined }, timeout); if (success) { this.session_channels.delete(token); } return success; } catch (_a) { return false; } }); } /** * Retrieves device subscription information. * @param timeout */ deviceSubscription() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { try { const response = yield this.get('/server/subscription', undefined, timeout); if (response) { return Object.assign(Object.assign({}, response), { expires: response.expires ? new Date(response.expires) : null }); } } catch (_a) { } }); } /** * Retrieves the guide status from the device * @param timeout */ guideStatus() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { try { const response = yield this.get('/server/guide/status', undefined, timeout); if (response) { return Object.assign(Object.assign({}, response), { last_update: new Date(response.last_update), limit: new Date(response.limit) }); } } catch (_a) { } }); } /** * Retrieves a list of the hard drives connected to the device. * @param timeout */ hardDrives() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { var _a; try { return ((_a = yield this.get('/server/harddrives', undefined, timeout)) !== null && _a !== void 0 ? _a : []); } catch (_b) { return []; } }); } /** * Retrieves device information * @param timeout */ info() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { try { return yield this.get('/server/info', undefined, timeout); } catch (_a) { } }); } /** * Sends a watch (streaming) session keepalive request so that the session does not time out and stop * @param tokenOrPlayerSession * @param timeout */ keepaliveSession(tokenOrPlayerSession_1) { return __awaiter(this, arguments, void 0, function* (tokenOrPlayerSession, timeout = this.timeout) { const token = typeof tokenOrPlayerSession === 'string' ? tokenOrPlayerSession : tokenOrPlayerSession.token; try { const channel = this.session_channels.get(token); const response = yield this.post(`/player/sessions/${token}/keepalive`, { lh: undefined }, undefined, timeout); if (response) { return Object.assign(Object.assign({}, response), { channel }); } } catch (_a) { } }); } /** * Retrieves device location information * @param timeout */ location() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { try { return yield this.get('/server/location', undefined, timeout); } catch (_a) { } }); } /** * Attempts to retrieve an existing watch (streaming) session * @param tokenOrPlayerSession * @param timeout */ session(tokenOrPlayerSession_1) { return __awaiter(this, arguments, void 0, function* (tokenOrPlayerSession, timeout = this.timeout) { const token = typeof tokenOrPlayerSession === 'string' ? tokenOrPlayerSession : tokenOrPlayerSession.token; try { const channel = this.session_channels.get(token); const response = yield this.get(`/player/sessions/${token}`, { lh: undefined }, timeout); if (response) { return Object.assign(Object.assign({}, response), { channel }); } } catch (_a) { } }); } /** * Retrieves the settings of the device. * @param timeout */ settings() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { try { return yield this.get('/settings/info', undefined, timeout); } catch (_a) { } }); } /** * Retrieves the list of supported storage types. * @param timeout */ storage() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { var _a; try { const response = yield this.get('/storage/info', undefined, timeout); return (_a = response === null || response === void 0 ? void 0 : response.supported_kinds) !== null && _a !== void 0 ? _a : []; } catch (_b) { return []; } }); } /** * Retrieves tuner information of the device. * @param timeout */ tuners() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { var _a; try { // todo: resolve the channel property with actual information return ((_a = yield this.get('/server/tuners', undefined, timeout)) !== null && _a !== void 0 ? _a : []); } catch (_b) { return []; } }); } /** * Retrieves device update information. * @param timeout */ updateInfo() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { try { const response = yield this.get('/server/update/info', undefined, timeout); if (response) { return Object.assign(Object.assign({}, response), { last_checked: new Date(response.last_checked), last_update: response.last_update ? new Date(response.last_update) : null }); } } catch (_a) { } }); } /** * Retrieves device update progress information. * @param timeout */ updateProgress() { return __awaiter(this, arguments, void 0, function* (timeout = this.timeout) { try { return yield this.get('/server/update/progress', undefined, timeout); } catch (_a) { } }); } /** * Initiates a channel watch (streaming) session on the device which must be managed via * `keepaliveSession` and `deleteSession` * @param channel_id * @param device_info * @param timeout */ watchChannel(channel_id_1) { return __awaiter(this, arguments, void 0, function* (channel_id, device_info = {}, timeout = 30000) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o; var _p, _q, _r, _s, _t, _u, _v, _w, _x; (_a = device_info.device_id) !== null && _a !== void 0 ? _a : (device_info.device_id = this.device_id); (_b = device_info.platform) !== null && _b !== void 0 ? _b : (device_info.platform = 'ios'); (_c = device_info.bandwidth) !== null && _c !== void 0 ? _c : (device_info.bandwidth = null); (_d = device_info.extra) !== null && _d !== void 0 ? _d : (device_info.extra = {}); (_e = (_p = device_info.extra).deviceId) !== null && _e !== void 0 ? _e : (_p.deviceId = '00000000-0000-0000-0000-000000000000'); (_f = (_q = device_info.extra).width) !== null && _f !== void 0 ? _f : (_q.width = 640); (_g = (_r = device_info.extra).height) !== null && _g !== void 0 ? _g : (_r.height = 480); (_h = (_s = device_info.extra).deviceModel) !== null && _h !== void 0 ? _h : (_s.deviceModel = 'iPhone15,3'); (_j = (_t = device_info.extra).lang) !== null && _j !== void 0 ? _j : (_t.lang = 'en_US'); (_k = (_u = device_info.extra).deviceOS) !== null && _k !== void 0 ? _k : (_u.deviceOS = 'iOS'); (_l = (_v = device_info.extra).deviceOSVersion) !== null && _l !== void 0 ? _l : (_v.deviceOSVersion = '18.4.1'); (_m = (_w = device_info.extra).limitedAdTracking) !== null && _m !== void 0 ? _m : (_w.limitedAdTracking = 1); (_o = (_x = device_info.extra).deviceMake) !== null && _o !== void 0 ? _o : (_x.deviceMake = 'Apple'); const info = yield this.info(); const channel = yield this.channel(channel_id); if (info && channel) { try { const response = yield this.post(`/guide/channels/${channel_id}/watch`, { lh: undefined }, device_info, timeout); if (response) { this.session_channels.set(response.token, channel); return Object.assign(Object.assign({}, response), { expires: new Date(response.expires), channel }); } } catch (_y) { } } }); } } exports.Tablo = Tablo; exports.default = Tablo; //# sourceMappingURL=tablo.js.map