├── .github └── dependabot.yml ├── CNAME ├── Procfile ├── README.md ├── __pycache__ └── scraper.cpython-310.pyc ├── _config.yml ├── api.py ├── config.ini ├── helper ├── __pycache__ │ └── findUrl.cpython-310.pyc └── findUrl.py ├── requirements.txt └── scraper.py /.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 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | douyinapi.manho30.me -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python api.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /__pycache__/scraper.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manho30/douyinapi/1f73e5aa6f080a2a870ea5fb1491fc5468f0cc46/__pycache__/scraper.cpython-310.pyc -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [api] 2 | # port number 3 | port = 5000 4 | # api 5 | use_api = True -------------------------------------------------------------------------------- /helper/__pycache__/findUrl.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manho30/douyinapi/1f73e5aa6f080a2a870ea5fb1491fc5468f0cc46/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音搜索,直接观看视频!''')) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.2 2 | requests==2.28.2 3 | configparser==5.3.0 -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------