Merge pull request #483 from calzoneman/ffmpeg-preflight

Preflight ffprobe requests with node to check headers/error conditions
This commit is contained in:
Calvin Montgomery 2015-05-25 16:08:43 -04:00
commit a4cd0659b6
2 changed files with 189 additions and 53 deletions

View file

@ -1,6 +1,10 @@
var Logger = require("./logger"); var Logger = require("./logger");
var Config = require("./config"); var Config = require("./config");
var spawn = require("child_process").spawn; var spawn = require("child_process").spawn;
var https = require("https");
var http = require("http");
var urlparse = require("url");
var statusMessages = require("./status-messages");
var USE_JSON = true; var USE_JSON = true;
@ -21,6 +25,50 @@ var audioOnlyContainers = {
"mp3": true "mp3": true
}; };
function testUrl(url, cb, redirCount) {
if (!redirCount) redirCount = 0;
var data = urlparse.parse(url);
if (!/https?:/.test(data.protocol)) {
return cb("Video links must start with http:// or https://");
}
if (!data.hostname) {
return cb("Invalid link");
}
var transport = (data.protocol === "https:") ? https : http;
data.method = "HEAD";
var req = transport.request(data, function (res) {
req.abort();
if (res.statusCode === 301 || res.statusCode === 302) {
if (redirCount > 2) {
return cb("Too many redirects. Please provide a direct link to the " +
"file");
}
return testUrl(res.headers['location'], cb, redirCount + 1);
}
if (res.statusCode !== 200) {
var message = statusMessages[res.statusCode];
if (!message) message = "";
return cb("HTTP " + res.statusCode + " " + message);
}
if (!/^audio|^video/.test(res.headers['content-type'])) {
return cb("Server did not return an audio or video file");
}
cb();
});
req.on("error", function (err) {
cb(err);
});
req.end();
}
function readOldFormat(buf) { function readOldFormat(buf) {
var lines = buf.split("\n"); var lines = buf.split("\n");
var tmp = { tags: {} }; var tmp = { tags: {} };
@ -149,64 +197,69 @@ exports.query = function (filename, cb) {
"or HTTPS"); "or HTTPS");
} }
exports.ffprobe(filename, function (err, data) { testUrl(filename, function (err) {
if (err) { if (err) {
if (err.code && err.code === "ENOENT") { return cb(err);
return cb("Failed to execute `ffprobe`. Set ffmpeg.ffprobe-exec to " + }
"the correct name of the executable in config.yaml. If " +
"you are using Debian or Ubuntu, it is probably avprobe.");
} else if (err.message) {
if (err.message.match(/protocol not found/i))
return cb("Link uses a protocol unsupported by this server's ffmpeg");
var m = err.message.match(/(http error .*)/i); exports.ffprobe(filename, function (err, data) {
if (m) return cb(m[1]); if (err) {
if (err.code && err.code === "ENOENT") {
return cb("Failed to execute `ffprobe`. Set ffmpeg.ffprobe-exec " +
"to the correct name of the executable in config.yaml. " +
"If you are using Debian or Ubuntu, it is probably " +
"avprobe.");
} else if (err.message) {
if (err.message.match(/protocol not found/i))
return cb("Link uses a protocol unsupported by this server's " +
"version of ffmpeg");
Logger.errlog.log(err.stack || err); Logger.errlog.log(err.stack || err);
return cb("Unable to query file data with ffmpeg");
} else {
Logger.errlog.log(err.stack || err);
return cb("Unable to query file data with ffmpeg");
}
}
try {
data = reformatData(data);
} catch (e) {
Logger.errlog.log(e.stack || e);
return cb("Unable to query file data with ffmpeg"); return cb("Unable to query file data with ffmpeg");
}
if (data.medium === "video") {
if (!acceptedCodecs.hasOwnProperty(data.type)) {
return cb("Unsupported video codec " + data.type);
}
data = {
title: data.title || "Raw Video",
duration: data.duration,
bitrate: data.bitrate,
codec: data.type
};
cb(null, data);
} else if (data.medium === "audio") {
if (!acceptedAudioCodecs.hasOwnProperty(data.acodec)) {
return cb("Unsupported audio codec " + data.acodec);
}
data = {
title: data.title || "Raw Audio",
duration: data.duration,
bitrate: data.bitrate,
codec: data.acodec
};
cb(null, data);
} else { } else {
Logger.errlog.log(err.stack || err); return cb("Parsed metadata did not contain a valid video or audio " +
return cb("Unable to query file data with ffmpeg"); "stream. Either the file is invalid or it has a format " +
"unsupported by this server's version of ffmpeg.");
} }
} });
try {
data = reformatData(data);
} catch (e) {
Logger.errlog.log(e.stack || e);
return cb("Unable to query file data with ffmpeg");
}
if (data.medium === "video") {
if (!acceptedCodecs.hasOwnProperty(data.type)) {
return cb("Unsupported video codec " + data.type);
}
data = {
title: data.title || "Raw Video",
duration: data.duration,
bitrate: data.bitrate,
codec: data.type
};
cb(null, data);
} else if (data.medium === "audio") {
if (!acceptedAudioCodecs.hasOwnProperty(data.acodec)) {
return cb("Unsupported audio codec " + data.acodec);
}
data = {
title: data.title || "Raw Audio",
duration: data.duration,
bitrate: data.bitrate,
codec: data.acodec
};
cb(null, data);
} else {
return cb("Parsed metadata did not contain a valid video or audio stream. " +
"Either the file is invalid or it has a format unsupported by " +
"this server's version of ffmpeg.");
}
}); });
}; };

83
lib/status-messages.js Normal file
View file

@ -0,0 +1,83 @@
// This status message map is taken from the node.js source code. The original
// copyright notice for lib/_http_server.js is reproduced below.
//
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
module.exports = {
100: "Continue",
101: "Switching Protocols",
102: "Processing",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
207: "Multi-Status",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Moved Temporarily",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
307: "Temporary Redirect",
308: "Permanent Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Time-out",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Request Entity Too Large",
414: "Request-URI Too Large",
415: "Unsupported Media Type",
416: "Requested Range Not Satisfiable",
417: "Expectation Failed",
418: "I'm a teapot",
422: "Unprocessable Entity",
423: "Locked",
424: "Failed Dependency",
425: "Unordered Collection",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Time-out",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
509: "Bandwidth Limit Exceeded",
510: "Not Extended",
511: "Network Authentication Required"
};