├── .gitignore ├── index.js ├── isAbsolute.js ├── mkdirpAsync.js ├── resolvePath.js ├── isVideoFile.js ├── findVideoFiles.js ├── cli ├── banner.js └── index.js ├── package.json ├── LICENSE ├── checkIntegrity.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | test.js 2 | corrupt.mp4 3 | valid.mp4 4 | logs/ 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const checkIntegrity = require('./checkIntegrity') 2 | 3 | module.exports = Object.freeze(Object.assign((...args)=>checkIntegrity(...args), {checkIntegrity})) 4 | -------------------------------------------------------------------------------- /isAbsolute.js: -------------------------------------------------------------------------------- 1 | const {normalize, resolve} = require('path') 2 | 3 | function isAbsolute(path) { 4 | return normalize(path + '/') === normalize(resolve(path) + '/'); 5 | } 6 | 7 | module.exports = isAbsolute 8 | -------------------------------------------------------------------------------- /mkdirpAsync.js: -------------------------------------------------------------------------------- 1 | const mkdirp = require('mkdirp') 2 | 3 | function mkdirpAsync(...args) { 4 | return new Promise((resolve, reject) => { 5 | mkdirp(...args, err => { 6 | if (err) return reject(err) 7 | resolve() 8 | }) 9 | }) 10 | } 11 | 12 | module.exports = mkdirpAsync 13 | -------------------------------------------------------------------------------- /resolvePath.js: -------------------------------------------------------------------------------- 1 | const isAbsolute = require('./isAbsolute') 2 | const {join} = require('path') 3 | 4 | function resolvePath(path, workingDirectory) { 5 | if (isAbsolute(path)) { 6 | return path 7 | } else { 8 | return join(workingDirectory, path) 9 | } 10 | } 11 | 12 | module.exports = resolvePath 13 | -------------------------------------------------------------------------------- /isVideoFile.js: -------------------------------------------------------------------------------- 1 | const {extname} = require('path') 2 | const knownVideoExtensions = [".webm",".mkv",".flv",".flv",".vob",".ogv",".ogg",".drc",".gif",".gifv",".mng",".avi",".mts",".m2ts",".mov",".qt",".wmv",".yuv",".rm",".rmvb",".asf",".amv",".mp4",".m4p",".m4v",".mpg",".mp2",".mpeg",".mpe",".mpv",".mpg",".mpeg",".m2v",".m4v",".svi",".3gp",".3g2",".mxf",".roq",".nsv",".flv",".f4v",".f4p",".f4a",".f4b"] 3 | 4 | function isVideoFile(path) { 5 | const extension = extname(path) 6 | if (!extension) return false 7 | return knownVideoExtensions.includes(extension.toLowerCase()) 8 | } 9 | 10 | module.exports = isVideoFile 11 | -------------------------------------------------------------------------------- /findVideoFiles.js: -------------------------------------------------------------------------------- 1 | const dir = require('node-dir') 2 | const {readdir} = require('mz/fs') 3 | const pathType = require('path-type') 4 | const isVideoFile = require('./isVideoFile') 5 | const {join} = require('path') 6 | 7 | async function findVideoFiles(directory, recursive=false) { 8 | let files = await readdir(directory) 9 | files = await Promise.all(files.map(async file => { 10 | file = join(directory, file) 11 | if (await pathType.dir(file)) { 12 | if (recursive === false) return [] 13 | return findVideoFiles(file, recursive) 14 | } else if (await pathType.file(file)) { 15 | return [file] 16 | } else { 17 | return [] 18 | } 19 | })) 20 | files = [].concat.apply([], files) 21 | return files.filter(isVideoFile) 22 | } 23 | 24 | module.exports = findVideoFiles 25 | -------------------------------------------------------------------------------- /cli/banner.js: -------------------------------------------------------------------------------- 1 | require('colors') 2 | 3 | const banner = (` 4 | ███ █▄ ███▄▄▄▄ ███ ▄████████ ▄██████▄ ▄████████ ▄█ ███ ▄██ ▄ 5 | ███ ███ ███▀▀▀██▄ ▀█████████▄ ███ ███ ███ ███ ███ ███ ███ ▀█████████▄ ███ ██▄ 6 | ███ ███ ███ ███ ▀███▀▀██ ███ █▀ ███ █▀ ███ ███ ███▌ ▀███▀▀██ ███▄▄▄███ 7 | ███ ███ ███ ███ ███ ▀ ▄███▄▄▄ ▄███ ▄███▄▄▄▄██▀ ███▌ ███ ▀ ▀▀▀▀▀▀███ 8 | ███ ███ ███ ███ ███ ▀▀███▀▀▀ ▀▀███ ████▄ ▀▀███▀▀▀▀▀ ███▌ ███ ▄██ ███ 9 | ███ ███ ███ ███ ███ ███ █▄ ███ ███ ▀███████████ ███ ███ ███ ███ 10 | ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ 11 | ████████▀ ▀█ █▀ ▄████▀ ██████████ ████████▀ ███ ███ █▀ ▄████▀ ▀█████▀ 12 | ███ ███ 13 | `) 14 | 15 | module.exports = banner 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "untegrity", 3 | "version": "1.0.7", 4 | "description": "Discipline videos with poor integrity", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "bin": { 10 | "untegrity": "cli/index.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/L1lith/Untegrity.git" 15 | }, 16 | "keywords": [ 17 | "video", 18 | "integrity", 19 | "check", 20 | "checker", 21 | "validate", 22 | "validator", 23 | "ffmpeg" 24 | ], 25 | "author": "L1lith", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/L1lith/Untegrity/issues" 29 | }, 30 | "homepage": "https://github.com/L1lith/Untegrity#readme", 31 | "dependencies": { 32 | "colors": "^1.3.2", 33 | "mkdirp": "^0.5.1", 34 | "mz": "^2.7.0", 35 | "nanoid": "^2.0.0", 36 | "node-dir": "^0.1.17", 37 | "path-type": "^3.0.0", 38 | "pm2": "^3.2.2", 39 | "rimraf": "^2.6.2", 40 | "yargs": "^12.0.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 L1lith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /checkIntegrity.js: -------------------------------------------------------------------------------- 1 | const {spawn} = require('child_process') 2 | const {join} = require('path') 3 | const nanoid = require('nanoid') 4 | const mkdirpAsync = require('./mkdirpAsync') 5 | const {readFile, unlink} = require('mz/fs') 6 | 7 | const logsDirectory = join(__dirname, 'logs') 8 | 9 | try { // Remove Leftover Logs from early termination just to prevent wasted hard drive space 10 | require('rimraf')(logsDirectory) 11 | } catch(error) {} 12 | 13 | let firstLaunch = true 14 | 15 | async function checkIntegrity(videoPath, options) { 16 | const {audioMode=false, returnErrors=false} = options || {} 17 | if (firstLaunch) { 18 | await mkdirpAsync(logsDirectory) 19 | firstLaunch = false 20 | } 21 | const errors = await runFFMPEGCheck(videoPath, {audioMode}) 22 | //console.log('hi', errors) 23 | //await (new Promise(res => setTimeout(res, 100000))) 24 | // await new Promise((resolve, reject) => { 25 | // exec(`ffmpeg -v error -i "${videoPath}"${audioMode === true ? ' -map 0:1' : ''} -f null - >"${errorLogPath}" 2>&1`, ((err, stdout, stderr) => { 26 | // if (err) return reject(err) 27 | // if (stderr) console.log(stderr) 28 | // //if (stdout) console.log('stdout') 29 | // resolve() 30 | // })) 31 | // }) 32 | const valid = errors.length === 0 33 | if (returnErrors === true) { 34 | return errors 35 | } 36 | return valid 37 | } 38 | 39 | function runFFMPEGCheck(videoPath, options) { 40 | return new Promise((resolve, reject) => { 41 | const {audioMode=false} = options || {} 42 | const errorLogPath = join(logsDirectory, nanoid() + '.log') 43 | const process = spawn('ffmpeg', `-v error -i "${videoPath}"${audioMode === true ? ' -map 0:1' : ''} -f null - >"${errorLogPath}" 2>&1`.split(' '), {shell: true}) 44 | process.on('close', code => { 45 | if (code === 0) { 46 | readFile(errorLogPath).then(errorBuffer => { 47 | const errors = errorBuffer.toString().split('\n').filter(line => line.trim().length > 0) 48 | unlink(errorLogPath).then(()=>{ 49 | console.log(errors.join('\n')) 50 | resolve(errors) 51 | }).catch(reject) 52 | }).catch(reject) 53 | } else { 54 | console.log("Fatal FFMPEG Error") 55 | unlink(errorLogPath).then(()=>{ 56 | resolve(['Fatal FFMPEG Error']) 57 | }).catch(reject) 58 | } 59 | }) 60 | }) 61 | } 62 | 63 | module.exports = checkIntegrity 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Untegrity 2 | Command line tool & node package for detecting and optionally removing corrupt videos one at a time or in mass. 3 | ## Installation 4 | First install FFMPEG ([Found Here](http://ffmpeg.zeranoe.com/builds/)) and ensure it's path is set in your environment variables. You can test this by simply running the command `ffmpeg` 5 | 6 | For usage from within a Node.js project 7 | ``` 8 | npm i -s untegrity 9 | ``` 10 | For command line usage 11 | ``` 12 | npm i -g untegrity 13 | ``` 14 | 15 | ## Command Line Basics 16 | Commands are formatted like this 17 | ``` 18 | untegrity path [...options] 19 | ``` 20 | We can check the integrity of a given video file like this, also it can accept folders instead. 21 | ``` 22 | untegrity ./video.mp4 23 | ``` 24 | Here's an example of recursively searching through directories and deleting the corrupt video files using audio content validation 25 | ``` 26 | untegrity . -dra 27 | ``` 28 | 29 | ## Node.js Basics 30 | We can check the integrity of a video like this 31 | ``` 32 | const {checkIntegrity} = require('untegrity') 33 | const join = require('join') 34 | 35 | async function run() { 36 | const valid = await checkIntegrity(join(__dirname, 'video.mp4')) 37 | if (valid) { 38 | console.log("Video Valid!) 39 | } else { 40 | console.log("Video Invalid :(") 41 | } 42 | } 43 | 44 | run().catch(console.log) 45 | ``` 46 | ## Command Line Arguments 47 | 48 | *Usage* 49 | ``` 50 | untegrity videoPath [-t --type | --type] [-r | --recursive] [-d | --rm | --del | --remove | --delete] [-a | --audio | --audioMode] [--noBanner] 51 | ``` 52 | | Flag | Description | 53 | |------|----------------------------------------------------------------------------------------------| 54 | | -r | If doing a folder search check subdirectories recursively | 55 | | -t | Specify either a video or folder path type to prevent unwanted behavior | 56 | | -d | Delete corrupt videos found. | 57 | | -a | Run in Audio Mode (Videos validated via audio tracks, potentially inaccurate but much faster)| 58 | | --noBanner | If you don't love beautiful ascii art ;( | 59 | 60 | ## Node.js Methods 61 | ### checkIntegrity 62 | Checks the integrity of a single video 63 | 64 | Usage 65 | ``` 66 | const {checkIntegrity} = require('untegrity') 67 | const {join} = require('path') 68 | 69 | checkIntegrity(join(__dirname, './movie.mp4')).then(valid => { 70 | if (valid) { 71 | console.log("Video Valid") 72 | } else { 73 | console.log("Video Invalid") 74 | } 75 | }).catch(console.log) 76 | ``` 77 | -------------------------------------------------------------------------------- /cli/index.js: -------------------------------------------------------------------------------- 1 | const args = require('yargs').argv 2 | const pathType = require('path-type') 3 | const resolvePath = require('../resolvePath') 4 | const findVideoFiles = require('../findVideoFiles') 5 | const checkIntegrity = require('../checkIntegrity') 6 | const {unlink} = require('mz/fs') 7 | const colors = require('colors') 8 | const banner = require('./banner') 9 | 10 | async function run() { 11 | if (args._.length > 1) throw 'Too many arguments' 12 | let path = args._[0] || args.path || args.p 13 | 14 | if (typeof path != 'string' || path.length < 1) throw "Must supply path string" 15 | let audioMode = args.audioMode || args.audio || args.a 16 | if (audioMode !== null && ['a', 'audio', 'audioMode'].some(property => args.hasOwnProperty(property)) && typeof audioMode != 'boolean') throw "The audio mode argument must be true/false" 17 | let type = args.type || args.t 18 | if (['dir', 'directory'].includes(type)) type = 'folder' 19 | if (type !== null && (args.hasOwnProperty('t') || args.hasOwnProperty('type')) && !["file", "folder"].includes(type)) throw "The type argument must be file or folder" 20 | const recursive = args.recursive || args.r 21 | if (recursive !== null && (args.hasOwnProperty('r') || args.hasOwnProperty('recursive')) && typeof recursive != 'boolean') throw "The recursive argument must be a boolean" 22 | if (type === 'file' && recursive === true) throw "Cannot run recursively while in file mode" 23 | const remove = false || args.remove || args.delete || args.rm || args.del || args.d 24 | if (remove !== null && ['rm', 'remove', 'del', 'delete'].some(property => args.hasOwnProperty(property)) && typeof remove != 'boolean') throw "The remove argument must be a boolean" 25 | const noBanner = false || args.noBanner 26 | if (args.hasOwnProperty('noBanner') && typeof args.noBanner != 'boolean') throw new Error("noBanner argument must be a boolean") 27 | path = resolvePath(path, process.cwd()) 28 | 29 | if (noBanner !== true) console.log(banner) 30 | if (audioMode === true) console.log(colors.cyan("Running in Audio Mode")) 31 | 32 | if (await pathType.file(path)) { 33 | if (type === 'folder') throw 'The supplied path is a file not a folder' 34 | console.log("Checking Video File") 35 | await checkVideoFile(path, {audioMode, remove}) 36 | } else if (await pathType.dir(path)) { 37 | if (type === 'file') throw 'The supplied path is a folder not a file' 38 | console.log("Beginning Directory Scan") 39 | const videoFiles = await findVideoFiles(path, recursive) 40 | if (videoFiles.length < 1) throw "No video files found" 41 | console.log(`Found ${videoFiles.length} video files, beginning individual scans`) 42 | for (let i = 0; i < videoFiles.length; i++) { 43 | const videoFile = videoFiles[i] 44 | console.log(`Scanning video file #${i + 1} at ${videoFile}`.cyan) 45 | await checkVideoFile(videoFile, {audioMode, remove}) 46 | } 47 | } else { 48 | throw "Path not Found." 49 | } 50 | } 51 | 52 | async function checkVideoFile(path, {audioMode, remove}) { 53 | const errors = await checkIntegrity(path, {returnErrors: true, audioMode}) 54 | if (errors.length < 1) { 55 | console.log("No errors found".green) 56 | } else { 57 | console.log("== ".red+errors.length + " errors found".red) 58 | if (remove === true) { 59 | console.log("Deleting Video File") 60 | await unlink(path) 61 | } 62 | } 63 | } 64 | 65 | run().catch(err => { 66 | if (typeof err == 'string') { 67 | console.log(("Error: " + err).red) 68 | } else { 69 | console.log(err) 70 | } 71 | process.exit(1) 72 | }) 73 | --------------------------------------------------------------------------------