├── CNAME
├── Procfile
├── _config.yml
├── requirements.txt
├── config.ini
├── __pycache__
└── scraper.cpython-310.pyc
├── helper
├── __pycache__
│ └── findUrl.cpython-310.pyc
└── findUrl.py
├── .github
└── dependabot.yml
├── api.py
├── README.md
└── scraper.py
/CNAME:
--------------------------------------------------------------------------------
1 | douyinapi.manho30.me
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: python api.py
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==2.3.2
2 | requests==2.28.2
3 | configparser==5.3.0
--------------------------------------------------------------------------------
/config.ini:
--------------------------------------------------------------------------------
1 | [api]
2 | # port number
3 | port = 5000
4 | # api
5 | use_api = True
--------------------------------------------------------------------------------
/__pycache__/scraper.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manho30/douyinapi/HEAD/__pycache__/scraper.cpython-310.pyc
--------------------------------------------------------------------------------
/helper/__pycache__/findUrl.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manho30/douyinapi/HEAD/helper/__pycache__/findUrl.cpython-310.pyc
--------------------------------------------------------------------------------
/helper/findUrl.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | '''
5 | deternmine the url inside of text
6 | '''
7 | def find_url(string):
8 | #
9 | url = re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', string)
10 | return url
11 |
12 | print(find_url('''0.58 XZZ:/ 女孩子主动找你,是因为她喜欢你,她不再主动找你,是因为你回的太敷衍,不是她不喜欢你了,是你让她觉得她很多余
13 | %情感 %治愈 %热门 %爱情 %正能量 %励志 %动漫 %文案%小陌治愈驿站 https://v.douyin.com/FEUgdxn/ 复制此链接,打开Dou音搜索,直接观看视频!'''))
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "pip" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/api.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 |
4 | from flask import Flask, request, jsonify
5 | import configparser
6 |
7 | from requests import Response
8 |
9 | from helper import findUrl
10 | from scraper import Douyin
11 |
12 | douyin = Douyin()
13 |
14 | app = Flask(__name__)
15 |
16 | config = configparser.ConfigParser()
17 | config.read('config.ini', encoding='utf-8')
18 | config = config['api']
19 |
20 | @app.route("/", methods=["POST", "GET"])
21 | def index():
22 | index_info = {
23 | 'ok': True,
24 | 'status': '200',
25 | 'message': {
26 | 'runing': True,
27 | 'version': '1.0.0',
28 | 'author': 'manho',
29 | 'time': '2020/06/03',
30 | 'msg': 'ads free, open source and free to use.'
31 | }
32 | }
33 | res = jsonify(index_info)
34 | res.headers.add('Access-Control-Allow-Origin', '*')
35 | return res
36 |
37 | @app.route("/api/v2", methods=["POST", "GET"])
38 | def video():
39 | try:
40 | url = re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', request.args.get('url'))
41 | except Exception as e:
42 | return jsonify({'ok': False, 'status': '400', 'message': 'url is required'})
43 | if url is None:
44 | return jsonify({'ok': False, 'status': '400', 'message': 'url is invalid'})
45 |
46 | return '''
47 |
48 |
49 | Redirect
50 |
51 |
52 |
53 | Redirecting you...
54 |
58 |
59 |
60 | '''.format(douyin.get_video(url[0]))
61 |
62 |
63 | @app.route("/api", methods=["POST", "GET"])
64 | def api():
65 | if config['use_api'] == 'True':
66 | url = request.args.get('url')
67 | if url:
68 | vid_url = findUrl.find_url(url)[0]
69 | if url:
70 | try:
71 | res = jsonify(douyin.douyin(vid_url))
72 | res.headers.add('Access-Control-Allow-Origin', '*')
73 | return res
74 | except Exception as e:
75 | res = jsonify({
76 | 'ok': False,
77 | 'status': '500',
78 | 'message': f'Internal server error. {e}'
79 | })
80 | res.headers.add('Access-Control-Allow-Origin', '*')
81 | return res
82 | else:
83 | res = jsonify({
84 | 'ok': False,
85 | 'status': '400',
86 | 'message': 'url is not valid.'
87 | })
88 | res.headers.add('Access-Control-Allow-Origin', '*')
89 | return res
90 | else:
91 | res = jsonify({
92 | 'ok': False,
93 | 'status': '400',
94 | 'message': 'url is required.'
95 | })
96 | res.headers.add('Access-Control-Allow-Origin', '*')
97 | return res
98 | else:
99 | res = jsonify({
100 | 'ok': False,
101 | 'status': '403',
102 | 'message': 'api is disabled.'
103 | })
104 | res.headers.add('Access-Control-Allow-Origin', '*')
105 | return res
106 |
107 | if __name__ == '__main__':
108 | # 开启WebAPI
109 | if os.environ.get('PORT'):
110 | port = int(os.environ.get('PORT'))
111 | else:
112 | # 默认端口
113 | port = config['port']
114 | app.run(host='0.0.0.0', port=port)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 抖音无水印下载API
2 | 这是一个免费,开源,简单,方便的抖音无水印下载API。
3 | ## 功能
4 | - 抖音视频/图集解析
5 | - 抖音视频/图集无水印下载
6 | ## 文档
7 | 接口地址如下:
8 |
9 | ```text
10 | https://douyinapi.herokuapp.com/api
11 | ```
12 | 请求参数:
13 | - url:抖音视频链接
14 |
15 |
16 | 1. 网页版直接复制
17 | ```text
18 | https://www.douyin.com/video/7102408528748367136
19 | ```
20 |
21 |
22 | 2. 口令
23 | ```text
24 | 0.58 XZZ:/ 女孩子主动找你,是因为她喜欢你,她不再主动找你,是因为你回的太敷衍,不是她不喜欢你了,是你让她觉得她很多余
25 | %情感 %治愈 %热门 %爱情 %正能量 %励志 %动漫 %文案%小陌治愈驿站 https://v.douyin.com/FEUgdxn/ 复制此链接,打开Dou音搜索,直接观看视频!
26 | ```
27 |
28 | 3. 缩短版
29 | ```text
30 | https://v.douyin.com/FEUgdxn/
31 | ```
32 |
33 | ### 请求格式
34 | ```
35 | http://douyinapi.herokuapp.com/api?url=<链接>
36 | ```
37 |
38 | ### 返回格式
39 | - 视频
40 | ```json
41 | {
42 | "ok":true,
43 | "result":{
44 | "author":{
45 | "avatar":"https://p6.douyinpic.com/aweme/1080x1080/aweme-avatar/tos-cn-avt-0015_e70b935d905b7f6b7520df99105d79f0.jpeg?from=116350172",
46 | "douyin_id":"liu1111525",
47 | "name":"小陌治愈驿站",
48 | "singnature":"可能是世界上最萌的(小笨蛋呐)\n 生活虽苦·但你很甜\n文案|投稿|拿视频|🛰:liu08091111《备注来意》"
49 | },
50 | "details":"https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=7102408528748367136",
51 | "music":{
52 | "author":"小陌治愈驿站",
53 | "duration":"11",
54 | "title":"@小陌治愈驿站创作的原声一小陌治愈驿站",
55 | "url":"https://sf3-cdn-tos.douyinstatic.com/obj/ies-music/7102408551435668254.mp3"
56 | },
57 | "video":{
58 | "descriptions":"女孩子主动找你,是因为她喜欢你,她不再主动找你,是因为你回的太敷衍,不是她不喜欢你了,是你让她觉得她很多余\n#情感 #治愈 #热门 #爱情 #正能量 #励志 #动漫 #文案#小陌治愈驿站",
59 | "statistics":{
60 | "comment_count":"280",
61 | "create_time":"1653658351",
62 | "hashtag":[
63 | "情感",
64 | "治愈",
65 | "热门",
66 | "爱情",
67 | "正能量",
68 | "励志",
69 | "动漫",
70 | "文案",
71 | "小陌治愈驿站"
72 | ],
73 | "likes_count":"5449",
74 | "play_count":"0",
75 | "share_count":"898"
76 | },
77 | "thumbnail_url":{
78 | "url_list":"https://p3-sign.douyinpic.com/tos-cn-p-0015/8c715b06d7694e9dbd8638a3e37ca169~c5_300x400.jpeg?x-expires=1655445600&x-signature=%2Fy1XkaAJxl5wtqYnBAgec47CqAw%3D&from=4257465056_large&s=PackSourceEnum_DOUYIN_REFLOW&se=false&sc=cover&l=202206031410500102120562260C670A3C"
79 | },
80 | "video_url":{
81 | "free_watermark":"https://v3-dy-o.zjcdn.com/3a61c2623a10f55017f1c09c702d0048/6299b406/video/tos/cn/tos-cn-ve-15c001-alinc2/3cb2379e02b44a03bc7c5b446989b95f/?a=1128&ch=0&cr=0&dr=0&cd=0%7C0%7C0%7C0&cv=1&br=502&bt=502&btag=80000&cs=0&ds=3&ft=ArkXtBnZqI2mo0PsxA-fkVQChw79HKJ&mime_type=video_mp4&qs=0&rc=OTlnZWc3OzU4Njk2ZzxoaUBpam9lOTo6ZnRnZDMzNGkzM0AwMS5fMS5fNi8xNV4yNmExYSNzZWxxcjRvMjVgLS1kLTBzcw%3D%3D&l=20220603141051010212142149486457ED",
82 | "free_watermark_1080p":"https://v3-dy-o.zjcdn.com/efe309acc3ebfaa3096213cbebd7d846/6299b405/video/tos/cn/tos-cn-ve-15c001-alinc2/3cb2379e02b44a03bc7c5b446989b95f/?a=1128&ch=0&cr=0&dr=0&cd=0%7C0%7C0%7C0&cv=1&br=502&bt=502&btag=80000&cs=0&ds=3&ft=ArkXtBnZqI2mo0P_xA-fkVQChw79HKJ&mime_type=video_mp4&qs=0&rc=OTlnZWc3OzU4Njk2ZzxoaUBpam9lOTo6ZnRnZDMzNGkzM0AwMS5fMS5fNi8xNV4yNmExYSNzZWxxcjRvMjVgLS1kLTBzcw%3D%3D&l=2022060314105001020812110132618720",
83 | "watermark_url":"https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300fg10000ca8d5qrc77u7t6obhlvg&ratio=720p&line=0"
84 | }
85 | }
86 | },
87 | "status":"200"
88 | }
89 | ```
90 | - 图集
91 | ```json
92 | {
93 | "ok":true,
94 | "result":{
95 | "album":{
96 | "descriptions":"为什么初中生不给过六一🥹",
97 | "heigth":"1920",
98 | "image_url":[
99 | "https://p6-sign.douyinpic.com/tos-cn-i-0813/d5c19515b21a4998bfb4eb6e213a19f2~noop.webp?x-expires=1656828000&x-signature=Fjk055diBz%2BJGcl91ZgJjksRFDg%3D&from=4257465056&s=PackSourceEnum_DOUYIN_REFLOW&se=false&biz_tag=aweme_images&l=202206031427520102081001702A638141",
100 | "https://p26-sign.douyinpic.com/tos-cn-i-0813/f4966b6edbeb4f969e5b72a3c1959183~noop.webp?x-expires=1656828000&x-signature=WpbPrd5AimKgxH3U3%2Bz0yOY2gNk%3D&from=4257465056&s=PackSourceEnum_DOUYIN_REFLOW&se=false&biz_tag=aweme_images&l=202206031427520102081001702A638141",
101 | "https://p6-sign.douyinpic.com/tos-cn-i-0813/4daeaca0dc7a4fb6b65c18535bc82aed~noop.webp?x-expires=1656828000&x-signature=1XGk6RFThE7CIA8pt2CGiIWO3Ww%3D&from=4257465056&s=PackSourceEnum_DOUYIN_REFLOW&se=false&biz_tag=aweme_images&l=202206031427520102081001702A638141"
102 | ],
103 | "width":"1440"
104 | },
105 | "author":{
106 | "avatar":"https://p6.douyinpic.com/aweme/1080x1080/aweme-avatar/tos-cn-avt-0015_874da29bfe40af6fefca313eae3cbf59.jpeg?from=116350172",
107 | "douyin_id":"zaizai77480",
108 | "name":"深",
109 | "singnature":"深爱zxy\n真的超级无敌无敌爱颜哥!😍\n颜哥让我等他回来!那我就勉为其难等一下吧😮💨\n颜宝我等你😘无论多久我都等你.\n异地恋中……..🥺"
110 | },
111 | "details":"https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=7104629824668372264",
112 | "music":{
113 | "author":"Good life",
114 | "duration":"9",
115 | "title":"@Good life创作的原声一Good life",
116 | "url":"https://sf3-cdn-tos.douyinstatic.com/obj/ies-music/7101290689490160397.mp3"
117 | },
118 | "statistics":{
119 | "comment_count":"14",
120 | "create_time":"1654175537",
121 | "hashtag":[],
122 | "likes_count":"1108",
123 | "play_count":"0",
124 | "share_count":"36"
125 | }
126 | },
127 | "status":"200"
128 | }
129 | ```
130 |
131 | ## 本地使用
132 | 1. 克隆本代码仓库
133 | ```bash
134 | $ git clone https://github.com/manho30/douyinapi.git
135 | $ cd douyinapi
136 | ```
137 |
138 | 2. 安装依赖库
139 | ```bash
140 | $ pip install -r requirements.txt
141 | ```
142 |
143 | 3. 启动服务器
144 | >服务器默认启动断口为 `5000`, 如不喜欢可更改`config.ini`中的 `port` 参数
145 | ```bash
146 | $ python3 api.py
147 | ```
148 |
149 | ## 部署到服务器
150 | 本教程使用免费的Heroku服务器,如果你想部署到服务器, 可以接着下面的步骤:
151 |
152 | 1. 创建一个新的Heroku应用
153 | 2. 将代码部署到Heroku应用, 输入命令:
154 | ```bash
155 | $ heroku git:remote -a
156 | $ git push heroku master
157 | ```
158 | 看不明白?
159 | 自己看看 YouTube
160 |
161 | ## 免责声明
162 | 本项目仅用于学习交流,如有侵权,请联系作者删除。
163 |
164 | 使用本项目所产生的法律责任,请自行承担。
165 |
--------------------------------------------------------------------------------
/scraper.py:
--------------------------------------------------------------------------------
1 | '''
2 | -*- encoding: utf-8 -*-
3 | scraping the offcial website of DouYin and return in json format.
4 | @author: manho
5 | @time: 2020/06/03
6 | @function: core of the program to scrap the offcial website of DouYin
7 | '''
8 |
9 | import requests
10 | import json
11 | import re
12 |
13 | class Douyin:
14 |
15 | def __init__(self):
16 | self.headers = {
17 | 'user-agent': 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Mobile Safari/537.36 Edg/87.0.664.66'
18 | }
19 |
20 |
21 | def douyin(self, url):
22 | '''
23 | :description: get the json data by DouYin API.
24 | :param url: douyin video url
25 | :return json format
26 | '''
27 | try:
28 |
29 | # check is the url consist 'user'
30 | if 'user' in url:
31 | return {
32 | 'ok': False,
33 | 'status': '400',
34 | 'message': 'Batch parsing of homepage is not currently support yet.'
35 | }
36 | else:
37 | # original video url
38 | res = requests.get(url, headers=self.headers, allow_redirects=False)
39 | try:
40 | # if there is a short url, then DOUYIN API will redirect to the url with video id.
41 | vid_url = res.headers['Location']
42 |
43 | # check is the url consist 'user'
44 | if 'user' in vid_url:
45 | return {
46 | 'ok': False,
47 | 'status': '400',
48 | 'message': 'Batch parsing of homepage is not currently support yet.'
49 | }
50 | except:
51 | vid_url = url
52 |
53 | '''
54 | get the video id
55 | # abit annoying, there is two type of url:
56 | # 1. https://www.douyin.com/video/123456789
57 | # 2. https://www.douyin.com/discover?modal_id=123456789
58 | '''
59 | try:
60 | # the first type link.
61 |
62 | vid_id = re.findall('video/(\d+)?', vid_url)[0]
63 | print('vid_id: ', vid_id)
64 |
65 | except:
66 |
67 | vid_id = re.findall('modal_id=(\d+)', vid_url)[0]
68 | print('vid_id: ', vid_id)
69 |
70 | # request to official DouYin API
71 | api = f'https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={vid_id}'
72 |
73 | data = json.loads(((requests.get(api, headers=self.headers)).text))
74 | # check is the data consist of album
75 | if data['item_list'][0]['images'] is not None:
76 | # handle the album
77 | image = []
78 | for i in data['item_list'][0]['images']:
79 | image.append(i['url_list'][1])
80 |
81 | hashtag = []
82 | for j in data['item_list'][0]['text_extra']:
83 | hashtag.append(j['hashtag_name'])
84 |
85 | if data['item_list'][0]['music']['title']:
86 | music = {
87 | 'title': str(data['item_list'][0]['music']['title']),
88 | 'url': str(data['item_list'][0]['music']['play_url']['url_list'][0]),
89 | 'author': str(data['item_list'][0]['music']['author']),
90 | 'duration': str(data['item_list'][0]['music']['duration'])
91 | }
92 | else:
93 | music = {}
94 | return {
95 | 'ok': True,
96 | 'status': '200',
97 | 'result': {
98 | 'author': {
99 | 'name': str(data['item_list'][0]['author']['nickname']),
100 | 'singnature': str(data['item_list'][0]['author']['signature']),
101 | 'avatar': str(data['item_list'][0]['author']['avatar_larger']['url_list'][0]),
102 | 'douyin_id': str(data['item_list'][0]['author']['unique_id'])
103 | },
104 | 'album': {
105 | 'image_url': image,
106 | 'heigth': str(data['item_list'][0]['video']['height']),
107 | 'width': str(data['item_list'][0]['video']['width']),
108 | 'descriptions': str(data['item_list'][0]['desc']),
109 | },
110 | 'statistics': {
111 | 'comment_count': str(data['item_list'][0]['statistics']['comment_count']),
112 | 'likes_count': str(data['item_list'][0]['statistics']['digg_count']),
113 | 'share_count': str(data['item_list'][0]['statistics']['share_count']),
114 | 'play_count': str(data['item_list'][0]['statistics']['play_count']),
115 | 'create_time': str(data['item_list'][0]['create_time']),
116 | 'hashtag': hashtag
117 | },
118 | 'music': music,
119 | 'details': api
120 | }
121 | }
122 | else:
123 | # handle the video
124 |
125 | hashtag = []
126 | for j in data['item_list'][0]['text_extra']:
127 | hashtag.append(j['hashtag_name'])
128 |
129 | vid_id = str(data['item_list'][0]['video']['vid'])
130 | try:
131 | r = requests.get("https://aweme.snssdk.com/aweme/v1/play/?video_id={}&radio=1080p&line=0".format(vid_id), headers=self.headers, allow_redirects=False)
132 | free_watermark_1080p = r.headers['Location']
133 | except:
134 | free_watermark_1080p = "None"
135 |
136 | # original video with watermark
137 | watermark = str(data['item_list'][0]['video']['play_addr']['url_list'][0])
138 |
139 | # water mark free video url
140 | free_watermark = str(data['item_list'][0]['video']['play_addr']['url_list'][0]).replace('playwm', 'play')
141 |
142 | res = requests.get(url=free_watermark, headers=self.headers, allow_redirects=False)
143 | free_watermark_720p = res.headers['Location']
144 |
145 |
146 | if data['item_list'][0]['music']['title']:
147 | music = {
148 | 'title': str(data['item_list'][0]['music']['title']),
149 | 'url': str(data['item_list'][0]['music']['play_url']['url_list'][0]),
150 | 'author': str(data['item_list'][0]['music']['author']),
151 | 'duration': str(data['item_list'][0]['music']['duration'])
152 | }
153 | else:
154 | music = {}
155 |
156 | return {
157 | 'ok': True,
158 | 'status': '200',
159 | 'result': {
160 | 'author': {
161 | 'name': str(data['item_list'][0]['author']['nickname']),
162 | 'singnature': str(data['item_list'][0]['author']['signature']),
163 | 'avatar': str(data['item_list'][0]['author']['avatar_larger']['url_list'][0]),
164 | 'douyin_id': str(data['item_list'][0]['author']['unique_id'])
165 | },
166 | 'video': {
167 | 'thumbnail_url': {
168 | 'url_list': str(data['item_list'][0]['video']['cover']['url_list'][0]),
169 | },
170 | 'video_url': {
171 | 'watermark_url': watermark,
172 | 'free_watermark_1080p': free_watermark_1080p,
173 | 'free_watermark': free_watermark_720p,
174 | },
175 | 'statistics': {
176 | 'comment_count': str(data['item_list'][0]['statistics']['comment_count']),
177 | 'likes_count': str(data['item_list'][0]['statistics']['digg_count']),
178 | 'share_count': str(data['item_list'][0]['statistics']['share_count']),
179 | 'play_count': str(data['item_list'][0]['statistics']['play_count']),
180 | 'create_time': str(data['item_list'][0]['create_time']),
181 | 'hashtag': hashtag
182 | },
183 | 'descriptions': str(data['item_list'][0]['desc']),
184 | },
185 | 'music': music,
186 | 'details': api
187 | }
188 | }
189 |
190 | except Exception as e:
191 | return {
192 | 'ok': False,
193 | 'status': '500',
194 | 'message': f'Internal server error. {e}'
195 | }
196 | def get_video(self, url):
197 | try:
198 | r = self.douyin(url)
199 | if r['ok']:
200 | return r['result']['video']['video_url']['free_watermark_1080p']
201 | except Exception as e:
202 | return {
203 | 'ok': False,
204 | 'status': '500',
205 | 'message': f'Internal server error. {e}'
206 | }
207 | # for debug only!
208 |
209 | if __name__ == '__main__':
210 | douyin = Douyin()
211 | print(douyin.get_video("https://v.douyin.com/FEUgdxn/ "))
212 |
--------------------------------------------------------------------------------