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:
14
src/utils/axiosInstance.ts
Normal file
14
src/utils/axiosInstance.ts
Normal 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
377
src/utils/cleaners/title.ts
Normal 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;
|
29
src/utils/fetchers/title.ts
Normal file
29
src/utils/fetchers/title.ts
Normal 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
62
src/utils/helpers.ts
Normal 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);
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user