-
-
Notifications
You must be signed in to change notification settings - Fork 10
TV series, seasons and episodes #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
fafd5d6 to
f6c21cc
Compare
|
Here's the code health analysis summary for commits Analysis Summary
|
📝 WalkthroughWalkthroughAdds DTO fields for series/season/episode metadata, a URL-id parser, new DOM-parsing helpers to detect/extract seasons, episodes, episode codes and parent info, integrates them into the movie service to populate new fields, updates tests and large HTML mocks, and tweaks demo invocation. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant MovieService as MovieService
participant MovieHelpers as MovieHelpers
participant GlobalHelper as GlobalHelper
participant DOM as HTMLElement
Client->>MovieService: buildMovie(el: HTMLElement)
MovieService->>MovieHelpers: getMovieType(el)
MovieHelpers-->>MovieService: type
alt type == "série"
MovieService->>MovieHelpers: getSerieasAndSeasonTitle(el)
MovieHelpers-->>MovieService: {seriesName, seasonName}
MovieService->>MovieHelpers: detectSeasonOrEpisodeListType(el)
MovieHelpers-->>MovieService: listType
alt listType == "seasons" or "episodes"
MovieService->>MovieHelpers: getSeasonsOrEpisodes(el, serie?)
MovieHelpers->>GlobalHelper: parseLastIdFromUrl(url)
GlobalHelper-->>MovieHelpers: id
MovieHelpers-->>MovieService: CSFDSeason[]
MovieService->>MovieService: set seasons/episodes
end
MovieService->>MovieHelpers: getSeasonorEpisodeParent(el, serie?)
MovieHelpers-->>MovieService: CSFDParent | null
MovieService->>MovieService: set parent
end
alt type == "epizoda"
MovieService->>MovieHelpers: getEpisodeCode(el)
MovieHelpers-->>MovieService: episodeCode
MovieService->>MovieService: set episodeCode
end
MovieService-->>Client: CSFDMovie (includes new fields)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #60 +/- ##
==========================================
- Coverage 99.52% 99.43% -0.10%
==========================================
Files 28 33 +5
Lines 630 705 +75
Branches 143 175 +32
==========================================
+ Hits 627 701 +74
- Misses 3 4 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
f6c21cc to
f884613
Compare
f884613 to
e9326cd
Compare
11636b4 to
a45258d
Compare
e9326cd to
da3c2fe
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🧹 Nitpick comments (3)
src/dto/movie.ts (1)
166-171: Clarify episode/season child type or keep CSFDSeason strict.If you intend to reuse this for both seasons and episodes, consider:
- Keep CSFDSeason strict (non-null fields) and ensure the parser filters out incomplete items (see helper fix).
- Or add an alias for readability:
export type CSFDEpisode = CSFDSeason;Confirm whether URLs stored here should be absolute; current helpers return relative links.
demo.ts (1)
11-11: Commented creator example is OK.Consider leaving a short note why it’s disabled.
src/services/movie.service.ts (1)
6-8: Rename helper: getSerieasAndSeasonTitle → getSeriesAndSeasonTitle.Fix the spelling in import and usage for clarity and future discoverability.
Apply:
- getSerieasAndSeasonTitle + getSeriesAndSeasonTitleand
-const { seriesName = null, seasonName = null } = type === 'série' ? getSerieasAndSeasonTitle(el) : {}; +const { seriesName = null, seasonName = null } = type === 'série' ? getSeriesAndSeasonTitle(el) : {};Also applies to: 26-30, 61-66
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
demo.ts(1 hunks)src/dto/movie.ts(3 hunks)src/helpers/global.helper.ts(1 hunks)src/helpers/movie.helper.ts(2 hunks)src/services/movie.service.ts(4 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/services/movie.service.ts (2)
src/helpers/movie.helper.ts (12)
getSerieasAndSeasonTitle(23-39)detectSeasonOrEpisodeListType(231-237)getMovieTitle(41-43)getMovieYear(82-90)getMovieDescriptions(178-182)getMovieGroup(261-269)getMovieVods(276-289)getMovieTags(334-337)getMovieBoxMovies(298-312)getSeasonsOrEpisodes(200-218)getSeasonorEpisodeParent(239-259)getEpisodeCode(220-229)src/dto/global.ts (1)
CSFDFilmTypes(25-36)
demo.ts (2)
src/index.ts (2)
csfd(51-57)movie(28-30)src/services/movie.service.ts (1)
movie(36-52)
src/helpers/movie.helper.ts (2)
src/helpers/global.helper.ts (2)
parseIdFromUrl(4-12)parseLastIdFromUrl(14-22)src/dto/movie.ts (2)
CSFDSeason(166-171)CSFDParent(27-30)
⏰ 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 (4)
src/dto/movie.ts (1)
20-25: New CSFDMovie fields look good; ensure DTOs match emitted data.The additions are coherent. After fixing helper nullability (see other comments), this shape should be stable.
Please confirm tests cover population of: seasons, episodes, parent, episodeCode, seasonName.
demo.ts (1)
5-5: Demo change is fine.No issues; keep demo noise out of CI runs if applicable.
src/helpers/movie.helper.ts (2)
220-229: Episode code extraction LGTM; consider unit test.Regex-based extraction is fine. Add a simple test to lock behavior with/without parentheses.
231-237: List type detection LGTM; add fallback test.Heuristic is simple and readable. Add tests for pages lacking the header to ensure null is returned.
src/helpers/movie.helper.ts
Outdated
| export const getSerieasAndSeasonTitle = (el: HTMLElement): { seriesName: string; seasonName: string | null } => { | ||
| const titleElement = el.querySelector('h1'); | ||
| if (!titleElement) { | ||
| return { seriesName: null, seasonName: null }; | ||
| } | ||
|
|
||
| const fullText = titleElement.innerText.trim(); | ||
|
|
||
| // Check if there's a series part indicated by ' - ' | ||
| if (fullText.includes(' - ')) { | ||
| const [seriesName, seasonName] = fullText.split(' - ').map(part => part.trim()); | ||
| return { seriesName, seasonName }; | ||
| } | ||
|
|
||
| // If no series part found, return just the name | ||
| return { seriesName: fullText, seasonName: null }; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Fix name and nullability: getSeriesAndSeasonTitle.
- Typo in exported name.
- Return type must allow nulls when
is missing.
Apply:
-export const getSerieasAndSeasonTitle = (el: HTMLElement): { seriesName: string; seasonName: string | null } => {
+export const getSeriesAndSeasonTitle = (
+ el: HTMLElement
+): { seriesName: string | null; seasonName: string | null } => {
const titleElement = el.querySelector('h1');
if (!titleElement) {
- return { seriesName: null, seasonName: null };
+ return { seriesName: null, seasonName: null };
}
const fullText = titleElement.innerText.trim();
// Check if there's a series part indicated by ' - '
if (fullText.includes(' - ')) {
const [seriesName, seasonName] = fullText.split(' - ').map(part => part.trim());
return { seriesName, seasonName };
}
// If no series part found, return just the name
- return { seriesName: fullText, seasonName: null };
+ return { seriesName: fullText, seasonName: null };
};Remember to update all imports/usages (movie.service.ts).
🏁 Script executed:
#!/bin/bash
rg -n "getSerieasAndSeasonTitle|getSeriesAndSeasonTitle" --type=tsLength of output: 344
🏁 Script executed:
#!/bin/bash
# Inspect the import and usage context in movie.service.ts
sed -n '25,35p' src/services/movie.service.ts
echo "---"
sed -n '58,68p' src/services/movie.service.tsLength of output: 657
Fix name and nullability: getSeriesAndSeasonTitle.
- Typo in exported name:
getSerieasAndSeasonTitle→getSeriesAndSeasonTitle - Return type must allow nulls for
seriesName(code returns null when<h1>is missing, but type declaresstring)
Apply:
-export const getSerieasAndSeasonTitle = (el: HTMLElement): { seriesName: string; seasonName: string | null } => {
+export const getSeriesAndSeasonTitle = (
+ el: HTMLElement
+): { seriesName: string | null; seasonName: string | null } => {
const titleElement = el.querySelector('h1');
if (!titleElement) {
return { seriesName: null, seasonName: null };
}
const fullText = titleElement.innerText.trim();
// Check if there's a series part indicated by ' - '
if (fullText.includes(' - ')) {
const [seriesName, seasonName] = fullText.split(' - ').map(part => part.trim());
return { seriesName, seasonName };
}
// If no series part found, return just the name
return { seriesName: fullText, seasonName: null };
};Update import in src/services/movie.service.ts line 29.
📝 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.
| export const getSerieasAndSeasonTitle = (el: HTMLElement): { seriesName: string; seasonName: string | null } => { | |
| const titleElement = el.querySelector('h1'); | |
| if (!titleElement) { | |
| return { seriesName: null, seasonName: null }; | |
| } | |
| const fullText = titleElement.innerText.trim(); | |
| // Check if there's a series part indicated by ' - ' | |
| if (fullText.includes(' - ')) { | |
| const [seriesName, seasonName] = fullText.split(' - ').map(part => part.trim()); | |
| return { seriesName, seasonName }; | |
| } | |
| // If no series part found, return just the name | |
| return { seriesName: fullText, seasonName: null }; | |
| }; | |
| export const getSeriesAndSeasonTitle = ( | |
| el: HTMLElement | |
| ): { seriesName: string | null; seasonName: string | null } => { | |
| const titleElement = el.querySelector('h1'); | |
| if (!titleElement) { | |
| return { seriesName: null, seasonName: null }; | |
| } | |
| const fullText = titleElement.innerText.trim(); | |
| // Check if there's a series part indicated by ' - ' | |
| if (fullText.includes(' - ')) { | |
| const [seriesName, seasonName] = fullText.split(' - ').map(part => part.trim()); | |
| return { seriesName, seasonName }; | |
| } | |
| // If no series part found, return just the name | |
| return { seriesName: fullText, seasonName: null }; | |
| }; |
🤖 Prompt for AI Agents
In src/helpers/movie.helper.ts around lines 23 to 39, rename the exported
function from getSerieasAndSeasonTitle to getSeriesAndSeasonTitle and change the
return type to { seriesName: string | null; seasonName: string | null } so the
function signature matches the code path that returns null when the <h1> is
missing; update any internal references accordingly. Then update the import in
src/services/movie.service.ts at line 29 to import getSeriesAndSeasonTitle
(instead of the misspelled name). Ensure the file exports the corrected
identifier and run a quick search to update any remaining call sites.
src/helpers/movie.helper.ts
Outdated
| export const getSeasonorEpisodeParent = (el: HTMLElement, serie?: { id: number; name: string; }): CSFDParent | null => { | ||
| const parents = el.querySelectorAll('.film-header h2 a'); | ||
| if (parents.length === 0) { | ||
| if (!serie) return null; | ||
| return { series: serie, season: null }; | ||
| } | ||
|
|
||
| const [parentSeries, parentSeason] = parents; | ||
|
|
||
| const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href')); | ||
| const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href')); | ||
| const seriesName = parentSeries?.textContent?.trim() || null; | ||
| const seasonName = parentSeason?.textContent?.trim() || null; | ||
|
|
||
| const series = seriesId && seriesName ? { id: seriesId, name: seriesName } : null; | ||
| const season = seasonId && seasonName ? { id: seasonId, name: seasonName } : null; | ||
|
|
||
| if (!series && !season) return null; | ||
|
|
||
| return { series, season }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Avoid passing undefined to parseIdFromUrl and align with CSFDParent nullability.
- parentSeason may be missing; ensure string fallback for href.
- With CSFDParent updated to allow nulls, this function becomes type-safe.
Apply:
- const [parentSeries, parentSeason] = parents;
+ const [parentSeries, parentSeason] = parents;
- const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href'));
- const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href'));
+ const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href') || '');
+ const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href') || '');
const seriesName = parentSeries?.textContent?.trim() || null;
const seasonName = parentSeason?.textContent?.trim() || null;
const series = seriesId && seriesName ? { id: seriesId, name: seriesName } : null;
const season = seasonId && seasonName ? { id: seasonId, name: seasonName } : null;
if (!series && !season) return null;
return { series, season };Add tests for: 0 parents (uses provided serie), 1 parent (series only), 2 parents (series + season).
🏁 Script executed:
#!/bin/bash
rg -n "getSeasonorEpisodeParent" --type=ts -C2Length of output: 1191
Now let me search the codebase directly for the implementations needed to verify the review:
Let me search the codebase now:Now I need to check the codebase directly using shell scripts:
#!/bin/bash
# Find parseIdFromUrl function definition
echo "=== Searching for parseIdFromUrl definition ==="
rg -n "parseIdFromUrl" --type=ts -A 5 -B 2
echo ""
echo "=== Searching for CSFDParent type definition ==="
rg -n "CSFDParent" --type=ts -A 3 -B 2
echo ""
echo "=== Searching for test files related to getSeasonorEpisodeParent ==="
fd "\.test\.|\.spec\." --type=f | head -20Based on my comprehensive verification, I need to note that:
-
Optional chaining behavior: When a reference is null or undefined, the expression returns undefined instead of causing an error. The code uses
parentSeason?.getAttribute('href'), which will returnundefinedifparentSeasonis null/undefined. -
The concern in the review: The suggestion to add
|| ''assumes thatparseIdFromUrleither:- Doesn't accept
undefinedas a parameter, OR - Doesn't handle it correctly
- Doesn't accept
-
The code pattern: The current code destructures from
parentsarray which could have 0, 1, or 2 elements. When there's only 1 element,parentSeasonwill beundefined.
Without access to the actual codebase files through shell execution, and given the review comment's specific concerns about type safety and the request for tests, I'll provide a rewritten comment based on the general TypeScript best practices evident from the web search results:
Type-safe handling of optional element and improved test coverage needed.
The function currently relies on array destructuring that may assign undefined to parentSeason when fewer than 2 elements exist. This causes getAttribute('href') to return undefined, which is then passed to parseIdFromUrl.
Ensure parseIdFromUrl handles undefined input appropriately, or add explicit fallback:
parentSeason?.getAttribute('href') || ''provides a safer empty string fallback- This aligns with strict null checking requirements
Tests needed for edge cases:
- 0 parents (uses provided
serieparameter) - 1 parent (series only, season is undefined)
- 2 parents (series + season)
#!/bin/bash
echo "=== Searching for parseIdFromUrl definition ==="
rg -n "parseIdFromUrl\s*=" --type=ts -A 10 | head -30
echo ""
echo "=== Searching for CSFDParent type definition ==="
rg -n "type CSFDParent|interface CSFDParent" --type=ts -A 5
echo ""
echo "=== Searching for test files ==="
find . -name "*.test.ts" -o -name "*.spec.ts" | grep -i "movie\|helper"
echo ""
echo "=== Checking if CSFDParent has been updated ==="
rg -n "CSFDParent" --type=ts | grep -v "import\|export"🏁 Script executed:
#!/bin/bash
echo "=== Search for parseIdFromUrl implementation ==="
rg -n "const parseIdFromUrl|function parseIdFromUrl|export.*parseIdFromUrl" --type=ts -A 8
echo ""
echo "=== Search for CSFDParent type definition ==="
rg -n "type CSFDParent|interface CSFDParent" --type=ts -A 6
echo ""
echo "=== Search for test files containing getSeasonorEpisodeParent ==="
rg -n "getSeasonorEpisodeParent" --type=ts -l | grep -E "\.(test|spec)\.ts$"
echo ""
echo "=== Check all files importing or using parseIdFromUrl ==="
rg -n "parseIdFromUrl" --type=ts -B 3 -A 1Length of output: 8474
Add string fallback for optional chaining and expand test coverage.
The issue is valid: parentSeason?.getAttribute('href') returns undefined when parentSeason is null/undefined, and parseIdFromUrl expects a string type parameter (line 4, src/helpers/global.helper.ts). While the function handles falsy values gracefully, TypeScript will flag this as a type error in strict mode.
Apply the suggested fix:
- const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href'));
- const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href'));
+ const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href') || '');
+ const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href') || '');This ensures a string is always passed. Empty string will be falsy in parseIdFromUrl and safely return null.
Also add tests for edge cases: 0 parents (uses provided serie), 1 parent (series only), 2 parents (series + season). No existing tests found for getSeasonorEpisodeParent.
🤖 Prompt for AI Agents
In src/helpers/movie.helper.ts around lines 239 to 259, the calls
parentSeries?.getAttribute('href') and parentSeason?.getAttribute('href') can be
undefined and TypeScript strict mode flags that parseIdFromUrl expects a string;
change those calls to provide a string fallback (e.g.
parentSeries?.getAttribute('href') || '') so parseIdFromUrl always receives a
string and still returns null for falsy input, and add unit tests covering the
three edge cases: 0 parents (passes through provided serie), 1 parent (series
only), and 2 parents (series + season) to validate outputs.
| similar: getMovieBoxMovies(asideEl, 'Podobné'), | ||
| seasons: seasonOrEpisodeListType === 'seasons' ? getSeasonsOrEpisodes(el) : null, | ||
| episodes: seasonOrEpisodeListType === 'episodes' ? getSeasonsOrEpisodes(el) : null, | ||
| parent: (type === 'seriál') ? null : getSeasonorEpisodeParent(el, { id: movieId, name: title }), | ||
| episodeCode: type === 'epizoda' ? getEpisodeCode(el) : null, | ||
| seasonName, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Parent/seasons/episodes population: verify nullability contract and absolute URLs.
- After adjusting CSFDParent to allow nullable members, this assignment is fine.
- Ensure getSeasonsOrEpisodes returns items with non-null id/name/url and absolute URLs to avoid downstream consumers breaking.
To verify field presence:
🏁 Script executed:
#!/bin/bash
rg -nC2 "seasons:|episodes:|parent:|episodeCode:|seasonName" src --type=tsLength of output: 3403
---
🏁 Script executed:
```shell
#!/bin/bash
# Find CSFDParent type definition and getSeasonsOrEpisodes return type
rg -nC5 "type CSFDParent|interface CSFDParent|getSeasonsOrEpisodes.*return|export.*getSeasonsOrEpisodes" src --type=ts
Length of output: 1168
🏁 Script executed:
#!/bin/bash
# Find CSFDSeason type definition and related types
rg -nC3 "type CSFDSeason|interface CSFDSeason" src --type=tsLength of output: 279
🏁 Script executed:
#!/bin/bash
# Search for getSeasonorEpisodeParent implementation details
rg -nC8 "getSeasonorEpisodeParent\s*=" src --type=tsLength of output: 1162
🏁 Script executed:
#!/bin/bash
# Search for getSeasonsOrEpisodes implementation
rg -nC8 "getSeasonsOrEpisodes\s*=" src --type=tsLength of output: 1169
---
🏁 Script executed:
```shell
#!/bin/bash
# Get complete getSeasonorEpisodeParent implementation
rg -nA20 "export const getSeasonorEpisodeParent" src/helpers/movie.helper.ts
Length of output: 934
🏁 Script executed:
#!/bin/bash
# Get complete getSeasonsOrEpisodes implementation including URL handling
rg -nA25 "export const getSeasonsOrEpisodes" src/helpers/movie.helper.tsLength of output: 1166
🏁 Script executed:
#!/bin/bash
# Check for any recent changes to CSFDParent or related code
cat src/dto/movie.ts | head -40Length of output: 919
Critical type contract violations in parent/seasons/episodes population require fixes.
The original review comment identified valid concerns that remain unaddressed:
-
CSFDParent type contract violated: The
getSeasonorEpisodeParentfunction (line 243) returns{ series: serie, season: null }, and line 258 can return objects where bothseasonandseriesare null, but the CSFDParent interface requires both as non-nullable{ id: number; name: string; }objects. -
getSeasonsOrEpisodes returns nullable fields: The function (lines 213-214) assigns null to
nameandurlfields via the|| nullfallback, violating the CSFDSeason interface contract which requires both fields as non-nullable strings. -
URLs are relative, not absolute: Line 214 returns
getAttribute('href')which produces relative URLs (e.g.,/film/...), not absolute URLs (e.g.,https://www.csfd.cz/film/...), causing downstream consumers to fail.
Requires action:
- Make CSFDParent.season and CSFDParent.series optional/nullable in the type definition
- Ensure getSeasonsOrEpisodes provides non-null name/url or handle null cases
- Prepend
https://www.csfd.czto relative URLs in getSeasonsOrEpisodes and getSeasonorEpisodeParent
🤖 Prompt for AI Agents
In src/services/movie.service.ts around lines 99-105, the
parent/seasons/episodes population violates the TypeScript contracts and returns
relative/nullable fields; update the CSFDParent type so season and series are
optional or nullable, modify getSeasonsOrEpisodes to never return null for
name/url (e.g., coerce empty strings or filter out items with missing values)
and ensure you build absolute URLs by prepending "https://www.csfd.cz" to any
relative href returned by getAttribute('href'), and update
getSeasonorEpisodeParent to return values that match the updated CSFDParent
shape (use nullable/optional fields and construct absolute URLs for any hrefs it
returns).
da3c2fe to
2039636
Compare
There was a problem hiding this 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
♻️ Duplicate comments (4)
src/helpers/movie.helper.ts (3)
23-39: Fix typo and nullability in getSeriesAndSeasonTitle; update imports/usages.Rename function and allow
seriesNameto be null when<h1>is missing.Apply:
-export const getSerieasAndSeasonTitle = (el: HTMLElement): { seriesName: string; seasonName: string | null } => { +export const getSeriesAndSeasonTitle = ( + el: HTMLElement +): { seriesName: string | null; seasonName: string | null } => { const titleElement = el.querySelector('h1'); if (!titleElement) { return { seriesName: null, seasonName: null }; } const fullText = titleElement.innerText.trim(); // Check if there's a series part indicated by ' - ' if (fullText.includes(' - ')) { const [seriesName, seasonName] = fullText.split(' - ').map(part => part.trim()); return { seriesName, seasonName }; } // If no series part found, return just the name return { seriesName: fullText, seasonName: null }; };Remember to update all imports/usages in
src/services/movie.service.ts.
200-218: Ensure non-null id/name/url and build absolute URLs in getSeasonsOrEpisodes.Current mapper can emit nulls and relative URLs; downstream consumers will break.
Apply:
-export const getSeasonsOrEpisodes = (el: HTMLElement, serie?: { id: number; title: string; }): CSFDSeason[] | null => { +export const getSeasonsOrEpisodes = ( + el: HTMLElement, + _serie?: { id: number; title: string } +): CSFDSeason[] | null => { const childrenList = el.querySelector('.film-episodes-list'); if (!childrenList) return null; - const childrenNodes = childrenList.querySelectorAll('.film-title'); - if (!childrenNodes?.length) return []; - - return childrenNodes.map((season) => { - const nameContainer = season.querySelector('.film-title-name'); - const infoContainer = season.querySelector('.info'); - - return { - id: parseLastIdFromUrl(nameContainer?.getAttribute('href') || ''), - name: nameContainer?.textContent?.trim() || null, - url: nameContainer?.getAttribute('href') || null, - info: infoContainer?.textContent?.replace(/[{()}]/g, '').trim() || null, - }; - }); + const childrenNodes = childrenList.querySelectorAll('.film-title'); + if (!childrenNodes?.length) return []; + + const items: CSFDSeason[] = []; + for (const node of childrenNodes) { + const nameEl = node.querySelector('.film-title-name'); + const infoEl = node.querySelector('.info'); + const href = nameEl?.getAttribute('href') || ''; + const id = parseLastIdFromUrl(href); + const name = nameEl?.textContent?.trim(); + if (!id || !name || !href) continue; + items.push({ + id, + name, + url: `https://www.csfd.cz${href}`, + info: infoEl?.textContent?.replace(/[{()}]/g, '').trim() || null, + }); + } + return items; }Note:
_serieis currently unused; either remove the param or pass it from the caller for future enrichment.
239-259: Do not pass undefined to parseIdFromUrl; confirm CSFDParent nullability. Also, consider renaming to getSeasonOrEpisodeParent.Provide string fallbacks for hrefs; result remains unchanged functionally. If
CSFDParentpermits nullable members, this is type-safe; otherwise, update the DTO.Apply:
const [parentSeries, parentSeason] = parents; - const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href')); - const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href')); + const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href') || ''); + const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href') || ''); const seriesName = parentSeries?.textContent?.trim() || null; const seasonName = parentSeason?.textContent?.trim() || null; const series = seriesId && seriesName ? { id: seriesId, name: seriesName } : null; const season = seasonId && seasonName ? { id: seasonId, name: seasonName } : null; if (!series && !season) return null; return { series, season };Optional: rename the function to
getSeasonOrEpisodeParentfor readability and update imports.src/services/movie.service.ts (1)
6-8: Rename import and usage to getSeriesAndSeasonTitle; avoid unsafe type assertion.Fix the helper name everywhere; consider avoiding
as CSFDFilmTypesifgetMovieTypemay return other values.Apply:
import { detectSeasonOrEpisodeListType, getEpisodeCode, @@ - getSeasonorEpisodeParent, - getSeasonsOrEpisodes, - getSerieasAndSeasonTitle + getSeasonorEpisodeParent, + getSeasonsOrEpisodes, + getSeriesAndSeasonTitle } from '../helpers/movie.helper'; @@ - const type = getMovieType(el) as CSFDFilmTypes; - const { seriesName = null, seasonName = null } = type === 'série' ? getSerieasAndSeasonTitle(el) : {}; + const type = getMovieType(el) as CSFDFilmTypes; + const { seriesName = null, seasonName = null } = type === 'série' ? getSeriesAndSeasonTitle(el) : {};Optional: instead of an assertion, narrow
typeby comparing the extracted text to your union of known values.Also applies to: 27-30, 61-66
🧹 Nitpick comments (3)
demo.ts (1)
5-5: Use an ID that showcases series/seasons to exercise new fields.Align the demo with the PR description (1431651) or another known series/season/episode ID so the new properties are visible when running the demo.
Apply:
-csfd.movie(621073).then((movie) => console.log(movie)); +csfd.movie(1431651).then((movie) => console.log(movie));src/helpers/movie.helper.ts (2)
220-229: Avoid capturing the year as episode code.The parentheses match can grab
(2018); exclude years.Apply:
- const match = text.match(/\(([^)]+)\)/); + const match = text.match(/\((?!(?:19|20)\d{2}\b)([^)]+)\)/);Add tests for typical formats:
S01E03,1/6,#1.1, and year-only titles.
231-237: Make list-type detection robust to case/spacing.Normalize text to lower-case; keep logic unchanged.
Apply:
- const headerText = el.querySelector('.box-header h3')?.innerText.trim() ?? ''; + const headerText = el.querySelector('.box-header h3')?.innerText?.trim().toLowerCase() ?? ''; - if (headerText.includes('Série')) return 'seasons'; - if (headerText.startsWith('Epizody')) return 'episodes'; + if (headerText.includes('série')) return 'seasons'; + if (headerText.startsWith('epizody')) return 'episodes';
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
demo.ts(1 hunks)src/dto/movie.ts(3 hunks)src/helpers/global.helper.ts(1 hunks)src/helpers/movie.helper.ts(2 hunks)src/services/movie.service.ts(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/dto/movie.ts
- src/helpers/global.helper.ts
🔇 Additional comments (3)
demo.ts (1)
11-11: Commenting out creator call is fine for a focused demo.Keeps output concise while testing series/seasons/episodes.
src/services/movie.service.ts (2)
102-105: Parent and seasonName wiring look good given helper fixes.Once the helper naming/URL/nullability fixes land, these fields should resolve correctly.
Ensure
CSFDParentallows nullableseries/seasonas returned bygetSeasonorEpisodeParent.
100-105: Episodes assignment likely mismatched with DTO type; pass series context and ensure mapper type aligns.
getSeasonsOrEpisodesreturnsCSFDSeason[], butepisodestypically expectsCSFDEpisode[]. Either make a dedicated episodes mapper or unify the item type. Also, pass the series context if you intend to use it.Apply:
- seasons: seasonOrEpisodeListType === 'seasons' ? getSeasonsOrEpisodes(el) : null, - episodes: seasonOrEpisodeListType === 'episodes' ? getSeasonsOrEpisodes(el) : null, + // provide context; adjust mapper/DTO if episodes use a distinct type + seasons: seasonOrEpisodeListType === 'seasons' ? getSeasonsOrEpisodes(el, { id: movieId, title }) : null, + episodes: seasonOrEpisodeListType === 'episodes' ? getSeasonsOrEpisodes(el, { id: movieId, title }) : null,Run to verify DTO expectations:
2039636 to
8ed4dae
Compare
There was a problem hiding this 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
♻️ Duplicate comments (1)
src/dto/movie.ts (1)
27-30: CSFDParent fields must be nullable.getSeasonorEpisodeParent can produce missing season/series; the current non-null types are incorrect.
Apply:
-export interface CSFDParent { - season: { id: number; name: string; }; - series: { id: number; name: string; }; -} +/** + * Parent relationship for seasons/episodes. + * At least one of season/series is non-null at runtime. + */ +export interface CSFDParent { + season: { id: number; name: string } | null; + series: { id: number; name: string } | null; +}
🧹 Nitpick comments (2)
src/dto/movie.ts (1)
20-25: Episodes typed as CSFDSeason[]: define a dedicated CSFDEpisode and use it.Episodes are not seasons; align the public API with the PR description and future parsing needs.
Apply:
export interface CSFDMovie extends CSFDScreening { @@ - episodes: CSFDSeason[] | null; + episodes: CSFDEpisode[] | null; @@ } +/** + * Episode metadata parsed from CSFD. + */ +export interface CSFDEpisode { + id: number; + name: string; + url: string; // absolute + info: string | null; + episodeCode?: string | null; +}src/services/movie.service.ts (1)
103-104: episodeCode/seasonName population: OK, but ensure DTO alignment.These fields match the DTO. Once CSFDEpisode is introduced, consider moving episodeCode into the per‑episode shape to avoid top-level sparsity.
I can open a follow‑up PR to adjust types and helper returns once you confirm the desired shapes.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
demo.ts(1 hunks)src/dto/movie.ts(3 hunks)src/helpers/global.helper.ts(1 hunks)src/helpers/movie.helper.ts(2 hunks)src/services/movie.service.ts(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/helpers/global.helper.ts
- demo.ts
- src/helpers/movie.helper.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/services/movie.service.ts (2)
src/helpers/movie.helper.ts (8)
getMovieType(271-274)getSerieasAndSeasonTitle(23-39)detectSeasonOrEpisodeListType(231-237)getMovieTitle(41-43)getMovieYear(82-90)getSeasonsOrEpisodes(200-218)getSeasonorEpisodeParent(239-259)getEpisodeCode(220-229)src/dto/global.ts (1)
CSFDFilmTypes(25-36)
⏰ 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)
src/services/movie.service.ts (2)
65-69: Title resolution looks good.Preferring seriesName for 'série' pages is appropriate.
61-66: Fix helper function name typo; 'série' and 'seriál' are distinct types.
getSerieasAndSeasonTitleis misspelled (should begetSeriesAndSeasonTitle). However, 'série' and 'seriál' are separate valid types in CSFDFilmTypes and are intentionally used in different contexts—no normalization needed.
| parent: (type === 'seriál') ? null : getSeasonorEpisodeParent(el, { id: movieId, name: title }), | ||
| episodeCode: type === 'epizoda' ? getEpisodeCode(el) : null, | ||
| seasonName, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parent computation bug: incorrect fallback sets a parent for films.
parent: (type === 'seriál') ? null : getSeasonorEpisodeParent(el, { id: movieId, name: title })
- For non‑series films, this passes the current movie as a “series” fallback, yielding a bogus parent.
- Compute parent only for seasons/episodes and avoid passing the fallback unless you truly know the series context.
Apply:
- parent: (type === 'seriál') ? null : getSeasonorEpisodeParent(el, { id: movieId, name: title }),
+ // Only derive parent on episode/season pages; no fallback to current movieId for films
+ parent:
+ (type === 'epizoda' || type === 'série')
+ ? getSeasonorEpisodeParent(el)
+ : null,Optionally, if you must supply a series fallback, pass it only when you have strong evidence of the series context (e.g., header breadcrumbs parsed).
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/services/movie.service.ts around lines 102-105, the parent field
currently computes a fallback parent for non-series films causing incorrect
parent assignments; change the logic to compute parent only when the item is a
season or episode (i.e., when type indicates 'seriál' or 'epizoda' as
appropriate) and do not call getSeasonorEpisodeParent for films. If you must
provide a series fallback, only pass a fallback object when you have explicit
series context (e.g., header/breadcrumbs); otherwise set parent to null for
non-series entries.
8ed4dae to
80336e9
Compare
There was a problem hiding this 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
♻️ Duplicate comments (6)
src/helpers/global.helper.ts (1)
14-22: Type signature and zero-coercion issues persist.The past review correctly identified these critical issues, but they remain unfixed in the current code:
- Type mismatch: Function declares return type
numberbut executesreturn null(lines 18, 20), which will fail understrictNullChecks.- Zero-coercion bug: Pattern
+id || null(line 18) incorrectly coerces the valid ID0tonull, losing data.Apply the previously suggested fix:
-export const parseLastIdFromUrl = (url: string): number => { +export const parseLastIdFromUrl = (url: string): number | null => { if (url) { - const idSlug = url?.split('/')[3]; - const id = idSlug?.split('-')[0]; - return +id || null; + const idSlug = url.split('/')[3]; + const num = Number((idSlug ?? '').split('-')[0]); + return Number.isFinite(num) ? num : null; } else { return null; } };Also apply the same pattern to
parseIdFromUrl(lines 4-12) for consistency.src/dto/movie.ts (2)
27-30: Make CSFDParent members nullable to match implementation.The implementation
getSeasonorEpisodeParent(lines 243, 253-254 inmovie.helper.ts) explicitly returnsnullfor eitherseasonorseries, but this interface declares both as non-null. This creates a type contract violation.Apply the previously suggested fix:
export interface CSFDParent { - season: { id: number; name: string; }; - series: { id: number; name: string; }; + season: { id: number; name: string } | null; + series: { id: number; name: string } | null; }
166-171: Fix type contract: CSFDSeason fields vs getSeasonsOrEpisodes implementation.The
CSFDSeasoninterface declaresnameandurlas non-null strings, butgetSeasonsOrEpisodes(lines 213-214 inmovie.helper.ts) explicitly returns them asstring | nullvia|| nullfallbacks. This type mismatch will cause issues.Either widen the DTO to match reality:
export interface CSFDSeason { id: number; - name: string; - url: string; + name: string | null; + url: string | null; info: string | null; }Or fix the helper to filter incomplete nodes and guarantee non-null values (preferred approach, as suggested in the past review).
src/helpers/movie.helper.ts (3)
23-39: Fix function name typo and return type mismatch.Two issues flagged in past review remain:
- Typo:
getSerieasAndSeasonTitleshould begetSeriesAndSeasonTitle(note the extra "a")- Type mismatch: Return type declares
seriesName: stringbut line 26 returns{ seriesName: null, seasonName: null }Apply the previously suggested fix:
-export const getSerieasAndSeasonTitle = (el: HTMLElement): { seriesName: string; seasonName: string | null } => { +export const getSeriesAndSeasonTitle = ( + el: HTMLElement +): { seriesName: string | null; seasonName: string | null } => { const titleElement = el.querySelector('h1'); if (!titleElement) { return { seriesName: null, seasonName: null }; } const fullText = titleElement.innerText.trim(); // Check if there's a series part indicated by ' - ' if (fullText.includes(' - ')) { const [seriesName, seasonName] = fullText.split(' - ').map(part => part.trim()); return { seriesName, seasonName }; } // If no series part found, return just the name return { seriesName: fullText, seasonName: null }; };Remember to update the import in
movie.service.tsline 29.
200-218: Filter invalid entries and build absolute URLs.The past review correctly identified that this function returns nullable
id/name/urlvalues and relative URLs, violating theCSFDSeasoncontract (which declares them non-null). This should be fixed to maintain type safety.Apply a filtering approach to ensure only valid entries are returned:
-export const getSeasonsOrEpisodes = (el: HTMLElement, serie?: { id: number; title: string; }): CSFDSeason[] | null => { +export const getSeasonsOrEpisodes = ( + el: HTMLElement, + _serie?: { id: number; title: string } +): CSFDSeason[] | null => { const childrenList = el.querySelector('.film-episodes-list'); if (!childrenList) return null; const childrenNodes = childrenList.querySelectorAll('.film-title'); if (!childrenNodes?.length) return []; - return childrenNodes.map((season) => { - const nameContainer = season.querySelector('.film-title-name'); - const infoContainer = season.querySelector('.info'); - - return { - id: parseLastIdFromUrl(nameContainer?.getAttribute('href') || ''), - name: nameContainer?.textContent?.trim() || null, - url: nameContainer?.getAttribute('href') || null, - info: infoContainer?.textContent?.replace(/[{()}]/g, '').trim() || null, - }; - }); + const items: CSFDSeason[] = []; + for (const node of childrenNodes) { + const nameEl = node.querySelector('.film-title-name'); + const infoEl = node.querySelector('.info'); + const href = nameEl?.getAttribute('href') || ''; + const id = parseLastIdFromUrl(href); + const name = nameEl?.textContent?.trim(); + if (!id || !name || !href) continue; + items.push({ + id, + name, + url: `https://www.csfd.cz${href}`, + info: infoEl?.textContent?.replace(/[{()}]/g, '').trim() || null, + }); + } + return items; }This ensures
CSFDSeasoncan maintain non-null contracts forid,name, andurl.
239-259: Add string fallback for optional chaining to prevent undefined.The past review correctly identified that
getAttribute('href')returnsundefinedwhen the element doesn't exist (lines 248-249), butparseIdFromUrlexpects astringparameter. TypeScript strict mode will flag this.Apply the previously suggested fix:
const [parentSeries, parentSeason] = parents; - const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href')); - const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href')); + const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href') || ''); + const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href') || ''); const seriesName = parentSeries?.textContent?.trim() || null; const seasonName = parentSeason?.textContent?.trim() || null; const series = seriesId && seriesName ? { id: seriesId, name: seriesName } : null; const season = seasonId && seasonName ? { id: seasonId, name: seasonName } : null; if (!series && !season) return null; return { series, season };This ensures
parseIdFromUrlalways receives a string, and the empty string will correctly returnnullper the function's logic.
🧹 Nitpick comments (1)
src/helpers/movie.helper.ts (1)
231-237: Consider using nullish coalescing for consistency.Minor suggestion: Use
??instead of logical OR for the default value to be more explicit about handling null/undefined.- const headerText = el.querySelector('.box-header h3')?.innerText.trim() ?? ''; + const headerText = el.querySelector('.box-header h3')?.innerText?.trim() ?? '';This ensures
trim()is only called ifinnerTextexists.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
demo.ts(1 hunks)src/dto/movie.ts(3 hunks)src/helpers/global.helper.ts(1 hunks)src/helpers/movie.helper.ts(2 hunks)src/services/movie.service.ts(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- demo.ts
- src/services/movie.service.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/helpers/movie.helper.ts (2)
src/helpers/global.helper.ts (2)
parseIdFromUrl(4-12)parseLastIdFromUrl(14-22)src/dto/movie.ts (2)
CSFDSeason(166-171)CSFDParent(27-30)
⏰ 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 (3)
src/dto/movie.ts (1)
20-25: LGTM: New fields appropriately nullable.The new fields on
CSFDMoviecorrectly declare nullable types (| null), which aligns with the conditional parsing logic inmovie.helper.ts.src/helpers/movie.helper.ts (2)
9-16: LGTM: Imports added correctly.New imports for
CSFDParent,CSFDSeason, andparseLastIdFromUrlare properly structured and necessary for the new functionality.
220-229: LGTM: Episode code extraction logic is sound.The function properly handles missing elements and uses regex to extract the episode code from parentheses. The nullable return type correctly reflects the optional nature of this field.
288840c to
c788bb9
Compare
There was a problem hiding this 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
♻️ Duplicate comments (7)
src/helpers/global.helper.ts (1)
14-22: Type mismatch and 0-coercion bug remain unfixed.The past review flagged these critical issues, but the code has not been updated:
Type signature mismatch: Function declares
numberreturn type but executesreturn null(lines 18, 20). This will fail understrictNullChecks.0-coercion bug: Pattern
+id || null(line 18) incorrectly converts the valid ID0tonull. UseNumber.isFinite()instead.Apply the fix from the previous review:
-export const parseLastIdFromUrl = (url: string): number => { +export const parseLastIdFromUrl = (url: string): number | null => { if (url) { - const idSlug = url?.split('/')[3]; - const id = idSlug?.split('-')[0]; - return +id || null; + const idSlug = url.split('/')[3]; + const num = Number((idSlug ?? '').split('-')[0]); + return Number.isFinite(num) ? num : null; } else { return null; } };Also apply the same fix to
parseIdFromUrl(lines 4-12) for consistency.src/dto/movie.ts (2)
27-30: CSFDParent type contract violation remains unfixed.Past review correctly identified that
getSeasonorEpisodeParent(in movie.helper.ts:243, 258) returns objects whereseasonorseriescan benull, but this interface declares both as non-nullable.Apply the suggested fix:
export interface CSFDParent { - season: { id: number; name: string; }; - series: { id: number; name: string; }; + season: { id: number; name: string } | null; + series: { id: number; name: string } | null; }
166-171: CSFDSeason type contract mismatch remains unfixed.Past review correctly identified that
getSeasonsOrEpisodes(movie.helper.ts:213-214) returnsnameandurlasstring | nullvia|| nullfallbacks, but this interface declares both as non-nullable strings.Either widen the DTO to accept nulls:
export interface CSFDSeason { id: number; - name: string; - url: string; + name: string | null; + url: string | null; info: string | null; }Or fix
getSeasonsOrEpisodesto filter out incomplete nodes and build absolute URLs (preferred approach detailed in the helper file review).src/helpers/movie.helper.ts (3)
23-39: Function name typo and type mismatch remain unfixed.Past review flagged two issues that persist:
- Typo:
getSerieasAndSeasonTitleshould begetSeriesAndSeasonTitle- Type mismatch: Return type declares
seriesName: stringbut line 26 returnsnullApply the fix:
-export const getSerieasAndSeasonTitle = (el: HTMLElement): { seriesName: string; seasonName: string | null } => { +export const getSeriesAndSeasonTitle = ( + el: HTMLElement +): { seriesName: string | null; seasonName: string | null } => { const titleElement = el.querySelector('h1'); if (!titleElement) { return { seriesName: null, seasonName: null }; } const fullText = titleElement.innerText.trim(); // Check if there's a series part indicated by ' - ' if (fullText.includes(' - ')) { const [seriesName, seasonName] = fullText.split(' - ').map(part => part.trim()); return { seriesName, seasonName }; } // If no series part found, return just the name return { seriesName: fullText, seasonName: null }; };Update the import in
src/services/movie.service.tsline 29.
200-218: Type contract violations and relative URLs remain unfixed.Past review flagged critical issues that persist in the current code:
- Lines 213-214 return nullable values (
|| null) violating CSFDSeason contract (requires non-nullnameandurl)- Line 214 returns relative URLs without base domain (e.g.,
/film/...instead ofhttps://www.csfd.cz/film/...)Apply the suggested fix:
-export const getSeasonsOrEpisodes = (el: HTMLElement, serie?: { id: number; title: string; }): CSFDSeason[] | null => { +export const getSeasonsOrEpisodes = ( + el: HTMLElement, + _serie?: { id: number; title: string } +): CSFDSeason[] | null => { const childrenList = el.querySelector('.film-episodes-list'); if (!childrenList) return null; const childrenNodes = childrenList.querySelectorAll('.film-title'); if (!childrenNodes?.length) return []; - return childrenNodes.map((season) => { - const nameContainer = season.querySelector('.film-title-name'); - const infoContainer = season.querySelector('.info'); - - return { - id: parseLastIdFromUrl(nameContainer?.getAttribute('href') || ''), - name: nameContainer?.textContent?.trim() || null, - url: nameContainer?.getAttribute('href') || null, - info: infoContainer?.textContent?.replace(/[{()}]/g, '').trim() || null, - }; - }); + const items: CSFDSeason[] = []; + for (const node of childrenNodes) { + const nameEl = node.querySelector('.film-title-name'); + const infoEl = node.querySelector('.info'); + const href = nameEl?.getAttribute('href') || ''; + const id = parseLastIdFromUrl(href); + const name = nameEl?.textContent?.trim(); + if (!id || !name || !href) continue; + items.push({ + id, + name, + url: `https://www.csfd.cz${href}`, + info: infoEl?.textContent?.replace(/[{()}]/g, '').trim() || null, + }); + } + return items; }
239-259: Type safety issue with undefined passing remains unfixed.Past review correctly identified that
parentSeason?.getAttribute('href')returnsundefinedwhenparentSeasonis null/undefined (e.g., whenparentsarray has only 1 element), butparseIdFromUrlexpects astringparameter.Apply the suggested fix:
const [parentSeries, parentSeason] = parents; - const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href')); - const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href')); + const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href') || ''); + const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href') || ''); const seriesName = parentSeries?.textContent?.trim() || null; const seasonName = parentSeason?.textContent?.trim() || null; const series = seriesId && seriesName ? { id: seriesId, name: seriesName } : null; const season = seasonId && seasonName ? { id: seasonId, name: seasonName } : null; if (!series && !season) return null; return { series, season };Also add test coverage for: 0 parents (uses fallback
serie), 1 parent (series only), 2 parents (series + season).src/services/movie.service.ts (1)
100-105: Multiple critical issues in parent/seasons/episodes population.Three problems persist:
Lines 100-101:
getSeasonsOrEpisodesreturns nullable fields and relative URLs, violating CSFDSeason contract (see movie.helper.ts:200-218).Line 102 parent computation bug: Logic
(type === 'seriál') ? null : getSeasonorEpisodeParent(...)incorrectly assigns a parent to regular films. The intent appears inverted—only episodes/seasons should have parents, not top-level series.Line 102 fallback misuse: Passing
{ id: movieId, name: title }as the series fallback for non-series items creates bogus self-references.Fix the parent computation logic:
- parent: (type === 'seriál') ? null : getSeasonorEpisodeParent(el, { id: movieId, name: title }), + // Only episodes/seasons have parents; films/series do not + parent: + (type === 'epizoda' || type === 'série') + ? getSeasonorEpisodeParent(el) + : null,And ensure
getSeasonsOrEpisodesis fixed per the movie.helper.ts review.
🧹 Nitpick comments (2)
src/dto/movie.ts (1)
20-25: New fields align with TV series support.The added fields (
seasons,episodes,parent,episodeCode,seasonName) appropriately extend the movie model for series/season/episode data. Note:episodescurrently reusesCSFDSeason[]type; consider a dedicatedCSFDEpisodetype in the future for clarity.src/helpers/movie.helper.ts (1)
231-237: Detection logic is straightforward.The string matching approach works for the current CSFD markup. Consider adding test coverage for the Czech string patterns to catch future markup changes.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
demo.ts(1 hunks)src/dto/movie.ts(3 hunks)src/helpers/global.helper.ts(1 hunks)src/helpers/movie.helper.ts(2 hunks)src/services/movie.service.ts(4 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/services/movie.service.ts (2)
src/helpers/movie.helper.ts (8)
getMovieType(271-274)getSerieasAndSeasonTitle(23-39)detectSeasonOrEpisodeListType(231-237)getMovieTitle(41-43)getMovieYear(82-90)getSeasonsOrEpisodes(200-218)getSeasonorEpisodeParent(239-259)getEpisodeCode(220-229)src/dto/global.ts (1)
CSFDFilmTypes(25-36)
demo.ts (2)
src/index.ts (2)
csfd(66-72)movie(39-42)src/services/movie.service.ts (1)
movie(36-52)
src/helpers/movie.helper.ts (2)
src/helpers/global.helper.ts (2)
parseIdFromUrl(4-12)parseLastIdFromUrl(14-22)src/dto/movie.ts (2)
CSFDSeason(166-171)CSFDParent(27-30)
🔇 Additional comments (3)
demo.ts (1)
7-7: LGTM! Demo updates align with new TV series functionality.The movie ID change and commented creator call are appropriate for testing the new season/episode features introduced in this PR.
Also applies to: 13-13
src/helpers/movie.helper.ts (1)
220-229: LGTM! Episode code extraction is correctly implemented.The regex pattern safely extracts content from parentheses, with proper null handling for missing elements.
src/services/movie.service.ts (1)
61-65: Title computation logic depends on unfixed helper issues.Lines 62 and 65 rely on
getSerieasAndSeasonTitle, which has:
- A typo in the function name
- Type mismatch (returns null but declares string for
seriesName)Once the helper is fixed (see review comment on movie.helper.ts:23-39), update the import on line 29 to use the corrected function name.
c788bb9 to
fcce368
Compare
There was a problem hiding this 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
♻️ Duplicate comments (6)
src/dto/movie.ts (1)
20-24: Fix CSFDParent / CSFDSeason type contracts to match helper behavior.
CSFDParentandCSFDSeasoncurrently declare fields as non‑nullable, but the helpers return nulls:
getSeasonorEpisodeParentcan produce{ series: null, season: {…} },{ series: {…}, season: null }, ornull, soseasonandseriesmust allownull.getSeasonsOrEpisodescan emit entries wherenameandurlarenull, andidcan benullfromparseLastIdFromUrl.This violates the published DTO contract and will hurt TS consumers (especially with
strictNullChecks).Recommend:
- Make
CSFDParentmembers nullable:-export interface CSFDParent { - season: { id: number; name: string; }; - series: { id: number; name: string; }; -} +export interface CSFDParent { + season: { id: number; name: string } | null; + series: { id: number; name: string } | null; +}
- Either:
- Keep
CSFDSeasonstrict and fixgetSeasonsOrEpisodesto filter out incomplete nodes and always return non‑nullid/name/url(preferred), or- Widen DTO fields to nullable (
id: number | null; name: string | null; url: string | null;) if you truly want to propagate partial items.Right now the implementation and typings are inconsistent.
Also applies to: 27-30
src/helpers/movie.helper.ts (3)
23-39: Rename and fix nullability ingetSerieasAndSeasonTitle.Issues:
- Exported name has a typo (
getSerieasAndSeasonTitle).- Return type says
seriesName: string, but you returnseriesName: nullwhen<h1>is missing.Recommend:
-export const getSerieasAndSeasonTitle = (el: HTMLElement): { seriesName: string; seasonName: string | null } => { +export const getSeriesAndSeasonTitle = ( + el: HTMLElement +): { seriesName: string | null; seasonName: string | null } => { const titleElement = el.querySelector('h1'); if (!titleElement) { return { seriesName: null, seasonName: null }; } const fullText = titleElement.innerText.trim(); if (fullText.includes(' - ')) { const [seriesName, seasonName] = fullText.split(' - ').map((part) => part.trim()); return { seriesName, seasonName }; } return { seriesName: fullText, seasonName: null }; };Then update the import and call site in
src/services/movie.service.tsto usegetSeriesAndSeasonTitle.
200-218: HardengetSeasonsOrEpisodes: avoid nulls, fix.trim()chain, and normalize URLs.Current implementation has several problems:
name,url, andidcan benull, butCSFDSeasondeclares them as non‑null.infoContainer?.textContent?.replace(/…/g, '').trim()will throw ifinfoContaineris missing, because.trim()is not behind optional chaining.urlis returned as a relative path; elsewhere you expose absolute URLs.Suggested rewrite:
-export const getSeasonsOrEpisodes = (el: HTMLElement, serie?: { id: number; title: string; }): CSFDSeason[] | null => { +export const getSeasonsOrEpisodes = (el: HTMLElement): CSFDSeason[] | null => { const childrenList = el.querySelector('.film-episodes-list'); if (!childrenList) return null; const childrenNodes = childrenList.querySelectorAll('.film-title'); if (!childrenNodes?.length) return []; - return childrenNodes.map((season) => { - const nameContainer = season.querySelector('.film-title-name'); - const infoContainer = season.querySelector('.info'); - - return { - id: parseLastIdFromUrl(nameContainer?.getAttribute('href') || ''), - name: nameContainer?.textContent?.trim() || null, - url: nameContainer?.getAttribute('href') || null, - info: infoContainer?.textContent?.replace(/[{()}]/g, '').trim() || null, - }; - }); + const items: CSFDSeason[] = []; + for (const node of childrenNodes) { + const nameEl = node.querySelector('.film-title-name'); + const infoEl = node.querySelector('.info'); + const href = nameEl?.getAttribute('href') || ''; + const id = parseLastIdFromUrl(href); + const name = nameEl?.textContent?.trim(); + + if (!id || !name || !href) continue; + + const rawInfo = infoEl?.textContent + ?.replace(/[{()}]/g, '') + .trim(); + + items.push({ + id, + name, + url: href.startsWith('http') ? href : `https://www.csfd.cz${href}`, + info: rawInfo || null, + }); + } + return items; }This keeps
CSFDSeasonnon‑nullable, avoids a potential runtime error on.trim(), and makes URLs consistent with the rest of the helpers.
239-258: AligngetSeasonorEpisodeParentwith types and avoid passing undefined toparseIdFromUrl.Issues:
parseIdFromUrl(parentSeries?.getAttribute('href'))can receiveundefined; the helper expects a string.- Function can return
{ series: null, season: {...} }or{ series: {...}, season: null }, butCSFDParentcurrently requires both fields non‑null.- With
parents.length === 0, you fabricate a parent{ series: serie, season: null }which is then used even for plain films via the service fallback, yielding a bogus “self‑parent”.At minimum:
- const [parentSeries, parentSeason] = parents; - - const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href')); - const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href')); + const [parentSeries, parentSeason] = parents; + + const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href') || ''); + const seasonId = parseIdFromUrl(parentSeason?.getAttribute('href') || ''); const seriesName = parentSeries?.textContent?.trim() || null; const seasonName = parentSeason?.textContent?.trim() || null; const series = seriesId && seriesName ? { id: seriesId, name: seriesName } : null; const season = seasonId && seasonName ? { id: seasonId, name: seasonName } : null; if (!series && !season) return null; return { series, season };Combined with updating
CSFDParentto allow nullable members, this becomes type‑safe and matches real DOM possibilities. Also consider dropping theseriefallback entirely and just returningnullwhenparents.length === 0; the movie service can then decide when to compute a parent.src/services/movie.service.ts (2)
61-66: Fix helper name usage and clarify title/seasonName derivation.Once
getSerieasAndSeasonTitleis renamed and its types fixed, update this call accordingly:- const type = getMovieType(el) as CSFDFilmTypes; - const { seriesName = null, seasonName = null } = type === 'série' ? getSerieasAndSeasonTitle(el) : {}; + const type = getMovieType(el) as CSFDFilmTypes; + const { seriesName = null, seasonName = null } = + type === 'série' ? getSeriesAndSeasonTitle(el) : {};The
titleassignment logic (type === 'série' && seriesName ? seriesName : getMovieTitle(el)) looks reasonable and keeps the DTO’s title focused on the series name for season pages.Also applies to: 99-105
100-104: Correctparentcomputation: avoid bogus self‑parent for films and restrict to seasons/episodes.Current logic:
parent: (type === 'seriál') ? null : getSeasonorEpisodeParent(el, { id: movieId, name: title }),For plain films (non‑series), this calls
getSeasonorEpisodeParentwith a fallback{ id: movieId, name: title }, and the helper returns{ series: serie, season: null }when no parents are found. That incorrectly assigns every film a “parent” equal to itself.Recommend:
- parent: (type === 'seriál') ? null : getSeasonorEpisodeParent(el, { id: movieId, name: title }), + // Only derive parent for season/episode detail pages; films and root series have no parent. + parent: + type === 'epizoda' || type === 'série' + ? getSeasonorEpisodeParent(el) + : null,This aligns
parentwith the real hierarchy (episodes/seasons have parents; films and main series entries do not).
🧹 Nitpick comments (3)
src/dto/movie.ts (1)
20-22: Consider a distinct episode DTO instead of reusing CSFDSeason.
episodes: CSFDSeason[] | nullreuses the season type for episodes. If episode items differ even slightly (e.g., include an episode code, air date, or per‑episode info vs per‑season info), a dedicatedCSFDEpisodeinterface will keep the model clearer and safer to evolve.If the structures are truly identical, adding a short docstring clarifying that
CSFDSeasonis also used for episode list entries would help future readers.Also applies to: 166-171
src/helpers/movie.helper.ts (1)
231-237: MakedetectSeasonOrEpisodeListTypemore robust.Right now it inspects only the first
.box-header h3. If the seasons/episodes section isn’t the first box, you’ll silently returnnull.Consider scanning all headers instead:
-export const detectSeasonOrEpisodeListType = (el: HTMLElement) => { - const headerText = el.querySelector('.box-header h3')?.innerText.trim() ?? ''; - - if (headerText.includes('Série')) return 'seasons'; - if (headerText.startsWith('Epizody')) return 'episodes'; - return null; -} +export const detectSeasonOrEpisodeListType = (el: HTMLElement): 'seasons' | 'episodes' | null => { + const headers = el.querySelectorAll('.box-header h3'); + for (const header of headers) { + const text = header.innerText.trim(); + if (text.includes('Série')) return 'seasons'; + if (text.startsWith('Epizody')) return 'episodes'; + } + return null; +}Not critical, but makes the detection less brittle if CSFD changes box order.
src/services/movie.service.ts (1)
100-103: Ensure seasons/episodes arrays and DTOs stay consistent with helper behavior.The wiring:
seasons: seasonOrEpisodeListType === 'seasons' ? getSeasonsOrEpisodes(el) : null, episodes: seasonOrEpisodeListType === 'episodes' ? getSeasonsOrEpisodes(el) : null, episodeCode: type === 'epizoda' ? getEpisodeCode(el) : null,is fine conceptually, but depends on
getSeasonsOrEpisodesreturning items that matchCSFDSeason(non‑null id/name/url, absolute URLs). Once you adjust the helper as suggested, this assignment will be consistent and safe for consumers. Until then, the DTO over‑promises non‑nullable fields.After updating the helper, consider adding a couple of integration tests hitting a known series, season, and episode page to lock in expected shapes of
seasons,episodes,parent, andepisodeCode.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
demo.ts(1 hunks)src/dto/movie.ts(3 hunks)src/helpers/global.helper.ts(1 hunks)src/helpers/movie.helper.ts(2 hunks)src/services/movie.service.ts(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/helpers/global.helper.ts
🧰 Additional context used
🧬 Code graph analysis (3)
demo.ts (2)
src/index.ts (2)
csfd(70-76)movie(39-42)src/services/movie.service.ts (1)
movie(36-52)
src/helpers/movie.helper.ts (2)
src/helpers/global.helper.ts (2)
parseIdFromUrl(4-12)parseLastIdFromUrl(14-22)src/dto/movie.ts (2)
CSFDSeason(166-171)CSFDParent(27-30)
src/services/movie.service.ts (1)
src/helpers/movie.helper.ts (6)
getSerieasAndSeasonTitle(23-39)detectSeasonOrEpisodeListType(231-237)getMovieTitle(41-43)getSeasonsOrEpisodes(200-218)getSeasonorEpisodeParent(239-259)getEpisodeCode(220-229)
🔇 Additional comments (1)
demo.ts (1)
7-7: Demo change looks fine.Switching the demo movie ID and commenting out the creator call is harmless and keeps the example simple.
fcce368 to
5169dba
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (4)
tests/mocks/series2-episode.mock.ts (1)
1-1234: Same fixture pattern and trade-offs as other large mocksThis follows the same “full-page HTML in a template string” approach as the other mocks. Functionally fine, but the same comments about noisy diffs and readability apply; see note on
series1-season1-episode.mock.tsfor possible ways to externalize or slim these fixtures.src/helpers/movie.helper.ts (3)
109-127: Fix function name typo and return type mismatch.This issue was flagged in a previous review but remains unresolved:
- Typo:
getSerieasAndSeasonTitleshould begetSeriesAndSeasonTitle- Type mismatch: Return type declares
seriesName: stringbut Line 114 returnsnull, causing a type error in strict mode.Apply the previously suggested fix:
-export const getSerieasAndSeasonTitle = ( +export const getSeriesAndSeasonTitle = ( el: HTMLElement -): { seriesName: string; seasonName: string | null } => { +): { seriesName: string | null; seasonName: string | null } => { const titleElement = el.querySelector('h1'); if (!titleElement) { return { seriesName: null, seasonName: null }; } const fullText = titleElement.innerText.trim(); // Check if there's a series part indicated by ' - ' if (fullText.includes(' - ')) { const [seriesName, seasonName] = fullText.split(' - ').map((part) => part.trim()); return { seriesName, seasonName }; } // If no series part found, return just the name return { seriesName: fullText, seasonName: null }; };Update the import in
src/services/movie.service.ts(line 29 per previous review).
288-309: Ensure non-nullable fields and absolute URLs in getSeasonsOrEpisodes.A previous review flagged this but the issue persists:
- Lines 304-305:
nameandurlcan benull, butCSFDSeasonlikely requires non-null values- Line 305: URLs are relative paths, not absolute
- Line 303: If
parseLastIdFromUrlreturnsnullfor invalid input,idbecomesnull(coerced from the return value)Filter out invalid entries and construct absolute URLs:
export const getSeasonsOrEpisodes = ( el: HTMLElement, - serie?: { id: number; title: string } + _serie?: { id: number; title: string } ): CSFDSeason[] | null => { const childrenList = el.querySelector('.film-episodes-list'); if (!childrenList) return null; const childrenNodes = childrenList.querySelectorAll('.film-title'); if (!childrenNodes?.length) return []; - return childrenNodes.map((season) => { - const nameContainer = season.querySelector('.film-title-name'); - const infoContainer = season.querySelector('.info'); - - return { - id: parseLastIdFromUrl(nameContainer?.getAttribute('href') || ''), - name: nameContainer?.textContent?.trim() || null, - url: nameContainer?.getAttribute('href') || null, - info: infoContainer?.textContent?.replace(/[{()}]/g, '').trim() || null - }; - }); + const items: CSFDSeason[] = []; + for (const node of childrenNodes) { + const nameEl = node.querySelector('.film-title-name'); + const infoEl = node.querySelector('.info'); + const href = nameEl?.getAttribute('href') || ''; + const id = parseLastIdFromUrl(href); + const name = nameEl?.textContent?.trim(); + if (!id || !name || !href) continue; + items.push({ + id, + name, + url: `https://www.csfd.cz${href}`, + info: infoEl?.textContent?.replace(/[{()}]/g, '').trim() || null + }); + } + return items; }
330-358: Add string fallback to prevent undefined being passed to parseIdFromUrl.This issue was flagged in a previous review and remains unresolved:
Line 347 passes
parentSeries?.getAttribute('href')(which can beundefined) toparseIdFromUrl, but the function signature insrc/helpers/global.helper.ts(lines 3-11) expects astringparameter.Apply the fix from the previous review:
- const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href')); - const seasonId = parseLastIdFromUrl(parentSeason?.getAttribute('href') || ''); + const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href') || ''); + const seasonId = parseLastIdFromUrl(parentSeason?.getAttribute('href') || '');This ensures both calls always pass a string. Empty strings will be handled by the
if (url)check inparseIdFromUrl, returningnullsafely.Also consider adding unit tests for edge cases (0, 1, 2 parents) as suggested in the previous review.
🧹 Nitpick comments (7)
tests/mocks/series1-season1-episode.mock.ts (1)
1-1277: Huge inline HTML fixture: consider externalizing for maintainabilityThis mock is valid and structurally rich, but inlining the full CSFD page as a template string makes diffs noisy and the test file hard to navigate. Consider moving the raw HTML into a
.htmlfixture file and loading it viafs.readFileSyncin tests, or at least documenting which parts are relied on by the parser to reduce future accidental edits.tests/mocks/series1-season1.mock.ts (1)
1-1357: Season mock is structurally correct; same comments on fixture sizeThis season mock appears structurally aligned with how the parser operates (episodes list, creators, JSON-LD, aside). As with the other mocks, consider external fixture files or minimal, focused HTML samples in future to reduce inline bulk, but there’s no functional issue here.
tests/movie.test.ts (2)
636-654: getEpisodeCode tests are well‑structured and cover main branchesThe three tests here validate:
- Extraction of a code in parentheses,
- Absence of code in the title,
- Missing
.film-header-name h1entirely.This matches the helper’s behavior (regex on the
<h1>text) and gives good coverage of success and null cases. No changes needed.You could also add one test that uses a full episode mock DOM (like from the HTML fixtures) to guard against future selector changes, but that’s optional given the integration tests in
tests/series.test.ts.
656-669: getSeasonorEpisodeParent tests only cover the fallback pathThese tests exercise the fallback behavior (no header anchors, with/without
serieargument), which is good. Note that the primary parsing path—where anchors exist under.film-header h2 aor.film-header h1 aand IDs are parsed viaparseIdFromUrl/parseLastIdFromUrl—is only exercised indirectly intests/series.test.ts.Given that, there’s no immediate change needed here, but if
getSeasonorEpisodeParentevolves further, consider adding a small unit test with synthetic header links to lock in the selector/ID parsing behavior.tests/series.test.ts (2)
1-28: parseMovieMock: solid test helper, but relies on a private methodUsing
parseMovieMockto drive realistic HTML throughMovieScraper.buildMovieis a good approach to avoid network I/O and directly validate parsing. The only trade‑off is calling the privatebuildMovievia(scraper as any), which couples tests to internal structure.If
buildMovie’s signature changes often, consider:
- Adding a thin public test‑only wrapper in
MovieScraper(e.g.fromHtml(...)), or- Exporting
buildMoviebehind a non‑public symbol guarded by/** @internal */.Not urgent, but it would make these tests less brittle to refactors in
movie.service.ts.
394-440: Cross‑pattern validations are sensible sanity checksThe shared validation block nicely consolidates:
- Basic invariants (positive IDs and
/film/URLs).- Mutual exclusivity for seasons vs episodes on main vs season pages.
- Episode parent references and presence of
episodeCode.- Behavior for series without seasons (episodes directly,
seasons = null).This is a good high‑level safety net over the more detailed tests above. If test runtime ever becomes an issue, you might cache parsed
CSFDMovieinstances instead of reparsing the same mocks in each test, but that’s an optimization, not a requirement.src/helpers/movie.helper.ts (1)
322-328: Consider i18n support for header text detection.The function hard-codes Czech text patterns ('Série', 'Epizody'). If the codebase supports multiple languages (based on
getLocalizedCreatorLabel), this may need localization.Example enhancement:
export const detectSeasonOrEpisodeListType = ( el: HTMLElement, language?: string ) => { const headerText = el.querySelector('.box-header h3')?.innerText.trim() ?? ''; const patterns = { cs: { seasons: 'Série', episodes: 'Epizody' }, sk: { seasons: 'Série', episodes: 'Epizódy' }, en: { seasons: 'Season', episodes: 'Episode' } }; const lang = patterns[language || 'cs'] || patterns.cs; if (headerText.includes(lang.seasons)) return 'seasons'; if (headerText.startsWith(lang.episodes)) return 'episodes'; return null; };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/helpers/movie.helper.ts(5 hunks)tests/fetchers.test.ts(1 hunks)tests/mocks/series1-season1-episode.mock.ts(1 hunks)tests/mocks/series1-season1.mock.ts(1 hunks)tests/mocks/series2-episode.mock.ts(1 hunks)tests/movie.test.ts(6 hunks)tests/series.test.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
tests/series.test.ts (2)
src/dto/movie.ts (1)
CSFDMovie(3-25)src/services/movie.service.ts (1)
MovieScraper(35-107)
tests/fetchers.test.ts (3)
src/services/movie.service.ts (1)
movie(36-51)src/dto/movie.ts (1)
CSFDMovie(3-25)src/dto/global.ts (1)
CSFDFilmTypes(25-36)
tests/movie.test.ts (1)
src/helpers/movie.helper.ts (3)
getSerieasAndSeasonTitle(109-127)getEpisodeCode(311-320)getSeasonorEpisodeParent(330-358)
src/helpers/movie.helper.ts (2)
src/dto/movie.ts (6)
CSFDCreatorGroups(145-158)CSFDCreatorGroupsEnglish(161-174)CSFDCreatorGroupsSlovak(176-186)CSFDParent(27-30)CSFDMovieCreator(80-91)CSFDBoxContent(195-195)src/helpers/global.helper.ts (2)
parseLastIdFromUrl(14-22)parseIdFromUrl(4-12)
🔇 Additional comments (7)
tests/movie.test.ts (2)
190-209: Updated trivia expectations look consistent with richer site contentThe adjusted expected trivia strings for movies and series (lines 194–197 and 205–209) match the style of CSFD “Zajímavosti” entries and keep the parser’s behavior constrained to the textual content. No issues here.
265-275: VOD expectation tweaks align with URL tracking paramsThe updated expected VOD URLs (e.g., added
utm_content=csfdand trailing URL fixes) are fine and keep tests strict about the scraper returning fully qualified links. Nothing else to change in this segment.tests/series.test.ts (3)
38-223: Pattern 1 (Simpsons): good coverage of series → season → episode behaviorThese three describe blocks thoroughly validate the “Series with Seasons” pattern:
- Main series page: seasons present, episodes absent, rating, creators, genres, no parent/episodeCode/seasonName.
- Season page: episodes present, seasons absent,
seasonNameset, parent containing only series.- Episode page: correct
title,type = 'epizoda',episodeCode, parent with both series and season, year, duration, creators, and descriptions.The assertions align with the provided mocks and the intended semantics of
CSFDMovie. No changes needed here.
225-312: Pattern 2 main series (The Curse): validates direct episodes correctlyThis suite checks the “Series with direct episodes” case as intended:
type = 'seriál', correct title/year, episodes list present, no seasons.- No parent/episodeCode/seasonName on the main series object.
- Ratings, creators, genres, VOD, and descriptions all asserted.
The additional check that
firstEpisode.infomatches/E\d+/is a nice way to ensure the episode code is captured into the list metadata. Looks good.
320-389: Pattern 2 episode (The Curse E01): parent and duration checks look correctThese tests correctly assert:
- Episode metadata (
title,type = 'epizoda',episodeCode = 'E01',year = 2023,duration = 61).- Parent has only a series (no season) with expected id/name.
- No seasons/episodes arrays on the episode object itself.
- Creators, genres, descriptions, and VOD entries exist.
This matches the provided mock HTML where there is only a series header link and no season context. No issues here.
tests/fetchers.test.ts (1)
155-252: LGTM! Comprehensive test coverage for series patterns.The test suite effectively covers both series patterns (with and without seasons) across all hierarchy levels: series, season, and episode pages. The assertions validate type, title, hierarchical relationships (parent series/season), and collection fields (seasons/episodes).
src/helpers/movie.helper.ts (1)
311-320: LGTM! Episode code extraction is robust.The function correctly handles missing elements and uses a simple regex pattern to extract the code from parentheses.
| describe('Get Series and Season Title', () => { | ||
| test('With series and season', () => { | ||
| const html = parse('<h1>Series Name - Season 1</h1>'); | ||
| const result = getSerieasAndSeasonTitle(html as any); | ||
| expect(result).toEqual({ seriesName: 'Series Name', seasonName: 'Season 1' }); | ||
| }); | ||
|
|
||
| test('Only series name', () => { | ||
| const html = parse('<h1>Movie Name</h1>'); | ||
| const result = getSerieasAndSeasonTitle(html as any); | ||
| expect(result).toEqual({ seriesName: 'Movie Name', seasonName: null }); | ||
| }); | ||
|
|
||
| test('No title element', () => { | ||
| const html = parse('<div></div>'); | ||
| const result = getSerieasAndSeasonTitle(html as any); | ||
| expect(result).toEqual({ seriesName: null, seasonName: null }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getSerieasAndSeasonTitle: tests reveal a type mismatch in the helper
These tests correctly cover:
- Series + season header (
"Series Name - Season 1"), - Plain title without season,
- Missing
<h1>element.
However, getSerieasAndSeasonTitle is currently declared to return { seriesName: string; seasonName: string | null }, yet its implementation (and this “No title element” test) return seriesName: null when the <h1> is absent. This is a TypeScript type violation.
Consider changing the helper signature to:
export const getSerieasAndSeasonTitle = (
el: HTMLElement
): { seriesName: string | null; seasonName: string | null } => { ... }so the implementation and tests are type-safe and consistent.
🤖 Prompt for AI Agents
In tests/movie.test.ts around lines 616-634, the helper getSerieasAndSeasonTitle
is returning seriesName|null in some cases but its TypeScript signature
currently requires seriesName: string; change the helper's exported signature to
return { seriesName: string | null; seasonName: string | null } so the
implementation and tests are type-safe; update the function declaration/export
to use the nullable seriesName type and fix any call sites or imports
accordingly to satisfy the new return type.
There was a problem hiding this 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
♻️ Duplicate comments (1)
src/dto/movie.ts (1)
27-30: Consider documenting CSFDParent invariants (series/season presence)With both
seasonandseriesallowed to benull, the type doesn’t express any runtime guarantees (e.g., “for an episode,seriesis always non‑null”, “at least one of them is non‑null”). A short doc comment explaining expected combinations would make this API easier to consume; a stronger alternative would be a discriminated union if you ever need stricter typing.
🧹 Nitpick comments (1)
src/dto/movie.ts (1)
20-21: Clarify episode vs. season typing (optional alias/type for episodes)
episodesis currently typed asCSFDSeason[] | null, andCSFDSeasonis defined as a generic{ id, name, url, info }item. That works structurally but can be semantically confusing, especially since the PR description mentionsCSFDEpisode[] | null.If you want clearer intent without changing the underlying shape, you could introduce an alias and retarget
episodes:- episodes: CSFDSeason[] | null; + episodes: CSFDEpisode[] | null;and elsewhere:
export type CSFDEpisode = CSFDSeason;This keeps the implementation simple while making the public API more self‑descriptive.
Also applies to: 194-199
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/dto/movie.ts(2 hunks)
🔇 Additional comments (1)
src/dto/movie.ts (1)
20-25: New series/season/episode fields on CSFDMovie are a safe, additive extensionAll newly added properties (
seasons,episodes,parent,episodeCode,seasonName) are nullable and thus backward‑compatible for consumers ofCSFDMovie. The DTO shape here looks consistent with the described feature.
5eead30 to
18f3b44
Compare
There was a problem hiding this 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
♻️ Duplicate comments (3)
src/helpers/movie.helper.ts (3)
109-127: Fix typo in exported namegetSerieasAndSeasonTitle.Logic and null-handling look good, but the exported name still contains a typo (
Serieas), which will leak into the public helper surface and callers.Consider renaming to
getSeriesAndSeasonTitleand updating all imports/usages accordingly.-export const getSerieasAndSeasonTitle = ( +export const getSeriesAndSeasonTitle = ( el: HTMLElement ): { seriesName: string | null; seasonName: string | null } => {
288-309: HardengetSeasonsOrEpisodesoutput: avoid null id/name/url and normalize URLs.As written, the mapper can emit entries with:
id=null/NaN whenhrefis missing or unparsable,name=nullwhen text is missing,url=nulland still relative (no host),which is likely inconsistent with
CSFDSeason(id/name/url typically non-nullable) and with other helpers that always return absolute URLs. Theserieparameter is also currently unused.Filtering invalid nodes and normalizing URLs will make the helper safer and keep the DTO strict.
-export const getSeasonsOrEpisodes = ( - el: HTMLElement, - serie?: { id: number; title: string } -): CSFDSeason[] | null => { - const childrenList = el.querySelector('.film-episodes-list'); - if (!childrenList) return null; - - const childrenNodes = childrenList.querySelectorAll('.film-title'); - if (!childrenNodes?.length) return []; - - return childrenNodes.map((season) => { - const nameContainer = season.querySelector('.film-title-name'); - const infoContainer = season.querySelector('.info'); - - return { - id: parseLastIdFromUrl(nameContainer?.getAttribute('href') || ''), - name: nameContainer?.textContent?.trim() || null, - url: nameContainer?.getAttribute('href') || null, - info: infoContainer?.textContent?.replace(/[{()}]/g, '').trim() || null - }; - }); -}; +export const getSeasonsOrEpisodes = ( + el: HTMLElement, + _serie?: { id: number; title: string } +): CSFDSeason[] | null => { + const childrenList = el.querySelector('.film-episodes-list'); + if (!childrenList) return null; + + const childrenNodes = childrenList.querySelectorAll('.film-title'); + if (!childrenNodes?.length) return []; + + const items: CSFDSeason[] = []; + for (const node of childrenNodes) { + const nameEl = node.querySelector('.film-title-name'); + const infoEl = node.querySelector('.info'); + const href = nameEl?.getAttribute('href') || ''; + const id = parseLastIdFromUrl(href); + const name = nameEl?.textContent?.trim(); + if (!id || !name || !href) continue; + + items.push({ + id, + name, + url: `https://www.csfd.cz${href}`, + info: infoEl?.textContent?.replace(/[{()}]/g, '').trim() || null + }); + } + return items; +};
330-358: Avoid passing nullable href toparseIdFromUrlingetSeasonorEpisodeParent.
parentSeries?.getAttribute('href')isstring | null, butparseIdFromUrlexpects astring. Ifhrefis missing, you currently end up passingnull, which is both a type error understrictNullChecksand potentially fragile at runtime. Earlier review already suggested adding a string fallback here.Align series and season handling by ensuring both calls always receive a string, and keep the final
series/seasonobjects gated on both id and name.- const [parentSeries, parentSeason] = parents; - - const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href')); - const seasonId = parseLastIdFromUrl(parentSeason?.getAttribute('href') || ''); + const [parentSeries, parentSeason] = parents; + + const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href') || ''); + const seasonId = parseLastIdFromUrl(parentSeason?.getAttribute('href') || '');
🧹 Nitpick comments (2)
src/helpers/movie.helper.ts (2)
311-320: Episode code extraction assumes only one parenthesized segment.The helper works if the header has a single
(...), but with something likeFoo (2008) (S01E01)it will return2008instead of the episode code. If CSFD headers can contain multiple parentheses, consider matching the last group instead.- const text = filmHeaderName.textContent?.trim() || ''; - const match = text.match(/\(([^)]+)\)/); + const text = filmHeaderName.textContent?.trim() || ''; + // Prefer the last (...) group in case there are multiple. + const match = text.match(/\(([^)]+)\)\s*$/);
322-328:detectSeasonOrEpisodeListTypeis fine; consider explicit return type.The implementation cleanly distinguishes
'seasons'vs'episodes'based on the Czech header text. For long‑term API stability, you might want to add an explicit return type: 'seasons' | 'episodes' | nullso the contract doesn’t widen if the implementation changes.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/dto/movie.ts(2 hunks)src/helpers/movie.helper.ts(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/dto/movie.ts
🔇 Additional comments (4)
src/helpers/movie.helper.ts (4)
11-24: New imports for parent/season and helpers look consistent.The added DTO imports (
CSFDParent,CSFDSeason) andparseLastIdFromUrlintegration align with the new helpers’ usage; no issues here.
34-52: Typed creator-group key union is clear and type-safe.Narrowing
keyto the explicit union of creator groups and typinglabelsaccordingly makes this API safer and self-documenting; implementation looks sound.
360-363:getMovieGroupsignature change matches creator label types.Updating
groupto accept the union ofCSFDCreatorGroups | CSFDCreatorGroupsEnglish | CSFDCreatorGroupsSlovakmatches whatgetLocalizedCreatorLabelreturns and keeps things type-safe; implementation remains unchanged and looks good.
400-403:getMovieBoxMoviessignature formatting change only.The multi-line signature is just a stylistic change; the behavior of the function is unchanged and remains correct.
18f3b44 to
2aa67db
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (5)
src/helpers/global.helper.ts (1)
14-22: Previously identified type and parsing issues remain unaddressed.The past review comment (marked as addressed in commits 415e5e5 to 2039636) identified critical issues that still exist in the current code:
- Type signature mismatch: Function declares
numberbut returnsnullon lines 18 and 20- 0-coercion bug: Line 18 uses
+id || nullwhich incorrectly converts valid ID0tonull- Unverified segment index: Line 16 uses index
[3]without verification against actual CSFD URL structureThe suggested fix from the past review should be applied:
-export const parseLastIdFromUrl = (url: string): number => { +export const parseLastIdFromUrl = (url: string): number | null => { if (url) { - const idSlug = url?.split('/')[3]; - const id = idSlug?.split('-')[0]; - return +id || null; + const idSlug = url.split('/')[3]; + const num = Number((idSlug ?? '').split('-')[0]); + return Number.isFinite(num) ? num : null; } else { return null; } };src/services/movie.service.ts (1)
102-102: Critical: Parent computation assigns incorrect parent to regular films.Line 102 uses
(type === 'seriál') ? null : getSeasonorEpisodeParent(el, { id: movieId, name: title }), which passes the current movie's ID and title as a "series" fallback for all non-series types, including standalone films. This creates bogus parent relationships.The parent field should only be populated for episodes and seasons, not for series or films:
- parent: (type === 'seriál') ? null : getSeasonorEpisodeParent(el, { id: movieId, name: title }), + parent: + (type === 'epizoda' || type === 'série') + ? getSeasonorEpisodeParent(el) + : null,Only pass a series fallback when you have explicit breadcrumb/header evidence of the series context (not available in current code).
src/helpers/movie.helper.ts (3)
288-309: Critical: getSeasonsOrEpisodes violates CSFDSeason contract with nullable fields and relative URLs.Past review (marked addressed in commits a56763b to 80336e9) identified these issues, but they remain in the current code:
- Lines 304-305 return nullable values (
|| null) violatingCSFDSeasoninterface which requires non-nullablename: stringandurl: string- Line 305 returns relative URLs without base domain (e.g.,
/film/...instead ofhttps://www.csfd.cz/film/...)Apply this fix to filter incomplete nodes and normalize URLs:
export const getSeasonsOrEpisodes = ( el: HTMLElement, - serie?: { id: number; title: string } + _serie?: { id: number; title: string } ): CSFDSeason[] | null => { const childrenList = el.querySelector('.film-episodes-list'); if (!childrenList) return null; const childrenNodes = childrenList.querySelectorAll('.film-title'); if (!childrenNodes?.length) return []; - return childrenNodes.map((season) => { - const nameContainer = season.querySelector('.film-title-name'); - const infoContainer = season.querySelector('.info'); - - return { - id: parseLastIdFromUrl(nameContainer?.getAttribute('href') || ''), - name: nameContainer?.textContent?.trim() || null, - url: nameContainer?.getAttribute('href') || null, - info: infoContainer?.textContent?.replace(/[{()}]/g, '').trim() || null - }; - }); + const items: CSFDSeason[] = []; + for (const node of childrenNodes) { + const nameEl = node.querySelector('.film-title-name'); + const infoEl = node.querySelector('.info'); + const href = nameEl?.getAttribute('href') || ''; + const id = parseLastIdFromUrl(href); + const name = nameEl?.textContent?.trim(); + if (!id || !name || !href) continue; + items.push({ + id, + name, + url: `https://www.csfd.cz${href}`, + info: infoEl?.textContent?.replace(/[{()}]/g, '').trim() || null + }); + } + return items; };
109-127: Fix function name typo and verify return type.The function name has a typo:
getSerieasAndSeasonTitleshould begetSeriesAndSeasonTitle. The return type correctly allows nullableseriesNameas addressed in past reviews, but the name needs correction.-export const getSerieasAndSeasonTitle = ( +export const getSeriesAndSeasonTitle = ( el: HTMLElement ): { seriesName: string | null; seasonName: string | null } => {Update the import in
src/services/movie.service.tsline 30 accordingly.
347-348: Add string fallback for getAttribute to prevent undefined.Line 347 calls
getAttribute('href')which can returnundefinedwhen the attribute is missing. WhileparseIdFromUrlhandles falsy values, TypeScript strict mode requires a string.- const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href')); + const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href') || ''); const seasonId = parseLastIdFromUrl(parentSeason?.getAttribute('href') || '');Line 348 already has the correct fallback.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
demo.ts(1 hunks)src/dto/movie.ts(2 hunks)src/helpers/global.helper.ts(1 hunks)src/helpers/movie.helper.ts(5 hunks)src/services/movie.service.ts(4 hunks)tests/fetchers.test.ts(1 hunks)tests/mocks/series1-season1-episode.mock.ts(1 hunks)tests/mocks/series1-season1.mock.ts(1 hunks)tests/mocks/series2-episode.mock.ts(1 hunks)tests/movie.test.ts(6 hunks)tests/series.test.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- tests/series.test.ts
- demo.ts
- tests/mocks/series1-season1.mock.ts
- tests/mocks/series1-season1-episode.mock.ts
🧰 Additional context used
🧬 Code graph analysis (3)
tests/movie.test.ts (1)
src/helpers/movie.helper.ts (3)
getSerieasAndSeasonTitle(109-127)getEpisodeCode(311-320)getSeasonorEpisodeParent(330-358)
tests/fetchers.test.ts (3)
src/index.ts (2)
movie(57-60)csfd(89-96)src/dto/movie.ts (1)
CSFDMovie(3-25)src/dto/global.ts (1)
CSFDFilmTypes(25-36)
src/services/movie.service.ts (2)
src/helpers/movie.helper.ts (12)
getMovieType(373-376)getSerieasAndSeasonTitle(109-127)detectSeasonOrEpisodeListType(322-328)getMovieTitle(129-131)getMovieYear(170-178)getMovieDuration(180-204)getMovieDescriptions(266-270)getMovieGenres(133-136)getMovieBoxMovies(400-417)getSeasonsOrEpisodes(288-309)getSeasonorEpisodeParent(330-358)getEpisodeCode(311-320)src/dto/global.ts (1)
CSFDFilmTypes(25-36)
🔇 Additional comments (6)
tests/mocks/series2-episode.mock.ts (1)
1-1234: LGTM! Test mock provides necessary HTML fixture.The HTML mock export is well-structured and provides comprehensive test data for series 2 episode parsing, including metadata, media content, reviews, and navigation elements. This complements existing mocks and supports the new episode-level parsing tests.
src/dto/movie.ts (2)
20-30: LGTM! DTO changes properly implement nullable hierarchical metadata.The new fields (
seasons,episodes,parent,episodeCode,seasonName) and interfaces (CSFDParent,CSFDSeason) correctly support TV series/season/episode hierarchies. TheCSFDParentinterface properly allows nullableseasonandseriesmembers as addressed in past reviews.
194-199: LGTM! CSFDSeason interface structure is appropriate.The interface properly defines season/episode metadata with required fields (
id,name,url) and optionalinfo. This aligns with the helper functions that populate these fields.tests/fetchers.test.ts (1)
155-252: LGTM! Comprehensive pattern-based test coverage.The expanded test suite now covers five distinct series/season/episode patterns, validating:
- Type and title extraction
- Seasons/episodes arrays population
- Parent series/season linkage
- Episode code parsing
- Null handling for absent fields
This thorough coverage significantly improves confidence in the new hierarchical parsing functionality.
tests/movie.test.ts (1)
616-669: LGTM! Well-structured test coverage for new helpers.The new test suites provide good coverage for the three helper functions:
getSerieasAndSeasonTitle: validates series/season extraction logicgetEpisodeCode: verifies episode code parsinggetSeasonorEpisodeParent: confirms parent linkage behaviorEdge cases are appropriately tested (missing elements, null fallbacks).
src/services/movie.service.ts (1)
60-65: LGTM! Type and title computation logic is sound.The new logic correctly:
- Computes type once and reuses it
- Extracts series/season names when type is 'série'
- Detects whether the page contains seasons or episodes list
- Falls back to standard title extraction when not a season page
| seasons: seasonOrEpisodeListType === 'seasons' ? getSeasonsOrEpisodes(el) : null, | ||
| episodes: seasonOrEpisodeListType === 'episodes' ? getSeasonsOrEpisodes(el) : null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -nA15 "export const getSeasonsOrEpisodes" src/helpers/movie.helper.tsRepository: bartholomej/node-csfd-api
Length of output: 721
🏁 Script executed:
rg -nA40 "export const getSeasonsOrEpisodes" src/helpers/movie.helper.tsRepository: bartholomej/node-csfd-api
Length of output: 1641
getSeasonsOrEpisodes does not filter incomplete nodes or normalize URLs.
The helper function at lines 288-309 in src/helpers/movie.helper.ts maps all childrenNodes without filtering incomplete entries. Nodes with missing id, name, or url are included in the result, with null values returned for absent properties. Additionally, URLs are returned as raw href attributes without normalization—the https://www.csfd.cz prefix is not prepended, so consumers will receive relative URLs (e.g., /film/...). Add filtering logic to exclude incomplete nodes and normalize URLs to absolute paths.
🤖 Prompt for AI Agents
In src/services/movie.service.ts around lines 100-101 (root cause in
src/helpers/movie.helper.ts lines 288-309): the getSeasonsOrEpisodes mapper
currently returns entries with missing id/name/url and leaves relative hrefs
unnormalized. Update getSeasonsOrEpisodes to first filter out any child nodes
that lack a non-empty id, name, or href; then map remaining nodes to objects
with normalized absolute URLs (if href starts with '/' prepend
"https://www.csfd.cz", otherwise keep as-is). Ensure the function returns only
fully populated entries (omit null/undefined properties) and update the return
type if necessary so callers receive only valid season/episode objects with
absolute urls.
There was a problem hiding this 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
🤖 Fix all issues with AI agents
In @src/helpers/movie.helper.ts:
- Around line 109-127: The exported function name getSerieasAndSeasonTitle has a
typo; rename it to getSeriesAndSeasonTitle (update the function declaration and
the export) and update any usages/imports across the codebase to reference
getSeriesAndSeasonTitle so callers don't break; ensure TypeScript
types/signature remain unchanged ({ seriesName: string | null; seasonName:
string | null }) and run tests/compile to catch any remaining references.
- Around line 330-358: Rename the function getSeasonorEpisodeParent to
getSeasonOrEpisodeParent (capital O) throughout the module: update the exported
function declaration name, any internal references, and the export list if
present, and update all call sites that import or use getSeasonorEpisodeParent
so they reference getSeasonOrEpisodeParent instead; keep the function signature
and behavior unchanged while ensuring any tests or imports (e.g., named imports
from movie.helper) are updated to the new identifier.
In @src/services/movie.service.ts:
- Line 102: The parent field is being set for all non-'seriál' types; restrict
it so parent is only populated when type is 'epizoda' or 'série'. Update the
expression that assigns parent (currently calling getSeasonorEpisodeParent(el, {
id: movieId, name: title })) to first check if type === 'epizoda' || type ===
'série' and only then call getSeasonorEpisodeParent; otherwise set parent to
null. Keep references to the same variables (type, el, movieId, title) and the
same helper getSeasonorEpisodeParent.
🧹 Nitpick comments (2)
src/dto/movie.ts (1)
20-24: Consider adding a clarifying comment about type reuse.Both
seasonsandepisodesuse theCSFDSeason[]type, which is correct since they share the same structure. However, a brief JSDoc comment explaining this design choice would help future maintainers understand why episodes aren't a separate type.📝 Suggested documentation enhancement
related: CSFDMovieListItem[]; similar: CSFDMovieListItem[]; + /** List of seasons (for series with season structure). Uses CSFDSeason for consistency. */ seasons: CSFDSeason[] | null; + /** List of episodes (direct or within season). Uses CSFDSeason for consistency. */ episodes: CSFDSeason[] | null; parent: CSFDParent | null;src/services/movie.service.ts (1)
100-101: Consider clarifying the dual usage ofgetSeasonsOrEpisodes.The same helper function
getSeasonsOrEpisodes(el)is used to populate both theseasonsandepisodesfields, differentiated only by theseasonOrEpisodeListTypecheck. While this may be intentional and functional, it could be confusing for maintainability.Consider either:
- Renaming the helper to reflect its polymorphic nature (e.g.,
getSeasonOrEpisodeList)- Creating separate helper functions (
getSeasons,getEpisodes) for clearer semantics- Adding a comment explaining why the same function is used for both
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
demo.tssrc/dto/movie.tssrc/helpers/global.helper.tssrc/helpers/movie.helper.tssrc/services/movie.service.tstests/fetchers.test.tstests/mocks/series1-season1-episode.mock.tstests/mocks/series1-season1.mock.tstests/mocks/series1-seasons.mock.tstests/mocks/series2-episode.mock.tstests/mocks/series2-episodes.mock.tstests/movie.test.tstests/series.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- demo.ts
- src/helpers/global.helper.ts
- tests/mocks/series1-season1-episode.mock.ts
- tests/mocks/series2-episode.mock.ts
- tests/movie.test.ts
🧰 Additional context used
🧬 Code graph analysis (3)
tests/series.test.ts (2)
src/dto/movie.ts (1)
CSFDMovie(3-25)src/services/movie.service.ts (1)
MovieScraper(35-107)
tests/fetchers.test.ts (3)
src/services/movie.service.ts (1)
movie(36-51)src/dto/movie.ts (1)
CSFDMovie(3-25)src/dto/global.ts (1)
CSFDFilmTypes(25-36)
src/helpers/movie.helper.ts (2)
src/dto/movie.ts (8)
CSFDCreatorGroups(143-156)CSFDCreatorGroupsEnglish(158-171)CSFDCreatorGroupsSlovak(173-183)CSFDSeason(194-199)CSFDParent(27-30)CSFDMovieCreator(80-91)CSFDBoxContent(192-192)CSFDMovieListItem(93-97)src/helpers/global.helper.ts (2)
parseLastIdFromUrl(14-22)parseIdFromUrl(4-12)
🔇 Additional comments (13)
tests/mocks/series1-season1.mock.ts (1)
1-1357: LGTM - Comprehensive test mock for season page.This mock provides a realistic HTML fixture for testing season page parsing. The large size is expected for a full CSFD page including navigation, ads, and tracking scripts. The structure aligns with the test requirements for extracting season metadata, episode lists, and parent series information.
tests/fetchers.test.ts (1)
156-253: Excellent test restructuring with clear pattern-based organization.The refactored test suite clearly separates series patterns and page types, making it much easier to understand the different scenarios:
- Pattern 1 (The Simpsons): Series → Seasons → Episodes hierarchy
- Pattern 2 (The Curse): Series → Episodes (no seasons)
Each pattern thoroughly validates the new DTO fields (seasons, episodes, parent, episodeCode, seasonName) across different page types.
src/dto/movie.ts (1)
194-199: LGTM - Clean interface definition.The
CSFDSeasoninterface appropriately captures season/episode metadata with nullableinfofield for flexibility.src/helpers/movie.helper.ts (3)
311-320: LGTM - Clean episode code extraction.The function correctly extracts the episode code from parentheses in the title text using a regex pattern.
322-328: LGTM - Clear season/episode detection.The function properly distinguishes between season and episode lists based on the header text.
288-309: Remove the unusedserieparameter from the function signature.The
serieparameter is never used in the function body and callers do not pass it. Unlike the similargetSeasonorEpisodeParentfunction which actively uses itsserieparameter, this parameter appears to be dead code and should be removed.tests/series.test.ts (5)
14-28: Consider the trade-offs of using reflection for testing.The helper uses reflection (
(scraper as any).buildMovie) to access the privatebuildMoviemethod. While this works for testing, it has downsides:
- Fragility: Tests will break if the method signature changes, with no compile-time safety
- Maintenance burden: The method's privacy contract is circumvented
Alternatives to consider:
- Make
buildMovieprotected instead of private- Create a dedicated testing helper method in the service
- Test through the public
movie()method with mockedfetchPageWhat's your preference for testing private methods? If reflection is the team standard, this is fine. Otherwise, consider refactoring for better type safety.
38-223: Excellent comprehensive test coverage for Pattern 1.The test suite thoroughly validates all aspects of the series-with-seasons pattern:
- ✅ Main series page with seasons list
- ✅ Season page with episodes list
- ✅ Episode page with full metadata
- ✅ Parent relationships at each level
- ✅ Mutual exclusivity of seasons/episodes
The three-level hierarchy is well tested with appropriate assertions for each page type.
232-312: LGTM - Thorough validation of direct-episodes pattern.Correctly validates series without intermediate season pages, ensuring episodes list is present directly on the main page and seasons remain null.
320-389: LGTM - Episode-without-season validation is complete.Properly tests the case where an episode belongs directly to a series (no season parent), confirming
parent.seasonis null whileparent.seriesis populated.
394-440: LGTM - Valuable cross-pattern validation tests.These tests ensure consistency across different series patterns and validate important invariants like mutual exclusivity of seasons/episodes.
src/services/movie.service.ts (2)
6-7: LGTM!The new helper imports are well-organized and appropriately scoped for the series/season/episode feature.
Also applies to: 27-30
61-65: All type string literals ('série','seriál','epizoda') used in this code are valid values in theCSFDFilmTypesdefinition. No action needed.
| export const getSerieasAndSeasonTitle = ( | ||
| el: HTMLElement | ||
| ): { seriesName: string | null; seasonName: string | null } => { | ||
| const titleElement = el.querySelector('h1'); | ||
| if (!titleElement) { | ||
| return { seriesName: null, seasonName: null }; | ||
| } | ||
|
|
||
| const fullText = titleElement.innerText.trim(); | ||
|
|
||
| // Check if there's a series part indicated by ' - ' | ||
| if (fullText.includes(' - ')) { | ||
| const [seriesName, seasonName] = fullText.split(' - ').map((part) => part.trim()); | ||
| return { seriesName, seasonName }; | ||
| } | ||
|
|
||
| // If no series part found, return just the name | ||
| return { seriesName: fullText, seasonName: null }; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Fix function name typo: "Serieas" → "Series".
The function name contains a typo: getSerieasAndSeasonTitle should be getSeriesAndSeasonTitle.
📝 Proposed fix
-export const getSerieasAndSeasonTitle = (
+export const getSeriesAndSeasonTitle = (
el: HTMLElement
): { seriesName: string | null; seasonName: string | null } => {📝 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.
| export const getSerieasAndSeasonTitle = ( | |
| el: HTMLElement | |
| ): { seriesName: string | null; seasonName: string | null } => { | |
| const titleElement = el.querySelector('h1'); | |
| if (!titleElement) { | |
| return { seriesName: null, seasonName: null }; | |
| } | |
| const fullText = titleElement.innerText.trim(); | |
| // Check if there's a series part indicated by ' - ' | |
| if (fullText.includes(' - ')) { | |
| const [seriesName, seasonName] = fullText.split(' - ').map((part) => part.trim()); | |
| return { seriesName, seasonName }; | |
| } | |
| // If no series part found, return just the name | |
| return { seriesName: fullText, seasonName: null }; | |
| }; | |
| export const getSeriesAndSeasonTitle = ( | |
| el: HTMLElement | |
| ): { seriesName: string | null; seasonName: string | null } => { | |
| const titleElement = el.querySelector('h1'); | |
| if (!titleElement) { | |
| return { seriesName: null, seasonName: null }; | |
| } | |
| const fullText = titleElement.innerText.trim(); | |
| // Check if there's a series part indicated by ' - ' | |
| if (fullText.includes(' - ')) { | |
| const [seriesName, seasonName] = fullText.split(' - ').map((part) => part.trim()); | |
| return { seriesName, seasonName }; | |
| } | |
| // If no series part found, return just the name | |
| return { seriesName: fullText, seasonName: null }; | |
| }; |
🤖 Prompt for AI Agents
In @src/helpers/movie.helper.ts around lines 109 - 127, The exported function
name getSerieasAndSeasonTitle has a typo; rename it to getSeriesAndSeasonTitle
(update the function declaration and the export) and update any usages/imports
across the codebase to reference getSeriesAndSeasonTitle so callers don't break;
ensure TypeScript types/signature remain unchanged ({ seriesName: string | null;
seasonName: string | null }) and run tests/compile to catch any remaining
references.
| export const getSeasonorEpisodeParent = ( | ||
| el: HTMLElement, | ||
| serie?: { id: number; name: string } | ||
| ): CSFDParent | null => { | ||
| // Try h2 first (for episodes), then h1 (for seasons) | ||
| let parents = el.querySelectorAll('.film-header h2 a'); | ||
| if (parents.length === 0) { | ||
| parents = el.querySelectorAll('.film-header h1 a'); | ||
| } | ||
|
|
||
| if (parents.length === 0) { | ||
| if (!serie) return null; | ||
| return { series: serie, season: null }; | ||
| } | ||
|
|
||
| const [parentSeries, parentSeason] = parents; | ||
|
|
||
| const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href')); | ||
| const seasonId = parseLastIdFromUrl(parentSeason?.getAttribute('href') || ''); | ||
| const seriesName = parentSeries?.textContent?.trim() || null; | ||
| const seasonName = parentSeason?.textContent?.trim() || null; | ||
|
|
||
| const series = seriesId && seriesName ? { id: seriesId, name: seriesName } : null; | ||
| const season = seasonId && seasonName ? { id: seasonId, name: seasonName } : null; | ||
|
|
||
| if (!series && !season) return null; | ||
|
|
||
| return { series, season }; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Fix function name typo: "Seasonor" → "SeasonOr".
The function name contains a typo: getSeasonorEpisodeParent should be getSeasonOrEpisodeParent (with capital O).
📝 Proposed fix
-export const getSeasonorEpisodeParent = (
+export const getSeasonOrEpisodeParent = (
el: HTMLElement,
serie?: { id: number; name: string }
): CSFDParent | null => {📝 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.
| export const getSeasonorEpisodeParent = ( | |
| el: HTMLElement, | |
| serie?: { id: number; name: string } | |
| ): CSFDParent | null => { | |
| // Try h2 first (for episodes), then h1 (for seasons) | |
| let parents = el.querySelectorAll('.film-header h2 a'); | |
| if (parents.length === 0) { | |
| parents = el.querySelectorAll('.film-header h1 a'); | |
| } | |
| if (parents.length === 0) { | |
| if (!serie) return null; | |
| return { series: serie, season: null }; | |
| } | |
| const [parentSeries, parentSeason] = parents; | |
| const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href')); | |
| const seasonId = parseLastIdFromUrl(parentSeason?.getAttribute('href') || ''); | |
| const seriesName = parentSeries?.textContent?.trim() || null; | |
| const seasonName = parentSeason?.textContent?.trim() || null; | |
| const series = seriesId && seriesName ? { id: seriesId, name: seriesName } : null; | |
| const season = seasonId && seasonName ? { id: seasonId, name: seasonName } : null; | |
| if (!series && !season) return null; | |
| return { series, season }; | |
| }; | |
| export const getSeasonOrEpisodeParent = ( | |
| el: HTMLElement, | |
| serie?: { id: number; name: string } | |
| ): CSFDParent | null => { | |
| // Try h2 first (for episodes), then h1 (for seasons) | |
| let parents = el.querySelectorAll('.film-header h2 a'); | |
| if (parents.length === 0) { | |
| parents = el.querySelectorAll('.film-header h1 a'); | |
| } | |
| if (parents.length === 0) { | |
| if (!serie) return null; | |
| return { series: serie, season: null }; | |
| } | |
| const [parentSeries, parentSeason] = parents; | |
| const seriesId = parseIdFromUrl(parentSeries?.getAttribute('href')); | |
| const seasonId = parseLastIdFromUrl(parentSeason?.getAttribute('href') || ''); | |
| const seriesName = parentSeries?.textContent?.trim() || null; | |
| const seasonName = parentSeason?.textContent?.trim() || null; | |
| const series = seriesId && seriesName ? { id: seriesId, name: seriesName } : null; | |
| const season = seasonId && seasonName ? { id: seasonId, name: seasonName } : null; | |
| if (!series && !season) return null; | |
| return { series, season }; | |
| }; |
🤖 Prompt for AI Agents
In @src/helpers/movie.helper.ts around lines 330 - 358, Rename the function
getSeasonorEpisodeParent to getSeasonOrEpisodeParent (capital O) throughout the
module: update the exported function declaration name, any internal references,
and the export list if present, and update all call sites that import or use
getSeasonorEpisodeParent so they reference getSeasonOrEpisodeParent instead;
keep the function signature and behavior unchanged while ensuring any tests or
imports (e.g., named imports from movie.helper) are updated to the new
identifier.
| similar: getMovieBoxMovies(asideEl, 'Podobné'), | ||
| seasons: seasonOrEpisodeListType === 'seasons' ? getSeasonsOrEpisodes(el) : null, | ||
| episodes: seasonOrEpisodeListType === 'episodes' ? getSeasonsOrEpisodes(el) : null, | ||
| parent: (type === 'seriál') ? null : getSeasonorEpisodeParent(el, { id: movieId, name: title }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the getSeasonorEpisodeParent implementation
echo "=== Looking for getSeasonorEpisodeParent implementation ==="
rg -n "getSeasonorEpisodeParent" --type ts -B 2 -A 10
# Check the movie.service.ts file around line 102
echo -e "\n=== Context around line 102 in movie.service.ts ==="
cat -n src/services/movie.service.ts | sed -n '95,110p'
# Look for type definitions and constants
echo -e "\n=== Type definitions and how types are used ==="
rg -n "type.*seriál|type.*epizoda|type.*série" --type ts
# Check for parent field usage patterns
echo -e "\n=== Where parent is set or used ==="
rg -n "parent.*:" --type ts -C 1Repository: bartholomej/node-csfd-api
Length of output: 35441
🏁 Script executed:
#!/bin/bash
# Get the full implementation of getSeasonorEpisodeParent
echo "=== Full getSeasonorEpisodeParent implementation ==="
cat -n src/helpers/movie.helper.ts | sed -n '330,365p'
# Look for tests that validate parent field behavior
echo -e "\n=== Tests checking parent field by movie type ==="
rg -n "\.parent" tests/ --type ts -B 5 -A 1
# Check if there are specific tests for film vs episode vs season parent behavior
echo -e "\n=== Series/Episode tests and parent expectations ==="
rg -n "parent.*null|parent.*series" tests/ --type tsRepository: bartholomej/node-csfd-api
Length of output: 50381
Restrict parent field population to episodes and seasons only.
The current logic calls getSeasonorEpisodeParent() for all non-'seriál' types, including regular films. The parent field should only be populated for 'epizoda' (episodes) and 'série' (seasons), not for standalone films.
Suggested fix
- parent: (type === 'seriál') ? null : getSeasonorEpisodeParent(el, { id: movieId, name: title }),
+ parent: (type === 'epizoda' || type === 'série') ? getSeasonorEpisodeParent(el, { id: movieId, name: title }) : null,📝 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.
| parent: (type === 'seriál') ? null : getSeasonorEpisodeParent(el, { id: movieId, name: title }), | |
| parent: (type === 'epizoda' || type === 'série') ? getSeasonorEpisodeParent(el, { id: movieId, name: title }) : null, |
🤖 Prompt for AI Agents
In @src/services/movie.service.ts at line 102, The parent field is being set for
all non-'seriál' types; restrict it so parent is only populated when type is
'epizoda' or 'série'. Update the expression that assigns parent (currently
calling getSeasonorEpisodeParent(el, { id: movieId, name: title })) to first
check if type === 'epizoda' || type === 'série' and only then call
getSeasonorEpisodeParent; otherwise set parent to null. Keep references to the
same variables (type, el, movieId, title) and the same helper
getSeasonorEpisodeParent.
What's new and why I made this pull request?
Resolves #43
I've prepared the first version of the parsing for series, seasons, and episodes. Ready for testing – feel free to join in!
Just use
await csfd.movie(1431651)method and there are some brand new properties:Pull request type
Summary by CodeRabbit
New Features
Updates
Tests
Chore
✏️ Tip: You can customize this high-level summary in your review settings.