Skip to content

Commit

Permalink
feat: support tmdb link card parser
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Jan 3, 2024
1 parent 29db44c commit a70f638
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 5 deletions.
3 changes: 2 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/


OPENAI_API_KEY=

TMDB_API_KEY=
Binary file modified public/favicon.ico
Binary file not shown.
48 changes: 48 additions & 0 deletions src/app/api/tmdb/[...all]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// reverse proxy to themoviedb api
//

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

import { NextServerResponse } from '~/lib/edge-function.server'

export const runtime = 'edge'
export const GET = async (req: NextRequest) => {
const pathname = req.nextUrl.pathname.split('/').slice(3)
const query = req.nextUrl.searchParams

query.delete('all')

const res = new NextServerResponse()
const allowedTypes = ['tv', 'movie']
const allowedPathLength = 2
if (
pathname.length > allowedPathLength ||
!allowedTypes.includes(pathname[0])
) {
return res.status(400).send('This request is not allowed')
}

const searchString = query.toString()

const url = `https://api.themoviedb.org/3/${pathname.join('/')}${
searchString ? `?${searchString}` : ''
}`

const headers = new Headers()
headers.set(
'User-Agent',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko), Shiro',
)
headers.set('Authorization', `Bearer ${process.env.TMDB_API_KEY}`)

if (!process.env.TMDB_API_KEY) {
return res.status(500).send('TMDB_API_KEY is not set')
}

const response = await fetch(url, {
headers,
})
const data = await response.json()
return NextResponse.json(data)
}
18 changes: 18 additions & 0 deletions src/components/icons/platform/TheMovieDB.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { SVGProps } from 'react'

export function SimpleIconsThemoviedatabase(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M6.62 12a2.291 2.291 0 0 1 2.292-2.295h-.013A2.291 2.291 0 0 1 11.189 12a2.291 2.291 0 0 1-2.29 2.291h.013A2.291 2.291 0 0 1 6.62 12m10.72-4.062h4.266a2.291 2.291 0 0 0 2.29-2.291a2.291 2.291 0 0 0-2.29-2.296H17.34a2.291 2.291 0 0 0-2.291 2.296a2.291 2.291 0 0 0 2.29 2.29zM2.688 20.645h8.285a2.291 2.291 0 0 0 2.291-2.292a2.291 2.291 0 0 0-2.29-2.295H2.687a2.291 2.291 0 0 0-2.291 2.295a2.291 2.291 0 0 0 2.29 2.292zm10.881-6.354h.81l1.894-4.586H15.19l-1.154 3.008h-.013l-1.135-3.008h-1.154zm4.208 0h1.011V9.705h-1.011zm2.878 0h3.235v-.93h-2.223v-.933h1.99v-.934h-1.99v-.855h2.107v-.934h-3.112zM1.31 7.941h1.01V4.247h1.31v-.895H0v.895h1.31zm3.747 0h1.011V5.959h1.958v1.984h1.011v-4.59h-1.01v1.711H6.061V3.351H5.057zm5.348 0h3.242v-.933H11.41v-.934h1.99v-.933h-1.99v-.856h2.107v-.934h-3.112zM.162 14.296h1.005v-3.52h.013l1.167 3.52h.765l1.206-3.52h.013v3.52h1.011v-4.59H3.82L2.755 12.7h-.013L1.686 9.705H.156zm14.534 6.353h1.641a3.188 3.188 0 0 0 .98-.149a2.531 2.531 0 0 0 .824-.437a2.123 2.123 0 0 0 .567-.713a2.193 2.193 0 0 0 .223-.983a2.399 2.399 0 0 0-.218-1.07a1.958 1.958 0 0 0-.586-.716a2.405 2.405 0 0 0-.873-.392a4.349 4.349 0 0 0-1.046-.13h-1.519zm1.013-3.656h.596a2.26 2.26 0 0 1 .606.08a1.514 1.514 0 0 1 .503.244a1.167 1.167 0 0 1 .34.412a1.28 1.28 0 0 1 .13.587a1.546 1.546 0 0 1-.13.658a1.127 1.127 0 0 1-.347.433a1.41 1.41 0 0 1-.518.238a2.797 2.797 0 0 1-.649.07h-.538zm4.686 3.656h1.88a2.997 2.997 0 0 0 .613-.064a1.735 1.735 0 0 0 .554-.214a1.221 1.221 0 0 0 .402-.39a1.105 1.105 0 0 0 .155-.606a1.188 1.188 0 0 0-.071-.415a1.01 1.01 0 0 0-.204-.34a1.087 1.087 0 0 0-.317-.24a1.297 1.297 0 0 0-.413-.13v-.012a1.203 1.203 0 0 0 .575-.366a.962.962 0 0 0 .216-.648a1.081 1.081 0 0 0-.149-.603a1.022 1.022 0 0 0-.389-.354a1.673 1.673 0 0 0-.54-.169a4.463 4.463 0 0 0-.6-.041h-1.712zm1.011-3.734h.687a1.4 1.4 0 0 1 .24.022a.748.748 0 0 1 .22.075a.432.432 0 0 1 .16.147a.418.418 0 0 1 .061.236a.47.47 0 0 1-.055.233a.433.433 0 0 1-.146.156a.62.62 0 0 1-.204.084a1.058 1.058 0 0 1-.23.026h-.745zm0 1.835h.765a1.96 1.96 0 0 1 .266.02a1.015 1.015 0 0 1 .26.07a.519.519 0 0 1 .204.152a.406.406 0 0 1 .08.26a.481.481 0 0 1-.06.253a.519.519 0 0 1-.16.168a.62.62 0 0 1-.217.09a1.155 1.155 0 0 1-.237.027H21.4z"
/>
</svg>
)
}
21 changes: 21 additions & 0 deletions src/components/icons/star.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { SVGProps } from 'react'

export function MingcuteStarHalfFill(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<g fill="none" fillRule="evenodd">
<path d="M24 0v24H0V0h24ZM12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036c-.01-.003-.019 0-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.016-.018Zm.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01l-.184-.092Z" />
<path
fill="currentColor"
d="M13.08 2.868a1.25 1.25 0 0 0-2.16 0L8.126 7.665L2.697 8.842a1.25 1.25 0 0 0-.667 2.054l3.7 4.141l-.56 5.525a1.25 1.25 0 0 0 1.748 1.27L12 19.592l5.082 2.24a1.25 1.25 0 0 0 1.748-1.27l-.56-5.525l3.7-4.14a1.25 1.25 0 0 0-.667-2.055l-5.428-1.176l-2.795-4.798ZM12 17.523c.172 0 .344.035.504.106l4.206 1.854l-.463-4.573a1.25 1.25 0 0 1 .312-.959l3.062-3.427l-4.492-.973a1.25 1.25 0 0 1-.816-.592L12 4.987v12.536Z"
/>
</g>
</svg>
)
}
5 changes: 2 additions & 3 deletions src/components/ui/link-card/LinkCard.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@
display: block;
margin-top: 0.429rem;
min-width: 0;
font-size: 1rem;
height: 1.143rem;
line-height: 1.143rem;
font-size: 0.9rem;
line-height: 1.4;
}

.image {
Expand Down
65 changes: 64 additions & 1 deletion src/components/ui/link-card/LinkCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import type { FC, ReactNode, SyntheticEvent } from 'react'
import { simpleCamelcaseKeys as camelcaseKeys } from '@mx-space/api-client'

import { LazyLoad } from '~/components/common/Lazyload'
import { MingcuteStarHalfFill } from '~/components/icons/star'
import { usePeek } from '~/components/widgets/peek/usePeek'
import { LanguageToColorMap } from '~/constants/language'
import { useIsClientTransition } from '~/hooks/common/use-is-client'
import { preventDefault } from '~/lib/dom'
import { fetchGitHubApi } from '~/lib/github'
import { clsxm } from '~/lib/helper'
import { getDominantColor } from '~/lib/image'
import { apiClient } from '~/lib/request'

Expand Down Expand Up @@ -44,6 +46,10 @@ type CardState = {
desc?: ReactNode
image?: string
color?: string

classNames?: Partial<{
image: string
}>
}

const LinkCardImpl: FC<LinkCardProps> = (props) => {
Expand Down Expand Up @@ -114,6 +120,7 @@ const LinkCardImpl: FC<LinkCardProps> = (props) => {

const LinkComponent = source === 'self' ? Link : 'a'

const classNames = cardInfo?.classNames || {}
return (
<LinkComponent
href={fullUrl}
Expand Down Expand Up @@ -160,7 +167,7 @@ const LinkCardImpl: FC<LinkCardProps> = (props) => {
</span>
{(loading || cardInfo?.image) && (
<span
className={styles['image']}
className={clsxm(styles['image'], classNames.image)}
data-image={cardInfo?.image || ''}
style={{
backgroundImage: cardInfo?.image
Expand Down Expand Up @@ -203,6 +210,7 @@ function validTypeAndFetchFunction(source: LinkCardSource, id: string) {
[LinkCardSource.GHCommit]: fetchGitHubCommitData,
[LinkCardSource.GHPr]: fetchGitHubPRData,
[LinkCardSource.Self]: fetchMxSpaceData,
[LinkCardSource.TMDB]: fetchTheMovieDBData,
} as Record<LinkCardSource, FetchObject>

const fetchFunction = fetchDataFunctions[source]
Expand Down Expand Up @@ -393,3 +401,58 @@ const fetchMxSpaceData: FetchObject = {
}
},
}

const fetchTheMovieDBData: FetchObject = {
isValid(id) {
// tv/218230
const [type, realId] = id.split('/')

const canParsedTypes = ['tv', 'movie']
return canParsedTypes.includes(type) && realId.length > 0
},
async fetch(id, setCardInfo, setFullUrl) {
const [type, realId] = id.split('/')

const json = await fetch(`/api/tmdb/${type}/${realId}?language=zh-CN`)
.then((r) => r.json())
.catch((err) => {
console.error('Error fetching TMDB data:', err)
throw err
})

const title = type === 'tv' ? json.name : json.title
const originalTitle =
type === 'tv' ? json.original_name : json.original_title
setCardInfo({
title: (
<span className="flex flex-wrap items-end gap-2">
<span>{title}</span>
{title !== originalTitle && (
<span className="text-sm opacity-70">({originalTitle})</span>
)}
<span className="inline-flex flex-shrink-0 items-center gap-1 self-center text-xs text-orange-400 dark:text-yellow-500">
<MingcuteStarHalfFill />
<span className="font-sans font-medium">
{json.vote_average > 0 && json.vote_average.toFixed(1)}
</span>
</span>
</span>
),
desc: (
<span className="line-clamp-none overflow-visible whitespace-pre-wrap">
{json.overview}
</span>
),
image: `https://image.tmdb.org/t/p/w500${json.poster_path}`,
color: uniqolor(json.name, {
saturation: [30, 35],
lightness: [60, 70],
}).color,

classNames: {
image: 'self-start mt-4',
},
})
setFullUrl(json.homepage)
},
}
1 change: 1 addition & 0 deletions src/components/ui/link-card/enums.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export enum LinkCardSource {
MixSpace = 'mx-space',
GHCommit = 'gh-commit',
GHPr = 'gh-pr',
TMDB = 'tmdb',
}
6 changes: 6 additions & 0 deletions src/components/ui/markdown/renderers/LinkRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
isGithubRepoUrl,
isGithubUrl,
isSelfArticleUrl,
isTMDBUrl,
isTweetUrl,
isYoutubeUrl,
parseGithubGistUrl,
Expand Down Expand Up @@ -107,6 +108,11 @@ export const BlockLinkRenderer = ({
<LinkCard source={LinkCardSource.Self} id={url.pathname.slice(1)} />
)
}
case isTMDBUrl(url): {
return (
<LinkCard source={LinkCardSource.TMDB} id={url.pathname.slice(1)} />
)
}

default:
return fallbackElement
Expand Down
9 changes: 9 additions & 0 deletions src/components/ui/rich-link/Favicon.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BilibiliIcon } from '~/components/icons/platform/BilibiliIcon'
import { GitHubBrandIcon } from '~/components/icons/platform/GitHubBrandIcon'
import { IcBaselineTelegram } from '~/components/icons/platform/Telegram'
import { SimpleIconsThemoviedatabase } from '~/components/icons/platform/TheMovieDB'
import { TwitterIcon } from '~/components/icons/platform/Twitter'
import { WikipediaIcon } from '~/components/icons/platform/WikipediaIcon'
import { SimpleIconsZhihu } from '~/components/icons/platform/ZhihuIcon'
Expand All @@ -9,6 +10,7 @@ import {
isBilibiliUrl,
isGithubUrl,
isTelegramUrl,
isTMDBUrl,
isTwitterUrl,
isWikipediaUrl,
isZhihuUrl,
Expand All @@ -21,6 +23,9 @@ const prefixToIconMap = {
BL: <BilibiliIcon className="text-[#469ECF]" />,
ZH: <SimpleIconsZhihu className="text-[#0084FF]" />,
WI: <WikipediaIcon className="text-current" />,
TMDB: (
<SimpleIconsThemoviedatabase className="text-[#0D243F] dark:text-[#5CB7D2]" />
),
}

const getUrlSource = (url: URL) => {
Expand Down Expand Up @@ -49,6 +54,10 @@ const getUrlSource = (url: URL) => {
type: 'WI',
test: isWikipediaUrl,
},
{
type: 'TMDB',
test: isTMDBUrl,
},
]

return map.find((item) => item.test(url))?.type
Expand Down
12 changes: 12 additions & 0 deletions src/lib/link-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ export const isWikipediaUrl = (url: URL) => {
return url.hostname.includes('wikipedia.org')
}

export const isTMDBUrl = (url: URL) => {
return url.hostname.includes('themoviedb.org')
}

export const parseSelfArticleUrl = (url: URL) => {
const [_, type, ...rest] = url.pathname.split('/')
switch (type) {
Expand Down Expand Up @@ -166,3 +170,11 @@ export const parseGithubPrUrl = (url: URL) => {
pr,
}
}

export const parseTMDBUrl = (url: URL) => {
const [_, type, id] = url.pathname.split('/')
return {
type,
id,
}
}

1 comment on commit a70f638

@vercel
Copy link

@vercel vercel bot commented on a70f638 Jan 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

shiro – ./

shiro-innei.vercel.app
springtide.vercel.app
shiro-git-main-innei.vercel.app
innei.in

Please sign in to comment.