diff --git a/common/get-movie-data.js b/common/get-movie-data.js new file mode 100644 index 0000000..f7e8f51 --- /dev/null +++ b/common/get-movie-data.js @@ -0,0 +1,57 @@ +const { MovieDb } = require("moviedb-promise"); +const { dailyCache } = require("./cache"); +require("dotenv").config(); + +const moviedb = new MovieDb(process.env.MOVIEDB_API_KEY); + +const searchMovieAndCacheResults = ({ + slug, + year: yearValue, + normalizedTitle, +}) => + dailyCache(`moviedb-search-${yearValue || "no-year"}-${slug}`, async () => { + const getPayload = (additional = {}) => ({ + query: normalizedTitle, + ...additional, + }); + + // If there's no year provided, just search for the title + if (!yearValue) { + return moviedb.searchMovie(getPayload()); + } + + const year = parseInt(yearValue, 10); + + // Try to find a movie first released on that year + let search = await moviedb.searchMovie( + getPayload({ primary_release_year: year }), + ); + + // If there's no matches, then try to find a movie with some release related + // to that year + if (search.results.length === 0) { + search = await moviedb.searchMovie(getPayload({ year })); + } + + // If there's no matches, sometimes the movie listing has the year off by 1, + // so try to find a movie with some release related to the next year + if (search.results.length === 0) { + return moviedb.searchMovie(getPayload({ year: year + 1 })); + } + + return search; + }); + +const getMovieInfoAndCacheResults = ({ id }) => + dailyCache(`moviedb-info-${id}`, async () => { + const payload = { + id, + append_to_response: "credits,external_ids,keywords,release_dates,videos", + }; + return moviedb.movieInfo(payload); + }); + +module.exports = { + searchMovieAndCacheResults, + getMovieInfoAndCacheResults, +}; diff --git a/common/hydrate.js b/common/hydrate.js index 08c18ad..0847e58 100644 --- a/common/hydrate.js +++ b/common/hydrate.js @@ -1,58 +1,10 @@ const slugify = require("slugify"); -const { MovieDb } = require("moviedb-promise"); -const { dailyCache } = require("./cache"); const { parseMinsToMs } = require("./utils"); const normalizeTitle = require("./normalize-title"); -require("dotenv").config(); - -const moviedb = new MovieDb(process.env.MOVIEDB_API_KEY); - -const searchMovieAndCacheResults = ({ - slug, - year: yearValue, - normalizedTitle, -}) => - dailyCache(`moviedb-search-${yearValue || "no-year"}-${slug}`, async () => { - const getPayload = (additional = {}) => ({ - query: normalizedTitle, - ...additional, - }); - - // If there's no year provided, just search for the title - if (!yearValue) { - return moviedb.searchMovie(getPayload()); - } - - const year = parseInt(yearValue, 10); - - // Try to find a movie first released on that year - let search = await moviedb.searchMovie( - getPayload({ primary_release_year: year }), - ); - - // If there's no matches, then try to find a movie with some release related - // to that year - if (search.results.length === 0) { - search = await moviedb.searchMovie(getPayload({ year })); - } - - // If there's no matches, sometimes the movie listing has the year off by 1, - // so try to find a movie with some release related to the next year - if (search.results.length === 0) { - return moviedb.searchMovie(getPayload({ year: year + 1 })); - } - - return search; - }); - -const getMovieInfoAndCacheResults = ({ id }) => - dailyCache(`moviedb-info-${id}`, async () => { - const payload = { - id, - append_to_response: "credits,external_ids,keywords,videos", - }; - return moviedb.movieInfo(payload); - }); +const { + searchMovieAndCacheResults, + getMovieInfoAndCacheResults, +} = require("./get-movie-data"); const getMovieTitleAndYearFrom = (title) => { const hasYear = title.trim().match(/^(.*?)\s*\((\d{4})\)$/); diff --git a/package-lock.json b/package-lock.json index b903684..9539cfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,12 @@ "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "cheerio": "^1.0.0-rc.12", + "compress-json": "^3.1.0", "date-fns": "^3.6.0", "dotenv": "^16.4.5", "ics": "^3.7.6", "moviedb-promise": "^4.0.7", + "nanoid": "^3.3.7", "playwright": "^1.48.1", "replace-special-characters": "^1.2.7", "slugify": "^1.6.6" @@ -1640,6 +1642,12 @@ "node": ">= 0.8" } }, + "node_modules/compress-json": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compress-json/-/compress-json-3.1.0.tgz", + "integrity": "sha512-Zcq4jRC5ZpfaOY3mbBWOANtGuMHJ/hsTENcwN1/lEkrogcoAF7HBma1RLe/CICZO6IquK1U0EaPzmnlDIFRNjA==", + "license": "BSD-2-Clause" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3334,6 +3342,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, diff --git a/package.json b/package.json index a82e21f..5e837be 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "output:highlight-hydration-misses-for-review": "node ./scripts/highlight-hydration-misses-for-review.js", "clear:cache": "rm -rf ./cache && git checkout ./cache", "clear:output": "rm -rf ./output && git checkout ./output", - "populate:output": "./scripts/get-latest-release-assets.sh" + "populate:output": "./scripts/get-latest-release-assets.sh", + "generate:combined-data": "node ./scripts/generate-combined-data.js" }, "author": "Alistair Brown ", "license": "MIT", @@ -19,10 +20,12 @@ "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "cheerio": "^1.0.0-rc.12", + "compress-json": "^3.1.0", "date-fns": "^3.6.0", "dotenv": "^16.4.5", "ics": "^3.7.6", "moviedb-promise": "^4.0.7", + "nanoid": "^3.3.7", "playwright": "^1.48.1", "replace-special-characters": "^1.2.7", "slugify": "^1.6.6" diff --git a/scripts/generate-combined-data.js b/scripts/generate-combined-data.js new file mode 100644 index 0000000..9357c86 --- /dev/null +++ b/scripts/generate-combined-data.js @@ -0,0 +1,179 @@ +const { writeFileSync } = require("node:fs"); +const path = require("node:path"); +const { nanoid } = require("nanoid"); +const { compress, trimUndefinedRecursively } = require("compress-json"); +const getSites = require("../common/get-sites"); +const normalizeTitle = require("../common/normalize-title"); +const { getMovieInfoAndCacheResults } = require("../common/get-movie-data"); +const { parseMinsToMs } = require("../common/utils"); + +const getId = () => nanoid(8); + +const getCertification = ({ release_dates: { results } }) => { + const result = results.find(({ iso_3166_1: locale }) => locale === "GB"); + if (!result) return undefined; + + const { release_dates: releaseDates } = result; + const releaseDateWithCertification = releaseDates.find( + ({ certification }) => !!certification, + ); + + if (!releaseDateWithCertification) return undefined; + return releaseDateWithCertification.certification; +}; + +const getDirectors = ({ credits: { crew } }) => + crew + .filter(({ job }) => job.toLowerCase() === "director") + .map(({ id, name }) => ({ id, name })); + +const getActors = ({ credits: { cast } }) => + cast + .slice(0, 10) + .filter(({ popularity }) => popularity >= 5) + .map(({ id, name }) => ({ id, name })); + +const getGenres = ({ genres }) => genres; + +const getYoutubeTrailer = ({ videos: { results } }) => { + const trailer = results.find( + ({ type, site }) => + type.toLowerCase() === "trailer" && site.toLowerCase() === "youtube", + ); + return trailer ? trailer.key : undefined; +}; + +const getImddId = ({ external_ids: externalIds = {} }) => externalIds.imdb_id; + +const data = getSites().reduce((mapping, site) => { + let attributes; + let shows; + try { + attributes = require( + path.join(__dirname, "..", "cinemas", `${site}`, "attributes.js"), + ); + shows = require(path.join(__dirname, "..", "output", `${site}-shows.json`)); + } catch (e) { + return mapping; + } + return { ...mapping, [site]: { attributes, shows } }; +}, {}); + +const siteData = { + venues: {}, + people: {}, + genres: {}, + movies: {}, +}; + +(async function () { + for (cinema in data) { + console.log(`[🎞️ Cinema: ${cinema}]`); + const venueId = getId(); + const { + attributes: { name, url, address, geo }, + shows, + } = data[cinema]; + + siteData.venues[venueId] = { + id: venueId, + name, + url, + address, + geo, + }; + + for (show of shows) { + const { title, url, overview, performances, moviedb } = show; + + let movieInfo; + if (moviedb) { + const outputTitle = title.slice(0, 35); + process.stdout.write( + ` - Retriving data for ${outputTitle} ... ${"".padEnd(35 - outputTitle.length, " ")}`, + ); + try { + movieInfo = await getMovieInfoAndCacheResults(moviedb); + console.log(`\t✅ Retrieved`); + } catch (e) { + console.log(`\t❌ Error retriving`); + throw e; + } + } + + const movieId = movieInfo ? movieInfo.id : getId(); + if (!siteData.movies[movieId]) { + if (movieInfo) { + const directors = getDirectors(movieInfo); + const actors = getActors(movieInfo); + const genres = getGenres(movieInfo); + + directors.forEach((crew) => (siteData.people[crew.id] = crew)); + actors.forEach((cast) => (siteData.people[cast.id] = cast)); + genres.forEach((genre) => (siteData.genres[genre.id] = genre)); + + siteData.movies[movieId] = { + id: movieId, + title: movieInfo.title, + normalizedTitla: normalizeTitle(movieInfo.title), + certification: getCertification(movieInfo), + overview: movieInfo.overview, + year: movieInfo.release_date.split("-")[0], + duration: parseMinsToMs(movieInfo.runtime), + directors: directors.map(({ id }) => id), + actors: actors.map(({ id }) => id), + genres: genres.map(({ id }) => id), + imdbId: getImddId(movieInfo), + youtubeTrailer: getYoutubeTrailer(movieInfo), + posterPath: movieInfo.poster_path, + showings: {}, + performances: [], + }; + } else { + siteData.movies[movieId] = { + id: movieId, + title: title, + isUnmatched: true, + showings: {}, + performances: [], + }; + } + } + + const showingId = getId(); + const movie = siteData.movies[movieId]; + + movie.showings[showingId] = { + id: showingId, + venueId, + title: title !== movie.title ? title : undefined, + url, + overview, + }; + + movie.performances = movie.performances.concat( + performances.map(({ time, notes, bookingUrl }) => ({ + showingId, + time, + notes: notes !== "" ? notes : undefined, + bookingUrl, + })), + ); + } + console.log(" "); + } + + process.stdout.write(`Compressing data ... `); + try { + const dataFile = `./output/combined-data.json`; + trimUndefinedRecursively(siteData); + const compressed = compress(siteData).toString(); + console.log(`✅ Compressed`); + + writeFileSync(dataFile, compressed); + console.log(`🗂️ Combined file created`); + } catch (e) { + console.log(`\t❌ Error compressing`); + throw e; + } +})(); diff --git a/scripts/genereate-readme.js b/scripts/generate-readme.js similarity index 100% rename from scripts/genereate-readme.js rename to scripts/generate-readme.js