Skip to content

Commit

Permalink
feat: switch to web scraping + fall back on tikwm for media
Browse files Browse the repository at this point in the history
  • Loading branch information
okdargy committed May 25, 2024
1 parent a6f715a commit 961aad1
Show file tree
Hide file tree
Showing 12 changed files with 3,329 additions and 116 deletions.
Binary file added bun.lockb
Binary file not shown.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
"name": "fxtiktok",
"version": "1.0.0",
"scripts": {
"dev": "wrangler dev src/index.ts",
"dev": "wrangler dev src/index.ts --env=local",
"build": "wrangler build src/index.ts",
"deploy": "wrangler deploy --minify src/index.ts"
},
"dependencies": {
"hono": "^3.11.7"
"@types/set-cookie-parser": "^2.4.7",
"hono": "^3.11.7",
"set-cookie-parser": "^2.6.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230914.0",
Expand Down
75 changes: 47 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { Hono } from 'hono'
import { cache } from 'hono/cache'

import { grabAwemeId, getVideoInfo } from './services/tiktok'
import { scrapeVideoData } from './services/tiktok'
import { grabAwemeId } from './services/tiktok'
import { VideoResponse, ErrorResponse } from './templates'
import generateAlternate from './util/generateAlternate'
import { returnHTMLResponse } from './util/ResponseHelper'
import { returnHTMLResponse } from './util/responseHelper'

import { ItemStruct } from './types/Web'

const app = new Hono()

app.get('/test/:videoId', async (c) => {
const { videoId } = c.req.param()
const awemeId = await getVideoInfo(videoId)
const awemeId = await scrapeVideoData(videoId)

if(awemeId instanceof Error) {
return new Response((awemeId as Error).message, { status: 500 })
Expand Down Expand Up @@ -62,28 +65,27 @@ async function handleVideo(c: any): Promise<Response> {
}

try {
const videoInfo = await getVideoInfo(id)
const videoInfo = await scrapeVideoData(id)

if (videoInfo instanceof Error) {
const responseContent = await ErrorResponse((videoInfo as Error).message);
return returnHTMLResponse(responseContent, 201);
}

const url = new URL(c.req.url);

if(url.hostname.includes('d.tnktok.com') || c.req.query('isDirect') === 'true') {
if(videoInfo.video.duration > 0) {
return new Response('', {
status: 302,
headers: {
'Location': 'https://fxtiktok-rewrite.dargy.workers.dev/generate/video/' + videoInfo.aweme_id
'Location': 'https://fxtiktok-rewrite.dargy.workers.dev/generate/video/' + videoInfo.id
}
})
} else {
return new Response('', {
status: 302,
headers: {
'Location': 'https://fxtiktok-rewrite.dargy.workers.dev/generate/image/' + videoInfo.aweme_id
'Location': 'https://fxtiktok-rewrite.dargy.workers.dev/generate/image/' + videoInfo.id
}
})
}
Expand All @@ -92,6 +94,7 @@ async function handleVideo(c: any): Promise<Response> {
return returnHTMLResponse(responseContent);
}
} catch(e) {
console.log(e);
const responseContent = await ErrorResponse((e as Error).message);
return returnHTMLResponse(responseContent, 201);
}
Expand All @@ -118,20 +121,26 @@ app.get(

app.get('/generate/video/:videoId', async (c) => {
const { videoId } = c.req.param()
const data = await getVideoInfo(videoId);

if (data instanceof Error) {
return new Response((data as Error).message, { status: 500,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
try {
/*
const data = await scrapeVideoData(videoId);
if (!(data instanceof Error)) {
if(data.video.playAddr) {
return c.redirect(data.video.playAddr)
} else {
return new Response('No video found', { status: 404,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
}
})
}
})
}

if(data.video.play_addr.url_list.length > 0) {
return c.redirect(data.video.play_addr.url_list[0])
} else {
return new Response('No video found', { status: 404,
}
*/
return c.redirect(`https://tikwm.com/video/media/play/${videoId}.mp4`);
} catch(e) {
return new Response((e as Error).message, { status: 500,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
}
Expand All @@ -141,16 +150,26 @@ app.get('/generate/video/:videoId', async (c) => {

app.get('/generate/image/:videoId', async (c) => {
const { videoId } = c.req.param()
const data = await getVideoInfo(videoId);

if (data instanceof Error) {
return new Response((data as Error).message, { status: 500 })
}

if(data.video.cover.url_list.length > 0) {
return c.redirect(data.video.cover.url_list[0])
} else {
return new Response(JSON.stringify(data), { status: 200 })
try {
/*
const data = await scrapeVideoData(videoId);
if (!(data instanceof Error)) {
if(data.imagePost.images.length > 0) {
return c.redirect(data.imagePost.images[0].imageURL.urlList[0])
} else {
return new Response(JSON.stringify(data), { status: 200 })
}
}
*/
return c.redirect(`https://tikwm.com/video/cover/${videoId}.webp`);
} catch(e) {
return new Response((e as Error).message, { status: 500,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
}
})
}
})

Expand Down
119 changes: 60 additions & 59 deletions src/services/tiktok.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,64 @@
import { TikTokAPIResponse, AwemeList } from "../types/Services";
import { WebJSONResponse, ItemStruct } from "../types/Web";
import Cookie from "../util/cookieHelper";
import cookieParser from "set-cookie-parser";

const cookie = new Cookie([]);

export async function grabAwemeId(videoId: string): Promise<String | Error> {
// https://vm.tiktok.com/ZMJmVWVpL/
const res = await fetch("https://vm.tiktok.com/" + videoId);
const url = new URL(res.url);

const awemeIdPattern = /\/@[\w\d_.]+\/(video|photo)\/(\d{19})/;
const match = url.pathname.match(awemeIdPattern);

if (match) {
return match[2];
} else {
throw new Error("Could not find awemeId");
}
}
// https://vm.tiktok.com/ZMJmVWVpL/
const res = await fetch("https://vm.tiktok.com/" + videoId);
const url = new URL(res.url);

const awemeIdPattern = /\/@[\w\d_.]+\/(video|photo)\/(\d{19})/;
const match = url.pathname.match(awemeIdPattern);

export async function getVideoInfo(
awemeId: String,
): Promise<AwemeList | Error> {
const apiUrl = new URL(
"https://api22-normal-c-alisg.tiktokv.com/aweme/v1/feed/?region=US&carrier_region=US",
);

const params = {
aweme_id: awemeId,
iid: "7318518857994389254",
device_id: "7318517321748022790",
channel: "googleplay",
app_name: "musical_ly",
version_code: "300904",
device_platform: "android",
device_type: "ASUS_Z01QD",
os_version: "9",
};

Object.keys(params).forEach((key) =>
apiUrl.searchParams.append(key, params[key]),
);

console.log(apiUrl.toString());

const res: Response = await fetch(apiUrl.toString(), {
headers: {
"User-Agent":
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36",
},
cf: {
cacheEverything: true,
cacheTtlByStatus: { "200-299": 86400, 404: 1, "500-599": 0 },
},
});
const json: TikTokAPIResponse = await res.json();
const videoInfo: AwemeList | undefined = json.aweme_list.find(
(aweme) => aweme.aweme_id === awemeId,
);

if (videoInfo) {
return videoInfo;
} else {
return new Error("Could not find video info");
}
if (match) {
return match[2];
} else {
throw new Error("Could not find awemeId");
}
}

export async function scrapeVideoData(
awemeId: string,
author?: string
): Promise<ItemStruct | Error> {
console.log('before', cookie.getUpdatingCookies());
const res = await fetch(`https://www.tiktok.com/@${author || "i"}"/video/${awemeId}`, {
method: "GET",
headers: {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
"Cookie": cookie.getCookiesAsString(),
},
cf: {
cacheEverything: true,
cacheTtlByStatus: { "200-299": 86400, 404: 1, "500-599": 0 },
},
});

console.log('string', cookie.getCookiesAsString());
console.log(res.headers)
let cookies = cookieParser(res.headers.get("set-cookie")!);
cookie.setCookies(cookies);

const html = await res.text();

try {
const resJson = html.split('<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">')[1].split('</script>')[0]
const json: WebJSONResponse = JSON.parse(resJson);

//console.log(Object.keys(json["__DEFAULT_SCOPE__"]));
if(!json["__DEFAULT_SCOPE__"]["webapp.video-detail"] || json["__DEFAULT_SCOPE__"]["webapp.video-detail"].statusCode == 10204) throw new Error("Could not find video data");
const videoInfo = json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"];
//console.log(videoInfo)

return videoInfo
} catch(err) {
console.log(err);
throw new Error("Could not parse video info");
}



}
44 changes: 44 additions & 0 deletions src/services/tiktokv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { TikTokAPIResponse, AwemeList } from "../types/API";

export async function getVideoInfo(
awemeId: string,
): Promise<AwemeList | Error> {
const apiUrl = new URL(
"https://api22-normal-c-alisg.tiktokv.com/aweme/v1/feed/",
);

apiUrl.search = new URLSearchParams({
region: "US",
carrier_region: "US",
aweme_id: awemeId,
iid: "7318518857994389254",
device_id: "7318517321748022790",
channel: "googleplay",
app_name: "musical_ly",
version_code: "300904",
device_platform: "android",
device_type: "ASUS_Z01QD",
os_version: "9",
}).toString();

const res: Response = await fetch(apiUrl.toString(), {
headers: {
"User-Agent":
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36",
},
cf: {
cacheEverything: true,
cacheTtlByStatus: { "200-299": 86400, 404: 1, "500-599": 0 },
},
});
const json: TikTokAPIResponse = await res.json();
const videoInfo: AwemeList | undefined = json.aweme_list.find(
(aweme) => aweme.aweme_id === awemeId,
);

if (videoInfo) {
return videoInfo;
} else {
return new Error("Could not find video info");
}
}
4 changes: 2 additions & 2 deletions src/templates/pages/Error.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AwemeList } from '../../types/Services';
import MetaHelper from '../../util/MetaHelper';
import { AwemeList } from '../../types/API';
import MetaHelper from '../../util/metaHelper';

export function ErrorResponse(error: string): JSX.Element {
return (
Expand Down
Loading

1 comment on commit 961aad1

@okdargy
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks to all the people on #7 for opening an issue about the api change

Please sign in to comment.