Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { csfd } from './src';
csfd.movie(10135).then((movie) => console.log(movie));

// csfd.search('matrix').then((search) => console.log(search));
// csfd.cinema(1, 'today').then((cinema) => console.log(cinema));

// Parse creator
csfd.creator(2120).then((creator) => console.log(creator));
Expand Down Expand Up @@ -32,4 +33,4 @@ csfd.creator(2120).then((creator) => console.log(creator));
// Only TV series
// csfd
// .userRatings('912-bart', { includesOnly: ['seriál'] })
// .then((ratings) => console.log(ratings));
// .then((ratings) => console.log(ratings));
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,4 @@
"lint-staged": {
"*.ts": "eslint --cache --fix"
}
}
}
19 changes: 17 additions & 2 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,16 @@ enum Errors {
CREATOR_FETCH_FAILED = 'CREATOR_FETCH_FAILED',
SEARCH_FETCH_FAILED = 'SEARCH_FETCH_FAILED',
USER_RATINGS_FETCH_FAILED = 'USER_RATINGS_FETCH_FAILED',
PAGE_NOT_FOUND = 'PAGE_NOT_FOUND'
CINEMAS_FETCH_FAILED = 'CINEMAS_FETCH_FAILED',
PAGE_NOT_FOUND = 'PAGE_NOT_FOUND',
}

enum Endpoint {
MOVIE = '/movie/:id',
CREATOR = '/creator/:id',
SEARCH = '/search/:query',
USER_RATINGS = '/user-ratings/:id'
USER_RATINGS = '/user-ratings/:id',
CINEMAS = '/cinemas'
}

type ErrorLog = {
Expand Down Expand Up @@ -173,6 +175,19 @@ app.get(Endpoint.USER_RATINGS, async (req, res) => {
}
});


app.get(Endpoint.CINEMAS, async (req, res) => {
try {
const result = await csfd.cinema(1, 'today');
logMessage('success', { error: null, message: `${Endpoint.CINEMAS}` }, req);
res.json(result);
} catch (error) {
const log: ErrorLog = { error: Errors.CINEMAS_FETCH_FAILED, message: 'Failed to fetch cinemas data: ' + error };
logMessage('error', log, req);
res.status(500).json(log);
}
});

app.use((req, res) => {
const log: ErrorLog = { error: Errors.PAGE_NOT_FOUND, message: 'The requested endpoint could not be found.' };
logMessage('warn', log, req);
Expand Down
109 changes: 109 additions & 0 deletions src/helpers/cinema.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
CSFDCinemaGroupedFilmsByDate,
CSFDCinemaMeta,
CSFDCinemaMovie
} from 'interfaces/cinema.interface';
import { HTMLElement } from 'node-html-parser';
import { CSFDColorRating } from '../interfaces/global';
import { Colors } from '../interfaces/user-ratings.interface';
import { parseColor, parseIdFromUrl } from './global.helper';

export const getColorRating = (el: HTMLElement): CSFDColorRating => {
return parseColor(el?.classNames.split(' ').pop() as Colors);
};

export const getCinemaId = (el: HTMLElement | null): number => {
const id = el?.id?.split('-')[1];
return +id;
};

export const getId = (url: string): number | null => {
if (url) {
return parseIdFromUrl(url);
}
return null;
};

export const getCoords = (el: HTMLElement | null): { lat: number; lng: number } | null => {

if (!el) return null;
const linkMapsEl = el.querySelector('a[href*="q="]');
if (!linkMapsEl) return null;

const linkMaps = linkMapsEl.getAttribute('href');
const [_, latLng] = linkMaps.split('q=');

const coords = latLng.split(',');
if (coords.length !== 2) return null;

const lat = Number(coords[0]);
const lng = Number(coords[1]);
if (Number.isFinite(lat) && Number.isFinite(lng)) {
return { lat, lng };
}
return null;
};

export const getCinemaUrl = (el: HTMLElement | null): string => {
if (!el) return '';
return el.querySelector('a[title="Přejít na webovou stránku kina"]')?.attributes.href ?? '';
};

export const parseCinema = (el: HTMLElement | null): { city: string; name: string } => {
const title = el.querySelector('.box-header h2').innerText.trim();
const [city, name] = title.split(' - ');
return { city, name };
};

export const getGroupedFilmsByDate = (el: HTMLElement | null): CSFDCinemaGroupedFilmsByDate[] => {
const divs = el.querySelectorAll(':scope > div');
const getDatesAndFilms = divs
.map((_, index) => index)
.filter((index) => index % 2 === 0)
.map((index) => {
const [date, films] = divs.slice(index, index + 2);
const dateText = date?.firstChild?.textContent?.trim() ?? null;
return { date: dateText, films: getFilms('', films) };
});

return getDatesAndFilms;
};

export const getFilms = (date: string, el: HTMLElement | null): CSFDCinemaMovie[] => {
const filmNodes = el.querySelectorAll('.cinema-table tr');

const films = filmNodes.map((filmNode) => {
const url = filmNode.querySelector('td.name h3 a')?.attributes.href;
const id = getId(url);
const title = filmNode.querySelector('.name h3')?.text.trim();
const colorRating = getColorRating(filmNode.querySelector('.name .icon'));
const showTimes = filmNode.querySelectorAll('.td-time')?.map((x) => x.textContent.trim());
const meta = filmNode.querySelectorAll('.td-title span')?.map((x) => x.text.trim());

return {
id,
title,
url,
colorRating,
showTimes,
meta: parseMeta(meta)
};
});
return films;
};

export const parseMeta = (meta: string[]): CSFDCinemaMeta[] => {
const metaConvert: CSFDCinemaMeta[] = [];

for (const element of meta) {
if (element === 'T') {
metaConvert.push('subtitles');
} else if (element === 'D') {
metaConvert.push('dubbing');
} else {
metaConvert.push(element);
}
}

return metaConvert;
};
21 changes: 18 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { CSFDCinema, CSFDCinemaPeriod } from 'interfaces/cinema.interface';
import { CSFDCreator } from './interfaces/creator.interface';
import { CSFDMovie } from './interfaces/movie.interface';
import { CSFDSearch } from './interfaces/search.interface';
import { CSFDUserRatingConfig, CSFDUserRatings } from './interfaces/user-ratings.interface';
import { CinemaScraper } from './services/cinema.service';
import { CreatorScraper } from './services/creator.service';
import { MovieScraper } from './services/movie.service';
import { SearchScraper } from './services/search.service';
Expand All @@ -12,8 +14,9 @@ export class Csfd {
private userRatingsService: UserRatingsScraper,
private movieService: MovieScraper,
private creatorService: CreatorScraper,
private searchService: SearchScraper
) {}
private searchService: SearchScraper,
private cinemaService: CinemaScraper
) { }

public async userRatings(
user: string | number,
Expand All @@ -33,10 +36,22 @@ export class Csfd {
public async search(text: string): Promise<CSFDSearch> {
return this.searchService.search(text);
}

public async cinema(district: number | string, period: CSFDCinemaPeriod): Promise<CSFDCinema[]> {
return this.cinemaService.cinemas(+district, period);
}
}

const movieScraper = new MovieScraper();
const userRatingsScraper = new UserRatingsScraper();
const cinemaScraper = new CinemaScraper();
const creatorScraper = new CreatorScraper();
const searchScraper = new SearchScraper();
export const csfd = new Csfd(userRatingsScraper, movieScraper, creatorScraper, searchScraper);

export const csfd = new Csfd(
userRatingsScraper,
movieScraper,
creatorScraper,
searchScraper,
cinemaScraper
);
25 changes: 25 additions & 0 deletions src/interfaces/cinema.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CSFDMovieListItem } from './movie.interface';

export interface CSFDCinema {
id: number;
name: string;
city: string;
url: string;
coords: { lat: number; lng: number };
region?: string;
screenings: CSFDCinemaGroupedFilmsByDate[];
}

export interface CSFDCinemaGroupedFilmsByDate {
date: string;
films: CSFDCinemaMovie[];
}

export interface CSFDCinemaMovie extends CSFDMovieListItem {
meta: CSFDCinemaMeta[];
showTimes: string[];
}

export type CSFDCinemaMeta = 'dubbing' | '3D' | 'subtitles' | string;

export type CSFDCinemaPeriod = 'today' | 'weekend' | 'week' | 'tomorrow' | 'month';
49 changes: 49 additions & 0 deletions src/services/cinema.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { HTMLElement, parse } from 'node-html-parser';
import { fetchPage } from '../fetchers';
import { CSFDCinema, CSFDCinemaPeriod } from '../interfaces/cinema.interface';
import { cinemasUrl } from '../vars';
import {
getCinemaId,
getCinemaUrl,
getCoords,
getGroupedFilmsByDate,
parseCinema
} from './../helpers/cinema.helper';

export class CinemaScraper {
private cinema: CSFDCinema[];

public async cinemas(
district: number = 1,
period: CSFDCinemaPeriod = 'today'
): Promise<CSFDCinema[]> {
const url = cinemasUrl(district, period);
const response = await fetchPage(url);

const cinemasHtml = parse(response);

const contentNode = cinemasHtml.querySelectorAll('#snippet--cinemas section.box');

this.buildCinemas(contentNode);
return this.cinema;
}

private buildCinemas(contentNode: HTMLElement[]) {
const cinemas: CSFDCinema[] = [];

contentNode.forEach((x) => {
const cinemaInfo = parseCinema(x);
const cinema: CSFDCinema = {
id: getCinemaId(x),
name: cinemaInfo?.name,
city: cinemaInfo?.city,
url: getCinemaUrl(x),
coords: getCoords(x),
screenings: getGroupedFilmsByDate(x)
};
cinemas.push(cinema);
});

this.cinema = cinemas;
}
}
6 changes: 3 additions & 3 deletions src/services/search.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class SearchScraper {
const users: CSFDSearchUser[] = [];
const tvSeries: CSFDSearchMovie[] = [];

moviesNode.map((m) => {
moviesNode.forEach((m) => {
const url = getUrl(m);

const movie: CSFDSearchMovie = {
Expand All @@ -57,7 +57,7 @@ export class SearchScraper {
movies.push(movie);
});

usersNode.map((m) => {
usersNode.forEach((m) => {
const url = getUserUrl(m);

const user: CSFDSearchUser = {
Expand All @@ -70,7 +70,7 @@ export class SearchScraper {
users.push(user);
});

tvSeriesNode.map((m) => {
tvSeriesNode.forEach((m) => {
const url = getUrl(m);

const user: CSFDSearchMovie = {
Expand Down
6 changes: 6 additions & 0 deletions src/vars.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CSFDCinemaPeriod } from 'interfaces/cinema.interface';

export const userRatingsUrl = (user: string | number, page?: number): string =>
`https://www.csfd.cz/uzivatel/${encodeURIComponent(user)}/hodnoceni/${
page ? '?page=' + page : ''
Expand All @@ -9,5 +11,9 @@ export const movieUrl = (movie: number): string =>
export const creatorUrl = (creator: number | string): string =>
`https://www.csfd.cz/tvurce/${encodeURIComponent(creator)}`;

export const cinemasUrl = (district: number | string, period: CSFDCinemaPeriod): string => {
return `https://www.csfd.cz/kino/?period=${period}&district=${district}`;
};

export const searchUrl = (text: string): string =>
`https://www.csfd.cz/hledat/?q=${encodeURIComponent(text)}`;
Loading