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

  1. /*
  2. //
  3. // BEGIN SONGBIRD GPL
  4. // 
  5. // This file is part of the Songbird web player.
  6. //
  7. // Copyright(c) 2005-2008 POTI, Inc.
  8. // http://songbirdnest.com
  9. // 
  10. // This file may be licensed under the terms of of the
  11. // GNU General Public License Version 2 (the "GPL").
  12. // 
  13. // Software distributed under the License is distributed 
  14. // on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either 
  15. // express or implied. See the GPL for the specific language 
  16. // governing rights and limitations.
  17. //
  18. // You should have received a copy of the GPL along with this 
  19. // program. If not, go to http://www.gnu.org/licenses/gpl.html
  20. // or write to the Free Software Foundation, Inc., 
  21. // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  22. // 
  23. // END SONGBIRD GPL
  24. //
  25.  */
  26. const Cc = Components.classes;
  27. const Ci = Components.interfaces;
  28. const Cr = Components.results;
  29. const Ce = Components.Exception;
  30. const Cu = Components.utils;
  31.  
  32. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  33. Cu.import("resource://app/jsmodules/SBJobUtils.jsm");
  34. Cu.import("resource://app/jsmodules/ArrayConverter.jsm");
  35. Cu.import("resource://app/jsmodules/sbProperties.jsm");
  36. Cu.import("resource://app/jsmodules/sbLibraryUtils.jsm");
  37. Cu.import("resource://app/jsmodules/StringUtils.jsm");
  38. Cu.import("resource://app/jsmodules/DebugUtils.jsm");
  39.  
  40. /**
  41.  * To log this module, set the following environment variable:
  42.  *   NSPR_LOG_MODULES=sbDirectoryImportJob:5
  43.  */
  44. const LOG = DebugUtils.generateLogFunction("sbDirectoryImportJob");
  45.  
  46. // used to identify directory import profiling runs
  47. var gCounter = 0;
  48.  
  49. /******************************************************************************
  50.  * Object implementing sbIDirectoryImportJob, responsible for finding media
  51.  * items on disk, adding them to the library and performing a metadata scan.
  52.  *
  53.  * Call begin() to start the job.
  54.  *****************************************************************************/
  55. function DirectoryImportJob(aInputArray, 
  56.                             aTypeSniffer,
  57.                             aMetadataScanner,
  58.                             aTargetMediaList, 
  59.                             aTargetIndex, 
  60.                             aImportService) {
  61.   if (!(aInputArray instanceof Ci.nsIArray && 
  62.         aInputArray.length > 0) ||
  63.       !(aTargetMediaList instanceof Ci.sbIMediaList)) {
  64.     throw Components.results.NS_ERROR_INVALID_ARG;
  65.   }
  66.   // Call super constructor
  67.   SBJobUtils.JobBase.call(this);
  68.  
  69.   this._inputFiles = ArrayConverter.JSArray(aInputArray);
  70.   this.targetMediaList = aTargetMediaList;
  71.   this.targetIndex = aTargetIndex;
  72.  
  73.   this._importService = aImportService;
  74.   this._typeSniffer = aTypeSniffer;
  75.   this._metadataScanner = aMetadataScanner;
  76.     
  77.   // TODO these strings probably need updating
  78.   this._titleText = SBString("media_scan.scanning");
  79.   this._statusText =  SBString("media_scan.adding");
  80.   
  81.   // Initially cancelable
  82.   this._canCancel = true;
  83.     
  84.   // initialize with an empty array
  85.   this._itemURIStrings = [];
  86.  
  87.   this._libraryUtils = Cc["@songbirdnest.com/Songbird/library/Manager;1"]
  88.                          .getService(Ci.sbILibraryUtils);
  89.  
  90.   if ("@songbirdnest.com/Songbird/TimingService;1" in Cc) {
  91.     this._timingService = Cc["@songbirdnest.com/Songbird/TimingService;1"]
  92.                             .getService(Ci.sbITimingService);
  93.     this._timingIdentifier = "DirImport" + gCounter++;
  94.   }
  95. }
  96. DirectoryImportJob.prototype = {
  97.   __proto__: SBJobUtils.JobBase.prototype,
  98.   
  99.   QueryInterface: XPCOMUtils.generateQI(
  100.     [Ci.sbIDirectoryImportJob, Ci.sbIJobProgress, Ci.sbIJobProgressUI,
  101.      Ci.sbIJobProgressListener, Ci.sbIJobCancelable, Ci.nsIObserver,
  102.      Ci.nsIClassInfo]),
  103.   
  104.   /** For nsIClassInfo **/
  105.   getInterfaces: function(count) {
  106.     var interfaces = [Ci.sbIDirectoryImportJob, Ci.sbIJobProgress, Ci.sbIJobProgressListener,
  107.                       Ci.sbIJobCancelable, Ci.nsIClassInfo, Ci.nsISupports];
  108.     count.value = interfaces.length;
  109.     return interfaces;
  110.   },
  111.  
  112.   totalAddedToMediaList     : 0,
  113.   totalAddedToLibrary       : 0,
  114.   totalDuplicates           : 0,  
  115.   targetMediaList           : null,
  116.   targetIndex               : null,
  117.   
  118.   // nsITimer used to poll the filescanner.  *sigh*
  119.   _pollingTimer             : null,
  120.   FILESCAN_POLL_INTERVAL    : 33,
  121.   
  122.   // sbIFileScan used to find media files in directories
  123.   _fileScanner              : null,
  124.   _fileScanQuery            : null,
  125.   
  126.   // Array of nsIFile directory paths or files
  127.   _inputFiles               : null,
  128.   _fileExtensions           : null,
  129.   _flaggedFileExtensions    : null,
  130.   _foundFlaggedExtensions   : null,
  131.   
  132.   // The sbIDirectoryImportService.  Called back on job completion.
  133.   _importService            : null,
  134.  
  135.   _typeSniffer              : null,
  136.  
  137.   _metadataScanner   : null,
  138.   
  139.   // JS Array of URI strings for all found media items
  140.   _itemURIStrings           : [],
  141.   
  142.   // Optional JS array of items found to already exist in the main library
  143.   _itemsInMainLib           : [],
  144.   // Rather than create all the items in one pass, then scan them all,
  145.   // we want to create, read, repeat with small batches.  This avoids
  146.   // wasting/fragmenting memory when importing 10,000+ tracks.
  147.   // TODO tweak
  148.   BATCHCREATE_SIZE          : 300,
  149.   
  150.   // Index into _itemURIStrings for the beginning of the
  151.   // next create/read/add batch
  152.   _nextURIIndex             : 0,
  153.   
  154.   // Temporary nsIArray of previously unknown sbIMediaItems, 
  155.   // used to pass newly created items to a metadata scan job. 
  156.   // Set in _onItemCreation.
  157.   _currentMediaItems        : null,
  158.   // The size of the current batch
  159.   _currentBatchSize         : 0,
  160.     
  161.   // True if we've forced the library into a batch state for
  162.   // performance reasons
  163.   _inLibraryBatch           : false,
  164.  
  165.   // sbILibraryUtils used to produce content URI's
  166.   _libraryUtils             : null,
  167.   
  168.   // Used to track performance
  169.   _timingService            : null,
  170.   _timingIdentifier         : null,
  171.   
  172.   /**
  173.    * Get an enumerator over all media items found in this job.  
  174.    * Will return an empty enumerator until the batch creation 
  175.    * phase is complete.  Best called when the entire job is done.
  176.    */
  177.   enumerateAllItems: function DirectoryImportJob_enumerateAllItems() {
  178.     // Ultimately the batch create job would give us this list, 
  179.     // but at the moment we have to recreate it ourselves
  180.     // using the list of URIs from the file scan.
  181.     
  182.     // If no URIs, just return the empty enumerator
  183.     if (this._itemURIStrings.length == 0) {
  184.       return ArrayConverter.enumerator([]);
  185.     }
  186.     
  187.     // If all the URIs resulted in new items, and 
  188.     // were processed in a single batch, then we can
  189.     // just return the current items.
  190.     if (this._currentMediaItems && 
  191.         this._itemURIStrings.length == this._currentMediaItems.length) {
  192.       return this._currentMediaItems.enumerate();
  193.     }
  194.     
  195.     
  196.     // Otherwise, we'll need to get media items for all the
  197.     // URIs that have been added.  We want to avoid instantiating 
  198.     // all the items at once (since there may be hundreds of thousands),
  199.     // so instead get a few at a time. 
  200.  
  201.     var uriEnumerator = ArrayConverter.enumerator(this._itemURIStrings);
  202.     var library = this.targetMediaList.library;
  203.     const BATCHSIZE = this.BATCHCREATE_SIZE;
  204.  
  205.     // This enumerator instantiates the new media items on demand.
  206.     var enumerator = {
  207.       mediaItems: [],
  208.       
  209.       // sbIMediaListEnumerationListener, used to fetch items
  210.       onEnumerationBegin: function() {},
  211.       onEnumeratedItem: function(list, item) {
  212.         this.mediaItems.push(item);
  213.       },
  214.       onEnumerationEnd: function() {},
  215.       
  216.       // nsISimpleEnumerator, used to dispense items
  217.       hasMoreElements: function() { 
  218.         return (this.mediaItems.length || 
  219.                 uriEnumerator.hasMoreElements());
  220.       },
  221.       getNext: function() {
  222.         // When the buffer runs out, fetch more items from the library
  223.         if (this.mediaItems.length == 0) {
  224.           // Get the next set of URIs
  225.           var propertyArray = SBProperties.createArray();
  226.           var counter = 0;
  227.           while (uriEnumerator.hasMoreElements() && counter < BATCHSIZE) {
  228.             var itemURIStr = uriEnumerator.getNext().QueryInterface(Ci.nsISupportsString);
  229.             propertyArray.appendProperty(SBProperties.contentURL, itemURIStr.data);
  230.             counter++;
  231.           } 
  232.           library.enumerateItemsByProperties(propertyArray, this);
  233.         }
  234.         
  235.         return this.mediaItems.shift();
  236.       },
  237.       
  238.       QueryInterface: XPCOMUtils.generateQI([
  239.           Ci.nsISimpleEnumerator, Ci.sbIMediaListEnumerationListener]),
  240.     };
  241.     
  242.     return enumerator;
  243.   },
  244.     
  245.   
  246.   /**
  247.    * Begin the import job
  248.    */
  249.   begin: function DirectoryImportJob_begin() {
  250.     if (this._timingService) {
  251.       this._timingService.startPerfTimer(this._timingIdentifier); 
  252.     }
  253.     
  254.     // Start by finding all the files in the given directories
  255.     
  256.     var Application = Cc["@mozilla.org/fuel/application;1"]
  257.                         .getService(Ci.fuelIApplication);
  258.     
  259.     this._fileScanner = Cc["@songbirdnest.com/Songbird/FileScan;1"]
  260.                           .createInstance(Components.interfaces.sbIFileScan);
  261.  
  262.     // Figure out what files we are looking for.
  263.     try {
  264.       var extensions = this._typeSniffer.mediaFileExtensions;
  265.       if (!Application.prefs.getValue("songbird.mediascan.enableVideoImporting", true)) {
  266.         // disable video, so scan only audio - see bug 13173
  267.         extensions = this._typeSniffer.audioFileExtensions;
  268.       }
  269.       this._fileExtensions = [];
  270.       while (extensions.hasMore()) {
  271.         this._fileExtensions.push(extensions.getNext());
  272.       }
  273.     } catch (e) {
  274.       dump("WARNING: DirectoryImportJob_begin could not find supported file extensions.  " +
  275.            "Assuming test mode, and using a hardcoded list.\n");
  276.       this._fileExtensions = ["mp3", "ogg", "flac"];
  277.     }
  278.  
  279.     // Add the unsupported file extensions as flagged extensions to the file
  280.     // scanner so that the user can be notified if any unsupported extensions
  281.     // were discovered.
  282.     //
  283.     // NOTE: if the user choose to ignore import warnings, do not bother adding
  284.     //       the filter list here.
  285.     var shouldWarnFlagExtensions = Application.prefs.getValue(
  286.       "songbird.mediaimport.warn_filtered_exts", true);
  287.     if (shouldWarnFlagExtensions) {
  288.       this._foundFlaggedExtensions =
  289.         Cc["@songbirdnest.com/moz/xpcom/threadsafe-array;1"]
  290.           .createInstance(Ci.nsIMutableArray);
  291.       this._flaggedFileExtensions = [];
  292.       try {
  293.         var unsupportedExtensions = this._typeSniffer.unsupportedVideoFileExtensions;
  294.         while (unsupportedExtensions.hasMore()) {
  295.           var item = unsupportedExtensions.getNext();
  296.           this._flaggedFileExtensions.push(item);
  297.         }
  298.       }
  299.       catch (e) {
  300.         Components.utils.reportError(
  301.             e + "\nCould not add unsupported file extensions to the file scan!");
  302.       }
  303.     }
  304.  
  305.     // XXX If possible, wrap the entire operation in an update batch
  306.     // so that onbatchend listeners dont go to work between the 
  307.     // end of the batchcreate and the start of the metadata scan.
  308.     // This is an ugly hack, but it prevents a few second hang
  309.     // when importing 100k tracks.
  310.     var library = this.targetMediaList.library;
  311.     if (library instanceof Ci.sbILocalDatabaseLibrary) {
  312.       this._inLibraryBatch = true;
  313.       library.forceBeginUpdateBatch();
  314.     }
  315.     
  316.     this._startNextDirectoryScan();
  317.     
  318.     // Now poll the file scan, since it apparently perf is much better this way
  319.     this._pollingTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  320.     this._pollingTimer.init(this, this.FILESCAN_POLL_INTERVAL, Ci.nsITimer.TYPE_REPEATING_SLACK);
  321.   },
  322.   
  323.   /**
  324.    * Kick off a FileScanQuery for the next directory in _inputFiles.
  325.    * Assumes there are directories left to scan.
  326.    */
  327.   _startNextDirectoryScan: function DirectoryImportJob__startNextDirectoryScan() {
  328.     // Just report the error rather then throwing, since this method
  329.     // is called by the timer.
  330.     if (this._fileScanQuery && this._fileScanQuery.isScanning()) {
  331.       Cu.reportError(
  332.         "DirectoryImportJob__startDirectoryScan called with a scan in progress?");
  333.       return;
  334.     }
  335.     
  336.     var file = this._inputFiles.shift(); // Process directories in the order provided
  337.     
  338.     if (file && file instanceof Ci.nsIFileURL) {
  339.       file = file.file;
  340.     }
  341.     
  342.     // If something is messed up, just report and wait. The poll function will
  343.     // move on to the next step.
  344.     if (!file || !(file instanceof Ci.nsIFile)) {
  345.       Cu.reportError("DirectoryImportJob__startNextDirectoryScan: invalid directory");
  346.       return;
  347.     }
  348.     
  349.     // We use the same _fileScanQuery object for scanning multiple directories.
  350.     if (!this._fileScanQuery) {
  351.       this._fileScanQuery = Cc["@songbirdnest.com/Songbird/FileScanQuery;1"]
  352.                               .createInstance(Components.interfaces.sbIFileScanQuery);
  353.       var fileScanQuery = this._fileScanQuery;
  354.       this._fileExtensions.forEach(function(ext) { fileScanQuery.addFileExtension(ext) });
  355.  
  356.       // Assign the unsupported file extensions as flagged extensions in the scanner.
  357.       if (this._flaggedFileExtensions) {
  358.         this._flaggedFileExtensions.forEach(function(ext) {
  359.           fileScanQuery.addFlaggedFileExtension(ext);
  360.         });
  361.       }
  362.     }
  363.     
  364.     if (file.exists() && file.isDirectory()) {
  365.       this._fileScanQuery.setDirectory(file.path);
  366.       this._fileScanQuery.setRecurse(true);
  367.       this._fileScanner.submitQuery(this._fileScanQuery);
  368.     } else {
  369.       var urispec = this._libraryUtils.getFileContentURI(file).spec;
  370.       
  371.       // Ensure that the file extension is not blacklisted.
  372.       var isValidExt = false;
  373.       var dotIndex = urispec.lastIndexOf(".");
  374.       if (dotIndex > -1) {
  375.         var fileExt = urispec.substr(dotIndex + 1);
  376.         for (var i = 0; i < this._fileExtensions.length; i++) {
  377.           if (this._fileExtensions[i] == fileExt) {
  378.             isValidExt = true;
  379.             break;
  380.           } 
  381.         }
  382.       }
  383.  
  384.       if (isValidExt) {
  385.         var supportsString = Cc["@mozilla.org/supports-string;1"]
  386.           .createInstance(Ci.nsISupportsString);
  387.         supportsString.data = urispec;
  388.         this._itemURIStrings.push(supportsString);
  389.       }    
  390.     }
  391.   },
  392.   
  393.   /** 
  394.    * Called by _pollingTimer. Checks on the current fileScanQuery,
  395.    * reporting progress and checking for completion.
  396.    */
  397.   _onPollFileScan: function DirectoryImportJob__onPollFileScan() {
  398.     
  399.     // If the current query is finished, collect the results and move on to
  400.     // the next directory
  401.     if (!this._fileScanQuery.isScanning()) {
  402.       // If there are more directories to scan, start the next one
  403.       if (this._inputFiles.length > 0) {
  404.         this._startNextDirectoryScan();
  405.       } else {
  406.         // Otherwise, we're done scanning directories, and it is time to collect
  407.         // information and create media items
  408.         var fileCount = this._fileScanQuery.getFileCount();
  409.         if (fileCount > 0 ) {
  410.           var strings = this._fileScanQuery.getResultRangeAsURIStrings(0, fileCount - 1);
  411.           Array.prototype.push.apply(this._itemURIStrings, ArrayConverter.JSArray(strings));
  412.         }
  413.         this._finishFileScan();
  414.         this._startMediaItemCreation();
  415.       }
  416.       // If the file scan query is still running just update the UI
  417.     } else {
  418.       var text = this._fileScanQuery.getLastFileFound();
  419.       text = decodeURIComponent(text.split("/").pop());
  420.       if (text.length > 60) {
  421.         text = text.substring(0, 10) + "..." + text.substring(text.length - 40);
  422.       }
  423.       this._statusText = text;
  424.       this.notifyJobProgressListeners();
  425.     }
  426.   },
  427.   
  428.   /** 
  429.    * Called when the file scanner finishes with the target directories.
  430.    * Responsible for shutting down the scanner.
  431.    */
  432.   _finishFileScan: function DirectoryImportJob__finishFileScan() {
  433.     if (this._fileScanQuery.flaggedExtensionsFound) {
  434.       var ioService = Cc["@mozilla.org/network/io-service;1"]
  435.                         .getService(Ci.nsIIOService);
  436.       // Add the leaf name of any flagged file paths to show the user at the
  437.       // end of the import. See bug 19553.
  438.       var flaggedExtCount = this._fileScanQuery.getFlaggedFileCount();
  439.       for (var i = 0; i < flaggedExtCount; i++) {
  440.         var curFlaggedPath = this._fileScanQuery.getFlaggedFilePath(i);
  441.         var curFlaggedURL = ioService.newURI(curFlaggedPath, null, null);
  442.         if (curFlaggedURL instanceof Ci.nsIFileURL) {
  443.           var curFlaggedFile = curFlaggedURL.QueryInterface(Ci.nsIFileURL).file;
  444.           var curSupportsStr = Cc["@mozilla.org/supports-string;1"]
  445.                                  .createInstance(Ci.nsISupportsString);
  446.           curSupportsStr.data = curFlaggedFile.leafName;
  447.  
  448.           this._foundFlaggedExtensions.appendElement(curSupportsStr, false);
  449.         }
  450.       }
  451.     }
  452.  
  453.     if (this._fileScanner) {
  454.       this._fileScanner.finalize();
  455.       this._fileScanner = null;
  456.     }
  457.     if (this._pollingTimer) {
  458.       this._pollingTimer.cancel();
  459.       this._pollingTimer = null;
  460.     }
  461.     // Set total number of items to process
  462.     this._total = this._itemURIStrings.length;
  463.   },
  464.  
  465.   /**
  466.    * This filters out items that are either duplicates or already exits in the
  467.    * main library. The existing items in the main library are put in
  468.    * aItemsInMainLib array. The remaining items are returned as an array.
  469.    * The duplicate check only involves the origin URL as the 
  470.    * batchCreateMediaItemsAsync will perform that check.
  471.    * 
  472.    * \param aURIs the list of URI's to filter
  473.    * \param aItemsInMainLib
  474.    */ 
  475.   _filterItems: function(aURIs, aItemsInMainLib) {
  476.     // If we're copying to another library we need to look in the main library
  477.     // for items that might have the origin URL the same as our URL's.
  478.     var targetLib = this.targetMediaList.library;
  479.     var mainLib = LibraryUtils.mainLibrary;
  480.     var isMainLib = this.targetMediaList.library.equals(mainLib);
  481.  
  482.     /**
  483.      * This function is used by the JS Array filter method to filter
  484.      * out duplicates. When a matching item is found the main library
  485.      * the item is added to the aItemsInMainLib array and removed
  486.      * from the array filterDupes is operating on. When the item
  487.      * is found in the target library it is just removed from the array
  488.      * and the dupe count incremented
  489.      */
  490.     function filterDupes(uri)
  491.     {
  492.       var uriObj = uri.QueryInterface(Ci.nsISupportsString);
  493.       var uriSpec = uriObj.data;
  494.  
  495.       LOG("Searching for URI: " + uriSpec);
  496.  
  497.       // If we're importing to a non-main library the see if it exists in
  498.       // the main library
  499.       if (!isMainLib) {
  500.         var items = [];
  501.         try {
  502.           // If we find it in the main library then save the item off and
  503.           // filter it out of the array
  504.           items = ArrayConverter.JSArray(
  505.                              mainLib.getItemsByProperty(SBProperties.contentURL,
  506.                              uriSpec));
  507.         }
  508.         catch (e) {
  509.           LOG("Exception: " + e);
  510.           // Exception expected if nothing is found. Just continue
  511.         }
  512.         if (items.length > 0) {
  513.           LOG("  Found in main library");
  514.           aItemsInMainLib.push(items[0].QueryInterface(Ci.sbIMediaItem));
  515.           return false;
  516.         }
  517.  
  518.       }
  519.       var items = [];
  520.       try
  521.       {
  522.         // If we find the item by origin URL in the target library then
  523.         // bump the dupe count and filter it out of the array
  524.         items = ArrayConverter.JSArray(
  525.                             targetLib.getItemsByProperty(SBProperties.originURL,
  526.                             uriSpec));
  527.       }
  528.       catch (e) {
  529.         LOG("Exception: " + e);
  530.       }
  531.       if (items.length > 0) {
  532.         LOG("  Found in target library");
  533.         ++this.totalDuplicates;
  534.         return false;
  535.       }
  536.       LOG("  Item needs to be created");
  537.       // Item wasn't found so leave it in the array
  538.       return true;
  539.     }
  540.  
  541.     return aURIs.filter(filterDupes);
  542.   },
  543.   /** 
  544.    * Begin creating sbIMediaItems for a batch of found media URIs.
  545.    * Does BATCHCREATE_SIZE at a time, looping back after 
  546.    * onJobDelegateCompleted.
  547.    */
  548.   _startMediaItemCreation: 
  549.   function DirectoryImportJob__startMediaItemCreation() {
  550.     if (!this._fileScanQuery) {
  551.       Cu.reportError(
  552.         "DirectoryImportJob__startMediaItemCreation called with invalid state");
  553.       this.complete();
  554.       return;
  555.     }
  556.     
  557.     var targetLib = this.targetMediaList.library;
  558.  
  559.     if (this._nextURIIndex >= this._itemURIStrings.length && 
  560.         this._itemsInMainLib.length === 0) {
  561.       LOG("Finish creating and adding all items");
  562.       this.complete();
  563.       return;
  564.     }
  565.     // For the items we found in the main library add them to the target library
  566.     else if (this._itemsInMainLib.length) {
  567.       LOG("Finish creating all items, now adding items");
  568.       // Setup listener object so we can mark the items as 
  569.       var self = this;
  570.       var addSomeListener = {
  571.         onProgress: function(aItemsProcessed, aCompleted) {},
  572.         onItemAdded: function(aMediaItem) {
  573.           aMediaItem.setProperty(SBProperties.originIsInMainLibrary, "1");
  574.         },
  575.         onComplete: function() {
  576.           LOG("Adding items completed");
  577.           self._itemsInMainLib = [];
  578.           self.complete();
  579.         }
  580.       }
  581.       // Now process items we found in the main library
  582.       targetLib.addMediaItems(ArrayConverter.enumerator(this._itemsInMainLib),
  583.                               addSomeListener,
  584.                               true);
  585.       return;
  586.     }
  587.     // Update status
  588.     this._statusText = SBString("media_scan.adding");
  589.     this.notifyJobProgressListeners();
  590.     
  591.     // Process the URIs a slice at a time, since creating all 
  592.     // of them at once may require a very large amount of memory.
  593.     this._currentBatchSize = Math.min(this.BATCHCREATE_SIZE,
  594.                                       this._itemURIStrings.length - this._nextURIIndex);
  595.     var endIndex = this._nextURIIndex + this._currentBatchSize;
  596.     var uris = this._itemURIStrings.slice(this._nextURIIndex, endIndex);
  597.     this._nextURIIndex = endIndex;
  598.  
  599.     LOG("Creating media items");
  600.  
  601.     this._itemsInMainLib = [];
  602.     LOG("Total items=" + uris.length);
  603.     uris = this._filterItems(uris, this._itemsInMainLib);
  604.  
  605.     LOG("Items in main library=" + this._itemsInMainLib.length);
  606.     LOG("Items needing to be created=" + uris.length);
  607.     // Bug 10228 - this needs to be replaced with an sbIJobProgress interface
  608.     var thisJob = this;
  609.     var batchCreateListener = {
  610.       onProgress: function(aIndex) {},
  611.       onComplete: function(aMediaItems, aResult) {
  612.         LOG("Finished creating batch of items");
  613.         thisJob._onItemCreation(aMediaItems, aResult, uris.length);
  614.       }
  615.     };
  616.  
  617.     // Create items that weren't in the main library or already in the target
  618.     // library
  619.     targetLib.batchCreateMediaItemsAsync(batchCreateListener, 
  620.                                          ArrayConverter.nsIArray(uris), 
  621.                                          null, 
  622.                                          false);
  623.   },
  624.   
  625.   /** 
  626.    * Called by sbILibrary.batchCreateMediaItemsAsync. 
  627.    * BatchCreateMediaItemsAsync needs to be updated to actually send progress.
  628.    * At the moment it only notifies when the process is over.
  629.    */
  630.   _onItemCreation: function DirectoryImportJob__onItemCreation(aMediaItems, 
  631.                                                                aResult, 
  632.                                                                itemsToCreateCount) {
  633.     // Get the completed item array.  Don't use the given item array on error.
  634.     // Use an empty one instead.
  635.     LOG("batchCreateMediaItemsAsync created " + aMediaItems.length)
  636.     if (Components.isSuccessCode(aResult)) {
  637.       this.totalDuplicates += (itemsToCreateCount - aMediaItems.length);
  638.       this._currentMediaItems = aMediaItems;
  639.     } else {
  640.       Cu.reportError("DirectoryImportJob__onItemCreation: aResult == " + aResult);
  641.       this._currentMediaItems = Components.classes["@songbirdnest.com/moz/xpcom/threadsafe-array;1"]
  642.                                       .createInstance(Components.interfaces.nsIArray);
  643.     }
  644.     
  645.     this.totalAddedToLibrary += this._currentMediaItems.length;
  646.     if (this._currentMediaItems.length > 0) {
  647.       
  648.       // Make sure we have metadata for all the added items
  649.       this._startMetadataScan();
  650.     } else {
  651.       // no items were created, probably because they were all old.
  652.       // try the next batch.
  653.       this._startMediaItemCreation();
  654.     }
  655.   },
  656.   
  657.   /** 
  658.    * Begin finding the metadata for the current set of media items.
  659.    * Forward sbIJobProgress through to the metadata job
  660.    */
  661.   _startMetadataScan: 
  662.   function DirectoryImportJob__startMetadataScan() {
  663.     if (this._currentMediaItems && this._currentMediaItems.length > 0) {
  664.       
  665.       var metadataJob = this._metadataScanner.read(this._currentMediaItems);
  666.         
  667.       // Pump metadata job progress to the UI
  668.       this.delegateJobProgress(metadataJob);
  669.     } else {
  670.       // Nothing to do. 
  671.       this.complete();
  672.     }
  673.   },
  674.  
  675.   /** 
  676.    * Insert found media items into the specified target location
  677.    */
  678.   _insertFoundItemsIntoTarget: 
  679.   function DirectoryImportJob__insertFoundItemsIntoTarget() {
  680.     // If we are inserting into a list, there is more to do than just importing 
  681.     // the tracks into its library, we also need to insert all the items (even
  682.     // the ones that previously existed) into the list at the requested position
  683.     if (!(this.targetMediaList instanceof Ci.sbILibrary)) {
  684.       try {
  685.         if (this._itemURIStrings.length > 0) {
  686.           var originalLength = this.targetMediaList.length;
  687.           // If we need to insert, then do so
  688.           if ((this.targetMediaList  instanceof Ci.sbIOrderableMediaList) && 
  689.               (this.targetIndex >= 0) && 
  690.               (this.targetIndex < this.targetMediaList.length)) {
  691.             this.targetMediaList.insertSomeBefore(this.targetIndex, this.enumerateAllItems());
  692.           } else {
  693.             // Otherwise, just add
  694.             this.targetMediaList.addSome(this.enumerateAllItems());
  695.           }
  696.           this.totalAddedToMediaList = this.targetMediaList.length - originalLength;
  697.         }
  698.       } catch (e) {
  699.         Cu.reportError(e); 
  700.       }
  701.     }
  702.   },
  703.  
  704.   /**
  705.    * Called when a sub-job (set via Job.delegateJobProgress) completes.
  706.    */
  707.   onJobDelegateCompleted: function DirectoryImportJob_onJobDelegateCompleted() {
  708.  
  709.     // Track overall progress, so that the progress bar reflects
  710.     // the total number of items to process, not the individual 
  711.     // batches.
  712.     this._progress += this._jobProgressDelegate.progress;
  713.     
  714.     // Stop delegating
  715.     this.delegateJobProgress(null);
  716.     
  717.     // For now the only job we delegate to is metadata... so when 
  718.     // it completes we go back to create the next set of media items.
  719.     this._startMediaItemCreation();
  720.   },
  721.  
  722.   /**
  723.    * Override JOBBase.progress, to make sure the metadata scan progress
  724.    * bar reflects the total item count, not the current batch
  725.    */
  726.   get progress() {
  727.     return (this._jobProgressDelegate) ? 
  728.       this._jobProgressDelegate.progress + this._progress : this._progress;
  729.   },
  730.   
  731.   get total() {
  732.     return this._total;
  733.   },
  734.   
  735.   /**
  736.    * Override JOBBase.titleText, since we want to maintain
  737.    * the same title throughout the import process
  738.    */
  739.   get titleText() {
  740.     return this._titleText;
  741.   },
  742.  
  743.   /** sbIJobCancelable **/
  744.   cancel: function DirectoryImportJob_cancel() {
  745.     if (!this.canCancel) {
  746.       throw new Error("DirectoryImportJob not currently cancelable")
  747.     }
  748.     
  749.     if (this._fileScanner) {
  750.       this._finishFileScan();
  751.       this.complete();
  752.     } else if (this._jobProgressDelegate) {
  753.       // Cancelling the sub-job will trigger onJobDelegateCompleted
  754.       this._jobProgressDelegate.cancel();
  755.     }
  756.     
  757.     if (this._fileScanQuery) {
  758.       this._fileScanQuery.cancel();
  759.       this._fileScanQuery = null;   
  760.     }
  761.     
  762.     // Remove anything that we've only partially processed
  763.     if (this._currentMediaItems && this._currentMediaItems.length > 0) {
  764.       this.targetMediaList.library.removeSome(this._currentMediaItems.enumerate());
  765.       this.totalAddedToMediaList = 0;
  766.       this.totalAddedToLibrary = 0;
  767.       this.totalDuplicates = 0;
  768.     }
  769.   },
  770.   
  771.   /**
  772.    * Stop everything, complete the job.
  773.    */
  774.   complete: function DirectoryImportJob_complete() {
  775.     
  776.     // Handle inserting into a media list at a specific index
  777.     this._insertFoundItemsIntoTarget();
  778.     
  779.     this._status = Ci.sbIJobProgress.STATUS_SUCCEEDED;
  780.     this._statusText = SBString("media_scan.complete");
  781.     this.notifyJobProgressListeners();
  782.     
  783.     this._importService.onJobComplete();
  784.     
  785.     // XXX If we forced the library into a batch mode
  786.     // in order to improve performance, make sure 
  787.     // we end the batch (if we fail to do this
  788.     // the tree view will never update)
  789.     var library = this.targetMediaList.library;
  790.     if (library instanceof Ci.sbILocalDatabaseLibrary &&
  791.         this._inLibraryBatch) 
  792.     {
  793.       this._inLibraryBatch = false;
  794.       library.forceEndUpdateBatch();
  795.       
  796.       // XXX Performance hack
  797.       // If we've imported a ton of items then most of the 
  798.       // library database is probably in memory.  
  799.       // This isn't useful, and makes a bad first impression,
  800.       // so lets just dump the entire DB cache.
  801.       if (this.totalAddedToLibrary > this.BATCHCREATE_SIZE) {
  802.         // More performance hackery. We run optimize with the ANALYZE step.
  803.         // This will ensure that all queries can use the best possible indexes.
  804.         library.optimize(true);
  805.  
  806.         // Analyze will load a bunch of stuff into memory so we want to release
  807.         // after analyze completes.            
  808.         var dbEngine = Cc["@songbirdnest.com/Songbird/DatabaseEngine;1"]
  809.                                      .getService(Ci.sbIDatabaseEngine);
  810.         dbEngine.releaseMemory();
  811.       }
  812.     }
  813.     
  814.     if (this._fileScanQuery) {
  815.       this._fileScanQuery.cancel();
  816.       this._fileScanQuery = null;   
  817.     }
  818.     
  819.     if (this._timingService) {
  820.       this._timingService.stopPerfTimer(this._timingIdentifier);
  821.     }
  822.  
  823.     // If flagged extensions were found, show the dialog.
  824.     if (this._foundFlaggedExtensions &&
  825.         this._foundFlaggedExtensions.length > 0)
  826.     {
  827.       var winMed = Cc["@mozilla.org/appshell/window-mediator;1"]
  828.                      .getService(Ci.nsIWindowMediator);
  829.       var sbWin = winMed.getMostRecentWindow("Songbird:Main");
  830.  
  831.       var prompter = Cc["@songbirdnest.com/Songbird/Prompter;1"]
  832.         .getService(Ci.sbIPrompter);
  833.       prompter.waitForWindow = false;
  834.  
  835.       var dialogBlock = Cc["@mozilla.org/embedcomp/dialogparam;1"]
  836.                           .createInstance(Ci.nsIDialogParamBlock);
  837.  
  838.       // Assign the flagged files 
  839.       dialogBlock.objects = this._foundFlaggedExtensions;
  840.       
  841.       // Now open the dialog.
  842.       prompter.openDialog(sbWin,
  843.           "chrome://songbird/content/xul/mediaimportWarningDialog.xul",
  844.           "mediaimportWarningDialog",
  845.           "chrome,centerscreen,modal=yes",
  846.           dialogBlock);
  847.     }
  848.   },
  849.   
  850.   /**
  851.    * nsIObserver, for nsITimer
  852.    */
  853.   observe: function DirectoryImportJob_observe(aSubject, aTopic, aData) {
  854.     this._onPollFileScan();
  855.   },
  856.   
  857.   /**
  858.    * sbIJobProgressUI
  859.    */
  860.   crop: "center"
  861. }
  862.  
  863.  
  864.  
  865.  
  866.  
  867.  
  868. /******************************************************************************
  869.  * Object implementing sbIDirectoryImportService. Used to start a 
  870.  * new media import job.
  871.  *****************************************************************************/
  872. function DirectoryImportService() {  
  873.   this._importJobs = [];
  874. }
  875.  
  876. DirectoryImportService.prototype = {
  877.   classDescription: "Songbird Directory Import Service",
  878.   classID:          Components.ID("{6e542f90-44a0-11dd-ae16-0800200c9a66}"),
  879.   contractID:       "@songbirdnest.com/Songbird/DirectoryImportService;1",
  880.   QueryInterface:   XPCOMUtils.generateQI([Ci.sbIDirectoryImportService]),
  881.   
  882.   // List of pending jobs.  We only want to allow one to run at a time, 
  883.   // since this can lock up the UI.
  884.   _importJobs: null,
  885.   
  886.   /**
  887.    * \brief Import any media files found in the given directories to 
  888.    *        the specified media list.
  889.    */
  890.   import: function DirectoryImportService_import(aDirectoryArray, aTargetMediaList, aTargetIndex) {
  891.     return this.importWithCustomSnifferAndMetadataScanner(
  892.       aDirectoryArray, null, null, aTargetMediaList, aTargetIndex);
  893.   },
  894.  
  895.   importWithCustomSnifferAndMetadataScanner: function DirectoryImportService_importWithCustomSniffer(
  896.       aDirectoryArray, aTypeSniffer, aMetadataScanner, aTargetMediaList, aTargetIndex) {
  897.     if (!aTypeSniffer) {
  898.       aTypeSniffer = Cc["@songbirdnest.com/Songbird/Mediacore/TypeSniffer;1"]
  899.                      .createInstance(Ci.sbIMediacoreTypeSniffer);
  900.     }
  901.     if (!aMetadataScanner) {
  902.       aMetadataScanner = Cc["@songbirdnest.com/Songbird/FileMetadataService;1"]
  903.                          .getService(Ci.sbIFileMetadataService);
  904.     }
  905.     // Default to main library if not target is provided
  906.     if (!aTargetMediaList) {
  907.       aTargetMediaList = LibraryUtils.mainLibrary;
  908.     }
  909.     
  910.     var job = new DirectoryImportJob(
  911.       aDirectoryArray, aTypeSniffer, aMetadataScanner, aTargetMediaList, aTargetIndex, this);
  912.     
  913.     this._importJobs.push(job);
  914.     
  915.     // If this is the only job, just start immediately.
  916.     // Otherwise it will be started when the current job finishes.
  917.     if (this._importJobs.length == 1) {
  918.       job.begin();
  919.     }
  920.     
  921.     return job;
  922.   },
  923.   
  924.   /**
  925.    * \brief Called by the active import job when it completes
  926.    */
  927.   onJobComplete: function DirectoryImportService_onJobComplete() {
  928.     this._importJobs.shift();
  929.     if (this._importJobs.length > 0) {
  930.       this._importJobs[0].begin();
  931.     }
  932.   }
  933.  
  934. } // DirectoryImportService.prototype
  935.  
  936.  
  937. function NSGetModule(compMgr, fileSpec) {
  938.   return XPCOMUtils.generateModule([DirectoryImportService]);
  939. }
  940.