Skip to content

Commit 776569e

Browse files
authored
Merge pull request #68 from bartholomej/feat/domain
feat: language support
2 parents e0b7f47 + 952f65c commit 776569e

23 files changed

+294
-110
lines changed

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
20
1+
22

server.ts

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { dirname, join } from 'path';
77
import { fileURLToPath } from 'url';
88
import { csfd } from './src';
99
import { CSFDFilmTypes } from './src/dto/global';
10+
import { CSFDLanguage } from './src/types';
1011

1112
const __filename = fileURLToPath(import.meta.url);
1213
const __dirname = dirname(__filename);
@@ -93,13 +94,23 @@ const port = process.env.PORT || 3000;
9394
// --- Config ---
9495
const API_KEY_NAME = process.env.API_KEY_NAME || 'x-api-key';
9596
const API_KEY = process.env.API_KEY;
97+
const RAW_LANGUAGE = process.env.LANGUAGE;
98+
const isSupportedLanguage = (value: unknown): value is CSFDLanguage =>
99+
value === 'cs' || value === 'en' || value === 'sk';
100+
101+
const BASE_LANGUAGE = isSupportedLanguage(RAW_LANGUAGE) ? RAW_LANGUAGE : undefined;
96102

97103
const API_KEYS_LIST = API_KEY
98104
? API_KEY.split(/[,;\s]+/)
99-
.map((k) => k.trim())
100-
.filter(Boolean)
105+
.map((k) => k.trim())
106+
.filter(Boolean)
101107
: [];
102108

109+
// Configure base URL if provided
110+
if (BASE_LANGUAGE) {
111+
csfd.setOptions({ language: BASE_LANGUAGE });
112+
}
113+
103114
// const limiterMinutes = 15;
104115

105116
// const LIMITER = rateLimit({
@@ -170,10 +181,15 @@ app.get(['/movie/', '/creator/', '/search/', '/user-ratings/', '/user-reviews/']
170181
});
171182

172183
app.get(Endpoint.MOVIE, async (req, res) => {
184+
const rawLanguage = req.query.language;
185+
const language = isSupportedLanguage(rawLanguage) ? rawLanguage : undefined;
186+
173187
try {
174-
const movie = await csfd.movie(+req.params.id);
188+
const movie = await csfd.movie(+req.params.id, {
189+
language
190+
});
175191
res.json(movie);
176-
logMessage('success', { error: null, message: `${Endpoint.MOVIE}: ${req.params.id}` }, req);
192+
logMessage('success', { error: null, message: `${Endpoint.MOVIE}: ${req.params.id}${language ? ` [${language}]` : ''}` }, req);
177193
} catch (error) {
178194
const log: ErrorLog = {
179195
error: Errors.MOVIE_FETCH_FAILED,
@@ -185,10 +201,14 @@ app.get(Endpoint.MOVIE, async (req, res) => {
185201
});
186202

187203
app.get(Endpoint.CREATOR, async (req, res) => {
204+
const rawLanguage = req.query.language;
205+
const language = isSupportedLanguage(rawLanguage) ? rawLanguage : undefined;
188206
try {
189-
const result = await csfd.creator(+req.params.id);
207+
const result = await csfd.creator(+req.params.id, {
208+
language
209+
});
190210
res.json(result);
191-
logMessage('success', { error: null, message: `${Endpoint.CREATOR}: ${req.params.id}` }, req);
211+
logMessage('success', { error: null, message: `${Endpoint.CREATOR}: ${req.params.id}${language ? ` [${language}]` : ''}` }, req);
192212
} catch (error) {
193213
const log: ErrorLog = {
194214
error: Errors.CREATOR_FETCH_FAILED,
@@ -200,10 +220,14 @@ app.get(Endpoint.CREATOR, async (req, res) => {
200220
});
201221

202222
app.get(Endpoint.SEARCH, async (req, res) => {
223+
const rawLanguage = req.query.language;
224+
const language = isSupportedLanguage(rawLanguage) ? rawLanguage : undefined;
203225
try {
204-
const result = await csfd.search(req.params.query);
226+
const result = await csfd.search(req.params.query, {
227+
language
228+
});
205229
res.json(result);
206-
logMessage('success', { error: null, message: `${Endpoint.SEARCH}: ${req.params.query}` }, req);
230+
logMessage('success', { error: null, message: `${Endpoint.SEARCH}: ${req.params.query}${language ? ` [${language}]` : ''}` }, req);
207231
} catch (error) {
208232
const log: ErrorLog = {
209233
error: Errors.SEARCH_FETCH_FAILED,
@@ -216,6 +240,9 @@ app.get(Endpoint.SEARCH, async (req, res) => {
216240

217241
app.get(Endpoint.USER_RATINGS, async (req, res) => {
218242
const { allPages, allPagesDelay, excludes, includesOnly, page } = req.query;
243+
const rawLanguage = req.query.language;
244+
const language = isSupportedLanguage(rawLanguage) ? rawLanguage : undefined;
245+
219246
try {
220247
const result = await csfd.userRatings(req.params.id, {
221248
allPages: allPages === 'true',
@@ -225,11 +252,13 @@ app.get(Endpoint.USER_RATINGS, async (req, res) => {
225252
? ((includesOnly as string).split(',') as CSFDFilmTypes[])
226253
: undefined,
227254
page: page ? +page : undefined
255+
}, {
256+
language
228257
});
229258
res.json(result);
230259
logMessage(
231260
'success',
232-
{ error: null, message: `${Endpoint.USER_RATINGS}: ${req.params.id}` },
261+
{ error: null, message: `${Endpoint.USER_RATINGS}: ${req.params.id}${language ? ` [${language}]` : ''}` },
233262
req
234263
);
235264
} catch (error) {
@@ -244,6 +273,9 @@ app.get(Endpoint.USER_RATINGS, async (req, res) => {
244273

245274
app.get(Endpoint.USER_REVIEWS, async (req, res) => {
246275
const { allPages, allPagesDelay, excludes, includesOnly, page } = req.query;
276+
const rawLanguage = req.query.language;
277+
const language = isSupportedLanguage(rawLanguage) ? rawLanguage : undefined;
278+
247279
try {
248280
const result = await csfd.userReviews(req.params.id, {
249281
allPages: allPages === 'true',
@@ -253,11 +285,13 @@ app.get(Endpoint.USER_REVIEWS, async (req, res) => {
253285
? ((includesOnly as string).split(',') as CSFDFilmTypes[])
254286
: undefined,
255287
page: page ? +page : undefined
288+
}, {
289+
language
256290
});
257291
res.json(result);
258292
logMessage(
259293
'success',
260-
{ error: null, message: `${Endpoint.USER_REVIEWS}: ${req.params.id}` },
294+
{ error: null, message: `${Endpoint.USER_REVIEWS}: ${req.params.id}${language ? ` [${language}]` : ''}` },
261295
req
262296
);
263297
} catch (error) {
@@ -271,9 +305,14 @@ app.get(Endpoint.USER_REVIEWS, async (req, res) => {
271305
});
272306

273307
app.get(Endpoint.CINEMAS, async (req, res) => {
308+
const rawLanguage = req.query.language;
309+
const language = isSupportedLanguage(rawLanguage) ? rawLanguage : undefined;
310+
274311
try {
275-
const result = await csfd.cinema(1, 'today');
276-
logMessage('success', { error: null, message: `${Endpoint.CINEMAS}` }, req);
312+
const result = await csfd.cinema(1, 'today', {
313+
language
314+
});
315+
logMessage('success', { error: null, message: `${Endpoint.CINEMAS}${language ? ` [${language}]` : ''}` }, req);
277316
res.json(result);
278317
} catch (error) {
279318
const log: ErrorLog = {
@@ -310,12 +349,15 @@ app.listen(port, () => {
310349
console.log(`Docs: ${packageJson.homepage}`);
311350
console.log(`Endpoints: ${Object.values(Endpoint).join(', ')}\n`);
312351

313-
console.log(`API is running on: http://localhost:${port}\n`);
352+
console.log(`API is running on: http://localhost:${port}`);
353+
if (BASE_LANGUAGE) {
354+
console.log(`Base language configured: ${BASE_LANGUAGE}\n`);
355+
}
314356
if (API_KEYS_LIST.length === 0) {
315357
console.log(
316358
'\x1b[31m%s\x1b[0m',
317359
'⚠️ Server is OPEN!\n- Your server will be open to the world and potentially everyone can use it without any restriction.\n- To enable some basic protection, set API_KEY environment variable (single value or comma-separated list) and provide the same value in request header: ' +
318-
API_KEY_NAME
360+
API_KEY_NAME
319361
);
320362
} else {
321363
console.log(

src/dto/movie.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,42 @@ export type CSFDCreatorGroups =
137137
| 'Hudba'
138138
| 'Hrají'
139139
| 'Produkce'
140+
| 'Casting'
140141
| 'Střih'
142+
| 'Zvuk'
143+
| 'Masky'
141144
| 'Předloha'
142145
| 'Scénografie'
143146
| 'Kostýmy';
144147

148+
149+
export type CSFDCreatorGroupsEnglish =
150+
| 'Directed by'
151+
| 'Screenplay'
152+
| 'Cinematography'
153+
| 'Composer'
154+
| 'Cast'
155+
| 'Produced by'
156+
| 'Casting'
157+
| 'Editing'
158+
| 'Sound'
159+
| 'Make-up'
160+
| 'Production design'
161+
| 'Based on'
162+
| 'Costumes';
163+
164+
export type CSFDCreatorGroupsSlovak =
165+
| 'Réžia'
166+
| 'Scenár'
167+
| 'Kamera'
168+
| 'Hudba'
169+
| 'Hrajú'
170+
| 'Predloha'
171+
| 'Produkcia'
172+
| 'Strih'
173+
| 'Kostýmy'
174+
| 'Scénografia';
175+
145176
export interface CSFDPremiere {
146177
country: string;
147178
format: string;

src/dto/options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface CSFDOptions {
2+
language?: CSFDLanguage;
3+
request?: RequestInit;
4+
}
5+
6+
export type CSFDLanguage = 'cs' | 'en' | 'sk';

src/dto/user-ratings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ export interface CSFDUserRatingConfig {
2222
page?: number;
2323
}
2424

25-
export type Colors = 'lightgrey' | 'blue' | 'red' | 'grey';
25+
export type CSFDColors = 'lightgrey' | 'blue' | 'red' | 'grey';

src/helpers/cinema.helper.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { HTMLElement } from 'node-html-parser';
22
import { CSFDCinemaGroupedFilmsByDate, CSFDCinemaMeta, CSFDCinemaMovie } from '../dto/cinema';
33
import { CSFDColorRating } from '../dto/global';
4-
import { Colors } from '../dto/user-ratings';
4+
import { CSFDColors } from '../dto/user-ratings';
55
import { parseColor, parseIdFromUrl } from './global.helper';
66

77
export const getCinemaColorRating = (el: HTMLElement | null): CSFDColorRating => {
88
const classes: string[] = el?.classNames.split(' ') ?? [];
99
const last = classes.length ? classes[classes.length - 1] : undefined;
10-
return last ? parseColor(last as Colors) : 'unknown';
10+
return last ? parseColor(last as CSFDColors) : 'unknown';
1111
};
1212

1313
export const getCinemaId = (el: HTMLElement | null): number => {

src/helpers/creator.helper.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { HTMLElement } from 'node-html-parser';
22
import { CSFDCreatorScreening } from '../dto/creator';
33
import { CSFDColorRating } from '../dto/global';
4-
import { Colors } from '../dto/user-ratings';
4+
import { CSFDColors } from '../dto/user-ratings';
55
import { addProtocol, parseColor, parseIdFromUrl } from './global.helper';
66

77
const getCreatorColorRating = (el: HTMLElement | null): CSFDColorRating => {
88
const classes: string[] = el?.classNames.split(' ') ?? [];
9-
const last = classes[classes.length - 1] as Colors | undefined;
9+
const last = classes[classes.length - 1] as CSFDColors | undefined;
1010
return parseColor(last);
1111
};
1212

src/helpers/global.helper.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CSFDColorRating } from '../dto/global';
2-
import { Colors } from '../dto/user-ratings';
2+
import { CSFDColors } from '../dto/user-ratings';
33

44
export const parseIdFromUrl = (url: string): number => {
55
if (url) {
@@ -26,7 +26,7 @@ export const getColor = (cls: string): CSFDColorRating => {
2626
}
2727
};
2828

29-
export const parseColor = (quality: Colors): CSFDColorRating => {
29+
export const parseColor = (quality: CSFDColors): CSFDColorRating => {
3030
switch (quality) {
3131
case 'lightgrey':
3232
return 'unknown';

src/helpers/movie.helper.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { CSFDColorRating } from '../dto/global';
33
import {
44
CSFDBoxContent,
55
CSFDCreatorGroups,
6+
CSFDCreatorGroupsEnglish,
7+
CSFDCreatorGroupsSlovak,
68
CSFDGenres,
79
CSFDMovieCreator,
810
CSFDMovieListItem,
@@ -13,6 +15,68 @@ import {
1315
} from '../dto/movie';
1416
import { addProtocol, getColor, parseISO8601Duration, parseIdFromUrl } from './global.helper';
1517

18+
/**
19+
* Maps language-specific movie creator group labels.
20+
* @param language - The language code (e.g., 'en', 'cs')
21+
* @param key - The key of the creator group (e.g., 'directors', 'writers')
22+
* @returns The localized label for the creator group
23+
*/
24+
export const getLocalizedCreatorLabel = (
25+
language: string | undefined,
26+
key: 'directors' | 'writers' | 'cinematography' | 'music' | 'actors' | 'basedOn' | 'producers' | 'filmEditing' | 'costumeDesign' | 'productionDesign' | 'casting' | 'sound' | 'makeup'
27+
): CSFDCreatorGroups | CSFDCreatorGroupsEnglish | CSFDCreatorGroupsSlovak => {
28+
const labels: Record<string, Record<string, CSFDCreatorGroups | CSFDCreatorGroupsEnglish | CSFDCreatorGroupsSlovak>> = {
29+
en: {
30+
directors: 'Directed by',
31+
writers: 'Screenplay',
32+
cinematography: 'Cinematography',
33+
music: 'Composer',
34+
actors: 'Cast',
35+
basedOn: 'Based on',
36+
producers: 'Produced by',
37+
filmEditing: 'Editing',
38+
costumeDesign: 'Costumes',
39+
productionDesign: 'Production design',
40+
casting: 'Casting',
41+
sound: 'Sound',
42+
makeup: 'Make-up'
43+
},
44+
cs: {
45+
directors: 'Režie',
46+
writers: 'Scénář',
47+
cinematography: 'Kamera',
48+
music: 'Hudba',
49+
actors: 'Hrají',
50+
basedOn: 'Předloha',
51+
producers: 'Produkce',
52+
filmEditing: 'Střih',
53+
costumeDesign: 'Kostýmy',
54+
productionDesign: 'Scénografie',
55+
casting: 'Casting',
56+
sound: 'Zvuk',
57+
makeup: 'Masky'
58+
},
59+
sk: {
60+
directors: 'Réžia',
61+
writers: 'Scenár',
62+
cinematography: 'Kamera',
63+
music: 'Hudba',
64+
actors: 'Hrajú',
65+
basedOn: 'Predloha',
66+
producers: 'Produkcia',
67+
filmEditing: 'Strih',
68+
costumeDesign: 'Kostýmy',
69+
productionDesign: 'Scénografia',
70+
casting: 'Casting',
71+
sound: 'Zvuk',
72+
makeup: 'Masky'
73+
}
74+
};
75+
76+
const lang = language || 'cs'; // Default to Czech
77+
return (labels[lang] || labels['cs'])[key];
78+
};
79+
1680
export const getMovieId = (el: HTMLElement): number => {
1781
const url = el.querySelector('.tabs .tab-nav-list a').attributes.href;
1882
return parseIdFromUrl(url);
@@ -177,7 +241,7 @@ const parseMoviePeople = (el: HTMLElement): CSFDMovieCreator[] => {
177241
);
178242
};
179243

180-
export const getMovieGroup = (el: HTMLElement, group: CSFDCreatorGroups): CSFDMovieCreator[] => {
244+
export const getMovieGroup = (el: HTMLElement, group: CSFDCreatorGroups | CSFDCreatorGroupsEnglish | CSFDCreatorGroupsSlovak): CSFDMovieCreator[] => {
181245
const creators = el.querySelectorAll('.creators h4');
182246
const element = creators.filter((elem) => elem.textContent.trim().includes(group))[0];
183247
if (element?.parentNode) {

src/helpers/search.helper.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { HTMLElement } from 'node-html-parser';
22
import { CSFDColorRating, CSFDFilmTypes } from '../dto/global';
33
import { CSFDMovieCreator } from '../dto/movie';
4-
import { Colors } from '../dto/user-ratings';
4+
import { CSFDColors } from '../dto/user-ratings';
55
import { addProtocol, parseColor, parseIdFromUrl } from './global.helper';
66

77
type Creator = 'Režie:' | 'Hrají:';
@@ -25,7 +25,7 @@ export const getSearchUrl = (el: HTMLElement): string => {
2525

2626
export const getSearchColorRating = (el: HTMLElement): CSFDColorRating => {
2727
return parseColor(
28-
el.querySelector('.article-header i.icon').classNames.split(' ').pop() as Colors
28+
el.querySelector('.article-header i.icon').classNames.split(' ').pop() as CSFDColors
2929
);
3030
};
3131

0 commit comments

Comments
 (0)