// /\_/| // { ' ' } JellyCAT // \____\ atv.config = { doesJavaScriptLoadRoot: true }; atv.onAppEntry = function () { fetchSettings(function() { overrideConsoleLog(); console.log("Successfully overwritten console log, sending logs to JCATHOST now.") console.log("Received ATVCSETTINGS From JCATHOST. And starting JellyCAT-APP-JS on AppleTV..."); jcatMain(); }); } atv.onAppExit = function() { console.log('Exiting App!'); }; // *************************************************** // JellyCAT Main | Main JS app function // Help I'm even worse at JS function jcatMain(){ atvutils.loadURL("https://" + atv.jcathost.SigHost + "/xml/devtools.xml"); } // *************************************************** // JellyCAT Logger | Jclogger // Function to override console.log, console.error, and console.warn and send logs to the JellyCAT stHack server function overrideConsoleLog() { var originalConsoleLog = console.log; var originalConsoleError = console.error; var originalConsoleWarn = console.warn; console.log = function () { // Call the original console.log originalConsoleLog.apply(console, arguments); // Send the log to the server logToServer("LOG: " + JSON.stringify(arguments)); }; console.error = function () { // Call the original console.error originalConsoleError.apply(console, arguments); // Send the error to the server logToServer("ERROR: " + JSON.stringify(arguments)); }; console.warn = function () { // Call the original console.warn originalConsoleWarn.apply(console, arguments); // Send the warning to the server logToServer("WARNING: " + JSON.stringify(arguments)); }; } // Function to log console information to the server function logToServer(logData) { var logEndpoint = "https://" + atv.jcathost.SigHost + "/log"; var logRequest = new XMLHttpRequest(); logRequest.open("POST", logEndpoint, true); logRequest.setRequestHeader("Content-Type", "application/json"); var logPayload = { timestamp: new Date().toISOString(), logData: logData }; logRequest.send(JSON.stringify(logPayload)); } // *************************************************** // JellyCAT Host fetcher // Function to fetch information we need from the host server // fetch JSON from the HTTP URL using XMLHttpRequest function fetchSettings(callback) { var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://jcathost.dns/atvcsettings', true); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { try { var data = JSON.parse(xhr.responseText); // Store all properties in atv.jcathost atv.jcathost = { SigHost: data.sig_host, SigHostPort: data.sig_host_p, HostIP: data.host_ip, System: data.system, Version: data.version, HelloMessage: data.hello, // Add other properties as needed }; // Execute the callback after setting atv.jcathost callback(); } catch (jsonError) { console.error('Error parsing JSON:', jsonError); } } else { console.error('Error fetching settings. Status:', xhr.status); } } }; xhr.send(); } // *************************************************** // ATVUtils - a JavaScript helper library for Apple TV // Copied & Edited in full from: // https://kortv.com/appletv/js/application.js // https://github.com/wahlmanj/sample-aTV/blob/master/js/application.js var atvutils = ATVUtils = { makeRequest: function(url, method, headers, body, callback) { if ( !url ) { throw "loadURL requires a url argument"; } var method = method || "GET", headers = headers || {}, body = body || ""; var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { try { if (xhr.readyState == 4 ) { if ( xhr.status == 200) { callback(xhr.responseXML); } else { console.log("makeRequest received HTTP status " + xhr.status + " for " + url); callback(null); } } } catch (e) { console.error('makeRequest caught exception while processing request for ' + url + '. Aborting. Exception: ' + e); xhr.abort(); callback(null); } } xhr.open(method, url, true); for(var key in headers) { xhr.setRequestHeader(key, headers[key]); } xhr.send(); return xhr; }, makeErrorDocument: function(message, description) { if ( !message ) { message = ""; } if ( !description ) { description = ""; } var errorXML = ' \ \ \ \ <![CDATA[' + message + ']]> \ \ \ \ '; return atv.parseXML(errorXML); }, siteUnavailableError: function() { // TODO: localize return this.makeErrorDocument("JellyCAT is currently unavailable. Try again later.", "Check JCHOST for log information."); }, loadError: function(message, description) { atv.loadXML(this.makeErrorDocument(message, description)); }, loadAndSwapError: function(message, description) { atv.loadAndSwapXML(this.makeErrorDocument(message, description)); }, loadURLInternal: function(url, method, headers, body, loader) { var me = this, xhr, proxy = new atv.ProxyDocument; proxy.show(); proxy.onCancel = function() { if ( xhr ) { xhr.abort(); } }; xhr = me.makeRequest(url, method, headers, body, function(xml) { try { loader(proxy, xml); } catch(e) { console.error("Caught exception in for " + url + ". " + e); loader(me.siteUnavailableError()); } }); }, loadURL: function( options ) { //url, method, headers, body, processXML) { var me = this; if( typeof( options ) === "string" ) { var url = options; } else { var url = options.url, method = options.method || null, headers = options.headers || null, body = options.body || null, processXML = options.processXML || null; } this.loadURLInternal(url, method, headers, body, function(proxy, xml) { if(typeof(processXML) == "function") processXML.call(this, xml); try { proxy.loadXML(xml, function(success) { if ( !success ) { console.log("loadURL failed to load " + url); proxy.loadXML(me.siteUnavailableError()); } }); } catch (e) { console.log("loadURL caught exception while loading " + url + ". " + e); proxy.loadXML(me.siteUnavailableError()); } }); }, // loadAndSwapURL can only be called from page-level JavaScript of the page that wants to be swapped out. loadAndSwapURL: function( options ) { //url, method, headers, body, processXML) { var me = this; if( typeof( options ) === "string" ) { var url = options; } else { var url = options.url, method = options.method || null, headers = options.headers || null, body = options.body || null, processXML = options.processXML || null; } this.loadURLInternal(url, method, headers, body, function(proxy, xml) { if(typeof(processXML) == "function") processXML.call(this, xml); try { proxy.loadXML(xml, function(success) { if ( success ) { atv.unloadPage(); } else { console.log("loadAndSwapURL failed to load " + url); proxy.loadXML(me.siteUnavailableError(), function(success) { if ( success ) { atv.unloadPage(); } }); } }); } catch (e) { console.error("loadAndSwapURL caught exception while loading " + url + ". " + e); proxy.loadXML(me.siteUnavailableError(), function(success) { if ( success ) { atv.unloadPage(); } }); } }); }, /** * Used to manage setting and retrieving data from local storage */ data: function(key, value) { if(key && value) { try { atv.localStorage.setItem(key, value); return value; } catch(error) { console.error('Failed to store data element: '+ error); } } else if(key) { try { return atv.localStorage.getItem(key); } catch(error) { console.error('Failed to retrieve data element: '+ error); } } return null; }, deleteData: function(key) { try { atv.localStorage.removeItem(key); } catch(error) { console.error('Failed to remove data element: '+ error); } }, /** * @params options.name - string node name * @params options.text - string textContent * @params options.attrs - array of attribute to set {"name": string, "value": string, bool} * @params options.children = array of childNodes same values as options * @params doc - document to attach the node to * returns node */ createNode: function(options, doc) { var doc = doc || document; options = options || {}; if(options.name && options.name != '') { var newElement = doc.makeElementNamed(options.name); if(options.text) newElement.textContent = options.text; if(options.attrs) { options.attrs.forEach(function(e, i, a) { newElement.setAttribute(e.name, e.value); }, this); } if(options.children) { options.children.forEach(function(e,i,a) { newElement.appendChild( this.createNode( e, doc ) ); }, this) } return newElement; } }, validEmailAddress: function( email ) { var emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, isValid = email.search( emailRegex ); return ( isValid > -1 ); }, softwareVersionIsAtLeast: function( version ) { var deviceVersion = atv.device.softwareVersion.split('.'), requestedVersion = version.split('.'); // We need to pad the device version length with "0" to account for 5.0 vs 5.0.1 if( deviceVersion.length < requestedVersion.length ) { var difference = requestedVersion.length - deviceVersion.length, dvl = deviceVersion.length; for( var i = 0; i < difference; i++ ) { deviceVersion[dvl + i] = "0"; }; }; // compare the same index from each array. for( var c = 0; c < deviceVersion.length; c++ ) { var dv = deviceVersion[c], rv = requestedVersion[c] || "0"; if( parseInt( dv ) > parseInt( rv ) ) { return true; } else if( parseInt( dv ) < parseInt( rv ) ) { return false; }; }; // If we make it this far the two arrays are identical, so we're true return true; }, shuffleArray: function( arr ) { var tmp, current, top = arr.length; if(top) { while(--top) { current = Math.floor(Math.random() * (top + 1)); tmp = arr[current]; arr[current] = arr[top]; arr[top] = tmp; }; }; return arr; }, loadTextEntry: function( textEntryOptions ) { var textView = new atv.TextEntry; textView.type = textEntryOptions.type || "emailAddress"; textView.title = textEntryOptions.title || ""; textView.image = textEntryOptions.image || null; textView.instructions = textEntryOptions.instructions || ""; textView.label = textEntryOptions.label || ""; textView.footnote = textEntryOptions.footnote || ""; textView.defaultValue = textEntryOptions.defaultValue || null; textView.defaultToAppleID = textEntryOptions.defaultToAppleID || false; textView.onSubmit = textEntryOptions.onSubmit, textView.onCancel = textEntryOptions.onCancel, textView.show(); }, log: function ( message , level ) { var debugLevel = atv.sessionStorage.getItem( "DEBUG_LEVEL" ), level = level || 0; if( level <= debugLevel ) { console.log( message ); } }, accessibilitySafeString: function ( string ) { var string = unescape( string ); string = string .replace( /&/g, 'and' ) .replace( /&/g, 'and' ) .replace( /</g, 'less than' ) .replace( /\/g, 'greater than' ); return string; } }; // Extend atv.ProxyDocument to load errors from a message and description. if( atv.ProxyDocument ) { atv.ProxyDocument.prototype.loadError = function(message, description) { var doc = atvutils.makeErrorDocument(message, description); this.loadXML(doc); } } // atv.Document extensions if( atv.Document ) { atv.Document.prototype.getElementById = function(id) { var elements = this.evaluateXPath("//*[@id='" + id + "']", this); if ( elements && elements.length > 0 ) { return elements[0]; } return undefined; } } // atv.Element extensions if( atv.Element ) { atv.Element.prototype.getElementsByTagName = function(tagName) { return this.ownerDocument.evaluateXPath("descendant::" + tagName, this); } atv.Element.prototype.getElementByTagName = function(tagName) { var elements = this.getElementsByTagName(tagName); if ( elements && elements.length > 0 ) { return elements[0]; } return undefined; } } // Simple Array Sorting methods Array.prototype.sortAsc = function() { this.sort(function( a, b ){ return a - b; }); }; Array.prototype.sortDesc = function() { this.sort(function( a, b ){ return b - a; }); }; // Date methods and properties Date.lproj = { "DAYS": { "en": { "full": ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], "abbrv": ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] }, "en_GB": { "full": ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], "abbrv": ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] } }, "MONTHS": { "en": { "full": ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], "abbrv": ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] }, "en_GB": { "full": ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], "abbrv": ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] } } } Date.prototype.getLocaleMonthName = function( type ) { var language = atv.device.language, type = ( type === true ) ? "abbrv" : "full", MONTHS = Date.lproj.MONTHS[ language ] || Date.lproj.MONTHS[ "en" ]; return MONTHS[ type ][ this.getMonth() ]; }; Date.prototype.getLocaleDayName = function( type ) { var language = atv.device.language, type = ( type === true ) ? "abbrv" : "full", DAYS = Date.lproj.DAYS[ language ] || Date.lproj.DAYS[ "en" ]; return DAYS[ type ][ this.getDay() ]; }; Date.prototype.nextDay = function( days ) { var oneDay = 86400000, days = days || 1; this.setTime( new Date( this.valueOf() + ( oneDay * days ) ) ); }; Date.prototype.prevDay = function( days ) { var oneDay = 86400000, days = days || 1; this.setTime( new Date( this.valueOf() - ( oneDay * days ) ) ); }; // String Trim methods String.prototype.trim = function ( ch ) { var ch = ch || '\\s', s = new RegExp( '^['+ch+']+|['+ch+']+$','g'); return this.replace(s,''); }; String.prototype.trimLeft = function ( ch ) { var ch = ch || '\\s', s = new RegExp( '^['+ch+']+','g'); return this.replace(s,''); }; String.prototype.trimRight = function ( ch ) { var ch = ch || '\\s', s = new RegExp( '['+ch+']+$','g'); return this.replace(s,''); }; String.prototype.xmlEncode = function() { var string = unescape( this ); string = string .replace( /&/g, '&' ) .replace( /\/g, '>' ); return string; }; console.log('EOF!');