feat(route): letterboxd watchlist (#20046)

* feat: add letterboxd watchlist

* feat: add caching

* fix: remove redundant async

* fix: allows empty

* Update index.ts

Co-authored-by: Tony <TonyRL@users.noreply.github.com>

* fix: use cleaner selector and avoid double slashes in URL

* fix: poster api url
This commit is contained in:
Tsung-Han Yu 2025-09-16 22:06:49 +08:00 committed by GitHub
parent 4e6aa429a5
commit 9dab61134c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 73 additions and 0 deletions

View File

@ -0,0 +1,65 @@
import { Route } from '@/types';
import { namespace } from './namespace';
import ofetch from '@/utils/ofetch';
import cache from '@/utils/cache';
import { load } from 'cheerio';
import type { Context } from 'hono';
const baseUrl = `https://${namespace.url}`;
export const route: Route = {
path: '/:username/watchlist',
categories: ['social-media'],
example: '/letterboxd/matthew/watchlist',
parameters: {
username: 'Letterboxd username',
},
radar: [
{
source: ['letterboxd.com/:username/watchlist/'],
},
],
name: 'User Watchlist',
maintainers: ['johan456789'],
handler,
url: 'letterboxd.com',
};
async function handler(ctx: Context) {
const { username } = ctx.req.param();
const currentUrl = `${baseUrl}/${username}/watchlist/`;
const html = await ofetch(currentUrl);
const $ = load(html);
const wrappers = $('div.react-component[data-component-class*="LazyPoster"]').toArray();
const items = await Promise.all(
wrappers.map(async (el) => {
const wrapper = $(el);
const linkPath = wrapper.attr('data-item-link') || wrapper.attr('data-target-link') || '';
const link = linkPath ? new URL(linkPath, baseUrl).href : undefined;
const title = (wrapper.attr('data-item-full-display-name') || wrapper.attr('data-item-name') || '') as string;
let image: string | undefined;
if (link) {
const posterApiUrl = `${link}poster/std/125/`;
const cacheKey = `letterboxd:poster:${posterApiUrl}`;
const posterData = await cache.tryGet<Record<string, any>>(cacheKey, () => ofetch(posterApiUrl, { responseType: 'json' }));
image = posterData.url2x || posterData.url;
}
return {
title,
link,
image,
};
})
);
return {
title: $('title').text() || `${username}'s Watchlist • Letterboxd`,
link: currentUrl,
item: items,
allowEmpty: true
};
}

View File

@ -0,0 +1,8 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Letterboxd',
url: 'letterboxd.com',
categories: ['social-media'],
lang: 'en',
};