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 };