Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/bin/export-reviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
35 changes: 23 additions & 12 deletions src/bin/lookup-movie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,22 @@ export async function runMovieLookup(movieId: number, json: boolean): Promise<vo

function printMovie(movie: CSFDMovie) {
const ratingColor =
movie.colorRating === 'good' ? c.green :
movie.colorRating === 'average' ? c.yellow :
movie.colorRating === 'bad' ? c.red : c.dim;
movie.colorRating === 'good'
? c.green
: movie.colorRating === 'average'
? c.yellow
: movie.colorRating === 'bad'
? c.red
: c.dim;

const row = (label: string, value: string) =>
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
Expand All @@ -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'));
Expand Down
29 changes: 20 additions & 9 deletions src/bin/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,27 @@ export async function runSearch(query: string, json: boolean): Promise<void> {

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]) =>
Expand All @@ -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})`) : '')
)
);
}

Expand Down
12 changes: 6 additions & 6 deletions src/bin/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/dto/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ export interface CSFDOptions {
request?: RequestInit;
}

export type CSFDLanguage = 'cs' | 'en' | 'sk';
export type CSFDLanguage = 'cs' | 'en' | 'sk';
3 changes: 3 additions & 0 deletions src/helpers/global.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export const parseIdFromUrl = (url: string): number => {
if (/^\d+-/.test(p)) {
return +p.split('-')[0] || null;
}
if (/^\d+$/.test(p)) {
return +p || null;
}
}

// Fallback
Expand Down
4 changes: 3 additions & 1 deletion src/helpers/search-user.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,3 @@ export const csfd = new Csfd(
);

export type * from './dto';

10 changes: 9 additions & 1 deletion src/services/movie.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,15 @@ export class MovieScraper {
} catch (e) {
console.error(LIB_PREFIX + ' Error parsing JSON-LD', e);
}
return this.buildMovie(id, movieHtml, movieNode as HTMLElement, asideNode as HTMLElement, pageClasses, jsonLd, options);
return this.buildMovie(
id,
movieHtml,
movieNode as HTMLElement,
asideNode as HTMLElement,
pageClasses,
jsonLd,
options
);
}

private buildMovie(
Expand Down
15 changes: 12 additions & 3 deletions src/services/user-ratings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getUserRatingYear
} from '../helpers/user-ratings.helper';
import { CSFDOptions } from '../types';
import { extractId } from '../helpers/global.helper';
import { LIB_PREFIX, userRatingsUrl } from '../vars';

export class UserRatingsScraper {
Expand All @@ -22,9 +23,14 @@ export class UserRatingsScraper {
config?: CSFDUserRatingConfig,
options?: CSFDOptions
): Promise<CSFDUserRatings[]> {
const id = extractId(user);
if (id === null || isNaN(id)) {
throw new Error('node-csfd-api: user must be a valid number or url');
}

let allMovies: CSFDUserRatings[] = [];
const pageToFetch = config?.page || 1;
const url = userRatingsUrl(user, pageToFetch > 1 ? pageToFetch : undefined, {
const url = userRatingsUrl(id, pageToFetch > 1 ? pageToFetch : undefined, {
language: options?.language
});
const response = await fetchPage(url, { ...options?.request });
Expand All @@ -40,7 +46,7 @@ export class UserRatingsScraper {
if (config?.allPages) {
for (let i = 2; i <= pages; i++) {
config.onProgress?.(i, pages);
const url = userRatingsUrl(user, i, { language: options?.language });
const url = userRatingsUrl(id, i, { language: options?.language });
const response = await fetchPage(url, { ...options?.request });

const items = parse(response);
Expand All @@ -62,7 +68,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
);
}
}

Expand Down
15 changes: 12 additions & 3 deletions src/services/user-reviews.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getUserReviewYear
} from '../helpers/user-reviews.helper';
import { CSFDOptions } from '../types';
import { extractId } from '../helpers/global.helper';
import { LIB_PREFIX, userReviewsUrl } from '../vars';

export class UserReviewsScraper {
Expand All @@ -24,9 +25,14 @@ export class UserReviewsScraper {
config?: CSFDUserReviewsConfig,
options?: CSFDOptions
): Promise<CSFDUserReviews[]> {
const id = extractId(user);
if (id === null || isNaN(id)) {
throw new Error('node-csfd-api: user must be a valid number or url');
}

let allReviews: CSFDUserReviews[] = [];
const pageToFetch = config?.page || 1;
const url = userReviewsUrl(user, pageToFetch > 1 ? pageToFetch : undefined, {
const url = userReviewsUrl(id, pageToFetch > 1 ? pageToFetch : undefined, {
language: options?.language
});
const response = await fetchPage(url, { ...options?.request });
Expand All @@ -42,7 +48,7 @@ export class UserReviewsScraper {
if (config?.allPages) {
for (let i = 2; i <= pages; i++) {
config.onProgress?.(i, pages);
const url = userReviewsUrl(user, i, { language: options?.language });
const url = userReviewsUrl(id, i, { language: options?.language });
const response = await fetchPage(url, { ...options?.request });

const items = parse(response);
Expand All @@ -64,7 +70,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
);
}
}

Expand Down
18 changes: 8 additions & 10 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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';
34 changes: 24 additions & 10 deletions src/vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type Options = {
const LANGUAGE_DOMAIN_MAP: Record<CSFDLanguage, string> = {
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;
Expand All @@ -29,13 +29,24 @@ export const getUrlByLanguage = (language?: CSFDLanguage): string => {
};

// User URLs
export const userUrl = (user: string | number, options: Options): string =>
`${getUrlByLanguage(options?.language)}/uzivatel/${encodeURIComponent(user)}`;
export const userUrl = (user: string | number, options: Options): string => {
// If user is a full url, return it directly to avoid double encoding
if (typeof user === 'string' && user.startsWith('http')) {
return user.replace(/\/$/, '');
}
return `${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 =>
Expand All @@ -45,9 +56,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)}`;
`${getUrlByLanguage(options?.language)}/hledat/?q=${encodeURIComponent(text)}`;