Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
5 changes: 3 additions & 2 deletions demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
import { csfd } from './src';

// Parse movie
csfd.movie(10135).then((movie) => console.log(movie));
csfd.cinema(1, 'today').then((cinema) => console.log(cinema));
// csfd.movie(10135).then((movie) => console.log(movie));

// csfd.search('matrix').then((search) => console.log(search));

// Parse creator
csfd.creator(2120).then((creator) => console.log(creator));
// csfd.creator(2120).then((creator) => console.log(creator));

/**
* USER RATINGS
Expand Down
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
107 changes: 107 additions & 0 deletions src/helpers/cinema.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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 } => {

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 => {
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() ?? "";
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;
};
19 changes: 17 additions & 2 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,7 +14,8 @@ export class Csfd {
private userRatingsService: UserRatingsScraper,
private movieService: MovieScraper,
private creatorService: CreatorScraper,
private searchService: SearchScraper
private searchService: SearchScraper,
private cinemaService: CinemaScraper
) {}

public async userRatings(
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, 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