From d10b07b9c510f658af3f45d788eb89cef9553766 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:55:49 +0000 Subject: [PATCH] feat(dx): add smart parsing for user inputs to allow raw IDs, slugs, and full URLs Co-authored-by: bartholomej <5861310+bartholomej@users.noreply.github.com> --- src/helpers/global.helper.ts | 28 ++++++++++++++++++++++ src/services/user-ratings.service.ts | 11 ++++++--- src/services/user-reviews.service.ts | 11 ++++++--- tests/helpers.test.ts | 35 +++++++++++++++++++++++++++- 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/helpers/global.helper.ts b/src/helpers/global.helper.ts index 6f2acb78..5f625bb4 100644 --- a/src/helpers/global.helper.ts +++ b/src/helpers/global.helper.ts @@ -50,6 +50,34 @@ export const extractId = (idOrUrl: number | string): number | null => { return null; }; +/** + * Extracts a user ID or slug from a number, string, slug, or full URL. + * Designed for Developer Experience (DX) to allow flexible inputs. + */ +export const extractUser = (userOrUrl: number | string): string | number | null => { + if (typeof userOrUrl === 'number') { + return isNaN(userOrUrl) ? null : userOrUrl; + } + + if (typeof userOrUrl === 'string') { + const trimmed = userOrUrl.trim(); + if (!trimmed) return null; + + if (trimmed.includes('/') || trimmed.includes('csfd.')) { + const parts = trimmed.split('/'); + const uzivatelIndex = parts.indexOf('uzivatel'); + if (uzivatelIndex !== -1 && parts[uzivatelIndex + 1]) { + return parts[uzivatelIndex + 1]; + } + return null; + } + + return trimmed; + } + + return null; +}; + export const parseLastIdFromUrl = (url: string): number => { if (url) { const idSlug = url?.split('/')[3]; diff --git a/src/services/user-ratings.service.ts b/src/services/user-ratings.service.ts index c100ca5c..46152217 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 { sleep, extractUser } from '../helpers/global.helper'; import { getUserRating, getUserRatingColorRating, @@ -22,9 +22,14 @@ export class UserRatingsScraper { config?: CSFDUserRatingConfig, options?: CSFDOptions ): Promise { + const extractedUser = extractUser(user); + if (!extractedUser) { + throw new Error('node-csfd-api: user must be a valid user ID or slug'); + } + let allMovies: CSFDUserRatings[] = []; const pageToFetch = config?.page || 1; - const url = userRatingsUrl(user, pageToFetch > 1 ? pageToFetch : undefined, { + const url = userRatingsUrl(extractedUser, pageToFetch > 1 ? pageToFetch : undefined, { language: options?.language }); const response = await fetchPage(url, { ...options?.request }); @@ -40,7 +45,7 @@ export class UserRatingsScraper { if (config?.allPages) { for (let i = 2; i <= pages; i++) { config.onProgress?.(i, pages); - const url = userRatingsUrl(user, i, { language: options?.language }); + const url = userRatingsUrl(extractedUser, i, { language: options?.language }); const response = await fetchPage(url, { ...options?.request }); const items = parse(response); diff --git a/src/services/user-reviews.service.ts b/src/services/user-reviews.service.ts index 95e28a43..93c81a5e 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 { sleep, extractUser } from '../helpers/global.helper'; import { getUserReviewColorRating, getUserReviewDate, @@ -24,9 +24,14 @@ export class UserReviewsScraper { config?: CSFDUserReviewsConfig, options?: CSFDOptions ): Promise { + const extractedUser = extractUser(user); + if (!extractedUser) { + throw new Error('node-csfd-api: user must be a valid user ID or slug'); + } + let allReviews: CSFDUserReviews[] = []; const pageToFetch = config?.page || 1; - const url = userReviewsUrl(user, pageToFetch > 1 ? pageToFetch : undefined, { + const url = userReviewsUrl(extractedUser, pageToFetch > 1 ? pageToFetch : undefined, { language: options?.language }); const response = await fetchPage(url, { ...options?.request }); @@ -42,7 +47,7 @@ export class UserReviewsScraper { if (config?.allPages) { for (let i = 2; i <= pages; i++) { config.onProgress?.(i, pages); - const url = userReviewsUrl(user, i, { language: options?.language }); + const url = userReviewsUrl(extractedUser, i, { language: options?.language }); const response = await fetchPage(url, { ...options?.request }); const items = parse(response); diff --git a/tests/helpers.test.ts b/tests/helpers.test.ts index a479925a..b07ce0df 100644 --- a/tests/helpers.test.ts +++ b/tests/helpers.test.ts @@ -1,5 +1,11 @@ import { describe, expect, test } from 'vitest'; -import { addProtocol, extractId, parseColor, parseIdFromUrl } from '../src/helpers/global.helper'; +import { + addProtocol, + extractId, + extractUser, + parseColor, + parseIdFromUrl +} from '../src/helpers/global.helper'; describe('Add protocol', () => { test('Handle without protocol', () => { @@ -49,6 +55,33 @@ describe('Parse Id', () => { }); }); +describe('extractUser', () => { + test('Handle numeric ID', () => { + expect(extractUser(912)).toBe(912); + }); + test('Handle numeric string', () => { + expect(extractUser('912')).toBe('912'); + }); + test('Handle slug', () => { + expect(extractUser('912-bart')).toBe('912-bart'); + }); + test('Handle text slug', () => { + expect(extractUser('admin')).toBe('admin'); + }); + test('Handle full URL', () => { + expect(extractUser('https://www.csfd.cz/uzivatel/912-bart/')).toBe('912-bart'); + }); + test('Handle full URL with language prefix', () => { + expect(extractUser('https://www.csfd.cz/en/uzivatel/912-bart/hodnoceni/')).toBe('912-bart'); + }); + test('Handle non-user URL', () => { + expect(extractUser('https://www.csfd.cz/film/123/')).toBe(null); + }); + test('Handle invalid strings', () => { + expect(extractUser(' ')).toBe(null); + }); +}); + describe('extractId', () => { test('Handle numeric ID', () => { expect(extractId(228329)).toBe(228329);