'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) { if (Ox.startsWith(entry.name, 'partial::')) { entry.remove(function() {}); } else { var name = entry.name.split('::'), filename = Ox.last(name), foldername = name[0] + "::" + filename.split('p')[0]; var key = foldername[0] + '/' + foldername; createTree(key, function(folder) { entry.moveTo(folder, filename, function(e) { var name = folder.name + '/' + e.name; that.local[name] = e.toURL(); that.storeBlob(new Blob(['ok']), key + '/done', function() {}); }, function() { Ox.print('error moving', filename); }); }); } } else if (entry.isDirectory) { entry.createReader().readEntries(function(prefixes) { prefixes.forEach(function(prefix) { prefix.isDirectory && prefix.createReader().readEntries(function(contents) { if (contents.filter(function(e) { return e.name == 'done'}).length) { contents.forEach(function(e) { if (e.name != 'done') { var name = prefix.name + '/' + e.name; that.local[name] = e.toURL(); } }); } }); }); }); } }); }); }, function(e) { Ox.Log('FS', 'Error:', e); }); } function createTree(folder, callback, root) { var parts = folder.split('/'); root = root || that.fs.root; root.getDirectory(parts.shift(), {create: true}, function(folder) { if (parts.length) { createTree(parts.join('/'), callback, folder); } else { callback(folder); } }, function(error) { Ox.Log('FS', 'error', error); callback(); }); } function cacheVideo(id, callback) { var key = id[0] + '/' + id + '::' + pandora.user.ui.videoResolution; active = true; createTree(key, function(folder) { pandora.api.get({id: id, keys: ['parts']}, function(result) { var parts = result.data.parts, sizes = []; downloadPart(0); function downloadPart(part) { var partName = key + '/' + pandora.user.ui.videoResolution + 'p' + (part + 1) + '.' + pandora.user.videoFormat, url = that.getVideoURL(id, pandora.user.ui.videoResolution, part + 1); if (url) { done({url: url}); } else { that.downloadVideoURL(id, pandora.user.ui.videoResolution, part + 1, false, function(result) { result.progress = 1/parts * (part + result.progress); if (!that.downloads[id]) { result.cancel && result.cancel(); return; } 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(result); } else { callback && callback(result); } }); } function done(result) { that.local[partName] = result.url; if (part + 1 == parts) { //fixme that.storeBlob(new Blob(['ok']), key + '/done', function() { delete that.downloads[id]; active = false; callback && callback(result); if (queue.length) { var next = queue.shift(); setTimeout(function() { cacheVideo(next[0], next[1]); }, 50); } }); } else { setTimeout(function() { downloadPart(part + 1); }); } } } }); }); } function renameFile(old, name, callback) { that.fs.root.getFile(old, {}, function(fileEntry) { fileEntry.moveTo(that.fs.root, name, callback, function(error) { Ox.Log('FS', 'failed to move', old, name); callback(); }); }, function() { Ox.Log('FS', 'could not find old file', old, name); callback(); }); } that.cacheVideo = function(id, callback) { if (that.getVideoURL(id, pandora.user.ui.videoResolution, 1) || that.downloads[id]) { callback && 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.cacheBlob = function(blob, id, resolution, index, callback) { var key = id[0] + '/' + id + '::' + resolution, name = that.getVideoName(id, resolution, index); createTree(key, function(folder) { that.storeBlob(blob, name[0] + '/' + name, function(response) { if (response.progress == -1) { callback(response); } else { that.storeBlob(new Blob(['ok']), key + '/done', callback); } }); }); }; that.getVideoName = function(id, resolution, part, track) { return pandora.getVideoURLName(id, resolution, part, track).replace(id + '\/', id + '::' + resolution + '/'); }; that.removeVideo = function(id, callback) { if (that.downloads && that.downloads[id] && that.downloads[id].cancel) { that.downloads[id].cancel(); delete that.downloads[id]; } // remove legacy files too 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 = pandora.getVideoURLName(id, resolution, part + 1).replace('/', '::'); that.fs.root.getFile(name, {create: false}, function(fileEntry) { // remove existing file fileEntry.remove(function(e) { if (that.local[name]) { delete that.local[name]; } }); }, function() { // file not found }); // remove partial file too that.fs.root.getFile('partial::' + name, {create: false}, function(fileEntry) { fileEntry.remove(function(e) {}); }); }); }); }); Ox.parallelForEach(pandora.site.video.resolutions, function(resolution, i, resolutions, cb) { var key = id + '::' + resolution; that.fs.root.getDirectory(key[0] + '/' + key, {create: false}, function(dir) { dir.removeRecursively(cb, cb); Object.keys(that.local).forEach(function(name) { if (Ox.startsWith(name, key)) { delete that.local[name]; } }); cb(); }, function() { cb(); }); }, function() { callback(); }); }; 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), 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(name, {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', url, 'resume from', meta.size); partialDownload(meta.size); } }); }, function() { Ox.Log('FS', url, '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, total: total, cancel: function() { xhr.abort(); active = false; if (queue.length) { var next = queue.shift(); setTimeout(function() { cacheVideo(next[0], next[1]); }, 50); } } }); } }); xhr.addEventListener('load', function() { var blob = xhr.response; setTimeout(function() { that.storeBlob(blob, name[0] + '/' + name, 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) { if (results.length) { Ox.parallelForEach(results, function(fileEntry, i, entries, next_entry) { if (fileEntry.isFile) { if (Ox.startsWith(fileEntry.name, 'partial::')) { next_entry(); } 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] && that.downloads[item].resolution == resolution)) { 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 }); } next_entry(); }); } } else if (fileEntry.isDirectory) { fileEntry.createReader().readEntries(function(prefixes) { if (prefixes.length) { Ox.parallelForEach(prefixes, function(prefix, i, prefixes, callback) { prefix.createReader().readEntries(function(contents) { if (contents.filter(function(f) { return f.name == 'done' }).length) { Ox.parallelMap(contents, function(e, i, col, callback) { e.getMetadata(function(meta) { callback(meta); }); }, function(meta) { var item = prefix.name.split('::')[0], resolution = parseInt(prefix.name.split('::')[1]), key = item + '::' + resolution; if (!(that.downloads && that.downloads[item] && that.downloads[item].resolution == resolution)) { files[key] = Ox.extend(files[key] || {}, { added: Ox.min(meta.map(function(m) { return m.modificationTime; })), id: item + '::' + resolution, item: item, progress: 1, resolution: resolution, size: Ox.sum(meta.map(function(m) { return m.size; })) }); } callback(); } ); } else { callback(); } }); }, next_entry); } else { next_entry(); } }); } else { next_entry(); } }, function() { 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]; }; that._tree = function(root) { root = root || that.fs.root; root.createReader().readEntries(function(results) { results.forEach(function(entry) { if (entry.isFile) { Ox.print(entry.fullPath); } else { that._tree(entry); } }); }); }; return that; }());