feat: major rewrite

the application is now rewritten in next.js. this commit also adds the ability to see trailers, did you know, more like this, etc. on title page.

BREAKING CHANGE: the whole application is rewritten from scratch.
This commit is contained in:
zyachel
2022-09-11 19:37:24 +05:30
committed by zyachel
parent 620ddf348a
commit 9891204f5a
129 changed files with 6314 additions and 4671 deletions

View File

@ -0,0 +1,14 @@
import axios from 'axios';
const axiosInstance = axios.create({
baseURL: 'https://www.imdb.com/',
timeout: 50000,
headers: {
...(process.env.AXIOS_USERAGENT && {
'User-Agent': process.env.AXIOS_USERAGENT,
}),
...(process.env.AXIOS_ACCEPT && { Accept: process.env.AXIOS_ACCEPT }),
},
});
export default axiosInstance;

377
src/utils/cleaners/title.ts Normal file
View File

@ -0,0 +1,377 @@
import RawTitle from '../../interfaces/misc/rawTitle';
import { formatDate } from '../helpers';
const cleanTitle = (rawData: RawTitle) => {
const {
props: {
pageProps: { aboveTheFoldData: main, mainColumnData: misc },
},
} = rawData;
const cleanData = {
titleId: main.id,
basic: {
id: main.id,
title: main.titleText.text,
// ...(main.originalTitleText.text.toLowerCase() !==
// main.titleText.text.toLowerCase() && {
// originalTitle: main.originalTitleText.text,
// }),
type: {
id: main.titleType.id as
| 'movie'
| 'tvSeries'
| 'tvEpisode'
| 'videoGame',
name: main.titleType.text,
},
status: {
id: main.productionStatus.currentProductionStage.id,
text: main.productionStatus.currentProductionStage.text,
},
ceritficate: main.certificate?.rating || null,
...(main.releaseYear && {
releaseYear: {
start: main.releaseYear.year,
end: main.releaseYear.endYear,
},
}),
runtime: main.runtime?.seconds || null,
ratings: {
avg: main.ratingsSummary.aggregateRating || null,
numVotes: main.ratingsSummary.voteCount,
},
...(main.meterRanking && {
ranking: {
position: main.meterRanking.currentRank,
change: main.meterRanking.rankChange.difference,
direction: main.meterRanking.rankChange.changeDirection as
| 'UP'
| 'DOWN'
| 'FLAT',
},
}),
genres: main.genres.genres.map(genre => ({
id: genre.id,
text: genre.text,
})),
plot: main.plot?.plotText?.plainText || null,
primaryCrew: main.principalCredits.map(type => ({
type: { category: type.category.text, id: type.category.id },
crew: type.credits.map(person => ({
attributes: person.attributes?.map(attr => attr.text) || null,
name: person.name.nameText.text,
id: person.name.id,
})),
})),
...(main.primaryImage && {
poster: {
url: main.primaryImage.url,
id: main.primaryImage.id,
caption: main.primaryImage.caption.plainText,
},
}),
},
cast: misc.cast.edges.map(cast => ({
name: cast.node.name.nameText.text,
id: cast.node.name.id,
image: cast.node.name.primaryImage?.url || null,
attributes: cast.node.attributes?.map(attr => attr.text) || null,
characters: cast.node.characters?.map(name => name.name) || null,
})),
media: {
...(main.primaryVideos.edges.length && {
trailer: {
id: main.primaryVideos.edges[0].node.id,
isMature: main.primaryVideos.edges[0].node.isMature,
thumbnail: main.primaryVideos.edges[0].node.thumbnail.url,
runtime: main.primaryVideos.edges[0].node.runtime.value,
caption: main.primaryVideos.edges[0].node.description.value,
urls: main.primaryVideos.edges[0].node.playbackURLs.map(url => ({
resolution: url.displayName.value,
mimeType: url.mimeType,
url: url.url,
})),
},
}),
images: {
total: misc.titleMainImages.total,
images: misc.titleMainImages.edges.map(image => ({
id: image.node.id,
url: image.node.url,
caption: image.node.caption,
})),
},
videos: {
total: misc.videos.total,
videos: misc.videoStrip.edges.map(video => ({
id: video.node.id,
type: video.node.contentType.displayName,
caption: video.node.name.value,
runtime: video.node.runtime.value,
thumbnail: video.node.thumbnail.url,
})),
},
},
accolades: {
wins: misc.wins.total,
nominations: misc.nominations.total,
...(misc.prestigiousAwardSummary && {
awards: {
name: misc.prestigiousAwardSummary.award.text,
id: misc.prestigiousAwardSummary.award.id,
event: misc.prestigiousAwardSummary.award.event,
nominations: misc.prestigiousAwardSummary.nominations,
wins: misc.prestigiousAwardSummary.wins,
},
}),
topRating: misc.ratingsSummary.topRanking?.rank || null,
},
meta: {
// for tv episode
...(main.series && {
infoEpisode: {
numSeason: main.series.episodeNumber?.seasonNumber || null,
numEpisode: main.series.episodeNumber?.episodeNumber || null,
prevId: main.series.previousEpisode?.id || null,
nextId: main.series.nextEpisode.id,
series: {
id: main.series.series.id,
title: main.series.series.titleText.text,
startYear: main.series.series.releaseYear.year,
endYear: main.series.series.releaseYear.endYear,
},
},
}),
// for tv series
...(misc.episodes && {
infoSeries: {
totalEpisodes: misc.episodes.episodes.total,
seasons: misc.episodes.seasons.map(season => season.number),
years: misc.episodes.years.map(year => year.year),
topRatedEpisode:
misc.episodes.topRated.edges[0].node.ratingsSummary.aggregateRating,
},
}),
},
keywords: {
total: main.keywords.total,
list: main.keywords.edges.map(word => word.node.text),
},
didYouKnow: {
...(misc.trivia.edges.length && {
trivia: {
total: misc.triviaTotal.total,
html: misc.trivia.edges[0].node.text.plaidHtml,
},
}),
...(misc.goofs.edges.length && {
goofs: {
total: misc.goofsTotal.total,
html: misc.goofs.edges[0].node.text.plaidHtml,
},
}),
...(misc.quotes.edges.length && {
quotes: {
total: misc.quotesTotal.total,
lines: misc.quotes.edges[0].node.lines.map(line => ({
name: line.characters?.[0].character || null,
id: line.characters?.[0].name?.id || null,
stageDirection: line.stageDirection || null,
text: line.text,
})),
},
}),
...(misc.crazyCredits.edges.length && {
crazyCredits: {
html: misc.crazyCredits.edges[0].node.text.plaidHtml,
},
}),
...(misc.alternateVersions.edges.length && {
alternativeVersions: {
total: misc.alternateVersions.total,
html: misc.alternateVersions.edges[0].node.text.plaidHtml,
},
}),
...(misc.connections.edges.length && {
connections: {
startText: misc.connections.edges[0].node.category.text,
title: {
id: misc.connections.edges[0].node.associatedTitle.id,
year: misc.connections.edges[0].node.associatedTitle.releaseYear
.year,
text: misc.connections.edges[0].node.associatedTitle.titleText.text,
},
},
}),
...(misc.soundtrack.edges.length && {
soundTrack: {
title: misc.soundtrack.edges[0].node.text,
htmls:
misc.soundtrack.edges[0].node.comments?.map(
html => html.plaidHtml
) || null,
},
}),
},
reviews: {
metacriticScore: main.metacritic?.metascore.score || null,
numCriticReviews: main.criticReviewsTotal.total,
numUserReviews: misc.reviews.total,
...(misc.featuredReviews.edges.length && {
featuredReview: {
id: misc.featuredReviews.edges[0].node.id,
reviewer: {
id: misc.featuredReviews.edges[0].node.author.userId,
name: misc.featuredReviews.edges[0].node.author.nickName,
},
rating: misc.featuredReviews.edges[0].node.authorRating,
date: formatDate(misc.featuredReviews.edges[0].node.submissionDate),
votes: {
up: misc.featuredReviews.edges[0].node.helpfulness.upVotes,
down: misc.featuredReviews.edges[0].node.helpfulness.downVotes,
},
review: {
summary: misc.featuredReviews.edges[0].node.summary.originalText,
html: misc.featuredReviews.edges[0].node.text.originalText
.plaidHtml,
},
},
}),
},
details: {
...(misc.releaseDate && {
releaseDate: {
date: formatDate(
misc.releaseDate.year,
misc.releaseDate.month - 1, // month starts from 0
misc.releaseDate.day
),
country: {
id: misc.releaseDate.country.id,
text: misc.releaseDate.country.text,
},
},
}),
...(misc.countriesOfOrigin && {
countriesOfOrigin: misc.countriesOfOrigin.countries.map(country => ({
id: country.id,
text: country.text,
})),
}),
...(misc.detailsExternalLinks.edges.length && {
officialSites: {
total: misc.detailsExternalLinks.total,
sites: misc.detailsExternalLinks.edges.map(site => ({
name: site.node.label,
url: site.node.url,
country: site.node.externalLinkRegion?.text || null,
})),
},
}),
...(misc.spokenLanguages && {
languages: misc.spokenLanguages.spokenLanguages.map(lang => ({
id: lang.id,
text: lang.text,
})),
}),
alsoKnownAs: misc.akas.edges[0]?.node.text || null,
...(misc.filmingLocations.edges.length && {
filmingLocations: {
total: misc.filmingLocations.total,
locations: misc.filmingLocations.edges.map(loc => loc.node.text),
},
}),
...(misc.production.edges.length && {
production: {
total: misc.companies.total,
companies: misc.production.edges.map(c => ({
id: c.node.company.id,
name: c.node.company.companyText.text,
})),
},
}),
},
boxOffice: {
...(misc.productionBudget && {
budget: {
amount: misc.productionBudget.budget.amount,
currency: misc.productionBudget.budget.currency,
},
}),
...(misc.worldwideGross && {
gross: {
amount: misc.worldwideGross.total.amount,
currency: misc.worldwideGross.total.currency,
},
}),
...(misc.lifetimeGross && {
grossUs: {
amount: misc.lifetimeGross.total.amount,
currency: misc.lifetimeGross.total.currency,
},
}),
...(misc.openingWeekendGross && {
openingGrossUs: {
amount: misc.openingWeekendGross.gross.total.amount,
currency: misc.openingWeekendGross.gross.total.currency,
date: formatDate(misc.openingWeekendGross.weekendEndDate),
},
}),
},
technicalSpecs: {
...(misc.technicalSpecifications.soundMixes.items.length && {
soundMixes: misc.technicalSpecifications.soundMixes.items.map(item => ({
id: item.id,
name: item.text,
})),
}),
...(misc.technicalSpecifications.aspectRatios.items.length && {
aspectRatios: misc.technicalSpecifications.aspectRatios.items.map(
item => item.aspectRatio
),
}),
...(misc.technicalSpecifications.colorations.items.length && {
colorations: misc.technicalSpecifications.colorations.items.map(
item => ({ id: item.conceptId, name: item.text })
),
}),
...(main.runtime && { runtime: main.runtime?.seconds }),
},
moreLikeThis: misc.moreLikeThisTitles.edges.map(title => ({
id: title.node.id,
title: title.node.titleText.text,
...(title.node.primaryImage && {
poster: {
id: title.node.primaryImage.id,
url: title.node.primaryImage.url,
},
}),
type: {
id: title.node.titleType.id as
| 'movie'
| 'tvSeries'
| 'tvEpisode'
| 'videoGame',
text: title.node.titleType.text,
},
certificate: title.node.certificate?.rating || null,
...(title.node.releaseYear && {
releaseYear: {
start: title.node.releaseYear.year,
end: title.node.releaseYear.endYear || null,
},
}),
runtime: title.node.runtime?.seconds || null,
ratings: {
avg: title.node.ratingsSummary.aggregateRating || null,
numVotes: title.node.ratingsSummary.voteCount,
},
genres: title.node.titleCardGenres.genres.map(genre => genre.text),
})),
};
return cleanData;
};
export default cleanTitle;

View File

@ -0,0 +1,29 @@
// external deps
import * as cheerio from 'cheerio';
// local files
import axiosInstance from '../axiosInstance';
import cleanTitle from '../cleaners/title';
import { AppError } from '../helpers';
// interfaces
import RawTitle from '../../interfaces/misc/rawTitle';
const title = async (titleId: string) => {
try {
// getting data
const res = await axiosInstance(`/title/${titleId}`);
const $ = cheerio.load(res.data);
const rawData = $('script#__NEXT_DATA__').text();
// cleaning it a bit
const parsedRawData: RawTitle = JSON.parse(rawData);
const cleanData = cleanTitle(parsedRawData);
// returning
return cleanData;
} catch (err: any) {
if (err.response?.status === 404)
throw new AppError('not found', 404, err.cause);
throw new AppError('something went wrong', 500, err.cause);
}
};
export default title;

62
src/utils/helpers.ts Normal file
View File

@ -0,0 +1,62 @@
export const formatTime = (timeInSecs: number) => {
if (!timeInSecs) return;
// year, month, date, hours, minutes, seconds
// (passing all except seconds zero because they don't matter. seconds will overflow to minutes and hours.)
const dateObj = new Date(0, 0, 0, 0, 0, timeInSecs);
const days = dateObj.getDate();
const hours = dateObj.getHours();
const minutes = dateObj.getMinutes();
const seconds = dateObj.getSeconds();
// example for movie runtime spanning days: /title/tt0284020
return `${days === 31 ? '' : days + 'd'} ${!hours ? '' : hours + 'h'} ${
!minutes ? '' : minutes + 'm'
} ${!seconds ? '' : seconds + 's'}`.trim();
};
export const formatNumber = (num: number) => {
return new Intl.NumberFormat('en-US', {
notation: 'compact',
}).format(num);
};
export function formatDate(dateStr: string): string;
export function formatDate(year: number, month: number, date: number): string;
export function formatDate(
dateStrOrYear: unknown,
month?: unknown,
date?: unknown
) {
const options = { dateStyle: 'medium' } as const;
if (
typeof dateStrOrYear === 'string' &&
typeof month === 'undefined' &&
typeof date === 'undefined'
)
return new Date(dateStrOrYear).toLocaleString('en-US', options);
return new Date(
dateStrOrYear as number,
month as number,
date as number
).toLocaleString('en-US', options);
}
export const formatMoney = (num: number, cur: string) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: cur,
maximumFractionDigits: 0,
}).format(num);
};
export const modifyIMDbImg = (url: string, widthInPx = 600) => {
return url.replaceAll('.jpg', `UX${widthInPx}.jpg`);
};
export const AppError = class extends Error {
constructor(message: string, public statusCode: number, cause?: any) {
super(message, cause);
Error.captureStackTrace(this, AppError);
}
};