diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..3b89550 --- /dev/null +++ b/.babelrc @@ -0,0 +1,9 @@ +{ + "env": { + "test": { + "plugins": [ + "@babel/plugin-transform-modules-commonjs" + ] + } + } +} \ No newline at end of file diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..8de5049 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,22 @@ +name: Node.js CI + +on: + pull_request: + branches: + - master + - develop + - 'release/**' + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '19.x' + - run: npm install + - run: npm test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 15d3837..5ab66a4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ node_modules/ *.DS_Store .idea helper.js -package-lock.json \ No newline at end of file +package-lock.json +.vscode +coverage \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..2e12958 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +export { search, film, types } from './src/index.js'; \ No newline at end of file diff --git a/package.json b/package.json index 2518540..55c2a82 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,30 @@ "name": "faparser", "version": "1.1.1", "description": "Parser for Filmaffinity site", - "main": "src/faparser.js", - "scripts": { - "test": "\"\"" + "main": "index.js", + "author": "Alberto Espinilla", + "license": "MIT", + "bugs": { + "url": "https://github.com/aespinilla/faparser/issues" }, + "homepage": "https://github.com/aespinilla/faparser#readme", "repository": { "type": "git", - "url": "git+https://github.com/aespinilla/faparser.git" + "url": "https://github.com/aespinilla/faparser.git" + }, + "type": "module", + "scripts": { + "test": "jest" + }, + "jest": { + "collectCoverage": true, + "moduleFileExtensions": [ + "js", + "jsx", + "json", + "ts", + "tsx" + ] }, "keywords": [ "filmaffinity", @@ -23,14 +40,14 @@ "series", "film" ], - "author": "Alberto Espinilla", - "license": "MIT", - "bugs": { - "url": "https://github.com/aespinilla/faparser/issues" - }, - "homepage": "https://github.com/aespinilla/faparser#readme", "dependencies": { - "cheerio": "1.0.0-rc.2", - "node-fetch": "^2.0" + "cheerio": "1.0.0-rc.2" + }, + "devDependencies": { + "@babel/core": "^7.20.12", + "@babel/plugin-transform-modules-commonjs": "^7.20.11", + "@babel/preset-env": "^7.20.2", + "jest": "^28.1.3", + "supertest": "^6.2.4" } } diff --git a/src/config/config.js b/src/config/config.js new file mode 100644 index 0000000..7e32fb5 --- /dev/null +++ b/src/config/config.js @@ -0,0 +1,11 @@ +export const Config = { + "BASE_URL": "https://www.filmaffinity.com", + "paths": { + "GENRE": "/moviegenre.php?genre=", + "TOPIC": "/movietopic.php?topic=", + "IMAGES": "/filmimages.php?movie_id=", + "TRAILERS": "/evideos.php?movie_id=", + "PRO_REVIEWS": "/pro-reviews.php?movie-id=", + "SEARCH": "/search.php?stype=" + } +}; \ No newline at end of file diff --git a/src/controller/filmController.js b/src/controller/filmController.js new file mode 100644 index 0000000..e929509 --- /dev/null +++ b/src/controller/filmController.js @@ -0,0 +1,11 @@ +import { request } from "../request/request.js"; +import { filmUrlBuilder as urlBuilder } from "../urlBuilder/index.js"; +import { filmParser as parse } from "../parser/index.js"; + +export const fetchFilm = async (data) => { + const url = urlBuilder(data); + const result = await request(url); + result.lang = data.lang; + const film = parse(result); + return { id: `${data.id}`, ...film } +} \ No newline at end of file diff --git a/src/controller/imagesController.js b/src/controller/imagesController.js new file mode 100644 index 0000000..9156f7f --- /dev/null +++ b/src/controller/imagesController.js @@ -0,0 +1,11 @@ +import { request } from "../request/request.js"; +import { imagesUrlBuilder as urlBuilder } from "../urlBuilder/index.js"; +import { imagesParser as parse } from "../parser/index.js"; + +export const fetchImages = async (data) => { + data.type = 'IMAGES' + const url = urlBuilder(data); + const response = await request(url); + const result = parse(response); + return result; +} \ No newline at end of file diff --git a/src/controller/index.js b/src/controller/index.js new file mode 100644 index 0000000..492c90b --- /dev/null +++ b/src/controller/index.js @@ -0,0 +1,5 @@ +export { fetchFilm as filmController } from './filmController.js'; +export { fetchImages as imagesController } from './imagesController.js'; +export { fetchProReviews as proReviewsController } from './proReviewsController.js'; +export { search as searchController } from './searchController.js'; +export { fetchTrailers as trailersController } from './trailersController.js'; \ No newline at end of file diff --git a/src/controller/proReviewsController.js b/src/controller/proReviewsController.js new file mode 100644 index 0000000..7c1d919 --- /dev/null +++ b/src/controller/proReviewsController.js @@ -0,0 +1,11 @@ +import { request } from "../request/request.js"; +import { proReviewsUrlBuilder as urlBuilder } from "../urlBuilder/index.js"; +import { proReviewsParser as parse } from "../parser/index.js"; + +export const fetchProReviews = async (data) => { + data.type = 'PRO_REVIEWS'; + const url = urlBuilder(data); + const response = await request(url); + const result = parse(response); + return result; +} \ No newline at end of file diff --git a/src/controller/searchController.js b/src/controller/searchController.js new file mode 100644 index 0000000..b8385a2 --- /dev/null +++ b/src/controller/searchController.js @@ -0,0 +1,81 @@ +import url from 'url'; +import { request } from "../request/request.js"; +import { searchUrlBuilder, genreUrlBuilder, topicUrlBuilder } from "../urlBuilder/index.js"; +import { filmParser, specialSearch, searchParser } from "../parser/index.js"; + +export const search = async (data) => { + if (data.type === 'TOPIC') { + const result = await getTopics(data); + result.lang = data.lang; + return buildOutput(result, false); + } + + if (data.type === 'GENRE') { + const result = await getGenres(data); + result.lang = data.lang; + return buildOutput(result, false); + } + + const url = searchUrlBuilder(data); + const response = await request(url); + response.lang = data.lang; + + if (isFilm(response.response.url)) { + const id = getId(response.response.url); + const film = filmParser(response); + const result = mapFilm(id, film); + return buildOutput([result], false); + } + + const result = searchParser(response); + return buildOutput(result.result, result.hasMore); +} + +const isFilm = (responseUrl) => { + const pathname = url.parse(responseUrl).pathname; + return pathname.includes('film'); +} + +const getId = (responseUrl) => { + const pathname = url.parse(responseUrl).pathname; + return pathname.substring(pathname.indexOf('film') + 'film'.length, pathname.indexOf('.')); +} + +const getTopics = async (data) => { + const url = topicUrlBuilder(data); + return await getSpecialSearch(url); +} + +const getGenres = async (data) => { + const url = genreUrlBuilder(data); + return await getSpecialSearch(url); +} + +const getSpecialSearch = async (url) => { + const response = await request(url); + const result = specialSearch(response); + return result; +} + +const mapFilm = (id, film) => { + return { + id: id, + url: film.url, + thumbnail: film.coverImages.imageUrlMed.replace("mmed", "msmall"), + year: film.year, + title: film.titles.title, + directors: film.directors, + cast: film.cast, + country: film.country, + rating: film.rating, + votes: film.votes + } +} + +const buildOutput = (result, hasMore) => { + return { + more: hasMore || false, + count: result.length, + result: result + } +} \ No newline at end of file diff --git a/src/controller/trailersController.js b/src/controller/trailersController.js new file mode 100644 index 0000000..c5d37f5 --- /dev/null +++ b/src/controller/trailersController.js @@ -0,0 +1,11 @@ +import { request } from "../request/request.js"; +import { trailersUrlBuilder as urlBuilder } from "../urlBuilder/index.js"; +import { trailersParser as parse } from "../parser/index.js"; + +export const fetchTrailers = async (data) => { + data.type = 'TRAILERS'; + const url = urlBuilder(data); + const response = await request(url); + const result = parse(response); + return result; +} \ No newline at end of file diff --git a/src/faparser.js b/src/faparser.js deleted file mode 100644 index 5c8674f..0000000 --- a/src/faparser.js +++ /dev/null @@ -1,73 +0,0 @@ -const parser = require('./parser') -const requestfa = require('./requestfa') - -async function search(data) { - data.isFilm = false - data.type = data.type || 'TITLE' - let res = await requestfa.FArequest(data) - res.lang = data.lang - res.type = data.type - return parser.parseSearch(res) -} - -async function preview(data) { - data.isFilm = true - let result = await requestfa.FArequest(data) - const film = parser.parseFilm(result) - const filmResult = { - id: data.id, - url: film.url, - thumbnail: film.imageUrlMed.replace("mmed", "msmall"), - year: film.year, - title: film.title, - directors: film.directors, - cast: film.cast, - country: film.country, - rating: film.rating ? film.rating.replace(',', '.') : 0, - votes: film.votes - } - return filmResult -} - -const film = async (data) => { - data.isFilm = true - let film = await filmTaskPromise(data) - return film -} - -async function filmTaskPromise(data) { - const f = data - const t = clone(data) - t.isFilm = false - t.type = 'TRAILERS' - const i = clone(data) - i.isFilm = false - i.type = 'IMAGES' - const r = clone(data) - r.isFilm = false - r.type = 'PRO_REVIEWS' - let result = await Promise.all([requestfa.requestSource(f), requestfa.requestSource(i), requestfa.requestSource(t), requestfa.requestSource(r)]) - const film = parser.parseFilm(result[0]) - film.id = data.id - film.images = parser.parseImages(result[1]) - film.trailers = parser.parseTrailers(result[2]) - film.proReviews = parser.parseProReviews(result[3]) - return film -} - -function clone(o) { - return JSON.parse(JSON.stringify(o)) -} - -module.exports = { - film: film, - preview: preview, - search: search, - types: { - TITLE: 'TITLE', - GENRE: 'GENRE', - TOPIC: 'TOPIC', - DIRECTOR: 'DIRECTOR', - CAST: 'CAST' - } -} \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..ba7de95 --- /dev/null +++ b/src/index.js @@ -0,0 +1,71 @@ +import { + filmController, + searchController, + trailersController, + imagesController, + proReviewsController +} from './controller/index.js'; + +const optionsFilm = { + lang: 'es', // es, en + extra: [] // trailers, images, reviews +} + +const optionsSearch = { + lang: 'es', // es, en + type: 'TITLE', //'TITLE', 'GENRE', 'TOPIC', 'DIRECTOR', 'CAST' + start: 0, // start page +} + +export const types = { + TITLE: 'TITLE', + GENRE: 'GENRE', + TOPIC: 'TOPIC', + DIRECTOR: 'DIRECTOR', + CAST: 'CAST' +} + +export const search = async (data) => { + const search = { ...data } + search.type = data.type || 'TITLE' + const result = await searchController(search); + return result; +} + +export const film = async (data) => { + let result = await Promise.all([ + fetchFilm(data), + fetchImages(data), + fetchTrailers(data), + fetchProReviews(data) + ]) + const [film, images, trailers, reviews] = result + film.images = images + film.trailers = trailers + film.proReviews = reviews + return film +} + +const fetchFilm = async (data) => { + const filmData = { ...data } + const result = await filmController(data); + return result; +} + +const fetchTrailers = async (data) => { + const trailers = { ...data } + const result = await trailersController(trailers); + return result; +} + +const fetchImages = async (data) => { + const images = { ...data } + const result = await imagesController(images); + return result; +} + +const fetchProReviews = async (data) => { + const reviews = { ...data } + const result = await proReviewsController(reviews); + return result; +} \ No newline at end of file diff --git a/src/parser.js b/src/parser.js deleted file mode 100644 index b97edc4..0000000 --- a/src/parser.js +++ /dev/null @@ -1,409 +0,0 @@ -/** - * Created by aespinilla on 8/3/17. - */ -const jQuery = require('cheerio') -const url = require('url') - -const BASE_URL = "https://www.filmaffinity.com" - -module.exports = { - parseFilm: parseFilm, - parseSearch: parseSearch, - parseTrailers: parseTrailers, - parseImages: parseImages, - parseProReviews: parseProReviews -} - -function parseFilm(data) { - try { - const content = jQuery(data.body) - const film = {} - film.url = data.url - film.imageUrl = content.find('#movie-main-image-container').find('a').attr('href'); - let imageUrlMed = content.find('#movie-main-image-container').find('img').attr('src'); - if (imageUrlMed.includes('noimgfull')) { - imageUrlMed = BASE_URL + imageUrlMed - film.imageUrl = imageUrlMed - } - film.imageUrlMed = imageUrlMed - film.rating = content.find('#movie-rat-avg').attr('content'); - film.votes = content.find('#movie-count-rat').find('span').attr('content') - film.title = content.find('#main-title').find('span').text().trim(); - content.find('.movie-info dt').each(function (index, a) { - const bind = jQuery(a).text().trim().toLowerCase(); - switch (bind) { - case "original title": - case "título original": { - const element = jQuery(a) - const tO = element.next().text().trim() - film.titleOrig = tO - const akas = [] - content.find('dd.akas li').each(function (index, akatitle) { - const ak = jQuery(akatitle).text() - akas.push(ak) - }) - if (akas.length != 0) { - film.akas = akas - film.titleOrig = tO.substring(0, tO.length - 3).trim() - } - break - } - case "year": - case "año": { - film.year = jQuery(a).next().text().trim() - break - } - case "running time": - case "duración": { - film.running = jQuery(a).next().text().trim() - break - } - case "country": - case "país": { - film.country = { - imgCountry: BASE_URL + jQuery(a).next().find('img').attr('src'), - country: jQuery(a).next().find('img').attr('alt'), - } - break - } - case "director": - case "dirección": { - film.directors = [] - jQuery(a).next().find('a').each(function (index2, directors) { - film.directors.push({ - name: jQuery(directors).find('span').text().trim(), - request: { - query: jQuery(directors).find('span').text().trim(), - type: 'DIRECTOR', - lang: data.lang - } - }) - }) - break - } - case "screenwriter": - case "guión": { - film.screenwriter = [] - jQuery(a).next().find('.nb span').each(function (index2, guion) { - film.screenwriter.push(jQuery(guion).text().trim()) - }) - break - } - case "music": - case "música": { - film.music = []; - jQuery(a).next().find('.nb span').each(function (index3, music) { - film.music.push(jQuery(music).text().trim()) - }) - break - } - case "cinematography": - case "fotografía": { - film.cinematography = []; - jQuery(a).next().find('.nb span').each(function (index3, foto) { - film.cinematography.push(jQuery(foto).text().trim()) - }) - break - } - case "cast": - case "reparto": { - film.cast = []; - jQuery(a).next().find('a').find('span').each(function (index3, actor) { - film.cast.push({ - name: jQuery(actor).text().trim(), - request: { - query: jQuery(actor).text().trim(), - type: 'CAST', - lang: data.lang - } - }) - }) - break - } - case "producer": - case "productora": { - film.production = jQuery(a).next().find('.nb span').text().trim() - break - } - case "genre": - case "género": { - film.genre = [] - jQuery(a).next().find('a').each(function (index3, genero) { - const link = jQuery(genero).attr('href') - const g = jQuery(genero).text().trim() - let gnr = url.parse(link, true).query.genre - if (!gnr) { - gnr = url.parse(link, true).query.topic - } - film.genre.push({ - name: g, - request: { - query: gnr, - type: link.includes('moviegenre.php') ? 'GENRE' : (link.includes('movietopic.php') ? 'TOPIC' : 'TITLE'), - lang: data.lang - } - }) - }) - break - } - case "synopsis / plot": - case "sinopsis": { - film.synopsis = jQuery(a).next().text().trim() - break - } - default: { - break - } - } - }) - - film.streamingPlatforms = { - subscription : [], - buy : [], - rent : [] - } - content.find( '#stream-wrapper > .body > .sub-title' ).each( function( _, streamingTitle ) { - - let providers - const streamingType = jQuery( streamingTitle ).text().trim().toLowerCase() - switch( streamingType ) { - case 'suscripción' : providers = film.streamingPlatforms.subscription; break; - case 'compra' : providers = film.streamingPlatforms.buy; break; - case 'alquiler' : providers = film.streamingPlatforms.rent; break; - default: - console.warn( 'Streaming type not controlled: ', streamingType ); - return; - } - - jQuery( streamingTitle ).next().find( 'a' ).each( function( _, providerNode ) { - const url = jQuery( providerNode ).attr( 'href' ) - const provider = jQuery( providerNode ).find( 'img' ).attr( 'alt' ).trim() - providers.push( { url, provider } ) - } ) - - }); - - return film - - } catch (err) { - console.error(err) - //throw ({code: 4, msg: 'Can not parse film'}) - } - return {} -} - -function parseSearch(data) { - const pathname = url.parse(data.response.request.uri.href).pathname; - if (pathname.includes('film')) { - const idTemp = pathname.substring(pathname.indexOf('film') + 'film'.length, pathname.indexOf('.')); - data.response.lang = data.lang - const film = parseFilm(data.response) - return { - more: false, - count: 1, - result: [{ - id: idTemp, - url: data.response.request.uri.href, - thumbnail: film.imageUrlMed.replace("mmed", "msmall"), - year: film.year, - title: film.title, - directors: film.directors, - cast: film.cast, - country: film.country, - rating: data.lang == 'es' && film.rating ? film.rating.replace('.', ',') : film.rating, - votes: film.votes - }] - } - } - - if (data.type === 'TOPIC' || data.type === 'GENRE') { - const sfilms = parseSpecialSearch({container: jQuery(data.body).find('.title-movies'), lang: data.lang}) - return { - more: false, - count: sfilms.length, - result: sfilms - } - } - - try { - const outPut = {} - const films = [] - const content = jQuery(data.body) - let year; - content.find('.se-it').each(function (index, a) { - const filmview = {}; - const relUrl = jQuery(a).find('.mc-title').find('a').attr('href'); - const idMatch = relUrl.match(/.*\/film(\d*)/); - filmview.id = idMatch !== null ? idMatch[1] : ""; - filmview.url = relUrl; - filmview.country = { - imgCountry: BASE_URL + jQuery(a).find('.mc-title').find('img').attr('src'), - country: jQuery(a).find('.mc-title').find('img').attr('alt') - } - if (jQuery(a).hasClass('mt')) { - year = jQuery(a).find('.ye-w').text(); - } - filmview.year = year; - let thumbnail = jQuery(a).find('.mc-poster').find('img').attr('src'); - if (thumbnail.includes('noimgfull')) { - thumbnail = BASE_URL + thumbnail - } - filmview.thumbnail = thumbnail - filmview.title = jQuery(a).find('.mc-title').find('a').attr('title').trim(); - filmview.directors = []; - jQuery(a).find('.mc-director').find('.credits').find('a').each(function (index, b) { - filmview.directors.push({ - name: jQuery(b).attr('title'), - request: { - query: jQuery(b).attr('title'), - type: 'DIRECTOR', - lang: data.lang - } - }) - - }) - filmview.cast = []; - jQuery(a).find('.mc-cast').find('.credits').find('a').each(function (index, d) { - filmview.cast.push({ - name: jQuery(d).attr('title'), - request: { - query: jQuery(d).attr('title'), - type: 'CAST', - lang: data.lang - } - }) - - }) - filmview.rating = jQuery(a).find('.avgrat-box').text() - filmview.votes = jQuery(a).find('.ratcount-box').text().trim() - films.push(filmview); - }) - if (content.find('.see-all-button').length) { - outPut.more = true - } else { - outPut.more = false - } - outPut.count = films.length - outPut.result = films - return outPut - } catch (err) { - console.error(err) - } - return [] -} - -function parseTrailers(data) { - try { - const content = jQuery(data.body) - const trailers = [] - content.find('iframe').each(function (index, data) { - const urlt = jQuery(data).attr('src') - trailers.push(urlt); - }) - return trailers - } catch (err) { - console.error(err) - //throw ({code: 4, msg: 'Can not parse film'}) - } - return [] -} - -function parseImages(data) { - const items = [] - jQuery(data.body).find('#main-image-wrapper').find('a').each(function (index, item) { - const href = jQuery(item).attr('href') - if (href.indexOf('.jpg') != -1) { - const item = { - large: href - } - if (href.indexOf('large') != -1) { - item.thumbnail = href.replace("large", "s200") - } else { - item.thumbnail = href - } - items.push(item) - } - }) - return items -} - -function parseProReviews(data) { - try { - const reviews = []; - jQuery(data.body).find('.wrap>table>tbody>tr').each(function (index, element) { - const elHtml = jQuery(element) - const review = {}; - const contryHtml = elHtml.find('.c>img') - review.country = { - imgCountry: BASE_URL + contryHtml.attr('src'), - country: contryHtml.attr('title') - }; - review.gender = elHtml.find('.gender>span').text().trim(); - const authorHtml = elHtml.find('.author'); - review.author = authorHtml.find('div').text().trim(); - review.source = authorHtml.find('strong').text().trim(); // This is for Filmaffinity review - review.source = review.source === "" ? authorHtml.find('em').text().trim() : review.source; - review.text = elHtml.find('.text').text().trim().replace(/"/g, ''); - review.url = elHtml.find('.text>a').attr('href'); - review.bias = elHtml.find('.fas.fa-circle').parent().find('span').text().trim(); - reviews.push(review); - }); - return reviews; - } catch (err) { - console.error(err) - } - return []; -} - -function parseSpecialSearch(data) { - try { - const films = [] - data.container.find('.record').each(function (index, element) { - const elHtml = jQuery(element) - const f = {} - f.id = elHtml.find('.movie-card').attr('data-movie-id') - f.thumbnail = elHtml.find('.mc-poster img').attr('src') - const titleHtml = elHtml.find('.mc-title') - f.url = BASE_URL + elHtml.find('a').attr('href') - f.title = titleHtml.find('a').attr('title').trim() - f.country = { - imgCountry: BASE_URL + titleHtml.find('img').attr('src'), - country: titleHtml.find('img').attr('alt') - } - f.year = titleHtml.text().substring(f.title.length + 2).replace(")", "").trim() - f.directors = [] - elHtml.find('.mc-director .credits a').each(function (index, elDir) { - const item = jQuery(elDir) - f.directors.push({ - name: item.attr('title'), - request: { - query: item.attr('title'), - type: 'DIRECTOR', - lang: data.lang - } - }) - - }) - f.cast = [] - elHtml.find('.mc-cast .credits a').each(function (index, elCast) { - const item = jQuery(elCast) - f.cast.push({ - name: item.attr('title'), - request: { - query: item.attr('title'), - type: 'CAST', - lang: data.lang - } - }) - }) - f.rating = elHtml.find('.avg-w').text().trim() - f.votes = elHtml.find('.votes2').text().trim() - //console.log(f) - films.push(f) - }) - return films - } catch (err) { - console.error(err) - } - return [] -} diff --git a/src/parser/filmParser.js b/src/parser/filmParser.js new file mode 100644 index 0000000..1478be6 --- /dev/null +++ b/src/parser/filmParser.js @@ -0,0 +1,286 @@ +import { Config } from '../config/config.js'; +import jQuery from 'cheerio'; +import url from 'url'; +import { parseNumber } from '../utils/utils.js'; + +const BASE_URL = Config.BASE_URL; + +export const parse = (data) => { + try { + const content = jQuery(data.body) + const titles = { + title: parseTitle(content), + } + const film = { + url: data.url, + coverImages: parseCoverImages(content), + rating: parseRating(content), + votes: parseVotes(content), + streamingPlatforms: parseStreamingPlatforms(content), + ...parseMovieInfo(content, titles, data.lang) + } + return film; + } catch (error) { + console.error(error); + return {} + } +} + +const parseCoverImages = (content) => { + try { + const imagesContainer = content.find('#movie-main-image-container'); + const imageUrl = imagesContainer.find('a').attr('href'); + let imageUrlMed = imagesContainer.find('img').attr('src'); + if (imageUrlMed.includes('noimgfull')) { + imageUrlMed = `${BASE_URL}${imageUrlMed}` + } + return { imageUrl, imageUrlMed } + } catch (error) { + console.error(error) + return null + } +} + +const parseTitle = (content) => { + try { + const title = content.find('#main-title').find('span').text(); + return title.trim(); + } catch (error) { + console.error(error); + return ''; + } +} + +const parseTitles = (element, content) => { + try { + let original = jQuery(element).next().text().trim() + const akas = content.find('dd.akas li').map((_, akatitle) => { + const ak = jQuery(akatitle).text(); + return ak; + }).toArray(); + + if (akas.length > 0) { + original = original.substring(0, original.length - 3).trim(); + return { akas, original } + } + return { original } + } catch (error) { + console.error(error); + return {}; + } +} + +const parseCountry = (content) => { + try { + const countryContainer = jQuery(content).next().find('img'); + const countryPath = countryContainer.attr('src') + return { + imgCountry: `${BASE_URL}${countryPath}`, + country: countryContainer.attr('alt'), + } + } catch (error) { + console.error(error) + return null; + } +} + +const parseRating = (content) => { + try { + const rating = content.find('#movie-rat-avg').attr('content'); + return parseNumber(rating) + } catch (error) { + console.error(error) + return null + } +} + +const parseVotes = (content) => { + try { + const votes = content.find('#movie-count-rat').find('span').attr('content'); + return parseNumber(votes) + } catch (error) { + console.error(error) + return null + } +} + +const parseYear = (content) => { + try { + const year = jQuery(content).next().text(); + return year.trim(); + } catch (error) { + console.log(error) + return '' + } +} + +const parseProduction = (content) => { + const productions = []; + jQuery(content).next().find('span.nb').each((_, item) => { + const container = jQuery(item); + const name = jQuery(item).text().trim().replace(',', ''); + productions.push(name); + if (container.next().is('i')) { + return false; + } + }); + return productions; +} + +const parseRunning = (content) => { + let text = jQuery(content).next().text().trim(); + text = text.replace('min.', '').trim(); + return parseNumber(text); +} + +const parseSypnosis = (content) => { + try { + const synopsis = jQuery(content).next().text(); + return synopsis.trim(); + } catch (error) { + console.log(error) + return '' + } +} + +const parsePeople = (content, type, lang) => { + return jQuery(content).next().find('a').find('span').map((_, item) => { + const name = jQuery(item).text().trim(); + return { + name: name, + request: { + query: name, + type: type, + lang: lang + } + } + }).toArray() +} + +const parseStaff = (content) => { + return jQuery(content).next().find('.nb span').map((_, item) => { + return jQuery(item).text().trim(); + }).toArray() +} + +const parseGenres = (content, lang) => { + return jQuery(content).next().find('a').map((_, item) => { + const linkContent = jQuery(item) + const link = linkContent.attr('href') + const name = linkContent.text().trim() + const query = url.parse(link, true).query.genre || url.parse(link, true).query.topic + return { + name: name, + request: { + query: query, + type: link.includes('moviegenre.php') ? 'GENRE' : (link.includes('movietopic.php') ? 'TOPIC' : 'TITLE'), + lang: lang + } + } + }).toArray(); +} + +const parseStreamingPlatforms = (content) => { + const streamingPlatforms = { + subscription: [], + buy: [], + rent: [] + } + + content.find('#stream-wrapper > .body > .sub-title').each(function (_, streamingTitle) { + let providers + const streamingType = jQuery(streamingTitle).text().trim().toLowerCase() + switch (streamingType) { + case 'suscripción': providers = streamingPlatforms.subscription; break; + case 'compra': providers = streamingPlatforms.buy; break; + case 'alquiler': providers = streamingPlatforms.rent; break; + default: + console.warn('Streaming type not controlled: ', streamingType); + return; + } + + jQuery(streamingTitle).next().find('a').each((_, providerNode) => { + const url = jQuery(providerNode).attr('href'); + const provider = jQuery(providerNode).find('img').attr('alt').trim(); + providers.push({ url, provider }); + }) + }); + + return streamingPlatforms +} + +const parseMovieInfo = (content, titles, lang) => { + const info = { + titles: titles + } + content.find('.movie-info dt').each((_, a) => { + const bind = jQuery(a).text().trim().toLowerCase(); + switch (bind) { + case "original title": + case "título original": { + info.titles = { ...info.titles, ...parseTitles(a, content) } + break + } + case "year": + case "año": { + info.year = parseYear(a); + break + } + case "running time": + case "duración": { + info.running = parseRunning(a); + break + } + case "country": + case "país": { + info.country = parseCountry(a); + break + } + case "director": + case "dirección": { + info.directors = parsePeople(a, 'DIRECTOR', lang) + break + } + case "screenwriter": + case "guion": + case "guión": { + info.screenwriter = parseStaff(a); + break + } + case "music": + case "música": { + info.music = parseStaff(a); + break + } + case "cinematography": + case "fotografía": { + info.cinematography = parseStaff(a); + break + } + case "cast": + case "reparto": { + info.cast = parsePeople(a, 'CAST', lang) + break + } + case "producer": + case "productora": { + info.production = parseProduction(a); + break + } + case "genre": + case "género": { + info.genre = parseGenres(a, lang); + break + } + case "synopsis": + case "sinopsis": { + info.synopsis = parseSypnosis(a); + break + } + default: { + break + } + } + }) + return info; +} \ No newline at end of file diff --git a/src/parser/imagesParser.js b/src/parser/imagesParser.js new file mode 100644 index 0000000..c15b901 --- /dev/null +++ b/src/parser/imagesParser.js @@ -0,0 +1,18 @@ +import jQuery from 'cheerio'; + +export const parse = (data) => { + return jQuery(data.body).find('#main-image-wrapper').find('a').map((_, item) => { + const href = jQuery(item).attr('href'); + if (href.includes('.jpg')) { + const item = { + large: href + } + if (href.includes('large')) { + item.thumbnail = href.replace("large", "s200"); + } else { + item.thumbnail = href; + } + return item; + } + }).toArray(); +} \ No newline at end of file diff --git a/src/parser/index.js b/src/parser/index.js new file mode 100644 index 0000000..76fb7b1 --- /dev/null +++ b/src/parser/index.js @@ -0,0 +1,6 @@ +export { parse as filmParser } from './filmParser.js'; +export { parse as imagesParser } from './imagesParser.js'; +export { parse as proReviewsParser } from './proReviewsParser.js'; +export { parse as searchParser } from './searchParser.js'; +export { parse as specialSearch } from './specialSearch.js'; +export { parse as trailersParser } from './trailersParser.js'; \ No newline at end of file diff --git a/src/parser/proReviewsParser.js b/src/parser/proReviewsParser.js new file mode 100644 index 0000000..da8294c --- /dev/null +++ b/src/parser/proReviewsParser.js @@ -0,0 +1,62 @@ +import { Config } from '../config/config.js'; +import jQuery from 'cheerio'; + +const BASE_URL = Config.BASE_URL; + +export const parse = (data) => { + try { + return jQuery(data.body).find('.wrap>table>tbody>tr').map((_, element) => { + const elHtml = jQuery(element); + const authorHtml = elHtml.find('.author'); + + const review = { + country: parseCountry(elHtml), + gender: parseGenre(elHtml), + author: parseAuthor(authorHtml), + url: parseUrl(elHtml), + text: parseText(elHtml), + bias: parseBias(elHtml), + source: parseSource(authorHtml) + }; + + return review; + }).toArray(); + } catch (error) { + console.error(error); + return []; + } +} + +const parseCountry = (content) => { + const contryHtml = content.find('.c>img') + const href = contryHtml.attr('src'); + return { + imgCountry: `${BASE_URL}${href}`, + country: contryHtml.attr('title') + }; +} + +const parseGenre = (content) => { + return content.find('.gender>span').text().trim(); +} + +const parseAuthor = (content) => { + return content.find('div').text().trim(); +} + +const parseUrl = (content) => { + return content.find('.text>a').attr('href'); +} + +const parseText = (content) => { + return content.find('.text').text().trim().replace(/"/g, ''); +} + +const parseBias = (content) => { + return content.find('.fas.fa-circle').parent().find('span').text().trim(); +} + +const parseSource = (content) => { + const source = content.find('strong').text().trim(); // This is for Filmaffinity review + return source === "" ? content.find('em').text().trim() : source; +} \ No newline at end of file diff --git a/src/parser/searchParser.js b/src/parser/searchParser.js new file mode 100644 index 0000000..55e75f4 --- /dev/null +++ b/src/parser/searchParser.js @@ -0,0 +1,85 @@ +import { Config } from '../config/config.js'; +import jQuery from 'cheerio'; +import { parseNumber } from '../utils/utils.js'; + +const BASE_URL = Config.BASE_URL; + +export const parse = (data) => { + try { + const content = jQuery(data.body) + let year; + const result = content.find('.se-it').map((_, a) => { + if (jQuery(a).hasClass('mt')) { + year = jQuery(a).find('.ye-w').text(); + } + + const filmview = { + id: parseId(a), + url: parseUrl(a), + country: parseCountry(a), + year: year, + thumbnail: parseThumbnail(a), + title: parseTitle(a), + directors: parsePeople(a, '.mc-director', 'DIRECTOR', data.lang), + cast: parsePeople(a, '.mc-cast', 'CAST', data.lang), + rating: getNumber(a, '.avgrat-box'), + votes: getNumber(a, '.ratcount-box') + }; + + return filmview; + }).toArray(); + const hasMore = content.find('.see-all-button').length > 0 + return { result, hasMore } + } catch (error) { + console.error(error); + return { result: [], hasMore: false } + } +} + +const parseUrl = (content) => { + return jQuery(content).find('.mc-title').find('a').attr('href'); +} + +const parseId = (content) => { + const url = parseUrl(content); + const idMatch = url.match(/.*\/film(\d*)/); + return idMatch !== null ? idMatch[1] : ""; +} + +const parseTitle = (content) => { + return jQuery(content).find('.mc-title').find('a').attr('title').trim(); +} + +const parseThumbnail = (content) => { + const thumbnail = jQuery(content).find('.mc-poster').find('img').attr('src'); + return thumbnail.includes('noimgfull') ? `${BASE_URL}${thumbnail}` : thumbnail; +} + +const parseCountry = (content) => { + const container = jQuery(content).find('.mc-title').find('img'); + const source = container.attr('src'); + const name = container.attr('alt'); + return { + imgCountry: `${BASE_URL}${source}`, + country: name + } +} + +const parsePeople = (element, selector, type, lang) => { + return jQuery(element).find(selector).find('.credits').find('a').map((_, b) => { + const name = jQuery(b).attr('title'); + return { + name: name, + request: { + query: name, + type: type, + lang: lang + } + } + }).toArray(); +} + +const getNumber = (content, selector) => { + const selection = jQuery(content).find(selector).text(); + return parseNumber(selection); +} \ No newline at end of file diff --git a/src/parser/specialSearch.js b/src/parser/specialSearch.js new file mode 100644 index 0000000..847af79 --- /dev/null +++ b/src/parser/specialSearch.js @@ -0,0 +1,81 @@ +import { Config } from '../config/config.js'; +import jQuery from 'cheerio'; +import { parseNumber } from '../utils/utils.js'; + +const BASE_URL = Config.BASE_URL; + +export const parse = (data) => { + try { + const content = jQuery(data.body).find('.title-movies'); + return content.find('.record').map((_, element) => { + const elHtml = jQuery(element) + const titleHtml = elHtml.find('.mc-title') + return { + id: parseId(elHtml), + title: parseTitle(titleHtml), + thumbnail: parseThumbnail(elHtml), + url: parseUrl(elHtml), + country: parseCountry(titleHtml), + year: parseYear(titleHtml), + rating: getNumber(elHtml, '.avg-w'), + votes: getNumber(elHtml, '.votes2'), + directors: parsePeople(elHtml, '.mc-director', 'DIRECTOR', data.lang), + cast: parsePeople(elHtml, '.mc-cast', 'CAST', data.lang) + } + }).toArray(); + } catch (error) { + console.error(error); + return []; + } +} + +const parseId = (content) => { + return content.find('.movie-card').attr('data-movie-id'); +} + +const parseThumbnail = (content) => { + return content.find('.mc-poster img').attr('src'); +} + +const parseTitle = (content) => { + return content.find('a').attr('title').trim(); +} + +const parseUrl = (content) => { + const href = content.find('a').attr('href'); + return href; +} + +const getNumber = (content, selector) => { + const value = content.find(selector).text().trim(); + return parseNumber(value); +} + +const parseCountry = (content) => { + const countryContainer = content.find('img'); + const imgSource = countryContainer.attr('src'); + const countryName = countryContainer.attr('alt') + return { + imgCountry: `${BASE_URL}${imgSource}`, + country: countryName + } +} + +const parseYear = (content) => { + const title = parseTitle(content); + return content.text().substring(title.length + 2).replace(")", "").trim(); +} + +const parsePeople = (content, selector, type, lang) => { + return content.find(`${selector} .credits a`).map((_, item) => { + const name = jQuery(item).attr('title'); + return { + name: name, + request: { + query: name, + type: type, + lang: lang + } + } + }).toArray(); +} \ No newline at end of file diff --git a/src/parser/trailersParser.js b/src/parser/trailersParser.js new file mode 100644 index 0000000..1383dbb --- /dev/null +++ b/src/parser/trailersParser.js @@ -0,0 +1,14 @@ +import jQuery from 'cheerio'; + +export const parse = (data) => { + try { + const content = jQuery(data.body); + return content.find('iframe').map((_, element) => { + const url = jQuery(element).attr('src'); + return url; + }).toArray(); + } catch (error) { + console.error(error); + return []; + } +} \ No newline at end of file diff --git a/src/remote/requestfa.js b/src/remote/requestfa.js deleted file mode 100644 index 7eab3e7..0000000 --- a/src/remote/requestfa.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Created by aespinilla on 20/6/17. - */ -const fetch = require('node-fetch') - -const request = require('request') - -const BASE_URL = "https://www.filmaffinity.com" - -const searchTypes = { - TITLE: "title", - CAST: "cast", - DIRECTOR: "director", - GENRE: "/moviegenre.php?genre=", - TOPIC: "/movietopic.php?topic=", - IMAGES: "/filmimages.php?movie_id=", - TRAILERS: "/evideos.php?movie_id=", - PRO_REVIEWS: "/pro-reviews.php?movie-id=", -} - -const requestSource = async (data) => { - const url = buildURL(data) - const response = await fetch(url); - const result = await response.text(); - return { - url: url, - response: response, - type: data.type, - isFilm: data.isFilm, - lang: data.lang, - body: result - } -} - -function buildURL(data) { - if (data.isFilm == true) { - return BASE_URL + '/' + data.lang + '/film' + data.id + '.html' - } - const lang = data.lang ? data.lang : 'es' - const type = data.type && (searchTypes.hasOwnProperty(data.type)) ? data.type : searchTypes.TITLE - const query = data.query - const start = data.start ? data.start : 0 - const orderBy = (typeof data.orderByYear === 'undefined' || (data.orderByYear !== 'undefined' && data.orderByYear === true)) ? '&orderby=year' : '' - let computedUrl = BASE_URL + '/' + lang - if (type === 'CAST' || type === 'DIRECTOR') { - computedUrl = computedUrl + '/search.php?stype=' + searchTypes[type] + '&sn' - computedUrl = computedUrl + '&stext=' + encodeURIComponent(query) + '&from=' + start + orderBy - } else if (type === 'GENRE' || type === 'TOPIC') { - computedUrl = computedUrl + searchTypes[type] + query + '&attr=rat_count&nodoc' - } else if (type === 'IMAGES' || type === 'TRAILERS' || type === 'PRO_REVIEWS') { - computedUrl = computedUrl + searchTypes[type] + data.id - } else { - computedUrl = computedUrl + '/search.php?stype=' + searchTypes[type] - computedUrl = computedUrl + '&stext=' + encodeURIComponent(query) + '&from=' + start + orderBy - } - //console.info('[' + new Date() + '] faparser: ' + 'Generated URL: ' + computedUrl) - return computedUrl.toLowerCase() -} - -module.exports = { requestSource: requestSource } \ No newline at end of file diff --git a/src/request/request.js b/src/request/request.js new file mode 100644 index 0000000..1430526 --- /dev/null +++ b/src/request/request.js @@ -0,0 +1,10 @@ +export async function request(url) { + const response = await fetch(url); + if (!response.ok) throw new Error(response.statusText); + const result = await response.text(); + return { + url: url, + response: response, + body: result + } +} \ No newline at end of file diff --git a/src/urlBuilder/filmUrlBuilder.js b/src/urlBuilder/filmUrlBuilder.js new file mode 100644 index 0000000..bfd6b3f --- /dev/null +++ b/src/urlBuilder/filmUrlBuilder.js @@ -0,0 +1,5 @@ +import { Config } from '../config/config.js'; + +export const build = (data) => { + return `${Config.BASE_URL}/${data.lang}/film${data.id}.html`; +} \ No newline at end of file diff --git a/src/urlBuilder/genreUrlBuilder.js b/src/urlBuilder/genreUrlBuilder.js new file mode 100644 index 0000000..8b990b0 --- /dev/null +++ b/src/urlBuilder/genreUrlBuilder.js @@ -0,0 +1,5 @@ +import { Config } from '../config/config.js'; + +export const build = (data) => { + return `${Config.BASE_URL}/${data.lang || 'es'}${Config.paths.GENRE}${data.query.toUpperCase()}&attr=rat_count&nodoc`; +} \ No newline at end of file diff --git a/src/urlBuilder/imagesUrlBuilder.js b/src/urlBuilder/imagesUrlBuilder.js new file mode 100644 index 0000000..87ef25c --- /dev/null +++ b/src/urlBuilder/imagesUrlBuilder.js @@ -0,0 +1,5 @@ +import { Config } from '../config/config.js'; + +export const build = (data) => { + return `${Config.BASE_URL}/${data.lang || 'es'}${Config.paths.IMAGES}${data.id}`; +} \ No newline at end of file diff --git a/src/urlBuilder/index.js b/src/urlBuilder/index.js new file mode 100644 index 0000000..63f4d2c --- /dev/null +++ b/src/urlBuilder/index.js @@ -0,0 +1,7 @@ +export { build as filmUrlBuilder } from './filmUrlBuilder.js'; +export { build as genreUrlBuilder } from './genreUrlBuilder.js'; +export { build as imagesUrlBuilder } from './imagesUrlBuilder.js'; +export { build as proReviewsUrlBuilder } from './proReviewsUrlBuilder.js'; +export { build as searchUrlBuilder } from './searchUrlBuilder.js'; +export { build as topicUrlBuilder } from './topicUrlBuilder.js'; +export { build as trailersUrlBuilder } from './trailersUrlBuilder.js'; \ No newline at end of file diff --git a/src/urlBuilder/proReviewsUrlBuilder.js b/src/urlBuilder/proReviewsUrlBuilder.js new file mode 100644 index 0000000..dd12054 --- /dev/null +++ b/src/urlBuilder/proReviewsUrlBuilder.js @@ -0,0 +1,5 @@ +import { Config } from '../config/config.js'; + +export const build = (data) => { + return `${Config.BASE_URL}/${data.lang || 'es'}${Config.paths.PRO_REVIEWS}${data.id}`; +} \ No newline at end of file diff --git a/src/urlBuilder/searchUrlBuilder.js b/src/urlBuilder/searchUrlBuilder.js new file mode 100644 index 0000000..4b25471 --- /dev/null +++ b/src/urlBuilder/searchUrlBuilder.js @@ -0,0 +1,17 @@ +import { Config } from '../config/config.js'; + +const peopleSearch = ['CAST', 'DIRECTOR']; +const types = ['TITLE', ...peopleSearch]; + +export const build = (data) => { + const lang = data.lang || 'es'; + const type = data.type && (types.includes(data.type)) ? data.type : 'TITLE'; + const start = data.start || 0; + const orderBy = (typeof data.orderByYear === 'undefined' || (data.orderByYear !== 'undefined' && data.orderByYear === true)) ? '&orderby=year' : ''; + + if (peopleSearch.includes(type.toUpperCase())) { + return `${Config.BASE_URL}/${lang}${Config.paths.SEARCH}${type.toLowerCase()}&sn&stext=${encodeURIComponent(data.query)}&from=${start}${orderBy}`; + } + + return `${Config.BASE_URL}/${lang}${Config.paths.SEARCH}${type.toLowerCase()}&stext=${encodeURIComponent(data.query)}&from=${start}${orderBy}`; +} \ No newline at end of file diff --git a/src/urlBuilder/topicUrlBuilder.js b/src/urlBuilder/topicUrlBuilder.js new file mode 100644 index 0000000..1766c43 --- /dev/null +++ b/src/urlBuilder/topicUrlBuilder.js @@ -0,0 +1,5 @@ +import { Config } from '../config/config.js'; + +export const build = (data) => { + return `${Config.BASE_URL}/${data.lang || 'es'}${Config.paths.TOPIC}${data.query.toUpperCase()}&attr=rat_count&nodoc`; +} \ No newline at end of file diff --git a/src/urlBuilder/trailersUrlBuilder.js b/src/urlBuilder/trailersUrlBuilder.js new file mode 100644 index 0000000..67ccccd --- /dev/null +++ b/src/urlBuilder/trailersUrlBuilder.js @@ -0,0 +1,5 @@ +import { Config } from '../config/config.js'; + +export const build = (data) => { + return `${Config.BASE_URL}/${data.lang || 'es'}${Config.paths.TRAILERS}${data.id}`; +} \ No newline at end of file diff --git a/src/utils/utils.js b/src/utils/utils.js new file mode 100644 index 0000000..118c0c9 --- /dev/null +++ b/src/utils/utils.js @@ -0,0 +1,4 @@ +export const parseNumber = (value) => { + const normalized = value.replace(',', '.').trim(); + return Number(normalized); +} \ No newline at end of file diff --git a/tests/controller/filmController.test.js b/tests/controller/filmController.test.js new file mode 100644 index 0000000..92675de --- /dev/null +++ b/tests/controller/filmController.test.js @@ -0,0 +1,34 @@ +import { filmController } from "../../src/controller"; +import * as requestMock from "../../src/request/request.js"; +import * as filmUrlBuilderMock from "../../src/urlBuilder/filmUrlBuilder"; +import * as filmParserMock from "../../src/parser/filmParser"; + +describe('Film controller tests', () => { + beforeEach(() => { + requestMock.request = jest.fn().mockReturnValue(Promise.resolve({ + url: 'http://fake.address', + response: {}, + body: 'Some body content' + })); + + filmUrlBuilderMock.build = jest.fn().mockReturnValue('http://fake.address'); + + filmParserMock.parse = jest.fn().mockReturnValue({ + id: 'xxxxx', + title: 'random', + url: 'http://fake.address' + }); + }); + + it('should return film object', async () => { + const id = 'xx'; + const lang = 'es'; + const input = { id, lang } + const output = await filmController(input); + + expect(output.url).toBe('http://fake.address'); + expect(requestMock.request).toHaveBeenCalledTimes(1); + expect(filmUrlBuilderMock.build).toHaveBeenCalledTimes(1); + expect(filmParserMock.parse).toHaveBeenCalledTimes(1); + }); +}); \ No newline at end of file diff --git a/tests/controller/imagesController.test.js b/tests/controller/imagesController.test.js new file mode 100644 index 0000000..2913460 --- /dev/null +++ b/tests/controller/imagesController.test.js @@ -0,0 +1,27 @@ +import { imagesController } from "../../src/controller"; +import * as requestMock from "../../src/request/request.js"; +import * as imagesUrlBuilderMock from "../../src/urlBuilder/imagesUrlBuilder.js"; +import * as parserMock from "../../src/parser/imagesParser.js"; + +describe('Images controller tests', () => { + beforeEach(() => { + requestMock.request = jest.fn().mockReturnValue(Promise.resolve({ + url: 'http://fake.address', + response: {}, + body: 'Some body content' + })); + + imagesUrlBuilderMock.build = jest.fn().mockReturnValue('http://fake.address'); + + parserMock.parse = jest.fn().mockReturnValue({}); + }); + + it('Should return images object', async () => { + const input = {id: 'XX'}; + const output = await imagesController(input); + expect(output).toMatchObject({}); + expect(requestMock.request).toHaveBeenCalledTimes(1); + expect(imagesUrlBuilderMock.build).toHaveBeenCalledTimes(1); + expect(parserMock.parse).toHaveBeenCalledTimes(1); + }); +}); \ No newline at end of file diff --git a/tests/controller/proReviewsController.test.js b/tests/controller/proReviewsController.test.js new file mode 100644 index 0000000..0cd3c04 --- /dev/null +++ b/tests/controller/proReviewsController.test.js @@ -0,0 +1,28 @@ +import { proReviewsController } from "../../src/controller"; +import * as requestMock from '../../src/request/request.js'; +import * as urlBuilderMock from '../../src/urlBuilder/proReviewsUrlBuilder.js'; +import * as parserMock from '../../src/parser/proReviewsParser.js'; + +describe('Pro reviews controller tests', () => { + + beforeEach(() => { + requestMock.request = jest.fn().mockReturnValue(Promise.resolve({ + url: 'http://fake.address', + response: {}, + body: 'Some body content' + })); + + urlBuilderMock.build = jest.fn().mockReturnValue('http://fake.address'); + + parserMock.parse = jest.fn().mockReturnValue({}); + }); + + it('Should return pro reviews object', async () => { + const input = {id: 'XX'}; + const output = await proReviewsController(input); + expect(output).toMatchObject({}); + expect(requestMock.request).toHaveBeenCalledTimes(1); + expect(urlBuilderMock.build).toHaveBeenCalledTimes(1); + expect(parserMock.parse).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tests/controller/trailersController.test.js b/tests/controller/trailersController.test.js new file mode 100644 index 0000000..f464c76 --- /dev/null +++ b/tests/controller/trailersController.test.js @@ -0,0 +1,27 @@ +import { trailersController } from "../../src/controller"; +import * as requestMock from "../../src/request/request.js"; +import * as urlBuilderMock from "../../src/urlBuilder/trailersUrlBuilder.js"; +import * as parserMock from "../../src/parser/trailersParser.js"; + +describe('TrailersController tests', () => { + beforeEach(() => { + requestMock.request = jest.fn().mockReturnValue(Promise.resolve({ + url: 'http://fake.address', + response: {}, + body: 'Some body content' + })); + + urlBuilderMock.build = jest.fn().mockReturnValue('http://fake.address'); + + parserMock.parse = jest.fn().mockReturnValue({}); + }); + + it('Should return trailers object', async () => { + const input = {id: 'XX'}; + const output = await trailersController(input); + expect(output).toMatchObject({}); + expect(requestMock.request).toHaveBeenCalledTimes(1); + expect(urlBuilderMock.build).toHaveBeenCalledTimes(1); + expect(parserMock.parse).toHaveBeenCalledTimes(1); + }); +}); \ No newline at end of file diff --git a/tests/e2e/faParser.test.js b/tests/e2e/faParser.test.js new file mode 100644 index 0000000..b325985 --- /dev/null +++ b/tests/e2e/faParser.test.js @@ -0,0 +1,15 @@ +import { film } from "../../index.js"; + +describe('faParser tests', () => { + it('Should return film when lang and id are provided', async () => { + const input = {id: '932476', lang: 'es'}; + const output = await film(input); + expect(output.id).toBe('932476'); + }); + + it('Should return film when no lang provided', async () => { + const input = {id: '932476'}; + const output = await film(input); + expect(output.id).toBe('932476'); + }); +}); \ No newline at end of file diff --git a/tests/request/request.test.js b/tests/request/request.test.js new file mode 100644 index 0000000..7149c99 --- /dev/null +++ b/tests/request/request.test.js @@ -0,0 +1,25 @@ +import { request } from "../../src/request/request.js"; + +describe('Request tests', () => { + it('should return successful response', async () => { + global.fetch = jest.fn().mockImplementationOnce(() => Promise.resolve({ + ok: true, + status: 200, + text: () => Promise.resolve('result'), + })); + const result = await request('http://fake.address'); + expect(result.url).toBe('http://fake.address'); + expect(result.body).toBe('result'); + expect(fetch).toHaveBeenCalledTimes(1); + }); + + it('should return failure response', async () => { + global.fetch = jest.fn().mockImplementationOnce(() => Promise.resolve({ + ok: false, + status: 500, + statusText: 'some error' + })); + await expect(request('http://fake.address')).rejects.toThrow('some error'); + expect(fetch).toHaveBeenCalledTimes(1); + }) +}); \ No newline at end of file diff --git a/tests/urlBuilder/filmUrlBuilder.test.js b/tests/urlBuilder/filmUrlBuilder.test.js new file mode 100644 index 0000000..2c77220 --- /dev/null +++ b/tests/urlBuilder/filmUrlBuilder.test.js @@ -0,0 +1,11 @@ +import { filmUrlBuilder } from "../../src/urlBuilder"; + +describe('Build film url', () => { + it('should return film url', async () => { + const lang = 'xx'; + const id = '999999'; + const input = { id: id, lang: lang } + const output = filmUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${lang}/film${id}.html`); + }); +}); \ No newline at end of file diff --git a/tests/urlBuilder/genreUrlBuilder.test.js b/tests/urlBuilder/genreUrlBuilder.test.js new file mode 100644 index 0000000..af3e098 --- /dev/null +++ b/tests/urlBuilder/genreUrlBuilder.test.js @@ -0,0 +1,20 @@ +import { genreUrlBuilder } from "../../src/urlBuilder"; + +describe('Build genre url', () => { + it('should return genre url given lang', () => { + const lang = 'xx'; + const query = 'xxxxxx'; + const expectedQuery = 'XXXXXX' + const input = {lang, query}; + const output = genreUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${lang}/moviegenre.php?genre=${expectedQuery}&attr=rat_count&nodoc`); + }); + + it('should return genre url with default lang', () => { + const query = 'XXXXXX'; + const expectedLang = 'es'; + const input = {query}; + const output = genreUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${expectedLang}/moviegenre.php?genre=${query}&attr=rat_count&nodoc`); + }); +}); \ No newline at end of file diff --git a/tests/urlBuilder/imagesUrlBuilder.test.js b/tests/urlBuilder/imagesUrlBuilder.test.js new file mode 100644 index 0000000..33bb032 --- /dev/null +++ b/tests/urlBuilder/imagesUrlBuilder.test.js @@ -0,0 +1,19 @@ +import { imagesUrlBuilder } from "../../src/urlBuilder"; + +describe('Build images url', () => { + it('should return images url given lang', () => { + const lang = 'xx'; + const id = '999999'; + const input = {lang, id}; + const output = imagesUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${lang}/filmimages.php?movie_id=${id}`); + }); + + it('should return images url', () => { + const lang = 'es'; + const id = '999999'; + const input = { id }; + const output = imagesUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${lang}/filmimages.php?movie_id=${id}`); + }); +}); \ No newline at end of file diff --git a/tests/urlBuilder/proReviewsUrlBuilder.test.js b/tests/urlBuilder/proReviewsUrlBuilder.test.js new file mode 100644 index 0000000..a85cb34 --- /dev/null +++ b/tests/urlBuilder/proReviewsUrlBuilder.test.js @@ -0,0 +1,19 @@ +import { proReviewsUrlBuilder } from "../../src/urlBuilder"; + +describe('Build pro review url', () => { + it('should return pro reviews url given lang', async () => { + const lang = 'xx'; + const id = '999999'; + const input = { id: id, lang: lang } + const output = proReviewsUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${lang}/pro-reviews.php?movie-id=${id}`); + }); + + it('should return pro reviews url', async () => { + const lang = 'es'; + const id = '999999'; + const input = { id: id } + const output = proReviewsUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${lang}/pro-reviews.php?movie-id=${id}`); + }); +}); \ No newline at end of file diff --git a/tests/urlBuilder/searchUrlBuilder.test.js b/tests/urlBuilder/searchUrlBuilder.test.js new file mode 100644 index 0000000..d706225 --- /dev/null +++ b/tests/urlBuilder/searchUrlBuilder.test.js @@ -0,0 +1,40 @@ +import { searchUrlBuilder } from "../../src/urlBuilder"; + +describe('Build search url', () => { + it('should return search url with title', () => { + const orderByYear = false; + const lang = 'xx'; + const type = 'TITLE'; + const query = "lorem ipsum"; + const start = 10; + const expectedType = 'title'; + const expectedQuery = 'lorem%20ipsum'; + const input = { orderByYear, lang, type, query, start } + const output = searchUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${lang}/search.php?stype=${expectedType}&stext=${expectedQuery}&from=${start}`); + }); + + it('should return search url with actor', () => { + const orderByYear = false; + const lang = 'xx'; + const type = 'CAST'; + const query = "lorem ipsum"; + const expectedStart = 0; + const expectedType = 'cast'; + const expectedQuery = 'lorem%20ipsum'; + const input = { orderByYear, lang, type, query } + const output = searchUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${lang}/search.php?stype=${expectedType}&sn&stext=${expectedQuery}&from=${expectedStart}`); + }); + + it('should return search url with title given required values', () => { + const expectedLang = 'es'; + const query = "lorem ipsum"; + const expectedStart = 0; + const expectedType = 'title'; + const expectedQuery = 'lorem%20ipsum'; + const input = { query } + const output = searchUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${expectedLang}/search.php?stype=${expectedType}&stext=${expectedQuery}&from=${expectedStart}&orderby=year`); + }); +}); \ No newline at end of file diff --git a/tests/urlBuilder/topicUrlBuilder.test.js b/tests/urlBuilder/topicUrlBuilder.test.js new file mode 100644 index 0000000..1cc489c --- /dev/null +++ b/tests/urlBuilder/topicUrlBuilder.test.js @@ -0,0 +1,19 @@ +import { topicUrlBuilder } from "../../src/urlBuilder"; + +describe('Build trailers url', () => { + it('should return topic url given lang', () => { + const lang = 'xx'; + const query = 'XXXXXX'; + const input = {lang, query}; + const output = topicUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${lang}/movietopic.php?topic=${query}&attr=rat_count&nodoc`); + }); + + it('should return topic url', () => { + const lang = 'es'; + const query = 'XXXXXX'; + const input = { query }; + const output = topicUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${lang}/movietopic.php?topic=${query}&attr=rat_count&nodoc`); + }); +}); \ No newline at end of file diff --git a/tests/urlBuilder/trailersUrlBuilder.test.js b/tests/urlBuilder/trailersUrlBuilder.test.js new file mode 100644 index 0000000..65751f3 --- /dev/null +++ b/tests/urlBuilder/trailersUrlBuilder.test.js @@ -0,0 +1,19 @@ +import { trailersUrlBuilder } from "../../src/urlBuilder"; + +describe('Build trailers url', () => { + it('should return trailers url given lang', () => { + const lang = 'xx'; + const id = '999999'; + const input = {lang: lang, id: id}; + const output = trailersUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${lang}/evideos.php?movie_id=${id}`); + }); + + it('should return trailers url', () => { + const lang = 'es'; + const id = '999999'; + const input = {id: id}; + const output = trailersUrlBuilder(input); + expect(output).toBe(`https://www.filmaffinity.com/${lang}/evideos.php?movie_id=${id}`); + }); +}); \ No newline at end of file diff --git a/tests/utils/utils.test.js b/tests/utils/utils.test.js new file mode 100644 index 0000000..2ff8965 --- /dev/null +++ b/tests/utils/utils.test.js @@ -0,0 +1,15 @@ +import * as utils from '../../src/utils/utils'; + +describe('Utils tests', () => { + it('should return normalized number', () => { + const input = '3,2'; + const output = utils.parseNumber(input); + expect(output).toBe(3.2); + }); + + it('should return null', () => { + const input = 'aabbcc'; + const output = utils.parseNumber(input); + expect(output).toBeNaN(); + }); +}); \ No newline at end of file