diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index 602c9cfe..a793ffb0 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -1,6 +1,10 @@ var Logger = require("./logger"); var Config = require("./config"); 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; @@ -21,6 +25,50 @@ var audioOnlyContainers = { "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) { var lines = buf.split("\n"); var tmp = { tags: {} }; @@ -149,64 +197,69 @@ exports.query = function (filename, cb) { "or HTTPS"); } - exports.ffprobe(filename, function (err, data) { + testUrl(filename, function (err) { 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 ffmpeg"); + return cb(err); + } - var m = err.message.match(/(http error .*)/i); - if (m) return cb(m[1]); + exports.ffprobe(filename, function (err, data) { + 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"); + } + + 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 { - Logger.errlog.log(err.stack || err); - return cb("Unable to query file data with ffmpeg"); + 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."); } - } - - 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."); - } + }); }); }; diff --git a/lib/status-messages.js b/lib/status-messages.js new file mode 100644 index 00000000..ae6a0142 --- /dev/null +++ b/lib/status-messages.js @@ -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" +};