UNPKG

epg-grabber

Version:

Node.js CLI tool for grabbing EPG from different sites

576 lines (527 loc) 22.6 kB
import { escapeString, formatDate, createXMLElement, toArray, toUnix } from '../core/utils' import { ProgramData, ProgramParserResult, XMLElement } from '../types' import padStart from 'lodash.padstart' import utc from 'dayjs/plugin/utc.js' import { Channel } from './channel' import dayjs from 'dayjs' dayjs.extend(utc) export class Program { site: string start: number stop: number channel: string titles: ProgramData.TextObject[] subTitles: ProgramData.TextObject[] descriptions: ProgramData.TextObject[] date: number | null categories: ProgramData.TextObject[] keywords: ProgramData.TextObject[] languages: ProgramData.TextObject[] origLanguages: ProgramData.TextObject[] length: ProgramData.LenghtObject[] urls: ProgramData.UrlObject[] countries: ProgramData.TextObject[] episodeNumbers: ProgramData.EpisodeNumberObject[] video: ProgramData.VideoObject | null audio: ProgramData.AudioObject | null previouslyShown: ProgramData.PreviouslyShownObject[] premiere: ProgramData.TextObject[] lastChance: ProgramData.TextObject[] new: boolean subtitles: ProgramData.SubtitlesObject[] ratings: ProgramData.RatingObject[] starRatings: ProgramData.RatingObject[] reviews: ProgramData.ReviewObject[] directors: ProgramData.PersonObject[] actors: ProgramData.PersonObject[] writers: ProgramData.PersonObject[] adapters: ProgramData.PersonObject[] producers: ProgramData.PersonObject[] composers: ProgramData.PersonObject[] editors: ProgramData.PersonObject[] presenters: ProgramData.PersonObject[] commentators: ProgramData.PersonObject[] guests: ProgramData.PersonObject[] images: ProgramData.ImageObject[] icons: ProgramData.IconObject[] constructor(data: ProgramData) { if (typeof data.site !== 'string') throw new Error('The "site" property must be of type "string"') if (typeof data.start !== 'number') throw new Error('The "start" property must be of type "number"') if (typeof data.stop !== 'number') throw new Error('The "stop" property must be of type "number"') if (typeof data.channel !== 'string') throw new Error('The "channel" property must be of type "string"') if (!Array.isArray(data.titles)) throw new Error('The "start" property must be of type "array"') this.site = data.site this.channel = data.channel this.start = data.start this.stop = data.stop this.titles = data.titles this.subTitles = Array.isArray(data.subTitles) ? data.subTitles : [] this.descriptions = Array.isArray(data.descriptions) ? data.descriptions : [] this.date = typeof data.date === 'number' ? data.date : null this.categories = Array.isArray(data.categories) ? data.categories : [] this.keywords = Array.isArray(data.keywords) ? data.keywords : [] this.languages = Array.isArray(data.languages) ? data.languages : [] this.origLanguages = Array.isArray(data.origLanguages) ? data.origLanguages : [] this.length = Array.isArray(data.length) ? data.length : [] this.urls = Array.isArray(data.urls) ? data.urls : [] this.countries = Array.isArray(data.countries) ? data.countries : [] this.episodeNumbers = Array.isArray(data.episodeNumbers) ? data.episodeNumbers : [] this.video = typeof data.video === 'object' ? data.video : null this.audio = typeof data.audio === 'object' ? data.audio : null this.previouslyShown = Array.isArray(data.previouslyShown) ? data.previouslyShown : [] this.premiere = Array.isArray(data.premiere) ? data.premiere : [] this.lastChance = Array.isArray(data.lastChance) ? data.lastChance : [] this.new = typeof data.new === 'boolean' ? data.new : false this.subtitles = Array.isArray(data.subtitles) ? data.subtitles : [] this.ratings = Array.isArray(data.ratings) ? data.ratings : [] this.starRatings = Array.isArray(data.starRatings) ? data.starRatings : [] this.reviews = Array.isArray(data.reviews) ? data.reviews : [] this.directors = Array.isArray(data.directors) ? data.directors : [] this.actors = Array.isArray(data.actors) ? data.actors : [] this.writers = Array.isArray(data.writers) ? data.writers : [] this.adapters = Array.isArray(data.adapters) ? data.adapters : [] this.producers = Array.isArray(data.producers) ? data.producers : [] this.composers = Array.isArray(data.composers) ? data.composers : [] this.editors = Array.isArray(data.editors) ? data.editors : [] this.presenters = Array.isArray(data.presenters) ? data.presenters : [] this.commentators = Array.isArray(data.commentators) ? data.commentators : [] this.guests = Array.isArray(data.guests) ? data.guests : [] this.images = Array.isArray(data.images) ? data.images : [] this.icons = Array.isArray(data.icons) ? data.icons : [] } static fromParserResult(data: ProgramParserResult, channel: Channel): Program { function toTextObject( value: ProgramParserResult.TextObject | string | null, lang: string = '' ): ProgramData.TextObject { if (!value) return { value: '', lang } if (typeof value === 'string') return { value, lang } return { value: typeof value.value === 'string' ? value.value : '', lang: typeof value.lang === 'string' ? value.lang : '' } } function toPersonObject( value: ProgramParserResult.PersonObject | string | null ): ProgramData.PersonObject { if (!value) return { value: '', urls: [], images: [] } if (typeof value === 'string') return { value, urls: [], images: [] } return { value: value.value, urls: toArray(value.urls || value.url).map(toUrlObject), images: toArray(value.images || value.image).map(toImageObject) } } function toSubtitlesObject( value: ProgramParserResult.SubtitlesObject | null ): ProgramData.SubtitlesObject { if (!value) return { type: '', language: [] } return { type: typeof value.type === 'string' ? value.type : '', language: toArray(value.language).map(text => toTextObject(text)) } } function toImageObject( value: ProgramParserResult.ImageObject | string | null ): ProgramData.ImageObject { if (!value) return { type: '', size: '', orient: '', system: '', value: '' } if (typeof value === 'string') return { type: '', size: '', orient: '', system: '', value } return { type: typeof value.type === 'string' ? value.type : '', size: typeof value.size === 'string' ? value.size : '', orient: typeof value.orient === 'string' ? value.orient : '', system: typeof value.system === 'string' ? value.system : '', value: typeof value.value === 'string' ? value.value : '' } } function toRatingObject( value: ProgramParserResult.RatingObject | string | null ): ProgramData.RatingObject { if (!value) return { system: '', icons: [], value: '' } if (typeof value === 'string') return { system: '', icons: [], value } return { system: typeof value.system === 'string' ? value.system : '', icons: toArray(value.icons || value.icon).map(toIconObject), value: typeof value.value === 'string' ? value.value : '' } } function toLengthObject( value: ProgramParserResult.LenghtObject | string | number | null ): ProgramData.LenghtObject { if (!value) return { units: '', value: '' } if (typeof value === 'string') return { units: '', value } if (typeof value === 'number') return { units: '', value: value.toString() } return { units: typeof value.units === 'string' ? value.units : '', value: typeof value.value === 'string' ? value.value : '' } } function toUrlObject( value: ProgramParserResult.UrlObject | string | null ): ProgramData.UrlObject { if (!value) return { system: '', value: '' } if (typeof value === 'string') return { system: '', value } return { system: typeof value.system === 'string' ? value.system : '', value: typeof value.value === 'string' ? value.value : '' } } function toVideoObject( value?: ProgramParserResult.VideoObject | null ): ProgramData.VideoObject { if (!value) return { present: '', colour: '', aspect: '', quality: '' } return { present: typeof value.present === 'string' ? value.present : '', colour: typeof value.colour === 'string' ? value.colour : '', aspect: typeof value.aspect === 'string' ? value.aspect : '', quality: typeof value.quality === 'string' ? value.quality : '' } } function toAudioObject( value?: ProgramParserResult.AudioObject | null ): ProgramData.AudioObject { if (!value) return { present: '', stereo: '' } return { present: typeof value.present === 'string' ? value.present : '', stereo: typeof value.stereo === 'string' ? value.stereo : '' } } function toReviewObject( value?: ProgramParserResult.ReviewObject | null ): ProgramData.ReviewObject { if (!value) return { type: '', source: '', reviewer: '', lang: '', value: '' } return { type: typeof value.type === 'string' ? value.type : '', source: typeof value.source === 'string' ? value.source : '', reviewer: typeof value.reviewer === 'string' ? value.reviewer : '', lang: typeof value.lang === 'string' ? value.lang : '', value: typeof value.value === 'string' ? value.value : '' } } function toPreviouslyShownObject( value?: ProgramParserResult.PreviouslyShownObject | null ): ProgramData.PreviouslyShownObject { if (!value) return { start: '', channel: '' } return { start: typeof value.start === 'string' ? value.start : '', channel: typeof value.channel === 'string' ? value.channel : '' } } function toIconObject( value?: ProgramParserResult.IconObject | string | null ): ProgramData.IconObject { if (!value) return { src: '', width: '', height: '' } if (typeof value === 'string') return { src: value, width: '', height: '' } return { src: typeof value.src === 'string' ? value.src : '', width: typeof value.width === 'string' ? value.width : typeof value.width === 'number' ? value.width.toString() : '', height: typeof value.height === 'string' ? value.height : typeof value.height === 'number' ? value.height.toString() : '' } } function makeEpisodeNumberObjects( seasonNumberAny?: string | number | null, episodeNumberAny?: string | number | null ): ProgramData.EpisodeNumberObject[] { if (!seasonNumberAny) seasonNumberAny = 1 if (!episodeNumberAny) return [] const seasonNumber = typeof seasonNumberAny === 'number' ? seasonNumberAny : parseInt(seasonNumberAny) const episodeNumber = typeof episodeNumberAny === 'number' ? episodeNumberAny : parseInt(episodeNumberAny) return [ createXMLTVNS(seasonNumber, episodeNumber), createOnScreen(seasonNumber, episodeNumber) ].filter((value: ProgramData.EpisodeNumberObject | null) => value !== null) } function toEpisodeNumberObject( value?: ProgramParserResult.EpisodeNumberObject | null ): ProgramData.EpisodeNumberObject { if (!value) return { system: '', value: '' } return { system: typeof value.system === 'string' ? value.system : '', value: typeof value.value === 'string' ? value.value : '' } } function createXMLTVNS( seasonNumber: number, episodeNumber: number ): ProgramData.EpisodeNumberObject | null { if (!episodeNumber) return null seasonNumber = seasonNumber || 1 return { system: 'xmltv_ns', value: `${seasonNumber - 1}.${episodeNumber - 1}.0/1` } } function createOnScreen( seasonNumber: number, episodeNumber: number ): ProgramData.EpisodeNumberObject | null { if (!episodeNumber) return null seasonNumber = seasonNumber || 1 const seasonNumberString = padStart(seasonNumber.toString(), 2, '0') const episodeNumberString = padStart(episodeNumber.toString(), 2, '0') return { system: 'onscreen', value: `S${seasonNumberString}E${episodeNumberString}` } } const lang = channel.lang || undefined const program = new Program({ site: channel.site, start: data.start ? toUnix(data.start) : 0, stop: data.stop ? toUnix(data.stop) : 0, channel: channel.xmltv_id || '', titles: toArray(data.titles || data.title).map(value => toTextObject(value, lang)), subTitles: toArray(data.subTitles || data.subTitle || data.sub_titles || data.sub_title).map( value => toTextObject(value, lang) ), descriptions: toArray(data.descriptions || data.description || data.desc).map(value => toTextObject(value, lang) ), date: data.date ? toUnix(data.date) : 0, categories: toArray(data.categories || data.category).map(value => toTextObject(value, lang)), keywords: toArray(data.keywords || data.keyword).map(value => toTextObject(value, lang)), languages: toArray(data.languages || data.language).map(value => toTextObject(value, lang)), origLanguages: toArray(data.origLanguages || data.origLanguage).map(value => toTextObject(value, lang) ), length: toArray(data.length).map(toLengthObject), urls: toArray(data.urls || data.url).map(toUrlObject), countries: toArray(data.countries || data.country).map(value => toTextObject(value, lang)), episodeNumbers: toArray( data.episodeNum || data.episodeNumber || data.episodeNumbers || makeEpisodeNumberObjects(data.season, data.episode) ).map(toEpisodeNumberObject), video: toVideoObject(data.video), audio: toAudioObject(data.audio), previouslyShown: toArray(data.previouslyShown).map(toPreviouslyShownObject), premiere: toArray(data.premiere).map(value => toTextObject(value, lang)), lastChance: toArray(data.lastChance).map(value => toTextObject(value, lang)), new: data.new || false, subtitles: toArray(data.subtitles).map(toSubtitlesObject), ratings: toArray(data.ratings || data.rating).map(toRatingObject), starRatings: toArray(data.starRatings || data.starRating).map(toRatingObject), reviews: toArray(data.reviews || data.review).map(toReviewObject), directors: toArray(data.directors || data.director).map(toPersonObject), actors: toArray(data.actors || data.actor).map(toPersonObject), writers: toArray(data.writers || data.writer).map(toPersonObject), adapters: toArray(data.adapters || data.adapter).map(toPersonObject), producers: toArray(data.producers || data.producer).map(toPersonObject), composers: toArray(data.composers || data.composer).map(toPersonObject), editors: toArray(data.editors || data.editor).map(toPersonObject), presenters: toArray(data.presenters || data.presenter).map(toPersonObject), commentators: toArray(data.commentators || data.commentator).map(toPersonObject), guests: toArray(data.guests || data.guest).map(toPersonObject), images: toArray(data.images || data.image).map(toImageObject), icons: toArray(data.icons || data.icon).map(toIconObject) }) return program } toXML(): string { const el = createXMLElement function createCastMember(position: string, person: ProgramData.PersonObject) { return el(position, {}, [ escapeString(person.value), ...person.urls.map(createURLElement), ...person.images.map(createImageElement) ]) } function createImageElement(image: ProgramData.ImageObject) { return el( 'image', { type: image.type, size: image.size, orient: image.orient, system: image.system }, [escapeString(image.value)] ) } function createURLElement(url: ProgramData.UrlObject) { return el('url', { system: url.system }, [escapeString(url.value)]) } const children: (XMLElement | null)[] = [ ...this.titles.map(title => el('title', { lang: title.lang }, [escapeString(title.value)])), ...this.subTitles.map(subTitle => el('sub-title', { lang: subTitle.lang }, [escapeString(subTitle.value)]) ), ...this.descriptions.map(desc => el('desc', { lang: desc.lang }, [escapeString(desc.value)])), el('credits', {}, [ ...this.directors.map(data => createCastMember('director', data)), ...this.actors.map(data => createCastMember('actor', data)), ...this.writers.map(data => createCastMember('writer', data)), ...this.adapters.map(data => createCastMember('adapter', data)), ...this.producers.map(data => createCastMember('producer', data)), ...this.composers.map(data => createCastMember('composer', data)), ...this.editors.map(data => createCastMember('editor', data)), ...this.presenters.map(data => createCastMember('presenter', data)), ...this.commentators.map(data => createCastMember('commentator', data)), ...this.guests.map(data => createCastMember('guest', data)) ]), el('date', {}, [formatDate(this.date, 'YYYYMMDD')]), ...this.categories.map(category => el('category', { lang: category.lang }, [escapeString(category.value)]) ), ...this.keywords.map(keyword => el('keyword', { lang: keyword.lang }, [escapeString(keyword.value)]) ), ...this.languages.map(language => el('language', { lang: language.lang }, [escapeString(language.value)]) ), ...this.origLanguages.map(origLanguage => el('orig-language', { lang: origLanguage.lang }, [escapeString(origLanguage.value)]) ), ...this.length.map(length => el('length', { units: length.units }, [escapeString(length.value)]) ), ...this.countries.map(country => el('country', { lang: country.lang }, [escapeString(country.value)]) ), ...this.urls.map(createURLElement), ...this.episodeNumbers.map(episode => el('episode-num', { system: episode.system }, [escapeString(episode.value)]) ), this.video ? el('video', {}, [ el('present', {}, typeof this.video.present === 'string' ? [this.video.present] : []), el('colour', {}, typeof this.video.colour === 'string' ? [this.video.colour] : []), el('aspect', {}, typeof this.video.aspect === 'string' ? [this.video.aspect] : []), el('quality', {}, typeof this.video.quality === 'string' ? [this.video.quality] : []) ]) : null, this.audio ? el('audio', {}, [ el('present', {}, typeof this.audio.present === 'string' ? [this.audio.present] : []), el('stereo', {}, typeof this.audio.stereo === 'string' ? [this.audio.stereo] : []) ]) : null, ...this.previouslyShown.map(previouslyShown => el( 'previously-shown', { start: previouslyShown.start, channel: previouslyShown.channel }, [] ) ), ...this.premiere.map(premiere => el('premiere', { lang: premiere.lang }, [escapeString(premiere.value)]) ), ...this.lastChance.map(lastChance => el('last-chance', { lang: lastChance.lang }, [escapeString(lastChance.value)]) ), this.new ? el('new') : '', ...this.subtitles.map(subtitles => el( 'subtitles', { type: subtitles.type }, subtitles.language.map(language => el('language', { lang: language.lang }, [escapeString(language.value)]) ) ) ), ...this.ratings.map(rating => el('rating', { system: rating.system }, [ el('value', {}, [escapeString(rating.value)]), ...rating.icons.map(icon => el('icon', { src: icon.src, width: icon.width, height: icon.height }) ) ]) ), ...this.starRatings.map(rating => el('star-rating', { system: rating.system }, [ el('value', {}, [escapeString(rating.value)]), ...rating.icons.map(icon => el('icon', { src: icon.src, width: icon.width, height: icon.height }) ) ]) ), ...this.reviews.map(review => el( 'review', { type: review.type, source: review.source, reviewer: review.reviewer, lang: review.lang }, [escapeString(review.value || '')] ) ), ...this.images.map(createImageElement), ...this.icons.map(icon => el('icon', { src: icon.src, width: icon.width, height: icon.height }) ) ] return el( 'programme', { start: formatDate(this.start, 'YYYYMMDDHHmmss ZZ'), stop: formatDate(this.stop, 'YYYYMMDDHHmmss ZZ'), channel: this.channel }, children ) } toObject(): ProgramData { return { site: this.site, start: this.start, stop: this.stop, channel: this.channel, titles: this.titles, subTitles: this.subTitles, descriptions: this.descriptions, date: this.date, categories: this.categories, keywords: this.keywords, languages: this.languages, origLanguages: this.origLanguages, length: this.length, urls: this.urls, countries: this.countries, episodeNumbers: this.episodeNumbers, video: this.video, audio: this.audio, previouslyShown: this.previouslyShown, premiere: this.premiere, lastChance: this.lastChance, new: this.new, subtitles: this.subtitles, ratings: this.ratings, starRatings: this.starRatings, reviews: this.reviews, directors: this.directors, actors: this.actors, writers: this.writers, adapters: this.adapters, producers: this.producers, composers: this.composers, editors: this.editors, presenters: this.presenters, commentators: this.commentators, guests: this.guests, images: this.images, icons: this.icons } } }