Files
UNIT3D-Community-Edition/resources/js/unit3d/parser.js
2021-10-21 12:05:23 -04:00

454 lines
12 KiB
JavaScript

const rules = {
source: {
CAM: ['camrip', 'cam-rip', 'cam', 'ts', 'telesync', 'pdvd'],
TC: ['tc', 'telecine'],
SCREENER: ['dvdscr', 'dvd-scr', 'dvdscreener', 'screener', 'scr', 'DDC'],
R5: ['r5'],
DVDRip: ['dvdrip', 'dvd-rip'],
BDRip: ['bdrip', 'bd-rip', 'brrip', 'br-rip'],
'WEB-DL': ['webdl', 'web-dl'],
WEBRip: ['webrip', 'web-rip'],
'DVD-R': ['dvdr', 'dvd-r', 'dvd-5', 'dvd-9', 'r6-dvd', 'dvd'],
BluRay: ['bluray', 'blu-ray', 'bdr'],
BDSCR: ['bluray-scr', 'bdscr'],
PDTV: ['pdtv'],
SDTV: ['sdtv', 'dsr', 'dsrip', 'satrip', 'dthrip', 'dvbrip'],
HDTV: ['hdtvrip', 'hdtv-rip', 'hdtv'],
},
subsource: {
AMZN: ['amzn', 'amazon'],
NF: ['nf', 'netflix'],
VUDU: ['vudu'],
HULU: ['hulu'],
YT: ['yt', 'youtube'],
},
encoding: {
DivX: ['divx'],
XviD: ['xvid'],
x264: ['x264', 'x.264', 'x 264'],
x265: ['x265', 'x.265', 'x 265'],
'H.264': ['h264', 'h.264', 'h 264'],
'H.265': ['h265', 'h.265', 'h 265'],
AVC: ['avc'],
'VC-1': ['vc-1', 'vc1'],
},
resolution: {
1: ['4320p', '8k'],
2: ['2160p', '4k', 'uhd'],
3: ['1080p'],
4: ['1080i'],
5: ['720p'],
6: ['576p', 'PAL'],
7: ['576i', 'PAL'],
8: ['480p', 'NTSC'],
9: ['480i', 'NTSC'],
},
dub: {
DUBBED: ['dubbed'],
LD: ['ld', 'licrodubbed', 'licro-dubbed'],
MD: ['md', 'microdubbed', 'micro-dubbed'],
},
language: {
MULTI: ['multi-vf2', 'multi'],
TRUEFRENCH: ['truefrench', 'vff', 'vf2'],
FRENCH: ['french', 'français', 'francais', 'fr', 'vf'],
VFQ: 'vfq',
VOSTFR: ['vostfr', 'stfr', 'subfrench'],
PERSIAN: 'persian',
AMHARIC: 'amharic',
ARABIC: 'arabic',
CAMBODIAN: 'cambodian',
CHINESE: 'chinise',
CREOLE: 'creole',
DANISH: 'danish',
DUTCH: 'dutch',
ENGLISH: ['english', 'eng', 'en', 'voa'],
ESTONIAN: 'estonian',
FILIPINO: 'filipino',
FINNISH: 'finnish',
FLEMISH: 'flemish',
GERMAN: 'german',
GREEK: 'greek',
HEBREW: 'hebrew',
INDONESIAN: 'indonesian',
IRISH: 'irish',
ITALIAN: 'italian',
JAPANESE: 'japanese',
KOREAN: 'korean',
LAOTIAN: 'laotian',
LATVIAN: 'latvian',
LITHUANIAN: 'lithuanian',
MALAY: 'malay',
MALAYSIAN: 'malaysian',
MAORI: 'maori',
NORWEGIAN: 'norwegian',
PASHTO: 'pashto',
POLISH: 'polish',
PORTUGUESE: 'portuguese',
ROMANIAN: 'romanian',
RUSSIAN: 'russian',
SPANISH: 'spanish',
SWAHILI: 'swahili',
SWEDISH: 'swedish',
SWISS: 'swiss',
TAGALOG: 'tagalog',
TAJIK: 'tajik',
THAI: 'thai',
TURKISH: 'turkish',
UKRAINIAN: 'ukrainian',
VIETNAMESE: 'vietnamese',
WELSH: 'welsh',
VO: 'vo',
},
container: {
MP4: 'mp4',
MKV: 'mkv',
},
audio: {
DTS: 'dts',
AAC: 'aac',
'DTS-HD': 'dts-hd',
'DTSHD-MA': 'dts-ma',
TRUEHD: 'truehd',
ATMOS: 'atmos',
'DTS-X': ['dts-x', 'dts x', 'dtsx'],
DD: ['dd'],
'DD+': ['ddp', 'dd+'],
FLAC: ['flac'],
},
channels: {
'1.0': ['1.0', '1 0'],
'2.0': ['2.0', '2 0'],
5.1: ['5.1', '5 1'],
6.1: ['6.1', '6 1'],
7.1: ['7.1', '7 1'],
9.1: ['9.1', '9 1'],
},
flags: {
'3D': '3d',
PROPER: 'proper',
LIMITED: 'limited',
FASTSUB: 'fastsub',
SUBFORCED: 'subforced',
SUBBED: 'subbed',
EXTENDED: 'extended',
THEATRICAL: 'theatrical',
WORKPRINT: ['workprint', 'wp'],
FANSUB: 'fansub',
REPACK: 'repack',
UNRATED: 'unrated',
NFOFIX: 'nfofix',
NTSC: 'ntsc',
PAL: 'pal',
INTERNAL: ['internal', 'cultfilms', 'blutonium', 'bluranium'],
FESTIVAL: 'festival',
STV: 'stv',
RETAIL: 'retails',
REMASTERED: 'remastered',
RATED: 'rated',
CHRONO: 'chrono',
UNCUT: 'uncut',
UNCENSORED: 'uncensored',
COMPLETE: 'complete',
UNTOUCHED: 'untouched',
DC: 'dc',
REMUX: 'remux',
DUAL: 'dual',
FINAL: 'final',
COLORIZED: 'colorized',
RESTORED: 'restored',
WS: 'ws',
DL: 'dl',
READNFO: ['read.nfo', 'read-nfo', 'readnfo'],
HSBS: 'hsbs',
HOU: 'hou',
DOC: 'doc',
RERIP: ['rerip', 're-rip'],
},
erase: ['\\[.*?torrent.*?\\]', '\\[.*\\]'],
};
const properties = [
'language',
'source',
'subsource',
'encoding',
'resolution',
'dub',
'year',
'flags',
'season',
'episode',
'episodes',
'type',
'group',
'container',
'audio',
'channels',
];
function clean(string, erase) {
let result = string;
erase.forEach(
(regexp) => (result = result.replace(new RegExp(`[\.\-]*?${regexp.replace(/\\\\/g, '\\')}[\.\-]*?`, 'ig'), ''))
);
result = result.trim();
result = result.replace(/[\[\]\(\)\;\:\!\s\\\.]+/g, '.');
result = result.replace(/\.(avi|mp4|mkv)/, '');
return result;
}
function deduce(property, name, multi = false) {
const result = {
match: null,
waste: name,
};
switch (property) {
case 'year': {
const regexp = /[\.\-](\d{4})([\.\-])?/;
const matches = name.match(regexp);
if (matches !== null) {
result.match = matches[1];
result.waste = name.replace(regexp, '$2');
}
return result;
}
case 'group': {
const regexp = /\-([a-zA-Z0-9_\.]+)$/;
const matches = name.match(regexp);
if (matches !== null) {
result.match = matches[1].length > 12 ? matches[1].replace(/[_\.].+?$/, '') : matches[1];
result.waste = name.replace(regexp, '');
}
return result;
}
case 'season': {
const regexp = /[\.\-]S(\d+)[\.\-]?(?:E\d+)?(?:[\.\-])/i;
const matches = name.match(regexp);
if (matches !== null) {
result.match = parseInt(matches[1]);
}
return result;
}
case 'episode': {
const regexps = {
episodes: /[\.\-](?:S(?:\d+))?[\.\-]?((?:-?E(?:\d+))+)(?:[\.\-])/i,
episode: /E(\d+)/gi,
};
const matches = name.match(regexps.episodes);
const match = [];
let crumbs = [];
while (matches && (crumbs = regexps.episode.exec(matches[1])) !== null) {
match.push(crumbs[1]);
}
result.match = match
.map(Number)
.sort((a, b) => a - b)
.reduce((res, episode, index, match) => (match.length === 1 ? episode : match.join('-')), null);
return result;
}
case 'episodes': {
const regexps = {
episodes: /[\.\-](?:S(?:\d+))?[\.\-]?((?:-?E(?:\d+))+)(?:[\.\-])/i,
episode: /E(\d+)/gi,
};
const matches = name.match(regexps.episodes);
const match = [];
let crumbs = [];
while (matches && (crumbs = regexps.episode.exec(matches[1])) !== null) {
match.push(crumbs[1]);
}
result.match = match.map(Number).sort((a, b) => a - b);
return result;
}
case 'type': {
result.match = 'Movie';
result.waste = name;
for (let regexp of [
/[\.\-]S\d+[\.\-]?(?:-?E\d+)*([\.\-])/i,
/[\.\-](?:-?E\d+)+([\.\-])/i,
/[\.\-](?:-?S\d+)+([\.\-])/i,
/[\.\-]S\d+[\.\-]?/i,
]) {
if (name.match(regexp)) {
result.match = 'TV Show';
result.waste = name.replace(regexp, '$1');
break;
}
}
return result;
}
}
const rule = rules[property];
const tags = Object.keys(rule);
single: {
for (let i = 0; i < tags.length; i++) {
const tag = tags[i];
const patterns = Array.isArray(rule[tag]) ? rule[tag] : [rule[tag]];
for (let j = 0; j < patterns.length; j++) {
const regexp = new RegExp('[.-]' + patterns[j] + '([.-]|$)', 'i');
if (result.waste.match(regexp)) {
result.match = multi ? (result.match || []).concat([tag]) : tag;
result.waste = result.waste.replace(regexp, '$1');
if (!multi && result.match) {
break single;
}
break;
}
}
}
}
return result;
}
function stringify(release, options) {
return release.title
.split(' ')
.concat([
release.year,
[
release.season ? 'S' + release.season.toString().padStart(2, '0') : null,
release.episodes.length
? 'E' + release.episodes.map((episode) => episode.toString().padStart(2, '0')).join('-E')
: null,
].join(''),
release.resolution,
release.encoding,
release.subsource,
release.source,
options.flagged ? release.flags : [],
release.audio,
release.channels,
//release.container,
//release.dub,
])
.filter((property) => property)
.join('.')
.concat('-' + (release.group || 'NOGROUP'));
}
function parse(name, options = { strict: false, flagged: true, erase: [], defaults: {} }) {
options.defaults = Object.assign(
properties
.filter((property) => !['type'].includes(property))
.reduce((obj, property) => Object.assign(obj, { [property]: null }), {}),
{
language: 'VO',
},
options.defaults
);
const cleaned = clean(name, [...(options.erase || []), ...rules.erase]);
let words = cleaned.replace(/[\.\-]+/, '.').split('.');
let waste = cleaned;
let handicap = [];
let release = {
original: name,
};
properties.map((property) => {
const result = deduce(property, waste, ['language', 'flags'].includes(property));
if (property === 'language' && result.match) {
result.match = result.match.length > 1 ? 'MULTI' : result.match[0];
}
waste = result.waste;
handicap = handicap.concat([!result.match && options.defaults[property] && property]);
release[property] = result.match || options.defaults[property];
});
release.title = waste
.replace(/[\.\-]+/, '.')
.split('.')
.filter((word, position) => word === words[position])
.map((word) => (word.split('').every((char) => ['i', 'I'].includes(char)) ? word.length : word))
.join(' ')
.toLowerCase()
.replace(/(^([a-zA-Z\p{M}]))|([ -][a-zA-Z\p{M}])/g, (s) => s.toUpperCase()); // ucwords
release.generated = stringify(release, options);
release.score = properties
.filter((property) => !['season', 'episodes', 'episode', 'type'].includes(property))
.filter((property) => !handicap.includes(property))
.filter((property) => release[property]).length;
let valid = !!['resolution', 'source', 'dub', 'encoding']
.filter((property) => !handicap.includes(property))
.filter((property) => release[property]).length;
if (options.strict && !valid) {
throw new Error('"' + release.original + '" does\'t follow scene release naming rules');
}
return release;
}
function guess(name, options) {
const release = parse(name, Object.assign({}, options, { strict: false }));
if (!release.year) {
release.year = new Date().getFullYear();
}
if (!release.resolution) {
if (['UHD'].includes(release.flags)) {
release.resolution = '2160p';
} else if (['BDSCR', 'BLURAY'].includes(release.source)) {
release.resolution = '1080p';
} else {
release.resolution = 'SD';
}
}
return release;
}
const title_parser = {
parse,
guess,
};