Skip to content

Commit

Permalink
feat: add live support
Browse files Browse the repository at this point in the history
  • Loading branch information
okdargy committed Jun 8, 2024
1 parent f73dd28 commit c516af8
Show file tree
Hide file tree
Showing 11 changed files with 376 additions and 52 deletions.
126 changes: 109 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Hono } from 'hono'
import { cache } from 'hono/cache'

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

const app = new Hono()
const awemeIdPattern = /^\d{1,19}$/
const awemeLinkPattern = /\/@([\w\d_.]+)\/(video|photo|live)\/?(\d{19})?/

const BOT_REGEX =
/bot|facebook|embed|got|firefox\/92|curl|wget|go-http|yahoo|generator|whatsapp|discord|preview|link|proxy|vkshare|images|analyzer|index|crawl|spider|python|cfnetwork|node/gi

Expand Down Expand Up @@ -43,30 +45,68 @@ app.get('/', (c) => {
})
})

async function handleVideo(c: any): Promise<Response> {
async function handleShort(c: any): Promise<Response> {
const { videoId } = c.req.param()
let id = videoId.split('.')[0]
let id = videoId.split('.')[0] // for .mp4, .webp, etc.

// First, we need to find where the vm link goes to
const res = await fetch('https://vm.tiktok.com/' + videoId)
const link = new URL(res.url)

// Clean any tracking parameters
link.search = ''

// If the user agent is a bot, redirect to the TikTok page
if (!BOT_REGEX.test(c.req.header('User-Agent') || '')) {
return new Response('', {
status: 302,
headers: {
Location: 'https://www.tiktok.com' + `${awemeIdPattern.test(id) ? c.req.path : '/t/' + id}`
Location: 'https://www.tiktok.com' + link.pathname
}
})
}

// If the videoId doesn't match the awemeIdPattern, that means we have shortened TikTok link and we need to grab the awemeId
if (!awemeIdPattern.test(id)) {
try {
const awemeId = await grabAwemeId(id)
id = awemeId
} catch (e) {
const responseContent = await ErrorResponse((e as Error).message)
return returnHTMLResponse(responseContent, 500)
}
// Now, we need to check if the video is a livestream or a photo/video
if (link.pathname.includes('/video') || link.pathname.includes('/photo')) {
return handleVideo(c)
} else if (link.pathname.includes('/live')) {
return handleLive(c)
} else {
const responseContent = await ErrorResponse('Invalid vm link')
return returnHTMLResponse(responseContent, 400)
}
}

async function handleVideo(c: any): Promise<Response> {
const { videoId } = c.req.param()
let id = videoId.split('.')[0] // for .mp4, .webp, etc.

// If the user agent is a bot, redirect to the TikTok page
if (!BOT_REGEX.test(c.req.header('User-Agent') || '')) {
const url = new URL(c.req.url)

// Remove tracking parameters
url.search = ''

return new Response('', {
status: 302,
headers: {
Location: 'https://www.tiktok.com' + url.pathname
}
})
}

if(!awemeIdPattern.test(id)) {
const url = await grabAwemeId(id)
const match = url.pathname.match(awemeLinkPattern)

if (match) {
id = match[3]
} else {
const responseContent = await ErrorResponse('Invalid video ID')
return returnHTMLResponse(responseContent, 400)
}
}

try {
const videoInfo = await scrapeVideoData(id)
Expand Down Expand Up @@ -108,6 +148,54 @@ async function handleVideo(c: any): Promise<Response> {
return returnHTMLResponse(responseContent, 500)
}
}
async function handleLive(c: any): Promise<Response> {
const { author, videoId } = c.req.param()
let authorName = author;

// If the user agent is a bot, redirect to the TikTok page
if (!BOT_REGEX.test(c.req.header('User-Agent') || '')) {
const url = new URL(c.req.url)

// Remove tracking parameters
url.search = ''

return new Response('', {
status: 302,
headers: {
Location: 'https://www.tiktok.com' + url.pathname
}
})
}

if(!author && !awemeIdPattern.test(videoId)) {
const url = await grabAwemeId(videoId)
const match = url.pathname.match(awemeLinkPattern)

if (match) {
authorName = match[1]
} else {
const responseContent = await ErrorResponse('Invalid live ID')
return returnHTMLResponse(responseContent, 400)
}
}

authorName = authorName.startsWith('@') ? authorName.substring(1) : authorName

try {
const liveData = await scrapeLiveData(authorName)

if (liveData instanceof Error) {
const responseContent = await ErrorResponse((liveData as Error).message)
return returnHTMLResponse(responseContent, 500)
}

const responseContent = await LiveResponse(liveData)
return returnHTMLResponse(responseContent)
} catch (e) {
const responseContent = await ErrorResponse((e as Error).message)
return returnHTMLResponse(responseContent, 500)
}
}

app.get('/generate/alternate', (c) => {
const content = JSON.stringify(generateAlternate(c))
Expand Down Expand Up @@ -209,7 +297,11 @@ app.get('/generate/image/:videoId/:imageCount', async (c) => {
const routes = [
{
path: '/:videoId',
handler: handleVideo
handler: handleShort
},
{
path: '/t/:videoId',
handler: handleShort
},
{
path: '/*/video/:videoId',
Expand All @@ -220,8 +312,8 @@ const routes = [
handler: handleVideo
},
{
path: '/t/:videoId',
handler: handleVideo
path: '/:author/live',
handler: handleLive
}
]

Expand Down
48 changes: 36 additions & 12 deletions src/services/tiktok.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { WebJSONResponse, ItemStruct } from '../types/Web'
import { LiveWebJSONResponse, LiveRoom } from '../types/Live'
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/
export async function grabAwemeId(videoId: string): Promise<URL> {
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')
}
return new URL(res.url)
}

export async function scrapeVideoData(awemeId: string, author?: string): Promise<ItemStruct | Error> {
Expand Down Expand Up @@ -56,3 +47,36 @@ export async function scrapeVideoData(awemeId: string, author?: string): Promise
throw new Error('Could not parse video info')
}
}

export async function scrapeLiveData(author: string): Promise<LiveRoom | Error> {
const res = await fetch(`https://www.tiktok.com/@${author}/live`, {
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 }
}
})

let cookies = cookieParser(res.headers.get('set-cookie')!)
cookie.setCookies(cookies)

const html = await res.text()

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

if (!json['LiveRoom']) throw new Error('Could not find live data')

return json['LiveRoom']
} catch (err) {
throw new Error('Could not parse live data')
}
}
1 change: 1 addition & 0 deletions src/templates/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './pages/VideoResponse'
export * from './pages/Error'
export * from './pages/LiveResponse'
2 changes: 1 addition & 1 deletion src/templates/pages/Error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function ErrorResponse(error: string): JSX.Element {
},
{
name: 'og:description',
content: 'An error occurred while trying to fetch the video. Please try again later.'
content: 'An error occurred while trying to fetch data. Please try again later.'
},
{
name: 'og:site_name',
Expand Down
47 changes: 47 additions & 0 deletions src/templates/pages/LiveResponse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import MetaHelper from '../../util/metaHelper'
import { LiveRoom } from '../../types/Live'

export function LiveResponse(data: LiveRoom): JSX.Element {
return (
<>
{MetaHelper([
{
name: 'og:title',
content: `🔴 ${data.liveRoomUserInfo.user.nickname} (@${data.liveRoomUserInfo.user.nickname})`
},
{
name: 'theme-color',
content: '#ff0050'
},
{
name: 'twitter:site',
content: `@${data.liveRoomUserInfo.user.uniqueId}` // @username
},
{
name: 'twitter:creator',
content: `@${data.liveRoomUserInfo.user.uniqueId}` // @username
},
{
name: 'twitter:title',
content: `🔴 ${data.liveRoomUserInfo.user.nickname} (@${data.liveRoomUserInfo.user.nickname})` // Nickname (@username)
},
{
name: 'og:url',
content: `https://www.tiktok.com/@${data.liveRoomUserInfo.user.uniqueId}/live`
},
{
name: 'og:description',
content: data.liveRoomUserInfo.liveRoom.title
},
{
name: 'og:image',
content: data.liveRoomUserInfo.user.avatarMedium
}
], {
unique_id: data.liveRoomUserInfo.user.uniqueId,
viewers: data.liveRoomUserInfo.liveRoom.liveRoomStats.userCount,
startTime: data.liveRoomUserInfo.liveRoom.startTime,
})}
</>
)
}
24 changes: 12 additions & 12 deletions src/templates/pages/VideoResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function VideoResponse(data: ItemStruct): JSX.Element {

let videoMeta: { name: string; content: string }[] = []

if (data.video.duration !== 0) {
if(data.video.duration !== 0) {
videoMeta = [
{
name: 'og:video',
Expand Down Expand Up @@ -58,28 +58,28 @@ export function VideoResponse(data: ItemStruct): JSX.Element {
videoMeta = [
...videoMeta,
{
name: 'og:image',
name: "og:image",
content: data.imagePost.images[i].imageURL.urlList[0]
},
{
name: 'og:image:type',
content: 'image/jpeg'
name: "og:image:type",
content: "image/jpeg"
},
{
name: 'og:image:width',
content: 'auto'
name: "og:image:width",
content: "auto"
},
{
name: 'og:image:height',
content: 'auto'
name: "og:image:height",
content: "auto"
},
{
name: 'og:type',
content: 'image.other'
name: "og:type",
content: "image.other"
},
{
name: 'twitter:card',
content: 'summary_large_image'
name: "twitter:card",
content: "summary_large_image"
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion src/tests/photo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('GET /@i/photo/:videoId', () => {
})

expect(res.status).toBe(500)
expect(await res.text()).toContain('An error occurred while trying to fetch the video. Please try again later.')
expect(await res.text()).toContain('An error occurred while trying to fetch data. Please try again later.')
})
})

Expand Down
2 changes: 1 addition & 1 deletion src/tests/video.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('GET /@i/video/:videoId', () => {
})

expect(res.status).toBe(500)
expect(await res.text()).toContain('An error occurred while trying to fetch the video. Please try again later.')
expect(await res.text()).toContain('An error occurred while trying to fetch data. Please try again later.')
})
})

Expand Down
Loading

0 comments on commit c516af8

Please sign in to comment.