feat(lib): add searchPreview method and transformer

This commit is contained in:
hansputera
2021-11-06 11:59:47 +07:00
parent 75db35767d
commit 100606d013
6 changed files with 263 additions and 13 deletions

View File

@@ -8,22 +8,23 @@ const SearchType = ['trend', 'videos'];
export default async (req: VercelRequest, res: VercelResponse) => {
try {
ow(req.query, ow.object.exactShape({
q: ow.string.minLength(5).maxLength(50),
t: ow.optional.string.validate((v) => ({
q: ow.string.minLength(3).maxLength(50),
t: ow.string.validate((v) => ({
validator: typeof v === 'string' && SearchType.includes(v.toLowerCase()),
message: 'Expected \'t\' is \'trend\' or \'videos\''
}))
}));
const t = req.query.t ? '' : req.query.t?.toLowerCase();
if (!t?.length || SearchType.indexOf(t) < 0) return res.status(400).json({ 'error': 'Invalid t' });
switch(t) {
switch(req.query.t) {
case SearchType[0]:
const result = await tiktok.searchPreview(req.query.q);
return res.json({ error: null, ...result });
const preview = await tiktok.searchPreview(req.query.q);
return res.json({ error: null, ...preview });
case SearchType[1]:
const full = await tiktok.searchFull(req.query.q);
return res.json({ error: null, ...full });
default:
return res.json({ error: 'Invalid t' });
return res.json({ error: 'Invalid \'t\'' });
}
} catch (e) {
res.status(400).json({

View File

@@ -1,6 +1,8 @@
import { fetch } from '.';
import type { SearchPreviewTypeResult } from '../types';
import { fetch, TFetch } from '.';
import { ItemEnums, SearchFullResult, SearchPreviewTypeResult } from '../types';
import { tiktokBase } from './config';
import { Transformer } from './transformer';
class TikTok {
async searchPreview(query: string) {
@@ -16,6 +18,31 @@ class TikTok {
}
}
async searchFull(query: string) {
const response = await TFetch('./api/search/general/full/' + this.buildParam(query));
const d = JSON.parse(response.body) as SearchFullResult;
const userIndex = d.data.findIndex(x => x.user_list);
if (userIndex >= 0) {
d.data[userIndex].user_list?.forEach(uList => {
(d.data as unknown[]).push({
type: ItemEnums.User,
...Transformer.transformUser(uList),
});
});
delete d.data[userIndex]; // remove user list item
}
return {
'q': encodeURIComponent(query),
'data': d.data.map(x => {
if (x.item) return { ...Transformer.transformVideo(x.item), type: ItemEnums.Video };
else return x;
})
}
}
private buildParam(q: string, region = 'ID'): string {
return `?aid=${Math.floor(Math.random() * 5000)}&app_language=en&app_name=tiktok_web&browser_language=en-US&browser_name=Mozilla&browser_online=true&browser_platform=Linux x86_64&browser_version=5.0 (X11)&channel=tiktok_web&cookie_enabled=true&device_id=7015806844518483457&device_platform=web_pc&focus_state=true&from_page=search&history_len=5&is_fullscreen=false&is_page_visible=true&keyword=${encodeURIComponent(q)}&os=linux&priority_region=&referer=&region=${region.toUpperCase()}&screen_height=768&screen_width=1364&tz_name=Asia/Jakarta`;
}

89
lib/transformer.ts Normal file
View File

@@ -0,0 +1,89 @@
import { UserResult, VideoItemResult } from '../types';
export class Transformer {
static transformUser(u: UserResult) {
return {
'id': u.user_info.uid,
'username': u.user_info.unique_id,
'avatar': {
'variants': u.user_info.avatar_thumb.url_list,
'id': u.user_info.avatar_thumb.uri,
'properties': {
'width': u.user_info.avatar_thumb.width,
'height': u.user_info.avatar_thumb.height
}
},
'bio': u.user_info.signature,
'nick': u.user_info.nickname,
'verified': Boolean(u.user_info.custom_verify),
'followers': u.user_info.follower_count,
'custom_verify': u.user_info.custom_verify
};
};
static transformAuthor(a: VideoItemResult['author']) {
return {
'id': a.id,
'username': a.uniqueId,
'isPrivate': a.privateAccount,
'nick': a.nickname,
'avatar': {
'thumbnail': a.avatarThumb,
'medium': a.avatarMedium,
'large': a.avatarLarge,
},
'bio': a.signature,
'verified': a.verified
}
};
static transformVideo(v: VideoItemResult) {
return {
'id': v.id,
'createdAt': {
'iso': new Date(v.createTime),
'date': v.createTime
},
'description': v.desc,
'properties': {
'video': {
'streamUrl': v.video.playAddr,
'downloadUrl': v.video.downloadAddr,
'quality': v.video.videoQuality,
'duration': v.video.duration,
'cover': {
'photo': v.video.originCover,
'animated': v.video.dynamicCover,
'share': v.video.shareCover.filter(s => s.length),
'reflow': v.video.reflowCover,
},
'bitrate': v.video.bitrate,
'format': v.video.format,
'height': v.video.height,
'width': v.video.width,
},
'audio': {
'id': v.music.id,
'title': v.music.title,
'duration': v.music.duration,
'sourceUrl': v.music.playUrl,
'cover': {
'thumbnail': v.music.coverThumb,
'medium': v.music.coverMedium,
'large': v.music.coverLarge,
},
'author': v.music.authorName,
'isOriginal': v.music.original,
'album': v.music.album.length ? v.music.album : '-',
},
'author': {
...this.transformAuthor(v.author),
'stats': v.authorStats,
},
'stats': v.stats,
'stickers': v.stickersOnItem,
'extra': v.textExtra,
}
};
}
}

4
types/enums.ts Normal file
View File

@@ -0,0 +1,4 @@
export enum ItemEnums {
User = 1,
Video = 2
}

View File

@@ -1 +1,2 @@
export * from './search';
export * from './search';
export * from './enums';

View File

@@ -29,4 +29,132 @@ export interface SearchPreviewTypeResult {
sug_list: SearchPreviewSug[];
// other record ignored because i think that isn't important
}
/** END SEARCH PREVIEW */
/** END SEARCH PREVIEW */
/** SEARCH FULL */
export interface UserResult {
user_info: {
uid: string;
nickname: string;
signature: string;
avatar_thumb: {
uri: string;
url_list: string[];
width: number;
height: number;
};
follow_status: number;
follower_count: number;
custom_verify?: string;
unique_id?: string;
room_id: number;
enterprise_verify_reason?: string;
cover_url?: string;
sec_uid: string;
}
}
interface Video {
id: string;
height: number;
width: number;
duration: number;
ratio: string;
cover: string;
originCover: string;
dynamicCover: string;
playAddr: string;
downloadAddr: string;
shareCover: string[];
reflowCover: string;
bitrate: number;
encodedType: string;
format: string;
videoQuality: string;
encodeUserTag: string;
};
interface Author {
id: string;
uniqueId: string;
nickname: string;
avatarThumb: string;
avatarMedium: string;
avatarLarge: string;
signature: string;
verified: boolean;
secUid: string;
secret: boolean;
ftc: boolean;
relation: number;
openFavorite: boolean;
commentSetting: number;
duetSetting: number;
stitchSetting: number;
privateAccount: boolean;
};
interface Music {
id: string;
title: string;
playUrl: string;
coverThumb: string;
coverMedium: string;
coverLarge: string;
authorName: string;
original: boolean;
duration: number;
album: string;
};
interface Stats {
diggCount: number;
shareCount: number;
commentCount: number;
playCount: number;
};
interface TextExtra {
awemeid: string;
start: number;
end: number;
hashtagName: string;
hastagId: string;
type: number;
userId: string;
isCommerce: boolean;
userUniqueId: string;
secUid: string;
};
interface StickerItem {
stickerType: number;
stickerText: string[];
};
export interface VideoItemResult {
id: string;
desc: string;
createTime: number;
video: Video;
author: Author;
music: Music;
stats: Stats;
authorStats: Omit<Stats, 'playCount' | 'commentCount' | 'shareCount'> & {
followingCount: number;
followerCount: number;
heartCount: number;
videoCount: number;
};
textExtra: TextExtra[];
stickersOnItem: StickerItem[];
}
export interface SearchFullResult {
data: {
type: number;
user_list?: UserResult[];
item?: VideoItemResult;
}[];
}
/** END SEARCH FULL */