home *** CD-ROM | disk | FTP | other *** search
/ Freelog 112 / FreelogNo112-NovembreDecembre2012.iso / Multimedia / Songbird / Songbird_2.0.0-2311_windows-i686-msvc8.exe / components / sbSmartMediaListsUpdater.js < prev    next >
Text File  |  2012-06-08  |  33KB  |  807 lines

  1. /*
  2.  *=BEGIN SONGBIRD GPL
  3.  *
  4.  * This file is part of the Songbird web player.
  5.  *
  6.  * Copyright(c) 2005-2010 POTI, Inc.
  7.  * http://www.songbirdnest.com
  8.  *
  9.  * This file may be licensed under the terms of of the
  10.  * GNU General Public License Version 2 (the ``GPL'').
  11.  *
  12.  * Software distributed under the License is distributed
  13.  * on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either
  14.  * express or implied. See the GPL for the specific language
  15.  * governing rights and limitations.
  16.  *
  17.  * You should have received a copy of the GPL along with this
  18.  * program. If not, go to http://www.gnu.org/licenses/gpl.html
  19.  * or write to the Free Software Foundation, Inc.,
  20.  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  21.  *
  22.  *=END SONGBIRD GPL
  23.  */
  24.  
  25. const Cc = Components.classes;
  26. const Ci = Components.interfaces;
  27. const Cr = Components.results;
  28. const Ce = Components.Exception;
  29.  
  30. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  31. Components.utils.import("resource://app/jsmodules/sbProperties.jsm");
  32. Components.utils.import("resource://app/jsmodules/sbLibraryUtils.jsm");
  33. Components.utils.import("resource://app/jsmodules/StringUtils.jsm");
  34.  
  35. const debugLog = false;
  36.  
  37. // ----------------------------------------------------------------------------
  38. // ----------------------------------------------------------------------------
  39.  
  40. function SmartMediaListsUpdater() {
  41.   // ask for a callback when the library manager is ready
  42.   var obs = Cc["@mozilla.org/observer-service;1"]
  43.               .getService(Ci.nsIObserverService);
  44.   obs.addObserver(this, 'songbird-library-manager-ready', false); 
  45. }
  46.  
  47. SmartMediaListsUpdater.prototype = {
  48.   // This is us.
  49.   constructor     : SmartMediaListsUpdater,
  50.   classDescription: "Songbird Smart Medialists Updater Module",
  51.   classID         : Components.ID("{35af253e-c7b0-40d6-a1a2-c747de924639}"),
  52.   contractID      : "@songbirdnest.com/Songbird/SmartMediaListsUpdater;1",
  53.   
  54.   // medialistlistener batch count
  55.   _batchCount            : 0,
  56.   
  57.   // hash table of properties that have been modified
  58.   _updatedProperties     : {},
  59.   
  60.   // hash table of lists to update
  61.   _updateQueue           : {},
  62.   
  63.   // hash table of lists to update condition to filter video items
  64.   _updateVideoQueue      : {},
  65.   
  66.   // currently updating a batch of smart playlists
  67.   _updating              : false,
  68.   
  69.   // Delay between the last db update and the first smart playlist rebuild
  70.   _updateInitialDelay    : 1000,
  71.   
  72.   // Maximum delay between first db update and the delayed check (the actual time
  73.   // may still be more if a batch has not finished, ie, we never check inside batches)
  74.   _maxInitialDelay       : 5000,
  75.   
  76.   // Timestamp of the first time we try to run the delayed batch, reset every time
  77.   // the check happens, but stays the same when it is further delayed)
  78.   _timerInitTime         : null,
  79.   
  80.   // Cause one more checkForUpdate at the end of the current update
  81.   _causeMoreChecks       : false,
  82.   
  83.   // Delay between each smart playlist rebuild
  84.   _updateSubsequentDelay : 500,
  85.  
  86.   // Query object for db access
  87.   _dbQuery               : null,
  88.  
  89.   // db table names
  90.   _dirtyPropertiesTable  : "smartplupd_dirty_properties",
  91.   _dirtyListsTable       : "smartplupd_dirty_lists",
  92.   _updateVideoListsTable : "smartplupd_update_video_lists",
  93.   _monitor               : null,
  94.   
  95.   // --------------------------------------------------------------------------
  96.   // setup init & shutdown
  97.   // --------------------------------------------------------------------------
  98.   observe: function(subject, topic, data) {
  99.     var obs = Cc["@mozilla.org/observer-service;1"]
  100.                 .getService(Ci.nsIObserverService);
  101.  
  102.     if (topic == "songbird-library-manager-ready") {
  103.       // To register on final-ui-startup directly will break the library
  104.       // sort data rebuilding during library migration for unknown reason.
  105.       // Workaround by registering on songbird-library-manager-ready first.
  106.       obs.removeObserver(this, "songbird-library-manager-ready");
  107.       obs.addObserver(this, "final-ui-startup", false); 
  108.     } else if (topic == "final-ui-startup") {
  109.       // Smart Playlist Update should happen after rebuild of sortable value.
  110.       // So wait for final-ui-startup to do the real update.
  111.       obs.removeObserver(this, "final-ui-startup");
  112.       obs.addObserver(this, "songbird-library-manager-before-shutdown", false);
  113.       this.initialize();
  114.     } else if (topic == "songbird-library-manager-before-shutdown") {
  115.       obs.removeObserver(this, "songbird-library-manager-before-shutdown");
  116.       this.shutdown();
  117.     }
  118.   },
  119.  
  120.   // --------------------------------------------------------------------------
  121.   // Initialization
  122.   // --------------------------------------------------------------------------
  123.   initialize: function() {
  124.     // listen for everything
  125.     this._monitor = 
  126.       new LibraryUtils.GlobalMediaListListener(this, 
  127.                                                false,
  128.                                                Ci.sbIMediaList.LISTENER_FLAGS_ITEMADDED |
  129.                                                Ci.sbIMediaList.LISTENER_FLAGS_AFTERITEMREMOVED |
  130.                                                Ci.sbIMediaList.LISTENER_FLAGS_ITEMUPDATED |
  131.                                                Ci.sbIMediaList.LISTENER_FLAGS_BATCHBEGIN |
  132.                                                Ci.sbIMediaList.LISTENER_FLAGS_BATCHEND |
  133.                                                Ci.sbIMediaList.LISTENER_FLAGS_LISTCLEARED,
  134.                                                null,
  135.                                                LibraryUtils.mainLibrary);
  136.  
  137.     // Init the dirty properties db and tables
  138.     this._dbQuery = Cc["@songbirdnest.com/Songbird/DatabaseQuery;1"]
  139.                       .createInstance(Ci.sbIDatabaseQuery);
  140.     this._dbQuery.setAsyncQuery(false);
  141.     this._dbQuery.setDatabaseGUID("songbird");
  142.  
  143.     // holds the dirty properties
  144.     this._dbQuery.resetQuery();
  145.     this._dbQuery.addQuery("CREATE TABLE IF NOT EXISTS " +
  146.                            this._dirtyPropertiesTable +
  147.                            " (propertyid TEXT UNIQUE NOT NULL)");
  148.     this._dbQuery.execute();
  149.  
  150.     // holds the dirty lists
  151.     this._dbQuery.resetQuery();
  152.     this._dbQuery.addQuery("CREATE TABLE IF NOT EXISTS " +
  153.                            this._dirtyListsTable +
  154.                            " (listguid TEXT UNIQUE NOT NULL)");
  155.     this._dbQuery.execute();
  156.  
  157.     // holds the update lists
  158.     this._dbQuery.resetQuery();
  159.     this._dbQuery.addQuery("CREATE TABLE IF NOT EXISTS " +
  160.                            this._updateVideoListsTable +
  161.                            " (listguid TEXT UNIQUE NOT NULL)");
  162.     this._dbQuery.execute();
  163.  
  164.     // Apply a function to every row value #0 in a db table
  165.     var query = this._dbQuery;
  166.     function applyOnTableValues(aTableId, aFunction) {
  167.       query.resetQuery();
  168.       query.addQuery("SELECT * FROM " + aTableId);
  169.       query.execute();
  170.       var result = query.getResultObject();
  171.       if (result && result.getRowCount() > 0) {
  172.         for (var i = 0; i < result.getRowCount(); i++) {
  173.           var value = result.getRowCell(i, 0);
  174.           aFunction(value);
  175.         }
  176.       }
  177.     }
  178.  
  179.     // remember our context
  180.     var that = this;
  181.  
  182.     // If we have lists in the dirty lists db table, we need to add them to the
  183.     // _updateQueue js table.
  184.     function addToUpdateQueue(aListGuid) {
  185.       // check that the list is still valid, just in case.
  186.       var mediaList = LibraryUtils.mainLibrary.getMediaItem(aListGuid);
  187.       if (mediaList instanceof Ci.sbIMediaList &&
  188.           mediaList.type == "smart") {
  189.         that._updateQueue[aListGuid] = mediaList;
  190.       }
  191.     }
  192.     applyOnTableValues(this._dirtyListsTable, addToUpdateQueue);
  193.  
  194.     // If we have properties in the dirty properties db table, we need to add
  195.     // them to the _updatedProperties js table.
  196.     function addToModifiedProperties(aPropertyID) {
  197.       that._updatedProperties[aPropertyID] = true;
  198.     }
  199.     applyOnTableValues(this._dirtyPropertiesTable, addToModifiedProperties);
  200.  
  201.     // If we have lists in the update video lists db table, we need to add them
  202.     // to the _updateVideoQueue js table.
  203.     var length = 0;
  204.     function addToUpdateVideoQueue(aListGuid) {
  205.       // check that the list is still valid, just in case.
  206.       var mediaList = LibraryUtils.mainLibrary.getMediaItem(aListGuid);
  207.       if (mediaList instanceof Ci.sbIMediaList &&
  208.           mediaList.type == "smart") {
  209.         that._updateVideoQueue[aListGuid] = mediaList;
  210.         ++length;
  211.       }
  212.     }
  213.     applyOnTableValues(this._updateVideoListsTable, addToUpdateVideoQueue);
  214.  
  215.     // Update the smart playlists condition to filter video items.
  216.     if (length) {
  217.       this.updateListConditions();
  218.       this.resetUpdateVideoListsTable();
  219.     }
  220.  
  221.     // Start an update if needed, after a delay. This will update any list
  222.     // in the update queue, as well as any list whose content is dependent on
  223.     // one of the dirty properties, if any. 
  224.     this.delayedUpdateCheck();
  225.   },
  226.   
  227.   // --------------------------------------------------------------------------
  228.   // Shutdown
  229.   // --------------------------------------------------------------------------
  230.   shutdown: function() {
  231.     // Clean up
  232.     this._timer = null;
  233.     this._secondaryTimer = null;
  234.     if (this._monitor) {
  235.       this._monitor.shutdown();
  236.       this._monitor = null;
  237.     }
  238.   },
  239.  
  240.   // --------------------------------------------------------------------------
  241.   // print a debug message in the console
  242.   // --------------------------------------------------------------------------
  243.   /*
  244.   _log: function(str, isError) {
  245.     if (debugLog || 
  246.         isError) {
  247.       Components.utils.reportError("smartMediaListsUpdater - " + str);
  248.     }
  249.   },
  250.   */
  251.   
  252.   // --------------------------------------------------------------------------
  253.   // Entering batch notification, increment counter
  254.   // --------------------------------------------------------------------------
  255.   onBatchBegin: function(aMediaList) {
  256.     this._batchCount++;
  257.   },
  258.   
  259.   // --------------------------------------------------------------------------
  260.   // Leaving batch notification, decrement counter.
  261.   // --------------------------------------------------------------------------
  262.   onBatchEnd: function(aMediaList) {
  263.     // If counter is zero, schedule an update check
  264.     if (--this._batchCount == 0) {
  265.       this.delayedUpdateCheck();
  266.     }
  267.   },
  268.  
  269.   // --------------------------------------------------------------------------
  270.   // Item was added, all its properties are new, so cause all smart
  271.   // playlists to eventually update  
  272.   // --------------------------------------------------------------------------
  273.   onItemAdded: function(aMediaList, aMediaItem, aIndex) {
  274.     if (!aMediaList ||
  275.         aMediaList instanceof Ci.sbILibrary) {
  276.       if (aMediaItem instanceof Ci.sbIMediaList) {
  277.         return true;
  278.       }
  279.       // new item imported in library,
  280.       // record the '*' property in the update table
  281.       this.recordUpdateProperty('*');
  282.     } else {
  283.       // record the fact that this playlist changed
  284.       this.recordUpdateProperty(aMediaList.guid);
  285.     }
  286.     // if we are in a batch, return true so we're not told about item
  287.     // additions in this batch anymore
  288.     if (this._batchCount > 0) 
  289.       return true;
  290.     // if we are not in a batch, schedule an update check
  291.     this.delayedUpdateCheck();
  292.   },
  293.  
  294.   // --------------------------------------------------------------------------
  295.   // Item was removed, all its properties are going away, so cause all
  296.   // smart playlists to eventually update
  297.   // --------------------------------------------------------------------------
  298.   onAfterItemRemoved: function(aMediaList, aMediaItem, aIndex) {
  299.     if (!aMediaList ||
  300.         aMediaList instanceof Ci.sbILibrary) {
  301.       if (aMediaItem instanceof Ci.sbIMediaList) {
  302.         return true;
  303.       }
  304.       // item removed from library,
  305.       // record the '*' property in the update table
  306.       this.recordUpdateProperty('*');
  307.     } else {
  308.       // record the fact that this playlist changed
  309.       this.recordUpdateProperty(aMediaList.guid);
  310.     }
  311.     // if we are in a batch, return true so we're not told about item
  312.     // additions in this batch anymore
  313.     if (this._batchCount > 0) 
  314.       return true;
  315.     // if we are not in a batch, schedule an update check
  316.     this.delayedUpdateCheck();
  317.   },
  318.  
  319.   // --------------------------------------------------------------------------
  320.   // Item was updated, add the modified properties to the property table
  321.   // then cause the corresponding smart playlists to eventually update
  322.   // --------------------------------------------------------------------------
  323.   onItemUpdated: function(aMediaList, aMediaItem, aProperties) {
  324.     // We don't care about property changes on lists
  325.     if (aMediaItem instanceof Ci.sbIMediaList)
  326.       return true;
  327.     
  328.     // If we are in a batch, and the "update all" flag has been 
  329.     // added to the property list, then there is no need
  330.     // to keep tracking which properties are dirty.
  331.     // This can save a huge amount of time when importing and
  332.     // scanning 10,000+ tracks.
  333.     if (this._batchCount > 0 && this._updatedProperties["*"]) {
  334.       return true;
  335.     }
  336.     
  337.     // for all properties in the array...
  338.     for (var i=0; i<aProperties.length; i++) {
  339.       var property = aProperties.getPropertyAt(i);
  340.       // record the property in the updated properties table
  341.       this.recordUpdateProperty(property.id);
  342.     }
  343.     // if we are in a batch, return false so that we keep receiving more
  344.     // notifications about property changes, since these could be about
  345.     // other properties than the ones we have been notified about in this
  346.     // call.
  347.     if (this._batchCount > 0) 
  348.       return false;
  349.     // if we are not in a batch, schedule an update check
  350.     this.delayedUpdateCheck();
  351.   },
  352.  
  353.   // --------------------------------------------------------------------------
  354.   // list was cleared, add the list to the playlist update table
  355.   // then cause the corresponding smart playlists to update
  356.   // --------------------------------------------------------------------------
  357.   onListCleared: function(list, excludeLists) {
  358.     // record the fact that this playlist changed
  359.     this.recordUpdateProperty(list.guid);
  360.     if (this._batchCount > 0) 
  361.       return false;
  362.     // if we are not in a batch, schedule an update check
  363.     this.delayedUpdateCheck();
  364.   },
  365.  
  366.   // --------------------------------------------------------------------------
  367.   // These do not get called since we don't ask for them, but still implement
  368.   // the complete interface
  369.   // --------------------------------------------------------------------------
  370.   onBeforeListCleared: function(list, excludeLists) {},
  371.   onBeforeItemRemoved: function(list, item, index) {},
  372.   onItemMoved: function(list, item, index) {},
  373.   
  374.   // --------------------------------------------------------------------------
  375.   // Add a property to the updated properties table
  376.   // --------------------------------------------------------------------------
  377.   recordUpdateProperty: function(aPropertyID) {
  378.     // if the property is not yet in the table, add it
  379.     if (!(aPropertyID in this._updatedProperties)) {
  380.       //this._log("Property change : " + aPropertyID);
  381.       this._updatedProperties[aPropertyID] = true;
  382.       // remember that this property is dirty, so that if we are shut down
  383.       // before the lists are updated, we can still resume the update on the
  384.       // next startup by calling delayedUpdateCheck, which will determine which
  385.       // lists need updating.
  386.       this.addPropertyToDirtyTable(aPropertyID);
  387.     }
  388.   },
  389.   
  390.   // --------------------------------------------------------------------------
  391.   // Update the smart playlist condition.
  392.   // --------------------------------------------------------------------------
  393.   updateListConditions: function() {
  394.     var propertyManager =
  395.       Cc["@songbirdnest.com/Songbird/Properties/PropertyManager;1"]
  396.         .getService(Ci.sbIPropertyManager);
  397.     var typePI = propertyManager.getPropertyInfo(SBProperties.contentType);
  398.  
  399.     var condition = {
  400.       property     : SBProperties.contentType,
  401.       operator     : typePI.getOperator(typePI.OPERATOR_NOTEQUALS),
  402.       leftValue    : "video",
  403.       rightValue   : null,
  404.       displayUnit  : null,
  405.     };
  406.     var defaultSmartPlaylists = [
  407.       SBString("smart.defaultlist.highestrated", "Highest Rated"),
  408.       SBString("smart.defaultlist.mostplayed", "Most Played"),
  409.       SBString("smart.defaultlist.recentlyadded", "Recently Added"),
  410.       SBString("smart.defaultlist.recentlyplayed", "Recently Played")
  411.     ];
  412.     var list;
  413.  
  414.     function objectConverter(a) {
  415.       var obj = {};
  416.       for (var i = 0; i < a.length; ++i) {
  417.         obj[a[i]] = '';
  418.       }
  419.       return obj;
  420.     }
  421.  
  422.     for (var guid in this._updateVideoQueue) {
  423.       list = this._updateVideoQueue[guid];
  424.       // Append the condition to filter video items.
  425.       if (list.name in objectConverter(defaultSmartPlaylists)) {
  426.         list.appendCondition(condition.property,
  427.                              condition.operator,
  428.                              condition.leftValue,
  429.                              condition.rightValue,
  430.                              condition.displayUnit);
  431.       }
  432.       else
  433.         continue;
  434.  
  435.       list.rebuild();
  436.     }
  437.   },
  438.  
  439.   // --------------------------------------------------------------------------
  440.   // Returns an array of all smart playlists
  441.   // --------------------------------------------------------------------------
  442.   getSmartPlaylists: function() {
  443.     var enumListener = {
  444.       items: [],
  445.       onEnumerationBegin: function(aMediaList) { },
  446.       onEnumerationEnd: function(aMediaList) { },
  447.       onEnumeratedItem: function(aMediaList, aMediaItem) {
  448.         // if this list is a smart playlist, add it to the array
  449.         if (aMediaItem.type == 'smart') {
  450.           this.items.push(aMediaItem);
  451.         }
  452.         // continue enumeration
  453.         return Ci.sbIMediaListEnumerationListener.CONTINUE;
  454.       },
  455.       QueryInterface:
  456.         XPCOMUtils.generateQI([Ci.sbIMediaListEnumerationListener])
  457.     };
  458.  
  459.     // create a property array so we can specify that we only want items with
  460.     // isList == 1, and hidden == 0
  461.     var pa = Cc["@songbirdnest.com/Songbird/Properties/MutablePropertyArray;1"]
  462.                .createInstance(Ci.sbIMutablePropertyArray);
  463.     pa.appendProperty(SBProperties.isList, "1");
  464.     pa.appendProperty(SBProperties.hidden, "0");
  465.     
  466.     // start the enumeration
  467.     LibraryUtils.mainLibrary.
  468.       enumerateItemsByProperties(pa, 
  469.                                  enumListener, 
  470.                                  Ci.sbIMediaList.ENUMERATIONTYPE_LOCKING);
  471.  
  472.     // return the array of smart playlists
  473.     return enumListener.items;
  474.   },
  475.   
  476.   // --------------------------------------------------------------------------
  477.   // schedule an update check. 
  478.   // --------------------------------------------------------------------------
  479.   delayedUpdateCheck: function() {
  480.     // if the update is already taking place, only update the update queue,
  481.     // so that the lists that need updating will be processed at the end of
  482.     // the current queue
  483.     if (this._updating) {
  484.       this._causeMoreChecks = true;
  485.       return;
  486.     }
  487.     // if timer has not been created, create it now
  488.     if (!this._timer)
  489.       this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  490.     // we may have triggered this timer already, and not enough time has passed
  491.     // for it to cause the update to begin, so cancel the timer and set it
  492.     // again. this has the effect of delaying further the updates as more
  493.     // medialistlistener notifications are received and until none has been
  494.     // received for a delay long enough for the timer notification to be issued.
  495.     // Note that we also check if 'a long time' has passed since we started
  496.     // delaying the update check, and force it if that's the case, so as to avoid
  497.     // being indefinitly blocked by something constantly updating properties.
  498.     var now = new Date().getTime();
  499.     if (!this._timerInitTime) this._timerInitTime = now;
  500.     this._timer.cancel();
  501.     if (this._batchCount == 0 && 
  502.         now - this._timerInitTime > this._maxInitialDelay) {
  503.       this.notify(this._timer);
  504.     } else {
  505.       // start the timer
  506.       this._timer.initWithCallback(this,
  507.                                    this._updateInitialDelay,
  508.                                    Ci.nsITimer.TYPE_ONE_SHOT);
  509.     }      
  510.   },
  511.   
  512.   // --------------------------------------------------------------------------
  513.   // timer notification
  514.   // --------------------------------------------------------------------------
  515.   notify: function(aTimer) {
  516.     // The delayed update timer has expired... if there are no new batches in 
  517.     // progress, check to see if we need to update the smart playlists
  518.     if (this._batchCount == 0) {
  519.       if (aTimer == this._timer) {
  520.         // reset first delayed update check time
  521.         this._timerInitTime = null;
  522.         // properties have been changed some time ago, we need to go through the
  523.         // list of smart playlists, and schedule an update for the ones whose
  524.         // content has potentially changed
  525.         this.checkForUpdates();
  526.       } else if (aTimer == this._secondaryTimer) {
  527.         // perform the update for the next smart playlist in the queue
  528.         this.performUpdates();
  529.       }
  530.     } else {
  531.       // Otherwise, defer the check for a while longer
  532.       this.delayedUpdateCheck();
  533.     }
  534.   },
  535.  
  536.   // --------------------------------------------------------------------------
  537.   // Check to see if we need to add any smart playlist in the update queue
  538.   // due to a property change that may affect the list's content.
  539.   // Note that this function may run while the updates are in progress (ie,
  540.   // a property has changed while the update was already started, and it is
  541.   // not finished yet). This is fine because all this function does is check
  542.   // the list of modified properties and add the appropriate lists to the
  543.   // update queue, so if the update is running, the newly added lists will
  544.   // run after the ones that were already scheduled.
  545.   // --------------------------------------------------------------------------
  546.   checkForUpdates: function() {
  547.     //this._log("checkForUpdates");
  548.     // if no properties have been modified, no list need to be added to
  549.     // the queue (this does not mean that the queue is empty).
  550.     if (!this.emptyOfProperties(this._updatedProperties)) {
  551.       // get all smart playlists
  552.       var lists = this.getSmartPlaylists();
  553.       // for all smart playlists...
  554.       for each (var list in lists) {
  555.         // if the list is already in the update queue, continue with the
  556.         // next one
  557.         if (list.guid in this._updateQueue)
  558.           continue;
  559.         // fetch the interface we need
  560.         list.QueryInterface(Ci.sbILocalDatabaseSmartMediaList);
  561.         // if this list is not auto updating, skip it
  562.         if (!list.autoUpdate)
  563.           continue;
  564.         // if we have a limit with a matching select property, add the list to
  565.         // the update queue and to the dirty lists table, otherwise, check
  566.         // individual conditions for a property match
  567.         if (list.limit != Ci.sbILocalDatabaseSmartMediaList.LIMIT_TYPE_NONE && 
  568.             ("*" in this._updatedProperties ||
  569.              list.selectPropertyID in this._updatedProperties)) {
  570.           this._updateQueue[list.guid] = list;
  571.           this.addListToDirtyTable(list);
  572.         } else {
  573.           // for all smart playlist conditions...
  574.           for (var c=0; c<list.conditionCount; c++) {
  575.             // get condition at index c
  576.             var condition = list.getConditionAt(c);
  577.             // if the condition property is in the table, or "*" is in the table,
  578.             // add the list to the update queue and to the dirty lists table
  579.             if ("*" in this._updatedProperties ||
  580.                 condition.propertyID in this._updatedProperties ||
  581.                 this.isPlaylistConditionMatch(condition.propertyID, condition.leftValue, this._updatedProperties)) {
  582.               this._updateQueue[list.guid] = list;
  583.               this.addListToDirtyTable(list);
  584.               // and continue on with the next list
  585.               break;
  586.             }
  587.           }
  588.         }
  589.       }
  590.       // reset the list of modified properties. If more properties are changed
  591.       // while we are in the process of updating, we only want those lists that
  592.       // match those new properties to be added to the queue (and they will only
  593.       // be added if they are either not in there already, ie, they were not
  594.       // scheduled for update before, or they have already been updated, in which
  595.       // case they need to be updated again). 
  596.       this._updatedProperties = {};
  597.       // empty the dirty properties table, since we're about to populate the
  598.       // dirty lists table (they're no longer needed, since they only serve to
  599.       // let us figure out which lists need updating on next startup in the case
  600.       // where we never got here)
  601.       this.resetDirtyPropertiesTable();
  602.     }
  603.     // if we're not updating yet, check if we need to start doing so
  604.     if (!this._updating &&
  605.         !this.emptyOfProperties(this._updateQueue)) {
  606.       // start updating, this will update the first list, and schedule the
  607.       // next one.
  608.       this.performUpdates();
  609.     }
  610.   },
  611.   
  612.   // --------------------------------------------------------------------------
  613.   // Test whether a condition uses a rule on a dirty playlist
  614.   // --------------------------------------------------------------------------
  615.   isPlaylistConditionMatch: function(prop, value, dirtyprops) {
  616.     if (prop != "http://songbirdnest.com/dummy/smartmedialists/1.0#playlist")
  617.       return false;
  618.     return (value in dirtyprops);
  619.   },
  620.   
  621.   // --------------------------------------------------------------------------
  622.   // actually performs the update for one list, then schedule the next
  623.   // --------------------------------------------------------------------------
  624.   performUpdates: function() {
  625.     // extract and remove the first list from the queue.
  626.     // is there a better way to do this with a map ?
  627.     var remaining = {};
  628.     var list;
  629.     for (var v in this._updateQueue) { 
  630.       if (!list)
  631.         list = this._updateQueue[v];
  632.       else
  633.         remaining[v] = this._updateQueue[v];
  634.     }
  635.     this._updateQueue = remaining;
  636.     
  637.     // this should really not be happening, but test anyway
  638.     if (!list) {
  639.       // print a console message since this is not supposed to happen
  640.       //this._log("list is null in sbSmartMediaListsUpdater.js", true);
  641.       // go back to idle mode
  642.       this._updating = false;
  643.       // make sure the dirty lists table is empty
  644.       this.resetDirtylistsTable();
  645.       return;
  646.     }
  647.  
  648.     //this._log("Updating list " + 
  649.     //          list.name + " (" + 
  650.     //          list.type + ", " + 
  651.     //          list.guid + ")");
  652.  
  653.     // we are now updating a whole bunch of playlists, one at a time
  654.     this._updating = true;
  655.  
  656.     // get a notification when the playlist is done rebuilding. The rebuild
  657.     // is actually synchronous for now, but going via a callback means that this
  658.     // code will not need any change if/when we use asynchronous updates instead
  659.     list.addSmartMediaListListener(this);
  660.     
  661.     // cause the rebuild
  662.     list.rebuild();
  663.   },
  664.  
  665.   // --------------------------------------------------------------------------
  666.   // Called when the smart playlist is done rebuilding.
  667.   // --------------------------------------------------------------------------
  668.   onRebuild: function(aSmartMediaList) {
  669.     // remove listener
  670.     aSmartMediaList.removeSmartMediaListListener(this);
  671.  
  672.     // Remove this list from the dirty db table, so we don't rebuild it on
  673.     // next startup, even if the app is shut down before we finish to update
  674.     // all the lists in the queue
  675.     this.removeListFromDirtyTable(aSmartMediaList);
  676.     
  677.     // if there are any more lists to update, start the timer again,
  678.     // with a shorter delay than the initial one
  679.     if (!this.emptyOfProperties(this._updateQueue)) {
  680.       if (!this._secondaryTimer)
  681.         this._secondaryTimer = 
  682.           Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  683.  
  684.       this._secondaryTimer.initWithCallback(this,
  685.                                             this._updateSubsequentDelay,
  686.                                             Ci.nsITimer.TYPE_ONE_SHOT);
  687.     } else {
  688.       // if nothing more to do, then go back to idle mode.
  689.       this._updating = false;
  690.       // the dirty lists table should be empty by now, but it doesn't hurt
  691.       // to make sure
  692.       this.resetDirtyListsTable();
  693.       // if we got more changes during the update, check them now
  694.       if (this._causeMoreChecks) {
  695.         this._causeMoreChecks = false;
  696.         this.delayedUpdateCheck();
  697.       }
  698.     }
  699.     this._currentListUpdate = null;
  700.   },
  701.  
  702.   // --------------------------------------------------------------------------
  703.   // returns true if a map is empty. is there a better way to do this ?
  704.   // --------------------------------------------------------------------------
  705.   emptyOfProperties: function(obj) {
  706.     var hasItems = false;
  707.     for each (var v in obj) { 
  708.       hasItems = true; 
  709.       break; 
  710.     }
  711.     return !hasItems;
  712.   },
  713.   
  714.   // --------------------------------------------------------------------------
  715.   // add one list to the dirty lists db table
  716.   // --------------------------------------------------------------------------
  717.   addListToDirtyTable: function(aMediaList) {
  718.     this._dbQuery.resetQuery();
  719.     this._dbQuery.addQuery("INSERT OR REPLACE INTO " + 
  720.                            this._dirtyListsTable + 
  721.                            " VALUES (\"" + 
  722.                            aMediaList.guid + 
  723.                            "\")");
  724.     this._dbQuery.execute();
  725.   },
  726.   
  727.   // --------------------------------------------------------------------------
  728.   // add one property to the dirty properties db table
  729.   // --------------------------------------------------------------------------
  730.   addPropertyToDirtyTable: function(aPropertyID) {
  731.     this._dbQuery.resetQuery();
  732.     this._dbQuery.addQuery("INSERT OR REPLACE INTO " + 
  733.                            this._dirtyPropertiesTable + 
  734.                            " VALUES (\"" + 
  735.                            aPropertyID + 
  736.                            "\")");
  737.     this._dbQuery.execute();
  738.   },
  739.   
  740.   // --------------------------------------------------------------------------
  741.   // remove one list from the dirty lists db table
  742.   // --------------------------------------------------------------------------
  743.   removeListFromDirtyTable: function(aMediaList) {
  744.     this._dbQuery.resetQuery();
  745.     this._dbQuery.addQuery("DELETE FROM " + 
  746.                            this._dirtyListsTable + 
  747.                            " WHERE listguid = \"" + 
  748.                            aMediaList.guid + 
  749.                            "\"");
  750.     this._dbQuery.execute();
  751.   },
  752.   
  753.   // --------------------------------------------------------------------------
  754.   // remove all rows from the dirty lists db table
  755.   // --------------------------------------------------------------------------
  756.   resetDirtyListsTable: function() {
  757.     this._dbQuery.resetQuery();
  758.     this._dbQuery.addQuery("DELETE FROM " + this._dirtyListsTable);
  759.     this._dbQuery.execute();
  760.   },
  761.  
  762.   // --------------------------------------------------------------------------
  763.   // remove all rows from the dirty properties db table
  764.   // --------------------------------------------------------------------------
  765.   resetDirtyPropertiesTable: function() {
  766.     this._dbQuery.resetQuery();
  767.     this._dbQuery.addQuery("DELETE FROM " + this._dirtyPropertiesTable);
  768.     this._dbQuery.execute();
  769.   },
  770.  
  771.   // --------------------------------------------------------------------------
  772.   // remove all rows from the update video lists db table
  773.   // --------------------------------------------------------------------------
  774.   resetUpdateVideoListsTable: function() {
  775.     this._dbQuery.resetQuery();
  776.     this._dbQuery.addQuery("DELETE FROM " + this._updateVideoListsTable);
  777.     this._dbQuery.execute();
  778.   },
  779.  
  780.   // --------------------------------------------------------------------------
  781.   // QueryInterface
  782.   // --------------------------------------------------------------------------
  783.   QueryInterface:
  784.     XPCOMUtils.generateQI([Ci.sbIMediaListListener,
  785.                            Ci.nsITimerCallback,
  786.                            Ci.sbILocalDatabaseSmartMediaListListener])
  787.  
  788. }; // SmartMediaListsUpdater.prototype
  789.  
  790. // ----------------------------------------------------------------------------
  791. // ----------------------------------------------------------------------------
  792.  
  793. function postRegister(aCompMgr, aFileSpec, aLocation) {
  794.   // Get instantiated on startup
  795.   XPCOMUtils.categoryManager
  796.             .addCategoryEntry('app-startup',
  797.                               'smartplaylists-updater', 
  798.                               'service,@songbirdnest.com/Songbird/SmartMediaListsUpdater;1',
  799.                               true, 
  800.                               true);
  801. }
  802.  
  803. // module
  804. var NSGetModule = 
  805.   XPCOMUtils.generateNSGetModule([SmartMediaListsUpdater], 
  806.                                  postRegister);
  807.