From c4add8f14211d884ea11900f19bf31fb39026e8a Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 24 May 2015 11:06:02 -0400 Subject: [PATCH 1/6] Preflight raw file requests to get better error messages --- lib/ffmpeg.js | 155 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 53 deletions(-) diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index 602c9cfe..e81a7345 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -1,6 +1,9 @@ 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 USE_JSON = true; @@ -21,6 +24,47 @@ var audioOnlyContainers = { "mp3": true }; +function testUrl(url, cb, redirected) { + 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 (redirected) { + return cb("Too many redirects. Please provide a direct link to the " + "file"); + } + return testUrl(res.headers['location'], cb, true); + } + + if (res.statusCode !== 200) { + return cb("HTTP " + res.statusCode + " " + res.statusMessage); + } + + 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 +193,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."); - } + }); }); }; From 334c0d933be12f5aac56948a47ccbfebc1c806cd Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 24 May 2015 11:09:56 -0400 Subject: [PATCH 2/6] Fix typo --- lib/ffmpeg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index e81a7345..ce16cad6 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -41,7 +41,7 @@ function testUrl(url, cb, redirected) { if (res.statusCode === 301 || res.statusCode === 302) { if (redirected) { - return cb("Too many redirects. Please provide a direct link to the " + return cb("Too many redirects. Please provide a direct link to the " + "file"); } return testUrl(res.headers['location'], cb, true); From 18199b32ad5dc9b62440a14d0c50af7db5139fb3 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 24 May 2015 11:19:59 -0400 Subject: [PATCH 3/6] Add status message map for pre-node v0.12 servers --- lib/ffmpeg.js | 5 +++- lib/status-messages.js | 59 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 lib/status-messages.js diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index ce16cad6..ee6d664c 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -4,6 +4,7 @@ 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; @@ -48,7 +49,9 @@ function testUrl(url, cb, redirected) { } if (res.statusCode !== 200) { - return cb("HTTP " + res.statusCode + " " + res.statusMessage); + var message = statusMessages[res.statusCode]; + if (!message) message = ""; + return cb("HTTP " + res.statusCode + " " + message); } if (!/^audio|^video/.test(res.headers['content-type'])) { diff --git a/lib/status-messages.js b/lib/status-messages.js new file mode 100644 index 00000000..1df4969b --- /dev/null +++ b/lib/status-messages.js @@ -0,0 +1,59 @@ +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' +}; From 241db797d3dff024979aa4ed1f1a7d4d8febc9c0 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 24 May 2015 11:22:08 -0400 Subject: [PATCH 4/6] Add node comment to status-messages.js --- lib/status-messages.js | 138 ++++++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 57 deletions(-) diff --git a/lib/status-messages.js b/lib/status-messages.js index 1df4969b..193b1cc9 100644 --- a/lib/status-messages.js +++ b/lib/status-messages.js @@ -1,59 +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' + 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" }; From 9b3a71d84f4e68e49cff9908e5141b48d032d082 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 24 May 2015 11:23:43 -0400 Subject: [PATCH 5/6] Fix typo --- lib/status-messages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/status-messages.js b/lib/status-messages.js index 193b1cc9..ae6a0142 100644 --- a/lib/status-messages.js +++ b/lib/status-messages.js @@ -60,7 +60,7 @@ module.exports = { 415: "Unsupported Media Type", 416: "Requested Range Not Satisfiable", 417: "Expectation Failed", - 418: "I\"m a teapot", + 418: "I'm a teapot", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", From a81f691d4ec09d3193f8abeea7a5db44e23ee251 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Mon, 25 May 2015 16:04:27 -0400 Subject: [PATCH 6/6] Allow 2 redirects --- lib/ffmpeg.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index ee6d664c..a793ffb0 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -25,7 +25,8 @@ var audioOnlyContainers = { "mp3": true }; -function testUrl(url, cb, redirected) { +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://"); @@ -41,11 +42,11 @@ function testUrl(url, cb, redirected) { req.abort(); if (res.statusCode === 301 || res.statusCode === 302) { - if (redirected) { + if (redirCount > 2) { return cb("Too many redirects. Please provide a direct link to the " + "file"); } - return testUrl(res.headers['location'], cb, true); + return testUrl(res.headers['location'], cb, redirCount + 1); } if (res.statusCode !== 200) {