Skip to content

Commit 28cba77

Browse files
authored
Create douyin_appsign.py
1 parent 023d6fd commit 28cba77

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed

douyin/douyin_appsign.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
# -*- coding:utf-8 -*-
2+
from contextlib import closing
3+
import requests, json, re, os, sys, random
4+
from ipaddress import ip_address
5+
from subprocess import Popen, PIPE
6+
import urllib
7+
8+
class DouYin(object):
9+
def __init__(self, width = 500, height = 300):
10+
"""
11+
抖音App视频下载
12+
"""
13+
rip = ip_address('0.0.0.0')
14+
while rip.is_private:
15+
rip = ip_address('.'.join(map(str, (random.randint(0, 255) for _ in range(4)))))
16+
self.headers = {
17+
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
18+
'accept-encoding': 'gzip, deflate, br',
19+
'accept-language': 'zh-CN,zh;q=0.9',
20+
'pragma': 'no-cache',
21+
'cache-control': 'no-cache',
22+
'upgrade-insecure-requests': '1',
23+
'user-agent': 'Mozilla/5.0 (Linux; U; Android 5.1.1; zh-cn; MI 4S Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.146 Mobile Safari/537.36 XiaoMi/MiuiBrowser/9.1.3',
24+
'X-Real-IP': str(rip),
25+
'X-Forwarded-For': str(rip),
26+
}
27+
28+
def getToken(self):
29+
req = requests.get('https://api.appsign.vip:2688/token/douyin/version/2.7.0').json()
30+
return self.save_json(req)
31+
32+
def getDevice(self):
33+
req = requests.get('https://api.appsign.vip:2688/douyin/device/new/version/2.7.0').json()
34+
device_info = req['data']
35+
return device_info
36+
37+
def getSign(self, token, query):
38+
req = requests.post('https://api.appsign.vip:2688/sign', json={'token': token, 'query': query}).json()
39+
if req['success']:
40+
sign = req['data']
41+
else:
42+
sign = req['success']
43+
return sign
44+
45+
def params2str(self, params):
46+
query = ''
47+
for k, v in params.items():
48+
query += '%s=%s&' % (k, v)
49+
query = query.strip('&')
50+
return query
51+
52+
def save_json(self, data):
53+
with open('douyin.txt', 'w') as f:
54+
json.dump(data, f, ensure_ascii=False)
55+
56+
def load_json(self):
57+
with open('douyin.txt', 'r') as f:
58+
data = json.load(f)
59+
return data
60+
61+
def get_video_urls(self, user_id):
62+
"""
63+
获得视频播放地址
64+
Parameters:
65+
user_id:查询的用户ID
66+
Returns:
67+
video_names: 视频名字列表
68+
video_urls: 视频链接列表
69+
nickname: 用户昵称
70+
"""
71+
video_names = []
72+
video_urls = []
73+
share_urls = []
74+
unique_id = ''
75+
max_cursor = 0
76+
has_more = 1
77+
device_info = self.getDevice()
78+
APPINFO = {
79+
'version_code': '2.7.0',
80+
'app_version': '2.7.0',
81+
'channel': 'App%20Stroe',
82+
'app_name': 'aweme',
83+
'build_number': '27014',
84+
'aid': '1128'
85+
}
86+
params = {
87+
'iid': device_info['iid'],
88+
'idfa': device_info['idfa'],
89+
'vid': device_info['vid'],
90+
'device_id': device_info['device_id'],
91+
'openudid': device_info['openudid'],
92+
'device_type': device_info['device_type'],
93+
'os_version': device_info['os_version'],
94+
'os_api': device_info['os_api'],
95+
'screen_width': device_info['screen_width'],
96+
'device_platform': device_info['device_platform'],
97+
'version_code': APPINFO['version_code'],
98+
'channel': APPINFO['channel'],
99+
'app_name': APPINFO['app_name'],
100+
'build_number': APPINFO['build_number'],
101+
'app_version': APPINFO['app_version'],
102+
'aid': APPINFO['aid'],
103+
'ac': 'WIFI',
104+
'count': '12',
105+
'keyword': user_id,
106+
'offset': '0'
107+
}
108+
query = self.params2str(params)
109+
if not os.path.isfile('douyin.txt'):
110+
self.getToken()
111+
token = self.load_json()['token']
112+
sign = self.getSign(token, query)
113+
if not sign:
114+
self.getToken()
115+
token = self.load_json()['token']
116+
sign = self.getSign(token, query)
117+
params['mas'] = sign['mas']
118+
params['as'] = sign['as']
119+
params['ts'] = sign['ts']
120+
headers = {
121+
'User-Agent': 'Aweme/2.7.0 (iPhone; iOS 11.0; Scale/2.00)'
122+
}
123+
req = requests.get('https://api.amemv.com/aweme/v1/general/search/', params=params, headers=headers)
124+
html = json.loads(req.text)
125+
uid = html['user_list'][0]['user_info']['uid']
126+
nickname = html['user_list'][0]['user_info']['nickname']
127+
unique_id = html['user_list'][0]['user_info']['unique_id']
128+
if unique_id != user_id:
129+
unique_id = html['user_list'][0]['user_info']['short_id']
130+
if unique_id != user_id:
131+
print('用户ID可能输入错误或无法搜索到此用户ID')
132+
sys.exit()
133+
share_user_url = 'https://www.amemv.com/share/user/%s' % uid
134+
share_user = requests.get(share_user_url, headers=self.headers)
135+
_dytk_re = re.compile(r"dytk:\s*'(.+)'")
136+
dytk = _dytk_re.search(share_user.text).group(1)
137+
print('JS签名下载中')
138+
urllib.request.urlretrieve('https://raw.githubusercontent.com/Jack-Cherish/python-spider/master/douyin/fuck-byted-acrawler.js', 'fuck-byted-acrawler.js')
139+
try:
140+
process = Popen(['node', 'fuck-byted-acrawler.js', str(uid)], stdout=PIPE, stderr=PIPE)
141+
except (OSError, IOError) as err:
142+
print('请先安装 node.js: https://nodejs.org/')
143+
sys.exit()
144+
sign = process.communicate()[0].decode().strip('\n').strip('\r')
145+
print('解析视频链接中')
146+
while has_more != 0:
147+
user_url = 'https://www.amemv.com/aweme/v1/aweme/post/?user_id=%s&max_cursor=%s&count=21&aid=1128&_signature=%s&dytk=%s' % (uid, max_cursor, sign, dytk)
148+
req = requests.get(user_url, headers=self.headers)
149+
while req.status_code != 200:
150+
req = requests.get(user_url, headers=self.headers)
151+
html = json.loads(req.text)
152+
for each in html['aweme_list']:
153+
share_desc = each['share_info']['share_desc']
154+
if os.name == 'nt':
155+
for c in r'\/:*?"<>|':
156+
nickname = nickname.replace(c, '').strip().strip('\.')
157+
share_desc = share_desc.replace(c, '').strip()
158+
share_id = each['aweme_id']
159+
if share_desc in ['抖音-原创音乐短视频社区', 'TikTok']:
160+
video_names.append(share_id + '.mp4')
161+
else:
162+
video_names.append(share_id + '-' + share_desc + '.mp4')
163+
share_urls.append(each['share_info']['share_url'])
164+
video_urls.append(each['video']['play_addr']['url_list'][0])
165+
max_cursor = html['max_cursor']
166+
has_more = html['has_more']
167+
168+
return video_names, video_urls, share_urls, nickname
169+
170+
def get_download_url(self, video_url, watermark_flag):
171+
"""
172+
获得带水印的视频播放地址
173+
Parameters:
174+
video_url:带水印的视频播放地址
175+
Returns:
176+
download_url: 带水印的视频下载地址
177+
"""
178+
# 带水印视频
179+
if watermark_flag == True:
180+
download_url = video_url
181+
# 无水印视频
182+
else:
183+
download_url = video_url.replace('playwm', 'play')
184+
185+
return download_url
186+
187+
def video_downloader(self, video_url, video_name, watermark_flag=False):
188+
"""
189+
视频下载
190+
Parameters:
191+
video_url: 带水印的视频地址
192+
video_name: 视频名
193+
watermark_flag: 是否下载带水印的视频
194+
Returns:
195+
196+
"""
197+
size = 0
198+
video_url = self.get_download_url(video_url, watermark_flag=watermark_flag)
199+
with closing(requests.get(video_url, headers=self.headers, stream=True)) as response:
200+
chunk_size = 1024
201+
content_size = int(response.headers['content-length'])
202+
if response.status_code == 200:
203+
sys.stdout.write(' [文件大小]:%0.2f MB\n' % (content_size / chunk_size / 1024))
204+
205+
with open(video_name, 'wb') as file:
206+
for data in response.iter_content(chunk_size = chunk_size):
207+
file.write(data)
208+
size += len(data)
209+
file.flush()
210+
211+
sys.stdout.write(' [下载进度]:%.2f%%' % float(size / content_size * 100) + '\r')
212+
sys.stdout.flush()
213+
214+
def run(self):
215+
"""
216+
运行函数
217+
Parameters:
218+
None
219+
Returns:
220+
None
221+
"""
222+
self.hello()
223+
user_id = input('请输入ID(例如792279162或Empty_1996):')
224+
watermark_flag = int(input('是否下载带水印的视频(0-否,1-是):'))
225+
video_names, video_urls, share_urls, nickname = self.get_video_urls(user_id)
226+
if nickname not in os.listdir():
227+
os.mkdir(nickname)
228+
print('视频下载中:共有%d个作品!\n' % len(video_urls))
229+
for num in range(len(video_urls)):
230+
print(' 解析第%d个视频链接 [%s] 中,请稍后!\n' % (num + 1, share_urls[num]))
231+
if '\\' in video_names[num]:
232+
video_name = video_names[num].replace('\\', '')
233+
elif '/' in video_names[num]:
234+
video_name = video_names[num].replace('/', '')
235+
else:
236+
video_name = video_names[num]
237+
if os.path.isfile(os.path.join(nickname, video_name)):
238+
print('视频已存在')
239+
else:
240+
self.video_downloader(video_urls[num], os.path.join(nickname, video_name), watermark_flag)
241+
print('\n')
242+
print('下载完成!')
243+
244+
def hello(self):
245+
"""
246+
打印欢迎界面
247+
Parameters:
248+
None
249+
Returns:
250+
None
251+
"""
252+
print('*' * 100)
253+
print('\t\t\t\t抖音App视频下载小助手')
254+
print('\t\t作者:Jack Cui、steven7851')
255+
print('*' * 100)
256+
257+
258+
if __name__ == '__main__':
259+
douyin = DouYin()
260+
douyin.run()

0 commit comments

Comments
 (0)