'use strict'; pandora.fs = (function() { var that = { local: {}, downloads: {}, enabled: false, }, active, queue = [], requestedBytes = 100*1024*1024*1024; // 100GB if(window.webkitRequestFileSystem) { that.enabled = true; window.webkitRequestFileSystem(window.PERSISTENT, requestedBytes, function(fs) { that.fs = fs; that.fs.root.createReader().readEntries(function(results) { results.forEach(function(entry) { if (entry.isFile && !Ox.startsWith(entry.name, 'partial::')) { that.local[entry.name] = entry.toURL(); } }); }); }, function(e) { Ox.Log('FS', 'Error:', e); }); } function cacheVideo(id, callback) { active = true; pandora.api.get({id: id, keys: ['parts']}, function(result) { var parts = result.data.parts, sizes = []; downloadPart(0); function downloadPart(part) { if (that.getVideoURL(id, pandora.user.ui.videoResolution, part + 1)) { done(); } else { that.downloadVideoURL(id, pandora.user.ui.videoResolution, part + 1, false, function(result) { result.progress = 1/parts * (part + result.progress); that.downloads[id].progress = result.progress; if (result.cancel) { that.downloads[id].cancel = result.cancel; } if (result.total) { sizes[part] = result.total; that.downloads[id].size = Ox.sum(sizes); } if (result.url) { done(); } callback && callback(result); }); } function done() { var n = 0; if (part + 1 == parts) { Ox.range(parts).forEach(function(part) { var name = that.getVideoName(id, pandora.user.ui.videoResolution, part + 1), partialName = 'partial::' + name; renameFile(partialName, name, function(fileEntry) { if (fileEntry) { that.local[name] = fileEntry.toURL(); } else { Ox.print('rename failed'); callback && callback({progress: -1, error: 'rename failed'}); } if (++n == parts) { callback && callback({ progress: 1 }); } }); }); delete that.downloads[id]; active = false; if (queue.length) { var next = queue.shift(); setTimeout(function() { cacheVideo(next[0], next[1]); }); } } else { setTimeout(function() { downloadPart(part + 1); }); } } } }); } function renameFile(old, name, callback) { that.fs.root.getFile(old, {}, function(fileEntry) { fileEntry.moveTo(that.fs.root, name); setTimeout(function() { that.fs.root.getFile(name, {}, callback, function() { callback() }); }); }, function() { Ox.Log('FS', 'failed to move', old, name); callback(); }); } that.cacheVideo = function(id, callback) { if (that.getVideoURL(id, pandora.user.ui.videoResolution, 1) || that.downloads[id]) { callback({progress: 1}); return; } else { that.downloads = that.downloads || {}; that.downloads[id] = { added: new Date(), cancel: function() { }, id: id + '::' + pandora.user.ui.videoResolution, item: id, progress: 0, resolution: pandora.user.ui.videoResolution, size: 0 }; (active || queue.length) ? queue.push([id, callback]) : cacheVideo(id, callback); } }; that.getVideoName = function(id, resolution, part, track) { return pandora.getVideoURLName(id, resolution, part, track).replace('/', '::'); }; that.removeVideo = function(id, callback) { if (that.downloads && that.downloads[id] && that.downloads[id].cancel) { that.downloads[id].cancel(); delete that.downloads[id]; active = false; if (queue.length) { var next = queue.shift(); setTimeout(function() { cacheVideo(next[0], next[1]); }); } } else { pandora.api.get({id: id, keys: ['parts']}, function(result) { var count = result.data.parts * pandora.site.video.resolutions.length, done = 0; Ox.range(result.data.parts).forEach(function(part) { pandora.site.video.resolutions.forEach(function(resolution) { var name = that.getVideoName(id, resolution, part + 1); that.fs.root.getFile(name, {create: false}, function(fileEntry) { // remove existing file fileEntry.remove(function(e) { if (that.local[name]) { delete that.local[name]; } }); ++done == count && callback(); }, function() { // file not found ++done == count && callback(); }); // remove partial file too that.fs.root.getFile('partial::' + name, {create: false}, function(fileEntry) { fileEntry.remove(function(e) {}); }); }); }); }); } }; that.storeBlob = function(blob, name, callback, append) { requestQuota(blob.size, function() { that.fs.root.getFile(name, {create: true}, function(fileEntry) { fileEntry.createWriter(function(fileWriter) { append && fileWriter.seek(fileWriter.length); fileWriter.onwriteend = function(e) { callback({ progress: 1, url: fileEntry.toURL() }); }; fileWriter.onerror = function(event) { Ox.Log('FS', 'Write failed: ' + event.toString()); callback({progress: -1, event: event}); }; fileWriter.write(blob); }, function(event) { callback({progress: -1, event: event}); }); }, function(event) { callback({progress: -1, event: event}); }); }, function(event) { callback({progress: -1, event: event}); }); function requestQuota(size, callback, error) { navigator.webkitPersistentStorage.queryUsageAndQuota(function(usage, quota) { if (quota - usage < size) { navigator.webkitPersistentStorage.requestQuota(quota + requestedBytes, function(grantedBytes) { callback(); }, error); } else { callback(); } }); } }; that.downloadVideoURL = function(id, resolution, part, track, callback) { //fixme: would be nice to download videos from subdomains, // currently throws a cross domain error var name = that.getVideoName(id, resolution, part, track), partialName = 'partial::' + name, url = '/' + pandora.getVideoURLName(id, resolution, part, track), blobSize = 5*1024*1024, total; Ox.Log('FS', 'start downloading', url); getSize(url, function(size) { Ox.Log('FS', 'HEAD', url, size); total = size; that.fs.root.getFile(partialName, {create: false}, function(fileEntry) { fileEntry.getMetadata(function(meta) { if (meta.size >= total) { Ox.Log('FS', 'file exists, done', meta.size, total); callback({ progress: 1, total: total, url: fileEntry.toURL() }) } else { Ox.Log('FS', 'resume from', meta.size); partialDownload(meta.size); } }); }, function() { Ox.Log('FS', 'new download'); partialDownload(0); }); }); function getSize(url, callback) { var xhr = new XMLHttpRequest(); xhr.open('HEAD', url, true); xhr.addEventListener('load', function(event) { callback(event.total); }); xhr.addEventListener('error', function (event) { setTimeout(function() { getSize(url, callback); }, 1000); }); xhr.addEventListener('abort', function (event) { callback({progress: -1, event: event}); }); xhr.addEventListener('timeout', function (event) { setTimeout(function() { getSize(url, callback); }, 1000); }); xhr.send(); } function partialDownload(offset) { var end = offset + blobSize - 1; if (total) { end = Math.min(end, total - 1); } var range = 'bytes=' + offset + '-' + end; Ox.Log('FS', 'download part', url, offset, end, total, range); var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.setRequestHeader('Range', range); xhr.withCredentials = true; xhr.responseType = 'blob'; xhr.timeout = 1000 * 60 * 5; xhr.addEventListener('progress', function(event) { if (event.lengthComputable) { if (!total) { var range = xhr.getResponseHeader('Content-Range'); total = Math.round(Ox.last(range.split('/'))); } var progress = (event.loaded + offset) / total; callback({ progress: progress, request: xhr, total: total, cancel: function() { xhr.abort(); } }); } }); xhr.addEventListener('load', function() { var blob = xhr.response; setTimeout(function() { that.storeBlob(blob, partialName, function(response) { if (offset + blob.size < total) { partialDownload(offset + blob.size); } else { callback(response); } }, true); }); }); xhr.addEventListener('error', function (event) { Ox.print('partial download failed. retrying in 1 second'); //fixme. make blobSize smaller if this fails? setTimeout(function() { partialDownload(offset); }, 1000); }); xhr.addEventListener('abort', function (event) { callback({progress: -1, event: event}); }); xhr.addEventListener('timeout', function (event) { Ox.print('partial download, timeout'); setTimeout(function() { partialDownload(offset); }, 1000); }); xhr.send(); } }; that.getVideos = function(callback) { var files = {}; that.fs.root.createReader().readEntries(function(results) { var n = 0; if (results.length) { results.forEach(function(fileEntry) { if (Ox.startsWith(fileEntry.name, 'partial::')) { ++n == results.length && callback(Ox.values(files).concat(Ox.values(that.downloads))); } else { fileEntry.getMetadata(function(meta) { var item = fileEntry.name.split('::')[0], resolution = parseInt(fileEntry.name.split('::')[1].split('p')[0]), part = parseInt(fileEntry.name.split('::')[1].split('p')[1].split('.')[0]), key = item + '::' + resolution; if (!(that.downloads && that.downloads[item])) { files[key] = Ox.extend(files[key] || {}, { added: meta.modificationTime, id: item + '::' + resolution, item: item, progress: 1, resolution: resolution, size: files[key] ? files[key].size + meta.size: meta.size }); } ++n == results.length && callback(Ox.values(files).concat(Ox.values(that.downloads))); }); } }); } else { callback(Ox.values(that.downloads)); } }); }; that.getVideoURL = function(id, resolution, part, track) { var name = that.getVideoName(id, resolution, part, track); return that.local[name]; }; return that; }());