diff --git a/src/api/index.ts b/src/api/index.ts index 1aa2988..71baa54 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,2 +1,2 @@ export const _tiktokurl: string = "https://www.tiktok.com" -export const _tiktokapi = (id: string): string => `https://api16-core.tiktokv.com/aweme/v1/feed/?aweme_id=${id}` +export const _tiktokapi = (id: string): string => `https://api.tiktokv.com/aweme/v1/feed/?aweme_id=${id}` diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 9097ddc..0000000 --- a/src/types/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./downloader" -export * from "./stalker" diff --git a/src/types/stalker.ts b/src/types/stalker.ts index 6c27bcf..526695a 100644 --- a/src/types/stalker.ts +++ b/src/types/stalker.ts @@ -4,6 +4,7 @@ export interface StalkResult { result?: { users: Users stats: Stats + posts: Posts[] } } @@ -15,6 +16,7 @@ export interface Users { avatarMedium: string signature: string verified: boolean + privateAccount: boolean region: string commerceUser: boolean usernameModifyTime: number @@ -28,4 +30,50 @@ export interface Stats { videoCount: number likeCount: number friendCount: number + postCount: number +} + +export interface Posts { + id: string + desc: string + createTime: number + author: string + locationCreated: string + hashtags: string[] + statistics: Statistics + video: Video + music: Music +} + +export interface Statistics { + likeCount: number + shareCount: number + commentCount: number + playCount: number + favoriteCount: number +} + +export interface Video { + id: string + duration: string + ratio: string + cover: string + originCover: string + dynamicCover: string + playAddr: string + downloadAddr: string + format: string + bitrate: number +} + +export interface Music { + id: string + title: string + album: string + playUrl: string + coverLarge: string + coverMedium: string + coverThumb: string + authorName: string + duration: string } diff --git a/src/types/downloader.ts b/src/types/tiktokapi.ts similarity index 100% rename from src/types/downloader.ts rename to src/types/tiktokapi.ts diff --git a/src/types/tiktokdownload.ts b/src/types/tiktokdownload.ts new file mode 100644 index 0000000..9a75154 --- /dev/null +++ b/src/types/tiktokdownload.ts @@ -0,0 +1,30 @@ +export interface TiktokFetchTT { + status: "success" | "error" + message?: string + result?: string +} + +export interface TiktokDownload { + status: "success" | "error" + message?: string + result?: { + type: "image" | "video" + desc: string + author: Author + statistics: Statistics + images?: string[] + video?: string + music: string + } +} + +export interface Author { + avatar: string + nickname: string +} + +export interface Statistics { + likeCount: string + commentCount: string + shareCount: string +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 9097ddc..c1740ab 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,2 @@ -export * from "./downloader" +export * from "./switch" export * from "./stalker" diff --git a/src/utils/stalker.ts b/src/utils/stalker.ts index eaee011..fdbd955 100644 --- a/src/utils/stalker.ts +++ b/src/utils/stalker.ts @@ -1,7 +1,7 @@ import axios from "axios" import { load } from "cheerio" import { _tiktokurl } from "../api" -import { StalkResult, Stats, Users } from "../types" +import { Music, Posts, StalkResult, Statistics, Stats, Users, Video } from "../types/stalker" const getCookie = () => new Promise((resolve, reject) => { @@ -33,6 +33,9 @@ export const TiktokStalk = (username: string, options: { cookie: string }): Prom }) } const user = result.UserModule + const itemKeys = Object.keys(result.ItemModule) + + // User Info Result const users: Users = { username: user.users[username].uniqueId, nickname: user.users[username].nickname, @@ -41,24 +44,92 @@ export const TiktokStalk = (username: string, options: { cookie: string }): Prom avatarMedium: user.users[username].avatarMedium, signature: user.users[username].signature, verified: user.users[username].verified, + privateAccount: user.users[username].privateAccount, region: user.users[username].region, commerceUser: user.users[username].commerceUserInfo.commerceUser, usernameModifyTime: user.users[username].uniqueIdModifyTime, nicknameModifyTime: user.users[username].nickNameModifyTime } + + // Statistics Result const stats: Stats = { followerCount: user.stats[username].followerCount, followingCount: user.stats[username].followingCount, heartCount: user.stats[username].heartCount, videoCount: user.stats[username].videoCount, likeCount: user.stats[username].diggCount, - friendCount: user.stats[username].friendCount + friendCount: user.stats[username].friendCount, + postCount: itemKeys.length } + + // Posts Result + const posts: Posts[] = [] + itemKeys.forEach((key) => { + const post = result.ItemModule[key] + let media + if (post.imagePost) { + // Images or Slide Posts Result + media = { + images: post.imagePost.images.map((v: any) => v.imageURL.urlList[0]) + } + } else { + // Video Posts Result + media = { + video: { + id: post.video.id, + duration: post.video.duration, + ratio: post.video.ratio, + cover: post.video.cover, + originCover: post.video.originCover, + dynamicCover: post.video.dynamicCover, + playAddr: post.video.playAddr, + downloadAddr: post.video.downloadAddr, + format: post.video.format, + bitrate: post.video.bitrate + } as Video + } + } + + // Music Posts Result + const music: Music = { + id: post.music.id, + title: post.music.title, + authorName: post.music.authorName, + album: post.music.album, + coverLarge: post.music.coverLarge, + coverMedium: post.music.coverMedium, + coverThumb: post.music.coverThumb, + playUrl: post.music.playUrl, + duration: post.music.duration + } + + // Statistics Posts Result + const statistics: Statistics = { + likeCount: post.stats.diggCount, + shareCount: post.stats.shareCount, + commentCount: post.stats.commentCount, + playCount: post.stats.playCount, + favoriteCount: post.stats.collectCount + } + + posts.push({ + id: post.id, + desc: post.desc, + createTime: post.createTime, + author: post.author, + locationCreated: post.locationCreated, + hashtags: post.challenges.map((v: any) => v.title), + statistics, + music, + ...media + }) + }) resolve({ status: "success", result: { users, - stats + stats, + posts } }) }) diff --git a/src/utils/switch.ts b/src/utils/switch.ts new file mode 100644 index 0000000..740f8e5 --- /dev/null +++ b/src/utils/switch.ts @@ -0,0 +1,17 @@ +import { TiktokDownload } from "./tiktokdownload" +import { TiktokAPI } from "./tiktokapi" + +export const TiktokDL = (url: string, options: { version: "v1" | "v2" }) => + new Promise(async (resolve, reject) => { + switch (options.version) { + case "v1": { + await TiktokAPI(url).then(resolve).catch(reject) + } + case "v2": { + await TiktokDownload(url).then(resolve).catch(reject) + } + default: { + await TiktokAPI(url).then(resolve).catch(reject) + } + } + }) diff --git a/src/utils/downloader.ts b/src/utils/tiktokapi.ts similarity index 97% rename from src/utils/downloader.ts rename to src/utils/tiktokapi.ts index 2f0361f..e5cf8bb 100644 --- a/src/utils/downloader.ts +++ b/src/utils/tiktokapi.ts @@ -1,6 +1,6 @@ import axios from "axios" import { _tiktokapi, _tiktokurl } from "../api" -import { Author, DLResult, Statistics, Music } from "../types" +import { Author, DLResult, Statistics, Music } from "../types/tiktokapi" const toMinute = (duration) => { const mins = ~~((duration % 3600) / 60) @@ -14,7 +14,7 @@ const toMinute = (duration) => { return ret } -export const TiktokDL = (url: string): Promise => +export const TiktokAPI = (url: string): Promise => new Promise((resolve, reject) => { url = url.replace("https://vm", "https://vt") axios diff --git a/src/utils/tiktokdownload.ts b/src/utils/tiktokdownload.ts new file mode 100644 index 0000000..f3a322b --- /dev/null +++ b/src/utils/tiktokdownload.ts @@ -0,0 +1,91 @@ +import Axios from "axios" +import { load } from "cheerio" +import { Author, Statistics, TiktokFetchTT } from "../types/tiktokdownload" + +const fetchTT = (): Promise => + new Promise(async (resolve, reject) => { + Axios.get("https://tiktokdownload.online/", { + headers: { + "user-agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0" + } + }) + .then(({ data }) => { + const regex = /form\.setAttribute\("include-vals",\s*"([^"]+)"\)/ + const match = data.match(regex) + if (match) { + const includeValsValue = match[1] + resolve({ status: "success", result: includeValsValue }) + } else { + resolve({ status: "error", message: "Not found" }) + } + }) + .catch((e) => resolve({ status: "error", message: e.message })) + }) + +export const TiktokDownload = (url: string) => + new Promise(async (resolve, reject) => { + const tt: TiktokFetchTT = await fetchTT() + if (tt.status !== "success") return resolve(tt) + Axios("https://tiktokdownload.online/abc?url=dl", { + method: "POST", + data: new URLSearchParams( + Object.entries({ + id: url, + locale: "en", + tt: tt.result + }) + ) + }) + .then(({ data }) => { + const $ = load(data) + + // Result + const desc = $("p.maintext").text().trim() + const author: Author = { + avatar: $("img.result_author").attr("src"), + nickname: $("h2").text().trim() + } + const statistics: Statistics = { + likeCount: $("#trending-actions > .justify-content-start").text().trim(), + commentCount: $("#trending-actions > .justify-content-center").text().trim(), + shareCount: $("#trending-actions > .justify-content-end").text().trim() + } + + // Images / Slide Result + const images: string[] = [] + $("ul.splide__list > li") + .get() + .map((img) => { + images.push($(img).find("img").attr("src")) + }) + + if (images.length !== 0) { + // Images / Slide Result + resolve({ + status: "success", + result: { + type: "image", + desc, + author, + statistics, + images, + music: $("a.music").attr("href") + } + }) + } else { + // Video Result + resolve({ + status: "success", + result: { + type: "video", + desc, + author, + statistics, + video: $("a.without_watermark").attr("href"), + music: $("a.music").attr("href") + } + }) + } + }) + .catch(console.log) + })