From bc3b3e21cd7ddb65f5daf247891e67cba1b05949 Mon Sep 17 00:00:00 2001 From: Tony Date: Sat, 25 May 2024 04:23:23 +0800 Subject: [PATCH] feat(route): imdb top charts (#15697) * feat(route): imdb top charts * fix: typo --- lib/routes/imdb/chart.ts | 61 ++++++++++++++++ lib/routes/imdb/namespace.ts | 6 ++ lib/routes/imdb/templates/chart.art | 15 ++++ lib/routes/imdb/types.ts | 103 ++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 lib/routes/imdb/chart.ts create mode 100644 lib/routes/imdb/namespace.ts create mode 100644 lib/routes/imdb/templates/chart.art create mode 100644 lib/routes/imdb/types.ts diff --git a/lib/routes/imdb/chart.ts b/lib/routes/imdb/chart.ts new file mode 100644 index 000000000..0c935de30 --- /dev/null +++ b/lib/routes/imdb/chart.ts @@ -0,0 +1,61 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import type { Context } from 'hono'; +import { ChartTitleSearchConnection } from './types'; +import path from 'path'; +import { getCurrentPath } from '@/utils/helpers'; +import { art } from '@/utils/render'; + +const __dirname = getCurrentPath(import.meta.url); +const render = (data) => art(path.join(__dirname, 'templates', 'chart.art'), data); + +export const route: Route = { + path: '/chart/:chart?', + categories: ['multimedia'], + parameters: { chart: 'The chart to display, `top` by default' }, + example: '/imdb/chart', + radar: [ + { + source: ['www.imdb.com/chart/:chart/'], + }, + ], + name: 'Charts', + maintainers: ['TonyRL'], + handler, + url: 'www.imdb.com/chart/top/', + description: `| Top 250 Movies | Most Popular Movies | Top 250 TV Shows | Most Popular TV Shows | + | -------------- | ------------------- | ---------------- | --------------------- | + | top | moviemeter | toptv | tvmeter |`, +}; + +async function handler(ctx: Context) { + const { chart = 'top' } = ctx.req.param(); + const baseUrl = 'https://www.imdb.com'; + const link = `${baseUrl}/chart/${chart}/`; + + const response = await ofetch(link); + const $ = cheerio.load(response); + const nextData = JSON.parse($('script#__NEXT_DATA__').text()); + const chartTitles = nextData.props.pageProps.pageData.chartTitles as ChartTitleSearchConnection; + + const items = chartTitles.edges.map(({ currentRank, node }) => ({ + title: `${currentRank}. ${node.titleText.text} (${node.releaseYear.year}${node.releaseYear.endYear ? `-${node.releaseYear.endYear}` : ''})`, + description: render({ + primaryImage: node.primaryImage, + originalTitleText: node.originalTitleText, + certificate: node.certificate, + ratingsSummary: node.ratingsSummary, + plot: node.plot, + }), + link: `${baseUrl}/title/${node.id}`, + category: node.titleGenres.genres.map((g) => chartTitles.genres.find((genre) => genre.filterId === g.genre.text)?.text), + })); + + return { + title: $('head title').text(), + description: $('head meta[name="description"]').attr('content'), + link, + item: items, + }; +} diff --git a/lib/routes/imdb/namespace.ts b/lib/routes/imdb/namespace.ts new file mode 100644 index 000000000..d4912d346 --- /dev/null +++ b/lib/routes/imdb/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'IMDb', + url: 'www.imdb.com', +}; diff --git a/lib/routes/imdb/templates/chart.art b/lib/routes/imdb/templates/chart.art new file mode 100644 index 000000000..57134175a --- /dev/null +++ b/lib/routes/imdb/templates/chart.art @@ -0,0 +1,15 @@ +{{ if primaryImage.url }} +
+ {{ primaryImage.caption.plainText }} +
{{ primaryImage.caption.plainText }}
+
+
+{{ /if }} + +Original title: {{ originalTitleText.text }}
+ +{{ if certificate }}{{ certificate.rating }}{{ /if }} +{{ if ratingsSummary.aggregateRating }}IMDb RATING: {{ ratingsSummary.aggregateRating }}/10 ({{ ratingsSummary.voteCount }}){{ /if }} +

+ +{{ plot.plotText.plainText }} diff --git a/lib/routes/imdb/types.ts b/lib/routes/imdb/types.ts new file mode 100644 index 000000000..06ef24f3f --- /dev/null +++ b/lib/routes/imdb/types.ts @@ -0,0 +1,103 @@ +interface SearchFacet { + filterId: string; + text: string; + total: number; + __typename: string; +} + +interface TitleGenres { + genre: { + text: string; + __typename: string; + }; + __typename: string; +} + +interface Title { + id: string; + titleText: { + text: string; + __typename: string; + }; + titleType: { + id: string; + text: string; + canHaveEpisodes: boolean; + displayableProperty: { + value: { + plainText: string; + __typename: string; + }; + __typename: string; + }; + __typename: string; + }; + originalTitleText: { + text: string; + __typename: string; + }; + primaryImage: { + id: string; + width: number; + height: number; + url: string; + caption: { + plainText: string; + __typename: string; + }; + __typename: string; + }; + releaseYear: { + year: number; + endYear: number | null; + __typename: string; + }; + ratingsSummary: { + aggregateRating: number; + voteCount: number; + __typename: string; + }; + runtime: { + seconds: number; + __typename: string; + }; + certificate: { + rating: string; + __typename: string; + } | null; + canRate: { + isRatable: boolean; + __typename: string; + }; + titleGenres: { + genres: TitleGenres[]; + __typename: string; + }; + canHaveEpisodes: boolean; + plot: { + plotText: { + plainText: string; + __typename: string; + }; + __typename: string; + }; + latestTrailer: { + id: string; + __typename: string; + } | null; + series: null; + __typename: string; +} + +interface ChartTitleEdge { + currentRank: number; + node: Title; + __typename: string; +} + +export interface ChartTitleSearchConnection { + edges: ChartTitleEdge[]; + genres: SearchFacet[]; + keywords: SearchFacet[]; + __typename: string; +}