From a70f63840c9fb8e1362987977f32251a5339822c Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 3 Jan 2024 15:25:59 +0800 Subject: [PATCH] feat: support tmdb link card parser Signed-off-by: Innei --- .env.template | 3 +- public/favicon.ico | Bin 0 -> 4286 bytes src/app/api/tmdb/[...all]/route.ts | 48 +++++++++++++ src/components/icons/platform/TheMovieDB.tsx | 18 +++++ src/components/icons/star.tsx | 21 ++++++ .../ui/link-card/LinkCard.module.css | 5 +- src/components/ui/link-card/LinkCard.tsx | 65 +++++++++++++++++- src/components/ui/link-card/enums.tsx | 1 + .../ui/markdown/renderers/LinkRenderer.tsx | 6 ++ src/components/ui/rich-link/Favicon.tsx | 9 +++ src/lib/link-parser.ts | 12 ++++ 11 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 src/app/api/tmdb/[...all]/route.ts create mode 100644 src/components/icons/platform/TheMovieDB.tsx create mode 100644 src/components/icons/star.tsx diff --git a/.env.template b/.env.template index 06b1f61848..5e5753cd6f 100644 --- a/.env.template +++ b/.env.template @@ -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= diff --git a/public/favicon.ico b/public/favicon.ico index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b97a417a37ef15a04ffe4557130f09d14ffe958b 100644 GIT binary patch literal 4286 zcmc(iNhrNv7{^~^h(yMP>1N2^=9dkb7Jp@BE(@#-VNJ1QWuuTtrZR+*1q;fMtc;N| zQ&y&;OeHhl|NWj{=l0(BUiZHK{Q5mQ_kG`Up7VU3Gd$-!6ve^#XD}%IRYtrOB|uS> ze~iUgrTl9wtQTTzi}>}s{H{=W=VuFDJryS>CunJD!PwXs zuCA{1bTQMpxVXUh_&5dz25^3UZq{b+V`XIpNl8iY_4P%0c{zrLhOocC|K+i~zrSO4 zb{0iNMOa>5wucq%b8~Zuii(19f56Yr56;fcaCCG;KtKTM>gsf_>*C@fii?X885s#D zCnsT(wrvT=E!pJf=jS6UD@*v{?d>gm7#tk5WkR&w-QA(KwpQ5k^z;;Cp*`9r!4V_`~NdxTyk&O68cGD{B3P*AwE9d#P|322bmQ5QBhGL+EP@F(~jOMidA*jr?gNp?<7j>#w1G3LH{Ug@2u;o)HupFS`)A|fJ8e16l%!^1

YhLN;Ta-#lZ3f`Ske6NARa zMiIMu?CRCIy}d<3LV}4+CfSlNzxm`o$ji&a+1Z(1^LF*t*Vp0W<72^hcXv0dBR?Y} z1Cx`J;2ru?vazut?ho3Rh3xR~aML&IxemOu3JVKyaB%P=_=krFG&MDeGgh+s ze3QC0H8tNl4|$gHPLuZJlT5OC2b7hSS+q+%&cXcr{QsH)-hIpi)G;RQk}2=tg@px+ zy&?7ZjE#*Ix$IY*VE)Z>#9DkYuOm|m{ovWBW@_IV6FogWBFC2ftUVteA4T3$Qc_}- zO}k{tu}d4wvl0^%ZTjHi;v#gI+cB?Z4#B<6TxfcF8VwB%NKa2UyXUpzAcHJx!kkro zA8GrlzqzK&ExA9KSE%zVwVwK&w#l$1Gcz-~eNfxB_f9r#>k;~>y({e5x7AKt_QtLC zi)+N#(cT%hINCP$rH#+lTg%jb^M6XlNoi>*LPA15Jtx&%o@>Fu!J-fQTKivB%dz*( ke4A$#?~u&QOc)FXkt<3r^G4ROjWJJM_R%9o8-G>)16jAIBme*a literal 0 HcmV?d00001 diff --git a/src/app/api/tmdb/[...all]/route.ts b/src/app/api/tmdb/[...all]/route.ts new file mode 100644 index 0000000000..00b3d992df --- /dev/null +++ b/src/app/api/tmdb/[...all]/route.ts @@ -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) +} diff --git a/src/components/icons/platform/TheMovieDB.tsx b/src/components/icons/platform/TheMovieDB.tsx new file mode 100644 index 0000000000..0feb4ee185 --- /dev/null +++ b/src/components/icons/platform/TheMovieDB.tsx @@ -0,0 +1,18 @@ +import type { SVGProps } from 'react' + +export function SimpleIconsThemoviedatabase(props: SVGProps) { + return ( + + + + ) +} diff --git a/src/components/icons/star.tsx b/src/components/icons/star.tsx new file mode 100644 index 0000000000..1ec60da905 --- /dev/null +++ b/src/components/icons/star.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from 'react' + +export function MingcuteStarHalfFill(props: SVGProps) { + return ( + + + + + + + ) +} diff --git a/src/components/ui/link-card/LinkCard.module.css b/src/components/ui/link-card/LinkCard.module.css index 984d8d3f07..a0a1814c17 100644 --- a/src/components/ui/link-card/LinkCard.module.css +++ b/src/components/ui/link-card/LinkCard.module.css @@ -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 { diff --git a/src/components/ui/link-card/LinkCard.tsx b/src/components/ui/link-card/LinkCard.tsx index 085dd11abd..2110a36ce0 100644 --- a/src/components/ui/link-card/LinkCard.tsx +++ b/src/components/ui/link-card/LinkCard.tsx @@ -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' @@ -44,6 +46,10 @@ type CardState = { desc?: ReactNode image?: string color?: string + + classNames?: Partial<{ + image: string + }> } const LinkCardImpl: FC = (props) => { @@ -114,6 +120,7 @@ const LinkCardImpl: FC = (props) => { const LinkComponent = source === 'self' ? Link : 'a' + const classNames = cardInfo?.classNames || {} return ( = (props) => { {(loading || cardInfo?.image) && ( const fetchFunction = fetchDataFunctions[source] @@ -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: ( + + {title} + {title !== originalTitle && ( + ({originalTitle}) + )} + + + + {json.vote_average > 0 && json.vote_average.toFixed(1)} + + + + ), + desc: ( + + {json.overview} + + ), + 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) + }, +} diff --git a/src/components/ui/link-card/enums.tsx b/src/components/ui/link-card/enums.tsx index 38cf23f0f2..52ea50bee6 100644 --- a/src/components/ui/link-card/enums.tsx +++ b/src/components/ui/link-card/enums.tsx @@ -6,4 +6,5 @@ export enum LinkCardSource { MixSpace = 'mx-space', GHCommit = 'gh-commit', GHPr = 'gh-pr', + TMDB = 'tmdb', } diff --git a/src/components/ui/markdown/renderers/LinkRenderer.tsx b/src/components/ui/markdown/renderers/LinkRenderer.tsx index 39ba8475e9..fc7d1641e1 100644 --- a/src/components/ui/markdown/renderers/LinkRenderer.tsx +++ b/src/components/ui/markdown/renderers/LinkRenderer.tsx @@ -13,6 +13,7 @@ import { isGithubRepoUrl, isGithubUrl, isSelfArticleUrl, + isTMDBUrl, isTweetUrl, isYoutubeUrl, parseGithubGistUrl, @@ -107,6 +108,11 @@ export const BlockLinkRenderer = ({ ) } + case isTMDBUrl(url): { + return ( + + ) + } default: return fallbackElement diff --git a/src/components/ui/rich-link/Favicon.tsx b/src/components/ui/rich-link/Favicon.tsx index 1ffcd0802d..ad9490a819 100644 --- a/src/components/ui/rich-link/Favicon.tsx +++ b/src/components/ui/rich-link/Favicon.tsx @@ -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' @@ -9,6 +10,7 @@ import { isBilibiliUrl, isGithubUrl, isTelegramUrl, + isTMDBUrl, isTwitterUrl, isWikipediaUrl, isZhihuUrl, @@ -21,6 +23,9 @@ const prefixToIconMap = { BL: , ZH: , WI: , + TMDB: ( + + ), } const getUrlSource = (url: URL) => { @@ -49,6 +54,10 @@ const getUrlSource = (url: URL) => { type: 'WI', test: isWikipediaUrl, }, + { + type: 'TMDB', + test: isTMDBUrl, + }, ] return map.find((item) => item.test(url))?.type diff --git a/src/lib/link-parser.ts b/src/lib/link-parser.ts index f726381d56..1c25e08af6 100644 --- a/src/lib/link-parser.ts +++ b/src/lib/link-parser.ts @@ -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) { @@ -166,3 +170,11 @@ export const parseGithubPrUrl = (url: URL) => { pr, } } + +export const parseTMDBUrl = (url: URL) => { + const [_, type, id] = url.pathname.split('/') + return { + type, + id, + } +}