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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/helpers/global.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,31 @@ export const parseIdFromUrl = (url: string): number => {
return +id || null;
};

/**
* Extracts a numeric ID from a number, string, slug, or full URL.
* Designed for Developer Experience (DX) to allow flexible inputs.
*/
export const extractId = (idOrUrl: number | string): number | null => {
if (typeof idOrUrl === 'number') {
return isNaN(idOrUrl) ? null : idOrUrl;
}

if (typeof idOrUrl === 'string') {
// Pure number string
if (/^\d+$/.test(idOrUrl)) {
return Number(idOrUrl);
}
// Direct slug with ID prefix (e.g. "228329-avatar")
if (/^\d+-/.test(idOrUrl)) {
return +idOrUrl.split('-')[0] || null;
}
// Fallback to URL parsing
return parseIdFromUrl(idOrUrl);
}

return null;
};

export const parseLastIdFromUrl = (url: string): number => {
if (url) {
const idSlug = url?.split('/')[3];
Expand Down
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ export class Csfd {
return this.userReviewsService.userReviews(user, config, opts);
}

public async movie(movie: number, options?: CSFDOptions): Promise<CSFDMovie> {
public async movie(movie: number | string, options?: CSFDOptions): Promise<CSFDMovie> {
const opts = options ?? this.defaultOptions;
return this.movieService.movie(+movie, opts);
return this.movieService.movie(movie, opts);
}

public async creator(creator: number, options?: CSFDOptions): Promise<CSFDCreator> {
public async creator(creator: number | string, options?: CSFDOptions): Promise<CSFDCreator> {
const opts = options ?? this.defaultOptions;
return this.creatorService.creator(+creator, opts);
return this.creatorService.creator(creator, opts);
}

public async search(text: string, options?: CSFDOptions): Promise<CSFDSearch> {
Expand Down
7 changes: 4 additions & 3 deletions src/services/creator.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HTMLElement, parse } from 'node-html-parser';
import { CSFDCreator } from '../dto/creator';
import { fetchPage } from '../fetchers';
import { extractId } from '../helpers/global.helper';
import {
getCreatorBio,
getCreatorBirthdayInfo,
Expand All @@ -12,9 +13,9 @@ import { CSFDOptions } from '../types';
import { creatorUrl } from '../vars';

export class CreatorScraper {
public async creator(creatorId: number, options?: CSFDOptions): Promise<CSFDCreator> {
const id = Number(creatorId);
if (isNaN(id)) {
public async creator(creatorId: number | string, options?: CSFDOptions): Promise<CSFDCreator> {
const id = extractId(creatorId);
if (id === null || isNaN(id)) {
throw new Error('node-csfd-api: creatorId must be a valid number');
}
const url = creatorUrl(id, { language: options?.language });
Expand Down
9 changes: 5 additions & 4 deletions src/services/movie.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { HTMLElement, parse } from 'node-html-parser';
import { CSFDFilmTypes } from '../dto/global';
import { CSFDMovie, MovieJsonLd } from '../dto/movie';
import { fetchPage } from '../fetchers';
import { extractId } from '../helpers/global.helper';
import {
detectSeasonOrEpisodeListType,
getEpisodeCode,
Expand Down Expand Up @@ -32,9 +33,9 @@ import { CSFDOptions } from '../types';
import { LIB_PREFIX, movieUrl } from '../vars';

export class MovieScraper {
public async movie(movieId: number, options?: CSFDOptions): Promise<CSFDMovie> {
const id = Number(movieId);
if (isNaN(id)) {
public async movie(movieId: number | string, options?: CSFDOptions): Promise<CSFDMovie> {
const id = extractId(movieId);
if (id === null || isNaN(id)) {
throw new Error('node-csfd-api: movieId must be a valid number');
}
const url = movieUrl(id, { language: options?.language });
Expand All @@ -52,7 +53,7 @@ export class MovieScraper {
} catch (e) {
console.error(LIB_PREFIX + ' Error parsing JSON-LD', e);
}
return this.buildMovie(+movieId, movieHtml, movieNode as HTMLElement, asideNode as HTMLElement, pageClasses, jsonLd, options);
return this.buildMovie(id, movieHtml, movieNode as HTMLElement, asideNode as HTMLElement, pageClasses, jsonLd, options);
}

private buildMovie(
Expand Down
20 changes: 19 additions & 1 deletion tests/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest';
import { addProtocol, parseColor, parseIdFromUrl } from '../src/helpers/global.helper';
import { addProtocol, extractId, parseColor, parseIdFromUrl } from '../src/helpers/global.helper';

describe('Add protocol', () => {
test('Handle without protocol', () => {
Expand Down Expand Up @@ -49,6 +49,24 @@ describe('Parse Id', () => {
});
});

describe('extractId', () => {
test('Handle numeric ID', () => {
expect(extractId(228329)).toBe(228329);
});
test('Handle numeric string', () => {
expect(extractId('228329')).toBe(228329);
});
test('Handle slug', () => {
expect(extractId('228329-avatar')).toBe(228329);
});
test('Handle full URL', () => {
expect(extractId('https://www.csfd.cz/film/228329-avatar/')).toBe(228329);
});
test('Handle invalid strings', () => {
expect(extractId('invalid-string')).toBe(null);
});
});

describe('Parse color', () => {
test('Red', () => {
const url = parseColor('red');
Expand Down
11 changes: 11 additions & 0 deletions tests/movie.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ describe('Movie Service coverage', () => {
await expect(movieScraper.movie(Number.NaN)).rejects.toThrow('movieId must be a valid number');
});

test('Fetch using slug format', async () => {
const movieScraper = new MovieScraper();
const fetchSpy = vi.spyOn(fetchers, 'fetchPage').mockResolvedValue(movieMock);

// Test slug capability
const movie = await movieScraper.movie('535121-projekt-adam');
expect(movie.id).toBe(535121);

fetchSpy.mockRestore();
});

test('JSON-LD parse fails gracefully', async () => {
const movieScraper = new MovieScraper();

Expand Down
Loading