tvguide
Version:
Node module that auto-gets current (Dutch) tv show information
505 lines (465 loc) • 16.7 kB
JavaScript
/* Dependencies */
var request = require('request');
/* Global debug enabler/disabler */
var debug = false;
/* Local vars */
var initialized = false
, stop = false
, channel_list = {}
// Current list of channel ID's
, id = []
// Guide containing current show information
, guide = { 'now': {}, 'next': {} }
, guide_web = 'http://www.tvgids.nl/json/lists/nustraks.php?channels='
, channels_web = 'http://www.tvgids.nl/json/lists/channels.php'
, guide_fields = ['zendernaam', 'aangekondigde_titel', 'datum', 'begintijd', 'eindtijd']
, guide_timezone_offset = 0
// Progresses (%) of current shows
, progresses = []
// Stuff for auto-update timing
, update_channels = []
, update_date = new Date()
, update_hours
, update_mins
;
/* Fetches a list of all currently hosted channels
* @callback(err) when channel_list is populated
*/
fetchChannelList = function (callback) {
request(channels_web, function (err0, response, body) {
if (err0) {
callback && callback(err0);
return;
}
try {
var parsed = JSON.parse(body);
var list = {};
for (var i in parsed) {
var index = parsed[i]['id'];
list[index] = parsed[i]['name'];
}
channel_list = list;
callback && callback(null);
}
catch (err1) {
callback && callback(err1);
}
});
}
/* Fetches channels specified by index (=position in id array)
* Multiple channel implementation to reduce url requests
* @indexes array if indexes to fetch
* @callback(err, channels) when received and parsed
*/
fetchChannels = function (indexes, callback) {
if (debug) {
debugString = '[tvguide] fetchChannels: ';
for (var i in indexes) {
debugString += indexes[i] + ' ';
}
console.log(debugString);
}
// Build url, if used for correct commas (not after last)
var length = indexes.length;
var channels = '';
for (var i in indexes) {
channels += id[indexes[i]];
if (i < (length - 1)) {
channels += ',';
}
}
var url = guide_web + channels;
// Request the current channel information
request(url, function (err, response, body) {
if (err) {
callback && callback(err);
}
var channels = JSON.parse(body);
// Succes, so callback (with indexes for easier use)
callback && callback(null, channels);
});
}
/* Updates selected channels in the local guide
* @callback(err) when done
*/
updateGuide = function (callback) {
// Fetch selected channels
debug && console.log('[tvguide] updateGuide: ' + update_channels);
fetchChannels(update_channels, function (err, channels) {
if (err) {
callback && callback(err);
return;
}
// Store all selected fields in the guide
for (var nr in update_channels) {
var now = channels[id[update_channels[nr]]][0];
var next = channels[id[update_channels[nr]]][1];
for (var f in guide_fields) {
if (guide_fields[f].indexOf('tijd') > -1) {
guide['now'][update_channels[nr]][guide_fields[f]] = (now && now[guide_fields[f]].substr(0, 5)) || '-';
guide['next'][update_channels[nr]][guide_fields[f]] = (next && next[guide_fields[f]].substr(0, 5)) || '-';
}
else {
guide['now'][update_channels[nr]][guide_fields[f]] = (now && now[guide_fields[f]]) || '-';
guide['next'][update_channels[nr]][guide_fields[f]] = (next && next[guide_fields[f]]) || '-';
}
}
}
callback && callback(null);
});
}
/* (re)initializes the entire guide
* @callback(err) when done
*/
initGuide = function (callback) {
debug && console.log('[tvguide] Initializing');
update_channels = [];
// Select channels
for (var i in id) {
update_channels[i] = i;
guide['now'][i] = {};
guide['next'][i] = {};
}
// Update the guide and get next update time and channels
updateGuide(function (err0) {
if (err0) {
callback(err0);
return;
}
// Guide is initialized
initialized = true;
getNextUpdate(function () {
debug && console.log('[tvguide] Init done\n');
waitUntil(function () {
callback(null);
});
});
getProgresses(function (err1) {
if (err1) {
callback(err1);
}
});
});
}
getTimeZoneOffsetInMiliseconds = function () {
return (new Date()).getTimezoneOffset() * -60e3;
};
function parseIntBase10(input) {
return parseInt(input, 10);
}
/* Returns next update time and channels to be updated from the local guide
* @return callback() when done
* @return globals update_hours, update_mins, update_channels
*/
getNextUpdate = function (callback) {
var ch_count = 1;
update_channels = ['0'];
var check;
for (var i in guide['next']) {
var rawDate = guide['next'][i]['datum'].split('-').map(parseIntBase10);
var rawTime = guide['next'][i]['begintijd'].split(':').map(parseIntBase10);
// Adjust non-correct dates from TVGids.nl
if ((rawDate[2]) < update_date.getDate()) {
rawDate[2] = update_date.getDate();
}
else if (rawTime[0] < 2) {
rawDate[2] += 1;
}
check = new Date(rawDate[0], (rawDate[1] - 1), rawDate[2], rawTime[0], rawTime[1], rawTime[2] || 0, 0);
// Set default if first channel
if (i == 0) {
update_date = check;
}
else {
if (check.getTime() == update_date.getTime()) {
update_channels[ch_count] = i;
ch_count++;
}
else if (check.getTime() < update_date.getTime()) {
ch_count = 1;
update_date = check;
update_channels = [i];
}
}
}
update_hours = update_date.getHours();
update_mins = update_date.getMinutes();
callback && callback();
}
/* Check for difference between Web and Local guides
* @Callback(bool) true if remote guide is different from local one
*/
checkChange = function (callback) {
var change = false;
fetchChannels([update_channels[0]], function (err, channels) {
if (err) {
callback && callback(err);
return;
}
var ch_digits = channels[id[update_channels[0]]][0]['eindtijd'].split(':');
var ch_hours = parseInt(ch_digits[0]);
var ch_mins = parseInt(ch_digits[1]);
change = ((ch_hours != update_hours) || (ch_mins != update_mins));
debug && console.log('[tvguide] checkChange: ' + change + ', prev:' + update_hours + ':' + update_mins, ', next:' + ch_hours + ':' + ch_mins);
callback(null, change);
});
}
/* Gets progresses of current shows
* @return array of current progresses
*/
getProgresses = function (callback) {
// Get current time
var now = new Date();
var now_hours = now.getHours() + guide_timezone_offset;
if (now_hours >= 24) {
now_hours -= 24;
}
var now_mins = now.getMinutes();
// Get show start/end
for (var i in guide['now']) {
var time_now = 60 * now_hours + now_mins;
var ch_digits_start = (guide['now'][i][guide_fields[guide_fields.length - 2]]).split(':');
var ch_hours_start = parseInt(ch_digits_start[0]);
var ch_mins_start = parseInt(ch_digits_start[1]);
var ch_digits_end = (guide['now'][i][guide_fields[guide_fields.length - 1]]).split(':');
var ch_hours_end = parseInt(ch_digits_end[0]);
var ch_mins_end = parseInt(ch_digits_end[1]);
var time_start = 60 * ch_hours_start + ch_mins_start;
var time_end = 60 * ch_hours_end + ch_mins_end;
// Adjust if show starts before 23:59 and ends after
if (time_end < time_start) {
if ((time_now - time_end) < 12) {
time_now += 24 * 60;
}
time_end += 24 * 60;
}
progresses[i] = ((time_now - time_start) / (time_end - time_start));
}
callback && callback(null);
}
/* Waits until a certain time is reached, then calls back
* @target_hours hours to wait
* @target_minutes minutes to wait
* @callback() when target time reached
*/
waitUntil = function (callback) {
var now = new Date();
var timeout = (update_date.getTime() - now.getTime());
if (timeout <= 0) {
callback && callback(null);
}
else {
setTimeout(function () {
callback && callback(null);
}, timeout);
}
debug && console.log('[tvguide] waiting from ' + now.toString() + '\n\t\t until ' + update_date.toString());
}
/* Auto-updater. Keeps calling itself when there are updates, firing the trigger
* @trigger(channels, guide) the function to be called with the updated channels list
*/
updater = function (trigger) {
debug && console.log('[tvguide] updater');
// Set maximum number of checks
var checks = 3;
if (stop) {
debug && console.log('[tvguide] Stopped');
return;
}
else {
// Check if there is indeed new data
var changeTimer = setInterval(function () {
checkChange(function (err0, change) {
if (err0) {
trigger(new Error(err0));
return;
}
checks--;
debug && console.log('[tvguide] checks:', checks);
if (change == true) {
clearInterval(changeTimer);
// There is new tvgids.nl data, so get it
updateGuide(function (err1) {
if (err1) {
trigger(new Error(err1));
return;
}
// Send the new data to the trigger callback
trigger(null, update_channels, guide);
// Get the next update time and channels
getNextUpdate(function (err2) {
if (err2) {
trigger(new Error(err2));
return;
}
// Wait for the new update time
waitUntil(function () {
// New update time and channels are available, so recurse
updater(trigger);
});
});
});
}
// 3 false checks in a row, so there is a problem
else if (checks == 0) {
debug && console.log('[tvguide] Channel not updating, re�nitializing');
initGuide(function () {
checks = 3;
});
}
});
}, 15000);
}
},
/* Public stuff */
module.exports = {
/* Starts the auto-updating TV guide
* @trigger(error, channels, guide) everytime there is updated data
* @channels array of updated channels
* @guide the entire updated guide
* The including server should use this to update connected clients
*/
Start: function (trigger) {
debug && console.log('[tvguide] Starting\n');
stop = false;
if (initialized) {
// Otart the auto-updater
trigger(null, update_channels, guide);
updater(trigger);
}
else {
trigger(new Error('[tvguide] Start: Guide not initialized. Use SetChannels first.'));
}
},
/* Stops the auto-updater
*/
Stop: function (callback) {
stop = true;
},
/* Starts interval calculation of current show progresses
* @interval Interval to call the callback
* @trigger(progresses) current progresses at the requested interval
* @progresses array of current progress per channel in percent
*/
StartProgress: function (interval, trigger) {
var timer = setInterval(function () {
if (stop) {
debug && console.log('[tvguide] Stopping progress updater');
clearInterval(timer);
return;
}
else {
if (initialized) {
// Calculate progresses for all shows
getProgresses(function (err) {
if (err) {
trigger(new Error(err));
return;
}
trigger(progresses);
});
}
else {
trigger(new Error('[tvguide] StartProgresses: guide not yet initialized!'));
debug && console.log('[tvguide] StartProgresses: guide not yet initialized');
}
}
}, interval);
},
/* Lists all available channels in console
*/
ListChannels: function () {
for (var i in channel_list) {
console.log('[' + i + '] ' + channel_list[i]);
}
},
/* Returns all available channels
* @return channel_list
*/
GetChannelList: function () {
return channel_list;
},
/* Returns only selected channels
*/
GetChannels: function () {
var length = Object.keys(guide['now']).length;
var channels = [];
for (var i = 0; i < length; i++) {
channels[i] = i;
}
return channels;
},
/* Lists the current guide in console
*/
ListGuide: function () {
console.log(guide);
},
/* Gets the current local guide
* @return guide
*/
GetGuide: function () {
return guide;
},
/* Gets the current show progresses
* @return progresses
*/
GetProgresses: function () {
return progresses
},
/* Set the channels to be retrieved to the guide
* Use ListChannels to see all available channels
* Order them in the way you want them to appear in the guide
* @new_channels array of new channels
* @callback(error) when the new channels are retrieved
*/
SetChannels: function (new_channels, callback) {
id = new_channels;
initGuide(function (err) {
callback && callback(err);
});
},
/* Lists all available guide fields in console
*/
ListGuideFields: function () {
fetchChannels([0], function (err, channels) {
console.log(Object.keys(channels[id[0]][0]));
});
},
/* Gets all available guide fields
*/
GetGuideFields: function (callback) {
fetchChannels([0], function (err, channels) {
var _keys = (Object.keys(channels[id[0]][0]));
return _keys;
});
},
/* Set the fields to be retrieved to the guide
* Use ListGuideFields to see all available fields.
* Order doesn't matter, as long as 'eindtijd' is the final one!
* @new_guide_fields array of new guide fields
* @callback(error) when the new fields are retrieved
*/
SetGuideFields: function (new_guide_fields, callback) {
guide_fields = new_guide_fields;
updateGuide(function (err) {
callback && callback(err);
});
},
SetGuideWeb: function (new_guide_web) {
guide_web = new_web;
Init();
},
/* Set the timezone offset
* @new_timezone_offset Time offset in hours
* @callback(error) when the new times are adjusted
*/
SetGuideTimezoneOffset: function (new_timezone_offset) {
guide_timezone_offset = new_timezone_offset;
return;
},
/* Set debugging
*/
SetDebug: function (value) {
debug = value;
}
}