import * as cheerio from 'cheerio'; import { COUNTRIES, CSFD_URL_MOVIE_VIDEOS, DEFAULT_CONFIG, GENRES, LANGUAGE_PRIORITY, LocalizedItems, LocalizedItemsTable, PREMIERE_TYPE_MAP } from './const'; import languages from '@cospired/i18n-iso-languages'; import countries from 'i18n-iso-countries'; import codes from 'iso-lang-codes'; import {Creator, CSFDConfig, CSFDItemProps, CSFDItemTrailer, CSFDType, Locale, Premieres, PremiereType} from './types'; import {Fetch} from '@media-info/fetch'; import {CheerioAPI} from "cheerio"; export class CSFDItem { body!: string; _$?: CheerioAPI; locale: Locale = 'cs'; children!: CSFDItem[]; fetcher: Fetch; private _areChildrenLoaded = false; id: number; get $() { return this._$ as CheerioAPI; } set $(v) { this._$ = v; } get areChildrenLoaded() { return this._areChildrenLoaded; } constructor(id: string | number, locale: Locale, private config: CSFDConfig = {}, fetcher?: Fetch) { this.id = parseInt(id.toString()); this.locale = locale; this.fetcher = fetcher || new Fetch({...DEFAULT_CONFIG, ...config}); } async _fetch(url: string) { if (this.config.debug) { console.log("CSFD: Fetching", this.id, url) } const req = await this.fetcher.req(url); if (this.config.debug) { console.log("CSFD: Fetched body", this.id, "Status:", req.status); } if (req.status === 404) { return null; } return req.data; } async _load() { const url = CSFD_URL_MOVIE_VIDEOS(this.id); const data = await this._fetch(url); if (!data) return; this.body = data; this.$ = cheerio.load(this.body); this.children = (await this.childrenIds).map(id => new CSFDItem(id, this.locale, this.fetcher.config, this.fetcher)); } async fetchChildren() { await this.fetcher.chunkedPromise(this.children, async (child) => { if (this.config.debug) { console.log("CSFD: Fetching child", child.id); } await child._load(); }); this._areChildrenLoaded = true; } async _fetchAllChildrenData() { return this.fetcher.chunkedPromise(this.children, async (child) => { return child.fetch(); }); } crypt(a: string) { return a.replace(/[a-z]/gi, function (a) { return String.fromCharCode(a.charCodeAt(0) + ('n' > a.toLowerCase() ? 13 : -13)) }); } _getPeople(creator: Creator) { return this.$('h4:contains("' + creator + ':")').parent().find('a:not(.more)').toArray().map(a => this.$(a).text().trim()); } _translatedItems(itemsMap: LocalizedItemsTable, locale? : Locale): LocalizedItems { const items = itemsMap[locale || this.locale]; return items || {}; } _localizedItem(item: string, itemsMap: LocalizedItemsTable, locale? : Locale) { const t = this._translatedItems(itemsMap, locale)[item]; if (!t && (locale || this.locale) !== 'cs') { console.warn("Missing translation for", item, "in", itemsMap.name); } return t || item; } _createDate(date: string) { const pattern = /(\d{2})\.(\d{2})\.(\d{4})/; const dateStr = date.replace(pattern, '$3-$2-$1'); return new Date(dateStr); }; _getCountryCode(country: string, locale?: Locale) { const code = countries.getAlpha2Code(this.localizedCountry(country, locale), locale || this.locale); if (!code) { console.warn('Missing country code for', country, `(${this.localizedCountry(country, locale)})`, 'in', locale, 'language!'); } return code; } localizedGenre(genre: string, locale?: Locale) { return this._localizedItem(genre, GENRES, locale); } localizedCountry(country: string, locale?: Locale) { return this._localizedItem(country, COUNTRIES, locale); } get cast() { return this._getPeople(Creator.CAST); } get director() { return this._getPeople(Creator.DIRECTOR); } get music() { return this._getPeople(Creator.MUSIC); } get writer() { return this._getPeople(Creator.WRITER); } get camera() { return this._getPeople(Creator.CAMERA); } get author() { return this._getPeople(Creator.AUTHOR); } get genre() { return this.$('.genres').text().split('/').map(i => this.localizedGenre(i.trim(), this.locale)); } get rating() { const rating = this.$('.film-rating-average').text().trim(); return rating ? Math.round(parseInt(rating)) / 10 : undefined; } get _mainTitle() { let title = this.$('.film-header-name h1').first().contents().filter(function () { return this.type === 'text'; }).text().trim(); if (title[0] === '-') { title = title.substring(1).trim(); } return title; } get mainTitle() { return this._mainTitle.replace(/\(.+\)$/, '').trim(); } get titles() { let elements = this.$('.film-names li'); return elements .map((i, element) => { let country = this.$(element).find('img').attr('alt')?.trim(); let name = this.$(element).contents().filter(function () { return this.type === 'text'; }).text().trim(); if (!country || !name) return undefined; const countryCode = this._getCountryCode(country, this.locale) ?? ''; const countryLanguages = codes.findCountryLanguages(countryCode); const languageCode = countryLanguages.find(c => LANGUAGE_PRIORITY.includes(c)) || countryLanguages.shift() || languages.getAlpha2Code(country, 'cs'); return { language: languageCode, country: countryCode, title: name.trim(), } }) .filter((i, e) => !!e) .get() } get trailer(): Promise { const data = this.$('.box-video').map((i, item) => { const item$ = this.$(item); return item$.find('video').map((i, source) => { const source$ = this.$(source); const data = source$.attr("data-videos"); let requestData = data ? JSON.parse(decodeURIComponent(data)) : ""; if (!requestData.length) return; requestData = requestData[0]; return {ele: item, data: requestData}; }).get(); }).get(); return (async () => { const res = []; for (const item of data) { const requestBody = await this.fetcher.req("/api/video-player/?data=" + item.data.request_data); const bodyContent = requestBody.data; const videoData: any = JSON.parse(atob(this.crypt(bodyContent))); const langName = this.$(item.ele).find('.figcaption-video-lang .flag').first().attr('alt'); const subLangCode = item.data.subtitles_language_id === 1 ? 'cs' : undefined; const lang = Object.entries(languages.getNames('cs')).find(([_, v]) => v === langName); const langCode = lang ? lang[0] : undefined; const video = Object.entries(videoData.sources).map(([q, i]: any) => { const j = i.filter((k: any) => k.type.includes("mp4")).shift(); return { quality: parseInt(q), type: j.type, src: j.src } }).sort((a, b) => b.quality - a.quality).shift(); res.push({ name: item.data.description, language: langCode, src: video?.src, quality: video?.quality, subtitles: videoData.subtitles || subLangCode ? [{ src: videoData.subtitles ? videoData.subtitles.src : undefined, language: videoData.subtitles ? videoData.subtitles.srclang : subLangCode, }] : [], }); } return res; })(); } get imdb(): string | undefined { const href = this.$(".links a[href*='imdb.com/title/tt']").first().attr('href'); const match = href ? href.match(/tt\d+/) : undefined; return match ? match[0] : undefined; } get mediaType(): CSFDType { let mediaType = CSFDType.FILM; for (const type of Object.values(CSFDType)) { if (this.$(`.film-header-name .type:contains("(${type})")`).length) { mediaType = type; break; } } return mediaType; } get votes(): number | undefined { const selector = this.$('.box-rating .ratings-btn .counter'); const content = selector.text().replace(/\D/g, ''); return content ? parseInt(content) : undefined; } get plot(): string | undefined { const selector = this.$('.plot-full'); const source = selector.find('.span-more-small').text().trim(); const content = selector.text().trim().replace(source, '').trim(); return content ? content : undefined; } get origin(): string[] | undefined { const text = this.$('.origin').text(); const firstItem = text.substring(0, text.indexOf(',')); return firstItem ? firstItem.split('/').map(x => this._getCountryCode(x.trim())).filter(Boolean) as string[] : undefined; } get year() { let element = this.$('.film-info-content .origin span')[0]; const yearString = this.$(element).text(); return parseInt(yearString.replace(/[()]/g, '').split('–')[0]); } get duration(): number | undefined { const text = this.$('.film-info-content .origin').text(); const time = text.substr(text.lastIndexOf(',') + 1); const hoursS = time ? time.match(/[0-9]+ h/)?.pop() : undefined; const minutesS = time ? time.match(/[0-9]+ m/)?.pop() : undefined; let duration; if (hoursS || minutesS) { let minutes = minutesS ? parseInt(minutesS) : 0; let hours = hoursS ? parseInt(hoursS) : 0; hours = hours ? hours * 60 * 60 : 0; minutes = minutes ? minutes * 60 : 0; duration = hours + minutes; } else { duration = undefined; } return duration; } get poster() { const url = this.$('.film-posters img:not(.empty-image)').attr('src'); return url && !url.includes('poster-free') ? url.replace(/\/cache\/resized\/w\d+/gm, '') : undefined; } _getPremiereType(text: string): PremiereType | undefined { for (const [k, v] of Object.entries(PREMIERE_TYPE_MAP)) { if (text.includes(v)) { return k as PremiereType; } } } get premieres() { const airedTableRows = this.$('.box-premieres li'); const results: Premieres = {}; airedTableRows.each((i, item) => { const item$ = this.$(item); let dateStr = item$.text().trim().match(/\d{2}([\/.-])\d{2}\1\d{4}/g)?.shift(); if (dateStr) { const type = this._getPremiereType(item$.find('p').text()); if (!type) return; const c = item$.find('.flag').attr('alt'); if (!c) return; const countryCode = this._getCountryCode(c, 'cs') ?? ''; const date = this._createDate(dateStr); if (!results[countryCode]) { results[countryCode] = {}; } const d = results[countryCode][type]; if (d && d < date) { return; } results[countryCode][type] = date; } }); return results; } get certification() { const cert = this.$('.age-restriction').text().replace(/\D+/gm, ''); return cert || undefined; } get parent() { let selector = this.$('.film-header-name h1 a'); if (!selector.length) { selector = this.$('.film-info header.film-header h2 a'); } const href = selector.last().attr('href'); const match = href?.match(/.*\/\b([0-9]+)/)?.pop(); return match ? parseInt(match) : undefined; } private _getChildrenIds(cheerioAPI: CheerioAPI) { const selector = cheerioAPI('.film-episodes-list li a'); return selector.map((i, item) => { const item$ = this.$(item); const match = item$.attr('href')?.match(/.*\/\b([0-9]+)/)?.pop(); return match ? parseInt(match) : undefined; }).get().filter(v => !!v); } get childrenIds() { const ids = this._getChildrenIds(this.$); const nextPage = this.nextPage; return (async () => { if (nextPage) { const data = await this._fetch(nextPage); if (data) { const cheerioAPI = cheerio.load(data); ids.push(...this._getChildrenIds(cheerioAPI)); } } return ids; })(); } get nextPage() { const selector = this.$('.film-episodes-list-pagination .page-next'); return selector.last().attr('href'); } get season() { const text = this.$('.film-episodes-list li .film-title-info').first().text(); const season = text?.match(/S[0-9]{2}/)?.pop() || this._mainTitle.match(/S[0-9]{2}/)?.pop(); return season ? parseInt(season.replace(/\D/g, '')) : undefined; } get episode() { const match = this._mainTitle.match(/E(\d+)/)?.pop(); return match ? parseInt(match.replace(/\D/g, '')) : undefined; } async fetch(children = true) { await this._load(); if (!this.body) { throw new Error("Could not get ID " + this.id + " from the server"); } if (children) { if (this.config.debug) console.log("Fetching", this.children.length, "children"); await this.fetchChildren(); } const data: CSFDItemProps = { ids: { csfd: this.id, imdb: this.imdb, }, parent_id: this.parent, children_ids: await this.childrenIds, main_title: this.mainTitle, titles: this.titles, year: this.year, duration: this.duration, mediaType: this.mediaType, poster: this.poster, season: this.season, episode: this.episode, plot: this.plot, rating: this.rating, votes: this.votes, origin: this.origin, premieres: this.premieres, genre: this.genre, director: this.director, writer: this.writer, cast: this.cast, author: this.author, music: this.music, camera: this.camera, trailers: await this.trailer, certification: this.certification, }; delete this._$; data.children = children ? await this._fetchAllChildrenData() : undefined; if (this.config.debug) console.log("Parsed", this.id); return data; } }