Skip to content

Conversation

bartholomej
Copy link
Owner

@bartholomej bartholomej commented Mar 27, 2022

ESM + CommonJS building

[Needs more testing]

Summary by CodeRabbit

  • New Features

    • Added a public search method and consolidated type exports into a DTO-based public API; package now exposes ESM/CJS/module/types entry points.
  • Refactor

    • Public helper names and creator type renamed to domain-prefixed variants (Movie/Creator/Search/UserRating); types reorganized into DTO modules and imports updated across the API.
  • Chores

    • Dual ESM/CJS build flow, packaging scripts, and generated barrel configuration added.
  • Tests

    • Test suites updated for renamed helpers, DTO types, and new fixtures.

@bartholomej bartholomej added the enhancement New feature or request label Mar 27, 2022
@bartholomej bartholomej self-assigned this Mar 27, 2022
@bartholomej bartholomej force-pushed the hybrid branch 2 times, most recently from 4d9756a to 32b39fd Compare June 4, 2022 10:59
@codecov-commenter
Copy link

codecov-commenter commented Jun 4, 2022

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (f3ccda9) to head (e33a989).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@             Coverage Diff             @@
##           master       #18      +/-   ##
===========================================
+ Coverage   98.69%   100.00%   +1.30%     
===========================================
  Files          26        26              
  Lines         536       541       +5     
  Branches      104       114      +10     
===========================================
+ Hits          529       541      +12     
+ Misses          7         0       -7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bartholomej bartholomej force-pushed the master branch 2 times, most recently from 0a3f327 to 12e71ec Compare January 30, 2023 09:53
@bartholomej bartholomej force-pushed the hybrid branch 2 times, most recently from 3fe5896 to e28bcdd Compare February 4, 2023 17:49
@bartholomej bartholomej force-pushed the hybrid branch 3 times, most recently from eba4339 to 5b8519c Compare September 14, 2025 09:59
Copy link

coderabbitai bot commented Sep 17, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Moved type declarations from interfaces/* into dto/* and re-exported them; introduced ESM/CJS dual build with barrels and path-fix scripts; renamed and re-scoped many helper functions to domain-prefixed names; added SearchScraper and a Csfd.search method; updated package exports, tests, and tsconfigs.

Changes

Cohort / File(s) Summary
Build & Packaging
package.json, barrels.json, tsconfig.json, tsconfig.esm.json, tsconfig.cjs.json
Added barrels.json; split build into ESM/CJS targets with new tsconfigs; adjusted package scripts (build, postbuild, fix-paths); updated package fields (module, types, exports); added barrelsby and json tooling.
Public Types & Aggregation
src/types.ts, src/dto/*, src/index.ts, server.ts, src/vars.ts
Moved public types from interfaces/* to dto/*; added src/types.ts re-exports; updated imports across code/tests; Csfd now accepts SearchScraper and exposes search(text).
DTO — Movie Creator Rename
src/dto/movie.*, usages in helpers/services/tests
Renamed CSFDCreatorCSFDMovieCreator and updated all related interfaces, arrays, and usages to CSFDMovieCreator[].
Helpers — Renames & DTO imports
src/helpers/* (movie, creator, cinema, search, user-ratings, global)
Renamed many exported helpers to domain-prefixed names (getMovie*, getCreator*, getCinema*, getSearch*, getUserRating*); moved several helpers to private; added new cinema helpers (getCinemaUrl, parseCinema, getCinemaUrlId); switched type imports to dto/* and added defensive null handling.
Services — Adapter to renamed helpers/types
src/services/*.ts (movie, creator, search, cinema, user-ratings)
Updated services to import DTO types and call renamed helper functions (e.g., getMovie*, getCreator*, getSearch*, getCinema*, getUserRating*); replaced getCoords usage with getCinemaCoords; adjusted builder sites to new helper names.
Public entry & factory
src/index.ts, csfd factory
Injected SearchScraper into Csfd constructor, added public search(text) delegating to SearchScraper, and updated csfd factory instantiation to include search service.
Tests & Mocks
tests/*
Updated tests to import DTO types and call renamed helpers; added/adjusted test suites and mocks (new cinema tests, edge-case checks, new mock pages); updated test import paths.
Types Re-exports
src/types.ts
Added export * re-exports for ./dto/cinema, ./dto/creator, ./dto/global, ./dto/movie, ./dto/search, ./dto/user-ratings.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Csfd as Csfd (src/index.ts)
  participant Search as SearchScraper
  participant HTTP as HTTP Fetch
  participant DOM as DOM Parser
  participant H as Search Helpers

  Client->>Csfd: search(text)
  Csfd->>Search: search(text)
  Search->>HTTP: GET /hledat?q=text
  HTTP-->>Search: HTML
  Search->>DOM: parse(HTML)
  loop per result
    Search->>H: getSearchUrl/Title/Year/Type/Poster/Origins/ColorRating
    H-->>Search: fields
    Search->>H: parseSearchPeople(directors/actors)
    H-->>Search: CSFDMovieCreator[]
  end
  Search-->>Csfd: CSFDSearch
  Csfd-->>Client: CSFDSearch
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I thump my paws at ESM delight,
Two builds now run by day and night.
DTOs hop in, helpers wear new names,
Search joins the burrow — hunting frames.
Barrels rolled, tests updated — rabbit claims. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description is too brief and does not follow the repository's required template: it omits the "What's new and why I made this pull request?" section and does not select a "Pull request type", nor does it detail the specific changes, testing performed, or any migration/breaking changes beyond the single note "[Needs more testing]". Please update the description to use the provided template: add a short "What's new and why" summary listing the core changes and rationale, mark the appropriate "Pull request type" checkbox, describe any breaking changes or migration steps, and include testing instructions/results and what remains to be tested so reviewers can validate the build changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "ESM + CommonJS build 🎉" is concise and accurately describes the primary change (adding ESM and CommonJS build outputs), so a reviewer scanning history will understand the main intent; the emoji is decorative but not misleading.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch hybrid

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

coderabbitai bot commented Sep 17, 2025

Caution

No docstrings were generated.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (13)
src/helpers/global.helper.ts (1)

61-71: ISO‑8601 duration parsing: null‑match crash and mandatory “T” bug

  • iso.match can be null → getDuration(null) will throw.
  • Your regex requires “T”; durations without a time part won’t match.

Also, returning only minutes ignores other units. Safer minimal fix below.

Apply:

 export const parseISO8601Duration = (iso: string): number => {
-  const iso8601DurationRegex =
-    /(-)?P(?:([.,\d]+)Y)?(?:([.,\d]+)M)?(?:([.,\d]+)W)?(?:([.,\d]+)D)?T(?:([.,\d]+)H)?(?:([.,\d]+)M)?(?:([.,\d]+)S)?/;
-
-  const matches = iso.match(iso8601DurationRegex);
-
-  const duration = getDuration(matches);
-
-  return +duration.minutes;
+  const iso8601DurationRegex =
+    /^(-)?P(?:([.,\d]+)Y)?(?:([.,\d]+)M)?(?:([.,\d]+)W)?(?:([.,\d]+)D)?(?:T(?:(?:([.,\d]+)H)?(?:([.,\d]+)M)?(?:([.,\d]+)S)?))?$/;
+  const m = iso.match(iso8601DurationRegex);
+  if (!m) return NaN;
+  const sign = m[1] ? -1 : 1;
+  const years   = parseFloat(m[2] ?? '0');
+  const months  = parseFloat(m[3] ?? '0');
+  const weeks   = parseFloat(m[4] ?? '0');
+  const days    = parseFloat(m[5] ?? '0');
+  const hours   = parseFloat(m[6] ?? '0');
+  const minutes = parseFloat(m[7] ?? '0');
+  const seconds = parseFloat(m[8] ?? '0');
+  // Approximate months/years in minutes (30d/month, 365d/year)
+  const total =
+    years * 525600 + months * 43200 + weeks * 10080 + days * 1440 +
+    hours * 60 + minutes + seconds / 60;
+  return sign * total;
 }
src/services/user-ratings.service.ts (3)

34-35: pages calculation can throw when .pagination is missing

You use optional chaining on pagesNode, but index with pagesNode again, which will crash if it’s null.

Apply:

-    const pages = +pagesNode?.childNodes[pagesNode.childNodes.length - 4].rawText || 1;
+    const pages =
+      pagesNode && pagesNode.childNodes.length >= 4
+        ? +pagesNode.childNodes[pagesNode.childNodes.length - 4].rawText
+        : 1;

25-36: films array never reset; results can accumulate across calls

Initialize this.films for each scrape to avoid data leakage if the instance is reused.

Apply:

   public async userRatings(
     user: string | number,
     config?: CSFDUserRatingConfig
   ): Promise<CSFDUserRatings[]> {
-    let allMovies: CSFDUserRatings[] = [];
+    this.films = [];
+    let allMovies: CSFDUserRatings[] = [];

61-62: Parameter type mismatch: getPage accepts non‑optional config

Call sites pass possibly undefined. Make config optional in getPage.

Apply:

-  private getPage(config: CSFDUserRatingConfig, movies: HTMLElement[]) {
+  private getPage(config: CSFDUserRatingConfig | undefined, movies: HTMLElement[]) {
tests/services.test.ts (1)

88-106: console.warn assertion never runs and no spy is set.

The matcher isn’t called, and the call happens before any spy can observe it.

Apply this rewrite (also import vi at top):

-import { describe, expect, test } from 'vitest';
+import { describe, expect, test, vi } from 'vitest';
@@
-describe('Exclude + includes together', () => {
-  // Fetch data with excludes + includes
-  const userRatingsScraper = new UserRatingsScraper();
-  const resBoth: Promise<CSFDUserRatings[]> = userRatingsScraper.userRatings(USER, {
-    includesOnly: ['seriál'],
-    excludes: ['film']
-  });
-
-  test('Should have warning', () => {
-    expect(console.warn).toHaveBeenCalled;
-  });
-
-  test('Should use includesOnly', async () => {
-    const results = await resBoth;
-
-    const tvSeries = results.filter((item) => item.type === 'seriál');
-    expect(tvSeries.length).toBe<number>(results.length);
-  });
-});
+describe('Exclude + includes together', () => {
+  test('Should have warning', async () => {
+    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+    const userRatingsScraper = new UserRatingsScraper();
+    await userRatingsScraper.userRatings(USER, {
+      includesOnly: ['seriál'],
+      excludes: ['film']
+    });
+    expect(warnSpy).toHaveBeenCalled();
+    warnSpy.mockRestore();
+  });
+
+  test('Should use includesOnly', async () => {
+    const userRatingsScraper = new UserRatingsScraper();
+    const results = await userRatingsScraper.userRatings(USER, {
+      includesOnly: ['seriál'],
+      excludes: ['film']
+    });
+    const tvSeries = results.filter((item) => item.type === 'seriál');
+    expect(tvSeries.length).toBe<number>(results.length);
+  });
+});
tests/fetchers.test.ts (2)

88-96: 404 test doesn’t assert rejection correctly.

Assert promise rejection instead of catching and using toContain.

Apply:

-  test('Fetch error URL', async () => {
-    try {
-      const url = userRatingsUrl(badId);
-      const html = await fetchPage(url);
-      expect(html).toBe('Error');
-    } catch (e) {
-      expect(e).toContain(Error);
-    }
-  });
+  test('Fetch error URL', async () => {
+    await expect(fetchPage(userRatingsUrl(badId))).rejects.toThrow();
+  });

100-107: Same issue as above for movie 404.

Apply:

-  test('Fetch error URL', async () => {
-    try {
-      const url = movieUrl(badId);
-      const html = await fetchPage(url);
-      expect(html).toBe('Error');
-    } catch (e) {
-      expect(e).toThrow(Error);
-    }
-  });
+  test('Fetch error URL', async () => {
+    await expect(fetchPage(movieUrl(badId))).rejects.toThrow();
+  });
src/services/creator.service.ts (1)

20-23: Guard against missing DOM nodes before parsing

If .creator-about or .creator-filmography is missing, helper calls will throw. Fail fast with a clear error.

Apply this diff:

-    const asideNode = creatorHtml.querySelector('.creator-about');
-    const filmsNode = creatorHtml.querySelector('.creator-filmography');
-    this.buildCreator(+creatorId, asideNode, filmsNode);
+    const asideNode = creatorHtml.querySelector('.creator-about');
+    const filmsNode = creatorHtml.querySelector('.creator-filmography');
+    if (!asideNode || !filmsNode) {
+      throw new Error('node-csfd-api: Unexpected HTML structure for creator page.');
+    }
+    this.buildCreator(id, asideNode, filmsNode);
src/services/movie.service.ts (1)

41-46: Guard null DOM nodes and JSON‑LD to prevent runtime crashes

Several selectors are assumed present; any CSFD layout change or non-200 page will throw (e.g., .page-content, .aside-movie-profile, .main-movie-profile, JSON‑LD script). Add checks and safe fallbacks before calling buildMovie.

Apply:

-    const pageClasses = movieHtml.querySelector('.page-content').classNames.split(' ');
-    const asideNode = movieHtml.querySelector('.aside-movie-profile');
-    const movieNode = movieHtml.querySelector('.main-movie-profile');
-    const jsonLd = movieHtml.querySelector('script[type="application/ld+json"]').innerText;
-    this.buildMovie(+movieId, movieNode, asideNode, pageClasses, jsonLd);
+    const pageEl = movieHtml.querySelector('.page-content');
+    const asideNode = movieHtml.querySelector('.aside-movie-profile');
+    const movieNode = movieHtml.querySelector('.main-movie-profile');
+    const jsonLdEl = movieHtml.querySelector('script[type="application/ld+json"]');
+
+    if (!pageEl || !movieNode || !asideNode) {
+      throw new Error('node-csfd-api: Unexpected page structure');
+    }
+
+    const pageClasses = pageEl.classNames?.split(' ') ?? [];
+    const jsonLd = jsonLdEl?.innerText ?? '{}';
+    this.buildMovie(+movieId, movieNode, asideNode, pageClasses, jsonLd);
src/index.ts (1)

13-19: Constructor signature change is a breaking API for consumers instantiating Csfd

searchService was inserted before cinemaService. Anyone doing new Csfd(...) will break. Either keep backward compatibility or switch to a params object.

Option: support old 4‑arg form transparently.

-export class Csfd {
-  constructor(
-    private userRatingsService: UserRatingsScraper,
-    private movieService: MovieScraper,
-    private creatorService: CreatorScraper,
-    private searchService: SearchScraper,
-    private cinemaService: CinemaScraper
-  ) { }
+export class Csfd {
+  constructor(
+    private userRatingsService: UserRatingsScraper,
+    private movieService: MovieScraper,
+    private creatorService: CreatorScraper,
+    searchOrCinema: SearchScraper | CinemaScraper,
+    cinemaMaybe?: CinemaScraper
+  ) {
+    if (searchOrCinema instanceof SearchScraper) {
+      this.searchService = searchOrCinema;
+      this.cinemaService = cinemaMaybe!;
+    } else {
+      this.cinemaService = searchOrCinema;
+      this.searchService = new SearchScraper();
+    }
+  }

Alternatively, deprecate direct constructor use and expose only the csfd factory.

src/helpers/search.helper.ts (1)

49-55: Bug: Array.from(el && el.querySelectorAll(...)) can throw

If el is falsy, el && ... becomes false and Array.from(false) throws. Also avoid shadowing param in callback.

-  const peopleNode = Array.from(el && el.querySelectorAll('.article-content p')).find((el) =>
-    el.textContent.includes(who)
-  );
+  const peopleNode = Array.from(el?.querySelectorAll('.article-content p') ?? []).find((p) =>
+    p.textContent?.includes(who)
+  );
src/helpers/cinema.helper.ts (1)

27-45: Harden coordinate parsing; avoid brittle split('q=')

Current code throws if href is missing or query param is structured differently. Use URLSearchParams and add guards.

 export const getCinemaCoords = (el: HTMLElement | null): { lat: number; lng: number } | null => {
 
   if (!el) return null;
   const linkMapsEl = el.querySelector('a[href*="q="]');
   if (!linkMapsEl) return null;
 
-  const linkMaps = linkMapsEl.getAttribute('href');
-  const [_, latLng] = linkMaps.split('q=');
-
-  const coords = latLng.split(',');
+  const href = linkMapsEl.getAttribute('href') || '';
+  const qs = href.split('?')[1] ?? '';
+  const params = new URLSearchParams(qs);
+  const q = params.get('q') || '';
+  if (!q) return null;
+  const coords = q.split(',');
   if (coords.length !== 2) return null;
 
   const lat = Number(coords[0]);
   const lng = Number(coords[1]);
   if (Number.isFinite(lat) && Number.isFinite(lng)) {
     return { lat, lng };
   }
   return null;
 };
src/helpers/creator.helper.ts (1)

22-43: Fix types and regex; age can be null

Age is set to null but typed as number. Also, nar. should escape the dot.

-export const getCreatorBirthdayInfo = (
-  el: HTMLElement | null
-): { birthday: string; age: number; birthPlace: string } => {
+export const getCreatorBirthdayInfo = (
+  el: HTMLElement | null
+): { birthday: string; age: number | null; birthPlace: string } => {
+  if (!el) return { birthday: '', age: null, birthPlace: '' };
   const infoBlock = el.querySelector('h1 + p');
   const text = infoBlock?.innerHTML.trim();
 
   const birthPlaceRow = infoBlock?.querySelector('.info-place')?.innerHTML.trim();
   const ageRow = infoBlock?.querySelector('.info')?.innerHTML.trim();
 
   let birthday: string = '';
 
   if (text) {
     const parts = text.split('\n');
     const birthdayRow = parts.find((x) => x.includes('nar.'));
     birthday = birthdayRow ? parseBirthday(birthdayRow) : '';
   }
 
-  const age = ageRow ? +parseAge(ageRow) : null;
+  const age = ageRow ? +parseAge(ageRow) : null;
   const birthPlace = birthPlaceRow ? parseBirthPlace(birthPlaceRow) : '';
 
   return { birthday, age, birthPlace };
 };

And update helper types below:

-const parseBirthday = (text: string): any => {
-  return text.replace(/nar./g, '').trim();
+const parseBirthday = (text: string): string => {
+  return text.replace(/nar\./g, '').trim();
 };
 
-const parseAge = (text: string): any => {
+const parseAge = (text: string): string => {
   return text.trim().replace(/\(/g, '').replace(/let\)/g, '').trim();
 };
 
-const parseBirthPlace = (text: string): any => {
+const parseBirthPlace = (text: string): string => {
   return text.trim().replace(/<br>/g, '').trim();
 };
🧹 Nitpick comments (44)
src/helpers/global.helper.ts (1)

4-12: Return type can be null; align the signature

parseIdFromUrl returns null when it can’t parse. Either make it number | null or return NaN/0 consistently.

Apply:

-export const parseIdFromUrl = (url: string): number => {
+export const parseIdFromUrl = (url: string): number | null => {
tsconfig.json (1)

10-10: strict vs strictNullChecks mismatch

You’ve enabled strict but disabled strictNullChecks. Acceptable short‑term, but it masks real nullability issues seen in helpers. Plan to enable soon.

src/services/user-ratings.service.ts (2)

51-53: Optional chaining for allPagesDelay

Inside the allPages branch, use optional chaining for clarity and to satisfy strict nullability if enabled later.

Apply:

-        if (config.allPagesDelay) {
+        if (config?.allPagesDelay) {
           await sleep(config.allPagesDelay);
         }

95-104: Redundant cast and double type parsing

colorRating helper already returns CSFDColorRating; drop the cast. Also, you compute type earlier during filtering—consider threading it down to avoid recomputation.

Apply:

-      colorRating: getUserRatingColorRating(el) as CSFDColorRating,
+      colorRating: getUserRatingColorRating(el),
src/helpers/user-ratings.helper.ts (4)

6-9: Nullability mismatch on ID

parseIdFromUrl can return null; reflect that in the signature to prevent latent NPEs.

Apply:

-export const getUserRatingId = (el: HTMLElement): number => {
+export const getUserRatingId = (el: HTMLElement): number | null => {

28-30: Nullability mismatch on year

Year extraction can fail; update the return type accordingly.

Apply:

-export const getUserRatingYear = (el: HTMLElement): number => {
+export const getUserRatingYear = (el: HTMLElement): number | null => {

32-35: Avoid duplicate parseColor; reuse the global helper

There are two parseColor implementations (here and in global.helper.ts). Keep one source of truth.

Apply:

-import { parseIdFromUrl } from './global.helper';
+import { parseIdFromUrl, parseColor } from './global.helper';
@@
-export const getUserRatingColorRating = (el: HTMLElement): CSFDColorRating => {
+export const getUserRatingColorRating = (el: HTMLElement): CSFDColorRating => {
   const color = parseColor(el.querySelector('td.name .icon').classNames.split(' ').pop() as Colors);
   return color;
 };
@@
-const parseColor = (quality: Colors): CSFDColorRating => {
-  switch (quality) {
-    case 'lightgrey':
-      return 'unknown';
-    case 'red':
-      return 'good';
-    case 'blue':
-      return 'average';
-    case 'grey':
-      return 'bad';
-    default:
-      return 'unknown';
-  }
-};
+// moved to global.helper.ts

1-16: Selector safety

Most querySelector calls assume elements exist; consider null‑checks to prevent runtime errors when the DOM shape varies.

src/vars.ts (1)

13-15: URL-encode district and period to avoid malformed URLs.

Safe even if they’re numbers/unions.

Apply this diff:

-export const cinemasUrl = (district: number | string, period: CSFDCinemaPeriod): string => {
-  return `https://www.csfd.cz/kino/?period=${period}&district=${district}`;
-};
+export const cinemasUrl = (district: number | string, period: CSFDCinemaPeriod): string => {
+  return `https://www.csfd.cz/kino/?period=${encodeURIComponent(period)}&district=${encodeURIComponent(String(district))}`;
+};
tests/services.test.ts (2)

22-22: Avoid async describe blocks.

describe should be synchronous; move async work into tests or hooks.

Apply:

-describe('AllPages', async () => {
+describe('AllPages', () => {

74-76: Remove noisy console.log in tests.

It pollutes CI output.

Apply:

-    console.log(results);
tsconfig.esm.json (1)

4-6: Use NodeNext module settings for true Node ESM resolution.

Aligns TS resolver with Node’s ESM semantics in hybrid packages.

Apply this diff:

-    "module": "ESNext",
-    "moduleResolution": "node",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",

Confirm your supported Node/TS versions match NodeNext semantics.

tests/fetchers.test.ts (1)

77-83: Make assertion resilient to extra fields.

Use partial match to avoid brittleness if more props are added.

Apply:

-    expect(creator.films.find((film) => film.title === 'Hej-rup!')).toEqual<CSFDCreatorScreening>({
-      id: 3106,
-      title: 'Hej-rup!',
-      year: 1934,
-      colorRating: 'good'
-    });
+    expect(creator.films.find((film) => film.title === 'Hej-rup!')).toEqual(
+      expect.objectContaining({
+        id: 3106,
+        title: 'Hej-rup!',
+        year: 1934,
+        colorRating: 'good'
+      })
+    );
src/services/cinema.service.ts (1)

14-15: Initialize class field to satisfy strict property initialization.

Prevents TS error when strictPropertyInitialization is enabled.

Apply:

-  private cinema: CSFDCinema[];
+  private cinema: CSFDCinema[] = [];
tests/creator.test.ts (3)

148-150: Prefer toBeNull() over casting null to any

This expectation should assert nullability directly; the current cast hides type issues and weakens the test.

Apply this diff:

-    const creator = getCreatorBio(asideNodeComposer);
-    expect(creator).toEqual<string>(null as any);
+    const creator = getCreatorBio(asideNodeComposer);
+    expect(creator).toBeNull();

171-174: Fix test name and use null-safe matcher

The test is asserting age but titled “Birthplace”. Also prefer toBeNull().

Apply this diff:

-  test('Birthplace', () => {
-    const creator = getCreatorBirthdayInfo(asideNodeComposer)?.age;
-    expect(creator).toEqual(null);
-  });
+  test('Age', () => {
+    const creator = getCreatorBirthdayInfo(asideNodeComposer)?.age;
+    expect(creator).toBeNull();
+  });

29-45: Consider tightening type expectations (optional)

Many assertions use generics on expect<T>, which provide no runtime benefit and can mask type drift. Using toBe/toEqual without generics keeps tests cleaner.

Also applies to: 50-62, 67-79, 86-102, 107-119, 124-136, 143-155, 162-174, 179-182

tests/cinema.helper.test.ts (1)

76-86: Date parameter looks unused — either remove it or pass a real date

getCinemaFilms('', table) passes an empty date. If the helper doesn’t use date, consider removing that parameter; otherwise pass a representative date to guard against future reliance on it.

Would you like me to prepare a follow-up PR to either (a) drop the date parameter from getCinemaFilms, or (b) thread an actual date from getGroupedFilmsByDate into this test?

src/services/creator.service.ts (1)

26-36: Avoid repeated DOM parsing and use nullish coalescing for age

Cache birthday info once; || null can incorrectly coerce 0 (use ?? null). Also minor nit: reuse id you already computed.

Apply this diff:

-  private buildCreator(id: number, asideEl: HTMLElement, filmsNode: HTMLElement) {
-    this.person = {
+  private buildCreator(id: number, asideEl: HTMLElement, filmsNode: HTMLElement) {
+    const info = getCreatorBirthdayInfo(asideEl);
+    this.person = {
       id,
-      name: getCreatorName(asideEl),
-      birthday: getCreatorBirthdayInfo(asideEl)?.birthday,
-      birthplace: getCreatorBirthdayInfo(asideEl)?.birthPlace,
-      photo: getCreatorPhoto(asideEl),
-      age: getCreatorBirthdayInfo(asideEl)?.age || null,
-      bio: getCreatorBio(asideEl),
+      name: getCreatorName(asideEl),
+      birthday: info?.birthday || '',
+      birthplace: info?.birthPlace || '',
+      photo: getCreatorPhoto(asideEl),
+      age: info?.age ?? null,
+      bio: getCreatorBio(asideEl),
       films: getCreatorFilms(filmsNode)
     };
   }
src/services/search.service.ts (2)

73-91: Rename variable from user to series in TV-series loop

Minor readability/consistency fix; the variable holds a TV series item, not a user.

Apply this diff:

-    tvSeriesNode.forEach((m) => {
-      const url = getSearchUrl(m);
-
-      const user: CSFDSearchMovie = {
+    tvSeriesNode.forEach((m) => {
+      const url = getSearchUrl(m);
+
+      const series: CSFDSearchMovie = {
         id: parseIdFromUrl(url),
         title: getSearchTitle(m),
         year: getSearchYear(m),
         url: `https://www.csfd.cz${url}`,
         type: getSearchType(m),
         colorRating: getSearchColorRating(m),
         poster: getSearchPoster(m),
         origins: getSearchOrigins(m),
         creators: {
           directors: parseSearchPeople(m, 'directors'),
           actors: parseSearchPeople(m, 'actors')
         }
       };
-      tvSeries.push(user);
+      tvSeries.push(series);
     });

36-58: DRY up movie/series parsing (optional)

Movie and TV-series parsing are identical. Extract a small parseMovieNode(m: HTMLElement): CSFDSearchMovie to reduce duplication and future drift.

I can draft the helper and inline replacements if you want it in this PR.

Also applies to: 73-91

tests/user-ratings.test.ts (3)

113-119: Use toBe for primitives and drop generics

Cleaner and idiomatic for string unions; generics on expect add noise.

Apply this diff:

-    const movie = getUserRatingColorRating(movies[3]);
-    expect(movie).toEqual<CSFDColorRating>('average');
+    const movie = getUserRatingColorRating(movies[3]);
+    expect(movie).toBe('average');

135-141: Same here: toBe is sufficient

Strings are primitives; toBe is fine and reads better.

Apply this diff:

-    const movie = getUserRatingUrl(movies[0]);
-    expect(movie).toEqual<string>('https://www.csfd.cz/film/1254361-stutz/');
+    const movie = getUserRatingUrl(movies[0]);
+    expect(movie).toBe('https://www.csfd.cz/film/1254361-stutz/');

104-111: Consider re-enabling the gray/black rating tests

They’re useful guardrails for color mapping regressions once stable.

I can help adjust fixtures/parsers to make these deterministic and re-enable them.

src/services/movie.service.ts (2)

29-29: Definite assignment for film field

Avoids TS strict initialization warnings and clarifies lifecycle.

-  private film: CSFDMovie;
+  private film!: CSFDMovie;

63-64: Avoid unsafe cast of type; validate against allowed enum values

Casting blindly may leak unexpected strings to public API if the markup changes.

-      type: getMovieType(el) as CSFDFilmTypes,
+      type: toFilmType(getMovieType(el)),

Add helper (outside the class):

const FILM_TYPES: readonly CSFDFilmTypes[] = [
  'film','TV film','pořad','seriál','divadelní záznam','koncert','série',
  'studentský film','amatérský film','hudební videoklip','epizoda'
] as const;

const toFilmType = (t: string): CSFDFilmTypes =>
  (FILM_TYPES as readonly string[]).includes(t) ? (t as CSFDFilmTypes) : 'film';
package.json (1)

9-12: Build pipeline simplification (optional)

You run tsc three times. If you use project references, tsc -b can orchestrate both CJS/ESM builds and speed up incremental builds.

Example:

-    "build": "tsc && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && yarn barrels",
+    "build": "tsc -b tsconfig.cjs.json tsconfig.esm.json && yarn barrels",

Also applies to: 17-17

tests/search.test.ts (1)

98-117: Add one negative-path test for robustness

Consider a single test case per group where the queried node misses an expected selector (e.g., no .film-title-name, no icon). This will guard against future regressions once null-safety is added in helpers.

I can craft minimal HTML snippets with missing nodes and add assertions for empty string/NaN (or null if you choose that contract). Want me to open a follow-up PR with those tests?

Also applies to: 252-265

src/helpers/cinema.helper.ts (3)

11-13: Null-safe color rating

Make this tolerant to missing icons and keep behavior stable when the element is absent.

-export const getCinemaColorRating = (el: HTMLElement): CSFDColorRating => {
-  return parseColor(el?.classNames.split(' ').pop() as Colors);
-};
+export const getCinemaColorRating = (el: HTMLElement | null): CSFDColorRating => {
+  if (!el) return 'unknown';
+  return parseColor((el.classNames as any)?.split?.(' ')?.pop() as Colors);
+};

Node-html-parser’s classNames can be string or string[] depending on version. Please confirm the version used in this repo. If it’s string[], we can switch to Array.isArray(el.classNames) ? el.classNames.slice(-1)[0] : ....


66-67: Remove unused parameter in getCinemaFilms; simplify call

date isn’t used. Drop it to avoid confusion.

-      return { date: dateText, films: getCinemaFilms('', films) };
+      return { date: dateText, films: getCinemaFilms(films) };

72-93: Null checks and safer ID parsing in getCinemaFilms

Guard against el being null/undefined and missing anchors.

-export const getCinemaFilms = (date: string, el: HTMLElement | null): CSFDCinemaMovie[] => {
-  const filmNodes = el.querySelectorAll('.cinema-table tr');
+export const getCinemaFilms = (el: HTMLElement | null): CSFDCinemaMovie[] => {
+  if (!el) return [];
+  const filmNodes = el?.querySelectorAll('.cinema-table tr') ?? [];
 
   const films = filmNodes.map((filmNode) => {
-    const url = filmNode.querySelector('td.name h3 a')?.attributes.href;
-    const id = getCinemaUrlId(url);
+    const url = filmNode.querySelector('td.name h3 a')?.attributes?.href;
+    const id = getCinemaUrlId(url || '');
     const title = filmNode.querySelector('.name h3')?.text.trim();
-    const colorRating = getCinemaColorRating(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());
 
     return {
       id,
       title,
       url,
       colorRating,
       showTimes,
       meta: parseMeta(meta)
     };
   });
   return films;
 };
src/dto/movie.ts (1)

49-59: Type rename is breaking; add BC alias

Downstream users may still import CSFDCreator. Provide a harmless alias.

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

Add right after CSFDMovieCreator is declared:

 export interface CSFDMovieCreator {
   /**
    * CSFD person ID.
    * ...
    */
   id: number;
   name: string;
   url: string;
 }
+
+// Backward compatibility alias (kept for external consumers)
+export type CSFDCreator = CSFDMovieCreator;
src/helpers/creator.helper.ts (4)

7-9: Null-safe rating extraction

Prevent crashes when the icon is missing.

-const getCreatorColorRating = (el: HTMLElement): CSFDColorRating => {
-  return parseColor(el?.classNames.split(' ').pop() as Colors);
-};
+const getCreatorColorRating = (el: HTMLElement | null): CSFDColorRating => {
+  if (!el) return 'unknown';
+  return parseColor((el.classNames as any)?.split?.(' ')?.pop() as Colors);
+};

18-20: Null guard for getCreatorName

Avoid NPE when el is null.

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

45-47: Return type can be null

getCreatorBio returns null when not found but is typed as string.

-export const getCreatorBio = (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 => {
+  return el?.querySelector('.article-content p')?.text?.trim().split('\n')[0].trim() ?? null;
+};

49-52: Null guard for photo

Prevent crashes and reflect nullable return.

-export const getCreatorPhoto = (el: HTMLElement | null): string => {
-  const image = el.querySelector('img').attributes.src;
-  return addProtocol(image);
-};
+export const getCreatorPhoto = (el: HTMLElement | null): string | null => {
+  const image = el?.querySelector('img')?.attributes?.src;
+  return image ? addProtocol(image) : null;
+};
tests/movie.test.ts (1)

592-594: Prefer toBeNull for clarity and type-safety

This expresses the intent better and avoids odd generics.

-  const movie = getMovieRatingCount(movieNode);
-  expect(movie).toEqual<CSFDMovieListItem[]>(null as any);
+  const movie = getMovieRatingCount(movieNode);
+  expect(movie).toBeNull();
src/helpers/movie.helper.ts (7)

72-96: Guard fallback path in getMovieDuration; fix typo

Avoid NPE when .origin is missing and correct comment.

   } catch (error) {
-    const origin = el.querySelector('.origin').innerText;
+    const originNode = el.querySelector('.origin') as HTMLElement | null;
+    const origin = originNode?.innerText;
+    if (!origin) return null;
     const timeString = origin.split(',');
     if (timeString.length > 2) {
-      // Get last time elelment
+      // Get last time element
       const timeString2 = timeString.pop().trim();
       // Clean it
       const timeRaw = timeString2.split('(')[0].trim();

98-121: Optional chaining on auxiliary fields in getMovieTitlesOther

Prevents crashes when flags or text are missing.

   const titlesOther = namesNode.map((el) => {
-    const country = el.querySelector('img.flag').attributes.alt;
-    const title = el.textContent.trim().split('\n')[0];
+    const country = el.querySelector('img.flag')?.attributes?.alt;
+    const title = el.textContent?.trim().split('\n')[0];
 
     if (country && title) {
       return {
         country,
         title
       };
     } else {
       return null;
     }
   });

122-137: Null guard for getMoviePoster

Accepts HTMLElement | null but doesn’t guard.

 export const getMoviePoster = (el: HTMLElement | null): string => {
-  const poster = el.querySelector('.film-posters img');
+  if (!el) return null;
+  const poster = el.querySelector('.film-posters img');

139-147: Null guard for getMovieRandomPhoto

 export const getMovieRandomPhoto = (el: HTMLElement | null): string => {
-  const imageNode = el.querySelector('.gallery-item picture img');
+  if (!el) return null;
+  const imageNode = el.querySelector('.gallery-item picture img');

149-156: Null guard for getMovieTrivia

 export const getMovieTrivia = (el: HTMLElement | null): string[] => {
-  const triviaNodes = el.querySelectorAll('.article-trivia ul li');
+  if (!el) return null;
+  const triviaNodes = el.querySelectorAll('.article-trivia ul li');

210-215: getBoxContent return type should include undefined

It can return undefined when the box isn’t found.

-const getBoxContent = (el: HTMLElement, box: string): HTMLElement => {
+const getBoxContent = (el: HTMLElement, box: string): HTMLElement | undefined => {

233-251: Guard premiere parsing against missing nodes

Avoid NPEs on pages without titles or flags.

 export const getMoviePremieres = (el: HTMLElement): CSFDPremiere[] => {
   const premiereNodes = el.querySelectorAll('.box-premieres li');
   const premiere: CSFDPremiere[] = [];
   for (const premiereNode of premiereNodes) {
-    const title = premiereNode.querySelector('p + span').attributes.title;
-
-    if (title) {
-      const [date, ...company] = title?.split(' ');
-
-      premiere.push({
-        country: premiereNode.querySelector('.flag')?.attributes.title || null,
-        format: premiereNode.querySelector('p').textContent.trim()?.split(' od')[0],
-        date,
-        company: company.join(' ')
-      });
-    }
+    const span = premiereNode.querySelector('p + span');
+    const title = span?.attributes?.title;
+    if (!title) continue;
+    const [date, ...company] = title.split(' ');
+    premiere.push({
+      country: premiereNode.querySelector('.flag')?.attributes?.title || null,
+      format: premiereNode.querySelector('p')?.textContent?.trim()?.split(' od')[0],
+      date,
+      company: company.join(' ')
+    });
   }
   return premiere;
 };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a6312a and 4af7602.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (31)
  • barrels.json (1 hunks)
  • package.json (4 hunks)
  • server.ts (1 hunks)
  • src/dto/cinema.ts (1 hunks)
  • src/dto/movie.ts (1 hunks)
  • src/dto/search.ts (2 hunks)
  • src/helpers/cinema.helper.ts (3 hunks)
  • src/helpers/creator.helper.ts (2 hunks)
  • src/helpers/global.helper.ts (1 hunks)
  • src/helpers/movie.helper.ts (13 hunks)
  • src/helpers/search.helper.ts (1 hunks)
  • src/helpers/user-ratings.helper.ts (2 hunks)
  • src/index.ts (1 hunks)
  • src/services/cinema.service.ts (2 hunks)
  • src/services/creator.service.ts (2 hunks)
  • src/services/movie.service.ts (2 hunks)
  • src/services/search.service.ts (3 hunks)
  • src/services/user-ratings.service.ts (3 hunks)
  • src/types.ts (1 hunks)
  • src/vars.ts (1 hunks)
  • tests/cinema.helper.test.ts (5 hunks)
  • tests/cinema.service.test.ts (1 hunks)
  • tests/creator.test.ts (7 hunks)
  • tests/fetchers.test.ts (1 hunks)
  • tests/movie.test.ts (13 hunks)
  • tests/search.test.ts (8 hunks)
  • tests/services.test.ts (1 hunks)
  • tests/user-ratings.test.ts (2 hunks)
  • tsconfig.cjs.json (1 hunks)
  • tsconfig.esm.json (1 hunks)
  • tsconfig.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (16)
src/services/user-ratings.service.ts (2)
src/helpers/user-ratings.helper.ts (7)
  • getUserRatingType (18-22)
  • getUserRatingId (6-9)
  • getUserRatingTitle (24-26)
  • getUserRatingYear (28-30)
  • getUserRatingUrl (41-44)
  • getUserRatingColorRating (32-35)
  • getUserRatingDate (37-39)
src/dto/global.ts (1)
  • CSFDColorRating (21-21)
src/services/movie.service.ts (3)
src/helpers/movie.helper.ts (19)
  • getMovieTitle (21-23)
  • getMovieYear (62-70)
  • getMovieDuration (72-96)
  • getMovieDescriptions (158-162)
  • getMovieGenres (25-28)
  • getMovieType (190-193)
  • getMovieOrigins (30-34)
  • getMovieColorRating (36-38)
  • getMovieRating (40-50)
  • getMovieRatingCount (52-60)
  • getMovieTitlesOther (98-120)
  • getMoviePoster (122-137)
  • getMovieRandomPhoto (139-147)
  • getMovieTrivia (149-156)
  • getMovieGroup (180-188)
  • getMovieVods (195-208)
  • getMovieTags (253-256)
  • getMoviePremieres (233-251)
  • getMovieBoxMovies (217-231)
src/dto/global.ts (1)
  • CSFDFilmTypes (25-36)
src/vars.ts (1)
  • movieUrl (7-8)
src/helpers/search.helper.ts (4)
src/dto/global.ts (2)
  • CSFDFilmTypes (25-36)
  • CSFDColorRating (21-21)
src/helpers/global.helper.ts (2)
  • parseColor (29-42)
  • addProtocol (44-46)
src/dto/user-ratings.ts (1)
  • Colors (21-21)
src/dto/movie.ts (1)
  • CSFDMovieCreator (66-77)
src/services/search.service.ts (3)
src/helpers/search.helper.ts (8)
  • getSearchUrl (22-24)
  • getSearchTitle (14-16)
  • getSearchYear (18-20)
  • getSearchType (9-12)
  • getSearchColorRating (26-30)
  • getSearchPoster (32-35)
  • getSearchOrigins (37-42)
  • parseSearchPeople (44-66)
src/dto/search.ts (1)
  • CSFDSearchMovie (11-15)
src/helpers/global.helper.ts (1)
  • parseIdFromUrl (4-12)
src/services/creator.service.ts (1)
src/helpers/creator.helper.ts (5)
  • getCreatorName (18-20)
  • getCreatorBirthdayInfo (22-43)
  • getCreatorPhoto (49-52)
  • getCreatorBio (45-47)
  • getCreatorFilms (66-95)
src/dto/search.ts (1)
src/dto/movie.ts (1)
  • CSFDMovieCreator (66-77)
tests/search.test.ts (3)
src/helpers/search.helper.ts (8)
  • getSearchTitle (14-16)
  • getSearchYear (18-20)
  • getSearchUrl (22-24)
  • getSearchType (9-12)
  • getSearchColorRating (26-30)
  • getSearchPoster (32-35)
  • getSearchOrigins (37-42)
  • parseSearchPeople (44-66)
src/dto/global.ts (2)
  • CSFDFilmTypes (25-36)
  • CSFDColorRating (21-21)
src/dto/movie.ts (1)
  • CSFDMovieCreator (66-77)
src/helpers/user-ratings.helper.ts (3)
src/dto/global.ts (2)
  • CSFDFilmTypes (25-36)
  • CSFDColorRating (21-21)
src/helpers/global.helper.ts (1)
  • parseColor (29-42)
src/dto/user-ratings.ts (1)
  • Colors (21-21)
src/services/cinema.service.ts (1)
src/helpers/cinema.helper.ts (1)
  • getCinemaCoords (27-45)
src/helpers/creator.helper.ts (4)
src/dto/global.ts (1)
  • CSFDColorRating (21-21)
src/helpers/global.helper.ts (3)
  • parseColor (29-42)
  • parseIdFromUrl (4-12)
  • addProtocol (44-46)
src/dto/user-ratings.ts (1)
  • Colors (21-21)
src/dto/creator.ts (1)
  • CSFDCreatorScreening (14-14)
tests/cinema.helper.test.ts (1)
src/helpers/cinema.helper.ts (3)
  • getCinemaUrlId (20-25)
  • getCinemaCoords (27-45)
  • getCinemaFilms (72-93)
tests/user-ratings.test.ts (2)
src/helpers/user-ratings.helper.ts (7)
  • getUserRatingId (6-9)
  • getUserRatingType (18-22)
  • getUserRatingTitle (24-26)
  • getUserRatingYear (28-30)
  • getUserRatingColorRating (32-35)
  • getUserRatingDate (37-39)
  • getUserRatingUrl (41-44)
src/dto/global.ts (2)
  • CSFDFilmTypes (25-36)
  • CSFDColorRating (21-21)
tests/movie.test.ts (5)
src/index.ts (1)
  • movie (28-30)
src/services/movie.service.ts (1)
  • movie (31-47)
src/helpers/movie.helper.ts (19)
  • getMovieId (16-19)
  • getMovieTitle (21-23)
  • getMoviePoster (122-137)
  • getMovieRandomPhoto (139-147)
  • getMovieTrivia (149-156)
  • getMovieDuration (72-96)
  • getMovieVods (195-208)
  • getMovieTitlesOther (98-120)
  • getMovieOrigins (30-34)
  • getMovieDescriptions (158-162)
  • getMovieGenres (25-28)
  • getMovieType (190-193)
  • getMovieYear (62-70)
  • getMovieRatingCount (52-60)
  • getMovieRating (40-50)
  • getMovieColorRating (36-38)
  • getMovieGroup (180-188)
  • getMoviePremieres (233-251)
  • getMovieBoxMovies (217-231)
src/dto/movie.ts (5)
  • CSFDVod (43-46)
  • CSFDTitlesOther (61-64)
  • CSFDMovieCreator (66-77)
  • CSFDPremiere (141-146)
  • CSFDMovieListItem (79-83)
src/dto/global.ts (1)
  • CSFDColorRating (21-21)
tests/creator.test.ts (2)
src/helpers/creator.helper.ts (5)
  • getCreatorName (18-20)
  • getCreatorBio (45-47)
  • getCreatorPhoto (49-52)
  • getCreatorBirthdayInfo (22-43)
  • getCreatorFilms (66-95)
src/dto/creator.ts (1)
  • CSFDCreatorScreening (14-14)
src/helpers/movie.helper.ts (3)
src/helpers/global.helper.ts (2)
  • parseIdFromUrl (4-12)
  • getColor (14-27)
src/dto/movie.ts (8)
  • CSFDGenres (85-127)
  • CSFDTitlesOther (61-64)
  • CSFDMovieCreator (66-77)
  • CSFDCreatorGroups (129-139)
  • CSFDVod (43-46)
  • CSFDBoxContent (148-148)
  • CSFDMovieListItem (79-83)
  • CSFDPremiere (141-146)
src/dto/global.ts (1)
  • CSFDColorRating (21-21)
src/helpers/cinema.helper.ts (3)
src/dto/global.ts (1)
  • CSFDColorRating (21-21)
src/helpers/global.helper.ts (1)
  • parseIdFromUrl (4-12)
src/dto/cinema.ts (1)
  • CSFDCinemaMovie (18-21)
🔇 Additional comments (15)
src/helpers/global.helper.ts (1)

1-2: Import path migration to DTOs looks good

No runtime impact; aligns with the DTO move.

server.ts (1)

6-6: DTO import path update is OK

Matches the refactor; no behavior change.

src/dto/cinema.ts (1)

1-1: Import path switch to DTO movie is correct

Consistent with the DTO migration; no behavioral change.

tsconfig.cjs.json (1)

1-7: Disable declaration emission for the CJS build

0 .d.ts found in dist/types — inconclusive; still recommend disabling declarations here (or run ESM and CJS builds sequentially) to avoid emit races/overwrites.

Apply to tsconfig.cjs.json:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "CommonJS",
-    "outDir": "./dist/cjs"
+    "outDir": "./dist/cjs",
+    "declaration": false
  }
}
src/vars.ts (1)

1-1: LGTM on DTO import path update.

tests/services.test.ts (1)

2-2: LGTM on DTO import migration.

tests/fetchers.test.ts (1)

3-4: LGTM on DTO import migration.

src/types.ts (1)

1-6: Centralized DTO re‑exports look good.

Keeps the public type surface tidy.

src/services/cinema.service.ts (1)

2-2: LGTM on DTO/helper migrations.

Imports and coords source updated consistently.

Also applies to: 6-7, 41-41

src/dto/search.ts (1)

2-2: LGTM on moving to CSFDMovieCreator.

Type surface aligns with movie DTOs.

Also applies to: 25-27, 30-32

tests/cinema.helper.test.ts (1)

45-56: Good coverage of happy- and null-paths for coords

Positive case + no-linkMapsEl case are both covered. Nice.

tests/user-ratings.test.ts (1)

35-43: IDs tests look solid

Covers both first and last entries; no concerns.

src/index.ts (1)

36-38: LGTM: new search facade wiring looks clean

Confirm no external docs/readme examples construct new Csfd(...). If they do, update them or add a deprecation note.

tests/search.test.ts (1)

3-15: LGTM: tests reflect the new Search helpers and DTO types

Good coverage across titles/years/urls/types/colors/posters/origins/people for movies and TV series. This gives confidence for the rename/refactor.

Also applies to: 23-334

src/helpers/cinema.helper.ts (1)

20-25: LGTM: graceful handling of empty URL

Returning null on falsy input aligns with downstream expectations.

Comment on lines +41 to 55
const url = getSearchUrl(m);

const movie: CSFDSearchMovie = {
id: parseIdFromUrl(url),
title: getTitle(m),
year: getYear(m),
title: getSearchTitle(m),
year: getSearchYear(m),
url: `https://www.csfd.cz${url}`,
type: getType(m),
colorRating: getColorRating(m),
poster: getPoster(m),
origins: getOrigins(m),
type: getSearchType(m),
colorRating: getSearchColorRating(m),
poster: getSearchPoster(m),
origins: getSearchOrigins(m),
creators: {
directors: parsePeople(m, 'directors'),
actors: parsePeople(m, 'actors')
directors: parseSearchPeople(m, 'directors'),
actors: parseSearchPeople(m, 'actors')
}
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

Double-check parseIdFromUrl return type

If parseIdFromUrl can return null, assigning to id: number violates the DTO. Either refine parseIdFromUrl to return number, guard against null, or widen the DTO if needed.

Run to verify the function signature and null returns:


🏁 Script executed:

#!/bin/bash
rg -n -C2 "export const parseIdFromUrl" --type ts
rg -n "\bparseIdFromUrl\s*\(" --type ts | rg -n -v "!==|!=|??|\|\|" -n

Length of output: 393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- src/helpers/global.helper.ts ----"
nl -ba src/helpers/global.helper.ts | sed -n '1,240p' || true

echo
echo "---- src/services/search.service.ts (first 120 lines) ----"
nl -ba src/services/search.service.ts | sed -n '1,160p' || true

echo
echo "---- locate CSFDSearchMovie definition ----"
rg -n "CSFDSearchMovie" --hidden --glob '!**/node_modules/**' || true

Length of output: 374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "PWD: $(pwd)"
echo

echo "---- src/helpers/global.helper.ts (up to 300 lines) ----"
if [ -f src/helpers/global.helper.ts ]; then
  sed -n '1,300p' src/helpers/global.helper.ts || true
else
  echo "MISSING: src/helpers/global.helper.ts"
fi
echo

echo "---- src/services/search.service.ts (up to 300 lines) ----"
if [ -f src/services/search.service.ts ]; then
  sed -n '1,300p' src/services/search.service.ts || true
else
  echo "MISSING: src/services/search.service.ts"
fi
echo

echo "---- Search for CSFDSearchMovie type/interface ----"
rg -n --hidden -uu "CSFDSearchMovie" || true

Length of output: 5549


parseIdFromUrl returns null but DTOs expect a non-nullable id

src/helpers/global.helper.ts shows parseIdFromUrl returns null in some branches (returns +id || null / return null), while the search DTOs (src/dto/global.ts → CSFDScreening.id and src/dto/search.ts → CSFDSearchMovie/CSFDSearchUser) declare id as number. Fix one of the following:

  • Change parseIdFromUrl to a safe non-null number (never return null) and keep DTOs as-is.
  • OR change parseIdFromUrl's signature to return number | null and update the DTOs (CSFDScreening / CSFDSearchMovie / CSFDSearchUser) to accept number | null.
  • OR guard at call sites (src/services/search.service.ts — the id assignments around lines ~41–55) before assigning/including items (skip, throw, or provide a fallback).

[src/helpers/global.helper.ts, src/services/search.service.ts, src/dto/global.ts, src/dto/search.ts]

🤖 Prompt for AI Agents
In src/services/search.service.ts around lines 41–55, parseIdFromUrl can return
null while CSFDSearchMovie.id is declared as a non-nullable number; fix by
guarding at the call site: call parseIdFromUrl, check if the returned id is
null, and if so skip creating/including that movie entry (or alternatively throw
or provide a defined fallback id), otherwise construct the CSFDSearchMovie with
the non-null id; this keeps DTOs unchanged and prevents assigning null to a
non-nullable field.

"compilerOptions": {
"target": "es2017",
"target": "es2015",
"lib": ["dom"],
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

lib excludes ES libs; Promise/Array types may break

Setting "lib": ["dom"] overrides defaults and drops ES libs. Add an ES lib to match target.

Apply:

-    "lib": ["dom"],
+    "lib": ["ES2019", "DOM"],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"lib": ["dom"],
"lib": ["ES2019", "DOM"],
🤖 Prompt for AI Agents
In tsconfig.json around line 4, the "lib" value only lists "dom" which overrides
defaults and omits ECMAScript library declarations (so Promise/Array etc. can be
missing); update the "lib" array to include an ES lib that matches your target
(for example add "es2019" or "es6" alongside "dom", e.g. ["es2019","dom"]),
ensuring the chosen ES lib version aligns with the "target" setting.

Copy link

deepsource-io bot commented Sep 17, 2025

Here's the code health analysis summary for commits f3ccda9..e33a989. View details on DeepSource ↗.

Analysis Summary

AnalyzerStatusSummaryLink
DeepSource JavaScript LogoJavaScript❌ Failure
❗ 6 occurences introduced
🎯 7 occurences resolved
View Check ↗

💡 If you’re a repository administrator, you can configure the quality gates from the settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
src/services/user-ratings.service.ts (2)

33-35: Potential crash when pagination is absent.

Optional chaining is ineffective because pagesNode.childNodes is accessed unguarded in the index expression. This can throw on single-page profiles.

Apply a safe parse:

-    const pagesNode = items.querySelector('.pagination');
-    const pages = +pagesNode?.childNodes[pagesNode.childNodes.length - 4].rawText || 1;
+    const pagesNode = items.querySelector('.pagination');
+    let pages = 1;
+    if (pagesNode) {
+      const children = pagesNode.childNodes ?? [];
+      // Heuristic: last numeric item before "next" control
+      const idx = Math.max(0, children.length - 4);
+      const text = (children[idx] as any)?.rawText?.trim?.() ?? '';
+      const n = parseInt(text, 10);
+      pages = Number.isFinite(n) ? n : 1;
+    }

21-29: Reset accumulator per call to avoid cross-call leakage.

this.films is never cleared; repeated invocations will accumulate stale results.

   public async userRatings(
     user: string | number,
     config?: CSFDUserRatingConfig
   ): Promise<CSFDUserRatings[]> {
-    let allMovies: CSFDUserRatings[] = [];
+    this.films = [];
+    let allMovies: CSFDUserRatings[] = [];
tests/fetchers.test.ts (2)

17-19: Remove generics on matchers; Vitest/Jest matchers aren’t generic.

Type args like , , <string[]>, on toEqual/toContain aren’t part of the matcher APIs and can cause TS errors.

Apply this diff snippet pattern (representative lines shown):

- expect(movieSelected?.year).toEqual<number>(2025);
+ expect(movieSelected?.year).toEqual(2025);

- expect(movieSelected?.userDate).toContain<string>('2025');
+ expect(movieSelected?.userDate).toContain('2025');

- expect(movie.type).toEqual<CSFDFilmTypes>('film');
+ expect(movie.type).toEqual('film');

- expect(movie.colorRating).toEqual<CSFDColorRating>('good');
+ expect(movie.colorRating).toEqual('good');

- expect(movie.origins[0]).toEqual<string>('USA');
+ expect(movie.origins[0]).toEqual('USA');

- expect(movie.year).toEqual<number>(1994);
+ expect(movie.year).toEqual(1994);

- expect(matrix?.creators?.directors.map((x) => x.name)).toEqual<string[]>(
+ expect(matrix?.creators?.directors.map((x) => x.name)).toEqual(
    expect.arrayContaining(['Lilly Wachowski'])
  );

- expect(creator.films.find((film) => film.title === 'Hej-rup!')).toEqual<CSFDCreatorScreening>({
+ expect(creator.films.find((film) => film.title === 'Hej-rup!')).toEqual({
    id: 3106,
    title: 'Hej-rup!',
    year: 1934,
    colorRating: 'good'
  });

Also applies to: 43-43, 46-46, 48-48, 52-53, 63-63, 77-82


94-94: Fix error assertions inside catch; use instance checks, not toThrow/toContain.

toThrow is for functions, not error instances; toContain doesn’t apply to Error.

Apply this diff:

-      expect(e).toContain(Error);
+      expect(e).toBeInstanceOf(Error);
-      expect(e).toThrow(Error);
+      expect(e).toBeInstanceOf(Error);

Also applies to: 106-106

src/helpers/creator.helper.ts (2)

22-43: getCreatorBirthdayInfo: null-safety and typings.

Accessing el.querySelector when el is null will throw; age can be null but typed as number.

-export const getCreatorBirthdayInfo = (
-  el: HTMLElement | null
-): { birthday: string; age: number; birthPlace: string } => {
-  const infoBlock = el.querySelector('h1 + p');
+export const getCreatorBirthdayInfo = (
+  el: HTMLElement | null
+): { birthday: string | null; age: number | null; birthPlace: string | null } => {
+  const infoBlock = el?.querySelector('h1 + p');
   const text = infoBlock?.innerHTML.trim();
@@
-  let birthday: string = '';
+  let birthday: string | null = null;
@@
-  const age = ageRow ? +parseAge(ageRow) : null;
-  const birthPlace = birthPlaceRow ? parseBirthPlace(birthPlaceRow) : '';
+  const age = ageRow ? parseAge(ageRow) : null;
+  const birthPlace = birthPlaceRow ? parseBirthPlace(birthPlaceRow) : null;
 
   return { birthday, age, birthPlace };
 };

66-95: getCreatorFilms: potential NPEs, uninitialized yearCache, and mixed return types.

  • el.querySelectorAll... when el is null will throw.
  • yearCache used before set.
  • May return {} which violates the return type.
  • getCreatorColorRating can receive null icon.
-export const getCreatorFilms = (el: HTMLElement | null): CSFDCreatorScreening[] => {
-  const filmNodes = el.querySelectorAll('.box')[0]?.querySelectorAll('table tr');
-  let yearCache: number;
-  const films = filmNodes.map((filmNode) => {
-    const id = getCreatorId(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 = getCreatorColorRating(filmNode.querySelector('.name .icon'));
-
-    // Cache year from previous film because there is a gap between movies with same year
-    if (year) {
-      yearCache = +year;
-    }
-
-    if (id && title) {
-      return {
-        id,
-        title,
-        year: year || yearCache,
-        colorRating
-      };
-    }
-    return {};
-  });
-  // Remove empty objects
-  const filmsUnique = films.filter(
-    (value) => Object.keys(value).length !== 0
-  ) as CSFDCreatorScreening[];
-  return filmsUnique;
-};
+export const getCreatorFilms = (el: HTMLElement | null): CSFDCreatorScreening[] => {
+  const rows = el?.querySelectorAll('.box')?.[0]?.querySelectorAll('table tr') ?? [];
+  let yearCache: number | null = null;
+  const out: CSFDCreatorScreening[] = [];
+  for (const row of rows) {
+    const id = getCreatorId(row.querySelector('td.name .film-title-name')?.getAttribute('href'));
+    const title = row.querySelector('.name')?.text?.trim();
+    const yearText = row.querySelector('.year')?.text?.trim();
+    const parsedYear = yearText ? Number(yearText) : NaN;
+    if (!Number.isNaN(parsedYear)) yearCache = parsedYear;
+    const year = Number.isNaN(parsedYear) ? yearCache : parsedYear;
+    const colorRating = getCreatorColorRating(row.querySelector('.name .icon'));
+    if (id != null && title && year != null) {
+      out.push({ id, title, year, colorRating });
+    }
+  }
+  return out;
+};
src/helpers/cinema.helper.ts (1)

27-45: Harden map coords parsing; guard missing href; handle encoded/extra params

Current split-based parsing breaks on encoded commas (%2C) and when extra params follow q. Use URL/URLSearchParams and decode.

 export const getCinemaCoords = (el: HTMLElement | null): { lat: number; lng: number } | null => {
 
   if (!el) return null;
   const linkMapsEl = el.querySelector('a[href*="q="]');
   if (!linkMapsEl) return null;
 
-  const linkMaps = linkMapsEl.getAttribute('href');
-  const [_, latLng] = linkMaps.split('q=');
-
-  const coords = latLng.split(',');
-  if (coords.length !== 2) return null;
-
-  const lat = Number(coords[0]);
-  const lng = Number(coords[1]);
-  if (Number.isFinite(lat) && Number.isFinite(lng)) {
-    return { lat, lng };
-  }
-  return null;
+  const href = linkMapsEl.getAttribute('href');
+  if (!href) return null;
+  try {
+    const u = new URL(href, 'https://csfd.cz');
+    const q = u.searchParams.get('q');
+    if (!q) return null;
+    const [latStr, lngStr] = decodeURIComponent(q).split(',');
+    const lat = Number.parseFloat(latStr);
+    const lng = Number.parseFloat(lngStr);
+    if (Number.isFinite(lat) && Number.isFinite(lng)) {
+      return { lat, lng };
+    }
+  } catch {
+    // ignore parse errors
+  }
+  return null;
 };
♻️ Duplicate comments (6)
src/helpers/creator.helper.ts (1)

11-16: Return-type mismatch in getCreatorId (returns null but typed number).

Keep signature nullable and accept undefined inputs.

Apply:

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

Additionally, align parseIdFromUrl’s return type to number | null in src/helpers/global.helper.ts:

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

Run to find and update any callers relying on non-null ids:

#!/bin/bash
rg -nP "parseIdFromUrl\s*\(" -C2
rg -nP "getCreatorId\s*\(" -C2
src/helpers/search.helper.ts (5)

14-16: Null‑safety for missing title node.

Guard selector and trim result.

-export const getSearchTitle = (el: HTMLElement): string => {
-  return el.querySelector('.film-title-name').text;
-};
+export const getSearchTitle = (el: HTMLElement): string => {
+  const n = el.querySelector('.film-title-name');
+  return (n?.text ?? '').trim();
+};

18-20: Optional chaining misuse: can throw when year node is absent.

.replace is accessed on possibly undefined. Also make conversion resilient.

-export const getSearchYear = (el: HTMLElement): number => {
-  return +el.querySelectorAll('.film-title-info .info')[0]?.innerText.replace(/[{()}]/g, '');
-};
+export const getSearchYear = (el: HTMLElement): number => {
+  const raw = el.querySelectorAll('.film-title-info .info')[0]
+    ?.innerText
+    ?.replace(/[{()}]/g, '')
+    ?.trim();
+  const num = raw ? Number(raw) : NaN;
+  return Number.isFinite(num) ? num : NaN;
+};

22-24: Null‑safety for URL.

Avoid .attributes on null.

-export const getSearchUrl = (el: HTMLElement): string => {
-  return el.querySelector('.film-title-name').attributes.href;
-};
+export const getSearchUrl = (el: HTMLElement): string => {
+  const href = el.querySelector('.film-title-name')?.attributes?.href;
+  return href ?? '';
+};

26-30: Harden color parsing when icon/class is missing.

Prevent .split on undefined and provide a safe fallback.

-export const getSearchColorRating = (el: HTMLElement): CSFDColorRating => {
-  return parseColor(
-    el.querySelector('.article-header i.icon').classNames.split(' ').pop() as Colors
-  );
-};
+export const getSearchColorRating = (el: HTMLElement): CSFDColorRating => {
+  const icon = el.querySelector('.article-header i.icon');
+  const color = icon?.classNames?.split(' ')?.pop() as Colors | undefined;
+  return parseColor((color ?? 'lightgrey') as Colors);
+};

32-35: Guard poster access.

Avoid .attributes.src on null and keep return type stable.

-export const getSearchPoster = (el: HTMLElement): string => {
-  const image = el.querySelector('img').attributes.src;
-  return addProtocol(image);
-};
+export const getSearchPoster = (el: HTMLElement): string => {
+  const image = el.querySelector('img')?.attributes?.src;
+  return image ? addProtocol(image) : '';
+};
🧹 Nitpick comments (11)
tests/movie.test.ts (3)

592-594: Fix wrong generic/type in anomaly test.

getMovieRatingCount returns number|null; the test asserts null but types it as CSFDMovieListItem[]. Use null assertion directly.

Apply:

-      expect(movie).toEqual<CSFDMovieListItem[]>(null as any);
+      expect(movie).toBeNull();

380-387: Disambiguate duplicate test names.

Two tests are named "Year". Give unique names to ease triage.

-  test('Year', () => {
+  test('Year (movie)', () => {
@@
-  test('Year', () => {
+  test('Year (rich)', () => {

116-120: Either enable or remove commented-out tests.

Keep the suite tidy or track with a TODO tag referencing an issue.

Also applies to: 136-139, 232-235

src/services/user-ratings.service.ts (3)

61-71: Make config optional in getPage signature.

Call sites pass possibly undefined; the method already guards internally.

-  private getPage(config: CSFDUserRatingConfig, movies: HTMLElement[]) {
+  private getPage(config: CSFDUserRatingConfig | undefined, movies: HTMLElement[]) {

39-55: Gate noisy logs behind a flag.

Unconditional console logs when allPages is set can spam consumers.

-      console.log('User', user, url);
-      console.log('Fetching all pages', pages);
+      if (config?.debug) {
+        console.log('User', user, url);
+        console.log('Fetching all pages', pages);
+      }
@@
-        console.log('Fetching page', i, 'out of', pages, '...');
+        if (config?.debug) console.log('Fetching page', i, 'out of', pages, '...');

96-103: Minor: avoid redundant casts if helpers are typed.

If getUserRatingColorRating/getUserRating already return CSFDColorRating/CSFDStars, drop the casts.

tests/fetchers.test.ts (1)

9-22: Quarantine “live” tests to reduce flakiness.

These depend on a real site and can break CI; gate behind env or use describe.runIf.

Example:

-describe('Live: Fetch rating page', () => {
+describe.runIf(!!process.env.CSFD_LIVE_TESTS)('Live: Fetch rating page', () => {

Also applies to: 32-55, 58-67, 70-84

src/helpers/search.helper.ts (2)

37-42: Tidy optional chaining to avoid future pitfalls.

You already guard originsRaw; remove redundant ?. and ensure .map isn’t accessed on possibly undefined.

-  const originsRaw = el.querySelector('.article-content p .info')?.text;
-  if (!originsRaw) return [];
-  const originsAll = originsRaw?.split(', ')?.[0];
-  return originsAll?.split('/').map((country) => country.trim());
+  const originsRaw = el.querySelector('.article-content p .info')?.text;
+  if (!originsRaw) return [];
+  const originsAll = originsRaw.split(', ')[0];
+  return originsAll.split('/').map((country) => country.trim());

44-66: Initialize discriminator deterministically and harden mapping.

Prevents “used before assign” warnings and handles missing href/text gracefully.

-export const parseSearchPeople = (el: HTMLElement, type: 'directors' | 'actors'): CSFDMovieCreator[] => {
-  let who: Creator;
-  if (type === 'directors') who = 'Režie:';
-  if (type === 'actors') who = 'Hrají:';
+export const parseSearchPeople = (el: HTMLElement, type: 'directors' | 'actors'): CSFDMovieCreator[] => {
+  const who: Creator = type === 'directors' ? 'Režie:' : 'Hrají:';
 
-  const peopleNode = Array.from(el && el.querySelectorAll('.article-content p')).find((el) =>
-    el.textContent.includes(who)
-  );
+  const peopleNode = Array.from(el.querySelectorAll('.article-content p')).find((p) =>
+    p.textContent?.includes(who)
+  );
 
   if (peopleNode) {
     const people = Array.from(peopleNode.querySelectorAll('a')) as unknown as HTMLElement[];
 
     return people.map((person) => {
+      const href = person.attributes?.href ?? '';
       return {
-        id: parseIdFromUrl(person.attributes.href),
-        name: person.innerText.trim(),
-        url: `https://www.csfd.cz${person.attributes.href}`
+        id: parseIdFromUrl(href),
+        name: person.innerText?.trim() ?? '',
+        url: href ? `https://www.csfd.cz${href}` : ''
       };
     });
   } else {
     return [];
   }
 };
tests/search.test.ts (2)

23-35: Reduce noise: avoid generics on toEqual and consider table‑driven tests.

Using toEqual<T> doesn’t affect runtime and adds clutter. Prefer plain assertions or as casts. Consider test.each to condense repetitive cases.

Example for titles:

-const movie = getSearchTitle(moviesNode[0]);
-expect(movie).toEqual<string>('Matrix');
+expect(getSearchTitle(moviesNode[0])).toEqual('Matrix');

Example table:

test.each([
  [0, 'Matrix'],
  [moviesNode.length - 1, 'Matrix hunter'],
  [5, 'Matrix - Reloaded'],
])('title at %d', (idx, expected) => {
  expect(getSearchTitle(moviesNode[idx])).toBe(expected);
});

Also applies to: 38-50, 53-66, 68-81, 83-96, 98-117, 119-136, 138-173, 177-190, 192-205, 207-220, 222-235, 237-250, 252-265, 267-284, 286-333


53-66: Add edge‑case tests for null‑safety (if you adopt the helper hardening).

Cover missing nodes (e.g., no .film-title-name, no img, no year node) to lock desired behavior ('', NaN, []).

Proposed additions:

test('missing title returns empty string', () => {
  const article = parse('<article class="main-movies"></article>');
  expect(getSearchTitle(article as any)).toBe('');
});

test('missing year returns NaN', () => {
  const article = parse('<article class="main-movies"><div class="film-title-info"><span class="info"></span></div></article>');
  expect(Number.isNaN(getSearchYear(article as any))).toBe(true);
});

test('missing poster returns empty string', () => {
  const article = parse('<article class="main-movies"></article>');
  expect(getSearchPoster(article as any)).toBe('');
});

Also applies to: 98-117, 252-265

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4af7602 and 2406c61.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (31)
  • barrels.json (1 hunks)
  • package.json (4 hunks)
  • server.ts (1 hunks)
  • src/dto/cinema.ts (1 hunks)
  • src/dto/movie.ts (1 hunks)
  • src/dto/search.ts (2 hunks)
  • src/helpers/cinema.helper.ts (3 hunks)
  • src/helpers/creator.helper.ts (2 hunks)
  • src/helpers/global.helper.ts (1 hunks)
  • src/helpers/movie.helper.ts (13 hunks)
  • src/helpers/search.helper.ts (1 hunks)
  • src/helpers/user-ratings.helper.ts (2 hunks)
  • src/index.ts (1 hunks)
  • src/services/cinema.service.ts (2 hunks)
  • src/services/creator.service.ts (2 hunks)
  • src/services/movie.service.ts (2 hunks)
  • src/services/search.service.ts (3 hunks)
  • src/services/user-ratings.service.ts (3 hunks)
  • src/types.ts (1 hunks)
  • src/vars.ts (1 hunks)
  • tests/cinema.helper.test.ts (5 hunks)
  • tests/cinema.service.test.ts (1 hunks)
  • tests/creator.test.ts (7 hunks)
  • tests/fetchers.test.ts (1 hunks)
  • tests/movie.test.ts (13 hunks)
  • tests/search.test.ts (8 hunks)
  • tests/services.test.ts (1 hunks)
  • tests/user-ratings.test.ts (2 hunks)
  • tsconfig.cjs.json (1 hunks)
  • tsconfig.esm.json (1 hunks)
  • tsconfig.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/services.test.ts
🚧 Files skipped from review as they are similar to previous changes (22)
  • src/types.ts
  • server.ts
  • src/helpers/global.helper.ts
  • src/dto/movie.ts
  • src/vars.ts
  • tsconfig.cjs.json
  • tests/cinema.service.test.ts
  • src/services/creator.service.ts
  • src/dto/search.ts
  • tsconfig.json
  • tsconfig.esm.json
  • src/dto/cinema.ts
  • barrels.json
  • src/services/cinema.service.ts
  • src/services/movie.service.ts
  • tests/creator.test.ts
  • package.json
  • tests/user-ratings.test.ts
  • src/helpers/movie.helper.ts
  • src/helpers/user-ratings.helper.ts
  • tests/cinema.helper.test.ts
  • src/services/search.service.ts
🧰 Additional context used
🧬 Code graph analysis (6)
src/services/user-ratings.service.ts (2)
src/helpers/user-ratings.helper.ts (7)
  • getUserRatingType (18-22)
  • getUserRatingId (6-9)
  • getUserRatingTitle (24-26)
  • getUserRatingYear (28-30)
  • getUserRatingUrl (41-44)
  • getUserRatingColorRating (32-35)
  • getUserRatingDate (37-39)
src/dto/global.ts (1)
  • CSFDColorRating (21-21)
tests/search.test.ts (3)
src/helpers/search.helper.ts (8)
  • getSearchTitle (14-16)
  • getSearchYear (18-20)
  • getSearchUrl (22-24)
  • getSearchType (9-12)
  • getSearchColorRating (26-30)
  • getSearchPoster (32-35)
  • getSearchOrigins (37-42)
  • parseSearchPeople (44-66)
src/dto/global.ts (2)
  • CSFDFilmTypes (25-36)
  • CSFDColorRating (21-21)
src/dto/movie.ts (1)
  • CSFDMovieCreator (66-77)
src/helpers/search.helper.ts (4)
src/dto/global.ts (2)
  • CSFDFilmTypes (25-36)
  • CSFDColorRating (21-21)
src/helpers/global.helper.ts (2)
  • parseColor (29-42)
  • addProtocol (44-46)
src/dto/user-ratings.ts (1)
  • Colors (21-21)
src/dto/movie.ts (1)
  • CSFDMovieCreator (66-77)
tests/movie.test.ts (4)
src/services/movie.service.ts (1)
  • movie (31-47)
src/helpers/movie.helper.ts (19)
  • getMovieId (16-19)
  • getMovieTitle (21-23)
  • getMoviePoster (122-137)
  • getMovieRandomPhoto (139-147)
  • getMovieTrivia (149-156)
  • getMovieDuration (72-96)
  • getMovieVods (195-208)
  • getMovieTitlesOther (98-120)
  • getMovieOrigins (30-34)
  • getMovieDescriptions (158-162)
  • getMovieGenres (25-28)
  • getMovieType (190-193)
  • getMovieYear (62-70)
  • getMovieRatingCount (52-60)
  • getMovieRating (40-50)
  • getMovieColorRating (36-38)
  • getMovieGroup (180-188)
  • getMoviePremieres (233-251)
  • getMovieBoxMovies (217-231)
src/dto/movie.ts (5)
  • CSFDVod (43-46)
  • CSFDTitlesOther (61-64)
  • CSFDMovieCreator (66-77)
  • CSFDPremiere (141-146)
  • CSFDMovieListItem (79-83)
src/dto/global.ts (1)
  • CSFDColorRating (21-21)
src/helpers/creator.helper.ts (4)
src/dto/global.ts (1)
  • CSFDColorRating (21-21)
src/helpers/global.helper.ts (3)
  • parseColor (29-42)
  • parseIdFromUrl (4-12)
  • addProtocol (44-46)
src/dto/user-ratings.ts (1)
  • Colors (21-21)
src/dto/creator.ts (1)
  • CSFDCreatorScreening (14-14)
src/helpers/cinema.helper.ts (3)
src/dto/global.ts (1)
  • CSFDColorRating (21-21)
src/helpers/global.helper.ts (1)
  • parseIdFromUrl (4-12)
src/dto/cinema.ts (1)
  • CSFDCinemaMovie (18-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (11)
tests/movie.test.ts (2)

3-12: DTO-based imports look good.

Types are correctly sourced from dto/* and helper import paths align with the refactor.


13-32: Helper migration is consistent and behavior-preserving.

getX → getMovieX renames are applied uniformly; expectations match the helper contracts shown in dto/helper snippets.

Also applies to: 113-161, 166-185, 189-212, 216-235, 239-281, 303-316, 320-327, 331-347, 351-362, 366-377, 381-396, 400-416, 420-430, 447-485, 489-550, 555-571, 577-587

src/services/user-ratings.service.ts (1)

2-4: DTO and helper import updates are correct.

Aligned with the DTO surface; no issues.

Also applies to: 7-15

src/index.ts (3)

1-5: DTO export migration is correct.

Public types now sourced from dto/*; consistent with the refactor.


13-19: Constructor wiring and new search() API look good.

Dependency order matches instantiation; method forwards correctly.

Also applies to: 36-38


51-57: Factory instantiation order is correct.

Matches constructor signature; safe for both ESM/CJS entrypoints.

tests/fetchers.test.ts (1)

3-4: DTO import path updates look good.

Consistent with the repo-wide move to dto/*.

Please confirm barrel exports (src/index) still re-export these DTO types for consumers if that’s intended.

src/helpers/creator.helper.ts (1)

2-5: Type imports from dto/ LGTM.*

Aligns helpers with the DTO move.

src/helpers/search.helper.ts (1)

14-35: Repo‑wide: scan for optional‑chaining → method‑call hazards

Optional chaining that only guards a property but not the subsequent method call (e.g., obj?.prop.replace(...)) can still call a method on undefined — audit and guard the method target (use ?. before the call, provide defaults, or assert presence).

Affected: src/helpers/search.helper.ts — lines 14–35 (examples: getSearchYear uses [0]?.innerText.replace(...); getSearchColorRating uses .classNames.split(...); getSearchTitle/getSearchUrl/getSearchPoster call properties on querySelector results without null-checks).

Run locally to find occurrences:
rg -nP --hidden -g '!.git' -g '!node_modules' -g '/.{ts,tsx,js,jsx}' '?.\s[A-Za-z0-9_$]+.[A-Za-z0-9_$]+\s*(' -C2
rg -nP --hidden -g '
/.{ts,tsx,js,jsx}' '?.\sinnerText.replace(' -C2
rg -nP --hidden -g '/.{ts,tsx,js,jsx}' '?.\sclassNames.split(' -C2
rg -nP --hidden -g '
/.{ts,tsx,js,jsx}' '?.\ssplit([^)]*).map(' -C2

Quick fixes: use expr?.prop?.replace(...), (expr ?? '').replace(...), or check for existence before calling.

src/helpers/cinema.helper.ts (2)

79-79: Callsite OK after color-rating fix

With getCinemaColorRating(el: HTMLElement | null), this line becomes null-safe. No further changes needed.

Ensure .name .icon exists in all tested pages; otherwise expect 'unknown' ratings from the fallback.


66-66: Keep the signature; guard for missing films

getCinemaFilms is called externally (tests/cinema.helper.test.ts:76 and src/helpers/cinema.helper.ts:66) — do not remove the unused date parameter; change the return to:
return { date: dateText, films: films ? getCinemaFilms('', films) : [] };

Likely an incorrect or invalid review comment.

@bartholomej bartholomej requested a review from Copilot September 18, 2025 15:29
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds dual ESM and CommonJS build support while refactoring the codebase to use prefixed helper functions and restructured DTO modules for better organization and type safety.

  • Adds dual ESM/CommonJS distribution with separate TypeScript configurations
  • Refactors helper functions to use domain-specific prefixes (Movie, Creator, Search, UserRating, Cinema)
  • Moves interfaces to DTO modules and creates centralized type exports

Reviewed Changes

Copilot reviewed 30 out of 35 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tsconfig.esm.json, tsconfig.cjs.json New TypeScript configurations for ESM and CJS builds
package.json Updates build scripts and export configurations for dual module support
src/types.ts New centralized type exports barrel file
barrels.json Configuration for generating barrel exports
src/helpers/*.ts Renamed helper functions with domain prefixes
src/dto/*.ts Restructured type imports from interfaces to DTO modules
tests/*.ts Updated test imports and function calls to match new naming
src/services/*.ts Updated to use new helper function names and DTO imports
Comments suppressed due to low confidence (1)

tests/cinema.helper.test.ts:1

  • Import path is incorrect. The file should import from '../src/dto/cinema' not '../src/dto/cinema.interface' since the file was renamed from cinema.interface.ts to cinema.ts.
import { HTMLElement, parse } from 'node-html-parser';

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
package.json (2)

9-11: Build order: verify barrels target (src vs dist).

Barrels run after all tsc builds. If barrels generate sources (src), run them before tsc; if they generate dist barrels, keep as-is. Please confirm the barrels.json target to avoid stale declarations.


17-17: Harden fix-paths step.

If npm-prepare-dist ever skips/copies a different package.json path, this command will silently do nothing. Consider guarding with an existence check and failing fast, or extend it to also ensure the "exports" map matches the root.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2406c61 and 3d710ee.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (1)
  • package.json (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (2)
package.json (2)

38-38: ESLint 9 + @typescript-eslint 8: confirm peer compatibility.

This combo should work, but eslint-config-google@0.14.0 may lag behind ESLint 9 in some setups. Please verify locally that there are no peer warnings and lint runs clean.

Also applies to: 40-42, 45-45, 47-47, 54-56


91-104: Fix conditional exports for types: move "types" to the top-level of "." and flatten import/require.

Types under import/require are ignored by TS/Node resolution. Put "types" as the first condition at the "." level; keep top-level "types" as fallback.

Apply:

-  "exports": {
-    ".": {
-      "import": {
-        "types": "./index.dto.d.ts",
-        "default": "./esm/index.js"
-      },
-      "require": {
-        "types": "./index.dto.d.ts",
-        "default": "./cjs/index.js"
-      }
-    }
-  }
+  "exports": {
+    ".": {
+      "types": "./index.dto.d.ts",
+      "import": "./esm/index.js",
+      "require": "./cjs/index.js"
+    }
+  }

Optional: also expose package.json for tooling:

   "exports": {
     ".": {
       "types": "./index.dto.d.ts",
       "import": "./esm/index.js",
       "require": "./cjs/index.js"
-    }
+    },
+    "./package.json": "./package.json"
   }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
tests/fetchers.test.ts (1)

1-5: Import types from the barrel to decouple tests from internal layout

Use the public types barrel (src/types.ts) instead of deep dto/* paths to reduce churn if files move.

Please confirm src/types.ts re‑exports these symbols.

-import { CSFDCinema } from '../src/dto/cinema';
-import { CSFDCreatorScreening } from '../src/dto/creator';
-import { CSFDColorRating, CSFDFilmTypes } from '../src/dto/global';
+import {
+  CSFDCinema,
+  CSFDCreatorScreening,
+  CSFDColorRating,
+  CSFDFilmTypes
+} from '../src/types';
package.json (2)

54-55: Minor: lint-staged + ESLint CLI OK. Consider using --max-warnings=0 in CI.

Non-blocking.


16-16: Tidy: call Vitest directly.

Avoid "yarn in yarn". Use Vitest directly for coverage.

-    "test:coverage": "yarn test run --coverage",
+    "test:coverage": "vitest run --coverage",
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2406c61 and cd5c6ab.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (4)
  • package.json (3 hunks)
  • tests/cinema.service.test.ts (1 hunks)
  • tests/creator.test.ts (7 hunks)
  • tests/fetchers.test.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/creator.test.ts
  • tests/cinema.service.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
tests/fetchers.test.ts (3)
src/index.ts (1)
  • csfd (51-57)
src/services/cinema.service.ts (1)
  • cinemas (16-29)
src/dto/cinema.ts (1)
  • CSFDCinema (3-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (8)
tests/fetchers.test.ts (3)

56-58: Negative test looks good; confirm error type is stable

If the implementation throws a specific error (e.g., TypeError or custom), consider asserting that type/message for stronger guarantees.


115-117: Negative test looks good; confirm error type is stable

Same note as for movie: if a specific error class/message is thrown, assert it for stronger guarantees.


73-99: Harden cinema tests: avoid brittle indexing, fix comment/title, add timeout

Guard against empty/short responses, remove duplicate test title, and extend beforeAll timeout for live fetch.
File: tests/fetchers.test.ts (around lines 73–99)

-// Search
+// Cinemas
 describe('Live: Cinemas', () => {
   let cinemas: CSFDCinema[]
-  beforeAll(async () => {
-    cinemas = await csfd.cinema(1, 'today');
-  });
+  beforeAll(async () => {
+    cinemas = await csfd.cinema(1, 'today');
+  }, 30000);
   test('Check city', async () => {
+    expect(cinemas.length).toBeGreaterThan(0);
     const pragueCinemas = cinemas.filter((x) => x.city.includes('Praha'));
     expect(pragueCinemas.length).toBeGreaterThan(0);
   });
   test('Check screenings', async () => {
-    const screenings = cinemas[0].screenings;
+    expect(cinemas[0]).toBeDefined();
+    const screenings = cinemas[0].screenings;
     expect(screenings.length).toBeGreaterThan(0);
   });
-  test('Check screenings', async () => {
-    const film = cinemas[0].screenings[0].films[0];
+  test('Check film details', async () => {
+    expect(cinemas[0]?.screenings?.[0]?.films?.[0]).toBeDefined();
+    const film = cinemas[0].screenings[0].films[0];
     expect(film.id).toBeDefined();
     expect(film.meta).toBeDefined();
     expect(film.title).toBeDefined();
     expect(film.url).toContain('/film/');
   });
   test('Check showtimes', async () => {
-    const film = cinemas[0].screenings[0].films[1].showTimes[0];
-    expect(film).toBeDefined();
+    expect(cinemas[0]?.screenings?.[0]?.films?.[0]).toBeDefined();
+    const showtime = cinemas[0].screenings[0].films[0].showTimes?.[0];
+    expect(showtime).toBeDefined();
   });
 });

Search didn't find a Csfd class in the repo — confirm csfd.cinema(...) is the correct API.

package.json (5)

9-11: Run barrels before compilation and remove redundant tsc invocation.

Barrel generation should precede tsc so generated indexes are compiled. Also, the initial bare tsc is likely redundant with the two targeted builds.

[Suggest_recommended_refactor]

-    "build": "tsc && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && yarn barrels",
+    "build": "yarn barrels && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json",

45-45: Barrelsby in devDeps is fine. Ensure barrels.json is excluded from publish.

Confirm barrels.json isn’t included in dist tarball (you publish from dist, so likely fine).


47-47: ESLint v9 chosen. Ensure config migrated to flat config.

If you still use legacy extends (e.g., eslint-config-google), expect breakage on v9.


91-104: Fix invalid export map: move "types" to top-level conditions and avoid nesting under "import"/"require".

Current structure nests "types" inside "import"/"require", which is not a valid conditional-exports shape. Place "types" as a sibling of "import"/"require" (ideally first), and optionally add a default and a subpath for package.json.

Apply:

-  "exports": {
-    ".": {
-      "import": {
-        "types": "./index.dto.d.ts",
-        "default": "./esm/index.js"
-      },
-      "require": {
-        "types": "./index.dto.d.ts",
-        "default": "./cjs/index.js"
-      }
-    }
-  }
+  "exports": {
+    ".": {
+      "types": "./index.dto.d.ts",
+      "import": "./esm/index.js",
+      "require": "./cjs/index.js",
+      "default": "./esm/index.js"
+    },
+    "./package.json": "./package.json"
+  }

38-43: Confirm ESLint mode or migrate eslint-config-google

eslint-config-google is an eslintrc-style (legacy) shareable config and does not provide a native ESLint v9 flat-config export — if your project runs ESLint in flat mode you will get lint failures; either enable eslintrc compatibility (ESLINT_USE_FLAT_CONFIG=false or use LegacyESLint) or migrate the config (e.g., @eslint/migrate-config) / replace with a flat-native config.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/helpers/creator.helper.ts (1)

22-43: Fix: null-safety and return-type in getCreatorBirthdayInfo

  • el.querySelector and .trim() calls can NPE; add ?..
  • age currently can be null but the return type declares number; also unary + on null yields 0 unexpectedly. Return number | null and drop the coercion.
-export const getCreatorBirthdayInfo = (
-  el: HTMLElement | null
-): { birthday: string; age: number; birthPlace: string } => {
-  const infoBlock = el.querySelector('h1 + p');
-  const text = infoBlock?.innerHTML.trim();
+export const getCreatorBirthdayInfo = (
+  el: HTMLElement | null
+): { birthday: string; age: number | null; birthPlace: string } => {
+  const infoBlock = el?.querySelector('h1 + p');
+  const text = infoBlock?.innerHTML?.trim();
@@
-  const birthPlaceRow = infoBlock?.querySelector('.info-place')?.innerHTML.trim();
-  const ageRow = infoBlock?.querySelector('.info')?.innerHTML.trim();
+  const birthPlaceRow = infoBlock?.querySelector('.info-place')?.innerHTML?.trim();
+  const ageRow = infoBlock?.querySelector('.info')?.innerHTML?.trim();
@@
-  const age = ageRow ? +parseAge(ageRow) : null;
+  const age = ageRow ? parseAge(ageRow) : null;
src/helpers/cinema.helper.ts (1)

27-45: Harden coords parsing; guard undefined href and parse q robustly

linkMapsEl.getAttribute('href') can be null; .split('q=') on null will throw, and the current approach breaks when extra query params are present. Parse via URLSearchParams with a safe fallback.

Apply:

 export const getCinemaCoords = (el: HTMLElement | null): { lat: number; lng: number } | null => {
 
   if (!el) return null;
-  const linkMapsEl = el.querySelector('a[href*="q="]');
-  if (!linkMapsEl) return null;
-
-  const linkMaps = linkMapsEl.getAttribute('href');
-  const [_, latLng] = linkMaps.split('q=');
-
-  const coords = latLng.split(',');
-  if (coords.length !== 2) return null;
-
-  const lat = Number(coords[0]);
-  const lng = Number(coords[1]);
-  if (Number.isFinite(lat) && Number.isFinite(lng)) {
-    return { lat, lng };
-  }
-  return null;
+  const a = el.querySelector('a[href*="q="]');
+  if (!a) return null;
+
+  const href = a.getAttribute('href') || '';
+
+  // Try robust parsing first
+  let q: string | null = null;
+  try {
+    const u = new URL(href, 'https://example.com');
+    q = u.searchParams.get('q');
+  } catch {
+    // ignore, fallback below
+  }
+  // Fallback: split manually and strip trailing params/fragments
+  if (!q) {
+    const afterQ = href.split('q=')[1];
+    q = afterQ ? afterQ.split(/[&#?]/)[0] : null;
+  }
+  if (!q) return null;
+
+  const [latStr, lngStr] = q.split(',', 2);
+  const lat = Number(latStr);
+  const lng = Number(lngStr);
+  return Number.isFinite(lat) && Number.isFinite(lng) ? { lat, lng } : null;
 };
🧹 Nitpick comments (2)
src/helpers/creator.helper.ts (1)

67-90: Minor robustness: film list traversal

  • Prefer getAttribute('href') over accessing attributes.href directly.
  • If querySelectorAll returns a NodeList-like in some versions, consider Array.from to ensure .map is available.
-export const getCreatorFilms = (el: HTMLElement | null): CSFDCreatorScreening[] => {
-  const filmNodes = el?.querySelectorAll('.box')?.[0]?.querySelectorAll('table tr') ?? [];
+export const getCreatorFilms = (el: HTMLElement | null): CSFDCreatorScreening[] => {
+  const filmNodes = el?.querySelectorAll('.box')?.[0]?.querySelectorAll('table tr') ?? [];
@@
-  const films = filmNodes.map((filmNode) => {
-    const id = getCreatorId(filmNode.querySelector('td.name .film-title-name')?.attributes?.href);
+  const films = (Array.from(filmNodes) as HTMLElement[]).map((filmNode) => {
+    const link = filmNode.querySelector('td.name .film-title-name');
+    const id = getCreatorId(link?.getAttribute('href') ?? undefined);
src/helpers/cinema.helper.ts (1)

66-66: Remove unused date param and update call

getCinemaFilms doesn’t use date. Drop it and simplify the call. Also default dateText to a string to satisfy typing.

Apply:

-      return { date: dateText, films: getCinemaFilms('', films) };
+      return { date: dateText ?? '', films: getCinemaFilms(films) };

And change the signature:

-export const getCinemaFilms = (date: string, el: HTMLElement | null): CSFDCinemaMovie[] => {
+export const getCinemaFilms = (el: HTMLElement | null): CSFDCinemaMovie[] => {

Also applies to: 72-72

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd5c6ab and 27c1bec.

📒 Files selected for processing (4)
  • src/helpers/cinema.helper.ts (2 hunks)
  • src/helpers/creator.helper.ts (2 hunks)
  • src/helpers/search.helper.ts (1 hunks)
  • tsconfig.esm.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tsconfig.esm.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/helpers/search.helper.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/helpers/cinema.helper.ts (4)
src/dto/global.ts (1)
  • CSFDColorRating (21-21)
src/helpers/global.helper.ts (2)
  • parseColor (29-42)
  • parseIdFromUrl (4-12)
src/dto/user-ratings.ts (1)
  • Colors (21-21)
src/dto/cinema.ts (1)
  • CSFDCinemaMovie (18-21)
src/helpers/creator.helper.ts (4)
src/dto/global.ts (1)
  • CSFDColorRating (21-21)
src/dto/user-ratings.ts (1)
  • Colors (21-21)
src/helpers/global.helper.ts (3)
  • parseColor (29-42)
  • parseIdFromUrl (4-12)
  • addProtocol (44-46)
src/dto/creator.ts (1)
  • CSFDCreatorScreening (14-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (9)
src/helpers/creator.helper.ts (6)

2-4: DTO imports look good

Aligned with the new DTO locations and types.


13-15: Type consistency with parseIdFromUrl

This signature is correct here, but parseIdFromUrl (global.helper.ts) returns null per snippet while typed as number. Update that function’s return type to number | null to avoid downstream casts and mismatches.

Outside this file, adjust:

- export const parseIdFromUrl = (url: string): number => {
+ export const parseIdFromUrl = (url: string): number | null => {

17-20: Safe name extraction

Optional chaining and nullable return are appropriate here.


51-54: LGTM: photo extraction guarded

Null-safe and uses addProtocol correctly.


56-65: LGTM: tightened parse helpers

Concrete return types and safer parsing are good improvements.


7-10: Fix: null-safety and classNames handling in getCreatorColorRating

el?.classNames.split(' ') will throw (classNames is already an array in node-html-parser), and parseColor(last) can get undefined. Add a safe fallback.

-const getCreatorColorRating = (el: HTMLElement | null): CSFDColorRating => {
-  const classes: string[] = el?.classNames.split(' ') ?? [];
-  const last = classes[classes.length - 1] as Colors | undefined;
-  return parseColor(last);
-};
+const getCreatorColorRating = (el: HTMLElement | null): CSFDColorRating => {
+  const classes = el?.classNames ?? [];
+  const last = classes[classes.length - 1] as Colors | undefined;
+  return parseColor(last ?? 'lightgrey');
+};
src/helpers/cinema.helper.ts (3)

6-9: LGTM: DTO/domain imports

Imports align with the DTO split and color/type usage.


77-90: Don’t assign null to required id; filter out invalid entries

CSFDMovieListItem.id is a number. Assigning null breaks typing; skip entries without a valid URL/ID instead.

Apply:

-  const films = filmNodes.map((filmNode) => {
-    const url = filmNode.querySelector('td.name h3 a')?.attributes.href;
-    const id = url ? getCinemaUrlId(url) : null;
-    const title = filmNode.querySelector('.name h3')?.text.trim();
-    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());
-
-    return {
-      id,
-      title,
-      url,
-      colorRating,
-      showTimes,
-      meta: parseMeta(meta)
-    };
-  });
+  const films = filmNodes
+    .map((filmNode) => {
+      const url = filmNode.querySelector('td.name h3 a')?.attributes.href;
+      const id = url ? getCinemaUrlId(url) : null;
+      if (id == null) return null;
+      const title = filmNode.querySelector('.name h3')?.text.trim() ?? '';
+      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());
+      return {
+        id,
+        title,
+        url: url!,
+        colorRating,
+        showTimes,
+        meta: parseMeta(meta)
+      };
+    })
+    .filter(Boolean) as CSFDCinemaMovie[];

11-14: Fix: null‑safe class handling; remove invalid .split on string[]

HTMLElement.classNames is string[]; current code calls .split on it and will also throw when el is null.

Apply:

-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 getCinemaColorRating = (el: HTMLElement | null): CSFDColorRating => {
+  const classes = el?.classNames ?? [];
+  const last = classes.length ? classes[classes.length - 1] : undefined;
+  return last ? parseColor(last as Colors) : 'unknown';
+};

Comment on lines +22 to 25
export const getCinemaUrlId = (url: string | null | undefined): number | null => {
if (!url) return null;
return parseIdFromUrl(url);
};
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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
tests/user-ratings.test.ts (3)

46-75: Prefer skipped tests over commented-out tests.

Use test.skip to keep intent visible and tracked in CI.

-  // test('TV film', () => {
-  //   const movie = getUserRatingType(movies[18]);
-  //   expect(movie).toEqual<CSFDFilmTypes>('TV film');
-  // });
+  test.skip('TV film', () => {
+    const movie = getUserRatingType(movies[18]);
+    expect(movie).toEqual<CSFDFilmTypes>('TV film');
+  });
-  // test('Pořad', () => {
-  //   const movie = getUserRatingType(movies[6]);
-  //   expect(movie).toEqual<CSFDFilmTypes>('pořad');
-  // });
+  test.skip('Pořad', () => {
+    const movie = getUserRatingType(movies[6]);
+    expect(movie).toEqual<CSFDFilmTypes>('pořad');
+  });

90-100: Consider removing generics from toEqual or asserting types with expectTypeOf.

The generics on toEqual add noise and may not be enforced. Prefer expect(movie).toEqual(2022) and type checks via expectTypeOf(movie).toEqualTypeOf<number>() if needed.

Example:

-    expect(movie).toEqual<number>(2022);
+    expect(movie).toEqual(2022);

103-191: Color class detection: add a case where the color class is not the last class (or refactor helper).

Current implementation in getUserRatingColorRating appears to rely on the last CSS class; real markup can reorder classes. Add a test (skipped until helper is updated) or refactor the helper to scan all classes for a known color.

Add this test:

+  test.skip('Valid color not last should still be recognized', () => {
+    // Color class appears before other classes
+    const mockElement = parse(`
+      <tr>
+        <td class="name">
+          <span class="icon red some-other-class another-one"></span>
+        </td>
+      </tr>
+    `);
+    const result = getUserRatingColorRating(mockElement);
+    expect(result).toEqual<CSFDColorRating>('good');
+  });

Optionally, deduplicate mock creation with a tiny helper in this file:

function buildRowWithIcon(classAttr: string): HTMLElement {
  return parse(`<tr><td class="name"><span class="icon ${classAttr}"></span></td></tr>`);
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b21a714 and e33a989.

📒 Files selected for processing (2)
  • tests/cinema.helper.test.ts (5 hunks)
  • tests/user-ratings.test.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/cinema.helper.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
tests/user-ratings.test.ts (2)
src/helpers/user-ratings.helper.ts (7)
  • getUserRatingId (6-9)
  • getUserRatingType (18-22)
  • getUserRatingTitle (24-26)
  • getUserRatingYear (28-30)
  • getUserRatingColorRating (32-35)
  • getUserRatingDate (37-39)
  • getUserRatingUrl (41-44)
src/dto/global.ts (2)
  • CSFDFilmTypes (25-36)
  • CSFDColorRating (21-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (6)
tests/user-ratings.test.ts (6)

6-13: Renamed helper imports align with the new API surface.

Looks consistent with the helper refactor.


35-44: ID parsing tests look good.

Covers first and last rows; assertions are clear.


79-85: Title tests are precise and readable.


193-201: Date parsing tests are clear and stable.


204-212: URL tests look correct and assert the absolute URL.


3-3: Use type-only import for DTO types

CSFDColorRating, CSFDFilmTypes and CSFDStars are exported as types from src/dto/global.ts — switch to a type-only import.

File: tests/user-ratings.test.ts

Apply this diff:

-import { CSFDColorRating, CSFDFilmTypes, CSFDStars } from '../src/dto/global';
+import type { CSFDColorRating, CSFDFilmTypes, CSFDStars } from '../src/dto/global';

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants