PageRenderTime 68ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 2ms

/src/js/qd.js

http://queued.googlecode.com/
JavaScript | 8580 lines | 5871 code | 849 blank | 1860 comment | 845 complexity | 6a49530350732425e620f032ff3ebdd0 MD5 | raw file
Possible License(s): Apache-2.0, MIT, BSD-3-Clause, LGPL-2.0

Large files files are truncated, but you can click here to view the full file

  1. /*
  2. Copyright (c) 2004-2009, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. /*
  7. This is a compiled version of Dojo, built for deployment and not for
  8. development. To get an editable version, please visit:
  9. http://dojotoolkit.org
  10. for documentation and information on getting the source.
  11. */
  12. /*
  13. Copyright (c) 2004-2009, The Dojo Foundation All Rights Reserved.
  14. Available via Academic Free License >= 2.1 OR the modified BSD license.
  15. see: http://dojotoolkit.org/license for details
  16. */
  17. /*
  18. This is a compiled version of Dojo, built for deployment and not for
  19. development. To get an editable version, please visit:
  20. http://dojotoolkit.org
  21. for documentation and information on getting the source.
  22. */
  23. if(!dojo._hasResource["qd.services.util"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  24. dojo._hasResource["qd.services.util"] = true;
  25. dojo.provide("qd.services.util");
  26. (function(){
  27. var reHexEntity=/&#x([^;]+);/g,
  28. reDecEntity=/&#([^;]+);/g;
  29. dojo.mixin(qd.services.util, {
  30. // summary:
  31. // A set of utility methods used throughout the Queued service layers.
  32. prepare: function(/* Object */args, /* dojo.Deferred? */d){
  33. // summary:
  34. // Prepare any deferred (or create a new one) and set
  35. // up the callback/errback pair on it. args.result
  36. // is the callback, args.error is the errback. Used
  37. // primarily by the communication services (online/offline).
  38. var dfd = d || new dojo.Deferred();
  39. if(args.result){
  40. dfd.addCallback(function(data, ioArgs){
  41. args.result.call(args, data, ioArgs);
  42. });
  43. }
  44. if(args.error){
  45. dfd.addErrback(function(evt, ioArgs){
  46. args.error.call(args, evt, ioArgs);
  47. });
  48. }
  49. return dfd; // dojo.Deferred
  50. },
  51. mixin: function(/* Object */dest, /* Object */override){
  52. // summary:
  53. // Custom mixin function that is considered "additive", as
  54. // opposed to simply overriding.
  55. // description:
  56. // Custom mixin function to stamp the properties from override
  57. // onto dest without clobbering member objects as you would in
  58. // a shallow copy like dojo.mixin does; this isn't particularly
  59. // robust or fast, but it works for our title and queue item objects.
  60. //
  61. // The basic property handling rules are:
  62. // - null doesn't overwrite anything, ever
  63. // - scalars get overwritten by anything, including new scalars
  64. // - arrays get overwritten by longer arrays or by objects
  65. // - objects get merged by recursively calling mixin()
  66. for(k in override){
  67. if(override[k] === null || override[k] === undefined){ continue; }
  68. if(dojo.isArray(override[k])){
  69. if(dojo.isArray(dest[k])){ // the longest array wins!
  70. if(override[k].length > dest[k].length){
  71. dest[k] = override[k].slice(0); // make a copy of the override.
  72. }
  73. } else {
  74. if(!dojo.isObject(dest[k])){
  75. dest[k] = override[k].slice(0);
  76. }
  77. }
  78. }
  79. else if(dojo.isObject(override[k])){
  80. if(dest[k] !== null && dojo.isObject(dest[k])){
  81. dest[k] = qd.services.util.mixin(dest[k], override[k]);
  82. }else{
  83. dest[k] = qd.services.util.mixin({}, override[k]);
  84. }
  85. }
  86. else{
  87. if(dest[k] === null || (!dojo.isArray(dest[k]) && !dojo.isObject(dest[k]))){
  88. if(!dest[k]){
  89. dest[k] = override[k];
  90. } else if (dest[k] && override[k] && dest[k] != override[k]){
  91. dest[k] = override[k];
  92. }
  93. }
  94. }
  95. }
  96. return dest; // Object
  97. },
  98. clean: function(/* String */str){
  99. // summary:
  100. // Pull out any HTML tags and replace any HTML entities with the
  101. // proper characters. Used primarily for the description/synopsis
  102. // of a title coming from one of the Netflix public RSS feeds.
  103. return str.replace(reHexEntity, function(){ // String
  104. return String.fromCharCode(parseInt(arguments[1],16));
  105. })
  106. .replace(reDecEntity, function(){
  107. return String.fromCharCode(parseInt(arguments[1],10));
  108. })
  109. .replace(/\&quot\;/g, '"')
  110. .replace(/\&apos\;/g, "'")
  111. .replace(/\&amp\;/g, "&")
  112. .replace(/<[^>]*>/g, "");
  113. },
  114. image: {
  115. // summary:
  116. // Helper functions for caching images for offline.
  117. url: function(url){
  118. // summary:
  119. // Return the best url for the image.
  120. // url: String
  121. // The Netflix URL to check against the local cache.
  122. var file = air.File.applicationStorageDirectory.resolvePath(url.replace("http://", ""));
  123. if(file.exists){
  124. return file.url;
  125. }
  126. return url; // String
  127. },
  128. store: function(url){
  129. // summary:
  130. // Return the best url for the image.
  131. // url: String
  132. // The Netflix URL to store to the local cache.
  133. var l = new air.URLLoader(), u = new air.URLRequest(url);
  134. var dfd = new dojo.Deferred();
  135. l.dataFormat = air.URLLoaderDataFormat.BINARY;
  136. // save the data once it has completed loading.
  137. l.addEventListener(air.Event.COMPLETE, function(evt){
  138. // make sure the cache directory is created
  139. var tmpUrl = url.replace("http://", "");
  140. var file = air.File.applicationStorageDirectory.resolvePath(tmpUrl);
  141. // this branch shouldn't happen but just in case...
  142. if(file.exists){
  143. file.deleteFile();
  144. }
  145. // open up the file object for writing.
  146. var fs = new air.FileStream();
  147. fs.open(file, air.FileMode.WRITE);
  148. fs.writeBytes(l.data, 0, l.data.length);
  149. fs.close();
  150. // fire the callback
  151. dfd.callback(file.url, url);
  152. });
  153. // do something about an error
  154. l.addEventListener(air.IOErrorEvent.IO_ERROR, function(evt){
  155. dfd.errback(url, evt);
  156. });
  157. // just in case a security error is thrown.
  158. l.addEventListener(air.SecurityErrorEvent.SECURITY_ERROR, function(evt){
  159. dfd.errback(url, evt);
  160. });
  161. // load the URL.
  162. l.load(u);
  163. return dfd; // dojo.Deferred
  164. }
  165. }
  166. });
  167. })();
  168. }
  169. if(!dojo._hasResource["qd.services.storage"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  170. dojo._hasResource["qd.services.storage"] = true;
  171. dojo.provide("qd.services.storage");
  172. (function(){
  173. var els = air.EncryptedLocalStore,
  174. ba = air.ByteArray;
  175. qd.services.storage = new (function(/* Boolean? */useCompression){
  176. // summary:
  177. // A singleton object that acts as the broker to the Encrypted Local Storage of AIR.
  178. var compress = useCompression || false;
  179. // basic common functionality
  180. this.item = function(/* String */key, /* Object? */value){
  181. // summary:
  182. // Provide a dojo-like interface for getting and
  183. // setting items in the Store.
  184. if(key === null || key === undefined || !key.length){
  185. throw new Error("qd.services.storage.item: you cannot pass an undefined or empty string as a key.");
  186. }
  187. if(value !== undefined){
  188. // setter branch
  189. var stream = new ba();
  190. stream.writeUTFBytes(dojo.toJson(value));
  191. if(compress){
  192. stream.compress();
  193. }
  194. els.setItem(key, stream);
  195. return value; // Object
  196. }
  197. // getter branch
  198. var stream = els.getItem(key);
  199. if(!stream){
  200. return null; // Object
  201. }
  202. if(compress){
  203. try {
  204. stream.uncompress();
  205. } catch(ex){
  206. // odds are we have an uncompressed thing here, so simply kill it and return null.
  207. els.removeItem(key);
  208. return null; // Object
  209. }
  210. }
  211. // just in case, we make sure there's no "undefined" in the pulled JSON.
  212. var s = stream.readUTFBytes(stream.length).replace("undefined", "null");
  213. return dojo.fromJson(s); // Object
  214. };
  215. this.remove = function(/* String */key){
  216. // summary:
  217. // Remove the item at key from the Encrypted Local Storage.
  218. if(key === null || key === undefined || !key.length){
  219. throw new Error("qd.services.storage.remove: you cannot pass an undefined or empty string as a key.");
  220. }
  221. els.removeItem(key);
  222. };
  223. this.clear = function(){
  224. // summary:
  225. // Clear out anything in the Encryped Local Storage.
  226. els.reset();
  227. this.onClear();
  228. };
  229. this.onClear = function(){
  230. // summary:
  231. // Stub function to run anything when the storage is cleared.
  232. };
  233. })();
  234. })();
  235. }
  236. if(!dojo._hasResource["qd.services.data"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  237. dojo._hasResource["qd.services.data"] = true;
  238. dojo.provide("qd.services.data");
  239. qd.services.data = new (function(){
  240. // summary:
  241. // A singleton object that handles any interaction with the
  242. // encrypted database.
  243. // database: String
  244. // The filename of the database.
  245. this._testKey = "h1dd3n!!11one1";
  246. this._testDb = "queued-test.db"; // revert to queued.db
  247. var _key = this._testKey;
  248. this.database = this._testDb;
  249. var initFile = "js/updates/resources/initialize.sql",
  250. initialized = false;
  251. var syncConn = new air.SQLConnection(),
  252. asyncConn = new air.SQLConnection(),
  253. inCreation = false,
  254. self = this;
  255. // Properties
  256. this.__defineGetter__("initialized", function(){
  257. // summary:
  258. // Returns whether the engine is initialized.
  259. return initialized; // Boolean
  260. });
  261. // these can't be getters, unfortunately.
  262. this.connection = function(/* Boolean? */async){
  263. // summary:
  264. // Return the proper connection.
  265. return (async ? asyncConn : syncConn); // air.SQLConnection
  266. };
  267. this.connected = function(/* Boolean? */async){
  268. // summary:
  269. // Returns whether the appropriate connection is actually connected.
  270. return (async ? asyncConn.connected : syncConn.connected ); // Boolean
  271. };
  272. this.transacting = function(/* Boolean? */async){
  273. // summary:
  274. // Return whether the appropriate connection is in the middle of a transaction.
  275. return (async ? asyncConn.inTransaction : syncConn.inTransaction ); // Boolean
  276. };
  277. this.lastId = function(/* Boolean? */async){
  278. // summary:
  279. // Return the lastId of the appropriate connection (INSERT/REPLACE).
  280. return (async ? asyncConn.lastInsertRowID : syncConn.lastInsertRowID ); // mixed
  281. };
  282. function eventSetup(/* air.SQLConnection */conn){
  283. // set up all of our event handlers on the passed connection
  284. // open the db, set up the connection handlers
  285. conn.addEventListener(air.SQLEvent.OPEN, self.onOpen);
  286. conn.addEventListener(air.SQLErrorEvent.ERROR, self.onError);
  287. conn.addEventListener(air.SQLEvent.CLOSE, self.onClose);
  288. conn.addEventListener(air.SQLEvent.ANALYZE, self.onAnalyze);
  289. conn.addEventListener(air.SQLEvent.DEANALYZE, self.onDeanalyze);
  290. conn.addEventListener(air.SQLEvent.COMPACT, self.onCompact);
  291. conn.addEventListener(air.SQLEvent.BEGIN, self.onBegin);
  292. conn.addEventListener(air.SQLEvent.COMMIT, self.onCommit);
  293. conn.addEventListener(air.SQLEvent.ROLLBACK, self.onRollback);
  294. conn.addEventListener(air.SQLEvent.CANCEL, self.onCancel);
  295. conn.addEventListener(air.SQLUpdateEvent.INSERT, self.onInsert);
  296. conn.addEventListener(air.SQLUpdateEvent.UPDATE, self.onUpdate);
  297. conn.addEventListener("delete", self.onDelete);
  298. return conn;
  299. }
  300. this.init = function(/* String */key, /* String */db, /* Boolean? */forceCreate){
  301. // summary:
  302. // Initialize the Queued data service.
  303. // set up the key
  304. var k = key||this._testKey;
  305. if(typeof(k) == "string"){
  306. _key = new air.ByteArray();
  307. _key.writeUTFBytes(k);
  308. k = _key;
  309. }
  310. if(key){ this._testKey = key; }
  311. this.database = db || this.database;
  312. // open the sync connection and test to see if it needs to run the create statements
  313. var sync = eventSetup(syncConn);
  314. sync.open(air.File.applicationStorageDirectory.resolvePath(this.database), "create", false, 1024, k);
  315. // open the async connection
  316. var async = eventSetup(asyncConn);
  317. async.openAsync(air.File.applicationStorageDirectory.resolvePath(this.database), "create", null, false, 1024, k);
  318. if(!forceCreate){
  319. var s = new air.SQLStatement();
  320. s.sqlConnection = sync;
  321. // latest change: remove integer ID from Title table. If it exists, recreate.
  322. s.text = "SELECT json FROM Title LIMIT 1";
  323. try{
  324. s.execute();
  325. } catch(e){
  326. this.create({ connection: sync, file: this.initFile });
  327. }
  328. } else {
  329. this.create({ connection: sync, file: this.initFile });
  330. }
  331. this.onInitialize();
  332. // attach to app.onExit
  333. var h = dojo.connect(qd.app, "onExit", function(evt){
  334. if(evt.isDefaultPrevented()){
  335. return;
  336. }
  337. dojo.disconnect(h);
  338. async.close();
  339. sync.close();
  340. air.trace("Database connections closed.");
  341. });
  342. };
  343. /*=====
  344. qd.services.data.__CreateArgs = function(file, connection){
  345. // summary:
  346. // Optional keyword arguments object for the create method.
  347. // file: String?
  348. // The filename to be used for creating the db.
  349. // connection: air.SQLConnection?
  350. // The connection to be used for creating the db. Defaults
  351. // to the synchronous connection.
  352. }
  353. =====*/
  354. this.create = function(/* qd.services.data.__CreateArgs */kwArgs){
  355. // summary:
  356. // Create the database.
  357. inCreation = true;
  358. var file = kwArgs && kwArgs.file || initFile,
  359. conn = kwArgs && kwArgs.connection || syncConn;
  360. var f = air.File.applicationDirectory.resolvePath(file);
  361. if(f.exists){
  362. // kill off the async connection first.
  363. asyncConn.close();
  364. asyncConn = null;
  365. var fs = new air.FileStream();
  366. fs.open(f, air.FileMode.READ);
  367. var txt = fs.readUTFBytes(fs.bytesAvailable);
  368. fs.close();
  369. var st = new Date();
  370. // break it apart.
  371. txt = txt.replace(/\t/g, "");
  372. var c="", inMerge = false, test = txt.split(/\r\n|\r|\n/), a=[];
  373. for(var i=0; i<test.length; i++){
  374. if(inMerge){
  375. c += test[i];
  376. if(test[i].indexOf(")")>-1){
  377. a.push(c);
  378. c = "";
  379. inMerge = false;
  380. }
  381. } else {
  382. if(test[i].indexOf("(")>-1 && test[i].indexOf(")")==-1){
  383. inMerge = true;
  384. c += test[i];
  385. } else {
  386. a.push(test[i]);
  387. }
  388. }
  389. }
  390. // use raw SQL statements here because of the need to preempt any
  391. // statements that might have been called while creating.
  392. for(var i=0, l=a.length; i<l; i++){
  393. var item = dojo.trim(a[i]);
  394. if(!item.length || item.indexOf("--")>-1){ continue; }
  395. var s = new air.SQLStatement();
  396. s.text = item;
  397. s.sqlConnection = conn;
  398. s.execute();
  399. }
  400. // profiling
  401. console.warn("db creation took " + (new Date().valueOf() - st.valueOf()) + "ms.");
  402. // re-open the async connection.
  403. asyncConn = new air.SQLConnection();
  404. var async = eventSetup(asyncConn);
  405. async.openAsync(air.File.applicationStorageDirectory.resolvePath(this.database), "create", null, false, 1024, _key);
  406. // fire off the onCreate event.
  407. this.onCreate();
  408. // run an analysis on it
  409. //conn.analyze();
  410. }
  411. };
  412. /*=====
  413. qd.services.data.fetch.__Args = function(sql, params, result, error){
  414. // sql: String
  415. // The SQL statement to be executed.
  416. // params: Object|Array?
  417. // Any parameters to be pushed into the SQL statement. If an
  418. // Array, expects the SQL statement to be using ?, if an object
  419. // it expects the SQL statement to be using keywords, prepended
  420. // with ":".
  421. // result: Function?
  422. // The callback to be executed when results are returned.
  423. // error: Function?
  424. // The callback to be executed when an error occurs.
  425. this.sql = sql;
  426. this.params = params;
  427. this.result = result;
  428. this.error = error;
  429. }
  430. =====*/
  431. function prep(/* qd.services.data.fetch.__Args */kwArgs, /* air.SQLStatement */s, /* Boolean */async){
  432. // summary:
  433. // Prepare the SQL statement and return it.
  434. s.sqlConnection = kwArgs.connection || (async ? asyncConn : syncConn);
  435. s.text = kwArgs.sql;
  436. if(kwArgs.params && dojo.isArray(kwArgs.params)){
  437. // allow the ordered list version
  438. for(var i=0, l=kwArgs.params.length; i<l; i++){
  439. s.parameters[i] = kwArgs.params[i];
  440. }
  441. } else {
  442. var params = kwArgs.params || {};
  443. for(var p in params){
  444. s.parameters[":" + p] = params[p];
  445. }
  446. }
  447. return s; // air.SQLStatement
  448. }
  449. var queue = [], createHandler;
  450. function exec(){
  451. var o = queue.shift();
  452. if(o){
  453. o.deferred.addCallback(exec);
  454. o.deferred.addErrback(exec);
  455. o.statement.execute();
  456. }
  457. }
  458. function query(/* qd.services.data.fetch.__Args */kwArgs, /* air.SQLStatement */s){
  459. // summary:
  460. // Inner function to communicate with the database.
  461. // set up the deferred.
  462. var dfd = new dojo.Deferred();
  463. // set up the event handlers.
  464. var onResult = function(evt){
  465. var result = s.getResult();
  466. dfd.callback(result);
  467. };
  468. var onError = function(evt){
  469. console.warn(evt);
  470. dfd.errback(evt);
  471. }
  472. if(kwArgs.result){
  473. dfd.addCallback(function(result){
  474. kwArgs.result.call(kwArgs, result.data, result);
  475. });
  476. }
  477. if(kwArgs.error){
  478. dfd.addErrback(function(evt){
  479. kwArgs.error.call(kwArgs, evt);
  480. });
  481. }
  482. s.addEventListener(air.SQLEvent.RESULT, onResult);
  483. s.addEventListener(air.SQLErrorEvent.ERROR, onError);
  484. queue.push({
  485. statement: s,
  486. deferred: dfd
  487. });
  488. if(!inCreation){
  489. exec();
  490. }
  491. else if(!createHandler){
  492. // we only want this to start once, don't go adding a bunch more connections
  493. createHandler = dojo.connect(self, "onCreate", function(){
  494. dojo.disconnect(createHandler);
  495. createHandler = null;
  496. exec();
  497. });
  498. }
  499. return dfd; // dojo.Deferred
  500. }
  501. this.fetch = function(/* qd.services.data.fetch.__Args */kwArgs){
  502. // summary:
  503. // Fetch (i.e. read) data out of the database. Can be used for write operations
  504. // but is not recommended; use execute for write ops. This method is hard-coded
  505. // to use the synchronous connection (i.e. thread-blocking).
  506. if(!kwArgs.sql){
  507. console.log("qd.services.data.fetch: no SQL passed. " + dojo.toJson(kwArgs));
  508. return null;
  509. }
  510. // fetch should use the sync connection unless an SQLConnection is passed with the kwArgs.
  511. var s = prep(kwArgs, new air.SQLStatement(), false),
  512. d = query(kwArgs, s);
  513. this.onFetch(kwArgs);
  514. return d; // dojo.Deferred
  515. };
  516. this.execute = function(/* qd.services.data.fetch.__Args */kwArgs){
  517. // summary:
  518. // Execute the passed SQL against the database. Should be used
  519. // for write operations (INSERT, REPLACE, DELETE, UPDATE). This
  520. // method is hard-coded to use the asynchronous connection.
  521. if(!kwArgs.sql){
  522. console.log("qd.services.data.execute: no SQL passed. " + dojo.toJson(kwArgs));
  523. return null;
  524. }
  525. // execute should use the async connection unless an SQLConnection is passed with the kwArgs.
  526. var s = prep(kwArgs, new air.SQLStatement(), true),
  527. d = query(kwArgs, s);
  528. this.onExecute(kwArgs);
  529. return d; // dojo.Deferred
  530. };
  531. // event stubs
  532. this.onError = function(/* air.Event */evt){ };
  533. this.onOpen = function(/* air.Event */evt){ };
  534. this.onClose = function(/* air.Event */evt){ };
  535. // analysis & maintenance
  536. this.onAnalyze = function(/* air.Event */evt){ };
  537. this.onDeanalyze = function(/* air.Event */evt){ };
  538. this.onCompact = function(/* air.Event */evt){ };
  539. this.onInitialize = function(){ };
  540. this.onCreate = function(){
  541. inCreation = false;
  542. };
  543. // adding other database files
  544. this.onAttach = function(/* air.Event */evt){ };
  545. this.onDetach = function(/* air.Event */evt){ };
  546. // transactions
  547. this.onBegin = function(/* air.Event */evt){ };
  548. this.onCommit = function(/* air.Event */evt){ };
  549. this.onRollback = function(/* air.Event */evt){ };
  550. // SQL execution
  551. this.onFetch = function(/* qd.services.data.fetch.__Args */kwArgs){ };
  552. this.onExecute = function(/* qd.services.data.fetch.__Args */kwArgs){ };
  553. this.onCancel = function(/* air.Event */evt){ };
  554. this.onInsert = function(/* air.Event */evt){ };
  555. this.onUpdate = function(/* air.Event */evt){ };
  556. this.onDelete = function(/* air.Event */evt){ };
  557. })();
  558. }
  559. if(!dojo._hasResource["qd.services.network"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  560. dojo._hasResource["qd.services.network"] = true;
  561. dojo.provide("qd.services.network");
  562. (function(){
  563. var monitor, monitorUrl="http://www.netflix.com";
  564. qd.services.network = new (function(){
  565. // summary:
  566. // A singleton object for access to the network layer of Queued.
  567. var self = this,
  568. pollInterval = 2500;
  569. var statusChange = function(e){
  570. self.onChange((monitor && monitor.available));
  571. };
  572. // Properties
  573. this.__defineGetter__("isRunning", function(){
  574. // summary:
  575. // Return whether or not the monitor is running.
  576. return (monitor && monitor.running); // Boolean
  577. });
  578. this.__defineGetter__("lastPoll", function(){
  579. // summary:
  580. // Return the last time the monitor checked the network status.
  581. return (monitor && monitor.lastStatusUpdate); // Date
  582. });
  583. this.__defineGetter__("available", function(){
  584. // summary:
  585. // Return whether or not the network is available.
  586. return monitor && monitor.available; // Boolean
  587. });
  588. this.__defineSetter__("available", function(/* Boolean */b){
  589. // summary:
  590. // Explicitly set the network availability
  591. if(monitor){
  592. monitor.available = b;
  593. }
  594. return (monitor && monitor.available); // Boolean
  595. });
  596. // FIXME: This is for DEV purposes only!
  597. this.offline = function(){
  598. monitor.stop();
  599. monitor.available = false;
  600. };
  601. this.online = function(){
  602. monitor.start();
  603. };
  604. this.initialized = false;
  605. // Methods
  606. this.init = function(/* String? */url){
  607. // summary:
  608. // Initialize the network services by creating and starting the monitor.
  609. // set up the offline monitor
  610. monitor = new air.URLMonitor(new air.URLRequest((url||monitorUrl)));
  611. monitor.pollInterval = pollInterval;
  612. monitor.addEventListener(air.StatusEvent.STATUS, statusChange);
  613. self.initialized = true;
  614. self.onInitialize(monitor.urlRequest.url);
  615. };
  616. this.start = function(){
  617. // summary:
  618. // Start the monitor services.
  619. if(!monitor){
  620. self.init();
  621. }
  622. console.log("qd.services.network.start: monitor is running.");
  623. self.onStart();
  624. return monitor.start();
  625. };
  626. this.stop = function(){
  627. // summary:
  628. // Stop the monitor services.
  629. console.log("qd.services.network.stop: monitor is stopped.");
  630. self.onStop();
  631. return (monitor && monitor.stop());
  632. };
  633. // Event stubs
  634. this.onInitialize = function(/* String */url){
  635. // summary:
  636. // Fires when the network services is initialized.
  637. qd.app.splash("Network services initialized");
  638. };
  639. this.onStart = function(){
  640. // summary:
  641. // Fires when the network services is started.
  642. qd.app.splash("Network services started");
  643. };
  644. this.onStop = function(){
  645. // summary:
  646. // Fires when the network services is stopped.
  647. };
  648. this.onChange = function(/* Boolean */isAvailable){
  649. // summary:
  650. // Stub event to connect to when the network status changes
  651. console.log("qd.services.network.onChange: current status is " + isAvailable);
  652. };
  653. })();
  654. })();
  655. }
  656. if(!dojo._hasResource["qd.services.parser"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  657. dojo._hasResource["qd.services.parser"] = true;
  658. dojo.provide("qd.services.parser");
  659. (function(){
  660. // summary:
  661. // A set of objects used to parse any XML returned by the Netflix API.
  662. var util = qd.services.util;
  663. // TITLES
  664. var baseTitle = {
  665. guid: null,
  666. rentalHistoryGuid: null,
  667. atHomeGuid: null,
  668. recommendationGuid: null,
  669. ratingGuid: null,
  670. type: null,
  671. title: null,
  672. art:{
  673. small: null,
  674. medium: null,
  675. large: null
  676. },
  677. releaseYear: null,
  678. runTime: null,
  679. rating: null,
  680. synopsis: null,
  681. ratings:{ average:null, predicted:null, user:null },
  682. categories: [],
  683. bonusMaterials: [],
  684. awards: {
  685. nominee:[],
  686. winner:[]
  687. },
  688. formats:{ },
  689. screenFormats: [],
  690. cast: [],
  691. directors: [],
  692. series: {},
  693. season: {},
  694. similars: [],
  695. audio: {},
  696. dates: {
  697. updated: null,
  698. availability: null
  699. },
  700. discs: [],
  701. episodes: [],
  702. fullDetail: false
  703. };
  704. // Parse helpers.
  705. function parseAwards(nl){
  706. var ret = [];
  707. for(var i=0,l=nl.length; i<l; i++){
  708. var n = nl[i];
  709. var tmp = {
  710. year: n.getAttribute("year")
  711. };
  712. var c = n.childNodes;
  713. for(var j=0, jl=c.length; j<jl; j++){
  714. var cn = c[j];
  715. if(cn.nodeType==1){
  716. if(cn.tagName=="category"){
  717. tmp.scheme = cn.getAttribute("scheme");
  718. tmp.term = cn.getAttribute("term");
  719. }
  720. else if(cn.tagName=="link"){
  721. var guid = cn.getAttribute("href");
  722. tmp.person = {
  723. guid: guid,
  724. id: guid.substr(guid.lastIndexOf("/")+1),
  725. title: cn.getAttribute("title")
  726. };
  727. }
  728. }
  729. }
  730. ret.push(tmp);
  731. }
  732. return ret;
  733. }
  734. function parseFormats(nl){
  735. var ret = {};
  736. for(var i=0, l=nl.length; i<l; i++){
  737. var available = nl[i].getAttribute("available_from");
  738. var d = parseInt(available+"000", 10);
  739. var n = nl[i].getElementsByTagName("category")[0];
  740. if(n){
  741. ret[n.getAttribute("term")] = (available)?new Date(d):null;
  742. }
  743. }
  744. return ret;
  745. }
  746. function parseDate(tenDijitStr){
  747. return dojo.date.locale.format(new Date(Number(tenDijitStr+"000")), {selector:"date", datePattern:"MM/dd/yy"});
  748. }
  749. function parseScreenFormats(nl){
  750. var ret = [];
  751. for(var i=0, l=nl.length; i<l; i++){
  752. var categories = nl[i].getElementsByTagName("category"),
  753. info = { title:"", screen:"" };
  754. for(var j=0, jl=categories.length; j<jl; j++){
  755. var c = categories[j];
  756. if(c.getAttribute("scheme").indexOf("title")>-1){
  757. info.title = c.getAttribute("term");
  758. } else {
  759. info.screen = c.getAttribute("term");
  760. }
  761. }
  762. ret.push(info);
  763. }
  764. return ret;
  765. }
  766. function parseLinks(nl){
  767. var ret = [];
  768. for(var i=0, l=nl.length; i<l; i++){
  769. var guid = nl[i].getAttribute("href");
  770. ret.push({
  771. guid: guid,
  772. title: nl[i].getAttribute("title")
  773. });
  774. }
  775. return ret;
  776. }
  777. function parseAudio(nl){
  778. var ret = {};
  779. for(var i=0, l=nl.length; i<l; i++){
  780. var node = nl[i], tfnode;
  781. for(var j=0; j<node.childNodes.length; j++){
  782. if(node.childNodes[j].nodeType != 1){ continue; }
  783. tfnode = node.childNodes[j];
  784. break;
  785. }
  786. var tf = tfnode.getAttribute("term");
  787. ret[tf]={ };
  788. for(var j=0; j<tfnode.childNodes.length; j++){
  789. if(tfnode.childNodes[j].nodeType != 1){ continue; }
  790. var lnode = tfnode.childNodes[j],
  791. tmp = [],
  792. lang = tfnode.childNodes[j].getAttribute("term");
  793. for(var k=0; k<lnode.childNodes.length; k++){
  794. if(lnode.childNodes[k].nodeType != 1){ continue; }
  795. tmp.push(lnode.childNodes[k].getAttribute("term"));
  796. }
  797. ret[tf][lang] = tmp;
  798. }
  799. }
  800. return ret;
  801. }
  802. // public functions
  803. var p = qd.services.parser;
  804. p.titles = {
  805. // summary:
  806. // The XML parser for any title information (movie, TV show, etc.)
  807. fromRss: function(/* XmlNode */node, /* Object? */obj){
  808. // summary:
  809. // Parse basic movie information from the passed RSS element.
  810. var o = dojo.clone(baseTitle);
  811. for(var i=0, l=node.childNodes.length; i<l; i++){
  812. var n=node.childNodes[i];
  813. if(n.nodeType != 1){ continue; } // ignore non-elements
  814. switch(n.tagName){
  815. case "title":
  816. var pieces = dojo.trim(n.textContent).match(/^\d+-\s(.*)$/);
  817. o.title = pieces ? pieces[1]: dojo.trim(n.textContent);
  818. break;
  819. case "guid":
  820. /*
  821. // TODO: TV series/seasons detection.
  822. var guid = dojo.trim(n.textContent);
  823. // swap out their ID for the real one.
  824. o.id = guid.substr(guid.lastIndexOf("/")+1);
  825. o.guid = "http://api.netflix.com/catalog/titles/movies/" + o.id;
  826. */
  827. break;
  828. case "description":
  829. var pieces = dojo.trim(n.textContent).match(/<img src="([^"]+)".*<br>([^$]*)/);
  830. if(pieces){
  831. o.art.small = pieces[1].replace("/small/", "/tiny/");
  832. o.art.medium = pieces[1];
  833. o.art.large = pieces[1].replace("/small/", "/large/");
  834. o.synopsis = util.clean(pieces[2]);
  835. }
  836. break;
  837. }
  838. }
  839. if(obj){
  840. o = util.mixin(obj, o);
  841. }
  842. return o; // Object
  843. },
  844. fromXml: function(/* XmlNode */node, /* Object?*/obj){
  845. // summary:
  846. // Parse the returned title information from the passed XmlNode.
  847. var o = dojo.clone(baseTitle);
  848. var links = node.ownerDocument.evaluate("./link", node),
  849. info = node.ownerDocument.evaluate("./*[name()!='link']", node),
  850. currentNode;
  851. while(currentNode = info.iterateNext()){
  852. switch(currentNode.tagName){
  853. case "id":
  854. // need to fork this a little because of "other" ids that are
  855. // possibly passed by Netflix.
  856. var test = currentNode.parentNode.tagName,
  857. value = dojo.trim(currentNode.textContent);
  858. if(test == "ratings_item"){
  859. o.ratingGuid = value;
  860. }
  861. else if(test == "rental_history_item"){
  862. o.rentalHistoryGuid = value;
  863. }
  864. else if(test == "at_home_item"){
  865. o.atHomeGuid = value;
  866. }
  867. else if(test == "recommendation"){
  868. o.recommendationGuid = value;
  869. }
  870. else {
  871. o.guid = value;
  872. }
  873. break;
  874. case "title":
  875. o.title = currentNode.getAttribute("regular");
  876. break;
  877. case "box_art":
  878. o.art = {
  879. small: currentNode.getAttribute("small"),
  880. medium: currentNode.getAttribute("medium"),
  881. large: currentNode.getAttribute("large")
  882. };
  883. break;
  884. case "release_year":
  885. o.releaseYear = dojo.trim(currentNode.textContent);
  886. break;
  887. case "runtime":
  888. o.runTime = parseInt(dojo.trim(currentNode.textContent), 10)/60;
  889. break;
  890. case "category":
  891. var scheme = currentNode.getAttribute("scheme");
  892. scheme = scheme.substr(scheme.lastIndexOf("/")+1);
  893. if(scheme == "mpaa_ratings" || scheme == "tv_ratings"){
  894. o.rating = currentNode.getAttribute("term");
  895. }
  896. else if (scheme == "genres"){
  897. o.categories.push(currentNode.getAttribute("term"));
  898. }
  899. break;
  900. case "user_rating":
  901. var val = currentNode.getAttribute("value");
  902. if(val == "not_interested"){
  903. o.ratings.user = val;
  904. }else{
  905. o.ratings.user = parseFloat(dojo.trim(currentNode.textContent), 10);
  906. }
  907. break;
  908. case "predicted_rating":
  909. o.ratings.predicted = parseFloat(dojo.trim(currentNode.textContent), 10);
  910. break;
  911. case "average_rating":
  912. o.ratings.average = parseFloat(dojo.trim(currentNode.textContent), 10);
  913. break;
  914. case "availability_date":
  915. o.dates.availability = parseDate(currentNode.textContent);
  916. break;
  917. case "updated":
  918. o.dates.updated = parseDate(currentNode.textContent);
  919. break;
  920. }
  921. }
  922. // do the links now.
  923. while(currentNode = links.iterateNext()){
  924. var type = currentNode.getAttribute("title"),
  925. rel = currentNode.getAttribute("rel");
  926. switch(rel){
  927. case "http://schemas.netflix.com/catalog/titles/synopsis":
  928. o.synopsis = util.clean(dojo.trim(currentNode.textContent));
  929. break;
  930. case "http://schemas.netflix.com/catalog/titles/awards":
  931. o.awards.nominee=parseAwards(currentNode.getElementsByTagName("award_nominee"));
  932. o.awards.winner=parseAwards(currentNode.getElementsByTagName("award_winner"));
  933. break;
  934. case "http://schemas.netflix.com/catalog/titles/format_availability":
  935. var nodes = currentNode.getElementsByTagName("availability");
  936. if(nodes && nodes.length){
  937. o.formats = parseFormats(nodes);
  938. }
  939. break;
  940. case "http://schemas.netflix.com/catalog/titles/screen_formats":
  941. o.screenFormats = parseScreenFormats(currentNode.getElementsByTagName("screen_format"));
  942. break;
  943. case "http://schemas.netflix.com/catalog/people.cast":
  944. o.cast = parseLinks(currentNode.getElementsByTagName("link"));
  945. break;
  946. case "http://schemas.netflix.com/catalog/people.directors":
  947. o.directors = parseLinks(currentNode.getElementsByTagName("link"));
  948. break;
  949. case "http://schemas.netflix.com/catalog/titles/languages_and_audio":
  950. o.audio = parseAudio(currentNode.getElementsByTagName("language_audio_format"));
  951. break;
  952. case "http://schemas.netflix.com/catalog/titles.similars":
  953. o.similars = parseLinks(currentNode.getElementsByTagName("link"));
  954. break;
  955. case "http://schemas.netflix.com/catalog/titles/bonus_materials":
  956. o.bonusMaterials = parseLinks(currentNode.getElementsByTagName("link"));
  957. break;
  958. case "http://schemas.netflix.com/catalog/titles/official_url":
  959. break;
  960. case "http://schemas.netflix.com/catalog/title":
  961. o.guid = currentNode.getAttribute("href");
  962. o.title = type;
  963. break;
  964. case "http://schemas.netflix.com/catalog/titles.series":
  965. o.series = {
  966. guid: currentNode.getAttribute("href"),
  967. title: type
  968. };
  969. break;
  970. case "http://schemas.netflix.com/catalog/titles.season":
  971. o.season = {
  972. guid: currentNode.getAttribute("href"),
  973. title: type
  974. };
  975. break;
  976. case "http://schemas.netflix.com/catalog/titles.discs":
  977. dojo.query("link", currentNode).forEach(function(disc){
  978. o.discs.push({
  979. guid: disc.getAttribute("href"),
  980. title: disc.getAttribute("title")
  981. });
  982. });
  983. break;
  984. case "http://schemas.netflix.com/catalog/titles.programs":
  985. dojo.query("link", currentNode).forEach(function(episode){
  986. o.episodes.push({
  987. guid: episode.getAttribute("href"),
  988. title: episode.getAttribute("title")
  989. });
  990. });
  991. break;
  992. }
  993. }
  994. if(obj){
  995. o = util.mixin(obj, o);
  996. }
  997. this.setType(o);
  998. o.fullDetail = true; // we have the full details now, so mark it as such.
  999. return o; // Object
  1000. },
  1001. setType: function(/* Object */o){
  1002. // summary:
  1003. // Post-process a parsed title to set a type on it.
  1004. if(o.guid.indexOf("discs")>-1){
  1005. o.type = "disc";
  1006. }
  1007. else if (o.guid.indexOf("programs")>-1){
  1008. o.type = "episode";
  1009. }
  1010. else if (o.guid.indexOf("series")>-1){
  1011. if(o.guid.indexOf("seasons")>-1){
  1012. o.type = "season";
  1013. } else {
  1014. o.type = "series";
  1015. }
  1016. }
  1017. else {
  1018. o.type = "movie"; // generic
  1019. }
  1020. }
  1021. };
  1022. p.queues = {
  1023. // summary:
  1024. // The XML parser for queue information (discs, instant, saved, rental history)
  1025. fromXml: function(/* XmlNode */node, /* Object? */obj){
  1026. // summary:
  1027. // Parse the returned XML into an object to be used by the application.
  1028. // object representing a queue item. Note that the title info is
  1029. // deliberately limited.
  1030. var item = {
  1031. queue: "/queues/disc",
  1032. guid: null,
  1033. id: null,
  1034. position: null,
  1035. availability: null,
  1036. updated: null,
  1037. shipped: null,
  1038. watched: null,
  1039. estimatedArrival: null,
  1040. returned: null,
  1041. viewed: null,
  1042. format: null,
  1043. title: {
  1044. guid: null,
  1045. title: null
  1046. }
  1047. };
  1048. var info = node.ownerDocument.evaluate("./*", node), currentNode;
  1049. while(currentNode = info.iterateNext()){
  1050. switch(currentNode.tagName){
  1051. case "id":
  1052. item.guid = dojo.trim(currentNode.textContent);
  1053. item.id = item.guid;
  1054. var l = item.guid.split("/");
  1055. l.pop(); // pull the id off
  1056. item.queue = l.slice(5).join("/");
  1057. break;
  1058. case "position":
  1059. item.position = parseInt(currentNode.textContent, 10);
  1060. break;
  1061. case "category":
  1062. var scheme = currentNode.getAttribute("scheme");
  1063. if(scheme == "http://api.netflix.com/categories/queue_availability"){
  1064. item.availability = dojo.trim(currentNode.textContent);
  1065. }
  1066. else if(scheme == "http://api.netflix.com/categories/title_formats"){
  1067. item.format = currentNode.getAttribute("term");
  1068. }
  1069. break;
  1070. case "updated":
  1071. item.updated = parseDate(currentNode.textContent);
  1072. break;
  1073. case "shipped_date":
  1074. item.shipped = parseDate(currentNode.textContent);
  1075. break;
  1076. case "watched_date":
  1077. item.watched = parseDate(currentNode.textContent);
  1078. break;
  1079. case "estimated_arrival_date":
  1080. item.estimatedArrival = parseDate(currentNode.textContent);
  1081. break;
  1082. case "returned_date":
  1083. item.returned = parseDate(currentNode.textContent);
  1084. case "viewed_time":
  1085. item.viewed = currentNode.textContent;
  1086. break;
  1087. case "link":
  1088. // we only care about the title this represents.
  1089. var rel = currentNode.getAttribute("rel");
  1090. if(rel == "http://schemas.netflix.com/catalog/title"){
  1091. // use the title parser on the main node here for basic info.
  1092. // Note that it is up to the calling code to merge this title's
  1093. // info with any existing info.
  1094. item.title = p.titles.fromXml(node);
  1095. }
  1096. else if(rel == "http://schemas.netflix.com/queues.available"){
  1097. // we do this here because for the available queues, Netflix embeds
  1098. // the position in the guid.
  1099. var l = currentNode.getAttribute("href");
  1100. // redo the id
  1101. item.id = l + "/" + item.guid.substr(item.guid.lastIndexOf("/")+1);
  1102. }
  1103. break;
  1104. }
  1105. }
  1106. if(obj){
  1107. item = util.mixin(obj, item);
  1108. }
  1109. return item; // Object
  1110. }
  1111. };
  1112. p.users = {
  1113. // summary:
  1114. // The XML parser for any user information
  1115. fromXml: function(/* XmlNode */node, /* Object? */obj){
  1116. // summary:
  1117. // Return a user object from the passed xml node.
  1118. var user = {
  1119. name: { first: null, last: null },
  1120. userId: null,
  1121. canInstantWatch: false,
  1122. preferredFormats: []
  1123. };
  1124. // ignore the links included for now.
  1125. var info = node.ownerDocument.evaluate("./*[name()!='link']", node), currentNode;
  1126. while(currentNode = info.iterateNext()){
  1127. switch(currentNode.tagName){
  1128. case "user_id":
  1129. user.userId = dojo.trim(currentNode.textContent);
  1130. break;
  1131. case "first_name":
  1132. user.name.first = dojo.trim(currentNode.textContent);
  1133. break;
  1134. case "last_name":
  1135. user.name.last = dojo.trim(currentNode.textContent);
  1136. break;
  1137. case "can_instant_watch":
  1138. user.canInstantWatch = dojo.trim(currentNode.textContent)=="true";
  1139. break;
  1140. case "preferred_formats":
  1141. dojo.query("category", currentNode).forEach(function(item){
  1142. user.preferredFormats.push(item.getAttribute("term"));
  1143. });
  1144. break;
  1145. }
  1146. }
  1147. if(obj){
  1148. obj = util.mixin(obj, user);
  1149. }
  1150. return user; // Object
  1151. }
  1152. };
  1153. p.people = {
  1154. // summary:
  1155. // The XML parser for any information on a person (actors, directors, etc.)
  1156. fromXml: function(/* XmlNode */node, /* Object? */obj){
  1157. // summary:
  1158. // Parse the information out of the passed XmlNode for people.
  1159. var person = {
  1160. id: null,
  1161. name: null,
  1162. bio: null,
  1163. filmography: null
  1164. };
  1165. var info = node.ownerDocument.evaluate("./name()", node), currentNode;
  1166. while(currentNode = info.iterateNext()){
  1167. switch(currentNode.tagName){
  1168. case "id":
  1169. case "name":
  1170. case "bio":
  1171. person[currentNode.tagName] = util.clean(dojo.trim(currentNode.textContent));
  1172. break;
  1173. case "link":
  1174. // ignore the alternate link
  1175. if(currentNode.getAttribute("rel") == "http://schemas.netflix.com/catalog/titles.filmography"){
  1176. person.filmography = currentNode.getAttribute("href");
  1177. }
  1178. break;
  1179. }
  1180. }
  1181. if(obj){
  1182. person = util.mixin(obj, person);
  1183. }
  1184. return person; // Object
  1185. }
  1186. };
  1187. p.status = {
  1188. // summary:
  1189. // The XML parser for any status-based updates (modifying a queue, ratings, etc.)
  1190. fromXml: function(/* XmlNode */node){
  1191. // summary:
  1192. // Parse the status info out of the passed node.
  1193. var obj = {};
  1194. for(var i=0, l=node.childNodes.length; i<l; i++){
  1195. var item = node.childNodes[i];
  1196. if(item.nodeType == 1){
  1197. switch(item.tagName){
  1198. case "status_code":
  1199. obj.code = dojo.trim(item.textContent);
  1200. break;
  1201. case "sub_code":
  1202. obj.subcode = dojo.trim(item.textContent);
  1203. break;
  1204. case "message":
  1205. case "etag":
  1206. obj[item.tagName] = dojo.trim(item.textContent);
  1207. break;
  1208. case "resources_created":
  1209. obj.created = dojo.query("queue_item", item).map(function(n){
  1210. return p.queues.fromXml(n);
  1211. });
  1212. break;
  1213. case "failed_title_refs":
  1214. obj.failed = dojo.query("link", item).map(function(n){
  1215. return {
  1216. guid: n.getAttribute("href"),
  1217. title: n.getAttribute("title")
  1218. };
  1219. });
  1220. break;
  1221. case "already_in_queue":
  1222. obj.inQueue = dojo.query("link", item).map(function(n){
  1223. return {
  1224. guid: n.getAttribute("href"),
  1225. title: n.getAttribute("title")
  1226. };
  1227. });
  1228. break;
  1229. }
  1230. }
  1231. }
  1232. return obj; // Object
  1233. }
  1234. };
  1235. })();
  1236. }
  1237. if(!dojo._hasResource["qd.services.online.feeds"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  1238. dojo._hasResource["qd.services.online.feeds"] = true;
  1239. dojo.provide("qd.services.online.feeds");
  1240. (function(){
  1241. var util = qd.services.util,
  1242. ps = qd.services.parser,
  1243. db = qd.services.data;
  1244. var rssFeeds = {
  1245. top25: [],
  1246. top100: null,
  1247. newReleases: null
  1248. };
  1249. var top25Feeds = [], top100Feed, newReleasesFeed;
  1250. var feedlistInit = function(){
  1251. db.fetch({
  1252. sql: "SELECT id, term, lastUpdated, isInstant, feed, xml FROM GenreFeed ORDER BY term",
  1253. result: function(data, result){
  1254. dojo.forEach(data, function(item){
  1255. if(item.feed.indexOf("Top100RSS")>-1){
  1256. rssFeeds.top100 = item;
  1257. } else if(item.feed.indexOf("NewReleasesRSS")>-1){
  1258. rssFeeds.newReleases = item;
  1259. } else {
  1260. rssFeeds.top25.push(item);
  1261. }
  1262. });
  1263. }
  1264. });
  1265. };
  1266. if(db.initialized){
  1267. feedlistInit();
  1268. } else {
  1269. var h = dojo.connect(db, "onInitialize", function(){
  1270. dojo.disconnect(h);
  1271. feedlistInit();
  1272. });
  1273. }
  1274. function getFeedObject(url){
  1275. if(url == rssFeeds.top100.feed){ return rssFeeds.top100; }
  1276. if(url == rssFeeds.newReleases.feed){ return rssFeeds.newReleases; }
  1277. for(var i=0; i<rssFeeds.top25.length; i++){
  1278. if(url == rssFeeds.top25[i].feed){
  1279. return rssFeeds.top25[i];
  1280. }
  1281. }
  1282. return null;
  1283. }
  1284. dojo.mixin(qd.services.online.feeds, {
  1285. // summary:
  1286. // The online-based service for handling Netflix's public RSS feeds.
  1287. list: function(){
  1288. // summary:
  1289. // Return the list of Top 25 RSS feeds.
  1290. return rssFeeds.top25; // Object[]
  1291. },
  1292. top100: function(){
  1293. // summary:
  1294. // Return the top 100 feed object.
  1295. return rssFeeds.top100; // Object
  1296. },
  1297. newReleases: function(){
  1298. // summary:
  1299. // Return the New Releases feed object.
  1300. return rssFeeds.newReleases; // Object
  1301. },
  1302. /*=====
  1303. qd.services.online.feeds.fetch.__FetchArgs = function(url, result, error){
  1304. // summary:
  1305. // Keyword object for getting the contents of an RSS feed.
  1306. // url: String
  1307. // The URL of the feed to fetch.
  1308. // result: Function?
  1309. // The callback to be fired when the RSS feed has been fetched and parsed.
  1310. // error: Function?
  1311. // The errback function to be fired if there is an error during the course of the fetch.
  1312. }
  1313. =====*/
  1314. fetch: function(/* qd.services.online.feeds.fetch.__FetchArgs */kwArgs){
  1315. // summary:
  1316. // Fetch the feed at the url in the feed object, and
  1317. // store/cache the feed when returned.
  1318. var dfd = util.prepare(kwArgs), feed=getFeedObject(kwArgs.url);
  1319. dojo.xhrGet({
  1320. url: kwArgs.url,
  1321. handleAs: "xml",
  1322. load: function(xml, ioArgs){
  1323. var node, parsed = [], items = xml.evaluate("//channel/item", xml);
  1324. while(node = items.iterateNext()){
  1325. parsed.push(ps.titles.fromRss(node));
  1326. }
  1327. // pre and post-process the results (image caching)
  1328. qd.services.online.process(parsed, dfd);
  1329. // push the xml doc into the database
  1330. var sql = "UPDATE GenreFeed SET LastUpdated = DATETIME(), xml=:xml WHERE id=:id ";
  1331. db.execute({
  1332. sql: sql,
  1333. params:{
  1334. id: feed.id,
  1335. xml: new XMLSerializer().serializeToString(xml).replace(/'/g, "''")
  1336. },
  1337. result: function(data){
  1338. // console.log("Stored the feed ("+feed.term+")");
  1339. }
  1340. });
  1341. },
  1342. error: function(err, ioArgs){
  1343. dfd.errback(new Error("qd.service.feeds.fetch: an error occurred when trying to get " + kwArgs.url));
  1344. }
  1345. });
  1346. return dfd; // dojo.Deferred
  1347. }
  1348. });
  1349. })();
  1350. }
  1351. if(!dojo._hasResource["qd.services.online.titles"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  1352. dojo._hasResource["qd.services.online.titles"] = true;
  1353. dojo.provide("qd.services.online.titles");
  1354. (function(){
  1355. var ps = qd.services.parser,
  1356. db = qd.services.data,
  1357. util = qd.services.util;
  1358. var some = encodeURIComponent("discs,episodes,seasons,synopsis,formats, screen formats"),
  1359. expand = encodeURIComponent("awards,cast,directors,discs,episodes,formats,languages and audio,screen formats,seasons,synopsis"); // similars
  1360. function saveTitle(item){
  1361. var param = {
  1362. guid: item.guid,
  1363. link: item.guid,
  1364. title: item.title,
  1365. synopsis: item.synopsis,
  1366. rating: item.rating,
  1367. item: dojo.toJson(item)
  1368. };
  1369. db.execute({
  1370. sql: "SELECT json FROM Title WHERE guid=:guid",
  1371. params: {
  1372. guid: item.guid
  1373. },
  1374. result: function(data){
  1375. if(data && data.length){
  1376. // mix-in the passed json with the stored one.
  1377. item = util.mixin(dojo.fromJson(data[0].json), item);
  1378. param.item = dojo.toJson(item);
  1379. }
  1380. db.execute({
  1381. sql: "REPLACE INTO Title (guid, link, title, lastUpdated, synopsis, rating, json)"
  1382. + " VALUES(:guid, :link, :title, DATETIME(), :synopsis, :rating, :item)",
  1383. params: param,
  1384. result: function(data){
  1385. // don't need this, keeping it for logging purposes.
  1386. },
  1387. error: function(err){
  1388. console.warn("titles.save: ERROR!", error);
  1389. }
  1390. });
  1391. }
  1392. });
  1393. }
  1394. function fetchItem(url, dfd, term){
  1395. var signer = qd.app.authorization;
  1396. dojo.xhrGet(dojox.io.OAuth.sign("GET", {
  1397. url: url,
  1398. handleAs: "xml",
  1399. load: function(xml, ioArgs){
  1400. // get the right node, parse it, cache it, and callback it.
  1401. var node, items = xml.evaluate((term!==undefined?"//catalog_title":"//ratings/ratings_item"), xml), ob;
  1402. while(node = items.iterateNext()){
  1403. if(term !== undefined){
  1404. var test = node.getElementsByTagName("title");
  1405. if(test.length && test[0].getAttribute("regular") == util.clean(term)){
  1406. ob = ps.titles.fromXml(node);
  1407. break;
  1408. }
  1409. } else {
  1410. ob = ps.titles.fromXml(node);
  1411. break;
  1412. }
  1413. }
  1414. if(ob){
  1415. if(term !== undefined){
  1416. // hit it again, this time with a guid (so we can get some ratings)
  1417. url = "http://api.netflix.com/users/" + signer.userId + "/ratings/title"
  1418. + "?title_refs=" + ob.guid
  1419. + "&expand=" + expand;
  1420. fetchItem(url, dfd);
  1421. } else {
  1422. // fire off the callback, passing it the returned object.
  1423. qd.services.item(ob);
  1424. qd.services.online.process(ob, dfd);
  1425. saveTitle(ob);
  1426. }
  1427. } else {
  1428. // in case we don't get an exact term match back from Netflix.
  1429. var e = new Error("No term match from Netflix for " + term);
  1430. e.xml = xml;
  1431. dfd.errback(e, ioArgs);
  1432. }
  1433. },
  1434. error: function(err, ioArgs){
  1435. // TODO: modify for an invalid signature check.
  1436. var e = new Error(err);
  1437. e.xml = ioArgs.xhr.responseXML;
  1438. dfd.errback(e, ioArgs);
  1439. }
  1440. }, signer), false);
  1441. }
  1442. function batchRatingsFetch(titles, args, ratings){
  1443. // summary:
  1444. // Private function to do actual ratings calls.
  1445. var signer = qd.app.authorization,
  1446. refs= [];
  1447. for(var p in titles){
  1448. refs.push(p);
  1449. }
  1450. var url = "http://api.netflix.com/users/" + signer.userId + "/ratings/title"
  1451. + "?title_refs=" + refs.join(",");
  1452. var signedArgs = dojox.io.OAuth.sign("GET", {
  1453. url: url,
  1454. handleAs: "xml",
  1455. load: function(xml, ioArgs){
  1456. // parse the info, merge and save
  1457. var node, items = xml.evaluate("//ratings/ratings_item", xml), a = [], toSave = [];
  1458. while(node = items.iterateNext()){
  1459. var tmp = ps.titles.fromXml(node),
  1460. r = tmp.ratings,
  1461. title = titles[tmp.guid];
  1462. if(!title || dojo.isString(title)){
  1463. // for some reason we only have a string.
  1464. title = tmp;
  1465. } else {
  1466. if(r.predicted !== null){ title.ratings.predicted = r.predicted; }
  1467. if(r.user !== null){ title.ratings.user = r.user; }
  1468. }
  1469. a.push(title);
  1470. qd.services.item(title);
  1471. toSave.push(title);
  1472. ratings.push(title);
  1473. }
  1474. args.result.call(args, a, ioArgs);
  1475. dojo.forEach(toSave, function(item){
  1476. saveTitle(item);
  1477. });
  1478. },
  1479. error: function(err, ioArgs){
  1480. console.warn("Batch ratings fetch: ", err);
  1481. }
  1482. }, signer);
  1483. return dojo.xhrGet(signedArgs);
  1484. }
  1485. function encode(s){
  1486. if(!s){ return ""; }
  1487. return encodeURIComponent(s)
  1488. .replace(/\!/g, "%2521")
  1489. .replace(/\*/g, "%252A")
  1490. .replace(/\'/g, "%2527")
  1491. .replace(/\(/g, "%2528")
  1492. .replace(/\)/g, "%2529");
  1493. }
  1494. dojo.mixin(qd.services.online.titles, {
  1495. // summary:
  1496. // The online-based service to get any title information, including
  1497. // ratings and recommendations.
  1498. save: function(/* Object */item){
  1499. // summary:
  1500. // Save the passed title to the database.
  1501. saveTitle(item);
  1502. },
  1503. clear: function(){
  1504. // summary:
  1505. // Clear all titles out of the database.
  1506. db.execute({
  1507. sql: "DELETE FROM Title",
  1508. result: function(data){
  1509. // don't do anything for now.
  1510. }
  1511. });
  1512. },
  1513. /*=====
  1514. qd.services.online.titles.find.__FindArgs = function(term, start, max, result, error){
  1515. // summary:
  1516. // Arguments object for doing a title search
  1517. // term: String
  1518. // The partial title to be looking for.
  1519. // start: Number?
  1520. // The page index to start on. Default is 0.
  1521. // max: Number?
  1522. // The maximum number of results to find. Default is 25 (supplied by Netflix).
  1523. // result: Function?
  1524. // The callback function that will be executed when a result is
  1525. // fetched.
  1526. // error: Function?
  1527. // The callback function to be executed if there is an error in fetching.
  1528. }
  1529. =====*/
  1530. find: function(/* qd.services.online.titles.find.__FindArgs */kwArgs){
  1531. // summary:
  1532. // Use the Netflix API directly, and try to cache any results as they come.
  1533. var dfd = util.prepare(kwArgs),
  1534. signer = qd.app.authorization;
  1535. var t = encodeURIComponent(util.clean(kwArgs.term));
  1536. if(t.match(/!|\*|\'|\(|\)/g)){
  1537. t = encode(t);
  1538. }
  1539. dojo.xhrGet(dojox.io.OAuth.sign("GET", {
  1540. url: "http://api.netflix.com/catalog/titles?"
  1541. + "term=" + t
  1542. + (kwArgs.start ? "&start_in…

Large files files are truncated, but you can click here to view the full file