1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
8 * MetadataCache is a map from Entry to an object containing properties.
9 * Properties are divided by types, and all properties of one type are accessed
11 * Some of the properties:
13 * filesystem: size, modificationTime
15 * drive: pinned, present, hosted, availableOffline
16 * streaming: (no property)
18 * Following are not fetched for non-present drive files.
19 * media: artist, album, title, width, height, imageTransform, etc.
20 * thumbnail: url, transform
22 * Following are always fetched from content, and so force the downloading
23 * of remote drive files. One should use this for required content metadata,
24 * i.e. image orientation.
25 * fetchedMedia: width, height, etc.
30 * cache.get([entry1, entry2], 'drive|filesystem', function(metadata) {
31 * if (metadata[0].drive.pinned && metadata[1].filesystem.size === 0)
32 * alert("Pinned and empty!");
35 * cache.set(entry, 'internal', {presence: 'deleted'});
37 * cache.clear([fileEntry1, fileEntry2], 'filesystem');
39 * // Getting fresh value.
40 * cache.clear(entry, 'thumbnail');
41 * cache.get(entry, 'thumbnail', function(thumbnail) {
42 * img.src = thumbnail.url;
45 * var cached = cache.getCached(entry, 'filesystem');
46 * var size = (cached && cached.size) || UNKNOWN_SIZE;
51 function MetadataCache() {
53 * Map from Entry (using Entry.toURL) to metadata. Metadata contains
54 * |properties| - an hierarchical object of values, and an object for each
55 * metadata provider: <prodiver-id>: {time, callbacks}
61 * List of metadata providers.
67 * List of observers added. Each one is an object with fields:
68 * re - regexp of urls;
69 * type - metadata type;
70 * callback - the callback.
79 this.currentCacheSize_ = 0;
82 * Time of first get query of the current batch. Items updated later than this
83 * will not be evicted.
86 this.lastBatchStart_ = new Date();
90 * Observer type: it will be notified if the changed Entry is exactly the same
91 * as the observed Entry.
93 MetadataCache.EXACT = 0;
96 * Observer type: it will be notified if the changed Entry is an immediate child
97 * of the observed Entry.
99 MetadataCache.CHILDREN = 1;
102 * Observer type: it will be notified if the changed Entry is a descendant of
103 * of the observer Entry.
105 MetadataCache.DESCENDANTS = 2;
108 * Margin of the cache size. This amount of caches may be kept in addition.
110 MetadataCache.EVICTION_THRESHOLD_MARGIN = 500;
113 * @param {VolumeManagerWrapper} volumeManager Volume manager instance.
114 * @return {MetadataCache!} The cache with all providers.
116 MetadataCache.createFull = function(volumeManager) {
117 var cache = new MetadataCache();
118 cache.providers_.push(new FilesystemProvider());
119 cache.providers_.push(new DriveProvider(volumeManager));
120 cache.providers_.push(new ContentProvider());
125 * Clones metadata entry. Metadata entries may contain scalars, arrays,
126 * hash arrays and Date object. Other objects are not supported.
127 * @param {Object} metadata Metadata object.
128 * @return {Object} Cloned entry.
130 MetadataCache.cloneMetadata = function(metadata) {
131 if (metadata instanceof Array) {
133 for (var index = 0; index < metadata.length; index++) {
134 result[index] = MetadataCache.cloneMetadata(metadata[index]);
137 } else if (metadata instanceof Date) {
138 var result = new Date();
139 result.setTime(metadata.getTime());
141 } else if (metadata instanceof Object) { // Hash array only.
143 for (var property in metadata) {
144 if (metadata.hasOwnProperty(property))
145 result[property] = MetadataCache.cloneMetadata(metadata[property]);
154 * @return {boolean} Whether all providers are ready.
156 MetadataCache.prototype.isInitialized = function() {
157 for (var index = 0; index < this.providers_.length; index++) {
158 if (!this.providers_[index].isInitialized()) return false;
164 * Sets the size of cache. The actual cache size may be larger than the given
166 * @param {number} size The cache size to be set.
168 MetadataCache.prototype.setCacheSize = function(size) {
169 this.currentCacheSize_ = size;
171 if (this.totalCount_ > this.currentEvictionThreshold_())
176 * Returns the current threshold to evict caches. When the number of caches
177 * exceeds this, the cache should be evicted.
178 * @return {number} Threshold to evict caches.
181 MetadataCache.prototype.currentEvictionThreshold_ = function() {
182 return this.currentCacheSize_ * 2 + MetadataCache.EVICTION_THRESHOLD_MARGIN;
186 * Fetches the metadata, puts it in the cache, and passes to callback.
187 * If required metadata is already in the cache, does not fetch it again.
188 * @param {Entry|Array.<Entry>} entries The list of entries. May be just a
190 * @param {string} type The metadata type.
191 * @param {function(Object)} callback The metadata is passed to callback.
193 MetadataCache.prototype.get = function(entries, type, callback) {
194 if (!(entries instanceof Array)) {
195 this.getOne(entries, type, callback);
199 if (entries.length === 0) {
200 if (callback) callback([]);
205 var remaining = entries.length;
206 this.startBatchUpdates();
208 var onOneItem = function(index, value) {
209 result[index] = value;
211 if (remaining === 0) {
212 this.endBatchUpdates();
213 if (callback) setTimeout(callback, 0, result);
217 for (var index = 0; index < entries.length; index++) {
219 this.getOne(entries[index], type, onOneItem.bind(this, index));
224 * Fetches the metadata for one Entry. See comments to |get|.
225 * @param {Entry} entry The entry.
226 * @param {string} type Metadata type.
227 * @param {function(Object)} callback The callback.
229 MetadataCache.prototype.getOne = function(entry, type, callback) {
230 if (type.indexOf('|') !== -1) {
231 var types = type.split('|');
233 var typesLeft = types.length;
235 var onOneType = function(requestedType, metadata) {
236 result[requestedType] = metadata;
238 if (typesLeft === 0) callback(result);
241 for (var index = 0; index < types.length; index++) {
242 this.getOne(entry, types[index], onOneType.bind(null, types[index]));
247 callback = callback || function() {};
249 var entryURL = entry.toURL();
250 if (!(entryURL in this.cache_)) {
251 this.cache_[entryURL] = this.createEmptyItem_();
255 var item = this.cache_[entryURL];
257 if (type in item.properties) {
258 callback(item.properties[type]);
262 this.startBatchUpdates();
263 var providers = this.providers_.slice();
267 var onFetched = function() {
268 if (type in item.properties) {
269 self.endBatchUpdates();
270 // Got properties from provider.
271 callback(item.properties[type]);
277 var onProviderProperties = function(properties) {
278 var id = currentProvider.getId();
279 var fetchedCallbacks = item[id].callbacks;
280 delete item[id].callbacks;
281 item.time = new Date();
282 self.mergeProperties_(entry, properties);
284 for (var index = 0; index < fetchedCallbacks.length; index++) {
285 fetchedCallbacks[index]();
289 var queryProvider = function() {
290 var id = currentProvider.getId();
291 if ('callbacks' in item[id]) {
292 // We are querying this provider now.
293 item[id].callbacks.push(onFetched);
295 item[id].callbacks = [onFetched];
296 currentProvider.fetch(entry, type, onProviderProperties);
300 var tryNextProvider = function() {
301 if (providers.length === 0) {
302 self.endBatchUpdates();
303 callback(item.properties[type] || null);
307 currentProvider = providers.shift();
308 if (currentProvider.supportsEntry(entry) &&
309 currentProvider.providesType(type)) {
320 * Returns the cached metadata value, or |null| if not present.
321 * @param {Entry|Array.<Entry>} entries The list of entries. May be just a
323 * @param {string} type The metadata type.
324 * @return {Object} The metadata or null.
326 MetadataCache.prototype.getCached = function(entries, type) {
328 if (!(entries instanceof Array)) {
334 for (var index = 0; index < entries.length; index++) {
335 var entryURL = entries[index].toURL();
336 result.push(entryURL in this.cache_ ?
337 (this.cache_[entryURL].properties[type] || null) : null);
340 return single ? result[0] : result;
344 * Puts the metadata into cache
345 * @param {Entry|Array.<Entry>} entries The list of entries. May be just a
347 * @param {string} type The metadata type.
348 * @param {Array.<Object>} values List of corresponding metadata values.
350 MetadataCache.prototype.set = function(entries, type, values) {
351 if (!(entries instanceof Array)) {
356 this.startBatchUpdates();
357 for (var index = 0; index < entries.length; index++) {
358 var entryURL = entries[index].toURL();
359 if (!(entryURL in this.cache_)) {
360 this.cache_[entryURL] = this.createEmptyItem_();
363 this.cache_[entryURL].properties[type] = values[index];
364 this.notifyObservers_(entries[index], type);
366 this.endBatchUpdates();
370 * Clears the cached metadata values.
371 * @param {Entry|Array.<Entry>} entries The list of entries. May be just a
373 * @param {string} type The metadata types or * for any type.
375 MetadataCache.prototype.clear = function(entries, type) {
376 if (!(entries instanceof Array))
379 var types = type.split('|');
381 for (var index = 0; index < entries.length; index++) {
382 var entry = entries[index];
383 var entryURL = entry.toURL();
384 if (entryURL in this.cache_) {
386 this.cache_[entryURL].properties = {};
388 for (var j = 0; j < types.length; j++) {
390 delete this.cache_[entryURL].properties[type];
398 * Clears the cached metadata values recursively.
399 * @param {Entry} entry An entry to be cleared recursively from cache.
400 * @param {string} type The metadata types or * for any type.
402 MetadataCache.prototype.clearRecursively = function(entry, type) {
403 var types = type.split('|');
404 var keys = Object.keys(this.cache_);
405 var entryURL = entry.toURL();
407 for (var index = 0; index < keys.length; index++) {
408 var cachedEntryURL = keys[index];
409 if (cachedEntryURL.substring(0, entryURL.length) === entryURL) {
411 this.cache_[cachedEntryURL].properties = {};
413 for (var j = 0; j < types.length; j++) {
415 delete this.cache_[cachedEntryURL].properties[type];
423 * Adds an observer, which will be notified when metadata changes.
424 * @param {Entry} entry The root entry to look at.
425 * @param {number} relation This defines, which items will trigger the observer.
426 * See comments to |MetadataCache.EXACT| and others.
427 * @param {string} type The metadata type.
428 * @param {function(Array.<Entry>, Object.<string, Object>)} observer Map of
429 * entries and corresponding metadata values are passed to this callback.
430 * @return {number} The observer id, which can be used to remove it.
432 MetadataCache.prototype.addObserver = function(
433 entry, relation, type, observer) {
434 var entryURL = entry.toURL();
436 if (relation === MetadataCache.CHILDREN)
437 re = entryURL + '(/[^/]*)?';
438 else if (relation === MetadataCache.DESCENDANTS)
439 re = entryURL + '(/.*)?';
443 var id = ++this.observerId_;
444 this.observers_.push({
445 re: new RegExp('^' + re + '$'),
456 * Removes the observer.
457 * @param {number} id Observer id.
458 * @return {boolean} Whether observer was removed or not.
460 MetadataCache.prototype.removeObserver = function(id) {
461 for (var index = 0; index < this.observers_.length; index++) {
462 if (this.observers_[index].id === id) {
463 this.observers_.splice(index, 1);
471 * Start batch updates.
473 MetadataCache.prototype.startBatchUpdates = function() {
475 if (this.batchCount_ === 1)
476 this.lastBatchStart_ = new Date();
480 * End batch updates. Notifies observers if all nested updates are finished.
482 MetadataCache.prototype.endBatchUpdates = function() {
484 if (this.batchCount_ !== 0) return;
485 if (this.totalCount_ > this.currentEvictionThreshold_())
487 for (var index = 0; index < this.observers_.length; index++) {
488 var observer = this.observers_[index];
491 for (var entryURL in observer.pending) {
492 if (observer.pending.hasOwnProperty(entryURL) &&
493 entryURL in this.cache_) {
494 var entry = observer.pending[entryURL];
496 properties[entryURL] =
497 this.cache_[entryURL].properties[observer.type] || null;
500 observer.pending = {};
501 if (entries.length > 0) {
502 observer.callback(entries, properties);
508 * Notifies observers or puts the data to pending list.
509 * @param {Entry} entry Changed entry.
510 * @param {string} type Metadata type.
513 MetadataCache.prototype.notifyObservers_ = function(entry, type) {
514 var entryURL = entry.toURL();
515 for (var index = 0; index < this.observers_.length; index++) {
516 var observer = this.observers_[index];
517 if (observer.type === type && observer.re.test(entryURL)) {
518 if (this.batchCount_ === 0) {
519 // Observer expects array of urls and map of properties.
521 property[entryURL] = this.cache_[entryURL].properties[type] || null;
525 observer.pending[entryURL] = entry;
532 * Removes the oldest items from the cache.
533 * This method never removes the items from last batch.
536 MetadataCache.prototype.evict_ = function() {
539 // We leave only a half of items, so we will not call evict_ soon again.
540 var desiredCount = this.currentEvictionThreshold_();
541 var removeCount = this.totalCount_ - desiredCount;
542 for (var url in this.cache_) {
543 if (this.cache_.hasOwnProperty(url) &&
544 this.cache_[url].time < this.lastBatchStart_) {
549 toRemove.sort(function(a, b) {
550 var aTime = this.cache_[a].time;
551 var bTime = this.cache_[b].time;
552 return aTime < bTime ? -1 : aTime > bTime ? 1 : 0;
555 removeCount = Math.min(removeCount, toRemove.length);
556 this.totalCount_ -= removeCount;
557 for (var index = 0; index < removeCount; index++) {
558 delete this.cache_[toRemove[index]];
563 * @return {Object} Empty cache item.
566 MetadataCache.prototype.createEmptyItem_ = function() {
567 var item = {properties: {}};
568 for (var index = 0; index < this.providers_.length; index++) {
569 item[this.providers_[index].getId()] = {};
575 * Caches all the properties from data to cache entry for the entry.
576 * @param {Entry} entry The file entry.
577 * @param {Object} data The properties.
580 MetadataCache.prototype.mergeProperties_ = function(entry, data) {
581 if (data === null) return;
582 var properties = this.cache_[entry.toURL()].properties;
583 for (var type in data) {
584 if (data.hasOwnProperty(type) && !properties.hasOwnProperty(type)) {
585 properties[type] = data[type];
586 this.notifyObservers_(entry, type);
592 * Base class for metadata providers.
595 function MetadataProvider() {
599 * @param {Entry} entry The entry.
600 * @return {boolean} Whether this provider supports the entry.
602 MetadataProvider.prototype.supportsEntry = function(entry) { return false; };
605 * @param {string} type The metadata type.
606 * @return {boolean} Whether this provider provides this metadata.
608 MetadataProvider.prototype.providesType = function(type) { return false; };
611 * @return {string} Unique provider id.
613 MetadataProvider.prototype.getId = function() { return ''; };
616 * @return {boolean} Whether provider is ready.
618 MetadataProvider.prototype.isInitialized = function() { return true; };
621 * Fetches the metadata. It's suggested to return all the metadata this provider
623 * @param {Entry} entry File entry.
624 * @param {string} type Requested metadata type.
625 * @param {function(Object)} callback Callback expects a map from metadata type
628 MetadataProvider.prototype.fetch = function(entry, type, callback) {
629 throw new Error('Default metadata provider cannot fetch.');
634 * Provider of filesystem metadata.
635 * This provider returns the following objects:
636 * filesystem: { size, modificationTime }
639 function FilesystemProvider() {
640 MetadataProvider.call(this);
643 FilesystemProvider.prototype = {
644 __proto__: MetadataProvider.prototype
648 * @param {Entry} entry The entry.
649 * @return {boolean} Whether this provider supports the entry.
651 FilesystemProvider.prototype.supportsEntry = function(entry) {
656 * @param {string} type The metadata type.
657 * @return {boolean} Whether this provider provides this metadata.
659 FilesystemProvider.prototype.providesType = function(type) {
660 return type === 'filesystem';
664 * @return {string} Unique provider id.
666 FilesystemProvider.prototype.getId = function() { return 'filesystem'; };
669 * Fetches the metadata.
670 * @param {Entry} entry File entry.
671 * @param {string} type Requested metadata type.
672 * @param {function(Object)} callback Callback expects a map from metadata type
675 FilesystemProvider.prototype.fetch = function(
676 entry, type, callback) {
677 function onError(error) {
681 function onMetadata(entry, metadata) {
684 size: entry.isFile ? (metadata.size || 0) : -1,
685 modificationTime: metadata.modificationTime
690 entry.getMetadata(onMetadata.bind(null, entry), onError);
694 * Provider of drive metadata.
695 * This provider returns the following objects:
696 * drive: { pinned, hosted, present, customIconUrl, etc. }
697 * thumbnail: { url, transform }
699 * @param {VolumeManagerWrapper} volumeManager Volume manager instance.
702 function DriveProvider(volumeManager) {
703 MetadataProvider.call(this);
706 * @type {VolumeManagerWrapper}
709 this.volumeManager_ = volumeManager;
711 // We batch metadata fetches into single API call.
713 this.callbacks_ = [];
714 this.scheduled_ = false;
716 this.callApiBound_ = this.callApi_.bind(this);
719 DriveProvider.prototype = {
720 __proto__: MetadataProvider.prototype
724 * @param {Entry} entry The entry.
725 * @return {boolean} Whether this provider supports the entry.
727 DriveProvider.prototype.supportsEntry = function(entry) {
728 var locationInfo = this.volumeManager_.getLocationInfo(entry);
729 return locationInfo && locationInfo.isDriveBased;
733 * @param {string} type The metadata type.
734 * @return {boolean} Whether this provider provides this metadata.
736 DriveProvider.prototype.providesType = function(type) {
737 return type === 'drive' || type === 'thumbnail' ||
738 type === 'streaming' || type === 'media';
742 * @return {string} Unique provider id.
744 DriveProvider.prototype.getId = function() { return 'drive'; };
747 * Fetches the metadata.
748 * @param {Entry} entry File entry.
749 * @param {string} type Requested metadata type.
750 * @param {function(Object)} callback Callback expects a map from metadata type
753 DriveProvider.prototype.fetch = function(entry, type, callback) {
754 this.entries_.push(entry);
755 this.callbacks_.push(callback);
756 if (!this.scheduled_) {
757 this.scheduled_ = true;
758 setTimeout(this.callApiBound_, 0);
763 * Schedules the API call.
766 DriveProvider.prototype.callApi_ = function() {
767 this.scheduled_ = false;
769 var entries = this.entries_;
770 var callbacks = this.callbacks_;
772 this.callbacks_ = [];
775 var task = function(entry, callback) {
776 // TODO(mtomasz): Make getDriveEntryProperties accept Entry instead of URL.
777 var entryURL = entry.toURL();
778 chrome.fileBrowserPrivate.getDriveEntryProperties(entryURL,
779 function(properties) {
780 callback(self.convert_(properties, entry));
784 for (var i = 0; i < entries.length; i++)
785 task(entries[i], callbacks[i]);
789 * @param {DriveEntryProperties} data Drive entry properties.
790 * @param {Entry} entry File entry.
791 * @return {boolean} True if the file is available offline.
793 DriveProvider.isAvailableOffline = function(data, entry) {
800 // What's available offline? See the 'Web' column at:
801 // http://support.google.com/drive/answer/1628467
802 var subtype = FileType.getType(entry).subtype;
803 return (subtype === 'doc' ||
804 subtype === 'draw' ||
805 subtype === 'sheet' ||
806 subtype === 'slides');
810 * @param {DriveEntryProperties} data Drive entry properties.
811 * @return {boolean} True if opening the file does not require downloading it
812 * via a metered connection.
814 DriveProvider.isAvailableWhenMetered = function(data) {
815 return data.isPresent || data.isHosted;
819 * Converts API metadata to internal format.
820 * @param {Object} data Metadata from API call.
821 * @param {Entry} entry File entry.
822 * @return {Object} Metadata in internal format.
825 DriveProvider.prototype.convert_ = function(data, entry) {
828 present: data.isPresent,
829 pinned: data.isPinned,
830 hosted: data.isHosted,
831 imageWidth: data.imageWidth,
832 imageHeight: data.imageHeight,
833 imageRotation: data.imageRotation,
834 availableOffline: DriveProvider.isAvailableOffline(data, entry),
835 availableWhenMetered: DriveProvider.isAvailableWhenMetered(data),
836 customIconUrl: data.customIconUrl || '',
837 contentMimeType: data.contentMimeType || '',
838 sharedWithMe: data.sharedWithMe,
842 if (!data.isPresent) {
843 // Block the local fetch for drive files, which require downloading.
844 result.thumbnail = {url: '', transform: null};
848 if ('thumbnailUrl' in data) {
850 url: data.thumbnailUrl,
854 if (!data.isPresent) {
855 // Indicate that the data is not available in local cache.
856 // It used to have a field 'url' for streaming play, but it is
857 // derprecated. See crbug.com/174560.
858 result.streaming = {};
865 * Provider of content metadata.
866 * This provider returns the following objects:
867 * thumbnail: { url, transform }
868 * media: { artist, album, title, width, height, imageTransform, etc. }
869 * fetchedMedia: { same fields here }
872 function ContentProvider() {
873 MetadataProvider.call(this);
875 // Pass all URLs to the metadata reader until we have a correct filter.
876 this.urlFilter_ = /.*/;
878 var path = document.location.pathname;
879 var workerPath = document.location.origin +
880 path.substring(0, path.lastIndexOf('/') + 1) +
881 'foreground/js/metadata/metadata_dispatcher.js';
883 this.dispatcher_ = new SharedWorker(workerPath).port;
884 this.dispatcher_.start();
886 this.dispatcher_.onmessage = this.onMessage_.bind(this);
887 this.dispatcher_.postMessage({verb: 'init'});
889 // Initialization is not complete until the Worker sends back the
890 // 'initialized' message. See below.
891 this.initialized_ = false;
893 // Map from Entry.toURL() to callback.
894 // Note that simultaneous requests for same url are handled in MetadataCache.
895 this.callbacks_ = {};
898 ContentProvider.prototype = {
899 __proto__: MetadataProvider.prototype
903 * @param {Entry} entry The entry.
904 * @return {boolean} Whether this provider supports the entry.
906 ContentProvider.prototype.supportsEntry = function(entry) {
907 return entry.toURL().match(this.urlFilter_);
911 * @param {string} type The metadata type.
912 * @return {boolean} Whether this provider provides this metadata.
914 ContentProvider.prototype.providesType = function(type) {
915 return type === 'thumbnail' || type === 'fetchedMedia' || type === 'media';
919 * @return {string} Unique provider id.
921 ContentProvider.prototype.getId = function() { return 'content'; };
924 * Fetches the metadata.
925 * @param {Entry} entry File entry.
926 * @param {string} type Requested metadata type.
927 * @param {function(Object)} callback Callback expects a map from metadata type
930 ContentProvider.prototype.fetch = function(entry, type, callback) {
931 if (entry.isDirectory) {
935 var entryURL = entry.toURL();
936 this.callbacks_[entryURL] = callback;
937 this.dispatcher_.postMessage({verb: 'request', arguments: [entryURL]});
941 * Dispatch a message from a metadata reader to the appropriate on* method.
942 * @param {Object} event The event.
945 ContentProvider.prototype.onMessage_ = function(event) {
946 var data = event.data;
949 'on' + data.verb.substr(0, 1).toUpperCase() + data.verb.substr(1) + '_';
951 if (!(methodName in this)) {
952 console.error('Unknown message from metadata reader: ' + data.verb, data);
956 this[methodName].apply(this, data.arguments);
960 * @return {boolean} Whether provider is ready.
962 ContentProvider.prototype.isInitialized = function() {
963 return this.initialized_;
967 * Handles the 'initialized' message from the metadata reader Worker.
968 * @param {Object} regexp Regexp of supported urls.
971 ContentProvider.prototype.onInitialized_ = function(regexp) {
972 this.urlFilter_ = regexp;
974 // Tests can monitor for this state with
975 // ExtensionTestMessageListener listener("worker-initialized");
976 // ASSERT_TRUE(listener.WaitUntilSatisfied());
977 // Automated tests need to wait for this, otherwise we crash in
978 // browser_test cleanup because the worker process still has
979 // URL requests in-flight.
980 var test = chrome.test || window.top.chrome.test;
981 test.sendMessage('worker-initialized');
982 this.initialized_ = true;
986 * Converts content metadata from parsers to the internal format.
987 * @param {Object} metadata The content metadata.
988 * @param {Object=} opt_result The internal metadata object ot put result in.
989 * @return {Object!} Converted metadata.
991 ContentProvider.ConvertContentMetadata = function(metadata, opt_result) {
992 var result = opt_result || {};
994 if ('thumbnailURL' in metadata) {
995 metadata.thumbnailTransform = metadata.thumbnailTransform || null;
997 url: metadata.thumbnailURL,
998 transform: metadata.thumbnailTransform
1002 for (var key in metadata) {
1003 if (metadata.hasOwnProperty(key)) {
1004 if (!('media' in result)) result.media = {};
1005 result.media[key] = metadata[key];
1009 if ('media' in result) {
1010 result.fetchedMedia = result.media;
1017 * Handles the 'result' message from the worker.
1018 * @param {string} url File url.
1019 * @param {Object} metadata The metadata.
1022 ContentProvider.prototype.onResult_ = function(url, metadata) {
1023 var callback = this.callbacks_[url];
1024 delete this.callbacks_[url];
1025 callback(ContentProvider.ConvertContentMetadata(metadata));
1029 * Handles the 'error' message from the worker.
1030 * @param {string} url File entry.
1031 * @param {string} step Step failed.
1032 * @param {string} error Error description.
1033 * @param {Object?} metadata The metadata, if available.
1036 ContentProvider.prototype.onError_ = function(url, step, error, metadata) {
1037 if (MetadataCache.log) // Avoid log spam by default.
1038 console.warn('metadata: ' + url + ': ' + step + ': ' + error);
1039 metadata = metadata || {};
1040 // Prevent asking for thumbnail again.
1041 metadata.thumbnailURL = '';
1042 this.onResult_(url, metadata);
1046 * Handles the 'log' message from the worker.
1047 * @param {Array.<*>} arglist Log arguments.
1050 ContentProvider.prototype.onLog_ = function(arglist) {
1051 if (MetadataCache.log) // Avoid log spam by default.
1052 console.log.apply(console, ['metadata:'].concat(arglist));