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
5 changes: 5 additions & 0 deletions barrels.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"directory": "./dist/",
"name": "index.dto.d",
"noHeader": true
}
36 changes: 27 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
"name": "node-csfd-api",
"version": "2.14.1",
"description": "ČSFD API in JavaScript. Amazing NPM library for scrapping csfd.cz :)",
"main": "index.js",
"author": "BART! <bart@bartweb.cz>",
"scripts": {
"start": "tsc -w",
"prebuild": "rimraf dist",
"build": "tsc",
"postbuild": "npm-prepare-dist -s postinstall -s prepare",
"build": "tsc && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && yarn barrels",
"barrels": "barrelsby --delete -c barrels.json",
"postbuild": "npm-prepare-dist -s postinstall -s prepare && yarn fix-paths",
"tsc": "tsc",
"demo": "tsx demo",
"lint": "eslint ./src/**/**/* --fix",
"test": "vitest",
"test:coverage": "yarn test run --coverage",
"fix-paths": "yarn json -I -f ./dist/package.json -e \"this.module='./esm/index.js';this.main='./cjs/index.js';this.types='./index.dto.d.ts'\"",
"publish:next": "yarn && yarn build && yarn test:coverage && cd dist && npm publish --tag next",
"postversion": "git push && git push --follow-tags",
"release:beta": "npm version preminor --preid=beta -m \"chore(update): prelease %s β\"",
Expand All @@ -34,22 +35,24 @@
"devDependencies": {
"@babel/preset-typescript": "^7.27.1",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.35.0",
"@eslint/js": "^9.36.0",
"@types/express": "^5.0.3",
"@types/node": "^24.3.1",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0",
"@types/node": "^24.5.2",
"@typescript-eslint/eslint-plugin": "^8.44.1",
"@typescript-eslint/parser": "^8.44.1",
"@vitest/coverage-istanbul": "^3.2.4",
"@vitest/ui": "3.2.4",
"barrelsby": "^2.8.1",
"dotenv": "^17.2.2",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"express": "^5.1.0",
"globals": "^16.4.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.6",
"json": "^11.0.0",
"lint-staged": "^16.2.0",
"npm-prepare-dist": "^0.5.0",
"prettier": "^3.6.2",
"rimraf": "^6.0.1",
Expand Down Expand Up @@ -84,5 +87,20 @@
"license": "MIT",
"lint-staged": {
"*.ts": "eslint --cache --fix"
},
"main": "./cjs/index.js",
"module": "./esm/index.js",
"types": "./index.dto.d.ts",
"exports": {
".": {
"import": {
"types": "./index.dto.d.ts",
"default": "./esm/index.js"
},
"require": {
"types": "./index.dto.d.ts",
"default": "./cjs/index.js"
}
}
}
}
2 changes: 1 addition & 1 deletion server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'dotenv/config';
import express, { NextFunction, Request, Response } from 'express';
import packageJson from './package.json';
import { csfd } from './src';
import { CSFDFilmTypes } from './src/interfaces/global';
import { CSFDFilmTypes } from './src/dto/global';

type Severity = 'info' | 'warn' | 'error' | 'success';

Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/cinema.interface.ts → src/dto/cinema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CSFDMovieListItem } from './movie.interface';
import { CSFDMovieListItem } from './movie';

export interface CSFDCinema {
id: number;
Expand Down
File renamed without changes.
File renamed without changes.
22 changes: 11 additions & 11 deletions src/interfaces/movie.interface.ts → src/dto/movie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,24 @@ export interface CSFDVod {
}

export interface CSFDCreators {
directors: CSFDCreator[];
writers: CSFDCreator[];
cinematography: CSFDCreator[];
music: CSFDCreator[];
actors: CSFDCreator[];
basedOn: CSFDCreator[];
producers: CSFDCreator[];
filmEditing: CSFDCreator[];
costumeDesign: CSFDCreator[];
productionDesign: CSFDCreator[];
directors: CSFDMovieCreator[];
writers: CSFDMovieCreator[];
cinematography: CSFDMovieCreator[];
music: CSFDMovieCreator[];
actors: CSFDMovieCreator[];
basedOn: CSFDMovieCreator[];
producers: CSFDMovieCreator[];
filmEditing: CSFDMovieCreator[];
costumeDesign: CSFDMovieCreator[];
productionDesign: CSFDMovieCreator[];
}

export interface CSFDTitlesOther {
country: string;
title: string;
}

export interface CSFDCreator {
export interface CSFDMovieCreator {
/**
* CSFD person ID.
*
Expand Down
8 changes: 4 additions & 4 deletions src/interfaces/search.interface.ts → src/dto/search.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CSFDScreening } from './global';
import { CSFDCreator } from './movie.interface';
import { CSFDMovieCreator } from './movie';

export interface CSFDSearch {
movies: CSFDSearchMovie[];
Expand All @@ -22,11 +22,11 @@ export interface CSFDSearchUser {
url: string;
}

export interface CSFDSearchCreator extends CSFDCreator {
export interface CSFDSearchCreator extends CSFDMovieCreator {
image: string;
}

export interface CSFDSearchCreators {
directors: CSFDCreator[];
actors: CSFDCreator[];
directors: CSFDMovieCreator[];
actors: CSFDMovieCreator[];
}
File renamed without changes.
30 changes: 15 additions & 15 deletions src/helpers/cinema.helper.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import { HTMLElement } from 'node-html-parser';
import { CSFDColorRating } from '../interfaces/global';
import { Colors } from '../interfaces/user-ratings.interface';
import {
CSFDCinemaGroupedFilmsByDate,
CSFDCinemaMeta,
CSFDCinemaMovie
} from './../interfaces/cinema.interface';
} from '../dto/cinema';
import { CSFDColorRating } from '../dto/global';
import { Colors } from '../dto/user-ratings';
import { parseColor, parseIdFromUrl } from './global.helper';

export const getColorRating = (el: HTMLElement): CSFDColorRating => {
return parseColor(el?.classNames.split(' ').pop() as Colors);
export const getCinemaColorRating = (el: HTMLElement | null): CSFDColorRating => {
const classes: string[] = el?.classNames.split(' ') ?? [];
const last = classes.length ? classes[classes.length - 1] : undefined;
return last ? parseColor(last as Colors) : 'unknown';
};

export const getCinemaId = (el: HTMLElement | null): number => {
const id = el?.id?.split('-')[1];
return +id;
};

export const getId = (url: string): number | null => {
if (url) {
return parseIdFromUrl(url);
}
return null;
export const getCinemaUrlId = (url: string | null | undefined): number | null => {
if (!url) return null;
return parseIdFromUrl(url);
};
Comment on lines +22 to 25
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Align parseIdFromUrl’s type: it returns null in some paths

This wrapper returns number | null, but parseIdFromUrl is declared as number while returning null in its implementation. Fix the source to avoid unsound typing.

Update src/helpers/global.helper.ts:

-export const parseIdFromUrl = (url: string): number => {
+export const parseIdFromUrl = (url: string): number | null => {
   if (url) {
     const idSlug = url?.split('/')[2];
     const id = idSlug?.split('-')[0];
     return +id || null;
   } else {
     return null;
   }
 };

Run to confirm all signatures/usages are consistent:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== Declarations =="
rg -nP -C2 'export\s+const\s+parseIdFromUrl\s*=\s*\(url:\s*string\)\s*:\s*number\b'
echo
echo "== Call sites (context) =="
rg -nP -C3 '\bparseIdFromUrl\s*\('

Length of output: 6013


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== parseIdFromUrl declaration =="
sed -n '1,120p' src/helpers/global.helper.ts
echo
echo "== Call sites with surrounding context (8 lines) =="
rg -nP -C8 '\bparseIdFromUrl\s*\(' || true

Length of output: 14142


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== Locations that return parseIdFromUrl (with context) =="
rg -nP -C4 'return\s+parseIdFromUrl\(' || true
echo
echo "== Functions exporting and returning parseIdFromUrl result (signatures) =="
rg -nP --hidden --no-ignore-vcs 'export\s+const\s+[A-Za-z0-9_]+\s*=\s*\(.*\)\s*:\s*number\b' -n -C1 || true
echo
echo "== DTO/type definitions referencing 'id' fields (searching common DTO names) =="
rg -nP '\bCSFD(Search(Movie|User)|Movie(ListItem)?|SearchMovie|SearchUser|MovieListItem)\b' || true
echo
echo "== File contents for DTOs and found helpers =="
sed -n '1,240p' src/dto/global.ts || true
sed -n '1,240p' src/dto/movie.ts || true
sed -n '1,240p' src/dto/search.ts || true
sed -n '1,240p' src/dto/user-ratings.ts || true

Length of output: 12183


Fix parseIdFromUrl signature and update callers

parseIdFromUrl is declared to return number but its implementation (and tests) return null on some inputs. Change the helper signature to:

export const parseIdFromUrl = (url: string | null | undefined): number | null =>

Then update/guard all call sites that assume a non-null number (either widen their return types to number | null or add runtime checks/assertions):

  • src/helpers/global.helper.ts — change signature as above
  • Update callers currently typed to return number:
    • src/helpers/movie.helper.ts (getMovieId, parseMoviePeople -> CSFDMovieCreator.id, getMovieBoxMovies -> CSFDMovieListItem.id)
    • src/helpers/user-ratings.helper.ts (getUserRatingId)
    • src/services/search.service.ts (id assignments for CSFDSearchMovie / CSFDSearchUser)
    • src/helpers/search.helper.ts (mapping person -> id)

Callers already nullable-aware and OK: src/helpers/creator.helper.ts:getCreatorId, src/helpers/cinema.helper.ts:getCinemaUrlId

🤖 Prompt for AI Agents
In src/helpers/cinema.helper.ts around lines 22 to 25, parseIdFromUrl is
declared to return number but actually can return null; update its signature to
return number | null (export const parseIdFromUrl = (url: string | null |
undefined): number | null =>) and then update all callers to handle nullable
results: change src/helpers/global.helper.ts signature to accept/return number |
null and adjust call sites that assume non-null in src/helpers/movie.helper.ts
(getMovieId, parseMoviePeople -> CSFDMovieCreator.id, getMovieBoxMovies ->
CSFDMovieListItem.id), src/helpers/user-ratings.helper.ts (getUserRatingId),
src/services/search.service.ts (assignments for CSFDSearchMovie /
CSFDSearchUser), and src/helpers/search.helper.ts (person -> id mapping) by
either widening their types to number | null or adding runtime null
checks/assertions before using the id; leave creator.helper.ts and
cinema.helper.ts callers as-is since they already handle nullable ids.


export const getCoords = (el: HTMLElement | null): { lat: number; lng: number } | null => {
export const getCinemaCoords = (el: HTMLElement | null): { lat: number; lng: number } | null => {

if (!el) return null;
const linkMapsEl = el.querySelector('a[href*="q="]');
Expand Down Expand Up @@ -63,20 +63,20 @@ export const getGroupedFilmsByDate = (el: HTMLElement | null): CSFDCinemaGrouped
.map((index) => {
const [date, films] = divs.slice(index, index + 2);
const dateText = date?.firstChild?.textContent?.trim() ?? null;
return { date: dateText, films: getFilms('', films) };
return { date: dateText, films: getCinemaFilms('', films) };
});

return getDatesAndFilms;
};

export const getFilms = (date: string, el: HTMLElement | null): CSFDCinemaMovie[] => {
export const getCinemaFilms = (date: string, el: HTMLElement | null): CSFDCinemaMovie[] => {
const filmNodes = el.querySelectorAll('.cinema-table tr');

const films = filmNodes.map((filmNode) => {
const url = filmNode.querySelector('td.name h3 a')?.attributes.href;
const id = getId(url);
const id = url ? getCinemaUrlId(url) : null;
const title = filmNode.querySelector('.name h3')?.text.trim();
const colorRating = getColorRating(filmNode.querySelector('.name .icon'));
const colorRating = getCinemaColorRating(filmNode.querySelector('.name .icon'));
const showTimes = filmNode.querySelectorAll('.td-time')?.map((x) => x.textContent.trim());
const meta = filmNode.querySelectorAll('.td-title span')?.map((x) => x.text.trim());

Expand Down
86 changes: 41 additions & 45 deletions src/helpers/creator.helper.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { HTMLElement } from 'node-html-parser';
import { CSFDCreatorScreening } from '../interfaces/creator.interface';
import { CSFDColorRating } from '../interfaces/global';
import { Colors } from '../interfaces/user-ratings.interface';
import { CSFDCreatorScreening } from '../dto/creator';
import { CSFDColorRating } from '../dto/global';
import { Colors } from '../dto/user-ratings';
import { addProtocol, parseColor, parseIdFromUrl } from './global.helper';

export const getColorRating = (el: HTMLElement): CSFDColorRating => {
return parseColor(el?.classNames.split(' ').pop() as Colors);
const getCreatorColorRating = (el: HTMLElement | null): CSFDColorRating => {
const classes: string[] = el?.classNames.split(' ') ?? [];
const last = classes[classes.length - 1] as Colors | undefined;
return parseColor(last);
};

export const getId = (url: string): number => {
if (url) {
return parseIdFromUrl(url);
}
return null;
export const getCreatorId = (url: string | null | undefined): number | null => {
return url ? parseIdFromUrl(url) : null;
};

export const getName = (el: HTMLElement | null): string => {
return el.querySelector('h1').innerText.trim();
export const getCreatorName = (el: HTMLElement | null): string | null => {
const h1 = el?.querySelector('h1');
return h1?.innerText?.trim() ?? null;
};

export const getBirthdayInfo = (
export const getCreatorBirthdayInfo = (
el: HTMLElement | null
): { birthday: string; age: number; birthPlace: string } => {
const infoBlock = el.querySelector('h1 + p');
Expand All @@ -42,54 +42,50 @@ export const getBirthdayInfo = (
return { birthday, age, birthPlace };
};

export const getBio = (el: HTMLElement | null): string => {
return el.querySelector('.article-content p')?.text.trim().split('\n')[0].trim() || null;
export const getCreatorBio = (el: HTMLElement | null): string | null => {
const p = el?.querySelector('.article-content p');
const first = p?.text?.trim().split('\n')[0]?.trim();
return first || null;
};

export const getPhoto = (el: HTMLElement | null): string => {
const image = el.querySelector('img').attributes.src;
return addProtocol(image);
export const getCreatorPhoto = (el: HTMLElement | null): string | null => {
const src = el?.querySelector('img')?.getAttribute('src');
return src ? addProtocol(src) : null;
};

export const parseBirthday = (text: string): any => {
return text.replace(/nar./g, '').trim();
};
const parseBirthday = (text: string): string => text.replace(/nar\./g, '').trim();

export const parseAge = (text: string): any => {
return text.trim().replace(/\(/g, '').replace(/let\)/g, '').trim();
const parseAge = (text: string): number | null => {
const digits = text.replace(/[^\d]/g, '');
return digits ? Number(digits) : null;
};

export const parseBirthPlace = (text: string): any => {
return text.trim().replace(/<br>/g, '').trim();
};
const parseBirthPlace = (text: string): string =>
text.trim().replace(/<br>/g, '').trim();


export const getFilms = (el: HTMLElement | null): CSFDCreatorScreening[] => {
const filmNodes = el.querySelectorAll('.box')[0]?.querySelectorAll('table tr');
let yearCache: number;
export const getCreatorFilms = (el: HTMLElement | null): CSFDCreatorScreening[] => {
const filmNodes = el?.querySelectorAll('.box')?.[0]?.querySelectorAll('table tr') ?? [];
let yearCache: number | null = null;
const films = filmNodes.map((filmNode) => {
const id = getId(filmNode.querySelector('td.name .film-title-name')?.attributes.href);
const title = filmNode.querySelector('.name')?.text.trim();
const year = +filmNode.querySelector('.year')?.text.trim();
const colorRating = getColorRating(filmNode.querySelector('.name .icon'));
const id = getCreatorId(filmNode.querySelector('td.name .film-title-name')?.attributes?.href);
const title = filmNode.querySelector('.name')?.text?.trim();
const yearText = filmNode.querySelector('.year')?.text?.trim();
const year = yearText ? +yearText : null;
const colorRating = getCreatorColorRating(filmNode.querySelector('.name .icon'));

// Cache year from previous film because there is a gap between movies with same year
if (year) {
if (typeof year === 'number' && !isNaN(year)) {
yearCache = +year;
}

if (id && title) {
return {
id,
title,
year: year || yearCache,
colorRating
};
const finalYear = year ?? yearCache;
if (id != null && title && finalYear != null) {
return { id, title, year: finalYear, colorRating };
}
return {};
return null;
});
// Remove empty objects
const filmsUnique = films.filter(
(value) => Object.keys(value).length !== 0
) as CSFDCreatorScreening[];
const filmsUnique = films.filter(Boolean) as CSFDCreatorScreening[];
return filmsUnique;
};
4 changes: 2 additions & 2 deletions src/helpers/global.helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CSFDColorRating } from '../interfaces/global';
import { Colors } from '../interfaces/user-ratings.interface';
import { CSFDColorRating } from '../dto/global';
import { Colors } from '../dto/user-ratings';

export const parseIdFromUrl = (url: string): number => {
if (url) {
Expand Down
Loading