feat(list): add list route

adds ability to see titles, names, and images lists

closes https://github.com/zyachel/libremdb/issues/6
This commit is contained in:
zyachel
2023-10-29 00:46:11 +05:30
parent 60fb23fc5b
commit 97f1432ac5
20 changed files with 818 additions and 0 deletions

View File

@ -1,4 +1,5 @@
export const titleKey = (titleId: string) => `title:${titleId}`;
export const nameKey = (nameId: string) => `name:${nameId}`;
export const listKey = (listId: string, pageNum = '1') => `list:${listId}?page=${pageNum}`;
export const findKey = (query: string) => `find:${query}`;
export const mediaKey = (url: string) => `media:${url}`;

141
src/utils/fetchers/list.ts Normal file
View File

@ -0,0 +1,141 @@
import axios, { AxiosError } from 'axios';
import * as cheerio from 'cheerio';
import type { Data, DataKind } from 'src/interfaces/shared/list';
import axiosInstance from 'src/utils/axiosInstance';
import { AppError, isIMDbImgPlaceholder } from 'src/utils/helpers';
const list = async (listId: string, pageNum = '1') => {
try {
const res = await axiosInstance(`/list/${listId}?page=${pageNum}`);
const $ = cheerio.load(res.data);
const $main = $('#main > .article');
const $meta = $main.children('#list-overview-summary');
const $footer = $main.find('.footer .desc .list-pagination');
const title = clean($main.children('h1.list-name'));
const numWithtype = clean($main.find('.sub-list .header .nav .desc')).split(' ');
if (numWithtype.length < 2) throw new AppError('invalid list', 400);
const [num, type] = numWithtype as [string, DataKind];
const meta = {
by: {
name: clean($meta.children('a')),
link: $meta.children('a').attr('href') ?? null,
},
created: clean($meta.find('#list-overview-created')),
updated: clean($meta.find('#list-overview-lastupdated')),
num,
type,
};
const description = clean($main.children('.list-description'));
const pagination = {
prev: $footer.children('a.prev-page').attr('href') ?? null,
range: clean($footer.children('.pagination-range')),
next: $footer.children('a.next-page').attr('href') ?? null,
};
const $imagesContainer = $main.find('.lister-list .media_index_thumb_list');
const $listItems = $main.find('.lister-list').children();
let data: Data<typeof type>[] = [];
// 1. images list
if (type === 'images') {
data = $imagesContainer
.find('a > img')
.map((_i, el) => $(el).attr('src'))
.toArray();
}
// 2. movies list
else if (type === 'titles') {
$listItems.each((_i, el) => {
let image = $(el).find('.lister-item-image > a > img.loadlate').attr('loadlate') ?? null;
if (image && isIMDbImgPlaceholder(image)) image = null;
const $content = $(el).children('.lister-item-content');
const $heading = $content.find('h3.lister-item-header > a');
const name = clean($heading);
const url = $heading.attr('href') ?? null;
const year = clean($heading.next('.lister-item-year'));
const $itemMeta = $content.find('h3.lister-item-header + p');
const certificate = clean($itemMeta.children('.certificate'));
const runtime = clean($itemMeta.children('.runtime'));
const genre = clean($itemMeta.children('.genre'));
const rating = clean($content.find('.ipl-rating-star__rating').first());
const metascore = clean($content.find('.metascore'));
const plot = clean($content.children('p[class=""]'));
// eg: [["Director", "Nabwana I.G.G."], ["Stars", "Kakule William, Sserunya Ernest, G. Puffs"]]
const otherInfo = $content
.children('p.text-muted.text-small')
.nextAll('p.text-muted.text-small')
.map((__i, infoEl) => {
const arr = clean($(infoEl)).replace(/\s+/g, ' ').split('|');
return arr.map(i => i.split(':'));
})
.toArray();
data.push({
image,
name,
url,
year,
certificate,
runtime,
genre,
plot,
rating,
metascore,
otherInfo,
});
});
}
// 3. actors list
else if (type === 'names') {
$listItems.each((_i, el) => {
let image = $(el).find('.lister-item-image > a > img').attr('src') ?? null;
if (image && isIMDbImgPlaceholder(image)) image = null;
const $content = $(el).children('.lister-item-content');
const $heading = $content.find('h3.lister-item-header > a');
const name = clean($heading);
const url = $heading.attr('href') ?? null;
const $itemMeta = $content.find('h3.lister-item-header + p');
const jobNKnownForRaw = clean($itemMeta.first()).split('|');
const job = jobNKnownForRaw.at(0) ?? null;
const knownFor = jobNKnownForRaw.at(1) ?? null;
const knownForLink = $itemMeta.children('a').attr('href') ?? null;
const about = clean($content.children('p:not([class])'));
data.push({
image,
name,
url,
job,
knownFor,
knownForLink,
about,
});
});
}
return { title, meta, description, pagination, data };
} catch (err: any) {
if (err instanceof AxiosError && err.response?.status === 404)
throw new AppError('not found', 404, err.cause);
if (err instanceof AppError) throw err;
throw new AppError('something went wrong', 500, err.cause);
}
};
export default list;
const clean = <T extends cheerio.Cheerio<any>>(item: T) => item.text().trim();

View File

@ -66,6 +66,9 @@ export const modifyIMDbImg = (url: string, widthInPx = 600) => {
return url;
};
const placeholderImageRegex = /https:\/\/m\.media-amazon.com\/images\/.{1}\/sash\/.*/;
export const isIMDbImgPlaceholder = (url?: string | null) => url ? placeholderImageRegex.test(url) : false;
export const getProxiedIMDbImgUrl = (url: string) => {
return `/api/media_proxy?url=${encodeURIComponent(url)}`;
};