├── .gitignore ├── LICENSE.md ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test └── basic.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ivo Georgiev 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # video-name-parser 2 | Parse names of video files to identify quality (e.g. 1080p), season/episode, year, etc. 3 | 4 | # Usage 5 | ```javascript 6 | var parseVideo = require("video-name-parser"); 7 | parseVideo("south park s01e01.avi"); // { name: "south park", season: 1, episode: [1], type: "series", tag: [] } 8 | parseVideo("south park s01e01e02.avi"); // { name: "south park", season: 1, episode: [1,2], type: "series", tag: [] } 9 | parseVideo("The.Wizard.of.Oz.1939.1080p.BrRip.x264.mp4"); // { name: "the wizard of oz", year: 1939, type: "movie", tag: [ "hd", "1080p" ] } 10 | parseVideo("something else.mp4"); // { type: "other" } // no year or season/ep found, assuming 'other' 11 | ``` 12 | 13 | ## Returned properties 14 | 15 | **name** - parsed name, in lower case, allowed numbers/letters, no special symbols 16 | 17 | **type** - can be `movie`, `series` or `other` - inferred from keywords / key phrases 18 | 19 | **tag** - additional tags inferred from the name, e.g. `1080p` 20 | 21 | **season** - number of the season 22 | 23 | **episode** - _array_ of episode numbers, returned for episodes 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Constants 3 | * */ 4 | var maxSegments = 3; 5 | 6 | var movieKeywords = ["1080p", "720p", "480p", "blurayrip", "brrip", "divx", "dvdrip", "hdrip", "hdtv", "tvrip", "xvid", "camrip"]; 7 | 8 | // Excluded is an object we use to exclude those keywords from consideration for detecting strings like "season X" 9 | var excluded = { }; 10 | movieKeywords.forEach(function(x) { excluded[x] = 1 }); 11 | 12 | var SEGMENTS_SPLIT = /\.| |-|;|_/g; 13 | var MATCH_FILES = /.mp4$|.mkv$|.avi$/; 14 | var minYear = 1900, maxYear = 2030; 15 | 16 | /* 17 | * TWO REFERENCES 18 | * * http://wiki.xbmc.org/index.php?title=Adding_videos_to_the_library/Naming_files/TV_shows 19 | * * http://wiki.xbmc.org/index.php?title=Advancedsettings.xml#.3Ctvshowmatching.3E 20 | */ 21 | 22 | 23 | function simplifyName(n) { 24 | return n.toLowerCase() 25 | .trim() 26 | .replace(/\([^\(]+\)$/, "") // remove brackets at end 27 | .replace(/&/g, "and") 28 | .replace(/[^0-9a-z ]+/g, " ") // remove special chars 29 | .split(" ").filter(function(r){return r}).join(" ") 30 | }; 31 | 32 | function parseVideoName(filePath, options) 33 | { 34 | //if (! filePath.match(MATCH_FILES)) return { type: "other" }; 35 | 36 | var options = options || {}; 37 | var meta = {}; 38 | 39 | var segments = filePath 40 | .replace(/\\/g, "/") // Support Windows slashes, lol 41 | .split("/") 42 | .reverse() 43 | .filter(function(x) { return x }) 44 | .slice(0, maxSegments); 45 | var firstNameSplit = segments[0].split(/\.| |_/); 46 | 47 | /* 48 | * Helpers to validate if we have found the proper metadata 49 | */ 50 | function saneSeason() 51 | { 52 | return meta.hasOwnProperty("season") && !isNaN(meta.season) 53 | }; 54 | function saneEpisode() 55 | { 56 | return Array.isArray(meta.episode) && meta.episode.length 57 | }; 58 | 59 | /* 60 | * Test for a year in the name 61 | * */ 62 | [segments[0], segments[1]].filter(function(x){return x}).forEach(function(seg) 63 | { 64 | var regex = /\b\d{4}\b/g 65 | var matches 66 | 67 | while (matches = regex.exec(seg)) 68 | { 69 | var number = parseInt(matches[0], 10); 70 | if (number >= minYear && number <= maxYear) 71 | meta.year = number 72 | } 73 | }); 74 | 75 | /* 76 | * Test for "aired" stamp; if aired stamp is there, we have a series 77 | */ 78 | var pad = function(x) { return ("00"+x).slice(-2) }; 79 | [segments[0], segments[1]].filter(function(x){return x}).forEach(function(seg) 80 | { 81 | var aired = seg.match(/(\d\d\d\d)(\.|-| )(\d\d)(\.|-| )(\d\d)/); 82 | if (aired && aired[1]) 83 | { 84 | var year = parseInt(aired[1], 10); 85 | if (year >= minYear && year <= maxYear) 86 | meta.aired = [year, pad(aired[3]), pad(aired[5])].join("-"); 87 | } 88 | }); 89 | 90 | /* 91 | * A typical pattern - "s05e12", "S01E01", etc. ; can be only "E01" 92 | * Those are matched only in the file name 93 | * 94 | * TODO: this stamp may be in another segment (e.g. directory name) 95 | * */ 96 | [segments[0], segments[1]].forEach(function(seg) { 97 | if (seg) seg.split(/\.| |_/).forEach(function(x, i) 98 | { 99 | /* 100 | * Card type one 101 | */ 102 | var seasonMatch = x.match(/S(\d{1,2})/i); /* TODO: bug: uc-css4cd2.avi is a false positive */ 103 | if (seasonMatch) 104 | meta.season = parseInt(seasonMatch[1], 10); 105 | 106 | /* TODO: consider the case when a hyphen is used for multiple episodes ; e.g. e1-3 */ 107 | var episodeMatch = x.match(/(?<=\W|\d)E(\d{2})/ig); 108 | if (episodeMatch) 109 | meta.episode = episodeMatch.map(function(y) { return parseInt(y.slice(1), 10) }); 110 | 111 | /* 112 | * 01x20 113 | */ 114 | var xStampMatch = x.match(/(\d\d?)x(\d\d?)/i); 115 | if (xStampMatch) 116 | { 117 | meta.season = parseInt(xStampMatch[1], 10); 118 | meta.episode = [ parseInt(xStampMatch[2], 10) ]; 119 | } 120 | // if (otherCardMatch) 121 | }); 122 | 123 | /* Extract name from this match */ 124 | var fullMatch = seg && seg.replace(/\.| |;|_/g, " ").match(/^([a-zA-Z0-9,-?!'& ]*) S(\d{1,2})E(\d{2})/i); 125 | if (!meta.name && meta.season && meta.episode && fullMatch && fullMatch[1]) meta.name = fullMatch[1]; 126 | // TODO: consider not going through simplifyName 127 | }); 128 | 129 | /* 130 | * This stamp must be tested before splitting (by dot) 131 | * a pattern of the kind [4.02] 132 | * This pattern should be arround characters which are not digits and not letters 133 | * */ 134 | 135 | var dotStampMatch = segments[0].match(/[^\da-zA-Z](\d\d?)\.(\d\d?)[^\da-zA-Z]/i); 136 | if (! (saneSeason() && saneEpisode()) && dotStampMatch && !meta.year) // exclude meta.year to avoid confusing movie matches - especially if they contain 5.1 for suround sound 137 | { 138 | meta.season = parseInt(dotStampMatch[1], 10); 139 | meta.episode = [ parseInt(dotStampMatch[2], 10) ]; 140 | } 141 | 142 | /* 143 | * A stamp of the style "804", meaning season 8, episode 4 144 | * */ 145 | if (!(saneSeason() && saneEpisode()) && !options.strict) 146 | { 147 | var stamp = firstNameSplit 148 | .reverse() /* search from the end */ 149 | .map(function(x) 150 | { 151 | if (x.match(new RegExp("\\d\\d\\d\\d?e"))) x = x.slice(0, -1); /* This is a weird case, but I've seen it: dexter.801e.720p.x264-kyr.mkv */ 152 | if (x.match(new RegExp("s\\d\\d\\d\\d?"))) x = x.slice(1); /* I've never seen this, but it might happen */ 153 | return x; 154 | }) 155 | .filter(function(x) 156 | { 157 | return !isNaN(x) && (x.length == 3 || (!meta.year && x.length == 4)) /* 4-digit only allowed if this has not been identified as a year */ 158 | })[0]; /* Notice how always the first match is choosen ; the second might be a part of the episode name (e.g. "Southpark - 102 - weight gain 4000"); 159 | * that presumes episode number/stamp comes before the name, which is where most human beings would put it */ 160 | 161 | /* Since this one is risky, do it only if we haven't matched a year (most likely not a movie) or if year is BEFORE stamp, like: show.2014.801.mkv */ 162 | if (stamp && (!meta.year || (meta.year && (firstNameSplit.indexOf(stamp.toString()) < firstNameSplit.indexOf(meta.year.toString()))))) 163 | { 164 | meta.episode = [ parseInt(stamp.slice(-2), 10) ]; 165 | meta.season = parseInt(stamp.slice(0, -2), 10); 166 | } 167 | } 168 | 169 | /* 170 | * "season 1", "season.1", "season1" 171 | * */ 172 | if (! saneSeason()) 173 | { 174 | var seasonMatch = segments.join("/").match(/season(\.| )?(\d{1,2})/ig); 175 | if (seasonMatch) 176 | meta.season = parseInt(seasonMatch[0].match(/\d/g).join(""), 10); 177 | 178 | var seasonEpMatch = segments.join("/").match(/Season (\d{1,2}) - (\d{1,2})/i); 179 | if (seasonEpMatch) { 180 | meta.season = parseInt(seasonEpMatch[1], 10); 181 | meta.episode = [ parseInt(seasonEpMatch[2], 10) ]; 182 | } 183 | } 184 | 185 | 186 | /* 187 | * "episode 13", "episode.13", "episode13", "ep13", etc. 188 | * */ 189 | if (! saneEpisode()) 190 | { 191 | /* TODO: consider the case when a hyphen is used for multiple episodes ; e.g. e1-3*/ 192 | var episodeMatch = segments.join("/").match(/ep(isode)?(\.| )?(\d+)/ig); 193 | if (episodeMatch) 194 | meta.episode = [ parseInt(episodeMatch[0].match(/\d/g).join(""), 10) ]; 195 | } 196 | 197 | /* 198 | * Try plain number 199 | * This will lead to more false positives than true positives; disabling 200 | * / 201 | /* 202 | if (saneSeason() && !saneEpisode()) 203 | { 204 | var epNumbers = firstNameSplit 205 | .filter(function(x) { return !isNaN(x) && (x.length == 2 || x.length == 1) }) 206 | .map(function(x) { return parseInt(x, 10) }) 207 | 208 | if (epNumbers.length) 209 | meta.episode = epNumbers; 210 | }*/ 211 | 212 | /* 213 | * If still nothing, the number from the second dir can be used, if the first dir has the show name?? 214 | * 215 | * OR, split everything by everything (hyphens, underscores), filter for numbers between 1 and 30 and use the assumption that the episode is on the rightmost and the series is left from it 216 | * (unless the numbers are a part of a date stamp) 217 | * */ 218 | 219 | /* 220 | * Which part (for movies which are split into .cd1. and .cd2., etc.. files) 221 | * TODO: WARNING: this assumes it's in the filename segment 222 | * 223 | * */ 224 | var diskNumberMatch = segments[0].match(/[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck]|d)[ _.-]*(\d)[^\d]/); /* weird regexp? */ 225 | if (diskNumberMatch) 226 | meta.diskNumber = parseInt(diskNumberMatch[1], 10); 227 | 228 | /* 229 | * The name of the series / movie 230 | * */ 231 | var isSample; 232 | 233 | (options.fromInside ? segments : [].concat(segments).reverse()).forEach(function(seg, i) 234 | { 235 | if (seg == segments[0]) { 236 | seg = seg.split(".").slice(0, -1).join("."); /* Remove extension */ 237 | 238 | var sourcePrefix = seg.match(/^\[(.*?)\]/) 239 | if (sourcePrefix) seg = seg.slice(sourcePrefix[0].length) 240 | } 241 | 242 | /* 243 | * WARNING: we must change how this works in order to handle cases like 244 | * "the office[1.01]" as well as "the office [1.01]"; if we split those at '[' or ']', we will get the name "the office 1 10" 245 | * For now, here's a hack to fix this 246 | */ 247 | var squareBracket = seg.indexOf("["); 248 | if (squareBracket > -1) seg = seg.slice(0, squareBracket); 249 | 250 | //var segSplit = seg.split(/\.| |-|\[|\]|;|_/), 251 | var segSplit = seg.split(SEGMENTS_SPLIT), 252 | word, 253 | nameParts = []; 254 | 255 | isSample = isSample || seg.match(/^sample/i)|| seg.match(/^etrg/i); 256 | 257 | /* No need to go further; */ 258 | if (meta.name) 259 | return; 260 | 261 | var lastIndex; 262 | segSplit.some(function(word, i) 263 | { 264 | lastIndex = i; 265 | /* words with basic punctuation and two-digit numbers; or numbers in the first position */ 266 | if (! (word.match("^[a-zA-Z,?!'&]*$") || (!isNaN(word) && word.length <= 2) || (!isNaN(word) && i==0)) 267 | || excluded[word.toLowerCase()] 268 | || ((["ep", "episode", "season"].indexOf(word.toLowerCase()) > -1) && !isNaN(segSplit[i+1])) // TODO: more than that, match for stamp too 269 | ) 270 | return true; 271 | 272 | nameParts.push(word); 273 | }); 274 | if (nameParts.length == 1 && !isNaN(nameParts[0])) return; /* Only a number: unacceptable */ 275 | 276 | meta.name = nameParts 277 | .filter(function(x) { return x && x.length > 0 }) 278 | .map(function(x) { return x[0].toUpperCase() + x.slice(1).toLowerCase() }) 279 | .join(" "); 280 | }); 281 | 282 | isSample = isSample || ((segments[1] || "").toLowerCase() == "sample"); /* The directory where the file resides */ 283 | 284 | /* 285 | * This is the time to decide the category of this video 286 | */ 287 | var canBeMovie; 288 | if (options.strict) canBeMovie = meta.hasOwnProperty("year"); 289 | else canBeMovie = meta.hasOwnProperty("year") || meta.hasOwnProperty("diskNumber") || movieKeywords.some(function(keyword) { return segments.join("/").toLowerCase().search(keyword) > -1 }); 290 | 291 | if (meta.name && meta.aired) 292 | meta.type = "series"; 293 | if (meta.name && saneSeason() && saneEpisode()) 294 | meta.type = "series"; 295 | else if (meta.name && canBeMovie) 296 | meta.type = "movie"; 297 | // Must be deprioritized compared to movies 298 | else if (meta.type != "movie" && meta.name && saneSeason()) // assume extras or bloopers? 299 | meta.type = "extras"; 300 | else 301 | meta.type = "other"; 302 | 303 | if (options.fileLength && options.fileLength < (meta.type.match(/movie/) ? 80 : 50)*1024*1024 && meta.type.match(/movie|series/) && !isSample) { 304 | meta.type = "other"; 305 | } 306 | 307 | if (meta.type != "series" || meta.aired) { 308 | delete meta.episode; 309 | delete meta.season; 310 | } 311 | 312 | // we have a year. put it to .aired instead 313 | if (meta.type == "series" && meta.year) { 314 | meta.aired = meta.aired || meta.year; 315 | delete meta.year; 316 | } 317 | 318 | meta.type += isSample ? "-sample" : ""; 319 | 320 | meta.name = meta.name && simplifyName(meta.name); 321 | 322 | /* Try to find the IMDB id from the NFO / hints 323 | */ 324 | //if (meta.nfo) 325 | // meta.imdb_id = (fs.readFileSync(meta.nfo).toString().match("tt\\d{7}") || [])[0]; 326 | 327 | if (options.hints && options.hints.imdb_id) 328 | meta.imdb_id = options.hints.imdb_id; 329 | 330 | meta.tag = []; 331 | if (filePath.match(/1080p/i)) { meta.tag.push("hd"); meta.tag.push("1080p"); } 332 | if (filePath.match(/720p/i)) { meta.tag.push("720p"); } 333 | if (filePath.match(/480p/i)) { meta.tag.push("480p"); } 334 | if (isSample) meta.tag.push("sample"); 335 | 336 | return meta; 337 | } 338 | 339 | module.exports = parseVideoName; 340 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-name-parser", 3 | "version": "1.4.7", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 11 | "dev": true 12 | }, 13 | "brace-expansion": { 14 | "version": "1.1.11", 15 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 16 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 17 | "dev": true, 18 | "requires": { 19 | "balanced-match": "1.0.0", 20 | "concat-map": "0.0.1" 21 | } 22 | }, 23 | "concat-map": { 24 | "version": "0.0.1", 25 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 26 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 27 | "dev": true 28 | }, 29 | "deep-equal": { 30 | "version": "1.0.1", 31 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 32 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", 33 | "dev": true 34 | }, 35 | "define-properties": { 36 | "version": "1.1.2", 37 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", 38 | "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", 39 | "dev": true, 40 | "requires": { 41 | "foreach": "2.0.5", 42 | "object-keys": "1.0.11" 43 | } 44 | }, 45 | "defined": { 46 | "version": "1.0.0", 47 | "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", 48 | "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", 49 | "dev": true 50 | }, 51 | "es-abstract": { 52 | "version": "1.10.0", 53 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.10.0.tgz", 54 | "integrity": "sha512-/uh/DhdqIOSkAWifU+8nG78vlQxdLckUdI/sPgy0VhuXi2qJ7T8czBmqIYtLQVpCIFYafChnsRsB5pyb1JdmCQ==", 55 | "dev": true, 56 | "requires": { 57 | "es-to-primitive": "1.1.1", 58 | "function-bind": "1.1.1", 59 | "has": "1.0.1", 60 | "is-callable": "1.1.3", 61 | "is-regex": "1.0.4" 62 | } 63 | }, 64 | "es-to-primitive": { 65 | "version": "1.1.1", 66 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", 67 | "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", 68 | "dev": true, 69 | "requires": { 70 | "is-callable": "1.1.3", 71 | "is-date-object": "1.0.1", 72 | "is-symbol": "1.0.1" 73 | } 74 | }, 75 | "for-each": { 76 | "version": "0.3.2", 77 | "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.2.tgz", 78 | "integrity": "sha1-LEBFC5NI6X8oEyJZO6lnBLmr1NQ=", 79 | "dev": true, 80 | "requires": { 81 | "is-function": "1.0.1" 82 | } 83 | }, 84 | "foreach": { 85 | "version": "2.0.5", 86 | "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", 87 | "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", 88 | "dev": true 89 | }, 90 | "fs.realpath": { 91 | "version": "1.0.0", 92 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 93 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 94 | "dev": true 95 | }, 96 | "function-bind": { 97 | "version": "1.1.1", 98 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 99 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 100 | "dev": true 101 | }, 102 | "glob": { 103 | "version": "7.1.2", 104 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 105 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 106 | "dev": true, 107 | "requires": { 108 | "fs.realpath": "1.0.0", 109 | "inflight": "1.0.6", 110 | "inherits": "2.0.3", 111 | "minimatch": "3.0.4", 112 | "once": "1.4.0", 113 | "path-is-absolute": "1.0.1" 114 | } 115 | }, 116 | "has": { 117 | "version": "1.0.1", 118 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", 119 | "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", 120 | "dev": true, 121 | "requires": { 122 | "function-bind": "1.1.1" 123 | } 124 | }, 125 | "inflight": { 126 | "version": "1.0.6", 127 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 128 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 129 | "dev": true, 130 | "requires": { 131 | "once": "1.4.0", 132 | "wrappy": "1.0.2" 133 | } 134 | }, 135 | "inherits": { 136 | "version": "2.0.3", 137 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 138 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 139 | "dev": true 140 | }, 141 | "is-callable": { 142 | "version": "1.1.3", 143 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", 144 | "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", 145 | "dev": true 146 | }, 147 | "is-date-object": { 148 | "version": "1.0.1", 149 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 150 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 151 | "dev": true 152 | }, 153 | "is-function": { 154 | "version": "1.0.1", 155 | "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", 156 | "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=", 157 | "dev": true 158 | }, 159 | "is-regex": { 160 | "version": "1.0.4", 161 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 162 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 163 | "dev": true, 164 | "requires": { 165 | "has": "1.0.1" 166 | } 167 | }, 168 | "is-symbol": { 169 | "version": "1.0.1", 170 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", 171 | "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", 172 | "dev": true 173 | }, 174 | "minimatch": { 175 | "version": "3.0.4", 176 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 177 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 178 | "dev": true, 179 | "requires": { 180 | "brace-expansion": "1.1.11" 181 | } 182 | }, 183 | "minimist": { 184 | "version": "1.2.0", 185 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 186 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 187 | "dev": true 188 | }, 189 | "object-inspect": { 190 | "version": "1.3.0", 191 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.3.0.tgz", 192 | "integrity": "sha512-OHHnLgLNXpM++GnJRyyhbr2bwl3pPVm4YvaraHrRvDt/N3r+s/gDVHciA7EJBTkijKXj61ssgSAikq1fb0IBRg==", 193 | "dev": true 194 | }, 195 | "object-keys": { 196 | "version": "1.0.11", 197 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", 198 | "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", 199 | "dev": true 200 | }, 201 | "once": { 202 | "version": "1.4.0", 203 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 204 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 205 | "dev": true, 206 | "requires": { 207 | "wrappy": "1.0.2" 208 | } 209 | }, 210 | "path-is-absolute": { 211 | "version": "1.0.1", 212 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 213 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 214 | "dev": true 215 | }, 216 | "path-parse": { 217 | "version": "1.0.5", 218 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 219 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", 220 | "dev": true 221 | }, 222 | "resolve": { 223 | "version": "1.4.0", 224 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", 225 | "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", 226 | "dev": true, 227 | "requires": { 228 | "path-parse": "1.0.5" 229 | } 230 | }, 231 | "resumer": { 232 | "version": "0.0.0", 233 | "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", 234 | "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", 235 | "dev": true, 236 | "requires": { 237 | "through": "2.3.8" 238 | } 239 | }, 240 | "string.prototype.trim": { 241 | "version": "1.1.2", 242 | "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", 243 | "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", 244 | "dev": true, 245 | "requires": { 246 | "define-properties": "1.1.2", 247 | "es-abstract": "1.10.0", 248 | "function-bind": "1.1.1" 249 | } 250 | }, 251 | "tape": { 252 | "version": "4.8.0", 253 | "resolved": "https://registry.npmjs.org/tape/-/tape-4.8.0.tgz", 254 | "integrity": "sha512-TWILfEnvO7I8mFe35d98F6T5fbLaEtbFTG/lxWvid8qDfFTxt19EBijWmB4j3+Hoh5TfHE2faWs73ua+EphuBA==", 255 | "dev": true, 256 | "requires": { 257 | "deep-equal": "1.0.1", 258 | "defined": "1.0.0", 259 | "for-each": "0.3.2", 260 | "function-bind": "1.1.1", 261 | "glob": "7.1.2", 262 | "has": "1.0.1", 263 | "inherits": "2.0.3", 264 | "minimist": "1.2.0", 265 | "object-inspect": "1.3.0", 266 | "resolve": "1.4.0", 267 | "resumer": "0.0.0", 268 | "string.prototype.trim": "1.1.2", 269 | "through": "2.3.8" 270 | } 271 | }, 272 | "through": { 273 | "version": "2.3.8", 274 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 275 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 276 | "dev": true 277 | }, 278 | "wrappy": { 279 | "version": "1.0.2", 280 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 281 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 282 | "dev": true 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-name-parser", 3 | "version": "1.4.7", 4 | "description": "Parse names of video files to identify quality (e.g. 1080p), season/episode, year, etc.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape test/*" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Ivshti/video-name-parser" 12 | }, 13 | "keywords": [ 14 | "parsing", 15 | "video", 16 | "naem" 17 | ], 18 | "author": "Ivo Georgiev (http://ivogeorgiev.com)", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/Ivshti/video-name-parser/issues" 22 | }, 23 | "homepage": "https://github.com/Ivshti/video-name-parser", 24 | "devDependencies": { 25 | "tape": "^4.5.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"); 2 | var parser = require("../index"); 3 | 4 | console.log(parser("Blade.Runner.2049.2017.1080p.WEB-DL", { strict: true })) 5 | 6 | var shows = [ 7 | "pioneer.one.s01e01.avi", //show with long-md 8 | "[ hoi ]pioneer.one.s01e01.avi", //show with long-md with prefix 9 | //"pioneer.one.s1e1.avi", //show with short-md //never seen; unsupported 10 | "pioneer.one.1x1.avi", //show with alt-short-md 11 | "pioneer.one.01x01.avi", //show with alt-long-md 12 | "pioneer.one.season.1.episode.1.avi", //show with full md //never seen 13 | "pioneer.one.1001.avi", //show with unmarked md 14 | //"pioneer.one.2014.217.avi", //show with year & unmarked short-md; unsupported 15 | //"pioneer.one.2014.1017.avi", //show with year & unmarked long-md; unsupported 16 | "pioneer.one.2014.s01e01.avi", //show with year & long-md 17 | "pioneer.one.101.s01e01.avi", //show with stamp & long-md; unsupported 18 | "pioneer.one.101.2014.s01e01.avi" //total fuck up ; unsupported 19 | ]; 20 | 21 | shows.forEach(function(str) { 22 | tape("show test - "+str, function(t) { 23 | var x = parser(str); 24 | t.equals(x.type, "series"); 25 | t.equals(x.name, "pioneer one"); 26 | if (x.year) t.equals(x.year, 2014); 27 | t.equals(x.season, 1); 28 | t.equals(x.episode && x.episode[0], 1); 29 | t.end(); 30 | }) 31 | }); 32 | 33 | var movies = [ 34 | // "pioneer.one.avi", //movie 35 | "pioneer.one.2014.avi", //movie with year 36 | "pioneer.one.201.2014.avi", //movie with stamp & year 37 | //"pioneer.one.1980.2014.avi", //movie with longstamp & year (new movie) 38 | //"pioneer.one.2020.2014.avi" //movie with longstam & year (old movie) 39 | ]; 40 | 41 | console.log(parser("South.Park.S20E07.Oh.Jeez.720p.Uncensored.Web-DL.EN-Sub.x264-[MULVAcoded].mkv")) 42 | movies.forEach(function(str) { 43 | tape("movie test - "+str, function(t) { 44 | var x = parser(str); 45 | t.equals(x.type, "movie"); 46 | t.equals(x.name, "pioneer one"); 47 | t.equals(x.year, 2014); 48 | t.end(); 49 | }) 50 | }); 51 | --------------------------------------------------------------------------------