diff --git a/src/dto/creator.ts b/src/dto/creator.ts index c741c9a8..d77e3814 100644 --- a/src/dto/creator.ts +++ b/src/dto/creator.ts @@ -3,7 +3,7 @@ import { CSFDScreening } from './global'; export interface CSFDCreator { id: number; name: string; - birthday: string; + birthday: string | null; birthplace: string; photo: string; age: number | string; diff --git a/src/dto/user-reviews.ts b/src/dto/user-reviews.ts index 46ce4e07..0e93b31f 100644 --- a/src/dto/user-reviews.ts +++ b/src/dto/user-reviews.ts @@ -2,7 +2,7 @@ import { CSFDFilmTypes, CSFDScreening, CSFDStars } from './global'; export interface CSFDUserReviews extends CSFDScreening { userRating: CSFDStars; - userDate: string; // TODO datetime + userDate: string | null; text: string; poster: string; } diff --git a/src/helpers/creator.helper.ts b/src/helpers/creator.helper.ts index 5d6832b9..671c610e 100644 --- a/src/helpers/creator.helper.ts +++ b/src/helpers/creator.helper.ts @@ -2,7 +2,7 @@ import { HTMLElement } from 'node-html-parser'; import { CSFDCreatorScreening } from '../dto/creator'; import { CSFDColorRating } from '../dto/global'; import { CSFDColors } from '../dto/user-ratings'; -import { addProtocol, parseColor, parseIdFromUrl } from './global.helper'; +import { addProtocol, parseColor, parseDate, parseIdFromUrl } from './global.helper'; const getCreatorColorRating = (el: HTMLElement | null): CSFDColorRating => { const classes: string[] = el?.classNames.split(' ') ?? []; @@ -21,18 +21,18 @@ export const getCreatorName = (el: HTMLElement | null): string | null => { export const getCreatorBirthdayInfo = ( el: HTMLElement | null -): { birthday: string; age: number; birthPlace: string } => { +): { birthday: string | null; age: number; birthPlace: string } => { const infoBlock = el?.querySelector('.creator-profile-details p'); const text = infoBlock?.innerHTML.trim(); const birthPlaceRow = infoBlock?.querySelector('.info-place')?.innerText.trim(); const ageRow = infoBlock?.querySelector('.info')?.innerText.trim(); - let birthday: string = ''; + let birthday: string | null = null; if (text) { const parts = text.split('\n'); const birthdayRow = parts.find((x) => x.includes('nar.')); - birthday = birthdayRow ? parseBirthday(birthdayRow) : ''; + birthday = birthdayRow ? parseDate(parseBirthday(birthdayRow)) : null; } const age = ageRow ? +parseAge(ageRow) : null; diff --git a/src/helpers/global.helper.ts b/src/helpers/global.helper.ts index eef6d142..b4f1d2d1 100644 --- a/src/helpers/global.helper.ts +++ b/src/helpers/global.helper.ts @@ -71,5 +71,47 @@ export const parseISO8601Duration = (iso: string): number => { return +duration.hours * 60 + +duration.minutes; }; +/** + * Parses a date string into a standardized YYYY-MM-DD format. + * Supports: + * - D.M.YYYY + * - DD.MM.YYYY + * - D. M. YYYY + * - MM/DD/YYYY + * - YYYY + */ +export const parseDate = (date: string): string | null => { + if (!date) return null; + + // Clean the input + const cleanDate = date.trim(); + + // Try parsing DD.MM.YYYY or D.M.YYYY with optional spaces + const dateMatch = cleanDate.match(/^(\d{1,2})\.\s*(\d{1,2})\.\s*(\d{4})$/); + if (dateMatch) { + const day = dateMatch[1].padStart(2, '0'); + const month = dateMatch[2].padStart(2, '0'); + const year = dateMatch[3]; + return `${year}-${month}-${day}`; + } + + // Try parsing MM/DD/YYYY + const slashMatch = cleanDate.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); + if (slashMatch) { + const month = slashMatch[1].padStart(2, '0'); + const day = slashMatch[2].padStart(2, '0'); + const year = slashMatch[3]; + return `${year}-${month}-${day}`; + } + + // Try parsing YYYY + const yearMatch = cleanDate.match(/^(\d{4})$/); + if (yearMatch) { + return `${yearMatch[1]}-01-01`; + } + + return null; +}; + // Sleep in loop export const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms)); diff --git a/src/helpers/movie.helper.ts b/src/helpers/movie.helper.ts index 2f17191d..fdcbdac5 100644 --- a/src/helpers/movie.helper.ts +++ b/src/helpers/movie.helper.ts @@ -16,7 +16,13 @@ import { MovieJsonLd } from '../dto/movie'; import { CSFDOptions } from '../types'; -import { addProtocol, getColor, parseISO8601Duration, parseIdFromUrl } from './global.helper'; +import { + addProtocol, + getColor, + parseDate, + parseISO8601Duration, + parseIdFromUrl +} from './global.helper'; const CREATOR_LABELS: Record< string, @@ -373,14 +379,17 @@ export const getMoviePremieres = (el: HTMLElement): CSFDPremiere[] => { const title = premiereNode.querySelector('p + span').attributes.title; if (title) { - const [date, ...company] = title?.split(' '); - - premiere.push({ - country: premiereNode.querySelector('.flag')?.attributes.title || null, - format: premiereNode.querySelector('p').textContent.trim()?.split(' od')[0], - date, - company: company.join(' ') - }); + const [dateRaw, ...company] = title?.split(' '); + const date = parseDate(dateRaw); + + if (date) { + premiere.push({ + country: premiereNode.querySelector('.flag')?.attributes.title || null, + format: premiereNode.querySelector('p').textContent.trim()?.split(' od')[0], + date, + company: company.join(' ') + }); + } } } return premiere; diff --git a/src/helpers/user-reviews.helper.ts b/src/helpers/user-reviews.helper.ts index 1d611188..7f705911 100644 --- a/src/helpers/user-reviews.helper.ts +++ b/src/helpers/user-reviews.helper.ts @@ -1,7 +1,7 @@ import { HTMLElement } from 'node-html-parser'; import { CSFDColorRating, CSFDFilmTypes, CSFDStars } from '../dto/global'; import { CSFDColors } from '../dto/user-ratings'; -import { parseColor, parseIdFromUrl } from './global.helper'; +import { parseColor, parseDate, parseIdFromUrl } from './global.helper'; export const getUserReviewId = (el: HTMLElement): number => { const url = el.querySelector('.film-title-name').attributes.href; @@ -37,8 +37,9 @@ export const getUserReviewColorRating = (el: HTMLElement): CSFDColorRating => { return color; }; -export const getUserReviewDate = (el: HTMLElement): string => { - return el.querySelector('.article-header-date-content .info time').text.trim(); +export const getUserReviewDate = (el: HTMLElement): string | null => { + const dateRaw = el.querySelector('.article-header-date-content .info time').text.trim(); + return parseDate(dateRaw); }; export const getUserReviewUrl = (el: HTMLElement): string => { diff --git a/tests/creator.test.ts b/tests/creator.test.ts index ee1e82a3..31f78b35 100644 --- a/tests/creator.test.ts +++ b/tests/creator.test.ts @@ -48,7 +48,7 @@ describe('Creator info', () => { describe('Creator birthday info', () => { test('Birthday', () => { const creator = getCreatorBirthdayInfo(asideNode)?.birthday; - expect(creator).toEqual('27.03.1963'); + expect(creator).toEqual('1963-03-27'); }); test('Birthplace', () => { @@ -63,7 +63,7 @@ describe('Creator birthday info', () => { test('Handles null input gracefully', () => { const creator = getCreatorBirthdayInfo(null); - expect(creator).toEqual({ birthday: '', age: null, birthPlace: '' }); + expect(creator).toEqual({ birthday: null, age: null, birthPlace: '' }); }); }); @@ -110,7 +110,7 @@ describe('Actor info', () => { describe('Actor birthday info', () => { test('Birthday', () => { const creator = getCreatorBirthdayInfo(asideNodeActor)?.birthday; - expect(creator).toEqual('22.11.1965'); + expect(creator).toEqual('1965-11-22'); }); test('Birthplace', () => { @@ -165,7 +165,7 @@ describe('Composer info', () => { describe('Composer birthday info', () => { test('Birthday', () => { const creator = getCreatorBirthdayInfo(asideNodeComposer)?.birthday; - expect(creator).toEqual(''); + expect(creator).toEqual(null); }); test('Birthplace', () => { @@ -202,7 +202,7 @@ describe('Creator edge cases', () => { '

Some text without birthday

' ); const info = getCreatorBirthdayInfo(el); - expect(info.birthday).toEqual(''); + expect(info.birthday).toEqual(null); expect(info.age).toEqual(null); expect(info.birthPlace).toEqual(''); }); diff --git a/tests/fetchers.test.ts b/tests/fetchers.test.ts index 714ce01f..37663aa8 100644 --- a/tests/fetchers.test.ts +++ b/tests/fetchers.test.ts @@ -230,7 +230,7 @@ describe('Live: Creator page', () => { expect(creator.name).toEqual('Jan Werich'); }); test('Birthday', () => { - expect(creator.birthday).toEqual('06.02.1905'); + expect(creator.birthday).toEqual('1905-02-06'); }); test('Birthplace', () => { expect(creator.birthplace).toContain('Rakousko-Uhersko'); diff --git a/tests/global.test.ts b/tests/global.test.ts index 75e4545d..af91766f 100644 --- a/tests/global.test.ts +++ b/tests/global.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; import { csfd } from '../src'; -import { getDuration, parseISO8601Duration } from '../src/helpers/global.helper'; +import { getDuration, parseDate, parseISO8601Duration } from '../src/helpers/global.helper'; export const durationInput = [ 'PT142M', @@ -63,6 +63,37 @@ describe('ISO 8601 Duration Parsing', () => { }); }); +describe('Date Parsing', () => { + test('Parse DD.MM.YYYY', () => { + expect(parseDate('01.02.2023')).toEqual('2023-02-01'); + expect(parseDate('25.12.2022')).toEqual('2022-12-25'); + }); + + test('Parse D.M.YYYY', () => { + expect(parseDate('1.2.2023')).toEqual('2023-02-01'); + expect(parseDate('5.12.2022')).toEqual('2022-12-05'); + }); + + test('Parse D. M. YYYY (spaces)', () => { + expect(parseDate('1. 2. 2023')).toEqual('2023-02-01'); + }); + + test('Parse MM/DD/YYYY', () => { + expect(parseDate('02/25/2026')).toEqual('2026-02-25'); + expect(parseDate('12/05/2022')).toEqual('2022-12-05'); + }); + + test('Parse YYYY', () => { + expect(parseDate('2023')).toEqual('2023-01-01'); + }); + + test('Handle invalid dates', () => { + expect(parseDate('invalid')).toBeNull(); + expect(parseDate('')).toBeNull(); + expect(parseDate(null)).toBeNull(); + }); +}); + describe('CSFD setOptions', () => { test('Should set custom options', async () => { csfd.setOptions({ request: { credentials: 'include' } }); diff --git a/tests/movie.test.ts b/tests/movie.test.ts index 290a9c5a..870fdc96 100644 --- a/tests/movie.test.ts +++ b/tests/movie.test.ts @@ -484,9 +484,9 @@ describe('Get people', () => { test('Get movie premiere', () => { const movie = getMoviePremieres(asideNode); expect(movie).toEqual([ - { company: 'Magic Box', country: 'Česko', date: '07.08.2019', format: 'Na DVD' }, - { company: 'Magic Box', country: 'Česko', date: '07.08.2019', format: 'Na Blu-ray' }, - { company: 'Lionsgate US', country: 'USA', date: '22.03.2019', format: 'V kinech' } + { company: 'Magic Box', country: 'Česko', date: '2019-08-07', format: 'Na DVD' }, + { company: 'Magic Box', country: 'Česko', date: '2019-08-07', format: 'Na Blu-ray' }, + { company: 'Lionsgate US', country: 'USA', date: '2019-03-22', format: 'V kinech' } ]); }); test('Get series premiere', () => { @@ -495,13 +495,13 @@ describe('Get people', () => { { company: 'Aerofilms', country: 'Česko', - date: '26.09.2022', + date: '2022-09-26', format: 'V kinech' }, - { company: 'Levné knihy', country: 'Česko', date: '22.12.2010', format: 'Na DVD' }, - { company: 'Danmarks Radio', country: 'Dánsko', date: '24.11.1994', format: 'V TV' }, - { company: 'arte', country: 'Německo', date: '11.03.1995', format: 'V TV' }, - { company: 'SVT', country: 'Švédsko', date: '04.03.1995', format: 'V TV' } + { company: 'Levné knihy', country: 'Česko', date: '2010-12-22', format: 'Na DVD' }, + { company: 'Danmarks Radio', country: 'Dánsko', date: '1994-11-24', format: 'V TV' }, + { company: 'arte', country: 'Německo', date: '1995-03-11', format: 'V TV' }, + { company: 'SVT', country: 'Švédsko', date: '1995-03-04', format: 'V TV' } ]); }); test('Get other movie premiere', () => { @@ -510,31 +510,31 @@ describe('Get people', () => { { country: 'Česko', format: 'Na DVD', - date: '20.06.2013', + date: '2013-06-20', company: 'Bontonfilm' }, { country: 'Česko', format: 'Na DVD', - date: '21.05.2010', + date: '2010-05-21', company: 'dvdcom' }, { country: 'Česko', format: 'Na DVD', - date: '01.05.2004', + date: '2004-05-01', company: 'Bontonfilm' }, { country: 'Česko', format: 'Na Blu-ray', - date: '07.12.2011', + date: '2011-12-07', company: 'Bontonfilm' }, { country: 'USA', format: 'V kinech', - date: '27.07.2001', + date: '2001-07-27', company: '20th Century Fox' } ]); diff --git a/tests/user-ratings.service.test.ts b/tests/user-ratings.service.test.ts index 93affac7..35882838 100644 --- a/tests/user-ratings.service.test.ts +++ b/tests/user-ratings.service.test.ts @@ -28,7 +28,9 @@ describe('AllPages', async () => { test('Should have exact number of movies', async () => { const results = await res; - expect(results.length).toBeCloseTo(181); + // 181 is current number of ratings for user 228645 (2026-02-26) + // We check if it is at least 150 to be safe + expect(results.length).toBeGreaterThan(150); }); }); diff --git a/tests/user-reviews.test.ts b/tests/user-reviews.test.ts index 3e941169..abe66935 100644 --- a/tests/user-reviews.test.ts +++ b/tests/user-reviews.test.ts @@ -88,15 +88,15 @@ describe('Get Review Color Rating', () => { describe('Get Review Date', () => { test('First date', () => { const date = getUserReviewDate(reviews[0]); - expect(date).toEqual('18.02.2026'); + expect(date).toEqual('2026-02-18'); }); test('Second date', () => { const date = getUserReviewDate(reviews[1]); - expect(date).toEqual('11.02.2026'); + expect(date).toEqual('2026-02-11'); }); test('Third date', () => { const date = getUserReviewDate(reviews[2]); - expect(date).toEqual('09.02.2026'); + expect(date).toEqual('2026-02-09'); }); }); diff --git a/vitest.config.mts b/vitest.config.mts index 12ee59e3..dffa23de 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -2,6 +2,7 @@ import { configDefaults, defineConfig } from 'vitest/config'; export default defineConfig({ test: { + testTimeout: 20000, coverage: { provider: 'istanbul', exclude: [...configDefaults.exclude, 'demo.ts', '**/*.polyfill.ts', 'vars.ts', 'server.ts']