UNPKG

@ctrl/video-filename-parser

Version:
276 lines (275 loc) 10.1 kB
const requestInfoExp = /^(?:\[[^\]\r\n]+\])+/; const sixDigitAirDateMatchExp = /"(?<=[_.-])(?<airdate>(?<!\d)(?<airyear>[1-9]\d{1})(?<airmonth>[0-1][0-9])(?<airday>[0-3][0-9]))(?=[_.-])/i; export function completeRange(arr) { const uniqArr = [...new Set(arr)]; if (uniqArr.length === 0) { return []; } const first = Number(uniqArr[0]); const last = Number(uniqArr[uniqArr.length - 1]); const range = expandIntegerRange(first, last); if (range === null) { return arr; } return range; } export function normalizeSixDigitAirDate(title, simpleTitle) { const sixDigitAirDateMatch = sixDigitAirDateMatchExp.exec(title); if (!sixDigitAirDateMatch?.groups) { return simpleTitle; } const airYear = sixDigitAirDateMatch.groups.airyear ?? ''; const airMonth = sixDigitAirDateMatch.groups.airmonth ?? ''; const airDay = sixDigitAirDateMatch.groups.airday ?? ''; if (airMonth === '00' && airDay === '00') { return simpleTitle; } const fixedDate = `20${airYear}.${airMonth}.${airDay}`; return simpleTitle.replace(sixDigitAirDateMatch.groups.airdate ?? '', fixedDate); } export function parseGenericMatchCollection(match, simpleTitle) { const { groups } = match; if (groups === undefined) { throw new Error('No match'); } if (hasAirDate(groups)) { return parseAirDateGroups(groups, simpleTitle); } const hasAbsoluteEpisode = hasAnyGroup(groups, 'absoluteepisode', 'absoluteepisode1'); const hasSeasonPart = hasAnyGroup(groups, 'seasonpart'); const hasEpisode = hasAnyGroup(groups, 'episode', 'episode1'); if (hasAbsoluteEpisode) { return parseAbsoluteEpisodeGroups(groups, simpleTitle); } if (hasSeasonPart) { return parsePartialSeasonGroups(groups, simpleTitle); } if (hasEpisode) { return parseSeasonEpisodeGroups(groups, simpleTitle); } return parseSeasonPackGroups(groups, simpleTitle); } export function parseAirDateMatch(match, simpleTitle) { return parseAirDateGroups(requireGroups(match), simpleTitle); } export function parseSeasonEpisodeMatch(match, simpleTitle) { return parseSeasonEpisodeGroups(requireGroups(match), simpleTitle); } export function parseSeasonPackMatch(match, simpleTitle) { return parseSeasonPackGroups(requireGroups(match), simpleTitle); } export function parsePartialSeasonMatch(match, simpleTitle) { return parsePartialSeasonGroups(requireGroups(match), simpleTitle); } export function parseAbsoluteEpisodeMatch(match, simpleTitle) { return parseAbsoluteEpisodeGroups(requireGroups(match), simpleTitle); } function requireGroups(match) { const { groups } = match; if (groups === undefined) { throw new Error('No match'); } return groups; } function hasAirDate(groups) { const airYear = Number.parseInt(groupValue(groups, 'airyear') ?? '', 10); return airYear >= 1900; } function parseAirDateGroups(groups, simpleTitle) { const { result } = createBaseResult(groups, simpleTitle); let lastTokenIndex = indexOfEnd(simpleTitle, groupValue(groups, 'title') ?? ''); const airYearText = groupValue(groups, 'airyear'); const airMonthText = groupValue(groups, 'airmonth'); const airDayText = groupValue(groups, 'airday'); if (!airYearText || !airMonthText || !airDayText) { return null; } const airYear = Number.parseInt(airYearText, 10); let airMonth = Number.parseInt(airMonthText, 10); let airDay = Number.parseInt(airDayText, 10); if (airMonth > 12) { const tempDay = airDay; airDay = airMonth; airMonth = tempDay; } const airDate = new Date(airYear, airMonth - 1, airDay); if (!isValidAirDate(airDate, airYear, airMonth, airDay)) { return null; } lastTokenIndex = Math.max(indexOfEnd(simpleTitle, airYearText), lastTokenIndex); lastTokenIndex = Math.max(indexOfEnd(simpleTitle, airMonthText), lastTokenIndex); lastTokenIndex = Math.max(indexOfEnd(simpleTitle, airDayText), lastTokenIndex); result.airDate = airDate; return finishResult(result, simpleTitle, lastTokenIndex); } function parseSeasonEpisodeGroups(groups, simpleTitle) { const { result, lastTokenIndex } = createBaseResult(groups, simpleTitle); const nextIndex = applySeasonNumbers(result, groups, simpleTitle, lastTokenIndex); const episodeResult = applyEpisodeNumbers(result, groups); if (episodeResult === null) { return null; } return finishResult(result, simpleTitle, nextIndex); } function parseSeasonPackGroups(groups, simpleTitle) { const { result, lastTokenIndex } = createBaseResult(groups, simpleTitle); const nextIndex = applySeasonNumbers(result, groups, simpleTitle, lastTokenIndex); if (groupValue(groups, 'extras')) { result.isSeasonExtra = true; } result.fullSeason = true; return finishResult(result, simpleTitle, nextIndex); } function parsePartialSeasonGroups(groups, simpleTitle) { const { result, lastTokenIndex } = createBaseResult(groups, simpleTitle); const nextIndex = applySeasonNumbers(result, groups, simpleTitle, lastTokenIndex); const seasonPart = groupValue(groups, 'seasonpart'); if (seasonPart) { result.seasonPart = Number.parseInt(seasonPart, 10); result.isPartialSeason = true; } else { result.fullSeason = true; } return finishResult(result, simpleTitle, nextIndex); } function parseAbsoluteEpisodeGroups(groups, simpleTitle) { const { result, lastTokenIndex } = createBaseResult(groups, simpleTitle); let nextIndex = applySeasonNumbers(result, groups, simpleTitle, lastTokenIndex); const episodeResult = applyEpisodeNumbers(result, groups); if (episodeResult === null) { return null; } const absoluteResult = applyAbsoluteEpisodeNumbers(result, groups); if (absoluteResult === null) { return null; } if (absoluteResult.lastCapture) { nextIndex = Math.max(indexOfEnd(simpleTitle, absoluteResult.lastCapture), nextIndex); } return finishResult(result, simpleTitle, nextIndex); } function createBaseResult(groups, simpleTitle) { const title = groupValue(groups, 'title') ?? ''; const seriesName = title .replaceAll('.', ' ') .replaceAll('_', ' ') .replace(requestInfoExp, '') .trim(); return { result: { seriesName, }, lastTokenIndex: indexOfEnd(simpleTitle, title), }; } function applySeasonNumbers(result, groups, simpleTitle, lastTokenIndex) { let nextIndex = lastTokenIndex; let seasons = [groupValue(groups, 'season'), groupValue(groups, 'season1')] .filter(isPresent) .map(x => { nextIndex = Math.max(indexOfEnd(simpleTitle, x), nextIndex); return Number(x); }); if (seasons.length > 1) { seasons = completeRange(seasons); } result.seasonNumbers = seasons; if (seasons.length > 1) { result.isMultiSeason = true; } return nextIndex; } function applyEpisodeNumbers(result, groups) { const episodeCaptures = [groupValue(groups, 'episode'), groupValue(groups, 'episode1')].filter(isPresent); if (episodeCaptures.length === 0) { return undefined; } const first = Number(episodeCaptures[0]); const last = Number(episodeCaptures[episodeCaptures.length - 1]); const range = expandIntegerRange(first, last); if (range === null) { return null; } result.episodeNumbers = range; return undefined; } function applyAbsoluteEpisodeNumbers(result, groups) { const absoluteEpisodeCaptures = [ groupValue(groups, 'absoluteepisode'), groupValue(groups, 'absoluteepisode1'), ].filter(isPresent); if (absoluteEpisodeCaptures.length === 0) { return {}; } const first = Number(absoluteEpisodeCaptures[0]); const lastCapture = absoluteEpisodeCaptures[absoluteEpisodeCaptures.length - 1] ?? ''; const last = Number(lastCapture); if (!Number.isFinite(first) || !Number.isFinite(last) || first > last) { return null; } if (first % 1 !== 0 || last % 1 !== 0) { if (absoluteEpisodeCaptures.length !== 1) { return null; } result.episodeNumbers = [first]; result.isSpecial = true; return { lastCapture }; } const range = expandIntegerRange(first, last); if (range === null) { return null; } result.episodeNumbers = range; if (groupValue(groups, 'special')) { result.isSpecial = true; } return { lastCapture }; } function finishResult(result, simpleTitle, lastTokenIndex) { const releaseTokens = lastTokenIndex === simpleTitle.length || lastTokenIndex === -1 ? simpleTitle : simpleTitle.slice(lastTokenIndex); return { ...result, releaseTokens, seriesTitle: result.seriesName, }; } function groupValue(groups, name) { const value = groups[name]; return value === undefined || value.length === 0 ? undefined : value; } function hasAnyGroup(groups, ...names) { return names.some(name => groupValue(groups, name) !== undefined); } function isPresent(value) { return value !== undefined; } function expandIntegerRange(first, last) { if (!Number.isInteger(first) || !Number.isInteger(last) || first > last) { return null; } return Array.from({ length: last - first + 1 }, (_, index) => index + first); } function isValidAirDate(airDate, airYear, airMonth, airDay) { if (Number.isNaN(airDate.getTime())) { return false; } if (airDate.getFullYear() !== airYear || airDate.getMonth() !== airMonth - 1 || airDate.getDate() !== airDay) { return false; } if (airDate.getTime() > Date.now()) { return false; } return airDate.getTime() >= new Date(1970, 1, 1).getTime(); } function indexOfEnd(str1, str2) { if (str2.length === 0) { return -1; } const io = str1.indexOf(str2); return io === -1 ? -1 : io + str2.length; }