Skip to content

Commit 6e66a0d

Browse files
committed
Proxy TikTok videos
1 parent 26ae65a commit 6e66a0d

File tree

3 files changed

+77
-29
lines changed

3 files changed

+77
-29
lines changed

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Flask==2.3.3
22
Flask-Cors==4.0.0
3-
yt-dlp==2024.5.20.232721.dev0
3+
yt-dlp==2024.5.22.232749.dev0
44
pymongo==4.5.0
55
boto3==1.28.37
66
requests==2.32.0

templates/video.html

+8-8
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,24 @@
44
<meta property="og:site_name" content="{{ appname }}">
55

66
<meta name="twitter:card" content="player" />
7-
<meta name="twitter:title" content="{{ videoInfo.creator }} (@{{ videoInfo.uploader }})" />
8-
<meta name="twitter:image" content="{{ videoInfo.thumbnail }}" />
7+
<meta name="twitter:title" content="{{ videoInfo.author.nickname }} (@{{ videoInfo.author.uniqueId }})" />
8+
<meta name="twitter:image" content="{{ vFormat["cover"] }}" />
99
<meta name="twitter:player:width" content="{{ vFormat.width }}" />
1010
<meta name="twitter:player:height" content="{{ vFormat.height }}" />
1111
<meta name="twitter:player:stream" content="{{ mp4URL|safe }}" />
1212
<meta name="twitter:player:stream:content_type" content="video/mp4" />
1313

14-
<meta property="og:url" content="{{ videoInfo.original_url }}" />
14+
<meta property="og:url" content="{{ original_url }}" />
1515
<meta property="og:video" content="{{ mp4URL|safe }}" />
1616
<meta property="og:video:secure_url" content="{{ mp4URL|safe }}" />
1717
<meta property="og:video:type" content="video/mp4" />
1818
<meta property="og:video:width" content="{{ vFormat.width }}" />
1919
<meta property="og:video:height" content="{{ vFormat.height }}" />
20-
<meta property="og:image" content="{{ videoInfo.thumbnail }}" />
20+
<meta property="og:image" content="{{ vFormat["cover"] }}" />
2121

22-
<meta property="og:description" content="{{ videoInfo.description }}" />
22+
<meta property="og:description" content="{{ videoInfo["desc"] }}" />
2323

24-
<link rel="alternate" href="https://{{ domainName }}/owoembed?text={{ statsLine }}&url={{ videoInfo.original_url }}" type="application/json+oembed" title="{{ videoInfo.creator }}">
24+
<link rel="alternate" href="https://{{ domainName }}/owoembed?text={{ statsLine }}&url={{ original_url }}" type="application/json+oembed" title="{{ videoInfo.creator }}">
2525

26-
<meta http-equiv="refresh" content="0; url = {{ videoInfo.original_url }}" /> {% endblock %} {% block body %} Please wait...
27-
<a href="{{ videoInfo.original_url }}">Or click here.</a> {% endblock %}
26+
<meta http-equiv="refresh" content="0; url = {{ original_url }}" /> {% endblock %} {% block body %} Please wait...
27+
<a href="{{ original_url }}">Or click here.</a> {% endblock %}

vxtiktok.py

+68-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from urllib.parse import quote, urljoin, urlparse
2-
from flask import Flask, render_template, request, redirect, send_file
2+
from flask import Flask, render_template, request, redirect, send_file, abort
33
from yt_dlp import YoutubeDL
44
from flask_cors import CORS
55
import json
@@ -31,35 +31,68 @@
3131

3232
tiktokArgs={}
3333

34+
def getWebDataFromResponse(response):
35+
if response.status_code != 200:
36+
return None
37+
# regex to find the json data: <script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application\/json">(.*)}<\/script>
38+
rx = re.compile(r'<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application\/json">(.*)}<\/script>')
39+
match = rx.search(response.text)
40+
if match == None:
41+
return None
42+
data = match.group(1) + "}"
43+
return json.loads(data)
44+
45+
3446
def message(text):
3547
return render_template(
3648
'message.html',
3749
message = text,
3850
appname=config.currentConfig["MAIN"]["appName"])
3951

4052
def findApiFormat(videoInfo):
41-
for format in videoInfo['formats']:
42-
if format['format_id'] == 'download_addr-0':
43-
return format
44-
# not found, search for the next best one
45-
for format in videoInfo['formats']:
46-
if format['url'].startswith('http://api'):
47-
return format
48-
# not found, return the first one
49-
return videoInfo['formats'][0]
53+
vid = videoInfo['video']
54+
return {"width": vid['width'], "height": vid['height'], "url": vid["downloadAddr"],"thumb":vid["cover"]}
5055

5156
def stripURL(url):
5257
return urljoin(url, urlparse(url).path)
5358

54-
def getVideoFromPostURL(url):
55-
with YoutubeDL(params={"extractor_args":{"tiktok":tiktokArgs}}) as ydl:
56-
result = ydl.extract_info(url, download=False)
59+
def getVideoFromPostURL(url,includeCookies=False):
60+
rb = requests.get(url, headers={
61+
"User-Agent": "Mozilla/5.0",
62+
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
63+
"Accept-Language": "en-US,en;q=0.5",
64+
"Sec-Fetch-Mode": "navigate",
65+
"Accept-Encoding": "gzip, deflate, br"
66+
})
67+
videoInfo = getWebDataFromResponse(rb)
68+
69+
if "webapp.video-detail" not in videoInfo["__DEFAULT_SCOPE__"]:
70+
return None
5771

58-
if result["formats"][0]["url"].endswith(".mp3") or (result["formats"][0]["width"] == 0 and result["formats"][0]["height"] == 0):
59-
# this is most likely a slideshow
60-
return getSlideshowFromPostURL(url)
61-
result["slideshowData"] = None
62-
return result
72+
vdata = videoInfo["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]
73+
if includeCookies:
74+
vdata["Cookies"] = rb.cookies.get_dict()
75+
return vdata
76+
77+
def downloadVideoFromPostURL(url):
78+
videoInfo = getVideoFromPostURL(url,includeCookies=True)
79+
vFormat = findApiFormat(videoInfo)
80+
cookies = videoInfo["Cookies"]
81+
82+
headers = {
83+
"User-Agent": "Mozilla/5.0",
84+
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
85+
"Accept-Language": "en-US,en;q=0.5",
86+
"Sec-Fetch-Mode": "navigate",
87+
"Accept-Encoding": "gzip, deflate, br"
88+
}
89+
headers["Cookie"] = "; ".join([f"{k}={v}" for k,v in cookies.items()])
90+
91+
92+
r = requests.get(vFormat["url"], headers=headers)
93+
if r.status_code != 200:
94+
return None
95+
return r.content
6396

6497
def getSlideshowFromPostURL(url): # thsi function assumes the url is a slideshow
6598
with YoutubeDL(params={"dump_intermediate_pages":True,"extractor_args":{"tiktok":tiktokArgs}}) as ydl:
@@ -92,6 +125,10 @@ def getSlideshowFromPostURL(url): # thsi function assumes the url is a slideshow
92125
return result
93126

94127
def build_stats_line(videoInfo):
128+
videoInfo["view_count"] = videoInfo["stats"]["playCount"]
129+
videoInfo["like_count"] = videoInfo["stats"]["diggCount"]
130+
videoInfo["repost_count"] = videoInfo["stats"]["shareCount"]
131+
videoInfo["comment_count"] = videoInfo["stats"]["commentCount"]
95132
if videoInfo['view_count'] > 0 or videoInfo['like_count'] > 0 or videoInfo['repostCount'] > 0 or videoInfo['comment_count'] > 0:
96133
text = ""
97134

@@ -114,6 +151,8 @@ def getVideoDataFromCacheOrDl(post_link):
114151
videoInfo = cachedItem
115152
else:
116153
videoInfo = getVideoFromPostURL(post_link)
154+
if videoInfo == None:
155+
return None
117156
cache.addToCache(post_link, videoInfo)
118157
return videoInfo
119158
except Exception as e:
@@ -126,17 +165,26 @@ def embed_tiktok(post_link):
126165
return message("Failed to get video data from TikTok")
127166
if "slideshowData" not in videoInfo or videoInfo["slideshowData"] == None:
128167
vFormat = findApiFormat(videoInfo)
129-
directURL = vFormat['url']
168+
169+
directURL = f"https://"+config.currentConfig["MAIN"]["domainName"]+"/vid/"+videoInfo["author"]["uniqueId"]+"/"+videoInfo["id"]+".mp4"
130170
else:
131171
vFormat = {"width": 1280, "height": 720}
132172
directURL = "https://"+config.currentConfig["MAIN"]["domainName"]+"/slideshow.mp4?url="+post_link
133173
statsLine = quote(build_stats_line(videoInfo))
134-
return render_template('video.html', videoInfo=videoInfo, mp4URL=directURL, vFormat=vFormat, appname=config.currentConfig["MAIN"]["appName"], statsLine=statsLine, domainName=config.currentConfig["MAIN"]["domainName"])
174+
return render_template('video.html', videoInfo=videoInfo, mp4URL=directURL, vFormat=vFormat, appname=config.currentConfig["MAIN"]["appName"], statsLine=statsLine, domainName=config.currentConfig["MAIN"]["domainName"],original_url = post_link)
135175

136176
@app.route('/')
137177
def main():
138178
return redirect(config.currentConfig["MAIN"]["repoURL"])
139179

180+
@app.route('/vid/<author>/<vid>.mp4')
181+
def video(author, vid):
182+
post_link = f"https://www.tiktok.com/@{author}/video/{vid}"
183+
videoData = downloadVideoFromPostURL(post_link)
184+
if videoData == None:
185+
abort(500)
186+
return send_file(io.BytesIO(videoData), mimetype='video/mp4')
187+
140188
@app.route('/owoembed')
141189
def alternateJSON():
142190
return {

0 commit comments

Comments
 (0)