diff --git a/src/helpers/global.helper.ts b/src/helpers/global.helper.ts index 6f2acb7..df4b0e0 100644 --- a/src/helpers/global.helper.ts +++ b/src/helpers/global.helper.ts @@ -50,6 +50,50 @@ export const extractId = (idOrUrl: number | string): number | null => { return null; }; +/** + * Extracts a user ID or text slug from a number, string, slug, or full URL. + * Designed for Developer Experience (DX) to allow flexible inputs for user endpoints. + */ +export const extractUser = (userOrUrl: number | string): number | string | null => { + if (typeof userOrUrl === 'number') { + return isNaN(userOrUrl) ? null : userOrUrl; + } + + if (typeof userOrUrl === 'string') { + // Pure number string + if (/^\d+$/.test(userOrUrl)) { + return Number(userOrUrl); + } + + // Check if it's a URL + if (userOrUrl.includes('/')) { + const parts = userOrUrl.split('/').filter(Boolean); + const uzivatelIndex = parts.indexOf('uzivatel'); + if (uzivatelIndex !== -1 && parts.length > uzivatelIndex + 1) { + const userSlug = parts[uzivatelIndex + 1]; + // If slug is numeric, return number, else return slug string + if (/^\d+-/.test(userSlug)) { + return +userSlug.split('-')[0] || null; + } + if (/^\d+$/.test(userSlug)) { + return +userSlug || null; + } + return userSlug; + } + } + + // Direct numeric slug with ID prefix (e.g. "912-hobit") + if (/^\d+-/.test(userOrUrl)) { + return +userOrUrl.split('-')[0] || null; + } + + // Return string slug as fallback (e.g. "admin") + return userOrUrl; + } + + 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 09244fe..5f00cea 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 { extractUser, sleep } from '../helpers/global.helper'; import { getUserRating, getUserRatingColorRating, @@ -22,9 +22,14 @@ export class UserRatingsScraper { config?: CSFDUserRatingConfig, options?: CSFDOptions ): Promise { + const userIdOrSlug = extractUser(user); + if (userIdOrSlug === null || (typeof userIdOrSlug === 'number' && isNaN(userIdOrSlug))) { + throw new Error('node-csfd-api: user must be a valid number, slug, or url'); + } + let allMovies: CSFDUserRatings[] = []; const pageToFetch = config?.page || 1; - const url = userRatingsUrl(user, pageToFetch > 1 ? pageToFetch : undefined, { + const url = userRatingsUrl(userIdOrSlug, 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(userIdOrSlug, 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 88f5f49..eb00971 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 { extractUser, sleep } from '../helpers/global.helper'; import { getUserReviewColorRating, getUserReviewDate, @@ -24,9 +24,14 @@ export class UserReviewsScraper { config?: CSFDUserReviewsConfig, options?: CSFDOptions ): Promise { + const userIdOrSlug = extractUser(user); + if (userIdOrSlug === null || (typeof userIdOrSlug === 'number' && isNaN(userIdOrSlug))) { + throw new Error('node-csfd-api: user must be a valid number, slug, or url'); + } + let allReviews: CSFDUserReviews[] = []; const pageToFetch = config?.page || 1; - const url = userReviewsUrl(user, pageToFetch > 1 ? pageToFetch : undefined, { + const url = userReviewsUrl(userIdOrSlug, 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(userIdOrSlug, 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 a479925..2b75474 100644 --- a/tests/helpers.test.ts +++ b/tests/helpers.test.ts @@ -1,5 +1,5 @@ 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', () => { @@ -67,6 +67,27 @@ describe('extractId', () => { }); }); +describe('extractUser', () => { + test('should return number for numeric input', () => { + expect(extractUser(912)).toBe(912); + }); + test('should return number for numeric string input', () => { + expect(extractUser('912')).toBe(912); + }); + test('should return number for slug input', () => { + expect(extractUser('912-hobit')).toBe(912); + }); + test('should return number for full url with numeric slug', () => { + expect(extractUser('https://www.csfd.cz/uzivatel/912-hobit/')).toBe(912); + }); + test('should return string for text slug', () => { + expect(extractUser('admin')).toBe('admin'); + }); + test('should return string for full url with text slug', () => { + expect(extractUser('https://www.csfd.cz/uzivatel/admin/')).toBe('admin'); + }); +}); + describe('Parse color', () => { test('Red', () => { const url = parseColor('red');