node-csfd-api-racintom 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +13 -0
- package/.eslintrc.json +33 -0
- package/.gitattributes +2 -0
- package/.github/FUNDING.yml +8 -0
- package/.github/pull_request_template.md +19 -0
- package/.github/workflows/main.yml +40 -0
- package/.github/workflows/publish.yml +73 -0
- package/.github/workflows/test.yml +43 -0
- package/.husky/pre-commit +1 -0
- package/.idea/codeStyles/Project.xml +72 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/misc.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/node-csfd-api.iml +9 -0
- package/.idea/prettier.xml +6 -0
- package/.idea/vcs.xml +7 -0
- package/.nvmrc +1 -0
- package/.prettierignore +8 -0
- package/.prettierrc +10 -0
- package/.vscode/settings.json +16 -0
- package/Dockerfile +19 -0
- package/README.md +510 -0
- package/demo.ts +35 -0
- package/eslint.config.mjs +55 -0
- package/package.json +86 -0
- package/server.ts +66 -0
- package/src/fetchers/fetch.polyfill.ts +7 -0
- package/src/fetchers/index.ts +25 -0
- package/src/helpers/creator.helper.ts +95 -0
- package/src/helpers/global.helper.ts +70 -0
- package/src/helpers/movie.helper.ts +276 -0
- package/src/helpers/search-user.helper.ts +19 -0
- package/src/helpers/search.helper.ts +66 -0
- package/src/helpers/user-ratings.helper.ts +62 -0
- package/src/index.ts +42 -0
- package/src/interfaces/creator.interface.ts +14 -0
- package/src/interfaces/global.ts +36 -0
- package/src/interfaces/movie.interface.ts +157 -0
- package/src/interfaces/search.interface.ts +32 -0
- package/src/interfaces/user-ratings.interface.ts +21 -0
- package/src/services/creator.service.ts +34 -0
- package/src/services/movie.service.ts +89 -0
- package/src/services/search.service.ts +101 -0
- package/src/services/user-ratings.service.ts +106 -0
- package/src/vars.ts +13 -0
- package/tests/creator.test.ts +182 -0
- package/tests/fetchers.test.ts +109 -0
- package/tests/global.test.ts +35 -0
- package/tests/helpers.test.ts +59 -0
- package/tests/mocks/creator-actor.html.ts +2244 -0
- package/tests/mocks/creator-composer-empty.html.ts +683 -0
- package/tests/mocks/creator-director.html.ts +3407 -0
- package/tests/mocks/movie1.html.ts +1430 -0
- package/tests/mocks/movie2.html.ts +740 -0
- package/tests/mocks/movie3.html.ts +1843 -0
- package/tests/mocks/movie4.html.ts +1568 -0
- package/tests/mocks/search.html.ts +838 -0
- package/tests/mocks/series1.html.ts +1540 -0
- package/tests/mocks/userRatings.html.ts +1354 -0
- package/tests/movie.test.ts +606 -0
- package/tests/search.test.ts +379 -0
- package/tests/services.test.ts +106 -0
- package/tests/user-ratings.test.ts +142 -0
- package/tests/vars.test.ts +34 -0
- package/tsconfig.json +23 -0
- package/vitest.config.mts +10 -0
package/server.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import packageJson from './package.json';
|
|
3
|
+
import { csfd } from './src';
|
|
4
|
+
import { CSFDFilmTypes } from './src/interfaces/global';
|
|
5
|
+
|
|
6
|
+
const app = express();
|
|
7
|
+
const port = process.env.PORT || 3000;
|
|
8
|
+
|
|
9
|
+
app.get('/', (_, res) => {
|
|
10
|
+
res.json({
|
|
11
|
+
name: packageJson.name,
|
|
12
|
+
version: packageJson.version,
|
|
13
|
+
docs: packageJson.homepage,
|
|
14
|
+
links: ['/movie/:id', '/creator/:id', '/search/:query', '/user-ratings/:id']
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
app.get(['/movie/', '/creator/', '/search/', '/user-ratings/'], (req, res) => {
|
|
19
|
+
res.json({ error: `ID is missing. Provide ID like this: ${req.url}${req.url.endsWith('/') ? '' : '/'}1234` });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
app.get('/movie/:id', async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const movie = await csfd.movie(+req.params.id);
|
|
25
|
+
res.json(movie);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
res.status(500).json({ error: 'Failed to fetch movie data' });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
app.get('/creator/:id', async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const result = await csfd.creator(+req.params.id);
|
|
34
|
+
res.json(result);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
res.status(500).json({ error: 'Failed to fetch creator data: ' + error });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
app.get('/search/:query', async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const result = await csfd.search(req.params.query);
|
|
43
|
+
res.json(result);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
res.status(500).json({ error: 'Failed to fetch search data: ' + error });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
app.get('/user-ratings/:id', async (req, res) => {
|
|
50
|
+
const { allPages, allPagesDelay, excludes, includesOnly } = req.query;
|
|
51
|
+
try {
|
|
52
|
+
const result = await csfd.userRatings(req.params.id, {
|
|
53
|
+
allPages: allPages === 'true',
|
|
54
|
+
allPagesDelay: allPagesDelay ? +allPagesDelay : undefined,
|
|
55
|
+
excludes: excludes ? (excludes as string).split(',') as CSFDFilmTypes[] : undefined,
|
|
56
|
+
includesOnly: includesOnly ? (includesOnly as string).split(',') as CSFDFilmTypes[] : undefined
|
|
57
|
+
});
|
|
58
|
+
res.json(result);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
res.status(500).json({ error: 'Failed to fetch user-ratings data: ' + error });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
app.listen(port, () => {
|
|
65
|
+
console.log(`API is running on http://localhost:${port}`);
|
|
66
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Check if `fetch` is available in global scope (nodejs 18+) or in window (browser). If not, use cross-fetch polyfill.
|
|
2
|
+
import { fetch as crossFetch } from 'cross-fetch';
|
|
3
|
+
export const fetchSafe =
|
|
4
|
+
(typeof fetch === 'function' && fetch) || // ServiceWorker fetch (Cloud Functions + Chrome extension)
|
|
5
|
+
(typeof global === 'object' && global.fetch) || // Node.js 18+ fetch
|
|
6
|
+
(typeof window !== 'undefined' && window.fetch) || // Browser fetch
|
|
7
|
+
crossFetch; // Polyfill fetch
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { fetchSafe } from './fetch.polyfill';
|
|
2
|
+
|
|
3
|
+
const USER_AGENTS = [
|
|
4
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
|
|
5
|
+
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/87.0.4280.77 Mobile/15E148 Safari/604.1',
|
|
6
|
+
'Mozilla/5.0 (Linux; Android 10; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Mobile Safari/537.36',
|
|
7
|
+
'Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Mobile Safari/537.36'
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const headers = {
|
|
11
|
+
'User-Agent': USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)]
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const fetchPage = async (url: string): Promise<string> => {
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetchSafe(url, { headers });
|
|
17
|
+
if (response.status >= 400 && response.status < 600) {
|
|
18
|
+
throw new Error(`node-csfd-api: Bad response ${response.status} for url: ${url}`);
|
|
19
|
+
}
|
|
20
|
+
return await response.text();
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error(e);
|
|
23
|
+
return 'Error';
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { HTMLElement } from 'node-html-parser';
|
|
2
|
+
import { CSFDCreatorScreening } from '../interfaces/creator.interface';
|
|
3
|
+
import { CSFDColorRating } from '../interfaces/global';
|
|
4
|
+
import { Colors } from '../interfaces/user-ratings.interface';
|
|
5
|
+
import { addProtocol, parseColor, parseIdFromUrl } from './global.helper';
|
|
6
|
+
|
|
7
|
+
export const getColorRating = (el: HTMLElement): CSFDColorRating => {
|
|
8
|
+
return parseColor(el?.classNames.split(' ').pop() as Colors);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getId = (url: string): number => {
|
|
12
|
+
if (url) {
|
|
13
|
+
return parseIdFromUrl(url);
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getName = (el: HTMLElement | null): string => {
|
|
19
|
+
return el.querySelector('h1').innerText.trim();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const getBirthdayInfo = (
|
|
23
|
+
el: HTMLElement | null
|
|
24
|
+
): { birthday: string; age: number; birthPlace: string } => {
|
|
25
|
+
const infoBlock = el.querySelector('h1 + p');
|
|
26
|
+
const text = infoBlock?.innerHTML.trim();
|
|
27
|
+
|
|
28
|
+
const birthPlaceRow = infoBlock?.querySelector('.info-place')?.innerHTML.trim();
|
|
29
|
+
const ageRow = infoBlock?.querySelector('.info')?.innerHTML.trim();
|
|
30
|
+
|
|
31
|
+
let birthday: string = '';
|
|
32
|
+
|
|
33
|
+
if (text) {
|
|
34
|
+
const parts = text.split('\n');
|
|
35
|
+
const birthdayRow = parts.find((x) => x.includes('nar.'));
|
|
36
|
+
birthday = birthdayRow ? parseBirthday(birthdayRow) : '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const age = ageRow ? +parseAge(ageRow) : null;
|
|
40
|
+
const birthPlace = birthPlaceRow ? parseBirthPlace(birthPlaceRow) : '';
|
|
41
|
+
|
|
42
|
+
return { birthday, age, birthPlace };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const getBio = (el: HTMLElement | null): string => {
|
|
46
|
+
return el.querySelector('.article-content p')?.text.trim().split('\n')[0].trim() || null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const getPhoto = (el: HTMLElement | null): string => {
|
|
50
|
+
const image = el.querySelector('img').attributes.src;
|
|
51
|
+
return addProtocol(image);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const parseBirthday = (text: string): any => {
|
|
55
|
+
return text.replace(/nar./g, '').trim();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const parseAge = (text: string): any => {
|
|
59
|
+
return text.trim().replace(/\(/g, '').replace(/let\)/g, '').trim();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const parseBirthPlace = (text: string): any => {
|
|
63
|
+
return text.trim().replace(/<br>/g, '').trim();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const getFilms = (el: HTMLElement | null): CSFDCreatorScreening[] => {
|
|
67
|
+
const filmNodes = el.querySelectorAll('.box')[0]?.querySelectorAll('table tr');
|
|
68
|
+
let yearCache: number;
|
|
69
|
+
const films = filmNodes.map((filmNode) => {
|
|
70
|
+
const id = getId(filmNode.querySelector('td.name .film-title-name')?.attributes.href);
|
|
71
|
+
const title = filmNode.querySelector('.name')?.text.trim();
|
|
72
|
+
const year = +filmNode.querySelector('.year')?.text.trim();
|
|
73
|
+
const colorRating = getColorRating(filmNode.querySelector('.name .icon'));
|
|
74
|
+
|
|
75
|
+
// Cache year from previous film because there is a gap between movies with same year
|
|
76
|
+
if (year) {
|
|
77
|
+
yearCache = +year;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (id && title) {
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
title,
|
|
84
|
+
year: year || yearCache,
|
|
85
|
+
colorRating
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {};
|
|
89
|
+
});
|
|
90
|
+
// Remove empty objects
|
|
91
|
+
const filmsUnique = films.filter(
|
|
92
|
+
(value) => Object.keys(value).length !== 0
|
|
93
|
+
) as CSFDCreatorScreening[];
|
|
94
|
+
return filmsUnique;
|
|
95
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CSFDColorRating } from '../interfaces/global';
|
|
2
|
+
import { Colors } from '../interfaces/user-ratings.interface';
|
|
3
|
+
|
|
4
|
+
export const parseIdFromUrl = (url: string): number => {
|
|
5
|
+
if (url) {
|
|
6
|
+
const idSlug = url?.split('/')[2];
|
|
7
|
+
const id = idSlug?.split('-')[0];
|
|
8
|
+
return +id || null;
|
|
9
|
+
} else {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const getColor = (cls: string): CSFDColorRating => {
|
|
15
|
+
switch (cls) {
|
|
16
|
+
case 'page-lightgrey':
|
|
17
|
+
return 'unknown';
|
|
18
|
+
case 'page-red':
|
|
19
|
+
return 'good';
|
|
20
|
+
case 'page-blue':
|
|
21
|
+
return 'average';
|
|
22
|
+
case 'page-grey':
|
|
23
|
+
return 'bad';
|
|
24
|
+
default:
|
|
25
|
+
return 'unknown';
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const parseColor = (quality: Colors): CSFDColorRating => {
|
|
30
|
+
switch (quality) {
|
|
31
|
+
case 'lightgrey':
|
|
32
|
+
return 'unknown';
|
|
33
|
+
case 'red':
|
|
34
|
+
return 'good';
|
|
35
|
+
case 'blue':
|
|
36
|
+
return 'average';
|
|
37
|
+
case 'grey':
|
|
38
|
+
return 'bad';
|
|
39
|
+
default:
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const addProtocol = (url: string): string => {
|
|
45
|
+
return url.startsWith('//') ? 'https:' + url : url;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const getDuration = (matches: any[]) => {
|
|
49
|
+
return {
|
|
50
|
+
sign: matches[1] === undefined ? '+' : '-',
|
|
51
|
+
years: matches[2] === undefined ? 0 : matches[2],
|
|
52
|
+
months: matches[3] === undefined ? 0 : matches[3],
|
|
53
|
+
weeks: matches[4] === undefined ? 0 : matches[4],
|
|
54
|
+
days: matches[5] === undefined ? 0 : matches[5],
|
|
55
|
+
hours: matches[6] === undefined ? 0 : matches[6],
|
|
56
|
+
minutes: matches[7] === undefined ? 0 : matches[7],
|
|
57
|
+
seconds: matches[8] === undefined ? 0 : matches[8]
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const parseISO8601Duration = (iso: string): number => {
|
|
62
|
+
const iso8601DurationRegex =
|
|
63
|
+
/(-)?P(?:([.,\d]+)Y)?(?:([.,\d]+)M)?(?:([.,\d]+)W)?(?:([.,\d]+)D)?T(?:([.,\d]+)H)?(?:([.,\d]+)M)?(?:([.,\d]+)S)?/;
|
|
64
|
+
|
|
65
|
+
const matches = iso.match(iso8601DurationRegex);
|
|
66
|
+
|
|
67
|
+
const duration = getDuration(matches);
|
|
68
|
+
|
|
69
|
+
return +duration.minutes;
|
|
70
|
+
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { HTMLElement } from 'node-html-parser';
|
|
2
|
+
import { CSFDColorRating } from '../interfaces/global';
|
|
3
|
+
import {
|
|
4
|
+
CSFDBoxContent,
|
|
5
|
+
CSFDCreator,
|
|
6
|
+
CSFDCreatorGroups,
|
|
7
|
+
CSFDGenres,
|
|
8
|
+
CSFDMovieListItem,
|
|
9
|
+
CSFDPremiere, CSFDSeasons,
|
|
10
|
+
CSFDTitlesOther,
|
|
11
|
+
CSFDVod,
|
|
12
|
+
CSFDVodService
|
|
13
|
+
} from '../interfaces/movie.interface';
|
|
14
|
+
import { addProtocol, getColor, parseISO8601Duration, parseIdFromUrl } from './global.helper';
|
|
15
|
+
|
|
16
|
+
export const getId = (el: HTMLElement): number => {
|
|
17
|
+
const url = el.querySelector('.tabs .tab-nav-list a').attributes.href;
|
|
18
|
+
return parseIdFromUrl(url);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const getTitle = (el: HTMLElement): string => {
|
|
22
|
+
return el.querySelector('h1').innerText.split(`(`)[0].trim();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const getGenres = (el: HTMLElement): CSFDGenres[] => {
|
|
26
|
+
const genresRaw = el.querySelector('.genres').textContent;
|
|
27
|
+
return genresRaw.split(' / ') as CSFDGenres[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getOrigins = (el: HTMLElement): string[] => {
|
|
31
|
+
const originsRaw = el.querySelector('.origin').textContent;
|
|
32
|
+
const origins = originsRaw.split(',')[0];
|
|
33
|
+
return origins.split(' / ');
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const getColorRating = (bodyClasses: string[]): CSFDColorRating => {
|
|
37
|
+
return getColor(bodyClasses[1]);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const getRating = (el: HTMLElement): number => {
|
|
41
|
+
const ratingRaw = el.querySelector('.film-rating-average').textContent;
|
|
42
|
+
const rating = ratingRaw?.replace(/%/g, '').trim();
|
|
43
|
+
const ratingInt = parseInt(rating);
|
|
44
|
+
|
|
45
|
+
if (Number.isInteger(ratingInt)) {
|
|
46
|
+
return ratingInt;
|
|
47
|
+
} else {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const getRatingCount = (el: HTMLElement): number => {
|
|
53
|
+
const ratingCountRaw = el.querySelector('.box-rating-container .counter')?.textContent;
|
|
54
|
+
const ratingCount = +ratingCountRaw?.replace(/[(\s)]/g, '');
|
|
55
|
+
if (Number.isInteger(ratingCount)) {
|
|
56
|
+
return ratingCount;
|
|
57
|
+
} else {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const getYear = (el: string): number => {
|
|
63
|
+
try {
|
|
64
|
+
const jsonLd = JSON.parse(el);
|
|
65
|
+
return +jsonLd.dateCreated;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('node-csfd-api: Error parsing JSON-LD', error);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const getDuration = (jsonLdRaw: string, el: HTMLElement): number => {
|
|
73
|
+
let duration = null;
|
|
74
|
+
try {
|
|
75
|
+
const jsonLd = JSON.parse(jsonLdRaw);
|
|
76
|
+
duration = jsonLd.duration;
|
|
77
|
+
return parseISO8601Duration(duration);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const origin = el.querySelector('.origin').innerText;
|
|
80
|
+
const timeString = origin.split(',');
|
|
81
|
+
if (timeString.length > 2) {
|
|
82
|
+
// Get last time elelment
|
|
83
|
+
const timeString2 = timeString.pop().trim();
|
|
84
|
+
// Clean it
|
|
85
|
+
const timeRaw = timeString2.split('(')[0].trim();
|
|
86
|
+
// Split by minutes and hours
|
|
87
|
+
const hoursMinsRaw = timeRaw.split('min')[0];
|
|
88
|
+
const hoursMins = hoursMinsRaw.split('h');
|
|
89
|
+
// Resolve hours + minutes format
|
|
90
|
+
duration = hoursMins.length > 1 ? +hoursMins[0] * 60 + +hoursMins[1] : +hoursMins[0];
|
|
91
|
+
return duration;
|
|
92
|
+
} else {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const getTitlesOther = (el: HTMLElement): CSFDTitlesOther[] => {
|
|
99
|
+
const namesNode = el.querySelectorAll('.film-names li');
|
|
100
|
+
|
|
101
|
+
if (!namesNode.length) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const titlesOther = namesNode.map((el) => {
|
|
106
|
+
const country = el.querySelector('img.flag').attributes.alt;
|
|
107
|
+
const title = el.textContent.trim().split('\n')[0];
|
|
108
|
+
|
|
109
|
+
if (country && title) {
|
|
110
|
+
return {
|
|
111
|
+
country,
|
|
112
|
+
title
|
|
113
|
+
};
|
|
114
|
+
} else {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return titlesOther.filter((x) => x);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const getPoster = (el: HTMLElement | null): string => {
|
|
123
|
+
const poster = el.querySelector('.film-posters img');
|
|
124
|
+
// Resolve empty image
|
|
125
|
+
if (poster) {
|
|
126
|
+
if (poster.classNames?.includes('empty-image')) {
|
|
127
|
+
return null;
|
|
128
|
+
} else {
|
|
129
|
+
// Full sized image (not thumb)
|
|
130
|
+
const imageThumb = poster.attributes.src.split('?')[0];
|
|
131
|
+
const image = imageThumb.replace(/\/w140\//, '/w1080/');
|
|
132
|
+
return addProtocol(image);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const getRandomPhoto = (el: HTMLElement | null): string => {
|
|
140
|
+
const imageNode = el.querySelector('.gallery-item picture img');
|
|
141
|
+
const image = imageNode?.attributes?.src;
|
|
142
|
+
if (image) {
|
|
143
|
+
return image.replace(/\/w663\//, '/w1326/');
|
|
144
|
+
} else {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const getTrivia = (el: HTMLElement | null): string[] => {
|
|
150
|
+
const triviaNodes = el.querySelectorAll('.article-trivia ul li');
|
|
151
|
+
if (triviaNodes?.length) {
|
|
152
|
+
return triviaNodes.map((node) => node.textContent.trim().replace(/(\r\n|\n|\r|\t)/gm, ''));
|
|
153
|
+
} else {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const getDescriptions = (el: HTMLElement): string[] => {
|
|
159
|
+
return el
|
|
160
|
+
.querySelectorAll('.body--plots .plot-full p, .body--plots .plots .plots-item p')
|
|
161
|
+
.map((movie) => movie.textContent?.trim().replace(/(\r\n|\n|\r|\t)/gm, ''));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const parsePeople = (el: HTMLElement): CSFDCreator[] => {
|
|
165
|
+
const people = el.querySelectorAll('a');
|
|
166
|
+
return (
|
|
167
|
+
people
|
|
168
|
+
// Filter out "more" links
|
|
169
|
+
.filter((x) => x.classNames.length === 0)
|
|
170
|
+
.map((person) => {
|
|
171
|
+
return {
|
|
172
|
+
id: parseIdFromUrl(person.attributes.href),
|
|
173
|
+
name: person.innerText.trim(),
|
|
174
|
+
url: `https://www.csfd.cz${person.attributes.href}`
|
|
175
|
+
};
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const getGroup = (el: HTMLElement, group: CSFDCreatorGroups): CSFDCreator[] => {
|
|
181
|
+
const creators = el.querySelectorAll('.creators h4');
|
|
182
|
+
const element = creators.filter((elem) => elem.textContent.trim().includes(group))[0];
|
|
183
|
+
if (element?.parentNode) {
|
|
184
|
+
return parsePeople(element.parentNode as HTMLElement);
|
|
185
|
+
} else {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const getType = (el: HTMLElement): string => {
|
|
191
|
+
const type = el.querySelector('.film-header-name .type');
|
|
192
|
+
return type?.innerText?.replace(/[{()}]/g, '') || 'film';
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const getVods = (el: HTMLElement | null): CSFDVod[] => {
|
|
196
|
+
let vods: CSFDVod[] = [];
|
|
197
|
+
if (el) {
|
|
198
|
+
const buttons = el.querySelectorAll('.box-buttons .button');
|
|
199
|
+
const buttonsVod = buttons.filter((x) => !x.classNames.includes('button-social'));
|
|
200
|
+
vods = buttonsVod.map((btn) => {
|
|
201
|
+
return {
|
|
202
|
+
title: btn.textContent.trim() as CSFDVodService,
|
|
203
|
+
url: btn.attributes.href
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return vods.length ? vods : [];
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Get box content
|
|
211
|
+
export const getBoxContent = (el: HTMLElement, box: string): HTMLElement => {
|
|
212
|
+
const headers = el.querySelectorAll('section.box .box-header');
|
|
213
|
+
return headers.find((header) => header.querySelector('h3').textContent.trim().includes(box))
|
|
214
|
+
?.parentNode;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const getBoxMovies = (el: HTMLElement, boxName: CSFDBoxContent): CSFDMovieListItem[] => {
|
|
218
|
+
const movieListItem: CSFDMovieListItem[] = [];
|
|
219
|
+
const box = getBoxContent(el, boxName);
|
|
220
|
+
const movieTitleNodes = box?.querySelectorAll('.article-header .film-title-name');
|
|
221
|
+
if (movieTitleNodes?.length) {
|
|
222
|
+
for (const item of movieTitleNodes) {
|
|
223
|
+
movieListItem.push({
|
|
224
|
+
id: parseIdFromUrl(item.attributes.href),
|
|
225
|
+
title: item.textContent.trim(),
|
|
226
|
+
url: `https://www.csfd.cz${item.attributes.href}`
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return movieListItem;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const getPremieres = (el: HTMLElement): CSFDPremiere[] => {
|
|
234
|
+
const premiereNodes = el.querySelectorAll('.box-premieres li');
|
|
235
|
+
const premiere: CSFDPremiere[] = [];
|
|
236
|
+
for (const premiereNode of premiereNodes) {
|
|
237
|
+
const title = premiereNode.querySelector('p + span').attributes.title;
|
|
238
|
+
|
|
239
|
+
if (title) {
|
|
240
|
+
const [date, ...company] = title?.split(' ');
|
|
241
|
+
|
|
242
|
+
premiere.push({
|
|
243
|
+
country: premiereNode.querySelector('.flag')?.attributes.title || null,
|
|
244
|
+
format: premiereNode.querySelector('p').textContent.trim()?.split(' od')[0],
|
|
245
|
+
date,
|
|
246
|
+
company: company.join(' ')
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return premiere;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export const getTags = (el: HTMLElement): string[] => {
|
|
254
|
+
const tagsRaw = el.querySelectorAll('.box-content a[href*="/podrobne-vyhledavani/?tag="]');
|
|
255
|
+
return tagsRaw.map((tag) => tag.textContent);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export const getSeasonsInfo = (el: HTMLElement): CSFDSeasons => {
|
|
259
|
+
const seasonsList = el.querySelector('.film-episodes-list')
|
|
260
|
+
if (seasonsList === null) {
|
|
261
|
+
return null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const seasons = seasonsList.querySelectorAll('.film-title')
|
|
265
|
+
|
|
266
|
+
return seasons.map(season => {
|
|
267
|
+
const nameContainer = season.querySelector('.film-title-name')
|
|
268
|
+
const infoContainer = season.querySelector('.film-title-info')
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
linkToDetail: nameContainer.getAttribute('href'),
|
|
272
|
+
name: nameContainer.textContent,
|
|
273
|
+
additionalInfo: infoContainer.textContent
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { HTMLElement } from 'node-html-parser';
|
|
2
|
+
import { addProtocol } from './global.helper';
|
|
3
|
+
|
|
4
|
+
export const getUser = (el: HTMLElement): string => {
|
|
5
|
+
return el.querySelector('.user-title-name').text;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const getUserRealName = (el: HTMLElement): string => {
|
|
9
|
+
return el.querySelector('.user-real-name')?.text.trim() || null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const getAvatar = (el: HTMLElement): string => {
|
|
13
|
+
const image = el.querySelector('.article-img img').attributes.src;
|
|
14
|
+
return addProtocol(image);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const getUserUrl = (el: HTMLElement): string => {
|
|
18
|
+
return el.querySelector('.user-title-name').attributes.href;
|
|
19
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { HTMLElement } from 'node-html-parser';
|
|
2
|
+
import { CSFDColorRating, CSFDFilmTypes } from '../interfaces/global';
|
|
3
|
+
import { CSFDCreator } from '../interfaces/movie.interface';
|
|
4
|
+
import { Colors } from '../interfaces/user-ratings.interface';
|
|
5
|
+
import { addProtocol, parseColor, parseIdFromUrl } from './global.helper';
|
|
6
|
+
|
|
7
|
+
type Creator = 'Režie:' | 'Hrají:';
|
|
8
|
+
|
|
9
|
+
export const getType = (el: HTMLElement): CSFDFilmTypes => {
|
|
10
|
+
const type = el.querySelectorAll('.film-title-info .info')[1];
|
|
11
|
+
return (type?.innerText.replace(/[{()}]/g, '') || 'film') as CSFDFilmTypes;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const getTitle = (el: HTMLElement): string => {
|
|
15
|
+
return el.querySelector('.film-title-name').text;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getYear = (el: HTMLElement): number => {
|
|
19
|
+
return +el.querySelectorAll('.film-title-info .info')[0]?.innerText.replace(/[{()}]/g, '');
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const getUrl = (el: HTMLElement): string => {
|
|
23
|
+
return el.querySelector('.film-title-name').attributes.href;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const getColorRating = (el: HTMLElement): CSFDColorRating => {
|
|
27
|
+
return parseColor(
|
|
28
|
+
el.querySelector('.article-header i.icon').classNames.split(' ').pop() as Colors
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getPoster = (el: HTMLElement): string => {
|
|
33
|
+
const image = el.querySelector('img').attributes.src;
|
|
34
|
+
return addProtocol(image);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const getOrigins = (el: HTMLElement): string[] => {
|
|
38
|
+
const originsRaw = el.querySelector('.article-content p .info')?.text;
|
|
39
|
+
if (!originsRaw) return [];
|
|
40
|
+
const originsAll = originsRaw?.split(', ')?.[0];
|
|
41
|
+
return originsAll?.split('/').map((country) => country.trim());
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const parsePeople = (el: HTMLElement, type: 'directors' | 'actors'): CSFDCreator[] => {
|
|
45
|
+
let who: Creator;
|
|
46
|
+
if (type === 'directors') who = 'Režie:';
|
|
47
|
+
if (type === 'actors') who = 'Hrají:';
|
|
48
|
+
|
|
49
|
+
const peopleNode = Array.from(el && el.querySelectorAll('.article-content p')).find((el) =>
|
|
50
|
+
el.textContent.includes(who)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (peopleNode) {
|
|
54
|
+
const people = Array.from(peopleNode.querySelectorAll('a')) as unknown as HTMLElement[];
|
|
55
|
+
|
|
56
|
+
return people.map((person) => {
|
|
57
|
+
return {
|
|
58
|
+
id: parseIdFromUrl(person.attributes.href),
|
|
59
|
+
name: person.innerText.trim(),
|
|
60
|
+
url: `https://www.csfd.cz${person.attributes.href}`
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { HTMLElement } from 'node-html-parser';
|
|
2
|
+
import { CSFDColorRating, CSFDFilmTypes, CSFDStars } from '../interfaces/global';
|
|
3
|
+
import { Colors } from '../interfaces/user-ratings.interface';
|
|
4
|
+
import { parseIdFromUrl } from './global.helper';
|
|
5
|
+
|
|
6
|
+
export const getId = (el: HTMLElement): number => {
|
|
7
|
+
const url = el.querySelector('td.name .film-title-name').attributes.href;
|
|
8
|
+
return parseIdFromUrl(url);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getUserRating = (el: HTMLElement): CSFDStars => {
|
|
12
|
+
const ratingText = el.querySelector('td.star-rating-only .stars').classNames.split(' ').pop();
|
|
13
|
+
|
|
14
|
+
const rating = ratingText.includes('stars-') ? +ratingText.split('-').pop() : 0;
|
|
15
|
+
return rating as CSFDStars;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getType = (el: HTMLElement): CSFDFilmTypes => {
|
|
19
|
+
const typeText = el.querySelectorAll('td.name .film-title-info .info');
|
|
20
|
+
|
|
21
|
+
return (typeText.length > 1 ? typeText[1].text.slice(1, -1) : 'film') as CSFDFilmTypes;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const getTitle = (el: HTMLElement): string => {
|
|
25
|
+
return el.querySelector('td.name .film-title-name').text;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const getYear = (el: HTMLElement): number => {
|
|
29
|
+
return +el.querySelectorAll('td.name .film-title-info .info')[0]?.text.slice(1, -1) || null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getColorRating = (el: HTMLElement): CSFDColorRating => {
|
|
33
|
+
const color = parseColor(el.querySelector('td.name .icon').classNames.split(' ').pop() as Colors);
|
|
34
|
+
return color;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const getDate = (el: HTMLElement): string => {
|
|
38
|
+
return el.querySelector('td.date-only').text.trim();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const getUrl = (el: HTMLElement): string => {
|
|
42
|
+
const url = el.querySelector('td.name .film-title-name').attributes.href;
|
|
43
|
+
return `https://www.csfd.cz${url}`;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const parseColor = (quality: Colors): CSFDColorRating => {
|
|
47
|
+
switch (quality) {
|
|
48
|
+
case 'lightgrey':
|
|
49
|
+
return 'unknown';
|
|
50
|
+
case 'red':
|
|
51
|
+
return 'good';
|
|
52
|
+
case 'blue':
|
|
53
|
+
return 'average';
|
|
54
|
+
case 'grey':
|
|
55
|
+
return 'bad';
|
|
56
|
+
default:
|
|
57
|
+
return 'unknown';
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Sleep in loop
|
|
62
|
+
export const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));
|