diff --git a/src/helpers/global.helper.ts b/src/helpers/global.helper.ts index bfb27f8e..e9ac27cb 100644 --- a/src/helpers/global.helper.ts +++ b/src/helpers/global.helper.ts @@ -145,3 +145,29 @@ export const parseDate = (date: string): string | null => { // Sleep in loop export const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms)); + +export const extractId = (id: number | string): number | null => { + if (typeof id === 'number') { + return id; + } + + if (typeof id !== 'string' || !id.trim()) { + return null; + } + + const str = id.trim(); + + if (/^\d+$/.test(str)) { + return +str; + } + + if (str.includes('/')) { + return parseIdFromUrl(str); + } + + if (/^\d+-/.test(str)) { + return +str.split('-')[0] || null; + } + + return null; +}; diff --git a/src/index.ts b/src/index.ts index b672d235..8c81e62d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,14 +54,14 @@ export class Csfd { return this.userReviewsService.userReviews(user, config, opts); } - public async movie(movie: number, options?: CSFDOptions): Promise { + public async movie(movie: number | string, options?: CSFDOptions): Promise { 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 { + public async creator(creator: number | string, options?: CSFDOptions): Promise { 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 { @@ -96,4 +96,3 @@ export const csfd = new Csfd( ); export type * from './dto'; - diff --git a/src/services/creator.service.ts b/src/services/creator.service.ts index b33be6ea..44057126 100644 --- a/src/services/creator.service.ts +++ b/src/services/creator.service.ts @@ -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, @@ -12,10 +13,10 @@ import { CSFDOptions } from '../types'; import { creatorUrl } from '../vars'; export class CreatorScraper { - public async creator(creatorId: number, options?: CSFDOptions): Promise { - const id = Number(creatorId); - if (isNaN(id)) { - throw new Error('node-csfd-api: creatorId must be a valid number'); + public async creator(creatorId: number | string, options?: CSFDOptions): Promise { + const id = extractId(creatorId); + if (!id) { + throw new Error('node-csfd-api: creatorId must be a valid number, string ID, or CSFD URL'); } const url = creatorUrl(id, { language: options?.language }); const response = await fetchPage(url, { ...options?.request }); @@ -24,7 +25,7 @@ export class CreatorScraper { const asideNode = creatorHtml.querySelector('.creator-about'); const filmsNode = creatorHtml.querySelector('.creator-filmography'); - return this.buildCreator(+creatorId, asideNode, filmsNode); + return this.buildCreator(id, asideNode, filmsNode); } private buildCreator(id: number, asideEl: HTMLElement, filmsNode: HTMLElement): CSFDCreator { diff --git a/src/services/movie.service.ts b/src/services/movie.service.ts index bb145106..a068f22f 100644 --- a/src/services/movie.service.ts +++ b/src/services/movie.service.ts @@ -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, @@ -32,10 +33,10 @@ import { CSFDOptions } from '../types'; import { LIB_PREFIX, movieUrl } from '../vars'; export class MovieScraper { - public async movie(movieId: number, options?: CSFDOptions): Promise { - const id = Number(movieId); - if (isNaN(id)) { - throw new Error('node-csfd-api: movieId must be a valid number'); + public async movie(movieId: number | string, options?: CSFDOptions): Promise { + const id = extractId(movieId); + if (!id) { + throw new Error('node-csfd-api: movieId must be a valid number, string ID, or CSFD URL'); } const url = movieUrl(id, { language: options?.language }); const response = await fetchPage(url, { ...options?.request }); @@ -52,7 +53,15 @@ 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( diff --git a/src/services/user-ratings.service.ts b/src/services/user-ratings.service.ts index 09244fe8..08c8d180 100644 --- a/src/services/user-ratings.service.ts +++ b/src/services/user-ratings.service.ts @@ -2,7 +2,7 @@ import { HTMLElement, parse } from 'node-html-parser'; import { CSFDColorRating, CSFDFilmTypes, CSFDStars } from '../dto/global'; import { CSFDUserRatingConfig, CSFDUserRatings } from '../dto/user-ratings'; import { fetchPage } from '../fetchers'; -import { sleep } from '../helpers/global.helper'; +import { extractId, sleep } from '../helpers/global.helper'; import { getUserRating, getUserRatingColorRating, @@ -22,9 +22,14 @@ export class UserRatingsScraper { config?: CSFDUserRatingConfig, options?: CSFDOptions ): Promise { + const userId = extractId(user); + if (!userId) { + throw new Error('node-csfd-api: user must be a valid number, string ID, or CSFD URL'); + } + let allMovies: CSFDUserRatings[] = []; const pageToFetch = config?.page || 1; - const url = userRatingsUrl(user, pageToFetch > 1 ? pageToFetch : undefined, { + const url = userRatingsUrl(userId, pageToFetch > 1 ? pageToFetch : undefined, { language: options?.language }); const response = await fetchPage(url, { ...options?.request }); @@ -62,7 +67,10 @@ export class UserRatingsScraper { const films: CSFDUserRatings[] = []; if (config) { if (config.includesOnly?.length && config.excludes?.length) { - console.warn(`${LIB_PREFIX} Both 'includesOnly' and 'excludes' were provided. 'includesOnly' takes precedence:`, config.includesOnly); + console.warn( + `${LIB_PREFIX} Both 'includesOnly' and 'excludes' were provided. 'includesOnly' takes precedence:`, + config.includesOnly + ); } } diff --git a/src/services/user-reviews.service.ts b/src/services/user-reviews.service.ts index 88f5f498..656f2828 100644 --- a/src/services/user-reviews.service.ts +++ b/src/services/user-reviews.service.ts @@ -2,7 +2,7 @@ import { HTMLElement, parse } from 'node-html-parser'; import { CSFDColorRating, CSFDFilmTypes, CSFDStars } from '../dto/global'; import { CSFDUserReviews, CSFDUserReviewsConfig } from '../dto/user-reviews'; import { fetchPage } from '../fetchers'; -import { sleep } from '../helpers/global.helper'; +import { extractId, sleep } from '../helpers/global.helper'; import { getUserReviewColorRating, getUserReviewDate, @@ -24,9 +24,14 @@ export class UserReviewsScraper { config?: CSFDUserReviewsConfig, options?: CSFDOptions ): Promise { + const userId = extractId(user); + if (!userId) { + throw new Error('node-csfd-api: user must be a valid number, string ID, or CSFD URL'); + } + let allReviews: CSFDUserReviews[] = []; const pageToFetch = config?.page || 1; - const url = userReviewsUrl(user, pageToFetch > 1 ? pageToFetch : undefined, { + const url = userReviewsUrl(userId, pageToFetch > 1 ? pageToFetch : undefined, { language: options?.language }); const response = await fetchPage(url, { ...options?.request }); @@ -64,7 +69,10 @@ export class UserReviewsScraper { const films: CSFDUserReviews[] = []; if (config) { if (config.includesOnly?.length && config.excludes?.length) { - console.warn(`${LIB_PREFIX} Both 'includesOnly' and 'excludes' were provided. 'includesOnly' takes precedence:`, config.includesOnly); + console.warn( + `${LIB_PREFIX} Both 'includesOnly' and 'excludes' were provided. 'includesOnly' takes precedence:`, + config.includesOnly + ); } } diff --git a/tests/helpers.test.ts b/tests/helpers.test.ts index 12a5d959..c3cb499e 100644 --- a/tests/helpers.test.ts +++ b/tests/helpers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { addProtocol, parseColor, parseIdFromUrl } from '../src/helpers/global.helper'; +import { addProtocol, parseColor, parseIdFromUrl, extractId } from '../src/helpers/global.helper'; describe('Add protocol', () => { test('Handle without protocol', () => { @@ -49,6 +49,41 @@ describe('Parse Id', () => { }); }); +describe('Extract Id', () => { + test('Handle plain numbers', () => { + const id = extractId(535121); + expect(id).toBe(535121); + }); + test('Handle numeric strings', () => { + const id = extractId('535121'); + expect(id).toBe(535121); + }); + test('Handle URL slugs', () => { + const id = extractId('535121-blade-runner-2049'); + expect(id).toBe(535121); + }); + test('Handle full CSFD URLs', () => { + const id = extractId('https://www.csfd.cz/film/535121-blade-runner-2049/prehled/'); + expect(id).toBe(535121); + }); + test('Handle CSFD URLs path only', () => { + const id = extractId('/film/535121-blade-runner-2049/prehled/'); + expect(id).toBe(535121); + }); + test('Handle non-id slug starting with numbers', () => { + const id = extractId('3d-printers'); + expect(id).toBe(null); + }); + test('Handle bad strings', () => { + const id = extractId('bad string'); + expect(id).toBe(null); + }); + test('Handle null/undefined correctly via TS bypass', () => { + const id = extractId(null as any); + expect(id).toBe(null); + }); +}); + describe('Parse color', () => { test('Red', () => { const url = parseColor('red');