feat: fetch images from media proxy on frontend

This commit is contained in:
httpjamesm 2022-10-31 17:37:36 -04:00
parent b7ee6863e5
commit dba2ba5aa4
5 changed files with 70 additions and 52 deletions

View File

@ -1,22 +1,27 @@
import { Fragment } from 'react'; import { Fragment } from 'react'
import Image from 'next/future/image'; import Image from 'next/future/image'
import Link from 'next/link'; import Link from 'next/link'
import { formatNumber, formatTime, modifyIMDbImg } from '../../utils/helpers'; import {
import { Basic } from '../../interfaces/shared/title'; formatNumber,
import styles from '../../styles/modules/components/title/basic.module.scss'; formatTime,
getProxiedIMDbImgUrl,
modifyIMDbImg,
} from '../../utils/helpers'
import { Basic } from '../../interfaces/shared/title'
import styles from '../../styles/modules/components/title/basic.module.scss'
type Props = { type Props = {
className: string; className: string
data: Basic; data: Basic
}; }
const Basic = ({ data, className }: Props) => { const Basic = ({ data, className }: Props) => {
const titleType = data.type.id; const titleType = data.type.id
const releaseTime = const releaseTime =
titleType === 'tvSeries' titleType === 'tvSeries'
? `${data.releaseYear?.start}-${data.releaseYear?.end || 'present'}` ? `${data.releaseYear?.start}-${data.releaseYear?.end || 'present'}`
: data.releaseYear?.start; : data.releaseYear?.start
return ( return (
<section <section
@ -29,7 +34,8 @@ const Basic = ({ data, className }: Props) => {
className={styles.imageContainer} className={styles.imageContainer}
style={{ style={{
backgroundImage: backgroundImage:
data.poster && `url(${modifyIMDbImg(data.poster.url, 300)})`, data.poster &&
`url(${getProxiedIMDbImgUrl(modifyIMDbImg(data.poster.url, 300))})`,
}} }}
> >
{data.poster ? ( {data.poster ? (
@ -39,11 +45,11 @@ const Basic = ({ data, className }: Props) => {
alt={data.poster.caption} alt={data.poster.caption}
priority priority
fill fill
sizes='300px' sizes="300px"
/> />
) : ( ) : (
<svg className={styles.image__NA}> <svg className={styles.image__NA}>
<use href='/svg/sprite.svg#icon-image-slash' /> <use href="/svg/sprite.svg#icon-image-slash" />
</svg> </svg>
)} )}
</div> </div>
@ -51,7 +57,7 @@ const Basic = ({ data, className }: Props) => {
<h1 className={`${styles.title} heading heading__primary`}> <h1 className={`${styles.title} heading heading__primary`}>
{data.title} {data.title}
</h1> </h1>
<ul className={styles.meta} aria-label='quick facts'> <ul className={styles.meta} aria-label="quick facts">
{data.status.id !== 'released' && ( {data.status.id !== 'released' && (
<li className={styles.meta__text}>{data.status.text}</li> <li className={styles.meta__text}>{data.status.text}</li>
)} )}
@ -72,7 +78,7 @@ const Basic = ({ data, className }: Props) => {
<p className={styles.rating}> <p className={styles.rating}>
<span className={styles.rating__num}>{data.ratings.avg}</span> <span className={styles.rating__num}>{data.ratings.avg}</span>
<svg className={styles.rating__icon}> <svg className={styles.rating__icon}>
<use href='/svg/sprite.svg#icon-rating'></use> <use href="/svg/sprite.svg#icon-rating"></use>
</svg> </svg>
<span className={styles.rating__text}> Avg. rating</span> <span className={styles.rating__text}> Avg. rating</span>
</p> </p>
@ -81,7 +87,7 @@ const Basic = ({ data, className }: Props) => {
{formatNumber(data.ratings.numVotes)} {formatNumber(data.ratings.numVotes)}
</span> </span>
<svg className={styles.rating__icon}> <svg className={styles.rating__icon}>
<use href='/svg/sprite.svg#icon-like-dislike'></use> <use href="/svg/sprite.svg#icon-like-dislike"></use>
</svg> </svg>
<span className={styles.rating__text}> No. of votes</span> <span className={styles.rating__text}> No. of votes</span>
</p> </p>
@ -93,7 +99,7 @@ const Basic = ({ data, className }: Props) => {
{formatNumber(data.ranking.position)} {formatNumber(data.ranking.position)}
</span> </span>
<svg className={styles.rating__icon}> <svg className={styles.rating__icon}>
<use href='/svg/sprite.svg#icon-graph-rising'></use> <use href="/svg/sprite.svg#icon-graph-rising"></use>
</svg> </svg>
<span className={styles.rating__text}> <span className={styles.rating__text}>
{' '} {' '}
@ -130,7 +136,7 @@ const Basic = ({ data, className }: Props) => {
<span className={styles.overview__text}>{data.plot || '-'}</span> <span className={styles.overview__text}>{data.plot || '-'}</span>
</p> </p>
} }
{data.primaryCrew.map(crewType => ( {data.primaryCrew.map((crewType) => (
<p className={styles.crewType} key={crewType.type.id}> <p className={styles.crewType} key={crewType.type.id}>
<span className={styles.crewType__heading}> <span className={styles.crewType__heading}>
{`${crewType.type.category}: `} {`${crewType.type.category}: `}
@ -147,7 +153,7 @@ const Basic = ({ data, className }: Props) => {
))} ))}
</div> </div>
</section> </section>
); )
}; }
export default Basic; export default Basic

View File

@ -1,37 +1,39 @@
import Image from 'next/future/image'; import Image from 'next/future/image'
import Link from 'next/link'; import Link from 'next/link'
import { NextRouter } from 'next/router'; import { NextRouter } from 'next/router'
import { Media } from '../../interfaces/shared/title'; import { Media } from '../../interfaces/shared/title'
import { modifyIMDbImg } from '../../utils/helpers'; import { getProxiedIMDbImgUrl, modifyIMDbImg } from '../../utils/helpers'
import styles from '../../styles/modules/components/title/media.module.scss'; import styles from '../../styles/modules/components/title/media.module.scss'
type Props = { type Props = {
className: string; className: string
media: Media; media: Media
router: NextRouter; router: NextRouter
}; }
const Media = ({ className, media, router }: Props) => { const Media = ({ className, media, router }: Props) => {
return ( return (
<div className={`${className} ${styles.media}`}> <div className={`${className} ${styles.media}`}>
{(media.trailer || !!media.videos.total) && ( {(media.trailer || !!media.videos.total) && (
<section className={styles.videos}> <section className={styles.videos}>
<h2 className='heading heading__secondary'>Videos</h2> <h2 className="heading heading__secondary">Videos</h2>
<div className={styles.videos__container}> <div className={styles.videos__container}>
{media.trailer && ( {media.trailer && (
<div key={router.asPath} className={styles.trailer}> <div key={router.asPath} className={styles.trailer}>
<video <video
aria-label='trailer video' aria-label="trailer video"
// it's a relatively new tag. hence jsx-all1 complains // it's a relatively new tag. hence jsx-all1 complains
aria-description={media.trailer.caption} aria-description={media.trailer.caption}
controls controls
playsInline playsInline
poster={modifyIMDbImg(media.trailer.thumbnail)} poster={getProxiedIMDbImgUrl(
modifyIMDbImg(media.trailer.thumbnail)
)}
className={styles.trailer__video} className={styles.trailer__video}
> >
{media.trailer.urls.map(source => ( {media.trailer.urls.map((source) => (
<source <source
key={source.url} key={source.url}
type={source.mimeType} type={source.mimeType}
@ -44,15 +46,15 @@ const Media = ({ className, media, router }: Props) => {
)} )}
{!!media.videos.total && {!!media.videos.total &&
media.videos.videos.map(video => ( media.videos.videos.map((video) => (
<Link href={`/video/${video.id}`} key={video.id}> <Link href={`/video/${video.id}`} key={video.id}>
<a className={styles.video}> <a className={styles.video}>
<Image <Image
className={styles.video__img} className={styles.video__img}
src={modifyIMDbImg(video.thumbnail)} src={modifyIMDbImg(video.thumbnail)}
alt='' alt=""
fill fill
sizes='400px' sizes="400px"
/> />
<p className={styles.video__caption}> <p className={styles.video__caption}>
{video.caption} ({video.runtime}s) {video.caption} ({video.runtime}s)
@ -65,16 +67,16 @@ const Media = ({ className, media, router }: Props) => {
)} )}
{!!media.images.total && ( {!!media.images.total && (
<section className={styles.images}> <section className={styles.images}>
<h2 className='heading heading__secondary'>Images</h2> <h2 className="heading heading__secondary">Images</h2>
<div className={styles.images__container}> <div className={styles.images__container}>
{media.images.images.map(image => ( {media.images.images.map((image) => (
<figure key={image.id} className={styles.image}> <figure key={image.id} className={styles.image}>
<Image <Image
className={styles.image__img} className={styles.image__img}
src={modifyIMDbImg(image.url)} src={modifyIMDbImg(image.url)}
alt='' alt=""
fill fill
sizes='400px' sizes="400px"
/> />
<figcaption className={styles.image__caption}> <figcaption className={styles.image__caption}>
{image.caption.plainText} {image.caption.plainText}
@ -85,6 +87,6 @@ const Media = ({ className, media, router }: Props) => {
</section> </section>
)} )}
</div> </div>
); )
}; }
export default Media; export default Media

View File

@ -14,7 +14,7 @@ export default async function handler(
if (!mediaUrl) { if (!mediaUrl) {
res.status(400) res.status(400)
res.send(null) res.end()
return return
} }
@ -24,7 +24,7 @@ export default async function handler(
mediaUrlParsed = new URL(mediaUrl) mediaUrlParsed = new URL(mediaUrl)
} catch { } catch {
res.status(400) res.status(400)
res.send(null) res.end()
return return
} }
@ -33,13 +33,13 @@ export default async function handler(
if (!mediaDomain.endsWith('media-amazon.com')) { if (!mediaDomain.endsWith('media-amazon.com')) {
res.status(400) res.status(400)
res.send(null) res.end()
return return
} }
if (mediaUrlParsed.protocol !== 'https:') { if (mediaUrlParsed.protocol !== 'https:') {
res.status(400) res.status(400)
res.send(null) res.end()
return return
} }
@ -54,7 +54,7 @@ export default async function handler(
if (!validExtension) { if (!validExtension) {
res.status(400) res.status(400)
res.send(null) res.end()
return return
} }
@ -79,6 +79,7 @@ export default async function handler(
if (!mediaRes.ok) { if (!mediaRes.ok) {
res.status(mediaRes.status) res.status(mediaRes.status)
res.end()
return return
} }

View File

@ -20,6 +20,7 @@ import { AppError } from '../../../interfaces/shared/error'
// styles // styles
import styles from '../../../styles/modules/pages/title/title.module.scss' import styles from '../../../styles/modules/pages/title/title.module.scss'
import Head from 'next/head' import Head from 'next/head'
import { getProxiedIMDbImgUrl } from '../../../utils/helpers'
type Props = { data: Title; error: null } | { error: AppError; data: null } type Props = { data: Title; error: null } | { error: AppError; data: null }
@ -50,7 +51,11 @@ const TitleInfo = ({ data, error }: Props) => {
<Head> <Head>
<meta <meta
title="og:image" title="og:image"
content={data.basic.poster?.url || '/icon-512.png'} content={
data.basic.poster?.url
? getProxiedIMDbImgUrl(data.basic.poster?.url)
: '/icon-512.png'
}
/> />
</Head> </Head>
<Layout className={styles.title}> <Layout className={styles.title}>

View File

@ -53,6 +53,10 @@ export const modifyIMDbImg = (url: string, widthInPx = 600) => {
return url.replaceAll('.jpg', `UX${widthInPx}.jpg`); return url.replaceAll('.jpg', `UX${widthInPx}.jpg`);
}; };
export const getProxiedIMDbImgUrl = (url: string) => {
return `/api/media_proxy?url=${encodeURIComponent(url)}`;
}
export const AppError = class extends Error { export const AppError = class extends Error {
constructor(message: string, public statusCode: number, cause?: any) { constructor(message: string, public statusCode: number, cause?: any) {
super(message, cause); super(message, cause);