mirror of
https://github.com/HDInnovations/UNIT3D-Community-Edition.git
synced 2026-02-11 22:19:47 -06:00
454 lines
12 KiB
JavaScript
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,
|
|
};
|