diff --git a/package-lock.json b/package-lock.json index 5235924..5c80f95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "antennas", - "version": "4.1.1", + "version": "4.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "antennas", - "version": "4.1.1", + "version": "4.2.0", "license": "MIT", "dependencies": { "axios": "^0.24.0", + "axios-digest": "^0.3.0", "js-yaml": "^3.13.1", "koa": "^2.5.0", "koa-logger": "^3.2.0", @@ -992,6 +993,17 @@ "follow-redirects": "^1.14.4" } }, + "node_modules/axios-digest": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/axios-digest/-/axios-digest-0.3.0.tgz", + "integrity": "sha512-zl7zThkh+YLSDUYwDqY1hVPndpDn4ghbB59JVhLIj19X5GJBaIts9+SI82O6D0P2wxz9uXLz+Mwnh1WkNDuXgQ==", + "dependencies": { + "axios": "^0.24.0", + "js-md5": "^0.7.3", + "js-sha256": "^0.9.0", + "js-sha512": "^0.8.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3622,6 +3634,21 @@ "node": ">=8" } }, + "node_modules/js-md5": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz", + "integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==" + }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "node_modules/js-sha512": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==" + }, "node_modules/js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -6833,6 +6860,17 @@ "follow-redirects": "^1.14.4" } }, + "axios-digest": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/axios-digest/-/axios-digest-0.3.0.tgz", + "integrity": "sha512-zl7zThkh+YLSDUYwDqY1hVPndpDn4ghbB59JVhLIj19X5GJBaIts9+SI82O6D0P2wxz9uXLz+Mwnh1WkNDuXgQ==", + "requires": { + "axios": "^0.24.0", + "js-md5": "^0.7.3", + "js-sha256": "^0.9.0", + "js-sha512": "^0.8.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -8765,6 +8803,21 @@ "istanbul-lib-report": "^3.0.0" } }, + "js-md5": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz", + "integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==" + }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "js-sha512": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==" + }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", diff --git a/package.json b/package.json index c465b56..ecb5aa5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "antennas", - "version": "4.1.2", + "version": "4.2.0", "description": "HDHomeRun emulator for Plex DVR to connect to Tvheadend.", "main": "index.js", "repository": "https://github.com/jfarseneau/antennas", @@ -11,6 +11,7 @@ }, "dependencies": { "axios": "^0.24.0", + "axios-digest": "^0.3.0", "js-yaml": "^3.13.1", "koa": "^2.5.0", "koa-logger": "^3.2.0", diff --git a/public/index.html b/public/index.html index f92c035..bbeb8fd 100644 --- a/public/index.html +++ b/public/index.html @@ -63,6 +63,10 @@

Config Settings

Tuner Count + + Channel Count + + diff --git a/public/js/index.js b/public/js/index.js index 8042dc9..b329d4a 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -27,6 +27,7 @@ fetch('/antennas_config.json').then((result) => { urlReplace('#tvheadendStreamUrl')(config.tvheadend_parsed_stream_uri); urlReplace('#antennasUrl')(config.antennas_url); replace('#tunerCount')(config.tuner_count); + replace('#channelCount')(config.channel_count); replace('#status')(config.status); }); diff --git a/src/lineup.js b/src/lineup.js index 4fdca5f..527b549 100644 --- a/src/lineup.js +++ b/src/lineup.js @@ -2,18 +2,25 @@ const tvheadendApi = require('./tvheadendApi'); module.exports = async (config) => { const response = await tvheadendApi.get('/api/channel/grid?start=0&limit=999999', config); - const { data } = response; - // TODO: Check if there's a Plex permission problem const lineup = []; - for (const channel of data.entries) { - if (channel.enabled) { - lineup.push({ - GuideNumber: String(channel.number), - GuideName: channel.name, - URL: `${config.tvheadend_stream_url}/stream/channel/${channel.uuid}`, - }); + + if (response) { + const { data } = response; + // TODO: Check if there's a Plex permission problem + + if (data && data.entries) { + for (const channel of data.entries) { + if (channel.enabled) { + lineup.push({ + GuideNumber: String(channel.number), + GuideName: channel.name, + URL: `${config.tvheadend_stream_url}/stream/channel/${channel.uuid}`, + }); + } + } } } + return lineup; }; diff --git a/src/lineup.test.js b/src/lineup.test.js index 45d5892..ca831ed 100644 --- a/src/lineup.test.js +++ b/src/lineup.test.js @@ -36,7 +36,7 @@ test.serial('calls the API with the right options', async (t) => { remote_timeshift: false, services: ['test-service'], tags: [], - bouquet: '' + bouquet: '', }], total: 1, }, @@ -61,5 +61,23 @@ test.serial('returns empty when Tvheadend has no channel', async (t) => { const actual = await lineup({ tvheadend_stream_url: 'https://stream.test' }); + t.deepEqual(actual, []); +}); + +test.serial('returns empty when Tvheadend returns undefined', async (t) => { + const expectedResponse = undefined; + tvheadendApiStub.resolves(expectedResponse); + + const actual = await lineup({ tvheadend_stream_url: 'https://stream.test' }); + + t.deepEqual(actual, []); +}); + +test.serial('returns empty when Tvheadend returns no data', async (t) => { + const expectedResponse = { data: undefined }; + tvheadendApiStub.resolves(expectedResponse); + + const actual = await lineup({ tvheadend_stream_url: 'https://stream.test' }); + t.deepEqual(actual, []); }); \ No newline at end of file diff --git a/src/router.js b/src/router.js index 206efd8..3fb6a6a 100644 --- a/src/router.js +++ b/src/router.js @@ -6,10 +6,14 @@ const tvheadendApi = require('./tvheadendApi'); async function getConnectionStatus(config) { try { const channels = await tvheadendApi.get('/api/channel/grid?start=0&limit=999999', config); - if (channels.data.total === 0) { - return 'Connected but no channels found from Tvheadend'; - } - return 'All systems go'; + let status = 'All systems go'; + if (channels?.response?.status === 403) { throw new Error('Username and password not accepted by Tvheadend'); } + if (channels?.code === 'ECONNREFUSED') { throw new Error('Unable to connect to Tvheadend'); } + if (channels?.data?.total === 0) { status = 'Connected but no channels found from Tvheadend'; } + return { + status, + channelCount: channels?.data?.total, + }; } catch (err) { console.log(` Antennas failed to connect to Tvheadend! @@ -21,9 +25,17 @@ async function getConnectionStatus(config) { Here's a dump of the error: ${err}`); - if (err.response.status === 401) { return 'Failed to authenticate with Tvheadend'; } - if (err.code === 'ECONNABORTED') { return 'Unable to find Tvheadend server, make sure the server is up and the configuration is pointing to the right spot'; } - return 'Unknown error, check the logs for more details'; + let status = 'Unknown error, check the logs for more details'; + + if (err && err.response && err.response.status === 401) { status = 'Failed to authenticate with Tvheadend'; } + if (err && err.code === 'ECONNABORTED') { status = 'Unable to find Tvheadend server, make sure the server is up and the configuration is pointing to the right spot'; } + if (err && err.message === 'Auth params error.' || err?.message === 'Username and password not accepted by Tvheadend' ) { status = 'Access denied to Tvheadend; check the username, password, and access rights'; } + if (err && err.message === 'Unable to connect to Tvheadend') { status = 'Unable to connect to Tvheadend; is it running?'; } + + return { + status, + channelCount: 0, + }; } } @@ -32,7 +44,9 @@ module.exports = (config, device) => { router.get('/antennas_config.json', async (ctx) => { ctx.type = 'application/json'; - config.status = await getConnectionStatus(config); + const { status, channelCount } = await getConnectionStatus(config); + config.status = status; + config.channel_count = channelCount; ctx.body = config; }); diff --git a/src/tvheadendApi.js b/src/tvheadendApi.js index fc42324..2b6132f 100644 --- a/src/tvheadendApi.js +++ b/src/tvheadendApi.js @@ -1,4 +1,5 @@ const axios = require('axios'); +const AxiosDigest = require('axios-digest').default; async function get(apiPath, config) { const options = { @@ -13,7 +14,16 @@ async function get(apiPath, config) { }; } - return axios.get(`${config.tvheadend_parsed_uri}${apiPath}`, options); + try { + return await axios.get(`${config.tvheadend_parsed_uri}${apiPath}`, options); + } catch (err) { + if (err && err.response && err.response.status === 401) { + const axiosDigest = new AxiosDigest(config.tvheadend_username, config.tvheadend_password); + return axiosDigest.get(`${config.tvheadend_parsed_uri}${apiPath}`); + } + + return err; + } } module.exports = { get };