UNPKG

tvguide

Version:

Node module that auto-gets current (Dutch) tv show information

505 lines (465 loc) 16.7 kB
/* 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; } }