@gibme/tablo.tv
Version:
API interface for interacting with a Tablo TV device
465 lines • 20.1 kB
JavaScript
"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