feat(search): add basic search functionality

this commit adds basic search feature.

fix: https://codeberg.org/zyachel/libremdb/issues/9, https://github.com/zyachel/libremdb/issues/10
This commit is contained in:
zyachel
2022-12-31 22:21:36 +05:30
parent 81eaf2fd5e
commit 0cff34a766
25 changed files with 1191 additions and 60 deletions

View File

@ -0,0 +1,71 @@
import RawFind from '../../interfaces/misc/rawFind';
const formatSAndE = (
season: string | undefined,
episode: string | undefined
) => {
if (season && season !== 'Unknown' && episode && episode !== 'Unknown')
return `S${season} E${episode}`;
return null;
};
const cleanFind = (rawFind: RawFind) => {
const {
props: { pageProps: d },
} = rawFind;
const cleanData = {
meta: {
exact: d.findPageMeta.isExactMatch,
type: d.findPageMeta.searchType || null,
titleType: d.findPageMeta.titleSearchType?.[0] || null,
},
people: d.nameResults.results.map(person => ({
id: person.id,
name: person.displayNameText,
aka: person.akaName || null,
jobCateogry: person.knownForJobCategory || null,
knownForTitle: person.knownForTitleText || null,
knownInYear: person.knownForTitleYear || null,
...(person.avatarImageModel && {
image: {
url: person.avatarImageModel.url,
caption: person.avatarImageModel.caption,
},
}),
})),
titles: d.titleResults.results.map(title => ({
id: title.id,
name: title.titleNameText,
type: title.titleTypeText,
releaseYear: title.titleReleaseText || null,
credits: title.topCredits,
...(title.titlePosterImageModel && {
image: {
url: title.titlePosterImageModel.url,
caption: title.titlePosterImageModel.caption,
},
}),
seriesId: title.seriesId || null,
seriesName: title.seriesNameText || null,
seriesType: title.seriesTypeText || null,
seriesReleaseYear: title.seriesReleaseText || null,
sAndE: formatSAndE(title.seriesSeasonText, title.seriesEpisodeText),
})),
companies: d.companyResults.results.map(company => ({
id: company.id,
name: company.companyName,
type: company.typeText,
country: company.countryText,
})),
keywords: d.keywordResults.results.map(keyword => ({
id: keyword.id,
text: keyword.keywordText,
numTitles: keyword.numTitles,
})),
};
return cleanData;
};
export default cleanFind;

View File

@ -0,0 +1,36 @@
/**
* @constant
*
* key: the key for the query that we make to fetch results
*
* name: Nice name to display on the client side
*
* val: the value that is associated with the key. also used to fetch results.
*
* **IMPORTANT**: see sample response from backend, and form submission url to better understand how these objects are used.
*/
export const resultTypes = {
types: [
{ name: 'Titles', val: 'tt', id: 'TITLE' },
{ name: 'People', val: 'nm', id: 'NAME' },
{ name: 'Companies', val: 'co', id: 'COMPANY' },
{ name: 'Keywords', val: 'kw', id: 'KEYWORD' },
],
key: 's',
} as const;
/**
* same as {@link resultTypes}.
*/
export const resultTitleTypes = {
types: [
{ name: 'Movies', val: 'ft', id: 'MOVIE' },
{ name: 'TV', val: 'tv', id: 'TV' },
{ name: 'TV Episodes', val: 'ep', id: 'TV_EPISODE' },
{ name: 'Music Videos', val: 'mu', id: 'MUSIC_VIDEO' },
{ name: 'Podcasts', val: 'ps', id: 'PODCAST_SERIES' },
{ name: 'Podcast Episodes', val: 'pe', id: 'PODCAST_EPISODE' },
{ name: 'Video Games', val: 'vg', id: 'VIDEO_GAME' },
],
key: 'ttype',
} as const;

View File

@ -0,0 +1,27 @@
// external deps
import * as cheerio from 'cheerio';
// local files
import axiosInstance from '../axiosInstance';
import { AppError } from '../helpers';
import RawFind from '../../interfaces/misc/rawFind';
import cleanFind from '../cleaners/find';
const basicSearch = async (queryStr: string) => {
try {
const res = await axiosInstance(`/find?${queryStr}`);
const $ = cheerio.load(res.data);
const rawData = $('script#__NEXT_DATA__').text();
const parsedRawData: RawFind = JSON.parse(rawData);
const cleanData = cleanFind(parsedRawData);
return cleanData;
} catch (err: any) {
if (err.response?.status === 404)
throw new AppError('not found', 404, err.cause);
throw new AppError('something went wrong', 500, err.cause);
}
};
export default basicSearch;

View File

@ -1,3 +1,9 @@
import {
ResultMetaTitleTypes,
ResultMetaTypes,
} from '../interfaces/shared/search';
import { resultTitleTypes } from './constants/find';
export const formatTime = (timeInSecs: number) => {
if (!timeInSecs) return;
// year, month, date, hours, minutes, seconds
@ -50,8 +56,14 @@ export const formatMoney = (num: number, cur: string) => {
}).format(num);
};
const imageRegex = /https:\/\/m\.media-amazon\.com\/images\/M\/[^.]*/;
export const modifyIMDbImg = (url: string, widthInPx = 600) => {
return url.replace(/\.jpg/g, `UX${widthInPx}.jpg`);
// as match returns either array or null, returning array in case it returns null. and destructuring it right away.
const [cleanImg] = url.match(imageRegex) || [];
if (cleanImg) return `${cleanImg}.UX${widthInPx}.jpg`;
return url;
};
export const getProxiedIMDbImgUrl = (url: string) => {
@ -65,3 +77,29 @@ export const AppError = class extends Error {
Error.captureStackTrace(this, AppError);
}
};
export const cleanQueryStr = (
entries: [string, string][],
filterable = ['q', 's', 'exact', 'ttype']
) => {
let queryStr = '';
entries.forEach(([key, val], i) => {
if (!val || !filterable.includes(key)) return;
queryStr += `${i > 0 ? '&' : ''}${key}=${val.trim()}`;
});
return queryStr;
};
export const getResTitleTypeHeading = (
type: ResultMetaTypes,
titleType: ResultMetaTitleTypes
) => {
if (type !== 'TITLE') return 'Titles';
for (let i = 0; i < resultTitleTypes.types.length; i++) {
const el = resultTitleTypes.types[i];
if (el.id === titleType) return el.name;
}
};