diff --git a/src/bin/export-reviews.ts b/src/bin/export-reviews.ts index c7b44955..e860a4c6 100644 --- a/src/bin/export-reviews.ts +++ b/src/bin/export-reviews.ts @@ -30,7 +30,17 @@ export async function runReviewsExport(userId: number, options: ExportReviewsOpt content = JSON.stringify(reviews, null, 2); fileName = `${userId}-reviews.json`; } else { - const headers = ['id', 'title', 'year', 'type', 'colorRating', 'userRating', 'date', 'url', 'text']; + const headers = [ + 'id', + 'title', + 'year', + 'type', + 'colorRating', + 'userRating', + 'date', + 'url', + 'text' + ]; content = [ headers.join(','), ...reviews.map((r) => diff --git a/src/bin/lookup-movie.ts b/src/bin/lookup-movie.ts index 3aed468b..eb50b9e3 100644 --- a/src/bin/lookup-movie.ts +++ b/src/bin/lookup-movie.ts @@ -13,15 +13,22 @@ export async function runMovieLookup(movieId: number, json: boolean): Promise value ? ` ${c.dim(label.padEnd(11))} ${value}` : ''; const names = (arr: { name: string }[], max = 5) => - arr.slice(0, max).map((x) => x.name).join(', '); + arr + .slice(0, max) + .map((x) => x.name) + .join(', '); const description = movie.descriptions?.[0] ? movie.descriptions[0].length > 160 @@ -35,18 +42,22 @@ function printMovie(movie: CSFDMovie) { '', c.bold(movie.title) + c.dim(` (${movie.year ?? '?'})`) + ' · ' + c.dim(movie.type ?? ''), c.dim('─'.repeat(52)), - row('Rating', movie.rating != null - ? ratingColor(c.bold(movie.rating + '%')) + c.dim(` (${movie.ratingCount?.toLocaleString()} ratings)`) - : c.dim('no rating')), - row('Genres', movie.genres?.join(', ') ?? ''), - row('Origins', movie.origins?.join(', ') ?? ''), - row('Duration', movie.duration ? movie.duration + ' min' : ''), + row( + 'Rating', + movie.rating != null + ? ratingColor(c.bold(movie.rating + '%')) + + c.dim(` (${movie.ratingCount?.toLocaleString()} ratings)`) + : c.dim('no rating') + ), + row('Genres', movie.genres?.join(', ') ?? ''), + row('Origins', movie.origins?.join(', ') ?? ''), + row('Duration', movie.duration ? movie.duration + ' min' : ''), row('Directors', names(movie.creators?.directors ?? [])), - row('Cast', names(movie.creators?.actors ?? [])), + row('Cast', names(movie.creators?.actors ?? [])), description ? '\n ' + c.dim(description) : '', vod ? '\n' + row('VOD', vod) : '', row('URL', c.dim(movie.url ?? '')), - '', + '' ].filter(Boolean); console.log(lines.join('\n')); diff --git a/src/bin/search.ts b/src/bin/search.ts index 9d1322bd..6d7abeb0 100644 --- a/src/bin/search.ts +++ b/src/bin/search.ts @@ -13,17 +13,27 @@ export async function runSearch(query: string, json: boolean): Promise { function printSearch(query: string, results: CSFDSearch) { const ratingDot = (colorRating: string | null) => - colorRating === 'good' ? c.green('●') : - colorRating === 'average' ? c.yellow('●') : - colorRating === 'bad' ? c.red('●') : c.dim('●'); + colorRating === 'good' + ? c.green('●') + : colorRating === 'average' + ? c.yellow('●') + : colorRating === 'bad' + ? c.red('●') + : c.dim('●'); const section = (label: string, count: number) => count > 0 ? `\n${c.bold(label)} ${c.dim(`(${count})`)}` : null; - const total = results.movies.length + results.tvSeries.length + results.creators.length + results.users.length; + const total = + results.movies.length + + results.tvSeries.length + + results.creators.length + + results.users.length; console.log(''); - console.log(`${c.bold('Search results for')} ${c.cyan(`"${query}"`)} ${c.dim(`— ${total} found`)}`); + console.log( + `${c.bold('Search results for')} ${c.cyan(`"${query}"`)} ${c.dim(`— ${total} found`)}` + ); console.log(c.dim('─'.repeat(52))); const movieLine = (r: CSFDSearch['movies'][0]) => @@ -43,15 +53,16 @@ function printSearch(query: string, results: CSFDSearch) { if (results.creators.length > 0) { console.log(section('Creators', results.creators.length)); - results.creators.forEach((r) => - console.log(` ${c.dim(String(r.id).padEnd(8))} ${r.name}`) - ); + results.creators.forEach((r) => console.log(` ${c.dim(String(r.id).padEnd(8))} ${r.name}`)); } if (results.users.length > 0) { console.log(section('Users', results.users.length)); results.users.forEach((r) => - console.log(` ${c.dim(String(r.id).padEnd(8))} ${r.user}` + (r.userRealName ? c.dim(` (${r.userRealName})`) : '')) + console.log( + ` ${c.dim(String(r.id).padEnd(8))} ${r.user}` + + (r.userRealName ? c.dim(` (${r.userRealName})`) : '') + ) ); } diff --git a/src/bin/utils.ts b/src/bin/utils.ts index bf25b895..033d9a53 100644 --- a/src/bin/utils.ts +++ b/src/bin/utils.ts @@ -3,12 +3,12 @@ export const useColor = process.stdout.isTTY && !process.env['NO_COLOR']; export const c = { - bold: (s: string) => useColor ? `\x1b[1m${s}\x1b[22m` : s, - dim: (s: string) => useColor ? `\x1b[2m${s}\x1b[22m` : s, - cyan: (s: string) => useColor ? `\x1b[36m${s}\x1b[39m` : s, - green: (s: string) => useColor ? `\x1b[32m${s}\x1b[39m` : s, - yellow: (s: string) => useColor ? `\x1b[33m${s}\x1b[39m` : s, - red: (s: string) => useColor ? `\x1b[31m${s}\x1b[39m` : s, + bold: (s: string) => (useColor ? `\x1b[1m${s}\x1b[22m` : s), + dim: (s: string) => (useColor ? `\x1b[2m${s}\x1b[22m` : s), + cyan: (s: string) => (useColor ? `\x1b[36m${s}\x1b[39m` : s), + green: (s: string) => (useColor ? `\x1b[32m${s}\x1b[39m` : s), + yellow: (s: string) => (useColor ? `\x1b[33m${s}\x1b[39m` : s), + red: (s: string) => (useColor ? `\x1b[31m${s}\x1b[39m` : s) }; export const err = (msg: string) => c.red(c.bold('✖ Error:')) + ' ' + msg; diff --git a/src/dto/options.ts b/src/dto/options.ts index 4b8ea9fd..e350bb79 100644 --- a/src/dto/options.ts +++ b/src/dto/options.ts @@ -3,4 +3,4 @@ export interface CSFDOptions { request?: RequestInit; } -export type CSFDLanguage = 'cs' | 'en' | 'sk'; \ No newline at end of file +export type CSFDLanguage = 'cs' | 'en' | 'sk'; diff --git a/src/helpers/creator.helper.ts b/src/helpers/creator.helper.ts index e9ddafa1..431fac11 100644 --- a/src/helpers/creator.helper.ts +++ b/src/helpers/creator.helper.ts @@ -2,7 +2,7 @@ import { HTMLElement } from 'node-html-parser'; import { CSFDCreatorScreening } from '../dto/creator'; import { CSFDColorRating } from '../dto/global'; import { CSFDColors } from '../dto/user-ratings'; -import { addProtocol, parseColor, parseDate, parseIdFromUrl } from './global.helper'; +import { addProtocol, getFirstLine, parseColor, parseDate, parseIdFromUrl } from './global.helper'; const getCreatorColorRating = (el: HTMLElement | null): CSFDColorRating => { const classes: string[] = el?.classNames.split(' ') ?? []; @@ -42,7 +42,7 @@ export const getCreatorBirthdayInfo = ( export const getCreatorBio = (el: HTMLElement | null): string | null => { const p = el?.querySelector('.article-content p'); - const first = p?.text?.trim().split('\n')[0]?.trim(); + const first = getFirstLine(p?.text?.trim())?.trim(); return first || null; }; diff --git a/src/helpers/global.helper.ts b/src/helpers/global.helper.ts index bfb27f8e..e8f2fa11 100644 --- a/src/helpers/global.helper.ts +++ b/src/helpers/global.helper.ts @@ -5,6 +5,18 @@ const LANG_PREFIX_REGEX = /^[a-z]{2,3}$/; const ISO8601_DURATION_REGEX = /(-)?P(?:([.,\d]+)Y)?(?:([.,\d]+)M)?(?:([.,\d]+)W)?(?:([.,\d]+)D)?T(?:([.,\d]+)H)?(?:([.,\d]+)M)?(?:([.,\d]+)S)?/; +export const getLastWord = (text: string): string => { + if (!text) return ''; + const idx = text.lastIndexOf(' '); + return idx === -1 ? text : text.substring(idx + 1); +}; + +export const getFirstLine = (text: string): string => { + if (!text) return ''; + const idx = text.indexOf('\n'); + return idx === -1 ? text : text.substring(0, idx); +}; + export const parseIdFromUrl = (url: string): number => { if (!url) return null; diff --git a/src/helpers/movie.helper.ts b/src/helpers/movie.helper.ts index 1cc2f8da..c0da9497 100644 --- a/src/helpers/movie.helper.ts +++ b/src/helpers/movie.helper.ts @@ -21,6 +21,7 @@ import type { CSFDOptions } from '../types'; import { addProtocol, getColor, + getFirstLine, parseDate, parseFilmType, parseISO8601Duration, @@ -147,7 +148,10 @@ export const getMovieOrigins = (el: HTMLElement): string[] => { const originNode = el.querySelector('.origin'); if (!originNode) return []; const text = originNode.childNodes[0]?.text || ''; - return text.split('/').map(x => x.trim()).filter(x => x); + return text + .split('/') + .map((x) => x.trim()) + .filter((x) => x); }; export const getMovieColorRating = (bodyClasses: string[]): CSFDColorRating => { @@ -219,7 +223,7 @@ export const getMovieTitlesOther = (el: HTMLElement): CSFDTitlesOther[] => { const titlesOther = namesNode.map((el) => { const country = el.querySelector('img.flag').attributes.alt; - const title = el.textContent.trim().split('\n')[0]; + const title = getFirstLine(el.textContent.trim()); if (country && title) { return { @@ -261,10 +265,12 @@ export const getMovieRandomPhoto = (el: HTMLElement | null): string => { } }; +const CLEAN_TEXT_REGEX = /(\r\n|\n|\r|\t)/gm; + export const getMovieTrivia = (el: HTMLElement | null): string[] => { const triviaNodes = el.querySelectorAll('.article-trivia ul li'); if (triviaNodes?.length) { - return triviaNodes.map((node) => node.textContent.trim().replace(/(\r\n|\n|\r|\t)/gm, '')); + return triviaNodes.map((node) => node.textContent.trim().replace(CLEAN_TEXT_REGEX, '')); } else { return null; } @@ -273,7 +279,7 @@ export const getMovieTrivia = (el: HTMLElement | null): string[] => { export const getMovieDescriptions = (el: HTMLElement): string[] => { return el .querySelectorAll('.body--plots .plot-full p, .body--plots .plots .plots-item p') - .map((movie) => movie.textContent?.trim().replace(/(\r\n|\n|\r|\t)/gm, '')); + .map((movie) => movie.textContent?.trim().replace(CLEAN_TEXT_REGEX, '')); }; const parseMoviePeople = (el: HTMLElement): CSFDMovieCreator[] => { @@ -434,7 +440,7 @@ export const getMovieGroup = ( export const getMovieType = (el: HTMLElement): CSFDFilmTypes => { const type = el.querySelector('.film-header-name .type'); - const text = type?.innerText?.replace(/[{()}]/g, '').split('\n')[0].trim() || 'film'; + const text = getFirstLine(type?.innerText?.replace(/[{()}]/g, ''))?.trim() || 'film'; return parseFilmType(text); }; diff --git a/src/helpers/search-user.helper.ts b/src/helpers/search-user.helper.ts index 66ecd143..1d8f5be1 100644 --- a/src/helpers/search-user.helper.ts +++ b/src/helpers/search-user.helper.ts @@ -9,7 +9,9 @@ export const getUserRealName = (el: HTMLElement): string => { const p = el.querySelector('.article-content p'); if (!p) return null; - const textNodes = p.childNodes.filter(n => n.nodeType === NodeType.TEXT_NODE && n.rawText.trim() !== ''); + const textNodes = p.childNodes.filter( + (n) => n.nodeType === NodeType.TEXT_NODE && n.rawText.trim() !== '' + ); const name = textNodes.length ? textNodes[0].rawText.trim() : null; return name; diff --git a/src/helpers/search.helper.ts b/src/helpers/search.helper.ts index 7ec62343..ac5ab682 100644 --- a/src/helpers/search.helper.ts +++ b/src/helpers/search.helper.ts @@ -2,7 +2,13 @@ import { HTMLElement } from 'node-html-parser'; import { CSFDColorRating, CSFDFilmTypes } from '../dto/global'; import { CSFDMovieCreator } from '../dto/movie'; import { CSFDColors } from '../dto/user-ratings'; -import { addProtocol, parseColor, parseFilmType, parseIdFromUrl } from './global.helper'; +import { + addProtocol, + getLastWord, + parseColor, + parseFilmType, + parseIdFromUrl +} from './global.helper'; type Creator = 'Režie:' | 'Hrají:'; @@ -26,7 +32,7 @@ export const getSearchUrl = (el: HTMLElement): string => { export const getSearchColorRating = (el: HTMLElement): CSFDColorRating => { return parseColor( - el.querySelector('.article-header i.icon').classNames.split(' ').pop() as CSFDColors + getLastWord(el.querySelector('.article-header i.icon').classNames) as CSFDColors ); }; diff --git a/src/helpers/user-ratings.helper.ts b/src/helpers/user-ratings.helper.ts index ac177ca1..1c935ad9 100644 --- a/src/helpers/user-ratings.helper.ts +++ b/src/helpers/user-ratings.helper.ts @@ -1,7 +1,7 @@ import { HTMLElement } from 'node-html-parser'; import { CSFDColorRating, CSFDFilmTypes, CSFDStars } from '../dto/global'; import { CSFDColors } from '../dto/user-ratings'; -import { parseColor, parseDate, parseFilmType, parseIdFromUrl } from './global.helper'; +import { getLastWord, parseColor, parseDate, parseFilmType, parseIdFromUrl } from './global.helper'; export const getUserRatingId = (el: HTMLElement): number => { const url = el.querySelector('td.name .film-title-name').attributes.href; @@ -9,7 +9,7 @@ export const getUserRatingId = (el: HTMLElement): number => { }; export const getUserRating = (el: HTMLElement): CSFDStars => { - const ratingText = el.querySelector('td.star-rating-only .stars').classNames.split(' ').pop(); + const ratingText = getLastWord(el.querySelector('td.star-rating-only .stars').classNames); const rating = ratingText.includes('stars-') ? +ratingText.split('-').pop() : 0; return rating as CSFDStars; diff --git a/src/helpers/user-reviews.helper.ts b/src/helpers/user-reviews.helper.ts index c7116ed1..00c45c61 100644 --- a/src/helpers/user-reviews.helper.ts +++ b/src/helpers/user-reviews.helper.ts @@ -1,7 +1,7 @@ import { HTMLElement } from 'node-html-parser'; import { CSFDColorRating, CSFDFilmTypes, CSFDStars } from '../dto/global'; import { CSFDColors } from '../dto/user-ratings'; -import { parseColor, parseDate, parseFilmType, parseIdFromUrl } from './global.helper'; +import { getLastWord, parseColor, parseDate, parseFilmType, parseIdFromUrl } from './global.helper'; export const getUserReviewId = (el: HTMLElement): number => { const url = el.querySelector('.film-title-name').attributes.href; @@ -9,7 +9,10 @@ export const getUserReviewId = (el: HTMLElement): number => { }; export const getUserReviewRating = (el: HTMLElement): CSFDStars => { - const ratingText = el.querySelector('.star-rating .stars').classNames.split(' ').pop(); + const starsNode = el.querySelector('.star-rating .stars'); + if (!starsNode) return 0 as CSFDStars; + + const ratingText = getLastWord(starsNode.classNames); const rating = ratingText.includes('stars-') ? +ratingText.split('-').pop() : 0; return rating as CSFDStars; @@ -32,7 +35,7 @@ export const getUserReviewYear = (el: HTMLElement): number | null => { export const getUserReviewColorRating = (el: HTMLElement): CSFDColorRating => { const icon = el.querySelector('.film-title-inline i.icon'); - const color = parseColor(icon?.classNames.split(' ').pop() as CSFDColors); + const color = parseColor(getLastWord(icon?.classNames) as CSFDColors); return color; }; diff --git a/src/index.ts b/src/index.ts index b672d235..17b8c665 100644 --- a/src/index.ts +++ b/src/index.ts @@ -96,4 +96,3 @@ export const csfd = new Csfd( ); export type * from './dto'; - diff --git a/src/services/movie.service.ts b/src/services/movie.service.ts index bb145106..4839606f 100644 --- a/src/services/movie.service.ts +++ b/src/services/movie.service.ts @@ -52,7 +52,15 @@ export class MovieScraper { } catch (e) { console.error(LIB_PREFIX + ' Error parsing JSON-LD', e); } - return this.buildMovie(+movieId, movieHtml, movieNode as HTMLElement, asideNode as HTMLElement, pageClasses, jsonLd, options); + return this.buildMovie( + +movieId, + movieHtml, + movieNode as HTMLElement, + asideNode as HTMLElement, + pageClasses, + jsonLd, + options + ); } private buildMovie( diff --git a/src/services/user-ratings.service.ts b/src/services/user-ratings.service.ts index 09244fe8..c100ca5c 100644 --- a/src/services/user-ratings.service.ts +++ b/src/services/user-ratings.service.ts @@ -62,7 +62,10 @@ export class UserRatingsScraper { const films: CSFDUserRatings[] = []; if (config) { if (config.includesOnly?.length && config.excludes?.length) { - console.warn(`${LIB_PREFIX} Both 'includesOnly' and 'excludes' were provided. 'includesOnly' takes precedence:`, config.includesOnly); + console.warn( + `${LIB_PREFIX} Both 'includesOnly' and 'excludes' were provided. 'includesOnly' takes precedence:`, + config.includesOnly + ); } } diff --git a/src/services/user-reviews.service.ts b/src/services/user-reviews.service.ts index 88f5f498..95e28a43 100644 --- a/src/services/user-reviews.service.ts +++ b/src/services/user-reviews.service.ts @@ -64,7 +64,10 @@ export class UserReviewsScraper { const films: CSFDUserReviews[] = []; if (config) { if (config.includesOnly?.length && config.excludes?.length) { - console.warn(`${LIB_PREFIX} Both 'includesOnly' and 'excludes' were provided. 'includesOnly' takes precedence:`, config.includesOnly); + console.warn( + `${LIB_PREFIX} Both 'includesOnly' and 'excludes' were provided. 'includesOnly' takes precedence:`, + config.includesOnly + ); } } diff --git a/src/types.ts b/src/types.ts index 0cfcef34..c5fdc0c6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,8 @@ -export * from "./dto/cinema"; -export * from "./dto/creator"; -export * from "./dto/global"; -export * from "./dto/movie"; -export * from "./dto/options"; -export * from "./dto/search"; -export * from "./dto/user-ratings"; -export * from "./dto/user-reviews"; - - +export * from './dto/cinema'; +export * from './dto/creator'; +export * from './dto/global'; +export * from './dto/movie'; +export * from './dto/options'; +export * from './dto/search'; +export * from './dto/user-ratings'; +export * from './dto/user-reviews'; diff --git a/src/vars.ts b/src/vars.ts index 41d54058..f1db3109 100644 --- a/src/vars.ts +++ b/src/vars.ts @@ -11,7 +11,7 @@ type Options = { const LANGUAGE_DOMAIN_MAP: Record = { cs: 'https://www.csfd.cz', en: 'https://www.csfd.cz/en', - sk: 'https://www.csfd.cz/sk', + sk: 'https://www.csfd.cz/sk' }; let BASE_URL = LANGUAGE_DOMAIN_MAP.cs; @@ -32,10 +32,16 @@ export const getUrlByLanguage = (language?: CSFDLanguage): string => { export const userUrl = (user: string | number, options: Options): string => `${getUrlByLanguage(options?.language)}/uzivatel/${encodeURIComponent(user)}`; -export const userRatingsUrl = (user: string | number, page?: number, options: Options = {}): string => - `${userUrl(user, options)}/hodnoceni/${page ? '?page=' + page : ''}`; -export const userReviewsUrl = (user: string | number, page?: number, options: Options = {}): string => - `${userUrl(user, options)}/recenze/${page ? '?page=' + page : ''}`; +export const userRatingsUrl = ( + user: string | number, + page?: number, + options: Options = {} +): string => `${userUrl(user, options)}/hodnoceni/${page ? '?page=' + page : ''}`; +export const userReviewsUrl = ( + user: string | number, + page?: number, + options: Options = {} +): string => `${userUrl(user, options)}/recenze/${page ? '?page=' + page : ''}`; // Movie URLs export const movieUrl = (movie: number, options: Options): string => @@ -45,9 +51,12 @@ export const creatorUrl = (creator: number | string, options: Options): string = `${getUrlByLanguage(options?.language)}/tvurce/${encodeURIComponent(creator)}`; // Cinema URLs -export const cinemasUrl = (district: number | string, period: CSFDCinemaPeriod, options: Options): string => - `${getUrlByLanguage(options?.language)}/kino/?period=${period}&district=${district}`; +export const cinemasUrl = ( + district: number | string, + period: CSFDCinemaPeriod, + options: Options +): string => `${getUrlByLanguage(options?.language)}/kino/?period=${period}&district=${district}`; // Search URLs export const searchUrl = (text: string, options: Options): string => - `${getUrlByLanguage(options?.language)}/hledat/?q=${encodeURIComponent(text)}`; \ No newline at end of file + `${getUrlByLanguage(options?.language)}/hledat/?q=${encodeURIComponent(text)}`;