upload improvements. closes #45

* ditch qq file uploader in favor of native xhr2
 * implement client-side optional auto-queuing
This commit is contained in:
Andrew Kelley 2014-03-24 15:49:51 -07:00
parent 1a7dc0e0a3
commit 3be4ad9cb6
10 changed files with 194 additions and 1347 deletions

View file

@ -22,14 +22,21 @@ DedupedQueue.prototype.idInQueue = function(id) {
return !!(this.pendingSet[id] || this.processingSet[id]);
};
DedupedQueue.prototype.add = function(id, item) {
DedupedQueue.prototype.add = function(id, item, cb) {
if (this.pendingSet[id]) return;
var queueItem = new QueueItem(id, item);
if (cb) queueItem.cbs.push(cb);
this.pendingSet[id] = queueItem;
this.pendingQueue.push(queueItem);
this.flush();
};
DedupedQueue.prototype.waitForId = function(id, cb) {
var queueItem = this.pendingSet[id] || this.processingSet[id];
if (!queueItem) return cb();
queueItem.cbs.push(cb);
};
DedupedQueue.prototype.flush = function() {
// if an item cannot go into the processing queue because an item with the
// same ID is already there, it goes into deferred
@ -59,15 +66,27 @@ DedupedQueue.prototype.startOne = function(queueItem) {
return;
}
callbackCalled = true;
delete self.processingSet[queueItem.id];
self.processingCount -= 1;
if (err) self.emit('error', err);
self.emit('oneEnd');
if (queueItem.cbs.length === 0) {
defaultCb(err);
} else {
for (var i = 0; i < queueItem.cbs.length; i += 1) {
queueItem.cbs[i](err);
}
}
self.flush();
function defaultCb(err) {
if (err) self.emit('error', err);
self.emit('oneEnd');
}
});
};
function QueueItem(id, item) {
this.id = id;
this.item = item;
this.cbs = [];
}

View file

@ -269,10 +269,29 @@ GrooveBasin.prototype.initializeDownload = function() {
GrooveBasin.prototype.initializeUpload = function() {
var self = this;
self.app.post('/upload', multipart, function(request, response) {
for (var name in request.files) {
var file = request.files[name];
self.player.importFile(file.path, file.originalFilename);
response.end();
var keys = [];
var pend = new Pend();
for (var key in request.files) {
var file = request.files[key];
pend.go(makeImportFn(file));
}
pend.wait(function() {
response.json(keys);
});
function makeImportFn(file) {
return function(cb) {
self.player.importFile(file.path, file.originalFilename, function(err, dbFile) {
if (err) {
console.error("Unable to import file:", file.path, "error:", err.stack);
} else if (!dbFile) {
console.error("Unable to locate new file due to race condition");
} else {
keys.push(dbFile.key);
}
cb();
});
};
}
});
};

View file

@ -367,12 +367,12 @@ Player.prototype.initialize = function(cb) {
}
};
Player.prototype.requestUpdateDb = function(dirName, forceRescan) {
Player.prototype.requestUpdateDb = function(dirName, forceRescan, cb) {
var fullPath = path.resolve(this.musicDirectory, dirName || "");
this.dirScanQueue.add(fullPath, {
dir: fullPath,
forceRescan: forceRescan,
});
}, cb);
};
Player.prototype.refreshFilesIndex = function(args, cb) {
@ -631,24 +631,30 @@ Player.prototype.setVolume = function(value) {
this.emit("volumeUpdate");
};
Player.prototype.importUrl = function(urlString) {
Player.prototype.importUrl = function(urlString, cb) {
var self = this;
cb = cb || logIfError;
var parsedUrl = url.parse(urlString);
var remoteFilename = path.basename(parsedUrl.pathname);
var req = superagent.get(urlString);
var ws = temp.createWriteStream({suffix: path.extname(urlString)});
req.pipe(ws);
ws.on('close', function(){
self.importFile(ws.path, remoteFilename, cleanAndLogIfErr);
self.importFile(ws.path, remoteFilename, cleanAndCb);
});
ws.on('error', cleanAndLogIfErr);
req.on('error', cleanAndLogIfErr);
ws.on('error', cleanAndCb);
req.on('error', cleanAndCb);
function cleanAndLogIfErr(err) {
function cleanAndCb(err, dbFile) {
temp.cleanup();
cb(err, dbFile);
}
function logIfError(err) {
if (err) {
console.error("Unable to import by URL.", err.stack, "URL:", urlString);
}
temp.cleanup();
}
};
@ -668,12 +674,15 @@ Player.prototype.importFile = function(srcFullPath, filenameHint, cb) {
pend.go(function(cb) {
tryMv(suggestedPath, cb);
});
pend.wait(cb);
pend.wait(function(err) {
if (err) return cb(err);
cb(null, newDbFile);
});
function tryMv(destRelPath, cb) {
var destFullPath = path.join(self.musicDirectory, destRelPath);
mv(srcFullPath, destFullPath, {mkdirp: true, clobber: false}, function(err) {
if (err && err.code === 'EEXIST') {
if (err) {
if (err.code === 'EEXIST') {
tryMv(uniqueFilename(destRelPath), cb);
} else {
@ -682,8 +691,14 @@ Player.prototype.importFile = function(srcFullPath, filenameHint, cb) {
return;
}
// in case it doesn't get picked up by a watcher
self.requestUpdateDb(path.dirname(destRelPath));
cb(err);
self.requestUpdateDb(path.dirname(destRelPath), false, function(err) {
if (err) return cb(err);
self.addQueue.waitForId(destRelPath, function(err) {
if (err) return cb(err);
newDbFile = self.dbFilesByPath[destRelPath];
cb();
});
});
});
}
});

View file

@ -68,9 +68,21 @@ PlayerServer.actions = {
},
'importUrl': {
permission: 'control',
args: 'string',
fn: function(self, client, urlString) {
self.player.importUrl(urlString);
args: 'object',
fn: function(self, client, args) {
var urlString = String(args.url);
var id = args.id;
self.player.importUrl(urlString, function(err, dbFile) {
var key = null;
if (err) {
console.error("Unable to import url:", urlString, "error:", err.stack);
} else if (!dbFile) {
console.error("Unable to import file due to race condition.");
} else {
key = dbFile.key;
}
client.sendMessage('importUrl', {id: id, key: key});
});
},
},
'subscribe': {

View file

@ -1,6 +1,5 @@
var $ = window.$;
var Handlebars = window.Handlebars;
var qq = window.qq;
var shuffle = require('mess');
var querystring = require('querystring');
@ -8,10 +7,10 @@ var zfill = require('zfill');
var PlayerClient = require('./playerclient');
var streaming = require('./streaming');
var Socket = require('./socket');
var uuid = require('uuid');
var dynamicModeOn = false;
var selection = {
ids: {
playlist: {},
@ -377,7 +376,8 @@ var localState = {
session_key: null,
scrobbling_on: false
},
authPassword: null
authPassword: null,
autoQueueUploads: true,
};
var $document = $(document);
var $window = $(window);
@ -396,7 +396,7 @@ var $nowplaying_elapsed = $nowplaying.find('.elapsed');
var $nowplaying_left = $nowplaying.find('.left');
var $vol_slider = $('#vol-slider');
var $settings = $('#settings');
var $upload_by_url = $('#upload-by-url');
var $uploadByUrl = $('#upload-by-url');
var $main_err_msg = $('#main-err-msg');
var $main_err_msg_text = $('#main-err-msg-text');
var $stored_playlists = $('#stored-playlists');
@ -404,6 +404,9 @@ var $upload = $('#upload');
var $track_display = $('#track-display');
var $lib_header = $('#library-pane .window-header');
var $pl_header = $pl_window.find('#playlist .header');
var $autoQueueUploads = $('#auto-queue-uploads');
var uploadInput = document.getElementById("upload-input");
var $uploadWidget = $("#upload-widget");
function saveLocalState(){
localStorage.setItem('state', JSON.stringify(localState));
@ -1165,7 +1168,7 @@ var keyboard_handlers = (function(){
shift: false,
handler: function(){
clickTab('upload');
$upload_by_url.focus().select();
$uploadByUrl.focus().select();
}
},
173: volumeDownHandler,
@ -1607,23 +1610,106 @@ function setUpTabsUi(){
}
}
function uploadFiles(files) {
if (files.length === 0) return;
var formData = new FormData();
for (var i = 0; i < files.length; i += 1) {
var file = files[i];
formData.append("file", file);
}
formData.append("autoQueue", localState.autoQueueUploads);
var $progressBar = $('<div></div>');
$progressBar.progressbar();
var $cancelBtn = $('<button>Cancel</button>');
$cancelBtn.on('click', onCancel);
$uploadWidget.append($progressBar);
$uploadWidget.append($cancelBtn);
var req = new XMLHttpRequest();
req.upload.addEventListener('progress', onProgress, false);
req.addEventListener('load', onLoad, false);
req.open('POST', '/upload');
req.send(formData);
uploadInput.value = null;
function onProgress(e) {
if (!e.lengthComputable) return;
var progress = e.loaded / e.total;
$progressBar.progressbar("option", "value", progress * 100);
}
function onLoad(e) {
if (localState.autoQueueUploads) {
var keys = JSON.parse(this.response);
player.queueTracks(keys);
}
cleanup();
}
function onCancel() {
req.abort();
cleanup();
}
function cleanup() {
$progressBar.remove();
$cancelBtn.remove();
}
}
function setAutoUploadBtnState() {
$autoQueueUploads
.button('option', 'label', localState.autoQueueUploads ? 'On' : 'Off')
.prop('checked', localState.autoQueueUploads)
.button('refresh');
}
function setUpUploadUi(){
var uploader;
uploader = new qq.FileUploader({
element: document.getElementById("upload-widget"),
action: '/upload',
encoding: 'multipart'
$autoQueueUploads.button({ label: "..." });
setAutoUploadBtnState();
$autoQueueUploads.on('click', function(event) {
var value = $(this).prop('checked');
localState.autoQueueUploads = value;
saveLocalState();
setAutoUploadBtnState();
});
$upload_by_url.on('keydown', function(event){
uploadInput.addEventListener('change', onChange, false);
function onChange(e) {
uploadFiles(this.files);
}
$uploadByUrl.on('keydown', function(event){
event.stopPropagation();
if (event.which === 27) {
$upload_by_url.val("").blur();
$uploadByUrl.val("").blur();
} else if (event.which === 13) {
var url = $upload_by_url.val();
$upload_by_url.val("").blur();
socket.send('importUrl', url);
importUrl();
}
});
function importUrl() {
var url = $uploadByUrl.val();
var id = uuid();
$uploadByUrl.val("").blur();
socket.on('importUrl', onImportUrl);
socket.send('importUrl', {
url: url,
id: id,
});
function onImportUrl(args) {
if (args.id !== id) return;
socket.removeListener('importUrl', onImportUrl);
if (localState.autoQueueUploads) {
player.queueTracks([args.key]);
}
}
}
}
function setUpSettingsUi(){

View file

@ -1,6 +1,5 @@
@import "vendor/reset.min.css"
@import "vendor/jquery-ui-1.10.4.custom.min.css"
@import "vendor/fileuploader.css"
user-select()
-moz-user-select arguments

View file

@ -1,31 +0,0 @@
.qq-uploader { position:relative; width: 100%;}
.qq-upload-button {
display:block; /* or inline-block */
width: 105px; padding: 7px 0; text-align:center;
background:#880000; border-bottom:1px solid #ddd;color:#fff;
}
.qq-upload-button-hover {background:#cc0000;}
.qq-upload-button-focus {outline:1px dotted black;}
.qq-upload-drop-area {
position:absolute; top:0; left:0; width:100%; height:100%; min-height: 70px; z-index:2;
background:#FF9797; text-align:center;
}
.qq-upload-drop-area span {
display:block; position:absolute; top: 50%; width:100%; margin-top:-8px; font-size:16px;
}
.qq-upload-drop-area-active {background:#FF7171;}
.qq-upload-list {margin:15px 35px; padding:0; list-style:disc;}
.qq-upload-list li { margin:0; padding:0; line-height:15px; font-size:12px;}
.qq-upload-file, .qq-upload-spinner, .qq-upload-size, .qq-upload-cancel, .qq-upload-failed-text {
margin-right: 7px;
}
.qq-upload-file {}
.qq-upload-spinner {display:inline-block; background: url("loading.gif"); width:15px; height:15px; vertical-align:text-bottom;}
.qq-upload-size,.qq-upload-cancel {font-size:11px;}
.qq-upload-failed-text {display:none;}
.qq-upload-fail .qq-upload-failed-text {display:inline;}

View file

@ -57,7 +57,12 @@
<div id="upload-pane" class="ui-widget-content ui-corner-all" style="display: none">
<div id="upload">
<input id="upload-by-url" type="text" placeholder="Paste URL here">
<div id="upload-widget"></div>
<div id="upload-widget">
<input type="file" id="upload-input" multiple="multiple" placeholder="Drag and drop or click to browse">
</div>
<div>
Automatically queue uploads: <input type="checkbox" id="auto-queue-uploads"><label for="auto-queue-uploads">On</label>
</div>
</div>
</div>
<div id="settings-pane" class="ui-widget-content ui-corner-all" style="display: none">
@ -93,7 +98,6 @@
</div>
<script src="vendor/jquery-2.1.0.min.js"></script>
<script src="vendor/jquery-ui-1.10.4.custom.min.js"></script>
<script src="vendor/fileuploader/fileuploader.js"></script>
<script src="vendor/handlebars.runtime.js"></script>
<script src="views.js"></script>
<script src="app.js"></script>

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 B