upload improvements. closes #45
* ditch qq file uploader in favor of native xhr2 * implement client-side optional auto-queuing
This commit is contained in:
parent
1a7dc0e0a3
commit
3be4ad9cb6
10 changed files with 194 additions and 1347 deletions
|
|
@ -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 = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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': {
|
||||
|
|
|
|||
|
|
@ -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(){
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
31
src/client/styles/vendor/fileuploader.css
vendored
31
src/client/styles/vendor/fileuploader.css
vendored
|
|
@ -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;}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
1276
src/public/vendor/fileuploader/fileuploader.js
vendored
1276
src/public/vendor/fileuploader/fileuploader.js
vendored
File diff suppressed because it is too large
Load diff
BIN
src/public/vendor/fileuploader/loading.gif
vendored
BIN
src/public/vendor/fileuploader/loading.gif
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 455 B |
Loading…
Reference in a new issue