node-csfd-api 5.3.0 → 5.4.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/bin/export-ratings.mjs +1 -1
- package/bin/mcp-server.mjs +1 -1
- package/bin/server.mjs +1 -1
- package/fetchers/index.js +53 -9
- package/fetchers/index.js.map +1 -1
- package/fetchers/index.mjs +53 -9
- package/fetchers/index.mjs.map +1 -1
- package/package.json +1 -1
- package/package.mjs +1 -1
- package/src/fetchers/fetch.polyfill.mjs +8 -0
- package/src/fetchers/index.mjs +76 -0
- package/src/helpers/cinema.helper.mjs +80 -0
- package/src/helpers/creator.helper.mjs +69 -0
- package/src/helpers/global.helper.mjs +101 -0
- package/src/helpers/movie.helper.mjs +302 -0
- package/src/helpers/search-creator.helper.mjs +17 -0
- package/src/helpers/search-user.helper.mjs +24 -0
- package/src/helpers/search.helper.mjs +46 -0
- package/src/helpers/user-ratings.helper.mjs +35 -0
- package/src/helpers/user-reviews.helper.mjs +48 -0
- package/src/index.mjs +64 -0
- package/src/services/cinema.service.mjs +32 -0
- package/src/services/creator.service.mjs +33 -0
- package/src/services/movie.service.mjs +62 -0
- package/src/services/search.service.mjs +76 -0
- package/src/services/user-ratings.service.mjs +66 -0
- package/src/services/user-reviews.service.mjs +68 -0
- package/src/vars.mjs +22 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { addProtocol, getColor, parseDate, parseFilmType, parseISO8601Duration, parseIdFromUrl, parseLastIdFromUrl } from "./global.helper.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/helpers/movie.helper.ts
|
|
5
|
+
const CREATOR_LABELS = {
|
|
6
|
+
en: {
|
|
7
|
+
directors: "Directed by",
|
|
8
|
+
writers: "Screenplay",
|
|
9
|
+
cinematography: "Cinematography",
|
|
10
|
+
music: "Composer",
|
|
11
|
+
actors: "Cast",
|
|
12
|
+
basedOn: "Based on",
|
|
13
|
+
producers: "Produced by",
|
|
14
|
+
filmEditing: "Editing",
|
|
15
|
+
costumeDesign: "Costumes",
|
|
16
|
+
productionDesign: "Production design",
|
|
17
|
+
casting: "Casting",
|
|
18
|
+
sound: "Sound",
|
|
19
|
+
makeup: "Make-up"
|
|
20
|
+
},
|
|
21
|
+
cs: {
|
|
22
|
+
directors: "Režie",
|
|
23
|
+
writers: "Scénář",
|
|
24
|
+
cinematography: "Kamera",
|
|
25
|
+
music: "Hudba",
|
|
26
|
+
actors: "Hrají",
|
|
27
|
+
basedOn: "Předloha",
|
|
28
|
+
producers: "Produkce",
|
|
29
|
+
filmEditing: "Střih",
|
|
30
|
+
costumeDesign: "Kostýmy",
|
|
31
|
+
productionDesign: "Scénografie",
|
|
32
|
+
casting: "Casting",
|
|
33
|
+
sound: "Zvuk",
|
|
34
|
+
makeup: "Masky"
|
|
35
|
+
},
|
|
36
|
+
sk: {
|
|
37
|
+
directors: "Réžia",
|
|
38
|
+
writers: "Scenár",
|
|
39
|
+
cinematography: "Kamera",
|
|
40
|
+
music: "Hudba",
|
|
41
|
+
actors: "Hrajú",
|
|
42
|
+
basedOn: "Predloha",
|
|
43
|
+
producers: "Produkcia",
|
|
44
|
+
filmEditing: "Strih",
|
|
45
|
+
costumeDesign: "Kostýmy",
|
|
46
|
+
productionDesign: "Scénografia",
|
|
47
|
+
casting: "Casting",
|
|
48
|
+
sound: "Zvuk",
|
|
49
|
+
makeup: "Masky"
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Maps language-specific movie creator group labels.
|
|
54
|
+
* @param language - The language code (e.g., 'en', 'cs')
|
|
55
|
+
* @param key - The key of the creator group (e.g., 'directors', 'writers')
|
|
56
|
+
* @returns The localized label for the creator group
|
|
57
|
+
*/
|
|
58
|
+
const getLocalizedCreatorLabel = (language, key) => {
|
|
59
|
+
return (CREATOR_LABELS[language || "cs"] || CREATOR_LABELS["cs"])[key];
|
|
60
|
+
};
|
|
61
|
+
const getSeriesAndSeasonTitle = (el) => {
|
|
62
|
+
const titleElement = el.querySelector("h1");
|
|
63
|
+
if (!titleElement) return {
|
|
64
|
+
seriesName: null,
|
|
65
|
+
seasonName: null
|
|
66
|
+
};
|
|
67
|
+
const fullText = titleElement.innerText.trim();
|
|
68
|
+
if (fullText.includes(" - ")) {
|
|
69
|
+
const [seriesName, seasonName] = fullText.split(" - ").map((part) => part.trim());
|
|
70
|
+
return {
|
|
71
|
+
seriesName,
|
|
72
|
+
seasonName
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
seriesName: fullText,
|
|
77
|
+
seasonName: null
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const getMovieTitle = (el) => {
|
|
81
|
+
return el.querySelector("h1").innerText.split(`(`)[0].trim();
|
|
82
|
+
};
|
|
83
|
+
const getMovieGenres = (el) => {
|
|
84
|
+
return el.querySelector(".genres").textContent.split(" / ");
|
|
85
|
+
};
|
|
86
|
+
const getMovieOrigins = (el) => {
|
|
87
|
+
return el.querySelector(".origin").textContent.split(",")[0].split(" / ");
|
|
88
|
+
};
|
|
89
|
+
const getMovieColorRating = (bodyClasses) => {
|
|
90
|
+
return getColor(bodyClasses[1]);
|
|
91
|
+
};
|
|
92
|
+
const getMovieRating = (el) => {
|
|
93
|
+
const rating = el.querySelector(".film-rating-average").textContent?.replace(/%/g, "").trim();
|
|
94
|
+
const ratingInt = parseInt(rating);
|
|
95
|
+
if (Number.isInteger(ratingInt)) return ratingInt;
|
|
96
|
+
else return null;
|
|
97
|
+
};
|
|
98
|
+
const getMovieRatingCount = (el) => {
|
|
99
|
+
const ratingCount = +(el.querySelector(".box-rating-container .counter")?.textContent)?.replace(/[(\s)]/g, "");
|
|
100
|
+
if (Number.isInteger(ratingCount)) return ratingCount;
|
|
101
|
+
else return null;
|
|
102
|
+
};
|
|
103
|
+
const getMovieYear = (jsonLd) => {
|
|
104
|
+
if (jsonLd && jsonLd.dateCreated) return +jsonLd.dateCreated;
|
|
105
|
+
return null;
|
|
106
|
+
};
|
|
107
|
+
const getMovieDuration = (jsonLd, el) => {
|
|
108
|
+
if (jsonLd && jsonLd.duration) try {
|
|
109
|
+
return parseISO8601Duration(jsonLd.duration);
|
|
110
|
+
} catch (e) {}
|
|
111
|
+
try {
|
|
112
|
+
const timeString = el.querySelector(".origin").innerText.split(",");
|
|
113
|
+
if (timeString.length > 2) {
|
|
114
|
+
const hoursMins = timeString.pop().trim().split("(")[0].trim().split("min")[0].split("h");
|
|
115
|
+
return hoursMins.length > 1 ? +hoursMins[0] * 60 + +hoursMins[1] : +hoursMins[0];
|
|
116
|
+
} else return null;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const getMovieTitlesOther = (el) => {
|
|
122
|
+
const namesNode = el.querySelectorAll(".film-names li");
|
|
123
|
+
if (!namesNode.length) return [];
|
|
124
|
+
return namesNode.map((el) => {
|
|
125
|
+
const country = el.querySelector("img.flag").attributes.alt;
|
|
126
|
+
const title = el.textContent.trim().split("\n")[0];
|
|
127
|
+
if (country && title) return {
|
|
128
|
+
country,
|
|
129
|
+
title
|
|
130
|
+
};
|
|
131
|
+
else return null;
|
|
132
|
+
}).filter((x) => x);
|
|
133
|
+
};
|
|
134
|
+
const getMoviePoster = (el) => {
|
|
135
|
+
const poster = el.querySelector(".film-posters img");
|
|
136
|
+
if (poster) if (poster.classNames?.includes("empty-image")) return null;
|
|
137
|
+
else return addProtocol(poster.attributes.src.split("?")[0].replace(/\/w140\//, "/w1080/"));
|
|
138
|
+
else return null;
|
|
139
|
+
};
|
|
140
|
+
const getMovieRandomPhoto = (el) => {
|
|
141
|
+
const image = el.querySelector(".gallery-item picture img")?.attributes?.src;
|
|
142
|
+
if (image) return image.replace(/\/w663\//, "/w1326/");
|
|
143
|
+
else return null;
|
|
144
|
+
};
|
|
145
|
+
const getMovieTrivia = (el) => {
|
|
146
|
+
const triviaNodes = el.querySelectorAll(".article-trivia ul li");
|
|
147
|
+
if (triviaNodes?.length) return triviaNodes.map((node) => node.textContent.trim().replace(/(\r\n|\n|\r|\t)/gm, ""));
|
|
148
|
+
else return null;
|
|
149
|
+
};
|
|
150
|
+
const getMovieDescriptions = (el) => {
|
|
151
|
+
return el.querySelectorAll(".body--plots .plot-full p, .body--plots .plots .plots-item p").map((movie) => movie.textContent?.trim().replace(/(\r\n|\n|\r|\t)/gm, ""));
|
|
152
|
+
};
|
|
153
|
+
const parseMoviePeople = (el) => {
|
|
154
|
+
return el.querySelectorAll("a").filter((x) => x.classNames.length === 0).map((person) => {
|
|
155
|
+
return {
|
|
156
|
+
id: parseIdFromUrl(person.attributes.href),
|
|
157
|
+
name: person.innerText.trim(),
|
|
158
|
+
url: `https://www.csfd.cz${person.attributes.href}`
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
const getMovieCreators = (el, options) => {
|
|
163
|
+
const creators = {
|
|
164
|
+
directors: [],
|
|
165
|
+
writers: [],
|
|
166
|
+
cinematography: [],
|
|
167
|
+
music: [],
|
|
168
|
+
actors: [],
|
|
169
|
+
basedOn: [],
|
|
170
|
+
producers: [],
|
|
171
|
+
filmEditing: [],
|
|
172
|
+
costumeDesign: [],
|
|
173
|
+
productionDesign: []
|
|
174
|
+
};
|
|
175
|
+
const groups = el.querySelectorAll(".creators h4");
|
|
176
|
+
const localizedLabels = [
|
|
177
|
+
"directors",
|
|
178
|
+
"writers",
|
|
179
|
+
"cinematography",
|
|
180
|
+
"music",
|
|
181
|
+
"actors",
|
|
182
|
+
"basedOn",
|
|
183
|
+
"producers",
|
|
184
|
+
"filmEditing",
|
|
185
|
+
"costumeDesign",
|
|
186
|
+
"productionDesign"
|
|
187
|
+
].map((key) => ({
|
|
188
|
+
key,
|
|
189
|
+
label: getLocalizedCreatorLabel(options?.language, key)
|
|
190
|
+
}));
|
|
191
|
+
for (const group of groups) {
|
|
192
|
+
const text = group.textContent.trim();
|
|
193
|
+
for (const { key, label } of localizedLabels) if (text.includes(label)) {
|
|
194
|
+
if (group.parentNode) creators[key] = parseMoviePeople(group.parentNode);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return creators;
|
|
199
|
+
};
|
|
200
|
+
const getSeasonsOrEpisodes = (el) => {
|
|
201
|
+
const childrenList = el.querySelector(".film-episodes-list");
|
|
202
|
+
if (!childrenList) return null;
|
|
203
|
+
const childrenNodes = childrenList.querySelectorAll(".film-title");
|
|
204
|
+
if (!childrenNodes?.length) return [];
|
|
205
|
+
return childrenNodes.map((season) => {
|
|
206
|
+
const nameContainer = season.querySelector(".film-title-name");
|
|
207
|
+
const infoContainer = season.querySelector(".info");
|
|
208
|
+
const href = nameContainer?.getAttribute("href");
|
|
209
|
+
const url = href ? href.startsWith("/") ? `https://www.csfd.cz${href}` : href : null;
|
|
210
|
+
return {
|
|
211
|
+
id: parseLastIdFromUrl(href || ""),
|
|
212
|
+
title: nameContainer?.textContent?.trim() || null,
|
|
213
|
+
url,
|
|
214
|
+
info: infoContainer?.textContent?.replace(/[{()}]/g, "").trim() || null
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
const getEpisodeCode = (el) => {
|
|
219
|
+
const filmHeaderName = el.querySelector(".film-header-name h1");
|
|
220
|
+
if (!filmHeaderName) return null;
|
|
221
|
+
const match = (filmHeaderName.textContent?.trim() || "").match(/\(([^)]+)\)/);
|
|
222
|
+
return match ? match[1] : null;
|
|
223
|
+
};
|
|
224
|
+
const detectSeasonOrEpisodeListType = (el) => {
|
|
225
|
+
const headerText = el.querySelector(".box-header h3")?.innerText.trim() ?? "";
|
|
226
|
+
if (headerText.includes("Série")) return "seasons";
|
|
227
|
+
if (headerText.startsWith("Epizody")) return "episodes";
|
|
228
|
+
return null;
|
|
229
|
+
};
|
|
230
|
+
const getSeasonOrEpisodeParent = (el) => {
|
|
231
|
+
let parents = el.querySelectorAll(".film-header h2 a");
|
|
232
|
+
if (parents.length === 0) parents = el.querySelectorAll(".film-header h1 a");
|
|
233
|
+
if (parents.length === 0) return null;
|
|
234
|
+
const [parentSeries, parentSeason] = parents;
|
|
235
|
+
const seriesId = parseIdFromUrl(parentSeries?.getAttribute("href"));
|
|
236
|
+
const seasonId = parseLastIdFromUrl(parentSeason?.getAttribute("href") || "");
|
|
237
|
+
const seriesTitle = parentSeries?.textContent?.trim() || null;
|
|
238
|
+
const seasonTitle = parentSeason?.textContent?.trim() || null;
|
|
239
|
+
const series = seriesId && seriesTitle ? {
|
|
240
|
+
id: seriesId,
|
|
241
|
+
title: seriesTitle
|
|
242
|
+
} : null;
|
|
243
|
+
const season = seasonId && seasonTitle ? {
|
|
244
|
+
id: seasonId,
|
|
245
|
+
title: seasonTitle
|
|
246
|
+
} : null;
|
|
247
|
+
if (!series && !season) return null;
|
|
248
|
+
return {
|
|
249
|
+
series,
|
|
250
|
+
season
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
const getMovieType = (el) => {
|
|
254
|
+
return parseFilmType(el.querySelector(".film-header-name .type")?.innerText?.replace(/[{()}]/g, "") || "film");
|
|
255
|
+
};
|
|
256
|
+
const getMovieVods = (el) => {
|
|
257
|
+
let vods = [];
|
|
258
|
+
if (el) vods = el.querySelectorAll(".box-buttons-vod .vod-badge a").map((btn) => {
|
|
259
|
+
return {
|
|
260
|
+
title: btn.textContent.trim(),
|
|
261
|
+
url: btn.attributes.href
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
return vods.length ? vods : [];
|
|
265
|
+
};
|
|
266
|
+
const getBoxContent = (el, box) => {
|
|
267
|
+
return el.querySelectorAll("section.box .box-header").find((header) => header.querySelector("h3")?.textContent.trim().includes(box))?.parentNode;
|
|
268
|
+
};
|
|
269
|
+
const getMovieBoxMovies = (el, boxName) => {
|
|
270
|
+
const movieListItem = [];
|
|
271
|
+
const movieTitleNodes = getBoxContent(el, boxName)?.querySelectorAll(".article-header .film-title-name");
|
|
272
|
+
if (movieTitleNodes?.length) for (const item of movieTitleNodes) movieListItem.push({
|
|
273
|
+
id: parseIdFromUrl(item.attributes.href),
|
|
274
|
+
title: item.textContent.trim(),
|
|
275
|
+
url: `https://www.csfd.cz${item.attributes.href}`
|
|
276
|
+
});
|
|
277
|
+
return movieListItem;
|
|
278
|
+
};
|
|
279
|
+
const getMoviePremieres = (el) => {
|
|
280
|
+
const premiereNodes = el.querySelectorAll(".box-premieres li");
|
|
281
|
+
const premiere = [];
|
|
282
|
+
for (const premiereNode of premiereNodes) {
|
|
283
|
+
const title = premiereNode.querySelector("p + span").attributes.title;
|
|
284
|
+
if (title) {
|
|
285
|
+
const [dateRaw, ...company] = title?.split(" ");
|
|
286
|
+
const date = parseDate(dateRaw);
|
|
287
|
+
if (date) premiere.push({
|
|
288
|
+
country: premiereNode.querySelector(".flag")?.attributes.title || null,
|
|
289
|
+
format: premiereNode.querySelector("p").textContent.trim()?.split(" od")[0],
|
|
290
|
+
date,
|
|
291
|
+
company: company.join(" ")
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return premiere;
|
|
296
|
+
};
|
|
297
|
+
const getMovieTags = (el) => {
|
|
298
|
+
return el.querySelectorAll(".box-content a[href*=\"/tag/\"]").map((tag) => tag.textContent);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
//#endregion
|
|
302
|
+
export { detectSeasonOrEpisodeListType, getEpisodeCode, getMovieBoxMovies, getMovieColorRating, getMovieCreators, getMovieDescriptions, getMovieDuration, getMovieGenres, getMovieOrigins, getMoviePoster, getMoviePremieres, getMovieRandomPhoto, getMovieRating, getMovieRatingCount, getMovieTags, getMovieTitle, getMovieTitlesOther, getMovieTrivia, getMovieType, getMovieVods, getMovieYear, getSeasonOrEpisodeParent, getSeasonsOrEpisodes, getSeriesAndSeasonTitle };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { addProtocol } from "./global.helper.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/helpers/search-creator.helper.ts
|
|
5
|
+
const getCreatorName = (el) => {
|
|
6
|
+
return el.querySelector(".user-title").text.trim();
|
|
7
|
+
};
|
|
8
|
+
const getCreatorImage = (el) => {
|
|
9
|
+
const image = el.querySelector("img").attributes.src;
|
|
10
|
+
return addProtocol(image);
|
|
11
|
+
};
|
|
12
|
+
const getCreatorUrl = (el) => {
|
|
13
|
+
return el.querySelector(".user-title a").attributes.href;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
export { getCreatorImage, getCreatorName, getCreatorUrl };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { addProtocol } from "./global.helper.mjs";
|
|
3
|
+
import { NodeType } from "node-html-parser";
|
|
4
|
+
|
|
5
|
+
//#region src/helpers/search-user.helper.ts
|
|
6
|
+
const getUser = (el) => {
|
|
7
|
+
return el.querySelector(".user-title-name").text;
|
|
8
|
+
};
|
|
9
|
+
const getUserRealName = (el) => {
|
|
10
|
+
const p = el.querySelector(".article-content p");
|
|
11
|
+
if (!p) return null;
|
|
12
|
+
const textNodes = p.childNodes.filter((n) => n.nodeType === NodeType.TEXT_NODE && n.rawText.trim() !== "");
|
|
13
|
+
return textNodes.length ? textNodes[0].rawText.trim() : null;
|
|
14
|
+
};
|
|
15
|
+
const getAvatar = (el) => {
|
|
16
|
+
const image = el.querySelector(".article-img img").attributes.src;
|
|
17
|
+
return addProtocol(image);
|
|
18
|
+
};
|
|
19
|
+
const getUserUrl = (el) => {
|
|
20
|
+
return el.querySelector(".user-title-name").attributes.href;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
export { getAvatar, getUser, getUserRealName, getUserUrl };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { addProtocol, parseColor, parseFilmType, parseIdFromUrl } from "./global.helper.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/helpers/search.helper.ts
|
|
5
|
+
const getSearchType = (el) => {
|
|
6
|
+
const type = el.querySelectorAll(".film-title-info .info")[1];
|
|
7
|
+
return parseFilmType(type?.innerText?.replace(/[{()}]/g, "")?.trim() || "film");
|
|
8
|
+
};
|
|
9
|
+
const getSearchTitle = (el) => {
|
|
10
|
+
return el.querySelector(".film-title-name").text;
|
|
11
|
+
};
|
|
12
|
+
const getSearchYear = (el) => {
|
|
13
|
+
return +el.querySelectorAll(".film-title-info .info")[0]?.innerText.replace(/[{()}]/g, "");
|
|
14
|
+
};
|
|
15
|
+
const getSearchUrl = (el) => {
|
|
16
|
+
return el.querySelector(".film-title-name").attributes.href;
|
|
17
|
+
};
|
|
18
|
+
const getSearchColorRating = (el) => {
|
|
19
|
+
return parseColor(el.querySelector(".article-header i.icon").classNames.split(" ").pop());
|
|
20
|
+
};
|
|
21
|
+
const getSearchPoster = (el) => {
|
|
22
|
+
const image = el.querySelector("img").attributes.src;
|
|
23
|
+
return addProtocol(image);
|
|
24
|
+
};
|
|
25
|
+
const getSearchOrigins = (el) => {
|
|
26
|
+
const originsRaw = el.querySelector(".article-content p .info")?.text;
|
|
27
|
+
if (!originsRaw) return [];
|
|
28
|
+
return (originsRaw?.split(", ")?.[0])?.split("/").map((country) => country.trim());
|
|
29
|
+
};
|
|
30
|
+
const parseSearchPeople = (el, type) => {
|
|
31
|
+
let who;
|
|
32
|
+
if (type === "directors") who = "Režie:";
|
|
33
|
+
if (type === "actors") who = "Hrají:";
|
|
34
|
+
const peopleNode = Array.from(el && el.querySelectorAll(".article-content p")).find((el) => el.textContent.includes(who));
|
|
35
|
+
if (peopleNode) return Array.from(peopleNode.querySelectorAll("a")).map((person) => {
|
|
36
|
+
return {
|
|
37
|
+
id: parseIdFromUrl(person.attributes.href),
|
|
38
|
+
name: person.innerText.trim(),
|
|
39
|
+
url: `https://www.csfd.cz${person.attributes.href}`
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
else return [];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
export { getSearchColorRating, getSearchOrigins, getSearchPoster, getSearchTitle, getSearchType, getSearchUrl, getSearchYear, parseSearchPeople };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseColor, parseDate, parseFilmType, parseIdFromUrl } from "./global.helper.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/helpers/user-ratings.helper.ts
|
|
5
|
+
const getUserRatingId = (el) => {
|
|
6
|
+
const url = el.querySelector("td.name .film-title-name").attributes.href;
|
|
7
|
+
return parseIdFromUrl(url);
|
|
8
|
+
};
|
|
9
|
+
const getUserRating = (el) => {
|
|
10
|
+
const ratingText = el.querySelector("td.star-rating-only .stars").classNames.split(" ").pop();
|
|
11
|
+
return ratingText.includes("stars-") ? +ratingText.split("-").pop() : 0;
|
|
12
|
+
};
|
|
13
|
+
const getUserRatingType = (el) => {
|
|
14
|
+
const typeNode = el.querySelector("td.name .film-title-info .info ~ .info");
|
|
15
|
+
return parseFilmType(typeNode ? typeNode.text : "film");
|
|
16
|
+
};
|
|
17
|
+
const getUserRatingTitle = (el) => {
|
|
18
|
+
return el.querySelector("td.name .film-title-name").text;
|
|
19
|
+
};
|
|
20
|
+
const getUserRatingYear = (el) => {
|
|
21
|
+
const yearNode = el.querySelector("td.name .film-title-info .info");
|
|
22
|
+
return yearNode ? +yearNode.text || null : null;
|
|
23
|
+
};
|
|
24
|
+
const getUserRatingColorRating = (el) => {
|
|
25
|
+
return parseColor(el.querySelector("td.name .icon").classNames.split(" ").pop());
|
|
26
|
+
};
|
|
27
|
+
const getUserRatingDate = (el) => {
|
|
28
|
+
return parseDate(el.querySelector("td.date-only").text.trim());
|
|
29
|
+
};
|
|
30
|
+
const getUserRatingUrl = (el) => {
|
|
31
|
+
return `https://www.csfd.cz${el.querySelector("td.name .film-title-name").attributes.href}`;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
export { getUserRating, getUserRatingColorRating, getUserRatingDate, getUserRatingId, getUserRatingTitle, getUserRatingType, getUserRatingUrl, getUserRatingYear };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseColor, parseDate, parseFilmType, parseIdFromUrl } from "./global.helper.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/helpers/user-reviews.helper.ts
|
|
5
|
+
const getUserReviewId = (el) => {
|
|
6
|
+
const url = el.querySelector(".film-title-name").attributes.href;
|
|
7
|
+
return parseIdFromUrl(url);
|
|
8
|
+
};
|
|
9
|
+
const getUserReviewRating = (el) => {
|
|
10
|
+
const ratingText = el.querySelector(".star-rating .stars").classNames.split(" ").pop();
|
|
11
|
+
return ratingText.includes("stars-") ? +ratingText.split("-").pop() : 0;
|
|
12
|
+
};
|
|
13
|
+
const getUserReviewType = (el) => {
|
|
14
|
+
const typeNode = el.querySelector(".film-title-info .info ~ .info");
|
|
15
|
+
return parseFilmType(typeNode ? typeNode.text.slice(1, -1) : "film");
|
|
16
|
+
};
|
|
17
|
+
const getUserReviewTitle = (el) => {
|
|
18
|
+
return el.querySelector(".film-title-name").text;
|
|
19
|
+
};
|
|
20
|
+
const getUserReviewYear = (el) => {
|
|
21
|
+
const infoSpan = el.querySelector(".film-title-info .info");
|
|
22
|
+
return infoSpan ? +infoSpan.text.replace(/[()]/g, "") || null : null;
|
|
23
|
+
};
|
|
24
|
+
const getUserReviewColorRating = (el) => {
|
|
25
|
+
return parseColor(el.querySelector(".film-title-inline i.icon")?.classNames.split(" ").pop());
|
|
26
|
+
};
|
|
27
|
+
const getUserReviewDate = (el) => {
|
|
28
|
+
return parseDate(el.querySelector(".article-header-date-content .info time").text.trim());
|
|
29
|
+
};
|
|
30
|
+
const getUserReviewUrl = (el) => {
|
|
31
|
+
return `https://www.csfd.cz${el.querySelector(".film-title-name").attributes.href}`;
|
|
32
|
+
};
|
|
33
|
+
const getUserReviewText = (el) => {
|
|
34
|
+
return el.querySelector(".comment").text.trim();
|
|
35
|
+
};
|
|
36
|
+
const getUserReviewPoster = (el) => {
|
|
37
|
+
const img = el.querySelector(".article-img img");
|
|
38
|
+
const srcset = img?.attributes.srcset;
|
|
39
|
+
if (srcset) {
|
|
40
|
+
const poster3x = srcset.split(",").map((s) => s.trim()).find((s) => s.endsWith("3x"));
|
|
41
|
+
if (poster3x) return `https:${poster3x.replace(/\s+3x$/, "").trim()}`;
|
|
42
|
+
}
|
|
43
|
+
const src = img?.attributes.src;
|
|
44
|
+
return src ? `https:${src}` : null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { getUserReviewColorRating, getUserReviewDate, getUserReviewId, getUserReviewPoster, getUserReviewRating, getUserReviewText, getUserReviewTitle, getUserReviewType, getUserReviewUrl, getUserReviewYear };
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CinemaScraper } from "./services/cinema.service.mjs";
|
|
3
|
+
import { CreatorScraper } from "./services/creator.service.mjs";
|
|
4
|
+
import { MovieScraper } from "./services/movie.service.mjs";
|
|
5
|
+
import { SearchScraper } from "./services/search.service.mjs";
|
|
6
|
+
import { UserRatingsScraper } from "./services/user-ratings.service.mjs";
|
|
7
|
+
import { UserReviewsScraper } from "./services/user-reviews.service.mjs";
|
|
8
|
+
|
|
9
|
+
//#region src/index.ts
|
|
10
|
+
var Csfd = class {
|
|
11
|
+
constructor(userRatingsService, userReviewsService, movieService, creatorService, searchService, cinemaService, defaultOptions) {
|
|
12
|
+
this.userRatingsService = userRatingsService;
|
|
13
|
+
this.userReviewsService = userReviewsService;
|
|
14
|
+
this.movieService = movieService;
|
|
15
|
+
this.creatorService = creatorService;
|
|
16
|
+
this.searchService = searchService;
|
|
17
|
+
this.cinemaService = cinemaService;
|
|
18
|
+
this.defaultOptions = defaultOptions;
|
|
19
|
+
}
|
|
20
|
+
setOptions({ request, language }) {
|
|
21
|
+
if (request !== void 0) this.defaultOptions = {
|
|
22
|
+
...this.defaultOptions,
|
|
23
|
+
request
|
|
24
|
+
};
|
|
25
|
+
if (language !== void 0) this.defaultOptions = {
|
|
26
|
+
...this.defaultOptions,
|
|
27
|
+
language
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async userRatings(user, config, options) {
|
|
31
|
+
const opts = options ?? this.defaultOptions;
|
|
32
|
+
return this.userRatingsService.userRatings(user, config, opts);
|
|
33
|
+
}
|
|
34
|
+
async userReviews(user, config, options) {
|
|
35
|
+
const opts = options ?? this.defaultOptions;
|
|
36
|
+
return this.userReviewsService.userReviews(user, config, opts);
|
|
37
|
+
}
|
|
38
|
+
async movie(movie, options) {
|
|
39
|
+
const opts = options ?? this.defaultOptions;
|
|
40
|
+
return this.movieService.movie(+movie, opts);
|
|
41
|
+
}
|
|
42
|
+
async creator(creator, options) {
|
|
43
|
+
const opts = options ?? this.defaultOptions;
|
|
44
|
+
return this.creatorService.creator(+creator, opts);
|
|
45
|
+
}
|
|
46
|
+
async search(text, options) {
|
|
47
|
+
const opts = options ?? this.defaultOptions;
|
|
48
|
+
return this.searchService.search(text, opts);
|
|
49
|
+
}
|
|
50
|
+
async cinema(district, period, options) {
|
|
51
|
+
const opts = options ?? this.defaultOptions;
|
|
52
|
+
return this.cinemaService.cinemas(+district, period, opts);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const movieScraper = new MovieScraper();
|
|
56
|
+
const userRatingsScraper = new UserRatingsScraper();
|
|
57
|
+
const userReviewsScraper = new UserReviewsScraper();
|
|
58
|
+
const cinemaScraper = new CinemaScraper();
|
|
59
|
+
const creatorScraper = new CreatorScraper();
|
|
60
|
+
const searchScraper = new SearchScraper();
|
|
61
|
+
const csfd = new Csfd(userRatingsScraper, userReviewsScraper, movieScraper, creatorScraper, searchScraper, cinemaScraper);
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { csfd };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fetchPage } from "../fetchers/index.mjs";
|
|
3
|
+
import { cinemasUrl } from "../vars.mjs";
|
|
4
|
+
import { getCinemaCoords, getCinemaId, getCinemaUrl, getGroupedFilmsByDate, parseCinema } from "../helpers/cinema.helper.mjs";
|
|
5
|
+
import { parse } from "node-html-parser";
|
|
6
|
+
|
|
7
|
+
//#region src/services/cinema.service.ts
|
|
8
|
+
var CinemaScraper = class {
|
|
9
|
+
async cinemas(district = 1, period = "today", options) {
|
|
10
|
+
const contentNode = parse(await fetchPage(cinemasUrl(district, period, { language: options?.language }), { ...options?.request })).querySelectorAll("#snippet--cinemas section[id*=\"cinema-\"]");
|
|
11
|
+
return this.buildCinemas(contentNode);
|
|
12
|
+
}
|
|
13
|
+
buildCinemas(contentNode) {
|
|
14
|
+
const cinemas = [];
|
|
15
|
+
contentNode.forEach((x) => {
|
|
16
|
+
const cinemaInfo = parseCinema(x);
|
|
17
|
+
const cinema = {
|
|
18
|
+
id: getCinemaId(x),
|
|
19
|
+
name: cinemaInfo?.name,
|
|
20
|
+
city: cinemaInfo?.city,
|
|
21
|
+
url: getCinemaUrl(x),
|
|
22
|
+
coords: getCinemaCoords(x),
|
|
23
|
+
screenings: getGroupedFilmsByDate(x)
|
|
24
|
+
};
|
|
25
|
+
cinemas.push(cinema);
|
|
26
|
+
});
|
|
27
|
+
return cinemas;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
export { CinemaScraper };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fetchPage } from "../fetchers/index.mjs";
|
|
3
|
+
import { creatorUrl } from "../vars.mjs";
|
|
4
|
+
import { getCreatorBio, getCreatorBirthdayInfo, getCreatorFilms, getCreatorName, getCreatorPhoto } from "../helpers/creator.helper.mjs";
|
|
5
|
+
import { parse } from "node-html-parser";
|
|
6
|
+
|
|
7
|
+
//#region src/services/creator.service.ts
|
|
8
|
+
var CreatorScraper = class {
|
|
9
|
+
async creator(creatorId, options) {
|
|
10
|
+
const id = Number(creatorId);
|
|
11
|
+
if (isNaN(id)) throw new Error("node-csfd-api: creatorId must be a valid number");
|
|
12
|
+
const creatorHtml = parse(await fetchPage(creatorUrl(id, { language: options?.language }), { ...options?.request }));
|
|
13
|
+
const asideNode = creatorHtml.querySelector(".creator-about");
|
|
14
|
+
const filmsNode = creatorHtml.querySelector(".creator-filmography");
|
|
15
|
+
return this.buildCreator(+creatorId, asideNode, filmsNode);
|
|
16
|
+
}
|
|
17
|
+
buildCreator(id, asideEl, filmsNode) {
|
|
18
|
+
const birthdayInfo = getCreatorBirthdayInfo(asideEl);
|
|
19
|
+
return {
|
|
20
|
+
id,
|
|
21
|
+
name: getCreatorName(asideEl),
|
|
22
|
+
birthday: birthdayInfo?.birthday,
|
|
23
|
+
birthplace: birthdayInfo?.birthPlace,
|
|
24
|
+
photo: getCreatorPhoto(asideEl),
|
|
25
|
+
age: birthdayInfo?.age || null,
|
|
26
|
+
bio: getCreatorBio(asideEl),
|
|
27
|
+
films: getCreatorFilms(filmsNode)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { CreatorScraper };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fetchPage } from "../fetchers/index.mjs";
|
|
3
|
+
import { movieUrl } from "../vars.mjs";
|
|
4
|
+
import { detectSeasonOrEpisodeListType, getEpisodeCode, getMovieBoxMovies, getMovieColorRating, getMovieCreators, getMovieDescriptions, getMovieDuration, getMovieGenres, getMovieOrigins, getMoviePoster, getMoviePremieres, getMovieRandomPhoto, getMovieRating, getMovieRatingCount, getMovieTags, getMovieTitle, getMovieTitlesOther, getMovieTrivia, getMovieType, getMovieVods, getMovieYear, getSeasonOrEpisodeParent, getSeasonsOrEpisodes, getSeriesAndSeasonTitle } from "../helpers/movie.helper.mjs";
|
|
5
|
+
import { parse } from "node-html-parser";
|
|
6
|
+
|
|
7
|
+
//#region src/services/movie.service.ts
|
|
8
|
+
var MovieScraper = class {
|
|
9
|
+
async movie(movieId, options) {
|
|
10
|
+
const id = Number(movieId);
|
|
11
|
+
if (isNaN(id)) throw new Error("node-csfd-api: movieId must be a valid number");
|
|
12
|
+
const movieHtml = parse(await fetchPage(movieUrl(id, { language: options?.language }), { ...options?.request }));
|
|
13
|
+
const pageClasses = movieHtml.querySelector(".page-content").classNames.split(" ");
|
|
14
|
+
const asideNode = movieHtml.querySelector(".aside-movie-profile");
|
|
15
|
+
const movieNode = movieHtml.querySelector(".main-movie-profile");
|
|
16
|
+
const jsonLdString = movieHtml.querySelector("script[type=\"application/ld+json\"]").innerText;
|
|
17
|
+
let jsonLd = null;
|
|
18
|
+
try {
|
|
19
|
+
jsonLd = JSON.parse(jsonLdString);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.error("node-csfd-api: Error parsing JSON-LD", e);
|
|
22
|
+
}
|
|
23
|
+
return this.buildMovie(+movieId, movieNode, asideNode, pageClasses, jsonLd, options);
|
|
24
|
+
}
|
|
25
|
+
buildMovie(movieId, el, asideEl, pageClasses, jsonLd, options) {
|
|
26
|
+
const type = getMovieType(el);
|
|
27
|
+
const { seriesName = null, seasonName = null } = type === "season" ? getSeriesAndSeasonTitle(el) : {};
|
|
28
|
+
const seasonOrEpisodeListType = detectSeasonOrEpisodeListType(el);
|
|
29
|
+
return {
|
|
30
|
+
id: movieId,
|
|
31
|
+
title: type === "season" && seriesName ? seriesName : getMovieTitle(el),
|
|
32
|
+
year: getMovieYear(jsonLd),
|
|
33
|
+
duration: getMovieDuration(jsonLd, el),
|
|
34
|
+
descriptions: getMovieDescriptions(el),
|
|
35
|
+
genres: getMovieGenres(el),
|
|
36
|
+
type,
|
|
37
|
+
url: movieUrl(movieId, { language: options?.language }),
|
|
38
|
+
origins: getMovieOrigins(el),
|
|
39
|
+
colorRating: getMovieColorRating(pageClasses),
|
|
40
|
+
rating: getMovieRating(asideEl),
|
|
41
|
+
ratingCount: getMovieRatingCount(asideEl),
|
|
42
|
+
titlesOther: getMovieTitlesOther(el),
|
|
43
|
+
poster: getMoviePoster(el),
|
|
44
|
+
photo: getMovieRandomPhoto(el),
|
|
45
|
+
trivia: getMovieTrivia(el),
|
|
46
|
+
creators: getMovieCreators(el, options),
|
|
47
|
+
vod: getMovieVods(asideEl),
|
|
48
|
+
tags: getMovieTags(asideEl),
|
|
49
|
+
premieres: getMoviePremieres(asideEl),
|
|
50
|
+
related: getMovieBoxMovies(asideEl, "Související"),
|
|
51
|
+
similar: getMovieBoxMovies(asideEl, "Podobné"),
|
|
52
|
+
seasons: seasonOrEpisodeListType === "seasons" ? getSeasonsOrEpisodes(el) : null,
|
|
53
|
+
episodes: seasonOrEpisodeListType === "episodes" ? getSeasonsOrEpisodes(el) : null,
|
|
54
|
+
parent: type === "season" || type === "episode" ? getSeasonOrEpisodeParent(el) : null,
|
|
55
|
+
episodeCode: type === "episode" ? getEpisodeCode(el) : null,
|
|
56
|
+
seasonName
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { MovieScraper };
|