Compare commits

...

131 Commits
v1.3 ... main

Author SHA1 Message Date
imgyh
8158292756 docs(changelog): 更新日志 2023-05-28 15:49:30 +08:00
imgyh
a790aad8df docs(readme): 完善文档 2023-05-28 15:48:20 +08:00
imgyh
1e69ee8a14 fix(webapi): 修复作品列表为空时不返回cursor
fix #40
2023-05-28 15:47:34 +08:00
imgyh
ff72578a9c docs(issue): 增加issue模板 2023-05-24 11:05:21 +08:00
imgyh
f7a3acc754 ci(pyinstaller): webApi打包增加静态文件
fix #39
2023-05-23 22:06:32 +08:00
imgyh
b38e210927 feat(douyin): 修改文件名命名风格 2023-05-22 16:39:53 +08:00
imgyh
ce385d9608 ci(docker): 更新自动化部署配置 2023-05-21 16:44:38 +08:00
imgyh
d87cf4f837 docs(readme): 更新文档 2023-05-21 16:44:02 +08:00
imgyh
336267fac4 docs(changelog): 更新日志 2023-05-21 16:27:53 +08:00
imgyh
2114bc925e feat(tiktok): 删除多余文件 2023-05-21 16:25:02 +08:00
imgyh
54e50598de docs(readme): 更新文档
re #36
2023-05-21 16:23:55 +08:00
imgyh
fc90897396 ci(tiktok): 修改打包配置 2023-05-21 16:20:36 +08:00
imgyh
942bc35912 feat(tiktok): 规范包目录结构 2023-05-21 16:17:21 +08:00
imgyh
3b1f9e7b5c docs(changelog): 更新日志 2023-04-24 20:46:59 +08:00
imgyh
40b4462464 docs(readme): 更新文档 2023-04-24 20:45:00 +08:00
imgyh
ee06c65269 docs(config): 更新配置文件 2023-04-24 20:43:18 +08:00
imgyh
ebf6671d33 feat(tiktok): 增加数据库功能与增量更新功能
re #24
2023-04-24 20:42:20 +08:00
imgyh
87b48aec06 fix(web): 解决浏览器提示不安全问题 2023-04-21 20:50:24 +08:00
imgyh
a140c2f3f8 fix(web): 解决浏览器提示不安全问题 2023-04-21 20:48:36 +08:00
imgyh
1c48f64b75 fix(web): 解决浏览器提示不安全问题 2023-04-21 20:20:31 +08:00
imgyh
321d4622a1 fix(tiktok): 优化视频链接获取 2023-04-21 20:18:56 +08:00
imgyh
a984186542 docs(changelog): 更新日志 2023-04-21 17:59:28 +08:00
imgyh
72aca75960 feat(web): 前段静态文件使用国内cdn 2023-04-21 17:58:34 +08:00
imgyh
064ae7dc63 style(tiktok): 整理代码格式 2023-04-21 16:53:37 +08:00
imgyh
48fcf902be test(test): 测试接口 2023-04-21 16:41:19 +08:00
imgyh
ada1851e86 fix(result): 修复1080p清晰度链接 2023-04-21 16:40:48 +08:00
imgyh
9aaab2f691 fix(tiktok): 修复接口失效问题
fix #33
2023-04-21 16:37:10 +08:00
imgyh
5c6dd6fb45 docs(changelog): 更新日志 2023-04-16 19:51:22 +08:00
imgyh
8f6c1ea70d fix(tiktok): 增加请求重试机制 2023-04-16 19:50:20 +08:00
imgyh
59b24472c7 ci(docker): 适配基础镜像平台 2023-04-16 19:09:22 +08:00
imgyh
25614f9a67 docs(changelog): 更新日志 2023-04-16 19:02:42 +08:00
imgyh
6808355e9f docs(readme): 更新文档 2023-04-16 19:00:59 +08:00
imgyh
1ef56da99b build(dockerfile): 增加node环境 2023-04-16 19:00:30 +08:00
imgyh
596a0fc633 feat(command): 未传入cookie则使用默认值 2023-04-16 18:59:09 +08:00
imgyh
2f13bd5122 fix(web): 前端适配新接口 2023-04-16 18:58:21 +08:00
imgyh
d3d091d9dd feat(web): 所有接口支持json与form两种格式, cookie不传使用默认值
fix #31
2023-04-16 18:56:15 +08:00
imgyh
372450b4d7 docs(readme): 更新文档 2023-04-14 22:42:25 +08:00
imgyh
918d6c9eba feat(web): 增加解析接口
fix #28
2023-04-14 22:41:56 +08:00
imgyh
2ea2ac07a8 fix(tiktok): 更改多作品接口请求逻辑, 不再调用单个作品的方法 2023-04-14 20:22:52 +08:00
imgyh
aebc14429d fix(command): 修复mode命令解析问题 2023-04-04 13:11:43 +08:00
imgyh
72a57668c7 docs(readme): 更新日志 2023-03-29 19:00:03 +08:00
imgyh
99c62922ca fix(tiktok): 缩短文件名长度,只使用数字字母汉字作为文件名
fix #19
2023-03-29 18:59:41 +08:00
imgyh
f2c1587e53 docs(changelog): 更新日志 2023-03-28 22:16:36 +08:00
imgyh
f0d8223f14 ci(pyinstaller): 增加配置文件下载 2023-03-28 22:15:39 +08:00
imgyh
01fbd743f8 fix(tiktok): 缩短文件名长度 2023-03-28 22:11:31 +08:00
imgyh
47b7662a66 docs(changelog): 更新日志 2023-03-28 21:39:58 +08:00
imgyh
0314e2520d docs(readme): 更新文档 2023-03-28 21:37:47 +08:00
imgyh
8ce13c0d72 fix(tiktok): 修改文件名匹配正则表达式 2023-03-28 21:08:35 +08:00
imgyh
2496ccd90c fix(tiktok): 修复配置文件路径获取问题 2023-03-28 20:17:02 +08:00
imgyh
f55dd004e9 fix(tiktok): 使用tqdm进度条 2023-03-28 20:16:24 +08:00
imgyh
cbfd137184 chore(requirements): 添加tqdm 2023-03-28 20:15:44 +08:00
imgyh
262c10495c docs(changelog): 更新日志 2023-03-28 17:24:14 +08:00
imgyh
e900d1d2ab docs(readme): 更新文档 2023-03-28 17:22:12 +08:00
imgyh
a6c86629e6 test(test): 删除以前的测试命令 2023-03-28 17:21:34 +08:00
imgyh
ec559e2913 feat(tiktok): 增加配置文件, 手动传入自己的cookie
fix #16
2023-03-28 17:21:04 +08:00
imgyh
9fc37f1048 fix(tiktok): 重试失败后返回已经获取到的数据, 而不是抛异常 2023-03-28 17:19:41 +08:00
imgyh
6b6978df49 chore(requirements): 添加PyYAML 2023-03-28 17:16:37 +08:00
imgyh
c9ece0bf50 feat(tiktok): 支持电脑网页版url作为链接 2023-03-27 21:21:23 +08:00
imgyh
20350b8889 feat(tiktok): 进度条滚动显示 2023-03-27 14:19:37 +08:00
imgyh
794632d6c5 fix(tiktok): 增加文件下载失败重试机制 2023-03-27 12:55:30 +08:00
imgyh
38fc76826d feat(tiktok): 单个作品使用多线程下载 2023-03-27 10:42:45 +08:00
imgyh
0fb3739b47 feat(tiktok): 使用rich进度条 2023-03-27 10:42:07 +08:00
imgyh
ebb60c8ee7 chore(requirements): 使用rich替换tqdm 2023-03-27 10:41:32 +08:00
imgyh
def3dd77e7 docs(readme): 更新文档 2023-03-25 21:31:19 +08:00
imgyh
8ea8871cc8 feat(tiktok): 增加json数据是否保存的开关 2023-03-25 21:20:58 +08:00
imgyh
c5b9ec5faf fix(tiktok): 修复主页作品获取失败
fix #15
2023-03-25 21:00:34 +08:00
imgyh
f753c5c52e fix(tiktok): 修复获取单个合集id失败 2023-03-25 20:30:50 +08:00
imgyh
132ab70094 docs(changelog): 更新日志 2023-03-22 19:01:37 +08:00
imgyh
04b3279975 test(test): 更改失效的链接 2023-03-22 18:59:18 +08:00
imgyh
939a417654 fix(tiktok): 单个作品不使用多线程 2023-03-22 18:58:49 +08:00
imgyh
f8ec5e3745 fix(tiktok): 获取x-bogus错误后重试, 单个作品不使用多线程 2023-03-22 18:58:22 +08:00
imgyh
89d4c7cd42 feat(tiktok): 使用线程池代替手动创建线程 2023-03-22 16:46:35 +08:00
imgyh
f224557461 fix(utils): 优化获取x-bogus时出错的提示信息 2023-03-22 16:44:30 +08:00
imgyh
4c1a4ed950 fix(tiktok): 修改文件夹名的正则匹配逻辑
fix #11
2023-03-19 14:09:59 +08:00
imgyh
e194a2818b fix(tiktok): 修改命令行帮助提示信息 2023-03-19 14:07:58 +08:00
imgyh
eb570b183c docs(readme): 更新文档 2023-03-19 13:16:00 +08:00
imgyh
d9c224bd2d feat(web): 前端提示信息优化 2023-03-19 13:15:12 +08:00
imgyh
e4a0ebba4f feat(live): 直播解析支持APP端分享链接 2023-03-19 13:07:17 +08:00
imgyh
39f3920e54 docs(changelog): 更新日志 2023-03-16 16:57:35 +08:00
imgyh
14f51a62aa docs(readme): 更新文档 2023-03-16 16:55:43 +08:00
imgyh
83e46322ee feat(tiktok): 适配多线程下载 2023-03-16 16:55:15 +08:00
imgyh
af63aadcfc chore(requirements): 添加tqdm 2023-03-16 16:54:20 +08:00
imgyh
85942f2ffc fix(tiktok): 修复无法获取图集id
fix #10
2023-03-16 12:33:21 +08:00
imgyh
322f004e69 docs(readme): 修改说明文档 2023-03-11 20:45:26 +08:00
imgyh
723da7640e ci(pyinstaller): 增加TikTokWeb打包 2023-03-11 20:44:49 +08:00
imgyh
8c5863d44f fix(tiktok): 修复接口未返回数据时重复请求的死循环(10s后还是未返回就抛异常) 2023-03-11 20:25:09 +08:00
imgyh
3fcacbd14c refactor(tiktok): 优化提示信息 2023-03-07 21:10:38 +08:00
imgyh
e608e5431b docs(changelog): 更新日志 2023-03-05 22:05:30 +08:00
imgyh
1b85b19891 fix(web): 取消指定端口为必选项 2023-03-05 22:02:59 +08:00
imgyh
3c2ed3cd78 docs(changelog): 更新日志 2023-03-05 21:34:57 +08:00
imgyh
50beb45228 docs(readme): 更新README 2023-03-05 21:34:27 +08:00
imgyh
7c44b9426d feat(url): 添加速度更快的X-Bogus接口 2023-03-05 21:31:12 +08:00
imgyh
469fe2c521 feat(live): 增加直播解析方法的提示信息开启或关闭选项 2023-03-05 20:50:28 +08:00
imgyh
98a9f74d4b feat(web): 增加命令行指定web端口 2023-03-05 20:48:38 +08:00
imgyh
80825b2fc2 refactor(web): 优化前端细节 2023-03-05 20:27:08 +08:00
imgyh
ee4ec700b8 docs(changelog): 更新日志 2023-03-04 22:33:01 +08:00
imgyh
04ec0f1c40 fix(live): 修复无法播放http的直播地址 2023-03-04 22:30:57 +08:00
imgyh
42436948f7 docs(changelog): 更新日志 2023-03-04 22:06:40 +08:00
imgyh
44670186af feat(utils): 重新加入远程调用X-Bogus接口作为备用,防止本地没有JS环境 2023-03-04 22:06:07 +08:00
imgyh
fb82300a27 docs(readme): 更新README 2023-03-04 20:29:11 +08:00
imgyh
6f2908c116 docs(changelog): 更新日志 2023-03-04 20:23:17 +08:00
imgyh
90aa10515f feat(web): 增加web端直播解析 2023-03-04 20:06:59 +08:00
imgyh
f7fdcd141b feat(live): 适配web端的直播解析 2023-03-04 20:05:17 +08:00
imgyh
2183ebdb41 refactor(utils): 删除冗余代码 2023-03-03 17:49:03 +08:00
imgyh
5536380d53 ci(pyinstaller): 增加打包exe时加入js文件 2023-03-03 17:33:20 +08:00
B1gM8c
a4328cd53b
feat(tiktok): 支持本地生成X-Bogus (#3)
* 支持本地生成X-Bogus

由于使用第三方接口必然会产生网络延迟等问题,故支持本地生成X-Bogus

参考项目https://github.com/B1gM8c/tiktok
2023-03-03 17:27:36 +08:00
imgyh
8f9fe9f1e7 docs(changelog): 更新日志 2023-03-02 21:32:39 +08:00
imgyh
2eae2e182d test(test): 测试passport_csrf_token 2023-03-02 21:28:50 +08:00
imgyh
ebe3be43ef fix(tiktok): cookies增加passport_csrf_token 2023-03-02 21:28:10 +08:00
imgyh
5945ffaef0 docs(changelog): 更新日志 2023-03-02 19:43:41 +08:00
imgyh
bb5a780b31 docs(readme): 增加下载前n个作品指南 2023-03-02 19:36:58 +08:00
imgyh
4efeb62701 feat(tiktok): 增加下载前n个作品功能 2023-03-02 19:34:39 +08:00
imgyh
7cb1c4be5c refactor(tiktok): 修改合集链接为统一常量 2023-03-02 17:18:46 +08:00
imgyh
fcaaf26a86 docs(readme): 增加音乐合集视频批量下载介绍 2023-03-02 17:15:11 +08:00
imgyh
c59e8f72df test(tiktok): 增加音乐合集功能测试 2023-03-02 17:13:55 +08:00
imgyh
b3aadf630a feat(tiktok): 增加音乐合集批量下载功能 2023-03-02 17:11:52 +08:00
imgyh
fa635ebe7f feat(url): 增加音乐合集URL 2023-03-02 17:11:02 +08:00
imgyh
c6bcff67da fix(tiktok): 自动获取ttwid 2023-03-02 16:34:18 +08:00
imgyh
665ca47b08 fix(command): 修复头像封面音乐总是会下载 2023-02-27 10:27:45 +08:00
imgyh
0d0c77f882 docs(changelog): 更新CHANGELOG 2023-02-23 11:09:51 +08:00
imgyh
76b3478b5d docs(changelog): 更新变更日志 2023-02-23 10:53:42 +08:00
imgyh
9138305356 test(test): 增加测试方法 2023-02-23 10:50:59 +08:00
imgyh
197d12627d fix(live): 统一直播获取的header, 直播接口添加X-Bogus 2023-02-23 10:50:02 +08:00
imgyh
8a01e681b5 fix(tiktok): 修复用户主页作品接口 2023-02-23 10:28:35 +08:00
imgyh
ded3850ecc docs(readme): 更新文档 2023-02-22 14:43:48 +08:00
imgyh
c5b890b693 style(tiktok): 改善提示信息 2023-02-22 12:35:22 +08:00
imgyh
ac73a97c19 fix(tiktok): 添加单个作品获取失败后重试逻辑 2023-02-22 10:49:30 +08:00
imgyh
c803db303d test(tiktoktest): 增加合集下载功能测试命令 2023-02-21 22:17:19 +08:00
imgyh
b95d718828 feat(tiktok): 增加合集下载功能
增加单个合集批量下载功能, 增加主页下所有合集批量下载功能
2023-02-21 22:16:11 +08:00
imgyh
d338e6bafc style(tiktokresult): 改进提示信息 2023-02-21 22:14:10 +08:00
imgyh
e3029be42b fix(tiktok): 修复主页作品接口 2023-02-21 18:21:06 +08:00
48 changed files with 4741 additions and 2187 deletions

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,31 @@
---
name: Bug report
about: 报告出现的Bug
title: "[BUG]"
labels: bug
assignees: imgyh
---
**Bug 描述**
对bug的清晰而简洁的描述。
**Bug 复现**
复现这次行为的步骤:
1. 更改了 '...'
2. 点击了 '....'
3. '....'
**预期行为**
简单描述预期结果。
**截图**
如果适用,请添加屏幕截图以帮助解释您的问题。
**环境信息 (请填写以下信息):**
- 操作系统: [e.g. windows]
- 命令: [e.g. DouyinCommand.py, WebApi.py, DouyinCommand.exe, WebApi.exe]
- 版本 [e.g. v1.6.4]
**附文**
在此处添加有关该问题的其他文字。

View File

@ -0,0 +1,17 @@
---
name: Feature request
about: 对该项目的建议
title: "[Feature]"
labels: enhancement
assignees: imgyh
---
**描述您想要的功能**
对你想要加入的功能的清晰而简洁的描述。
**描述你想要替换的功能或需要修改的解决方案**
对您考虑过的任何替代解决方案或功能的清晰简洁的描述。
**附文**
在此处添加有关功能请求的任何其他文字或屏幕截图。

22
.github/ISSUE_TEMPLATE/help_want.md vendored Normal file
View File

@ -0,0 +1,22 @@
---
name: Help Want
about: 想要得到帮助非Bug问题如程序不会用、部署相关等问题
title: "[Help]"
labels: help wanted
assignees: imgyh
---
**问题描述**
对您的遇到的问题清晰而简洁的描述。
**截图**
如果适用,请添加屏幕截图以帮助解释您的问题。
**环境信息 (请填写以下信息):**
- 操作系统: [e.g. windows]
- 命令: [e.g. DouyinCommand.py, WebApi.py, DouyinCommand.exe, WebApi.exe]
- 版本 [e.g. v1.6.4]
**附文**
在此处添加有关该问题的其他文字。

View File

@ -51,6 +51,7 @@ jobs:
platforms: | platforms: |
linux/amd64 linux/amd64
linux/386 linux/386
linux/arm/v7
linux/arm64/v8 linux/arm64/v8
# docker build arg, 注入 APP_NAME/APP_VERSION # docker build arg, 注入 APP_NAME/APP_VERSION
build-args: | build-args: |

View File

@ -28,7 +28,8 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install -r requirements.txt python -m pip install -r requirements.txt
pyinstaller -i ./static/img/favicon.ico -F TikTokCommand.py pyinstaller -i ./static/img/favicon.ico -F DouYinCommand.py
pyinstaller -i ./static/img/favicon.ico -F WebApi.py --add-data "templates;templates" --add-data "static;static"
- name: Create Release and Upload Release Asset - name: Create Release and Upload Release Asset
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
@ -45,6 +46,8 @@ jobs:
# 如果指定了名称,将使用指定的名称;否则,将自动生成一个名称。 # 如果指定了名称,将使用指定的名称;否则,将自动生成一个名称。
# 如果指定了正文,正文将被添加到自动生成的注释中。 # 如果指定了正文,正文将被添加到自动生成的注释中。
files: | # 多个文件要加 | files: | # 多个文件要加 |
dist/TikTokCommand.exe dist/DouYinCommand.exe
dist/WebApi.exe
config.yml
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 默认为${{ github.token }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 默认为${{ github.token }}

321
.gitignore vendored
View File

@ -1,160 +1,161 @@
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
# C extensions # C extensions
*.so *.so
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
share/python-wheels/ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
MANIFEST MANIFEST
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it. # before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest *.manifest
*.spec *.spec
# Installer logs # Installer logs
pip-log.txt pip-log.txt
pip-delete-this-directory.txt pip-delete-this-directory.txt
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
.tox/ .tox/
.nox/ .nox/
.coverage .coverage
.coverage.* .coverage.*
.cache .cache
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*.cover *.cover
*.py,cover *.py,cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
cover/ cover/
# Translations # Translations
*.mo *.mo
*.pot *.pot
# Django stuff: # Django stuff:
*.log *.log
local_settings.py local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
# Flask stuff: # Flask stuff:
instance/ instance/
.webassets-cache .webassets-cache
# Scrapy stuff: # Scrapy stuff:
.scrapy .scrapy
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
.pybuilder/ .pybuilder/
target/ target/
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints
# IPython # IPython
profile_default/ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
# For a library or package, you might want to ignore these files since the code is # For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in: # intended to run in multiple environments; otherwise, check them in:
# .python-version # .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies # However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not # having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock #Pipfile.lock
# poetry # poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more # This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries. # commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock #poetry.lock
# pdm # pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock #pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control. # in version control.
# https://pdm.fming.dev/#use-with-ide # https://pdm.fming.dev/#use-with-ide
.pdm.toml .pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/ __pypackages__/
# Celery stuff # Celery stuff
celerybeat-schedule celerybeat-schedule
celerybeat.pid celerybeat.pid
# SageMath parsed files # SageMath parsed files
*.sage.py *.sage.py
# Environments # Environments
.env .env
.venv .venv
env/ env/
venv/ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
.spyproject .spyproject
# Rope project settings # Rope project settings
.ropeproject .ropeproject
# mkdocs documentation # mkdocs documentation
/site /site
# mypy # mypy
.mypy_cache/ .mypy_cache/
.dmypy.json .dmypy.json
dmypy.json dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# pytype static type analyzer # pytype static type analyzer
.pytype/ .pytype/
# Cython debug symbols # Cython debug symbols
cython_debug/ cython_debug/
# PyCharm # PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/ .idea/
*.db

View File

@ -1,3 +1,214 @@
# [](https://github.com/imgyh/tiktok/compare/v1.6.4...v) (2023-05-28)
### Bug Fixes
* **webapi:** 修复作品列表为空时不返回cursor ([1e69ee8](https://github.com/imgyh/tiktok/commit/1e69ee8a140b7761f3ebdebf1b1d9e31af79bd2c)), closes [#40](https://github.com/imgyh/tiktok/issues/40)
### Features
* **douyin:** 修改文件名命名风格 ([b38e210](https://github.com/imgyh/tiktok/commit/b38e2109271a07e408bbbad2454ebc99f48437fe))
# [](https://github.com/imgyh/tiktok/compare/v1.6.3...v) (2023-05-21)
### Features
* **tiktok:** 规范包目录结构 ([942bc35](https://github.com/imgyh/tiktok/commit/942bc359126a0ce9241b44576063bf42b161d02a))
* **tiktok:** 删除多余文件 ([2114bc9](https://github.com/imgyh/tiktok/commit/2114bc925edf8c050834f1ba2f3ddb4be22b6ed3))
# [](https://github.com/imgyh/tiktok/compare/v1.6.2...v) (2023-04-24)
### Bug Fixes
* **tiktok:** 优化视频链接获取 ([321d462](https://github.com/imgyh/tiktok/commit/321d4622a16a26cc8587e16c0e5b9f8601c16f99))
* **web:** 解决浏览器提示不安全问题 ([87b48ae](https://github.com/imgyh/tiktok/commit/87b48aec06bce777c427d03ab02590fd7089bb9b))
### Features
* **tiktok:** 增加数据库功能与增量更新功能 ([ebf6671](https://github.com/imgyh/tiktok/commit/ebf6671d336767595a01941562db397546ab2fe9)), closes [#24](https://github.com/imgyh/tiktok/issues/24)
# [](https://github.com/imgyh/tiktok/compare/v1.6.1...v) (2023-04-21)
### Bug Fixes
* **result:** 修复1080p清晰度链接 ([ada1851](https://github.com/imgyh/tiktok/commit/ada1851e869ae4a65a43fb07eff3c22baeced087))
* **tiktok:** 修复接口失效问题 ([9aaab2f](https://github.com/imgyh/tiktok/commit/9aaab2f691adff5f43c0f40899b1eb37cd4665d5)), closes [#33](https://github.com/imgyh/tiktok/issues/33)
### Features
* **web:** 前段静态文件使用国内cdn ([72aca75](https://github.com/imgyh/tiktok/commit/72aca75960b0f2fe4ef918682debec5c9af8cb59))
# [](https://github.com/imgyh/tiktok/compare/v1.6.0...v) (2023-04-16)
### Bug Fixes
* **command:** 修复mode命令解析问题 ([aebc144](https://github.com/imgyh/tiktok/commit/aebc14429ded01203b2d3b3385d7f1bdcbafae1a))
* **tiktok:** 增加请求重试机制 ([8f6c1ea](https://github.com/imgyh/tiktok/commit/8f6c1ea70dd4da63c772f45f0326047deb3daef7))
* **tiktok:** 更改多作品接口请求逻辑, 不再调用单个作品的方法 ([2ea2ac0](https://github.com/imgyh/tiktok/commit/2ea2ac07a844421d665684e3273c3b6b8e7064a6))
* **tiktok:** 缩短文件名长度,只使用数字字母汉字作为文件名 ([99c6292](https://github.com/imgyh/tiktok/commit/99c62922ca701de2e7eccb68b3c3e67b98d9fcef)), closes [#19](https://github.com/imgyh/tiktok/issues/19)
* **web:** 前端适配新接口 ([2f13bd5](https://github.com/imgyh/tiktok/commit/2f13bd5122b25e507cf55a58aea24396016414da))
### Features
* **command:** 未传入cookie则使用默认值 ([596a0fc](https://github.com/imgyh/tiktok/commit/596a0fc63308c2ea515c305161f115e6914f7504))
* **web:** 增加解析接口 ([918d6c9](https://github.com/imgyh/tiktok/commit/918d6c9ebaa47ce7259fc2f23efbd53b320095a9)), closes [#28](https://github.com/imgyh/tiktok/issues/28)
* **web:** 所有接口支持json与form两种格式, cookie不传使用默认值 ([d3d091d](https://github.com/imgyh/tiktok/commit/d3d091d9ddadf1f01588b7e273b40356ed09cfa2)), closes [#31](https://github.com/imgyh/tiktok/issues/31)
# [](https://github.com/imgyh/tiktok/compare/v1.5.5...v) (2023-03-28)
### Bug Fixes
* **tiktok:** 使用tqdm进度条 ([f55dd00](https://github.com/imgyh/tiktok/commit/f55dd004e9eea039fea04ed9e8e1325bca62c363))
* **tiktok:** 修复主页作品获取失败 ([c5b9ec5](https://github.com/imgyh/tiktok/commit/c5b9ec5faf90c4d00c5cc24b48a774404344be19)), closes [#15](https://github.com/imgyh/tiktok/issues/15)
* **tiktok:** 修复获取单个合集id失败 ([f753c5c](https://github.com/imgyh/tiktok/commit/f753c5c52e4fa54f04f05d555d61b22b544a2169))
* **tiktok:** 修复配置文件路径获取问题 ([2496ccd](https://github.com/imgyh/tiktok/commit/2496ccd90ca5232c61a2ed213f85235fa26354b3))
* **tiktok:** 修改文件名匹配正则表达式 ([8ce13c0](https://github.com/imgyh/tiktok/commit/8ce13c0d72379accbfd38ba083728df87719d471))
* **tiktok:** 增加文件下载失败重试机制 ([794632d](https://github.com/imgyh/tiktok/commit/794632d6c5dce68b9eade8094e74231aa90421f0))
* **tiktok:** 缩短文件名长度 ([01fbd74](https://github.com/imgyh/tiktok/commit/01fbd743f82552e52099637871246ddc21949fc2))
* **tiktok:** 重试失败后返回已经获取到的数据, 而不是抛异常 ([9fc37f1](https://github.com/imgyh/tiktok/commit/9fc37f1048fbd182e00eea89b71ec644f7d9df56))
### Features
* **tiktok:** 使用rich进度条 ([0fb3739](https://github.com/imgyh/tiktok/commit/0fb3739b4734910b6a0d35fdb0033921ef854adb))
* **tiktok:** 单个作品使用多线程下载 ([38fc768](https://github.com/imgyh/tiktok/commit/38fc76826d20257ec63bb7fcdea6eeca38a8aa6d))
* **tiktok:** 增加json数据是否保存的开关 ([8ea8871](https://github.com/imgyh/tiktok/commit/8ea8871cc88b1199bfcb6c2ff7aef16fd1f733c3))
* **tiktok:** 增加配置文件, 手动传入自己的cookie ([ec559e2](https://github.com/imgyh/tiktok/commit/ec559e2913c70836b97ea2634604ac6ca6734a60)), closes [#16](https://github.com/imgyh/tiktok/issues/16)
* **tiktok:** 支持电脑网页版url作为链接 ([c9ece0b](https://github.com/imgyh/tiktok/commit/c9ece0bf502c1a6a6e6b2e12c8ffcbce3303ce6a))
* **tiktok:** 进度条滚动显示 ([20350b8](https://github.com/imgyh/tiktok/commit/20350b8889343bb93ec60081e6369f96d868203b))
# [](https://github.com/imgyh/tiktok/compare/v1.5.4...v) (2023-03-22)
### Bug Fixes
* **tiktok:** 修改命令行帮助提示信息 ([e194a28](https://github.com/imgyh/tiktok/commit/e194a2818b64aaa9b2a5ab172302c87a4f4a5790))
* **tiktok:** 修改文件夹名的正则匹配逻辑 ([4c1a4ed](https://github.com/imgyh/tiktok/commit/4c1a4ed950476c0945bf7986254d75d108d3019f)), closes [#11](https://github.com/imgyh/tiktok/issues/11)
* **tiktok:** 单个作品不使用多线程 ([939a417](https://github.com/imgyh/tiktok/commit/939a417654183a2ac2766a25bdecc13575752e61))
* **tiktok:** 获取x-bogus错误后重试, 单个作品不使用多线程 ([f8ec5e3](https://github.com/imgyh/tiktok/commit/f8ec5e3745d587d08dcba6464a0f990913f55ce5))
* **utils:** 优化获取x-bogus时出错的提示信息 ([f224557](https://github.com/imgyh/tiktok/commit/f2245574611f125f69be9bbe9a2d376c0241bc87))
### Features
* **live:** 直播解析支持APP端分享链接 ([e4a0ebb](https://github.com/imgyh/tiktok/commit/e4a0ebba4f39cc55e8d070e12b6597ab5c3745d3))
* **tiktok:** 使用线程池代替手动创建线程 ([89d4c7c](https://github.com/imgyh/tiktok/commit/89d4c7cd4253cd3ac885b55774dff2853c0d6e4f))
* **web:** 前端提示信息优化 ([d9c224b](https://github.com/imgyh/tiktok/commit/d9c224bd2d5fd382fb22b37f5e68a85894bb9aef))
# [](https://github.com/imgyh/tiktok/compare/v1.5.3...v) (2023-03-16)
### Bug Fixes
* **tiktok:** 修复接口未返回数据时重复请求的死循环(10s后还是未返回就抛异常) ([8c5863d](https://github.com/imgyh/tiktok/commit/8c5863d44f4ae4a5242c8191f98bc0f3936e8e84))
* **tiktok:** 修复无法获取图集id ([85942f2](https://github.com/imgyh/tiktok/commit/85942f2ffce97d853dab96da87737d98f450347e)), closes [#10](https://github.com/imgyh/tiktok/issues/10)
### Features
* **tiktok:** 适配多线程下载 ([83e4632](https://github.com/imgyh/tiktok/commit/83e46322ee13bd14841332538fef22e27e2f0e59))
# [](https://github.com/imgyh/tiktok/compare/v1.5.2...v) (2023-03-05)
### Bug Fixes
* **web:** 取消指定端口为必选项 ([1b85b19](https://github.com/imgyh/tiktok/commit/1b85b1989119cb35888e4e2d4f4018ed276f68d2))
### Features
* **live:** 增加直播解析方法的提示信息开启或关闭选项 ([469fe2c](https://github.com/imgyh/tiktok/commit/469fe2c5217ff22e42c1523d089a119415266c23))
* **url:** 添加速度更快的X-Bogus接口 ([7c44b94](https://github.com/imgyh/tiktok/commit/7c44b9426dc30f3be97d6e5824bf628c47276a87))
* **web:** 增加命令行指定web端口 ([98a9f74](https://github.com/imgyh/tiktok/commit/98a9f74d4b0b319ea1aeb3883c105c0b805105f1))
# [](https://github.com/imgyh/tiktok/compare/v1.5.1...v) (2023-03-04)
### Bug Fixes
* **live:** 修复无法播放http的直播地址 ([04ec0f1](https://github.com/imgyh/tiktok/commit/04ec0f1c400adb5bfacf74dca0114ec9d625e3cc))
### Features
* **live:** 适配web端的直播解析 ([f7fdcd1](https://github.com/imgyh/tiktok/commit/f7fdcd141b3a9877f5fd888383bfa48875d023bf))
* **tiktok:** 支持本地生成X-Bogus ([#3](https://github.com/imgyh/tiktok/issues/3)) ([a4328cd](https://github.com/imgyh/tiktok/commit/a4328cd53bd8a0342cf053050a8066130e008cde))
* **utils:** 重新加入远程调用X-Bogus接口作为备用,防止本地没有JS环境 ([4467018](https://github.com/imgyh/tiktok/commit/44670186afdcb1314194b0c00f39d1baa9681985))
* **web:** 增加web端直播解析 ([90aa105](https://github.com/imgyh/tiktok/commit/90aa10515f7bc90ed35c9484e2993083a533d6cc))
# [](https://github.com/imgyh/tiktok/compare/v1.5.0...v) (2023-03-02)
### Bug Fixes
* **tiktok:** cookies增加passport_csrf_token ([ebe3be4](https://github.com/imgyh/tiktok/commit/ebe3be43ef676c39cd1cd63cc606df1a9e5b1995))
# (2023-03-02)
### Bug Fixes
* **command:** 修复头像封面音乐总是会下载 ([665ca47](https://github.com/imgyh/tiktok/commit/665ca47b08623699606d56e424eb096a92afa9fe))
* **tiktok:** 自动获取ttwid ([c6bcff6](https://github.com/imgyh/tiktok/commit/c6bcff67da8a659afb1c722ab40da733f1a79403))
### Features
* **tiktok:** 增加下载前n个作品功能 ([4efeb62](https://github.com/imgyh/tiktok/commit/4efeb62701acc6cf8d9fd06d1a80499a7ad5c6cc))
* **tiktok:** 增加音乐合集批量下载功能 ([b3aadf6](https://github.com/imgyh/tiktok/commit/b3aadf630ad8be9b79fa26a18799336c38569645))
* **url:** 增加音乐合集URL ([fa635eb](https://github.com/imgyh/tiktok/commit/fa635ebe7f70478e8408b5a8afe30ff0b1ff890f))
# (2023-02-23)
### Bug Fixes
* **live:** 统一直播获取的header, 直播接口添加X-Bogus ([197d126](https://github.com/imgyh/tiktok/commit/197d12627d855f3353dba3fd68f0b308593f62e8))
* **tiktok:** 修复主页作品接口 ([e3029be](https://github.com/imgyh/tiktok/commit/e3029be42b021dcdad0736800a4f13428ddd5b98))
* **tiktok:** 修复用户主页作品接口 ([8a01e68](https://github.com/imgyh/tiktok/commit/8a01e681b5206c27a44f4ba10f840e856686e33b))
* **tiktok:** 添加单个作品获取失败后重试逻辑 ([ac73a97](https://github.com/imgyh/tiktok/commit/ac73a97c19840bd7147f3a7e4b400a37b1365fb2))
### Features
* **tiktok:** 增加合集下载功能 ([b95d718](https://github.com/imgyh/tiktok/commit/b95d7188282de5474861043ab011ed27baa79796))
# (2023-02-20) # (2023-02-20)

View File

@ -1,13 +1,16 @@
# This Dockerfile is used to build an Python environment # This Dockerfile is used to build an Python environment
FROM python:3.9-slim-bullseye FROM python:3.9-slim-bullseye
LABEL maintainer="imgyh<admin@imgyh.com>" LABEL maintainer="imgyh<admin@imgyh.com>"
WORKDIR /app WORKDIR /app
ADD . $WORKDIR ADD . $WORKDIR
RUN pip3 install -r requirements.txt RUN sed -i s/deb.debian.org/mirrors.aliyun.com/g /etc/apt/sources.list
CMD ["python3", "TikTokWeb.py"] RUN pip3 install -r requirements_docker.txt
ENV TZ=Asia/Shanghai
CMD ["python3", "WebApi.py"]

376
DouYinCommand.py Normal file
View File

@ -0,0 +1,376 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : DouYinCommand.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 16:01
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import argparse
import os
import sys
import json
import yaml
import time
from apiproxy.douyin.douyin import Douyin
from apiproxy.douyin.download import Download
from apiproxy.douyin import douyin_headers
from apiproxy.common import utils
configModel = {
"link": [],
"path": os.getcwd(),
"music": True,
"cover": True,
"avatar": True,
"json": True,
"folderstyle": True,
"mode": ["post"],
"number": {
"post": 0,
"like": 0,
"allmix": 0,
"mix": 0,
"music": 0,
},
'database': True,
"increase": {
"post": False,
"like": False,
"allmix": False,
"mix": False,
"music": False,
},
"thread": 5,
"cookie": None
}
def argument():
parser = argparse.ArgumentParser(description='抖音批量下载工具 使用帮助')
parser.add_argument("--cmd", "-C", help="使用命令行(True)或者配置文件(False), 默认为False",
type=utils.str2bool, required=False, default=False)
parser.add_argument("--link", "-l",
help="作品(视频或图集)、直播、合集、音乐集合、个人主页的分享链接或者电脑浏览器网址, 可以设置多个链接(删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/ 或者 https://www.douyin.com/开头的)",
type=str, required=False, default=[], action="append")
parser.add_argument("--path", "-p", help="下载保存位置, 默认当前文件位置",
type=str, required=False, default=os.getcwd())
parser.add_argument("--music", "-m", help="是否下载视频中的音乐(True/False), 默认为True",
type=utils.str2bool, required=False, default=True)
parser.add_argument("--cover", "-c", help="是否下载视频的封面(True/False), 默认为True, 当下载视频时有效",
type=utils.str2bool, required=False, default=True)
parser.add_argument("--avatar", "-a", help="是否下载作者的头像(True/False), 默认为True",
type=utils.str2bool, required=False, default=True)
parser.add_argument("--json", "-j", help="是否保存获取到的数据(True/False), 默认为True",
type=utils.str2bool, required=False, default=True)
parser.add_argument("--folderstyle", "-fs", help="文件保存风格, 默认为True",
type=utils.str2bool, required=False, default=True)
parser.add_argument("--mode", "-M", help="link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like)或者用户所有合集(mix), 默认为post, 可以设置多种模式",
type=str, required=False, default=[], action="append")
parser.add_argument("--postnumber", help="主页下作品下载个数设置, 默认为0 全部下载",
type=int, required=False, default=0)
parser.add_argument("--likenumber", help="主页下喜欢下载个数设置, 默认为0 全部下载",
type=int, required=False, default=0)
parser.add_argument("--allmixnumber", help="主页下合集下载个数设置, 默认为0 全部下载",
type=int, required=False, default=0)
parser.add_argument("--mixnumber", help="单个合集下作品下载个数设置, 默认为0 全部下载",
type=int, required=False, default=0)
parser.add_argument("--musicnumber", help="音乐(原声)下作品下载个数设置, 默认为0 全部下载",
type=int, required=False, default=0)
parser.add_argument("--database", "-d", help="是否使用数据库, 默认为True 使用数据库; 如果不使用数据库, 增量更新不可用",
type=utils.str2bool, required=False, default=True)
parser.add_argument("--postincrease", help="是否开启主页作品增量下载(True/False), 默认为False",
type=utils.str2bool, required=False, default=False)
parser.add_argument("--likeincrease", help="是否开启主页喜欢增量下载(True/False), 默认为False",
type=utils.str2bool, required=False, default=False)
parser.add_argument("--allmixincrease", help="是否开启主页合集增量下载(True/False), 默认为False",
type=utils.str2bool, required=False, default=False)
parser.add_argument("--mixincrease", help="是否开启单个合集下作品增量下载(True/False), 默认为False",
type=utils.str2bool, required=False, default=False)
parser.add_argument("--musicincrease", help="是否开启音乐(原声)下作品增量下载(True/False), 默认为False",
type=utils.str2bool, required=False, default=False)
parser.add_argument("--thread", "-t",
help="设置线程数, 默认5个线程",
type=int, required=False, default=5)
parser.add_argument("--cookie", help="设置cookie, 格式: \"name1=value1; name2=value2;\" 注意要加冒号",
type=str, required=False, default='')
args = parser.parse_args()
if args.thread <= 0:
args.thread = 5
return args
def yamlConfig():
curPath = os.path.dirname(os.path.realpath(sys.argv[0]))
yamlPath = os.path.join(curPath, "config.yml")
f = open(yamlPath, 'r', encoding='utf-8')
cfg = f.read()
configDict = yaml.load(stream=cfg, Loader=yaml.FullLoader)
try:
if configDict["link"] != None:
configModel["link"] = configDict["link"]
except Exception as e:
print("[ 警告 ]:link未设置, 程序退出...\r\n")
try:
if configDict["path"] != None:
configModel["path"] = configDict["path"]
except Exception as e:
print("[ 警告 ]:path未设置, 使用当前路径...\r\n")
try:
if configDict["music"] != None:
configModel["music"] = configDict["music"]
except Exception as e:
print("[ 警告 ]:music未设置, 使用默认值True...\r\n")
try:
if configDict["cover"] != None:
configModel["cover"] = configDict["cover"]
except Exception as e:
print("[ 警告 ]:cover未设置, 使用默认值True...\r\n")
try:
if configDict["avatar"] != None:
configModel["avatar"] = configDict["avatar"]
except Exception as e:
print("[ 警告 ]:avatar未设置, 使用默认值True...\r\n")
try:
if configDict["json"] != None:
configModel["json"] = configDict["json"]
except Exception as e:
print("[ 警告 ]:json未设置, 使用默认值True...\r\n")
try:
if configDict["folderstyle"] != None:
configModel["folderstyle"] = configDict["folderstyle"]
except Exception as e:
print("[ 警告 ]:folderstyle未设置, 使用默认值True...\r\n")
try:
if configDict["mode"] != None:
configModel["mode"] = configDict["mode"]
except Exception as e:
print("[ 警告 ]:mode未设置, 使用默认值post...\r\n")
try:
if configDict["number"]["post"] != None:
configModel["number"]["post"] = configDict["number"]["post"]
except Exception as e:
print("[ 警告 ]:post number未设置, 使用默认值0...\r\n")
try:
if configDict["number"]["like"] != None:
configModel["number"]["like"] = configDict["number"]["like"]
except Exception as e:
print("[ 警告 ]:like number未设置, 使用默认值0...\r\n")
try:
if configDict["number"]["allmix"] != None:
configModel["number"]["allmix"] = configDict["number"]["allmix"]
except Exception as e:
print("[ 警告 ]:allmix number未设置, 使用默认值0...\r\n")
try:
if configDict["number"]["mix"] != None:
configModel["number"]["mix"] = configDict["number"]["mix"]
except Exception as e:
print("[ 警告 ]:mix number未设置, 使用默认值0...\r\n")
try:
if configDict["number"]["music"] != None:
configModel["number"]["music"] = configDict["number"]["music"]
except Exception as e:
print("[ 警告 ]:music number未设置, 使用默认值0...\r\n")
try:
if configDict["database"] != None:
configModel["database"] = configDict["database"]
except Exception as e:
print("[ 警告 ]:database未设置, 使用默认值False...\r\n")
try:
if configDict["increase"]["post"] != None:
configModel["increase"]["post"] = configDict["increase"]["post"]
except Exception as e:
print("[ 警告 ]:post 增量更新未设置, 使用默认值False...\r\n")
try:
if configDict["increase"]["like"] != None:
configModel["increase"]["like"] = configDict["increase"]["like"]
except Exception as e:
print("[ 警告 ]:like 增量更新未设置, 使用默认值False...\r\n")
try:
if configDict["increase"]["allmix"] != None:
configModel["increase"]["allmix"] = configDict["increase"]["allmix"]
except Exception as e:
print("[ 警告 ]:allmix 增量更新未设置, 使用默认值False...\r\n")
try:
if configDict["increase"]["mix"] != None:
configModel["increase"]["mix"] = configDict["increase"]["mix"]
except Exception as e:
print("[ 警告 ]:mix 增量更新未设置, 使用默认值False...\r\n")
try:
if configDict["increase"]["music"] != None:
configModel["increase"]["music"] = configDict["increase"]["music"]
except Exception as e:
print("[ 警告 ]:music 增量更新未设置, 使用默认值False...\r\n")
try:
if configDict["thread"] != None:
configModel["thread"] = configDict["thread"]
except Exception as e:
print("[ 警告 ]:thread未设置, 使用默认值5...\r\n")
try:
if configDict["cookies"] != None:
cookiekey = configDict["cookies"].keys()
cookieStr = ""
for i in cookiekey:
cookieStr = cookieStr + i + "=" + configDict["cookies"][i] + "; "
configModel["cookie"] = cookieStr
except Exception as e:
pass
try:
if configDict["cookie"] != None:
configModel["cookie"] = configDict["cookie"]
except Exception as e:
pass
def main():
start = time.time() # 开始时间
args = argument()
if args.cmd:
configModel["link"] = args.link
configModel["path"] = args.path
configModel["music"] = args.music
configModel["cover"] = args.cover
configModel["avatar"] = args.avatar
configModel["json"] = args.json
configModel["folderstyle"] = args.folderstyle
if args.mode == None or args.mode == []:
args.mode = []
args.mode.append("post")
configModel["mode"] = list(set(args.mode))
configModel["number"]["post"] = args.postnumber
configModel["number"]["like"] = args.likenumber
configModel["number"]["allmix"] = args.allmixnumber
configModel["number"]["mix"] = args.mixnumber
configModel["number"]["music"] = args.musicnumber
configModel["database"] = args.database
configModel["increase"]["post"] = args.postincrease
configModel["increase"]["like"] = args.likeincrease
configModel["increase"]["allmix"] = args.allmixincrease
configModel["increase"]["mix"] = args.mixincrease
configModel["increase"]["music"] = args.musicincrease
configModel["thread"] = args.thread
configModel["cookie"] = args.cookie
else:
yamlConfig()
if configModel["link"] == []:
return
if configModel["cookie"] is not None and configModel["cookie"] != "":
douyin_headers["Cookie"] = configModel["cookie"]
configModel["path"] = os.path.abspath(configModel["path"])
print("[ 提示 ]:数据保存路径 " + configModel["path"])
if not os.path.exists(configModel["path"]):
os.mkdir(configModel["path"])
dy = Douyin(database=configModel["database"])
dl = Download(thread=configModel["thread"], music=configModel["music"], cover=configModel["cover"],
avatar=configModel["avatar"], resjson=configModel["json"],
folderstyle=configModel["folderstyle"])
for link in configModel["link"]:
print("--------------------------------------------------------------------------------")
print("[ 提示 ]:正在请求的链接: " + link + "\r\n")
url = dy.getShareLink(link)
key_type, key = dy.getKey(url)
if key_type == "user":
print("[ 提示 ]:正在请求用户主页下作品\r\n")
data = dy.getUserDetailInfo(sec_uid=key)
nickname = ""
if data is not None and data != {}:
nickname = utils.replaceStr(data['user']['nickname'])
userPath = os.path.join(configModel["path"], "user_" + nickname + "_" + key)
if not os.path.exists(userPath):
os.mkdir(userPath)
for mode in configModel["mode"]:
print("--------------------------------------------------------------------------------")
print("[ 提示 ]:正在请求用户主页模式: " + mode + "\r\n")
if mode == 'post' or mode == 'like':
datalist = dy.getUserInfo(key, mode, 35, configModel["number"][mode], configModel["increase"][mode])
if datalist is not None and datalist != []:
modePath = os.path.join(userPath, mode)
if not os.path.exists(modePath):
os.mkdir(modePath)
dl.userDownload(awemeList=datalist, savePath=modePath)
elif mode == 'mix':
mixIdNameDict = dy.getUserAllMixInfo(key, 35, configModel["number"]["allmix"])
if mixIdNameDict is not None and mixIdNameDict != {}:
for mix_id in mixIdNameDict:
print(f'[ 提示 ]:正在下载合集 [{mixIdNameDict[mix_id]}] 中的作品\r\n')
mix_file_name = utils.replaceStr(mixIdNameDict[mix_id])
datalist = dy.getMixInfo(mix_id, 35, 0, configModel["increase"]["allmix"], key)
if datalist is not None and datalist != []:
modePath = os.path.join(userPath, mode)
if not os.path.exists(modePath):
os.mkdir(modePath)
dl.userDownload(awemeList=datalist, savePath=os.path.join(modePath, mix_file_name))
print(f'[ 提示 ]:合集 [{mixIdNameDict[mix_id]}] 中的作品下载完成\r\n')
elif key_type == "mix":
print("[ 提示 ]:正在请求单个合集下作品\r\n")
datalist = dy.getMixInfo(key, 35, configModel["number"]["mix"], configModel["increase"]["mix"], "")
if datalist is not None and datalist != []:
mixname = utils.replaceStr(datalist[0]["mix_info"]["mix_name"])
mixPath = os.path.join(configModel["path"], "mix_" + mixname + "_" + key)
if not os.path.exists(mixPath):
os.mkdir(mixPath)
dl.userDownload(awemeList=datalist, savePath=mixPath)
elif key_type == "music":
print("[ 提示 ]:正在请求音乐(原声)下作品\r\n")
datalist = dy.getMusicInfo(key, 35, configModel["number"]["music"], configModel["increase"]["music"])
if datalist is not None and datalist != []:
musicname = utils.replaceStr(datalist[0]["music"]["title"])
musicPath = os.path.join(configModel["path"], "music_" + musicname + "_" + key)
if not os.path.exists(musicPath):
os.mkdir(musicPath)
dl.userDownload(awemeList=datalist, savePath=musicPath)
elif key_type == "aweme":
print("[ 提示 ]:正在请求单个作品\r\n")
datanew, dataraw = dy.getAwemeInfo(key)
if datanew is not None and datanew != {}:
datalist = []
datalist.append(datanew)
awemePath = os.path.join(configModel["path"], "aweme")
if not os.path.exists(awemePath):
os.mkdir(awemePath)
dl.userDownload(awemeList=datalist, savePath=awemePath)
elif key_type == "live":
print("[ 提示 ]:正在进行直播解析\r\n")
live_json = dy.getLiveInfo(key)
if configModel["json"]:
livePath = os.path.join(configModel["path"], "live")
if not os.path.exists(livePath):
os.mkdir(livePath)
live_file_name = utils.replaceStr(key + live_json["nickname"])
# 保存获取到json
print("[ 提示 ]:正在保存获取到的信息到result.json\r\n")
with open(os.path.join(livePath, live_file_name + ".json"), "w", encoding='utf-8') as f:
f.write(json.dumps(live_json, ensure_ascii=False, indent=2))
f.close()
end = time.time() # 结束时间
print('\n' + '[下载完成]:总耗时: %d分钟%d\n' % (int((end - start) / 60), ((end - start) % 60))) # 输出下载用时时间
if __name__ == "__main__":
main()

View File

@ -83,6 +83,7 @@ class TikTok(object):
userVideoUrls.append(videoRealUrl) userVideoUrls.append(videoRealUrl)
return userVideoUrls return userVideoUrls
tk = TikTok() tk = TikTok()
# tk.oneVideoInfo() # tk.oneVideoInfo()
tk.userVideoInfo() tk.userVideoInfo()

467
README.md
View File

@ -8,41 +8,76 @@
开源地址https://github.com/imgyh/tiktok 开源地址https://github.com/imgyh/tiktok
博客文档https://www.imgyh.com/archives/41.html
抖音去水印工具Web demohttps://dy.gyh.im/ 抖音去水印工具Web demohttps://dy.gyh.im/
**联系方式:**
> [TG](https://t.me/gyh9527)
> [TG群组](https://t.me/GYHgroup)
> [Email](mailto:admin@imgyh.com)
> [Blog](https://www.imgyh.com)
## 抖音去水印工具 Feature ## 抖音去水印工具 Feature
* 通过作品分享链接获取去水印作品、音乐、封面图、头像 * 通过作品分享链接获取去水印作品、音乐、封面图、头像
* 获取点赞数、评论数、收藏数、分享数、作品描述等信息 * 获取点赞数、评论数、收藏数、分享数、作品描述等信息
* 支持直播解析
* 基于Flask实现 Web 交互界面 * 基于Flask实现 Web 交互界面
* 提供相关接口,支持单个作品、直播、主页喜欢、主页作品、主页合集、合集、音乐(原声)通过接口获取
![tiktokweb](img/tiktokweb.jpg) ![WebApi](img/WebApi.jpg)
![tiktokweb video](img/tiktokwebvideo.jpg) ![WebApi video](img/WebApivideo.jpg)
![tiktokweb preview video](img/tiktokwebpreviewvideo.jpg) ![WebApi preview video](img/WebApipreviewvideo.jpg)
![tiktokweb image](img/tiktokwebimage.jpg) ![WebApi image](img/WebApiimage.jpg)
![tiktokweb preview image](img/tiktokwebpreviewimage.jpg) ![WebApi preview image](img/WebApipreviewimage.jpg)
## 抖音批量下载工具 Feature ## 抖音批量下载工具 Feature
* 支持个人主页链接、作品分享链接、抖音直播Web链接 * 支持个人主页链接、作品分享链接、抖音直播Web链接、合集链接、音乐(原声)集合链接
* 支持单个作品下载、主页作品下载、主页喜欢下载 * 支持单个作品下载、主页作品下载、主页喜欢下载、直播解析、单个合集下载、主页所有合集下载、音乐(原声)集合下载
* 下载视频、视频封面、音乐、头像 * 下载视频、视频封面、音乐、头像
* 去水印下载 * 去水印下载
* 自动跳过已下载 * 自动跳过已下载
* 支持指定下载作品数量
* 多线程下载
* 支持多链接下载
* 增量更新与数据持久化到数据库, 保存每条作品信息到数据库, 并根据数据库是否存在来增量请求下载
![](img/tiktokcommand1.jpg) ![DouYinCommand1](img/DouYinCommand1.jpg)
![](img/tiktokcommand2.jpg) ![DouYinCommand2](img/DouYinCommand2.jpg)
![tiktokcommandl ive](img/tiktokcommandlive.jpg) ![DouYinCommandl ive](img/DouYinCommandlive.jpg)
![tiktokcommand download](img/tiktokcommanddownload.jpg) ![DouYinCommand download](img/DouYinCommanddownload.jpg)
![tiktokcommand download detail](img/tiktokcommanddownloaddetail.jpg) ![DouYinCommand download detail](img/DouYinCommanddownloaddetail.jpg)
# 使用方法 # 使用方法
- 支持的地址格式, 形如
```
抖音app分享链接:
1. 作品(视频或图集)、直播、合集、音乐集合、个人主页 https://v.douyin.com/BugmVVD/
抖音网页版浏览器URL:
2. 单个视频 https://www.douyin.com/video/6915675899241450760
3. 单个图集 https://www.douyin.com/note/7014363562642623777
4. 用户主页 https://www.douyin.com/user/MS4wLjABAAAA06y3Ctu8QmuefqvUSU7vr0c_ZQnCqB0eaglgkelLTek
5. 单个合集 https://www.douyin.com/collection/7208829743762769975
6. 音乐(原声)下的视频 https://www.douyin.com/music/7149936801028131598
7. 直播 https://live.douyin.com/759547612580
```
## 抖音去水印工具 ## 抖音去水印工具
使用抖音去水印工具有三种方式 ### 使用方式
使用抖音去水印工具有4种方式
1. (推荐)直接使用我搭建的抖音去水印工具https://dy.gyh.im/ 1. (推荐)直接使用我搭建的抖音去水印工具https://dy.gyh.im/
2. 使用docker运行 2. 使用docker运行
``` ```
@ -54,14 +89,33 @@ docker run -d -p 5000:5000 --name tiktok --restart=always imgyh/tiktokweb
``` ```
cd /path/to/tiktok cd /path/to/tiktok
python -m pip install -r requirements.txt python -m pip install -r requirements.txt
python TikTokWeb.py python WebApi.py
```
4. windows用户也可以下载 Releases 中的 [WebApi.exe](https://github.com/imgyh/tiktok/releases) 文件双击运行
5. 指定端口运行
```
# 指定端口运行
python WebApi.py -p 5001
.\WebApi.exe -p 5001
``` ```
访问: http://localhost:5000 访问: http://localhost:5000
## 抖音批量下载工具 ## 抖音批量下载工具
windows用户下载 Releases 中的 [TikTokCommand.exe](https://github.com/imgyh/tiktok/releases) 文件在cmd中运行 批量下载有两种方式运行, 配置文件和命令行
默认使用配置文件方式
> !!!!!! 请先获取cookie再使用
### 安装依赖
windows用户下载 Releases 中的 [DouYinCommand.exe](https://github.com/imgyh/tiktok/releases) 文件运行
windows用户本地有`python3.9`环境, 也可按照linux与mac用户的方式运行 windows用户本地有`python3.9`环境, 也可按照linux与mac用户的方式运行
linux与mac用户下载本项目, 在本地`python3.9`环境中运行, 首先需要安装依赖, 安装命令 linux与mac用户下载本项目, 在本地`python3.9`环境中运行, 首先需要安装依赖, 安装命令
@ -71,92 +125,417 @@ cd /path/to/tiktok
python -m pip install -r requirements.txt python -m pip install -r requirements.txt
``` ```
运行示例: ### 使用Docker
> !!!!!! 请先获取cookie再使用
请映射以下两个目录(三个位置需要修改), 根据实际情况修改目录地址
`/path/to/tiktok` 源代码目录
`/path/to/downloads` 下载位置
```
docker run -d -p 5000:5000 --name tiktok --restart=always -v /path/to/tiktok:/app -v /path/to/downloads:/path/to/downloads imgyh/tiktokweb
```
将所有用到 `python DouYinCommand.py` 替换成 `docker exec -it tiktok python3 DouYinCommand.py`
### 配置文件方式
> !!!!!! 请自己登录网页版抖音后F12获取cookie
> !!!!!! 请仔细阅读配置示例[config.yml](./config.yml)中的描述
配置文件名必须叫 `config.yml`, 并将其放在DouYinCommand.py或者DouYinCommand.exe同一个目录下
直接运行DouYinCommand.py或者DouYinCommand.exe, 无需在命令中加入任何参数, 所有参数都从配置文件中读取
基本配置示例[config.yml](./config.yml)
### 命令行方式
> 以下为命令行的一些信息, 每个选项的详细说明参见配置示例[config.yml](./config.yml)中的描述
- 获取帮助信息 - 获取帮助信息
``` ```
windows用户: windows用户:
.\TikTokCommand.exe -h .\DouYinCommand.exe -h
linux与mac用户: linux与mac用户:
python TikTokCommand.py -h python DouYinCommand.py -h
``` ```
- 参数介绍 - 参数介绍
``` ```
-h, --help 展示帮助页 -h, --help 展示帮助信息
--link LINK, -l LINK 1.作品(视频或图集)与个人主页抖音分享链接(删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/) --cmd CMD, -C CMD 使用命令行(True)或者配置文件(False), 默认为False
2.解析直播网页版网址(https://live.douyin.com/802939216127) --link LINK, -l LINK 作品(视频或图集)、直播、合集、音乐集合、个人主页的分享链接或者电脑浏览器网址, 可以设置多个链接
--path PATH, -p PATH 下载保存位置 (删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/ 或者 https://www.douyin.com/开头的)
--music MUSIC, -m MUSIC 是否下载视频中的音乐(True/False), 默认为True --path PATH, -p PATH 下载保存位置, 默认当前文件位置
--cover COVER, -c COVER 是否下载视频的封面(True/False), 默认为True, 当下载视频时有效 --music MUSIC, -m MUSIC 是否下载视频中的音乐(True/False), 默认为True
--avatar AVATAR, -a AVATAR 是否下载作者的头像(True/False), 默认为True --cover COVER, -c COVER 是否下载视频的封面(True/False), 默认为True, 当下载视频时有效
--mode MODE, -M MODE link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like), 默认为post --avatar AVATAR, -a AVATAR 是否下载作者的头像(True/False), 默认为True
--json JSON, -j JSON 是否保存获取到的数据(True/False), 默认为True
--folderstyle FOLDERSTYLE, -fs FOLDERSTYLE 文件保存风格, 默认为True
--mode MODE, -M MODE link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like)或者用户所有合集(mix), 默认为post, 可以设置多种模式
--postnumber POSTNUMBER 主页下作品下载个数设置, 默认为0 全部下载
--likenumber LIKENUMBER 主页下喜欢下载个数设置, 默认为0 全部下载
--allmixnumber ALLMIXNUMBER 主页下合集下载个数设置, 默认为0 全部下载
--mixnumber MIXNUMBER 单个合集下作品下载个数设置, 默认为0 全部下载
--musicnumber MUSICNUMBER 音乐(原声)下作品下载个数设置, 默认为0 全部下载
--database DATABASE, -d DATABASE 是否使用数据库, 默认为True 使用数据库; 如果不使用数据库, 增量更新不可用
--postincrease POSTINCREASE 是否开启主页作品增量下载(True/False), 默认为False
--likeincrease LIKEINCREASE 是否开启主页喜欢增量下载(True/False), 默认为False
--allmixincrease ALLMIXINCREASE 是否开启主页合集增量下载(True/False), 默认为False
--mixincrease MIXINCREASE 是否开启单个合集下作品增量下载(True/False), 默认为False
--musicincrease MUSICINCREASE 是否开启音乐(原声)下作品增量下载(True/False), 默认为False
--thread THREAD, -t THREAD 设置线程数, 默认5个线程
--cookie COOKIE 设置cookie, 格式: "name1=value1; name2=value2;" 注意要加冒号
```
- 多链接多模式混合下载, 可以传入多个链接和多个模式(post、like、mix)
```
windows用户:
.\DouYinCommand.exe -C True `
-l https://live.douyin.com/759547612580 `
-l https://v.douyin.com/BugmVVD/ `
-l https://v.douyin.com/BugrFTN/ `
-l https://v.douyin.com/B72pdU5/ `
-l https://v.douyin.com/B72QgDw/ `
-l https://v.douyin.com/AJp8D3f/ `
-l https://v.douyin.com/B38oovu/ `
-l https://v.douyin.com/S6YMNXs/ `
-p C:\project\test `
-M post `
-M like `
-M mix `
--postnumber 5 `
--likenumber 5 `
--allmixnumber 1 `
--mixnumber 5 `
--musicnumber 5 `
--cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True \
-l https://live.douyin.com/759547612580 \
-l https://v.douyin.com/BugmVVD/ \
-l https://v.douyin.com/BugrFTN/ \
-l https://v.douyin.com/B72pdU5/ \
-l https://v.douyin.com/B72QgDw/ \
-l https://v.douyin.com/AJp8D3f/ \
-l https://v.douyin.com/B38oovu/ \
-l https://v.douyin.com/S6YMNXs/ \
-p /path/to/downdir \
-M post \
-M like \
-M mix \
--postnumber 5 \
--likenumber 5 \
--allmixnumber 1 \
--mixnumber 5 \
--musicnumber 5 \
--cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
``` ```
- 下载单个作品 - 下载单个作品
``` ```
windows用户: windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/kcvMpuN/ -p C:\project\test .\DouYinCommand.exe -C True -l https://v.douyin.com/kcvMpuN/ -p C:\project\test --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户: linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/kcvMpuN/ -p /path/to/downdir python DouYinCommand.py -C True -l https://v.douyin.com/kcvMpuN/ -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
``` ```
- 下载主页全部作品 - 下载主页全部作品
``` ```
windows用户: windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/kcvSCe9/ -p C:\project\test .\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户: linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
``` ```
- 下载主页全部喜欢 - 增量更新主页下作品(postincrease 选项)
``` ```
windows用户: windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -M like .\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test --postincrease True --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户: linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -M like python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir --postincrease True --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
``` ```
- 关闭头像下载, cover, music 也是一样的设置对应选项为 False - 关闭数据库, 增量更新不可用(database 选项)
``` ```
windows用户: windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -a False .\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test --database False --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户: linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -a False python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir --database False --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 所有视频在一个文件夹下(folderstyle 选项)
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test --folderstyle False --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir --folderstyle False --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页前n个作品(postnumber 选项)
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test --postnumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir --postnumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页全部喜欢(-M like 选项)
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -M like --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -M like --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页前n个喜欢(-M like --likenumber 选项)
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -M like --likenumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -M like --likenumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载单个合集全部作品
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/B3J63Le/ -p C:\project\test --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/B3J63Le/ -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载单个合集前n个作品(--mixnumber 选项)
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/B3J63Le/ -p C:\project\test --mixnumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/B3J63Le/ -p /path/to/downdir --mixnumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页全部合集下所有作品(-M mix 选项)
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/B38oovu/ -p C:\project\test -M mix --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/B38oovu/ -p /path/to/downdir -M mix --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页前n个合集下所有作品(-M mix --allmixnumber 选项)
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/B38oovu/ -p C:\project\test -M mix --allmixnumber 2 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/B38oovu/ -p /path/to/downdir -M mix --allmixnumber 2 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载音乐(原声)集合下所有作品
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/S6YMNXs/ -p C:\project\test --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/S6YMNXs/ -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载音乐(原声)集合下前n个作品(--musicnumber 选项)
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/S6YMNXs/ -p C:\project\test --musicnumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/S6YMNXs/ -p /path/to/downdir --musicnumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 关闭头像下载, cover, music json数据也是一样的设置对应选项为 False
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -a False --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -a False --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 多线程设置, 默认5个线程, 可以自己调节线程数
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -t 8 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -t 8 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
``` ```
- 直播推流地址解析 - 直播推流地址解析
``` ```
windows用户: windows用户:
.\TikTokCommand.exe -l https://live.douyin.com/802939216127 -p /path/to/downdir .\DouYinCommand.exe -C True -l https://live.douyin.com/802939216127 -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
或者
.\DouYinCommand.exe -C True -l https://v.douyin.com/SnXMoh2/ -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户: linux与mac用户:
python TikTokCommand.py -l https://live.douyin.com/802939216127 -p /path/to/downdir python DouYinCommand.py -C True -l https://live.douyin.com/802939216127 -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
或者
python DouYinCommand.py -C True -l https://v.douyin.com/SnXMoh2/ -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
``` ```
# Web版接口
> !!!!!! 请传入cookie再使用
1. 单个作品、图集接口
```
接口地址: 127.0.0.1:5000/douyin/aweme
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://v.douyin.com/kcvMpuN/",
"cookie":"xxxx"
}
```
2. 直播解析接口
```
接口地址: 127.0.0.1:5000/douyin/live
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://v.douyin.com/DdWaSBd/",
"cookie":"xxxx"
}
```
3. 主页作品
```
接口地址: 127.0.0.1:5000/douyin/user/post
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://v.douyin.com/B72pdU5/",
"cursor":0,
"cookie":"xxxx"
}
```
4. 主页喜欢
```
接口地址: 127.0.0.1:5000/douyin/user/like
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://v.douyin.com/AoWVvYH/",
"cursor":0,
"cookie":"xxxx"
}
```
5. 主页合集
```
接口地址: 127.0.0.1:5000/douyin/user/mix
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://v.douyin.com/B38oovu/",
"cursor":0,
"cookie":"xxxx"
}
```
6. 单个合集
```
接口地址: 127.0.0.1:5000/douyin/mix
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://www.douyin.com/collection/7217644759668492345", // https://v.douyin.com 这种类型也可以
"cursor":0,
"cookie":"xxxx"
}
```
7. 音乐(原声)
```
接口地址: 127.0.0.1:5000/douyin/music
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://v.douyin.com/S6YMNXs/",
"cursor":0,
"cookie":"xxxx"
}
```
# ToDo # ToDo
- [ ] 获取分享的音乐链接下的所有作品 - [x] 单个合集下载
- [x] 主页所有合集下载
- [x] 获取分享的音乐(原声)链接下的所有作品
- [x] 指定下载作品数量
- [ ] 获取热搜榜数据 - [ ] 获取热搜榜数据
- [ ] 多主页链接批量下载 - [x] 多链接批量下载
- [ ] 多线程下载 - [x] 多线程下载
- [ ] 保存数据至数据库 - [x] 保存数据至数据库
- [ ] 制作成接口 - [x] 制作成接口
- [ ] 获取收藏与观看历史
- [ ] 直播间数据
# 鸣谢 # 鸣谢
本项目部分思路来自[TikTokDownload](https://github.com/Johnserf-Seed/TikTokDownload) 本项目部分思路来自[TikTokDownload](https://github.com/Johnserf-Seed/TikTokDownload)
# 赞赏
## 支付宝
![alipay](./img/alipay.jpg)
## 微信
![wechat](./img/wechat.jpg)
# 申明 # 申明
本项目只作为学习用途, 切勿他用 本项目只作为学习用途, 切勿他用
# License # License
[MIT](https://opensource.org/licenses/MIT) © [imgyh](https://www.imgyh.com/) [MIT](https://opensource.org/licenses/MIT) © [imgyh](https://www.imgyh.com/)
# Star History
[![Star History Chart](https://api.star-history.com/svg?repos=imgyh/tiktok&type=Date)](https://star-history.com/#imgyh/tiktok&Date)

396
TikTok.py
View File

@ -1,396 +0,0 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Description:TikTok.py
@Date :2023/01/27 19:36:18
@Author :imgyh
@version :1.0
@Github :https://github.com/imgyh
@Mail :admin@imgyh.com
-------------------------------------------------
Change Log : 2023/02/11 修改接口
-------------------------------------------------
'''
import re
import requests
import json
import time
import os
import copy
from TikTokUtils import Utils
from TikTokUrls import Urls
from TikTokResult import Result
class TikTok(object):
def __init__(self):
self.urls = Urls()
self.utils = Utils()
self.result = Result()
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
'referer': 'https://www.douyin.com/',
'Cookie': '__ac_nonce=063f2dac800532463833e; s_v_web_id=verify_lec77mky_PdkXTRSx_VwYB_4B32_BT52_CM9JP3QLtWnX; msToken=%s;odin_tt=324fb4ea4a89c0c05827e18a1ed9cf9bf8a17f7705fcc793fec935b637867e2a5a9b8168c885554d029919117a18ba69;' % self.utils.generate_random_str(107)
}
# 从分享链接中提取网址
def getShareLink(self, string):
# findall() 查找匹配正则表达式的字符串
return re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', string)[0]
# 得到 作品id 或者 用户id
# 传入 url 支持 https://www.iesdouyin.com 与 https://v.douyin.com
def getKey(self, url):
key = None
key_type = None
try:
r = requests.get(url=url, headers=self.headers)
except Exception as e:
print('[ 错误 ]:输入链接有误!\r')
return key_type, key
# 抖音把图集更新为note
# 作品 第一步解析出来的链接是share/video/{aweme_id}
# https://www.iesdouyin.com/share/video/7037827546599263488/?region=CN&mid=6939809470193126152&u_code=j8a5173b&did=MS4wLjABAAAA1DICF9-A9M_CiGqAJZdsnig5TInVeIyPdc2QQdGrq58xUgD2w6BqCHovtqdIDs2i&iid=MS4wLjABAAAAomGWi4n2T0H9Ab9x96cUZoJXaILk4qXOJlJMZFiK6b_aJbuHkjN_f0mBzfy91DX1&with_sec_did=1&titleType=title&schema_type=37&from_ssr=1&utm_source=copy&utm_campaign=client_share&utm_medium=android&app=aweme
# 用户 第一步解析出来的链接是share/user/{sec_uid}
# https://www.iesdouyin.com/share/user/MS4wLjABAAAA06y3Ctu8QmuefqvUSU7vr0c_ZQnCqB0eaglgkelLTek?did=MS4wLjABAAAA1DICF9-A9M_CiGqAJZdsnig5TInVeIyPdc2QQdGrq58xUgD2w6BqCHovtqdIDs2i&iid=MS4wLjABAAAAomGWi4n2T0H9Ab9x96cUZoJXaILk4qXOJlJMZFiK6b_aJbuHkjN_f0mBzfy91DX1&with_sec_did=1&sec_uid=MS4wLjABAAAA06y3Ctu8QmuefqvUSU7vr0c_ZQnCqB0eaglgkelLTek&from_ssr=1&u_code=j8a5173b&timestamp=1674540164&ecom_share_track_params=%7B%22is_ec_shopping%22%3A%221%22%2C%22secuid%22%3A%22MS4wLjABAAAA-jD2lukp--I21BF8VQsmYUqJDbj3FmU-kGQTHl2y1Cw%22%2C%22enter_from%22%3A%22others_homepage%22%2C%22share_previous_page%22%3A%22others_homepage%22%7D&utm_source=copy&utm_campaign=client_share&utm_medium=android&app=aweme
urlstr = str(r.request.path_url)
if "/share/user/" in urlstr:
# 获取用户 sec_uid
if '?' in r.request.path_url:
for one in re.finditer(r'user\/([\d\D]*)([?])', str(r.request.path_url)):
key = one.group(1)
else:
for one in re.finditer(r'user\/([\d\D]*)', str(r.request.path_url)):
key = one.group(1)
key_type = "user"
elif "/share/video/" in urlstr:
# 获取作品 aweme_id
key = re.findall('video/(\d+)?', urlstr)[0]
key_type = "aweme"
elif "live.douyin.com" in r.url:
key = r.url.replace('https://live.douyin.com/', '')
key_type = "live"
if key is None or key_type is None:
print('[ 错误 ]:输入链接有误!无法获取 id\r')
return key_type, key
print('[ 提示 ]:作品或者用户的 id = %s\r' % key)
return key_type, key
# 传入 aweme_id
# 返回 数据 字典
def getAwemeInfo(self, aweme_id):
if aweme_id is None:
return None
# 单作品接口返回 'aweme_detail'
# 主页作品接口返回 'aweme_list'->['aweme_detail']
jx_url = self.urls.POST_DETAIL + self.utils.getXbogus(
url=f'aweme_id={aweme_id}&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333')
try:
raw = requests.get(url=jx_url, headers=self.headers).text
datadict = json.loads(raw)
except Exception as e:
print("[ 错误 ]:接口未返回数据, 请检查后重新运行!\r")
return None
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
if datadict['aweme_detail'] is None:
print('[ 错误 ]:作品不存在, 请检查后重新运行!\r')
return None
# 默认为视频
awemeType = 0
try:
# datadict['aweme_detail']["images"] 不为 None 说明是图集
if datadict['aweme_detail']["images"] is not None:
awemeType = 1
except Exception as e:
print("[ 警告 ]:接口中未找到 images\r")
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, datadict['aweme_detail'])
return self.result.awemeDict, datadict
# 传入 url 支持 https://www.iesdouyin.com 与 https://v.douyin.com
# mode : post | like 模式选择 like为用户点赞 post为用户发布
def getUserInfo(self, sec_uid, mode="post", count=35):
if sec_uid is None:
return None
max_cursor = 0
self.awemeList = []
print("[ 提示 ]:正在获取所有作品数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在进行第 " + str(times) + " 次请求...\r")
if mode == "post":
url = self.urls.USER_POST + self.utils.getXbogus(
url=f'sec_uid={sec_uid}&count={count}&max_cursor={max_cursor}')
elif mode == "like":
url = self.urls.USER_FAVORITE_A + self.utils.getXbogus(
url=f'sec_user_id={sec_uid}&count={count}&max_cursor={max_cursor}&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333')
else:
print("[ 错误 ]:模式选择错误, 仅支持post和like, 请检查后重新运行!\r")
return None
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
res = requests.get(url=url, headers=self.headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["aweme_list"])) + ' 条数据')
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
print("[ 警告 ]:接口未返回数据, 正在重新请求!\r")
for aweme in datadict["aweme_list"]:
# 获取 aweme_id
aweme_id = aweme["aweme_id"]
# 深拷贝 dict 不然list里面全是同样的数据
datanew, dataraw = self.getAwemeInfo(aweme_id)
self.awemeList.append(copy.deepcopy(datanew))
# 更新 max_cursor
max_cursor = datadict["max_cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("[ 提示 ]:所有作品数据获取完成...\r\n")
break
else:
print("[ 提示 ]:第 " + str(times) + " 次请求成功...\r")
return self.awemeList
def getLiveInfo(self, web_rid: str):
# web_rid = live_url.replace('https://live.douyin.com/', '')
live_api = 'https://live.douyin.com/webcast/room/web/enter/?aid=6383&device_platform=web&web_rid=%s' % (web_rid)
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
'cookie' : '__ac_nonce=063f2f2fe002b0c1cf5a3; ttwid=1|_P0qI1eym6Of_Wz2s3FhDRThixb46o2hSYqHFIcdaHM|1676866302|3dd715d4512ff13abbd1aaedc19257b8bfe55b2bbbcad6a95de237776729ba54'
}
try:
response = requests.get(live_api, headers=headers)
live_json = json.loads(response.text)
except Exception as e:
print("[ 错误 ]:接口未返回数据, 请检查后重新运行!\r")
return None
if live_json == {} or live_json['status_code'] != 0:
print("[ 错误 ]:接口未返回信息\r")
return None
# 清空字典
self.result.clearDict(self.result.liveDict)
# 是否在播
self.result.liveDict["status"] = live_json['data']['data'][0]['status']
if self.result.liveDict["status"] == 4:
print('[ 📺 ]:当前直播已结束,正在退出')
return self.result.liveDict
# 直播标题
self.result.liveDict["title"] = live_json['data']['data'][0]['title']
# 观看人数
self.result.liveDict["user_count"] = live_json['data']['data'][0]['user_count_str']
# 昵称
self.result.liveDict["nickname"] = live_json['data']['data'][0]['owner']['nickname']
# sec_uid
self.result.liveDict["sec_uid"] = live_json['data']['data'][0]['owner']['sec_uid']
# 直播间观看状态
self.result.liveDict["display_long"] = live_json['data']['data'][0]['room_view_stats']['display_long']
# 推流
self.result.liveDict["flv_pull_url"] = live_json['data']['data'][0]['stream_url']['flv_pull_url']
try:
# 分区
self.result.liveDict["partition"] = live_json['data']['partition_road_map']['partition']['title']
self.result.liveDict["sub_partition"] = live_json['data']['partition_road_map']['sub_partition']['partition'][
'title']
except Exception as e:
self.result.liveDict["partition"] = ''
self.result.liveDict["sub_partition"] = ''
info = '[ 💻 ]:直播间:%s 当前%s 主播:%s 分区:%s-%s\r' % (
self.result.liveDict["title"], self.result.liveDict["display_long"], self.result.liveDict["nickname"],
self.result.liveDict["partition"], self.result.liveDict["sub_partition"])
print(info)
flv = []
print('[ 🎦 ]:直播间清晰度')
for i, f in enumerate(self.result.liveDict["flv_pull_url"].keys()):
print('[ %s ]: %s' % (i, f))
flv.append(f)
rate = int(input('[ 🎬 ]输入数字选择推流清晰度:'))
# 显示清晰度列表
print('[ %s ]:%s' % (flv[rate], self.result.liveDict["flv_pull_url"][flv[rate]]))
print('[ 📺 ]:复制链接使用下载工具下载')
return self.result.liveDict
# 来自 https://blog.csdn.net/weixin_43347550/article/details/105248223
def progressBarDownload(self, url, filepath):
start = time.time() # 下载开始时间
response = requests.get(url, stream=True, headers=self.headers)
size = 0 # 初始化已下载大小
chunk_size = 1024 # 每次下载的数据大小
content_size = int(response.headers['content-length']) # 下载文件总大小
try:
if response.status_code == 200: # 判断是否响应成功
print('[ 开始下载 ]:文件大小:{size:.2f} MB'.format(
size=content_size / chunk_size / 1024)) # 开始下载,显示下载文件大小
with open(filepath, 'wb') as file: # 显示进度条
for data in response.iter_content(chunk_size=chunk_size):
file.write(data)
size += len(data)
print('\r' + '[ 下载进度 ]:%s%.2f%%' % (
'>' * int(size * 50 / content_size), float(size / content_size * 100)), end=' ')
end = time.time() # 下载结束时间
print('\n' + '[ 下载完成 ]:耗时: %.2f\n' % (
end - start)) # 输出下载用时时间
except Exception as e:
# 下载异常 删除原来下载的文件, 可能未下成功
if os.path.exists(filepath):
os.remove(filepath)
print("[ 错误 ]:下载出错\r")
def awemeDownload(self, awemeDict: dict, music=True, cover=True, avatar=True, savePath=os.getcwd()):
if awemeDict is None:
return
if not os.path.exists(savePath):
os.mkdir(savePath)
try:
# 使用作品 创建时间+描述 当文件夹
file_name = self.utils.replaceStr(awemeDict["create_time"] + " " + awemeDict["desc"])
aweme_path = os.path.join(savePath, file_name)
if not os.path.exists(aweme_path):
os.mkdir(aweme_path)
# 保存获取到的字典信息
print("[ 提示 ]:正在保存获取到的信息到result.json\r\n")
with open(os.path.join(aweme_path, "result.json"), "w", encoding='utf-8') as f:
f.write(json.dumps(awemeDict, ensure_ascii=False, indent=2))
f.close()
# 下载 视频
if awemeDict["awemeType"] == 0:
print("[ 提示 ]:正在下载视频...\r")
video_path = os.path.join(aweme_path, file_name + ".mp4")
if os.path.exists(video_path):
print("[ 提示 ]:视频已存在为您跳过...\r\n")
else:
try:
url = awemeDict["video"]["play_addr"]["url_list"]
if url != "":
self.progressBarDownload(url, video_path)
except Exception as e:
print("[ 错误 ]:无法获取到视频url\r\n")
# 下载 图集
if awemeDict["awemeType"] == 1:
print("[ 提示 ]:正在下载图集...\r")
for ind, image in enumerate(awemeDict["images"]):
image_path = os.path.join(aweme_path, "image" + str(ind) + ".jpeg")
if os.path.exists(image_path):
print("[ 提示 ]:图片已存在为您跳过...\r\n")
else:
try:
url = image["url_list"][0]
if url != "":
self.progressBarDownload(url, image_path)
except Exception as e:
print("[ 错误 ]:无法获取到图片url\r\n")
# 下载 音乐
if music:
print("[ 提示 ]:正在下载音乐...\r")
music_name = self.utils.replaceStr(awemeDict["music"]["title"])
music_path = os.path.join(aweme_path, music_name + ".mp3")
if os.path.exists(music_path):
print("[ 提示 ]:音乐已存在为您跳过...\r\n")
else:
try:
url = awemeDict["music"]["play_url"]["url_list"][0]
if url != "":
self.progressBarDownload(url, music_path)
except Exception as e:
print("[ 错误 ]:无法获取到音乐url\r\n")
# 下载 cover
if cover and awemeDict["awemeType"] == 0:
print("[ 提示 ]:正在下载视频cover图...\r")
cover_path = os.path.join(aweme_path, "cover.jpeg")
if os.path.exists(cover_path):
print("[ 提示 ]:cover 已存在为您跳过...\r\n")
else:
try:
url = awemeDict["video"]["cover_original_scale"]["url_list"][0]
if url != "":
self.progressBarDownload(url, cover_path)
except Exception as e:
print("[ 错误 ]:无法获取到cover url\r\n")
# 下载 avatar
if avatar:
print("[ 提示 ]:正在下载用户头像...\r")
avatar_path = os.path.join(aweme_path, "avatar.jpeg")
if os.path.exists(avatar_path):
print("[ 提示 ]:avatar 已存在为您跳过...\r\n")
else:
try:
url = awemeDict["author"]["avatar"]["url_list"][0]
if url != "":
self.progressBarDownload(url, avatar_path)
except Exception as e:
print("[ 错误 ]:无法获取到avatar url\r\n")
except Exception as e:
print("[ 错误 ]:请检查json信息是否正确\r\n")
def userDownload(self, awemeList: list, music=True, cover=True, avatar=True, savePath=os.getcwd()):
if awemeList is None:
return
if not os.path.exists(savePath):
os.mkdir(savePath)
for ind, aweme in enumerate(awemeList):
print("[ 提示 ]:正在下载 %s 的作品 %s/%s\r"
% (aweme["author"]["nickname"], str(ind + 1), len(awemeList)))
self.awemeDownload(aweme, music, cover, avatar, savePath)
# time.sleep(0.5)
if __name__ == "__main__":
pass

View File

@ -1,71 +0,0 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Description:TikTok.py
@Date :2023/01/27 19:36:18
@Author :imgyh
@version :1.0
@Github :https://github.com/imgyh
@Mail :admin@imgyh.com
-------------------------------------------------
Change Log :
-------------------------------------------------
'''
import argparse
import os
import json
from TikTok import TikTok
def argument():
parser = argparse.ArgumentParser(description='抖音批量下载工具 使用帮助')
parser.add_argument("--link", "-l",
help="1.作品(视频或图集)与个人主页抖音分享链接(删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/)\r\n"
"2.解析直播网页版网址(https://live.douyin.com/802939216127)",
type=str, required=True)
parser.add_argument("--path", "-p", help="下载保存位置",
type=str, required=True)
parser.add_argument("--music", "-m", help="是否下载视频中的音乐(True/False), 默认为True",
type=bool, required=False, default=True)
parser.add_argument("--cover", "-c", help="是否下载视频的封面(True/False), 默认为True, 当下载视频时有效",
type=bool, required=False, default=True)
parser.add_argument("--avatar", "-a", help="是否下载作者的头像(True/False), 默认为True",
type=bool, required=False, default=True)
parser.add_argument("--mode", "-M", help="link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like), 默认为post",
type=str, required=False, default="post")
args = parser.parse_args()
return args
def main():
args = argument()
tk = TikTok()
url = tk.getShareLink(args.link)
key_type, key = tk.getKey(url)
if key is None or key_type is None:
return
elif key_type == "user":
datalist = tk.getUserInfo(key, args.mode, 35)
tk.userDownload(awemeList=datalist, music=args.music, cover=args.cover, avatar=args.avatar,
savePath=args.path)
elif key_type == "aweme":
datanew, dataraw = tk.getAwemeInfo(key)
tk.awemeDownload(awemeDict=datanew, music=args.music, cover=args.cover, avatar=args.avatar,
savePath=args.path)
elif key_type == "live":
live_json = tk.getLiveInfo(key)
if not os.path.exists(args.path):
os.mkdir(args.path)
# 保存获取到json
print("[ 提示 ]:正在保存获取到的信息到result.json\r\n")
with open(os.path.join(args.path, "result.json"), "w", encoding='utf-8') as f:
f.write(json.dumps(live_json, ensure_ascii=False, indent=2))
f.close()
if __name__ == "__main__":
main()

View File

@ -1,68 +0,0 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Description:TikTok.py
@Date :2023/02/11 13:06:23
@Author :imgyh
@version :1.0
@Github :https://github.com/imgyh
@Mail :admin@imgyh.com
-------------------------------------------------
Change Log :
-------------------------------------------------
'''
from TikTok import TikTok
def getAwemeInfo():
share_link_video = "3.56 uSy:/ 复制打开抖音,看看【小透明的作品】没有女朋友就用我的吧哈哈哈哈 # 表情包锁屏 https://v.douyin.com/BugmVVD/"
share_link_pic = "8.20 MJI:/ 复制打开抖音,看看【舍溪的图文作品】我又来放图集啦~还有你们要的小可爱大图也放啦~# ... https://v.douyin.com/BugrFTN/"
tk = TikTok()
url = tk.getShareLink(share_link_pic)
key_type, key = tk.getKey(url)
datanew, dataraw = tk.getAwemeInfo(key)
print(datanew)
def getUserInfo():
share_link_post = "1- 长按复制此条消息打开抖音搜索查看TA的更多作品。 https://v.douyin.com/BupCppt/"
share_link_like = "2- 长按复制此条消息打开抖音搜索查看TA的更多作品。 https://v.douyin.com/BusJrfr/"
tk = TikTok()
url = tk.getShareLink(share_link_like)
key_type, key = tk.getKey(url)
awemeList = tk.getUserInfo(key, mode="like", count=35)
print(awemeList)
def getLiveInfo():
live_link = "https://live.douyin.com/40768897856"
tk = TikTok()
url = tk.getShareLink(live_link)
key_type, key = tk.getKey(url)
live_json = tk.getLiveInfo(key)
print(live_json)
if __name__ == "__main__":
# getAwemeInfo()
# getUserInfo()
# getLiveInfo()
pass
################################# 测试命令 ######################################################
# 视频
# python TikTokCommand.py -l https://v.douyin.com/BugmVVD/ -p /mnt/c/project/test1
# .\TikTokCommand.exe -l https://v.douyin.com/BugmVVD/ -p .\test1
# 图集
# python TikTokCommand.py -l https://v.douyin.com/BugrFTN/ -p /mnt/c/project/test2
# .\TikTokCommand.exe -l https://v.douyin.com/BugrFTN/ -p .\test2
# 主页作品(视频)
# python TikTokCommand.py -l https://v.douyin.com/BupCppt/ -p /mnt/c/project/test3
# .\TikTokCommand.exe -l https://v.douyin.com/BupCppt/ -p .\test3
# 主页作品(视频与图集混合)
# python TikTokCommand.py -l https://v.douyin.com/B72pdU5/ -p /mnt/c/project/test4
# .\TikTokCommand.exe -l https://v.douyin.com/B72pdU5/ -p .\test4
# 主页喜欢(视频)
# python TikTokCommand.py -l https://v.douyin.com/B72QgDw/ -p /mnt/c/project/test5 -M like
# .\TikTokCommand.exe -l https://v.douyin.com/B72QgDw/ -p .\test5 -M like
#################################################################################################

View File

@ -1,68 +0,0 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Description:TikTok.py
@Date :2023/01/27 19:36:18
@Author :imgyh
@version :1.0
@Github :https://github.com/imgyh
@Mail :admin@imgyh.com
-------------------------------------------------
Change Log :
-------------------------------------------------
'''
import random
import re
import json
import requests
from TikTokUrls import Urls
class Utils(object):
def __init__(self):
pass
def generate_random_str(self, randomlength=16):
"""
根据传入长度产生随机字符串
"""
random_str = ''
base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789='
length = len(base_str) - 1
for _ in range(randomlength):
random_str += base_str[random.randint(0, length)]
return random_str
def replaceStr(self, filenamestr: str):
"""
替换非法字符缩短字符长度使其能成为文件名
"""
# 匹配 汉字 字母 数字 空格
match = "([0-9A-Za-z\u4e00-\u9fa5 -._]+)"
result = re.findall(match, filenamestr)
result = "".join(result).strip()
if len(result) > 80:
result = result[:80]
# 去除前后空格
return result
def getXbogus(self, url, headers=None):
urls = Urls()
try:
response = json.loads(requests.post(
url= urls.GET_XB_PATH, data={"param" : url}, headers=headers).text)
params = response["param"]
xb = response["X-Bogus"]
except Exception as e:
print('[ 错误 ]:X-Bogus接口异常, 可能是访问流量高, 接口限流请稍等几分钟再次尝试')
return
return params #, xb
if __name__ == "__main__":
pass

View File

@ -1,57 +0,0 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Description:TikTok.py
@Date :2023/01/27 19:36:18
@Author :imgyh
@version :1.0
@Github :https://github.com/imgyh
@Mail :admin@imgyh.com
-------------------------------------------------
Change Log :
-------------------------------------------------
'''
from flask import *
from TikTok import TikTok
def work(share_link):
tk = TikTok()
url = tk.getShareLink(share_link)
key_type, key = tk.getKey(url)
datanew, dataraw = tk.getAwemeInfo(key)
return datanew
app = Flask(__name__)
# 设置编码
app.config['JSON_AS_ASCII'] = False
@app.route("/douyin", methods=["POST"])
def douyin():
usefuldict = {}
if request.method == "POST":
result = request.form
else:
usefuldict["status_code"] = 500
return jsonify(usefuldict)
try:
usefuldict = work(result["share_link"])
usefuldict["status_code"] = 200
except Exception as error:
usefuldict["status_code"] = 500
return jsonify(usefuldict)
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
if __name__ == "__main__":
app.run(debug=False, host="0.0.0.0", port=5000)

159
WebApi.py Normal file
View File

@ -0,0 +1,159 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : WebApi.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 18:52
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
from flask import *
from apiproxy.douyin.douyinapi import DouyinApi
from apiproxy.douyin import douyin_headers
import argparse
def douyinwork(share_link, max_cursor, mode, cookie):
dy = DouyinApi()
if cookie is not None and cookie != "":
douyin_headers["Cookie"] = cookie
url = dy.getShareLink(share_link)
key_type, key = dy.getKey(url)
data = None
rawdata = None
cursor = None
has_more = None
if key_type == "user":
if mode == 'post' or mode == 'like':
data, rawdata, cursor, has_more = dy.getUserInfoApi(sec_uid=key, mode=mode, count=35,
max_cursor=max_cursor)
elif mode == 'mix':
data, rawdata, cursor, has_more = dy.getUserAllMixInfoApi(sec_uid=key, count=35, cursor=max_cursor)
elif mode == 'detail':
rawdata = dy.getUserDetailInfoApi(sec_uid=key)
data = rawdata
elif key_type == "mix":
data, rawdata, cursor, has_more = dy.getMixInfoApi(mix_id=key, count=35, cursor=max_cursor)
elif key_type == "music":
data, rawdata, cursor, has_more = dy.getMusicInfoApi(music_id=key, count=35, cursor=max_cursor)
elif key_type == "aweme":
data, rawdata = dy.getAwemeInfoApi(aweme_id=key)
elif key_type == "live":
data, rawdata = dy.getLiveInfoApi(web_rid=key)
datadict = {}
datadict["data"] = data
datadict["rawdata"] = rawdata
datadict["cursor"] = cursor
datadict["has_more"] = has_more
return datadict
def deal(mode=None):
usefuldict = {}
if request.headers.get("content_type") == "application/json":
result = request.get_json(force=True)
else:
result = request.form
share_link = None
cursor = 0
cookie = None
try:
share_link = result["share_link"]
cursor = result["cursor"]
cookie = result["cookie"]
except Exception as e:
usefuldict["status_code"] = 500
try:
if share_link is not None and share_link != "":
usefuldict = douyinwork(share_link, cursor, mode, cookie)
usefuldict["status_code"] = 200
except Exception as e:
usefuldict["status_code"] = 500
return jsonify(usefuldict)
app = Flask(__name__)
# 设置编码
app.config['JSON_AS_ASCII'] = False
def argument():
parser = argparse.ArgumentParser(description='抖音去水印工具 使用帮助')
parser.add_argument("--port", "-p", help="Web端口",
type=int, required=False, default=5000)
args = parser.parse_args()
return args
@app.route("/douyin/music", methods=["POST"])
def douyinMusic():
return deal()
@app.route("/douyin/mix", methods=["POST"])
def douyinMix():
return deal()
@app.route("/douyin/user/mix", methods=["POST"])
def douyinUserMix():
return deal(mode="mix")
@app.route("/douyin/user/like", methods=["POST"])
def douyinUserLike():
return deal(mode="like")
@app.route("/douyin/user/post", methods=["POST"])
def douyinUserPost():
return deal(mode="post")
@app.route("/douyin/user/detail", methods=["POST"])
def douyinUserDetail():
return deal(mode="detail")
@app.route("/douyin/aweme", methods=["POST"])
def douyinAweme():
return deal()
@app.route("/douyin/live", methods=["POST"])
def douyinLive():
return deal()
@app.route("/douyin", methods=["POST"])
def douyin():
return deal()
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
if __name__ == "__main__":
args = argument()
app.run(debug=False, host="0.0.0.0", port=args.port)

20
apiproxy/__init__.py Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : __init__.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 14:32
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : __init__.py.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 16:10
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
from .utils import Utils
utils = Utils()

201
apiproxy/common/utils.py Normal file
View File

@ -0,0 +1,201 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : utils.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 15:18
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import random
import requests
import re
import os
import sys
import hashlib
import base64
import time
import apiproxy
class Utils(object):
def __init__(self):
pass
def replaceStr(self, filenamestr: str):
"""
替换非法字符缩短字符长度使其能成为文件名
"""
# 匹配 汉字 字母 数字 空格
match = "([0-9A-Za-z\u4e00-\u9fa5]+)"
result = re.findall(match, filenamestr)
result = "".join(result).strip()
if len(result) > 20:
result = result[:20]
# 去除前后空格
return result
def resource_path(self, relative_path):
if getattr(sys, 'frozen', False): # 是否Bundle Resource
base_path = sys._MEIPASS
else:
base_path = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_path, relative_path)
def str2bool(self, v):
if isinstance(v, bool):
return v
if v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
return True
def generate_random_str(self, randomlength=16):
"""
根据传入长度产生随机字符串
"""
random_str = ''
base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789='
length = len(base_str) - 1
for _ in range(randomlength):
random_str += base_str[random.randint(0, length)]
return random_str
# https://www.52pojie.cn/thread-1589242-1-1.html
def getttwid(self):
url = 'https://ttwid.bytedance.com/ttwid/union/register/'
data = '{"region":"cn","aid":1768,"needFid":false,"service":"www.ixigua.com","migrate_info":{"ticket":"","source":"node"},"cbUrlProtocol":"https","union":true}'
res = requests.post(url=url, data=data)
for i, j in res.cookies.items():
return j
def getXbogus(self, payload, form='', ua=apiproxy.ua):
xbogus = self.get_xbogus(payload, ua, form)
params = payload + "&X-Bogus=" + xbogus
return params
def get_xbogus(self, payload, ua, form):
short_str = "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe="
arr2 = self.get_arr2(payload, ua, form)
garbled_string = self.get_garbled_string(arr2)
xbogus = ""
for i in range(0, 21, 3):
char_code_num0 = garbled_string[i]
char_code_num1 = garbled_string[i + 1]
char_code_num2 = garbled_string[i + 2]
base_num = char_code_num2 | char_code_num1 << 8 | char_code_num0 << 16
str1 = short_str[(base_num & 16515072) >> 18]
str2 = short_str[(base_num & 258048) >> 12]
str3 = short_str[(base_num & 4032) >> 6]
str4 = short_str[base_num & 63]
xbogus += str1 + str2 + str3 + str4
return xbogus
def get_garbled_string(self, arr2):
p = [
arr2[0], arr2[10], arr2[1], arr2[11], arr2[2], arr2[12], arr2[3], arr2[13], arr2[4], arr2[14],
arr2[5], arr2[15], arr2[6], arr2[16], arr2[7], arr2[17], arr2[8], arr2[18], arr2[9]
]
char_array = [chr(i) for i in p]
f = []
f.extend([2, 255])
tmp = ['ÿ']
bytes_ = self._0x30492c(tmp, "".join(char_array))
for i in range(len(bytes_)):
f.append(bytes_[i])
return f
def get_arr2(self, payload, ua, form):
salt_payload_bytes = hashlib.md5(hashlib.md5(payload.encode()).digest()).digest()
salt_payload = [byte for byte in salt_payload_bytes]
salt_form_bytes = hashlib.md5(hashlib.md5(form.encode()).digest()).digest()
salt_form = [byte for byte in salt_form_bytes]
ua_key = ['\u0000', '\u0001', '\u000e']
salt_ua_bytes = hashlib.md5(base64.b64encode(self._0x30492c(ua_key, ua))).digest()
salt_ua = [byte for byte in salt_ua_bytes]
timestamp = int(time.time())
canvas = 1489154074
arr1 = [
64, # 固定
0, # 固定
1, # 固定
14, # 固定 这个还要再看一下14,12,0都出现过
salt_payload[14], # payload 相关
salt_payload[15],
salt_form[14], # form 相关
salt_form[15],
salt_ua[14], # ua 相关
salt_ua[15],
(timestamp >> 24) & 255,
(timestamp >> 16) & 255,
(timestamp >> 8) & 255,
(timestamp >> 0) & 255,
(canvas >> 24) & 255,
(canvas >> 16) & 255,
(canvas >> 8) & 255,
(canvas >> 0) & 255,
64, # 校验位
]
for i in range(1, len(arr1) - 1):
arr1[18] ^= arr1[i]
arr2 = [arr1[0], arr1[2], arr1[4], arr1[6], arr1[8], arr1[10], arr1[12], arr1[14], arr1[16], arr1[18], arr1[1],
arr1[3], arr1[5], arr1[7], arr1[9], arr1[11], arr1[13], arr1[15], arr1[17]]
return arr2
def _0x30492c(self, a, b):
d = [i for i in range(256)]
c = 0
result = bytearray(len(b))
for i in range(256):
c = (c + d[i] + ord(a[i % len(a)])) % 256
e = d[i]
d[i] = d[c]
d[c] = e
t = 0
c = 0
for i in range(len(b)):
t = (t + 1) % 256
c = (c + d[t]) % 256
e = d[t]
d[t] = d[c]
d[c] = e
result[i] = ord(b[i]) ^ d[(d[t] + d[c]) % 256]
return result
if __name__ == "__main__":
pass

View File

@ -0,0 +1,27 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : __init__.py.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 14:44
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import apiproxy
from apiproxy.common import utils
douyin_headers = {
'User-Agent': apiproxy.ua,
'referer': 'https://www.douyin.com/',
'accept-encoding': None,
'Cookie': f"msToken={utils.generate_random_str(107)}; ttwid={utils.getttwid()}; odin_tt=324fb4ea4a89c0c05827e18a1ed9cf9bf8a17f7705fcc793fec935b637867e2a5a9b8168c885554d029919117a18ba69; passport_csrf_token=f61602fc63757ae0e4fd9d6bdcee4810;"
}

172
apiproxy/douyin/database.py Normal file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : database.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 15:15
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import sqlite3
import json
class DataBase(object):
def __init__(self):
self.conn = sqlite3.connect('data.db')
self.cursor = self.conn.cursor()
self.create_user_post_table()
self.create_user_like_table()
self.create_mix_table()
self.create_music_table()
def create_user_post_table(self):
sql = """CREATE TABLE if not exists t_user_post (
id integer primary key autoincrement,
sec_uid varchar(200),
aweme_id integer unique,
rawdata json
);"""
try:
self.cursor.execute(sql)
self.conn.commit()
except Exception as e:
pass
def get_user_post(self, sec_uid: str, aweme_id: int):
sql = """select id, sec_uid, aweme_id, rawdata from t_user_post where sec_uid=? and aweme_id=?;"""
try:
self.cursor.execute(sql, (sec_uid, aweme_id))
self.conn.commit()
res = self.cursor.fetchone()
return res
except Exception as e:
pass
def insert_user_post(self, sec_uid: str, aweme_id: int, data: dict):
insertsql = """insert into t_user_post (sec_uid, aweme_id, rawdata) values(?,?,?);"""
try:
self.cursor.execute(insertsql, (sec_uid, aweme_id, json.dumps(data)))
self.conn.commit()
except Exception as e:
pass
def create_user_like_table(self):
sql = """CREATE TABLE if not exists t_user_like (
id integer primary key autoincrement,
sec_uid varchar(200),
aweme_id integer unique,
rawdata json
);"""
try:
self.cursor.execute(sql)
self.conn.commit()
except Exception as e:
pass
def get_user_like(self, sec_uid: str, aweme_id: int):
sql = """select id, sec_uid, aweme_id, rawdata from t_user_like where sec_uid=? and aweme_id=?;"""
try:
self.cursor.execute(sql, (sec_uid, aweme_id))
self.conn.commit()
res = self.cursor.fetchone()
return res
except Exception as e:
pass
def insert_user_like(self, sec_uid: str, aweme_id: int, data: dict):
insertsql = """insert into t_user_like (sec_uid, aweme_id, rawdata) values(?,?,?);"""
try:
self.cursor.execute(insertsql, (sec_uid, aweme_id, json.dumps(data)))
self.conn.commit()
except Exception as e:
pass
def create_mix_table(self):
sql = """CREATE TABLE if not exists t_mix (
id integer primary key autoincrement,
sec_uid varchar(200),
mix_id varchar(200),
aweme_id integer,
rawdata json
);"""
try:
self.cursor.execute(sql)
self.conn.commit()
except Exception as e:
pass
def get_mix(self, sec_uid: str, mix_id: str, aweme_id: int):
sql = """select id, sec_uid, mix_id, aweme_id, rawdata from t_mix where sec_uid=? and mix_id=? and aweme_id=?;"""
try:
self.cursor.execute(sql, (sec_uid, mix_id, aweme_id))
self.conn.commit()
res = self.cursor.fetchone()
return res
except Exception as e:
pass
def insert_mix(self, sec_uid: str, mix_id: str, aweme_id: int, data: dict):
insertsql = """insert into t_mix (sec_uid, mix_id, aweme_id, rawdata) values(?,?,?,?);"""
try:
self.cursor.execute(insertsql, (sec_uid, mix_id, aweme_id, json.dumps(data)))
self.conn.commit()
except Exception as e:
pass
def create_music_table(self):
sql = """CREATE TABLE if not exists t_music (
id integer primary key autoincrement,
music_id varchar(200),
aweme_id integer unique,
rawdata json
);"""
try:
self.cursor.execute(sql)
self.conn.commit()
except Exception as e:
pass
def get_music(self, music_id: str, aweme_id: int):
sql = """select id, music_id, aweme_id, rawdata from t_music where music_id=? and aweme_id=?;"""
try:
self.cursor.execute(sql, (music_id, aweme_id))
self.conn.commit()
res = self.cursor.fetchone()
return res
except Exception as e:
pass
def insert_music(self, music_id: str, aweme_id: int, data: dict):
insertsql = """insert into t_music (music_id, aweme_id, rawdata) values(?,?,?);"""
try:
self.cursor.execute(insertsql, (music_id, aweme_id, json.dumps(data)))
self.conn.commit()
except Exception as e:
pass
if __name__ == '__main__':
pass

688
apiproxy/douyin/douyin.py Normal file
View File

@ -0,0 +1,688 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : douyin.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 14:52
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import re
import requests
import json
import time
import copy
from apiproxy.douyin import douyin_headers
from apiproxy.douyin.urls import Urls
from apiproxy.douyin.result import Result
from apiproxy.douyin.database import DataBase
from apiproxy.common import utils
class Douyin(object):
def __init__(self, database=False):
self.urls = Urls()
self.result = Result()
self.database = database
if database:
self.db = DataBase()
# 用于设置重复请求某个接口的最大时间
self.timeout = 10
# 从分享链接中提取网址
def getShareLink(self, string):
# findall() 查找匹配正则表达式的字符串
return re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', string)[0]
# 得到 作品id 或者 用户id
# 传入 url 支持 https://www.iesdouyin.com 与 https://v.douyin.com
def getKey(self, url):
key = None
key_type = None
try:
r = requests.get(url=url, headers=douyin_headers)
except Exception as e:
print('[ 错误 ]:输入链接有误!\r')
return key_type, key
# 抖音把图集更新为note
# 作品 第一步解析出来的链接是share/video/{aweme_id}
# https://www.iesdouyin.com/share/video/7037827546599263488/?region=CN&mid=6939809470193126152&u_code=j8a5173b&did=MS4wLjABAAAA1DICF9-A9M_CiGqAJZdsnig5TInVeIyPdc2QQdGrq58xUgD2w6BqCHovtqdIDs2i&iid=MS4wLjABAAAAomGWi4n2T0H9Ab9x96cUZoJXaILk4qXOJlJMZFiK6b_aJbuHkjN_f0mBzfy91DX1&with_sec_did=1&titleType=title&schema_type=37&from_ssr=1&utm_source=copy&utm_campaign=client_share&utm_medium=android&app=aweme
# 用户 第一步解析出来的链接是share/user/{sec_uid}
# https://www.iesdouyin.com/share/user/MS4wLjABAAAA06y3Ctu8QmuefqvUSU7vr0c_ZQnCqB0eaglgkelLTek?did=MS4wLjABAAAA1DICF9-A9M_CiGqAJZdsnig5TInVeIyPdc2QQdGrq58xUgD2w6BqCHovtqdIDs2i&iid=MS4wLjABAAAAomGWi4n2T0H9Ab9x96cUZoJXaILk4qXOJlJMZFiK6b_aJbuHkjN_f0mBzfy91DX1&with_sec_did=1&sec_uid=MS4wLjABAAAA06y3Ctu8QmuefqvUSU7vr0c_ZQnCqB0eaglgkelLTek&from_ssr=1&u_code=j8a5173b&timestamp=1674540164&ecom_share_track_params=%7B%22is_ec_shopping%22%3A%221%22%2C%22secuid%22%3A%22MS4wLjABAAAA-jD2lukp--I21BF8VQsmYUqJDbj3FmU-kGQTHl2y1Cw%22%2C%22enter_from%22%3A%22others_homepage%22%2C%22share_previous_page%22%3A%22others_homepage%22%7D&utm_source=copy&utm_campaign=client_share&utm_medium=android&app=aweme
# 合集
# https://www.douyin.com/collection/7093490319085307918
urlstr = str(r.request.path_url)
if "/user/" in urlstr:
# 获取用户 sec_uid
if '?' in r.request.path_url:
for one in re.finditer(r'user\/([\d\D]*)([?])', str(r.request.path_url)):
key = one.group(1)
else:
for one in re.finditer(r'user\/([\d\D]*)', str(r.request.path_url)):
key = one.group(1)
key_type = "user"
elif "/video/" in urlstr:
# 获取作品 aweme_id
key = re.findall('video/(\d+)?', urlstr)[0]
key_type = "aweme"
elif "/note/" in urlstr:
# 获取note aweme_id
key = re.findall('note/(\d+)?', urlstr)[0]
key_type = "aweme"
elif "/mix/detail/" in urlstr:
# 获取合集 id
key = re.findall('/mix/detail/(\d+)?', urlstr)[0]
key_type = "mix"
elif "/collection/" in urlstr:
# 获取合集 id
key = re.findall('/collection/(\d+)?', urlstr)[0]
key_type = "mix"
elif "/music/" in urlstr:
# 获取原声 id
key = re.findall('music/(\d+)?', urlstr)[0]
key_type = "music"
elif "/webcast/reflow/" in urlstr:
key1 = re.findall('reflow/(\d+)?', urlstr)[0]
url = self.urls.LIVE2 + utils.getXbogus(
f'live_id=1&room_id={key1}&app_id=1128')
res = requests.get(url, headers=douyin_headers)
resjson = json.loads(res.text)
key = resjson['data']['room']['owner']['web_rid']
key_type = "live"
elif "live.douyin.com" in r.url:
key = r.url.replace('https://live.douyin.com/', '')
key_type = "live"
if key is None or key_type is None:
print('[ 错误 ]:输入链接有误!无法获取 id\r')
return key_type, key
return key_type, key
# 传入 aweme_id
# 返回 数据 字典
def getAwemeInfo(self, aweme_id):
print('[ 提示 ]:正在请求的作品 id = %s\r' % aweme_id)
if aweme_id is None:
return None
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
# 单作品接口返回 'aweme_detail'
# 主页作品接口返回 'aweme_list'->['aweme_detail']
jx_url = self.urls.POST_DETAIL + utils.getXbogus(
f'aweme_id={aweme_id}&device_platform=webapp&aid=6383')
raw = requests.get(url=jx_url, headers=douyin_headers).text
datadict = json.loads(raw)
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return {}, {}
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
# datadict['aweme_detail']["images"] 不为 None 说明是图集
if datadict['aweme_detail']["images"] is not None:
awemeType = 1
except Exception as e:
print("[ 警告 ]:接口中未找到 images\r")
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, datadict['aweme_detail'])
return self.result.awemeDict, datadict
# 传入 url 支持 https://www.iesdouyin.com 与 https://v.douyin.com
# mode : post | like 模式选择 like为用户点赞 post为用户发布
def getUserInfo(self, sec_uid, mode="post", count=35, number=0, increase=False):
print('[ 提示 ]:正在请求的用户 id = %s\r\n' % sec_uid)
if sec_uid is None:
return None
if number <= 0:
numflag = False
else:
numflag = True
max_cursor = 0
awemeList = []
increaseflag = False
numberis0 = False
print("[ 提示 ]:正在获取所有作品数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在对 [主页] 进行第 " + str(times) + " 次请求...\r")
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
if mode == "post":
url = self.urls.USER_POST + utils.getXbogus(
f'sec_user_id={sec_uid}&count={count}&max_cursor={max_cursor}&device_platform=webapp&aid=6383')
elif mode == "like":
url = self.urls.USER_FAVORITE_A + utils.getXbogus(
f'sec_user_id={sec_uid}&count={count}&max_cursor={max_cursor}&device_platform=webapp&aid=6383')
else:
print("[ 错误 ]:模式选择错误, 仅支持post、like、mix, 请检查后重新运行!\r")
return None
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["aweme_list"])) + ' 条数据\r')
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return awemeList
for aweme in datadict["aweme_list"]:
if self.database:
# 退出条件
if increase is False and numflag and numberis0:
break
if increase and numflag and numberis0 and increaseflag:
break
# 增量更新, 找到非置顶的最新的作品发布时间
if mode == "post":
if self.db.get_user_post(sec_uid=sec_uid, aweme_id=aweme['aweme_id']) is not None:
if increase and aweme['is_top'] == 0:
increaseflag = True
else:
self.db.insert_user_post(sec_uid=sec_uid, aweme_id=aweme['aweme_id'], data=aweme)
elif mode == "like":
if self.db.get_user_like(sec_uid=sec_uid, aweme_id=aweme['aweme_id']) is not None:
if increase and aweme['is_top'] == 0:
increaseflag = True
else:
self.db.insert_user_like(sec_uid=sec_uid, aweme_id=aweme['aweme_id'], data=aweme)
# 退出条件
if increase and numflag is False and increaseflag:
break
if increase and numflag and numberis0 and increaseflag:
break
else:
if numflag and numberis0:
break
if numflag:
number -= 1
if number == 0:
numberis0 = True
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if aweme["images"] is not None:
awemeType = 1
except Exception as e:
print("[ 警告 ]:接口中未找到 images\r")
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, aweme)
if self.result.awemeDict is not None and self.result.awemeDict != {}:
awemeList.append(copy.deepcopy(self.result.awemeDict))
if self.database:
if increase and numflag is False and increaseflag:
print("\r\n[ 提示 ]: [主页] 下作品增量更新数据获取完成...\r\n")
break
elif increase is False and numflag and numberis0:
print("\r\n[ 提示 ]: [主页] 下指定数量作品数据获取完成...\r\n")
break
elif increase and numflag and numberis0 and increaseflag:
print("\r\n[ 提示 ]: [主页] 下指定数量作品数据获取完成, 增量更新数据获取完成...\r\n")
break
else:
if numflag and numberis0:
print("\r\n[ 提示 ]: [主页] 下指定数量作品数据获取完成...\r\n")
break
# 更新 max_cursor
max_cursor = datadict["max_cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("\r\n[ 提示 ]: [主页] 下所有作品数据获取完成...\r\n")
break
else:
print("\r\n[ 提示 ]:[主页] 第 " + str(times) + " 次请求成功...\r\n")
return awemeList
def getLiveInfo(self, web_rid: str):
print('[ 提示 ]:正在请求的直播间 id = %s\r\n' % web_rid)
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
live_api = self.urls.LIVE + utils.getXbogus(
f'aid=6383&device_platform=web&web_rid={web_rid}')
response = requests.get(live_api, headers=douyin_headers)
live_json = json.loads(response.text)
if live_json != {} and live_json['status_code'] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return {}
# 清空字典
self.result.clearDict(self.result.liveDict)
# 类型
self.result.liveDict["awemeType"] = 2
# 是否在播
self.result.liveDict["status"] = live_json['data']['data'][0]['status']
if self.result.liveDict["status"] == 4:
print('[ 📺 ]:当前直播已结束,正在退出')
return self.result.liveDict
# 直播标题
self.result.liveDict["title"] = live_json['data']['data'][0]['title']
# 直播cover
self.result.liveDict["cover"] = live_json['data']['data'][0]['cover']['url_list'][0]
# 头像
self.result.liveDict["avatar"] = live_json['data']['data'][0]['owner']['avatar_thumb']['url_list'][0].replace(
"100x100", "1080x1080")
# 观看人数
self.result.liveDict["user_count"] = live_json['data']['data'][0]['user_count_str']
# 昵称
self.result.liveDict["nickname"] = live_json['data']['data'][0]['owner']['nickname']
# sec_uid
self.result.liveDict["sec_uid"] = live_json['data']['data'][0]['owner']['sec_uid']
# 直播间观看状态
self.result.liveDict["display_long"] = live_json['data']['data'][0]['room_view_stats']['display_long']
# 推流
self.result.liveDict["flv_pull_url"] = live_json['data']['data'][0]['stream_url']['flv_pull_url']
try:
# 分区
self.result.liveDict["partition"] = live_json['data']['partition_road_map']['partition']['title']
self.result.liveDict["sub_partition"] = \
live_json['data']['partition_road_map']['sub_partition']['partition']['title']
except Exception as e:
self.result.liveDict["partition"] = ''
self.result.liveDict["sub_partition"] = ''
info = '[ 💻 ]:直播间:%s 当前%s 主播:%s 分区:%s-%s\r' % (
self.result.liveDict["title"], self.result.liveDict["display_long"], self.result.liveDict["nickname"],
self.result.liveDict["partition"], self.result.liveDict["sub_partition"])
print(info)
flv = []
print('[ 🎦 ]:直播间清晰度')
for i, f in enumerate(self.result.liveDict["flv_pull_url"].keys()):
print('[ %s ]: %s' % (i, f))
flv.append(f)
rate = int(input('[ 🎬 ]输入数字选择推流清晰度:'))
self.result.liveDict["flv_pull_url0"] = self.result.liveDict["flv_pull_url"][flv[rate]]
# 显示清晰度列表
print('[ %s ]:%s' % (flv[rate], self.result.liveDict["flv_pull_url"][flv[rate]]))
print('[ 📺 ]:复制链接使用下载工具下载')
return self.result.liveDict
def getMixInfo(self, mix_id: str, count=35, number=0, increase=False, sec_uid=''):
print('[ 提示 ]:正在请求的合集 id = %s\r\n' % mix_id)
if mix_id is None:
return None
if number <= 0:
numflag = False
else:
numflag = True
cursor = 0
awemeList = []
increaseflag = False
numberis0 = False
print("[ 提示 ]:正在获取合集下的所有作品数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在对 [合集] 进行第 " + str(times) + " 次请求...\r")
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
url = self.urls.USER_MIX + utils.getXbogus(
f'mix_id={mix_id}&cursor={cursor}&count={count}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["aweme_list"])) + ' 条数据\r')
if datadict is not None:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return awemeList
for aweme in datadict["aweme_list"]:
if self.database:
# 退出条件
if increase is False and numflag and numberis0:
break
if increase and numflag and numberis0 and increaseflag:
break
# 增量更新, 找到非置顶的最新的作品发布时间
if self.db.get_mix(sec_uid=sec_uid, mix_id=mix_id, aweme_id=aweme['aweme_id']) is not None:
if increase and aweme['is_top'] == 0:
increaseflag = True
else:
self.db.insert_mix(sec_uid=sec_uid, mix_id=mix_id, aweme_id=aweme['aweme_id'], data=aweme)
# 退出条件
if increase and numflag is False and increaseflag:
break
if increase and numflag and numberis0 and increaseflag:
break
else:
if numflag and numberis0:
break
if numflag:
number -= 1
if number == 0:
numberis0 = True
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if aweme["images"] is not None:
awemeType = 1
except Exception as e:
print("[ 警告 ]:接口中未找到 images\r")
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, aweme)
if self.result.awemeDict is not None and self.result.awemeDict != {}:
awemeList.append(copy.deepcopy(self.result.awemeDict))
if self.database:
if increase and numflag is False and increaseflag:
print("\r\n[ 提示 ]: [合集] 下作品增量更新数据获取完成...\r\n")
break
elif increase is False and numflag and numberis0:
print("\r\n[ 提示 ]: [合集] 下指定数量作品数据获取完成...\r\n")
break
elif increase and numflag and numberis0 and increaseflag:
print("\r\n[ 提示 ]: [合集] 下指定数量作品数据获取完成, 增量更新数据获取完成...\r\n")
break
else:
if numflag and numberis0:
print("\r\n[ 提示 ]: [合集] 下指定数量作品数据获取完成...\r\n")
break
# 更新 max_cursor
cursor = datadict["cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("\r\n[ 提示 ]:[合集] 下所有作品数据获取完成...\r\n")
break
else:
print("\r\n[ 提示 ]:[合集] 第 " + str(times) + " 次请求成功...\r\n")
return awemeList
def getUserAllMixInfo(self, sec_uid, count=35, number=0):
print('[ 提示 ]:正在请求的用户 id = %s\r\n' % sec_uid)
if sec_uid is None:
return None
if number <= 0:
numflag = False
else:
numflag = True
cursor = 0
mixIdNameDict = {}
print("[ 提示 ]:正在获取主页下所有合集 id 数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在对 [合集列表] 进行第 " + str(times) + " 次请求...\r")
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
url = self.urls.USER_MIX_LIST + utils.getXbogus(
f'sec_user_id={sec_uid}&count={count}&cursor={cursor}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["mix_infos"])) + ' 条数据\r')
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return mixIdNameDict
for mix in datadict["mix_infos"]:
mixIdNameDict[mix["mix_id"]] = mix["mix_name"]
if numflag:
number -= 1
if number == 0:
break
if numflag and number == 0:
print("\r\n[ 提示 ]:[合集列表] 下指定数量合集数据获取完成...\r\n")
break
# 更新 max_cursor
cursor = datadict["cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("[ 提示 ]:[合集列表] 下所有合集 id 数据获取完成...\r\n")
break
else:
print("\r\n[ 提示 ]:[合集列表] 第 " + str(times) + " 次请求成功...\r\n")
return mixIdNameDict
def getMusicInfo(self, music_id: str, count=35, number=0, increase=False):
print('[ 提示 ]:正在请求的音乐集合 id = %s\r\n' % music_id)
if music_id is None:
return None
if number <= 0:
numflag = False
else:
numflag = True
cursor = 0
awemeList = []
increaseflag = False
numberis0 = False
print("[ 提示 ]:正在获取音乐集合下的所有作品数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在对 [音乐集合] 进行第 " + str(times) + " 次请求...\r")
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
url = self.urls.MUSIC + utils.getXbogus(
f'music_id={music_id}&cursor={cursor}&count={count}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["aweme_list"])) + ' 条数据\r')
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return awemeList
for aweme in datadict["aweme_list"]:
if self.database:
# 退出条件
if increase is False and numflag and numberis0:
break
if increase and numflag and numberis0 and increaseflag:
break
# 增量更新, 找到非置顶的最新的作品发布时间
if self.db.get_music(music_id=music_id, aweme_id=aweme['aweme_id']) is not None:
if increase and aweme['is_top'] == 0:
increaseflag = True
else:
self.db.insert_music(music_id=music_id, aweme_id=aweme['aweme_id'], data=aweme)
# 退出条件
if increase and numflag is False and increaseflag:
break
if increase and numflag and numberis0 and increaseflag:
break
else:
if numflag and numberis0:
break
if numflag:
number -= 1
if number == 0:
numberis0 = True
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if aweme["images"] is not None:
awemeType = 1
except Exception as e:
print("[ 警告 ]:接口中未找到 images\r")
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, aweme)
if self.result.awemeDict is not None and self.result.awemeDict != {}:
awemeList.append(copy.deepcopy(self.result.awemeDict))
if self.database:
if increase and numflag is False and increaseflag:
print("\r\n[ 提示 ]: [音乐集合] 下作品增量更新数据获取完成...\r\n")
break
elif increase is False and numflag and numberis0:
print("\r\n[ 提示 ]: [音乐集合] 下指定数量作品数据获取完成...\r\n")
break
elif increase and numflag and numberis0 and increaseflag:
print("\r\n[ 提示 ]: [音乐集合] 下指定数量作品数据获取完成, 增量更新数据获取完成...\r\n")
break
else:
if numflag and numberis0:
print("\r\n[ 提示 ]: [音乐集合] 下指定数量作品数据获取完成...\r\n")
break
# 更新 cursor
cursor = datadict["cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("\r\n[ 提示 ]:[音乐集合] 下所有作品数据获取完成...\r\n")
break
else:
print("\r\n[ 提示 ]:[音乐集合] 第 " + str(times) + " 次请求成功...\r\n")
return awemeList
def getUserDetailInfo(self, sec_uid):
if sec_uid is None:
return None
datadict = {}
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
url = self.urls.USER_DETAIL + utils.getXbogus(
f'sec_user_id={sec_uid}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
if datadict is not None and datadict["status_code"] == 0:
return datadict
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return datadict
if __name__ == "__main__":
pass

View File

@ -0,0 +1,398 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : douyinapi.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/20 22:13
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import re
import requests
import json
import time
import copy
from apiproxy.douyin import douyin_headers
from apiproxy.douyin.urls import Urls
from apiproxy.douyin.result import Result
from apiproxy.common import utils
class DouyinApi(object):
def __init__(self):
self.urls = Urls()
self.result = Result()
# 用于设置重复请求某个接口的最大时间
self.timeout = 10
# 从分享链接中提取网址
def getShareLink(self, string):
# findall() 查找匹配正则表达式的字符串
return re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', string)[0]
# 得到 作品id 或者 用户id
# 传入 url 支持 https://www.iesdouyin.com 与 https://v.douyin.com
def getKey(self, url):
key = None
key_type = None
try:
r = requests.get(url=url, headers=douyin_headers)
except Exception as e:
print('[ 错误 ]:输入链接有误!\r')
return key_type, key
# 抖音把图集更新为note
# 作品 第一步解析出来的链接是share/video/{aweme_id}
# https://www.iesdouyin.com/share/video/7037827546599263488/?region=CN&mid=6939809470193126152&u_code=j8a5173b&did=MS4wLjABAAAA1DICF9-A9M_CiGqAJZdsnig5TInVeIyPdc2QQdGrq58xUgD2w6BqCHovtqdIDs2i&iid=MS4wLjABAAAAomGWi4n2T0H9Ab9x96cUZoJXaILk4qXOJlJMZFiK6b_aJbuHkjN_f0mBzfy91DX1&with_sec_did=1&titleType=title&schema_type=37&from_ssr=1&utm_source=copy&utm_campaign=client_share&utm_medium=android&app=aweme
# 用户 第一步解析出来的链接是share/user/{sec_uid}
# https://www.iesdouyin.com/share/user/MS4wLjABAAAA06y3Ctu8QmuefqvUSU7vr0c_ZQnCqB0eaglgkelLTek?did=MS4wLjABAAAA1DICF9-A9M_CiGqAJZdsnig5TInVeIyPdc2QQdGrq58xUgD2w6BqCHovtqdIDs2i&iid=MS4wLjABAAAAomGWi4n2T0H9Ab9x96cUZoJXaILk4qXOJlJMZFiK6b_aJbuHkjN_f0mBzfy91DX1&with_sec_did=1&sec_uid=MS4wLjABAAAA06y3Ctu8QmuefqvUSU7vr0c_ZQnCqB0eaglgkelLTek&from_ssr=1&u_code=j8a5173b&timestamp=1674540164&ecom_share_track_params=%7B%22is_ec_shopping%22%3A%221%22%2C%22secuid%22%3A%22MS4wLjABAAAA-jD2lukp--I21BF8VQsmYUqJDbj3FmU-kGQTHl2y1Cw%22%2C%22enter_from%22%3A%22others_homepage%22%2C%22share_previous_page%22%3A%22others_homepage%22%7D&utm_source=copy&utm_campaign=client_share&utm_medium=android&app=aweme
# 合集
# https://www.douyin.com/collection/7093490319085307918
urlstr = str(r.request.path_url)
if "/user/" in urlstr:
# 获取用户 sec_uid
if '?' in r.request.path_url:
for one in re.finditer(r'user\/([\d\D]*)([?])', str(r.request.path_url)):
key = one.group(1)
else:
for one in re.finditer(r'user\/([\d\D]*)', str(r.request.path_url)):
key = one.group(1)
key_type = "user"
elif "/video/" in urlstr:
# 获取作品 aweme_id
key = re.findall('video/(\d+)?', urlstr)[0]
key_type = "aweme"
elif "/note/" in urlstr:
# 获取note aweme_id
key = re.findall('note/(\d+)?', urlstr)[0]
key_type = "aweme"
elif "/mix/detail/" in urlstr:
# 获取合集 id
key = re.findall('/mix/detail/(\d+)?', urlstr)[0]
key_type = "mix"
elif "/collection/" in urlstr:
# 获取合集 id
key = re.findall('/collection/(\d+)?', urlstr)[0]
key_type = "mix"
elif "/music/" in urlstr:
# 获取原声 id
key = re.findall('music/(\d+)?', urlstr)[0]
key_type = "music"
elif "/webcast/reflow/" in urlstr:
key1 = re.findall('reflow/(\d+)?', urlstr)[0]
url = self.urls.LIVE2 + utils.getXbogus(
f'live_id=1&room_id={key1}&app_id=1128')
res = requests.get(url, headers=douyin_headers)
resjson = json.loads(res.text)
key = resjson['data']['room']['owner']['web_rid']
key_type = "live"
elif "live.douyin.com" in r.url:
key = r.url.replace('https://live.douyin.com/', '')
key_type = "live"
if key is None or key_type is None:
print('[ 错误 ]:输入链接有误!无法获取 id\r')
return key_type, key
return key_type, key
def getAwemeInfoApi(self, aweme_id):
if aweme_id is None:
return None
start = time.time() # 开始时间
while True:
try:
jx_url = self.urls.POST_DETAIL + utils.getXbogus(
f'aweme_id={aweme_id}&device_platform=webapp&aid=6383')
raw = requests.get(url=jx_url, headers=douyin_headers).text
datadict = json.loads(raw)
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if datadict['aweme_detail']["images"] is not None:
awemeType = 1
except Exception as e:
pass
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, datadict['aweme_detail'])
return self.result.awemeDict, datadict
def getUserInfoApi(self, sec_uid, mode="post", count=35, max_cursor=0):
if sec_uid is None:
return None
awemeList = []
start = time.time() # 开始时间
while True:
try:
if mode == "post":
url = self.urls.USER_POST + utils.getXbogus(
f'sec_user_id={sec_uid}&count={count}&max_cursor={max_cursor}&device_platform=webapp&aid=6383')
elif mode == "like":
url = self.urls.USER_FAVORITE_A + utils.getXbogus(
f'sec_user_id={sec_uid}&count={count}&max_cursor={max_cursor}&device_platform=webapp&aid=6383')
else:
return None
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
for aweme in datadict["aweme_list"]:
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if aweme["images"] is not None:
awemeType = 1
except Exception as e:
pass
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, aweme)
if self.result.awemeDict is not None and self.result.awemeDict != {}:
awemeList.append(copy.deepcopy(self.result.awemeDict))
return awemeList, datadict, datadict["max_cursor"], datadict["has_more"]
def getLiveInfoApi(self, web_rid: str):
start = time.time() # 开始时间
while True:
try:
live_api = self.urls.LIVE + utils.getXbogus(
f'aid=6383&device_platform=web&web_rid={web_rid}')
response = requests.get(live_api, headers=douyin_headers)
live_json = json.loads(response.text)
if live_json != {} and live_json['status_code'] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
# 清空字典
self.result.clearDict(self.result.liveDict)
# 类型
self.result.liveDict["awemeType"] = 2
# 是否在播
self.result.liveDict["status"] = live_json['data']['data'][0]['status']
if self.result.liveDict["status"] == 4:
return self.result.liveDict, live_json
# 直播标题
self.result.liveDict["title"] = live_json['data']['data'][0]['title']
# 直播cover
self.result.liveDict["cover"] = live_json['data']['data'][0]['cover']['url_list'][0]
# 头像
self.result.liveDict["avatar"] = live_json['data']['data'][0]['owner']['avatar_thumb']['url_list'][0].replace(
"100x100", "1080x1080")
# 观看人数
self.result.liveDict["user_count"] = live_json['data']['data'][0]['user_count_str']
# 昵称
self.result.liveDict["nickname"] = live_json['data']['data'][0]['owner']['nickname']
# sec_uid
self.result.liveDict["sec_uid"] = live_json['data']['data'][0]['owner']['sec_uid']
# 直播间观看状态
self.result.liveDict["display_long"] = live_json['data']['data'][0]['room_view_stats']['display_long']
# 推流
self.result.liveDict["flv_pull_url"] = live_json['data']['data'][0]['stream_url']['flv_pull_url']
try:
# 分区
self.result.liveDict["partition"] = live_json['data']['partition_road_map']['partition']['title']
self.result.liveDict["sub_partition"] = \
live_json['data']['partition_road_map']['sub_partition']['partition']['title']
except Exception as e:
self.result.liveDict["partition"] = ''
self.result.liveDict["sub_partition"] = ''
flv = []
for i, f in enumerate(self.result.liveDict["flv_pull_url"].keys()):
flv.append(f)
self.result.liveDict["flv_pull_url0"] = self.result.liveDict["flv_pull_url"][flv[0]]
return self.result.liveDict, live_json
def getMixInfoApi(self, mix_id: str, count=35, cursor=0):
if mix_id is None:
return None
awemeList = []
start = time.time() # 开始时间
while True:
try:
url = self.urls.USER_MIX + utils.getXbogus(
f'mix_id={mix_id}&cursor={cursor}&count={count}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
if datadict is not None:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
for aweme in datadict["aweme_list"]:
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if aweme["images"] is not None:
awemeType = 1
except Exception as e:
pass
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, aweme)
if self.result.awemeDict is not None and self.result.awemeDict != {}:
awemeList.append(copy.deepcopy(self.result.awemeDict))
return awemeList, datadict, datadict["cursor"], datadict["has_more"]
def getUserAllMixInfoApi(self, sec_uid, count=35, cursor=0):
if sec_uid is None:
return None
mixIdlist = []
start = time.time() # 开始时间
while True:
try:
url = self.urls.USER_MIX_LIST + utils.getXbogus(
f'sec_user_id={sec_uid}&count={count}&cursor={cursor}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
for mix in datadict["mix_infos"]:
mixIdNameDict = {}
mixIdNameDict["https://www.douyin.com/collection/" + mix["mix_id"]] = mix["mix_name"]
mixIdlist.append(mixIdNameDict)
return mixIdlist, datadict, datadict["cursor"], datadict["has_more"]
def getMusicInfoApi(self, music_id: str, count=35, cursor=0):
if music_id is None:
return None
awemeList = []
start = time.time() # 开始时间
while True:
try:
url = self.urls.MUSIC + utils.getXbogus(
f'music_id={music_id}&cursor={cursor}&count={count}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
for aweme in datadict["aweme_list"]:
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if aweme["images"] is not None:
awemeType = 1
except Exception as e:
pass
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, aweme)
if self.result.awemeDict is not None and self.result.awemeDict != {}:
awemeList.append(copy.deepcopy(self.result.awemeDict))
return awemeList, datadict, datadict["cursor"], datadict["has_more"]
def getUserDetailInfoApi(self, sec_uid):
if sec_uid is None:
return None
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
url = self.urls.USER_DETAIL + utils.getXbogus(
f'sec_user_id={sec_uid}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
if datadict is not None and datadict["status_code"] == 0:
return datadict
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
if __name__ == "__main__":
pass

205
apiproxy/douyin/download.py Normal file
View File

@ -0,0 +1,205 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : download.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 15:18
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import os
import json
import time
import requests
from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED
from apiproxy.douyin import douyin_headers
from apiproxy.common import utils
class Download(object):
def __init__(self, thread=5, music=True, cover=True, avatar=True, resjson=True, folderstyle=True):
self.thread = thread
self.music = music
self.cover = cover
self.avatar = avatar
self.resjson = resjson
self.folderstyle = folderstyle
def progressBarDownload(self, url, filepath, desc):
response = requests.get(url, stream=True, headers=douyin_headers)
chunk_size = 1024 # 每次下载的数据大小
content_size = int(response.headers['content-length']) # 下载文件总大小
try:
if response.status_code == 200: # 判断是否响应成功
with open(filepath, 'wb') as file, tqdm(total=content_size,
unit="iB",
desc=desc,
unit_scale=True,
unit_divisor=1024,
) as bar: # 显示进度条
for data in response.iter_content(chunk_size=chunk_size):
size = file.write(data)
bar.update(size)
except Exception as e:
# 下载异常 删除原来下载的文件, 可能未下成功
if os.path.exists(filepath):
os.remove(filepath)
print("[ 错误 ]:下载出错\r")
def awemeDownload(self, awemeDict: dict, savePath=os.getcwd()):
if awemeDict is None:
return
if not os.path.exists(savePath):
os.mkdir(savePath)
try:
# 使用作品 创建时间+描述 当文件夹
file_name = awemeDict["create_time"] + "_" + utils.replaceStr(awemeDict["desc"])
if self.folderstyle:
aweme_path = os.path.join(savePath, file_name)
if not os.path.exists(aweme_path):
os.mkdir(aweme_path)
else:
aweme_path = savePath
# 保存获取到的字典信息
if self.resjson:
try:
with open(os.path.join(aweme_path, file_name + "_result.json"), "w", encoding='utf-8') as f:
f.write(json.dumps(awemeDict, ensure_ascii=False, indent=2))
f.close()
except Exception as e:
print("[ 错误 ]:保存 result.json 失败... 作品名: " + file_name + "\r\n")
desc = file_name[:30]
# 下载 视频
if awemeDict["awemeType"] == 0:
video_path = os.path.join(aweme_path, file_name + "_video.mp4")
if os.path.exists(video_path):
pass
else:
try:
url = awemeDict["video"]["play_addr"]["url_list"][0]
if url != "":
self.isdwownload = False
self.alltask.append(
self.pool.submit(self.progressBarDownload, url, video_path, "[ 视频 ]:" + desc))
except Exception as e:
print("[ 警告 ]:视频下载失败,请重试... 作品名: " + file_name + "\r\n")
# 下载 图集
if awemeDict["awemeType"] == 1:
for ind, image in enumerate(awemeDict["images"]):
image_path = os.path.join(aweme_path, file_name + "_image_" + str(ind) + ".jpeg")
if os.path.exists(image_path):
pass
else:
try:
url = image["url_list"][0]
if url != "":
self.isdwownload = False
self.alltask.append(
self.pool.submit(self.progressBarDownload, url, image_path, "[ 图集 ]:" + desc))
except Exception as e:
print("[ 警告 ]:图片下载失败,请重试... 作品名: " + file_name + "\r\n")
# 下载 音乐
if self.music:
music_name = utils.replaceStr(awemeDict["music"]["title"])
music_path = os.path.join(aweme_path, file_name + "_music_" + music_name + ".mp3")
if os.path.exists(music_path):
pass
else:
try:
url = awemeDict["music"]["play_url"]["url_list"][0]
if url != "":
self.isdwownload = False
self.alltask.append(
self.pool.submit(self.progressBarDownload, url, music_path, "[ 原声 ]:" + desc))
except Exception as e:
print("[ 警告 ]:音乐(原声)下载失败,请重试... 作品名: " + file_name + "\r\n")
# 下载 cover
if self.cover and awemeDict["awemeType"] == 0:
cover_path = os.path.join(aweme_path, file_name + "_cover.jpeg")
if os.path.exists(cover_path):
pass
else:
try:
url = awemeDict["video"]["cover"]["url_list"][0]
if url != "":
self.isdwownload = False
self.alltask.append(
self.pool.submit(self.progressBarDownload, url, cover_path, "[ 封面 ]:" + desc))
except Exception as e:
print("[ 警告 ]:cover下载失败,请重试... 作品名: " + file_name + "\r\n")
# 下载 avatar
if self.avatar:
avatar_path = os.path.join(aweme_path, file_name + "_avatar.jpeg")
if os.path.exists(avatar_path):
pass
else:
try:
url = awemeDict["author"]["avatar"]["url_list"][0]
if url != "":
self.isdwownload = False
self.alltask.append(
self.pool.submit(self.progressBarDownload, url, avatar_path, "[ 头像 ]:" + desc))
except Exception as e:
print("[ 警告 ]:avatar下载失败,请重试... 作品名: " + file_name + "\r\n")
except Exception as e:
print("[ 错误 ]:下载作品时出错\r\n")
def userDownload(self, awemeList: list, savePath=os.getcwd()):
if awemeList is None:
return
if not os.path.exists(savePath):
os.mkdir(savePath)
self.alltask = []
self.pool = ThreadPoolExecutor(max_workers=self.thread)
start = time.time() # 开始时间
for aweme in awemeList:
self.awemeDownload(awemeDict=aweme, savePath=savePath)
wait(self.alltask, return_when=ALL_COMPLETED)
# 检查下载是否完成
while True:
print("[ 提示 ]:正在检查下载是否完成...")
self.isdwownload = True
# 下载上一步失败的
for aweme in awemeList:
self.awemeDownload(awemeDict=aweme, savePath=savePath)
wait(self.alltask, return_when=ALL_COMPLETED)
if self.isdwownload:
break
end = time.time() # 结束时间
print('\n' + '[下载完成]:耗时: %d分钟%d\n' % (int((end - start) / 60), ((end - start) % 60))) # 输出下载用时时间
if __name__ == "__main__":
pass

View File

@ -1,272 +1,315 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- encoding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
@Description:TikTok.py @FileName : result.py
@Date :2023/02/11 13:06:23 @Project : apiproxy
@Author :imgyh @Description:
@version :1.0 @Author : imgyh
@Github :https://github.com/imgyh @Mail : admin@imgyh.com
@Mail :admin@imgyh.com @Github : https://github.com/imgyh
------------------------------------------------- @Site : https://www.imgyh.com
Change Log : @Date : 2023/5/12 15:16
------------------------------------------------- @Version : v1.0
''' @ChangeLog
------------------------------------------------
import time
import copy ------------------------------------------------
'''
class Result(object):
def __init__(self): import time
# 作者信息 import copy
self.authorDict = {
"avatar_thumb": {
"height": "", class Result(object):
"uri": "", def __init__(self):
"url_list": [], # 作者信息
"width": "" self.authorDict = {
}, "avatar_thumb": {
"avatar": { "height": "",
"height": "", "uri": "",
"uri": "", "url_list": [],
"url_list": [], "width": ""
"width": "" },
}, "avatar": {
"cover_url": { "height": "",
"height": "", "uri": "",
"uri": "", "url_list": [],
"url_list": [], "width": ""
"width": "" },
}, "cover_url": {
# 喜欢的作品数 "height": "",
"favoriting_count": "", "uri": "",
# 粉丝数 "url_list": [],
"follower_count": "", "width": ""
# 关注数 },
"following_count": "", # 喜欢的作品数
# 昵称 "favoriting_count": "",
"nickname": "", # 粉丝数
# 是否允许下载 "follower_count": "",
"prevent_download": "", # 关注数
# 用户 url id "following_count": "",
"sec_uid": "", # 昵称
# 是否私密账号 "nickname": "",
"secret": "", # 是否允许下载
# 短id "prevent_download": "",
"short_id": "", # 用户 url id
# 签名 "sec_uid": "",
"signature": "", # 是否私密账号
# 总获赞数 "secret": "",
"total_favorited": "", # 短id
# 用户id "short_id": "",
"uid": "", # 签名
# 用户自定义唯一id 抖音号 "signature": "",
"unique_id": "", # 总获赞数
# 年龄 "total_favorited": "",
"user_age": "", # 用户id
"uid": "",
} # 用户自定义唯一id 抖音号
# 图片信息 "unique_id": "",
self.picDict = { # 年龄
"height": "", "user_age": "",
"mask_url_list": "",
"uri": "", }
"url_list": [], # 图片信息
"width": "" self.picDict = {
} "height": "",
# 音乐信息 "mask_url_list": "",
self.musicDict = { "uri": "",
"cover_hd": { "url_list": [],
"height": "", "width": ""
"uri": "", }
"url_list": [], # 音乐信息
"width": "" self.musicDict = {
}, "cover_hd": {
"cover_large": { "height": "",
"height": "", "uri": "",
"uri": "", "url_list": [],
"url_list": [], "width": ""
"width": "" },
}, "cover_large": {
"cover_medium": { "height": "",
"height": "", "uri": "",
"uri": "", "url_list": [],
"url_list": [], "width": ""
"width": "" },
}, "cover_medium": {
"cover_thumb": { "height": "",
"height": "", "uri": "",
"uri": "", "url_list": [],
"url_list": [], "width": ""
"width": "" },
}, "cover_thumb": {
# 音乐作者抖音号 "height": "",
"owner_handle": "", "uri": "",
# 音乐作者id "url_list": [],
"owner_id": "", "width": ""
# 音乐作者昵称 },
"owner_nickname": "", # 音乐作者抖音号
"play_url": { "owner_handle": "",
"height": "", # 音乐作者id
"uri": "", "owner_id": "",
"url_key": "", # 音乐作者昵称
"url_list": [], "owner_nickname": "",
"width": "" "play_url": {
}, "height": "",
# 音乐名字 "uri": "",
"title": "", "url_key": "",
} "url_list": [],
# 视频信息 "width": ""
self.videoDict = { },
"play_addr": { # 音乐名字
"uri": "", "title": "",
"url_list": "", }
}, # 视频信息
"cover_original_scale": { self.videoDict = {
"height": "", "play_addr": {
"uri": "", "uri": "",
"url_list": [], "url_list": [],
"width": "" },
}, "cover_original_scale": {
"dynamic_cover": { "height": "",
"height": "", "uri": "",
"uri": "", "url_list": [],
"url_list": [], "width": ""
"width": "" },
}, "dynamic_cover": {
"origin_cover": { "height": "",
"height": "", "uri": "",
"uri": "", "url_list": [],
"url_list": [], "width": ""
"width": "" },
}, "origin_cover": {
"cover": { "height": "",
"height": "", "uri": "",
"uri": "", "url_list": [],
"url_list": [], "width": ""
"width": "" },
} "cover": {
} "height": "",
# 作品信息 "uri": "",
self.awemeDict = { "url_list": [],
# 作品创建时间 "width": ""
"create_time": "", }
# awemeType=0 视频, awemeType=1 图集 }
"awemeType": "", # mix信息
# 作品 id self.mixInfo = {
"aweme_id": "", "cover_url": {
# 作者信息 "height": "",
"author": self.authorDict, "uri": "",
# 作品描述 "url_list": [],
"desc": "", "width": 720
# 图片 },
"images": [], "ids": "",
# 音乐 "is_serial_mix": "",
"music": self.musicDict, "mix_id": "",
# 视频 "mix_name": "",
"video": self.videoDict, "mix_pic_type": "",
# 作品信息统计 "mix_type": "",
"statistics": { "statis": {
"admire_count": "", "current_episode": "",
"collect_count": "", "updated_to_episode": ""
"comment_count": "", }
"digg_count": "", }
"play_count": "", # 作品信息
"share_count": "" self.awemeDict = {
} # 作品创建时间
} "create_time": "",
# 用户作品信息 # awemeType=0 视频, awemeType=1 图集, awemeType=2 直播
self.awemeList = [] "awemeType": "",
# 直播信息 # 作品 id
self.liveDict = { "aweme_id": "",
# 是否在播 # 作者信息
"status": "", "author": self.authorDict,
# 直播标题 # 作品描述
"title": "", "desc": "",
# 观看人数 # 图片
"user_count": "", "images": [],
# 昵称 # 音乐
"nickname": "", "music": self.musicDict,
# sec_uid # 合集
"sec_uid": "", "mix_info": self.mixInfo,
# 直播间观看状态 # 视频
"display_long": "", "video": self.videoDict,
# 推流 # 作品信息统计
"flv_pull_url": "", "statistics": {
# 分区 "admire_count": "",
"partition": "", "collect_count": "",
"sub_partition": "" "comment_count": "",
} "digg_count": "",
"play_count": "",
# 将得到的json数据dataRaw精简成自己定义的数据dataNew "share_count": ""
# 转换得到的数据 }
def dataConvert(self, awemeType, dataNew, dataRaw): }
for item in dataNew: # 用户作品信息
try: self.awemeList = []
# 作品创建时间 # 直播信息
if item == "create_time": self.liveDict = {
dataNew['create_time'] = time.strftime( # awemeType=0 视频, awemeType=1 图集, awemeType=2 直播
"%Y-%m-%d %H.%M.%S", time.localtime(dataRaw['create_time'])) "awemeType": "",
continue # 是否在播
# 设置 awemeType "status": "",
if item == "awemeType": # 直播标题
dataNew["awemeType"] = awemeType "title": "",
continue # 直播cover
# 当 解析的链接 是图片时 "cover": "",
if item == "images": # 头像
if awemeType == 1: "avatar": "",
for image in dataRaw[item]: # 观看人数
for i in image: "user_count": "",
self.picDict[i] = image[i] # 昵称
# 字典要深拷贝 "nickname": "",
self.awemeDict["images"].append(copy.deepcopy(self.picDict)) # sec_uid
continue "sec_uid": "",
# 当 解析的链接 是视频时 # 直播间观看状态
if item == "video": "display_long": "",
if awemeType == 0: # 推流
self.dataConvert(awemeType, dataNew[item], dataRaw[item]) "flv_pull_url": "",
continue # 分区
# 将小头像放大 "partition": "",
if item == "avatar": "sub_partition": "",
for i in dataNew[item]: # 最清晰的地址
if i == "url_list": "flv_pull_url0": "",
for j in self.awemeDict["author"]["avatar_thumb"]["url_list"]: }
dataNew[item][i].append(j.replace("100x100", "1080x1080"))
elif i == "uri":
dataNew[item][i] = self.awemeDict["author"]["avatar_thumb"][i].replace("100x100",
"1080x1080") # 将得到的json数据dataRaw精简成自己定义的数据dataNew
else: # 转换得到的数据
dataNew[item][i] = self.awemeDict["author"]["avatar_thumb"][i] def dataConvert(self, awemeType, dataNew, dataRaw):
continue for item in dataNew:
try:
# 原来的json是[{}] 而我们的是 {} # 作品创建时间
if item == "cover_url": if item == "create_time":
self.dataConvert(awemeType, dataNew[item], dataRaw[item][0]) dataNew['create_time'] = time.strftime(
continue "%Y-%m-%d %H.%M.%S", time.localtime(dataRaw['create_time']))
continue
# 根据 uri 获取 1080p 视频 # 设置 awemeType
if item == "play_addr": if item == "awemeType":
dataNew[item]["uri"] = dataRaw["bit_rate"][0]["play_addr"]["uri"] dataNew["awemeType"] = awemeType
# 使用 这个api 可以获得1080p continue
dataNew[item]["url_list"] = "https://aweme.snssdk.com/aweme/v1/play/?video_id=%s&ratio=1080p&line=0" \ # 当 解析的链接 是图片时
% dataNew[item]["uri"] if item == "images":
continue if awemeType == 1:
for image in dataRaw[item]:
# 常规 递归遍历 字典 for i in image:
if isinstance(dataNew[item], dict): self.picDict[i] = image[i]
self.dataConvert(awemeType, dataNew[item], dataRaw[item]) # 字典要深拷贝
else: self.awemeDict["images"].append(copy.deepcopy(self.picDict))
# 赋值 continue
dataNew[item] = dataRaw[item] # 当 解析的链接 是视频时
except Exception as e: if item == "video":
print("[ 警告 ]:转换数据时在接口中未找到 %s\r" % (item)) if awemeType == 0:
self.dataConvert(awemeType, dataNew[item], dataRaw[item])
def clearDict(self, data): continue
for item in data: # 将小头像放大
# 常规 递归遍历 字典 if item == "avatar":
if isinstance(data[item], dict): for i in dataNew[item]:
self.clearDict(data[item]) if i == "url_list":
elif isinstance(data[item], list): for j in self.awemeDict["author"]["avatar_thumb"]["url_list"]:
data[item] = [] dataNew[item][i].append(j.replace("100x100", "1080x1080"))
else: elif i == "uri":
data[item] = "" dataNew[item][i] = self.awemeDict["author"]["avatar_thumb"][i].replace("100x100",
"1080x1080")
else:
dataNew[item][i] = self.awemeDict["author"]["avatar_thumb"][i]
continue
# 原来的json是[{}] 而我们的是 {}
if item == "cover_url":
self.dataConvert(awemeType, dataNew[item], dataRaw[item][0])
continue
# 根据 uri 获取 1080p 视频
if item == "play_addr":
dataNew[item]["uri"] = dataRaw["bit_rate"][0]["play_addr"]["uri"]
# 使用 这个api 可以获得1080p
# dataNew[item]["url_list"] = "https://aweme.snssdk.com/aweme/v1/play/?video_id=%s&ratio=1080p&line=0" \
# % dataNew[item]["uri"]
dataNew[item]["url_list"] = copy.deepcopy(dataRaw["bit_rate"][0]["play_addr"]["url_list"])
continue
# 常规 递归遍历 字典
if isinstance(dataNew[item], dict):
self.dataConvert(awemeType, dataNew[item], dataRaw[item])
else:
# 赋值
dataNew[item] = dataRaw[item]
except Exception as e:
# 删除这个警告, 总是让人误会出错了
# print("[ 警告 ]:转换数据时在接口中未找到 %s\r" % (item))
pass
def clearDict(self, data):
for item in data:
# 常规 递归遍历 字典
if isinstance(data[item], dict):
self.clearDict(data[item])
elif isinstance(data[item], list):
data[item] = []
else:
data[item] = ""
if __name__ == '__main__':
pass

View File

@ -1,69 +1,80 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- encoding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
@Description:TikTok.py @FileName : urls.py
@Date :2023/02/11 13:06:23 @Project : apiproxy
@Author :imgyh @Description:
@version :1.0 @Author : imgyh
@Github :https://github.com/imgyh @Mail : admin@imgyh.com
@Mail :admin@imgyh.com @Github : https://github.com/imgyh
------------------------------------------------- @Site : https://www.imgyh.com
Change Log : @Date : 2023/5/12 15:04
------------------------------------------------- @Version : v1.0
''' @ChangeLog
------------------------------------------------
class Urls(object): ------------------------------------------------
def __init__(self): '''
# https://langyue.cc/APIdocV1.0.html
######################################### WEB #########################################
# 首页推荐 class Urls(object):
self.TAB_FEED = 'https://www.douyin.com/aweme/v1/web/tab/feed/?' def __init__(self):
######################################### WEB #########################################
# 用户短信息给多少个用户secid就返回多少的用户信息 # 首页推荐
self.USER_SHORT_INFO = 'https://www.douyin.com/aweme/v1/web/im/user/info/?' self.TAB_FEED = 'https://www.douyin.com/aweme/v1/web/tab/feed/?'
# 用户详细信息 # 用户短信息给多少个用户secid就返回多少的用户信息
self.USER_DETAIL = 'https://www.douyin.com/aweme/v1/web/user/profile/other/?' self.USER_SHORT_INFO = 'https://www.douyin.com/aweme/v1/web/im/user/info/?'
# 用户作品 # 用户详细信息
# cookies 暂时只需要 __ac_nonce, s_v_web_id两个参数 self.USER_DETAIL = 'https://www.douyin.com/aweme/v1/web/user/profile/other/?'
# url 暂时不需要携带 msToken, X-Bogus, _signature
# 每次返回数据很少 # 用户作品
self.USER_POST = 'https://m.douyin.com/web/api/v2/aweme/post/?' self.USER_POST = 'https://www.douyin.com/aweme/v1/web/aweme/post/?'
# 2023/02/19 失效
# self.USER_POST = 'https://www.douyin.com/aweme/v1/web/aweme/post/?' # 作品信息
self.POST_DETAIL = 'https://www.douyin.com/aweme/v1/web/aweme/detail/?'
# 作品信息
self.POST_DETAIL = 'https://www.douyin.com/aweme/v1/web/aweme/detail/?' # 用户喜欢A
# 需要 odin_tt
# 用户喜欢A self.USER_FAVORITE_A = 'https://www.douyin.com/aweme/v1/web/aweme/favorite/?'
self.USER_FAVORITE_A = 'https://www.douyin.com/aweme/v1/web/aweme/favorite/?'
# 用户喜欢B
# 用户喜欢B self.USER_FAVORITE_B = 'https://www.iesdouyin.com/web/api/v2/aweme/like/?'
self.USER_FAVORITE_B = 'https://www.iesdouyin.com/web/api/v2/aweme/like/?'
# 用户历史
# 用户历史 self.USER_HISTORY = 'https://www.douyin.com/aweme/v1/web/history/read/?'
self.USER_HISTORY = 'https://www.douyin.com/aweme/v1/web/history/read/?'
# 用户收藏
# 用户收藏 self.USER_COLLECTION = 'https://www.douyin.com/aweme/v1/web/aweme/listcollection/?'
self.USER_COLLECTION = 'https://www.douyin.com/aweme/v1/web/aweme/listcollection/?'
# 用户评论
# 用户评论 self.COMMENT = 'https://www.douyin.com/aweme/v1/web/comment/list/?'
self.COMMENT = 'https://www.douyin.com/aweme/v1/web/comment/list/?'
# 首页朋友作品
# 首页朋友作品 self.FRIEND_FEED = 'https://www.douyin.com/aweme/v1/web/familiar/feed/?'
self.FRIEND_FEED = 'https://www.douyin.com/aweme/v1/web/familiar/feed/?'
# 关注用户作品
# 关注用户作品 self.FOLLOW_FEED = 'https://www.douyin.com/aweme/v1/web/follow/feed/?'
self.FOLLOW_FEED = 'https://www.douyin.com/aweme/v1/web/follow/feed/?'
# 合集下所有作品
# X-Bogus Path # 只需要X-Bogus
# 60 秒内,请求同一URI累计超过 600 次,封锁IP 300 秒 self.USER_MIX = 'https://www.douyin.com/aweme/v1/web/mix/aweme/?'
self.GET_XB_PATH = 'https://tiktok.199933.xyz/xb'
# 用户所有合集列表
####################################################################################### # 需要 ttwid
self.USER_MIX_LIST = 'https://www.douyin.com/aweme/v1/web/mix/list/?'
if __name__ == '__main__':
Urls() # 直播
self.LIVE = 'https://live.douyin.com/webcast/room/web/enter/?'
self.LIVE2 = 'https://webcast.amemv.com/webcast/room/reflow/info/?'
# 音乐
self.MUSIC = 'https://www.douyin.com/aweme/v1/web/music/aweme/?'
#######################################################################################
if __name__ == '__main__':
pass

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : __init__.py.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 14:43
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''

111
config.yml Normal file
View File

@ -0,0 +1,111 @@
#######################################
# 说明:
# 1. 井号(#)为注释
# 2. 缩进严格对齐,使用空格缩进, 注意有些冒号后面有一个空格, 有些没有空格
# 3. 请使用英文字符
# 4. 更多yaml语法请上网查看
#######################################
# 作品(视频或图集)、直播、合集、音乐集合、个人主页的分享链接或者电脑浏览器网址
# (删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/ 或者 https://www.douyin.com/开头的)
# 可以设置多个链接, 确保至少一个链接
# 必选
link:
- https://live.douyin.com/759547612580
- https://v.douyin.com/BugmVVD/
- https://v.douyin.com/BugrFTN/
- https://v.douyin.com/B38oovu/
- https://v.douyin.com/S6YMNXs/
# 下载保存位置, 默认当前文件位置
# 必选
path: C:\project\test333
# 是否下载视频中的音乐(True/False), 默认为True
# 可选
music: True
# 是否下载视频的封面(True/False), 默认为True, 当下载视频时有效
# 可选
cover: True
# 是否下载作者的头像(True/False), 默认为True
# 可选
avatar: True
# 是否保存获取到的数据(True/False), 默认为True
# 可选
json: True
folderstyle: True # True -> 每个视频是一个单独的文件夹; False -> 所有视频共用一个文件夹
# True
# user_xxx_xxx
# - like/post/mix
# - 2022-11-28 13.09.56_xxx
# - 2022-11-28 13.09.56_xxx.mp4
# - 2022-11-29 12.09.56_xxx
# - 2022-11-29 12.09.56_xxx.mp4
# False
# user_xxx_xxx
# - like/post/mix
# - 2022-11-28 13.09.56_xxx.mp4
# - 2022-11-29 12.09.56_xxx.mp4
# link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like)或者用户所有合集(mix), 默认为post, 可以设置多种模式
# 可选
mode:
- post
- like
- mix
# 下载作品个数设置
# 可选
number:
post: 5 # 主页下作品下载个数设置, 默认为0 全部下载
like: 5 # 主页下喜欢下载个数设置, 默认为0 全部下载
allmix: 1 # 主页下合集下载个数设置, 默认为0 全部下载
mix: 5 # 单个合集下作品下载个数设置, 默认为0 全部下载
music: 5 # 音乐(原声)下作品下载个数设置, 默认为0 全部下载
database: True # 如果不使用数据库, 增量更新将不可用
# 增量下载, 下载作品范围: 抖音最新作品到本地的最新作品之间的作品, 如果本地没有该链接的任何视频则全部下载
# 可配合 number 选项一起使用
# 情况1: number(假如设置5) 和 increase(假如抖音博主更新了3条作品,本地并未下载) 则会获取5条数据并下载
# 情况2: number(假如设置5) 和 increase(假如抖音博主更新了6条作品,本地并未下载) 则会获取6条数据并下载
# 情况3: number(假如设置5) 和 increase(假如本地并未下载该博主视频) 则会获取所有的视频
# 情况4: 当获取主页所有mix时(mode是mix模式)比较特殊, number(allmix) 控制下载多少个合集, increase(allmix) 对每个合集进行增量更新
# 可选
increase:
post: False # 是否开启主页作品增量下载(True/False), 默认为False
like: False # 是否开启主页喜欢增量下载(True/False), 默认为False
allmix: False # 是否开启主页合集增量下载(True/False), 默认为False
mix: False # 是否开启单个合集下作品增量下载(True/False), 默认为False
music: False # 是否开启音乐(原声)下作品增量下载(True/False), 默认为False
# 设置线程数, 默认5个线程
# 可选
thread: 5
# cookie 请登录网页抖音后F12查看
# cookies 和 cookie 二选一, 要使用这种形式, 请注释下面的cookie
# 目前只需要msToken、ttwid、odin_tt、passport_csrf_token、sid_guard
# 可以动态添加, 程序会根据填的键查找,并没有写死, 如果抖音需要更多的cookie自己加上就行了
cookies:
msToken: xxx
ttwid: xxx
odin_tt: xxx
passport_csrf_token: xxx
sid_guard: xxx
# cookie 请登录网页抖音后F12查看
# cookies 和 cookie 二选一, 要使用这种形式, 请注释上面的cookies及包含的所有键值对
# 设置了这个后上面的cookies选项自动失效, 这个优先级更高
# 格式: "name1=value1; name2=value2;" 注意要加冒号
# 冒号中的内容包括不限于以下键值对, 如果抖音需要更多的cookie自己加上就行了
#cookie: "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

View File

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 157 KiB

View File

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

BIN
img/alipay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
img/wechat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,3 +1,5 @@
requests==2.28.2 requests==2.28.2
flask==2.2.2 flask==2.2.2
pyinstaller==5.7.0 pyinstaller==5.7.0
tqdm==4.65.0
PyYAML==6.0

4
requirements_docker.txt Normal file
View File

@ -0,0 +1,4 @@
requests==2.28.2
flask==2.2.2
tqdm==4.65.0
PyYAML==6.0

View File

@ -6,8 +6,8 @@
* -------------------------- */ * -------------------------- */
@font-face { @font-face {
font-family: 'FontAwesome'; font-family: 'FontAwesome';
src: url('https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/fonts/fontawesome-webfont.eot?v=4.7.0'); src: url('../fonts/fontawesome-webfont.eot?v=4.7.0');
src: url('https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,163 +1,237 @@
// 发 post 请求 // 发 post 请求
function SendAjax() { function SendAjax() {
var data = {}; var data = {};
data = $('#form1').serialize(); data = $('#form1').serialize();
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: "/douyin", url: "/douyin",
data: data, data: data,
dataType: 'json', dataType: 'json',
beforeSend: function () { beforeSend: function () {
$("#loading").attr("style", "display:block;");//在请求后台数据之前显示loading图标 $("#loading").attr("style", "display:block;");//在请求后台数据之前显示loading图标
$("#download").attr("style", "display:none;");//隐藏 download $("#download").attr("style", "display:none;");//隐藏 download
}, },
success: function (result) { success: function (result) {
// console.log(result);//打印服务端返回的数据(调试用) // console.log(result);//打印服务端返回的数据(调试用)
if (result.status_code === 200) { if (result.status_code === 200) {
if (result.awemeType === 0) { result = result.data
$("#awemeType").html("预览视频"); if (result.awemeType === 0) {
$("#video").attr("href", result.video.play_addr.url_list); $("#awemeType").html("预览视频");
$("#pre_video").attr("src", result.video.play_addr.url_list); $("#AwemeOrLive").html("下载视频");
$("#video").attr("style", "display:inline;");//显示 video $("#video").attr("href", removeHttp(result.video.play_addr.url_list[0]));
} $("#pre_video").attr("src", removeHttp(result.video.play_addr.url_list[0]));
if (result.awemeType === 1) { $("#video").attr("style", "display:inline;");//显示 video
$("#awemeType").html("预览图集"); }
var images = result.images; if (result.awemeType === 1) {
var licontent = ""; // 拼接输入的 li 标签的字符串 $("#awemeType").html("预览图集");
for (var i = 0; i < images.length; i++) { var images = result.images;
licontent += "<li><img src= " + images[i].url_list[0] + "></li>" var licontent = ""; // 拼接输入的 li 标签的字符串
} for (var i = 0; i < images.length; i++) {
document.getElementById("images").innerHTML = licontent; licontent += "<li><img src= " + removeHttp(images[i].url_list[0]) + "></li>"
$("#video").attr("style", "display:none;");//隐藏 video }
} document.getElementById("images").innerHTML = licontent;
$("#cover").attr("href", result.video.cover_original_scale.url_list[0]); $("#video").attr("style", "display:none;");//隐藏 video
$("#pre_video").attr("poster", result.video.dynamic_cover.url_list[0]); }
$("#music").attr("href", result.music.play_url.url_list[0]); if (result.awemeType === 0 || result.awemeType === 1) {
$("#cover").attr("href", removeHttp(result.video.cover_original_scale.url_list[0]));
$("#avatar").attr("src", result.author.avatar.url_list[0]); $("#pre_video").attr("poster", removeHttp(result.video.dynamic_cover.url_list[0]));
$("#avatar").attr("alt", result.author.nickname); $("#music").attr("href", removeHttp(result.music.play_url.url_list[0]));
$("#nickname").html(result.author.nickname);
$("#desc").html(result.desc); $("#avatar").attr("src", removeHttp(result.author.avatar.url_list[0]));
$("#avatar").attr("alt", result.author.nickname);
$("#nickname").html(result.author.nickname);
var count = result.statistics.digg_count; $("#desc").html(result.desc);
var digg_count;
if (count < 1000) {
digg_count = count var count = result.statistics.digg_count;
} else if (count >= 1000 && count < 10000) { var digg_count;
digg_count = (count / 1000).toFixed(1) + "K" if (count < 1000) {
} else { digg_count = count
digg_count = (count / 10000).toFixed(1) + "W" } else if (count >= 1000 && count < 10000) {
} digg_count = (count / 1000).toFixed(1) + "K"
$("#aweme_digg_count").html(digg_count); } else {
count = result.statistics.comment_count; digg_count = (count / 10000).toFixed(1) + "W"
var comment_count; }
if (count < 1000) { $("#aweme_digg_count").html(digg_count);
comment_count = count count = result.statistics.comment_count;
} else if (count >= 1000 && count < 10000) { var comment_count;
comment_count = (count / 1000).toFixed(1) + "K" if (count < 1000) {
} else { comment_count = count
comment_count = (count / 10000).toFixed(1) + "W" } else if (count >= 1000 && count < 10000) {
} comment_count = (count / 1000).toFixed(1) + "K"
$("#aweme_comment_count").html(comment_count); } else {
count = result.statistics.collect_count; comment_count = (count / 10000).toFixed(1) + "W"
var collect_count; }
if (count < 1000) { $("#aweme_comment_count").html(comment_count);
collect_count = count count = result.statistics.collect_count;
} else if (count >= 1000 && count < 10000) { var collect_count;
collect_count = (count / 1000).toFixed(1) + "K" if (count < 1000) {
} else { collect_count = count
collect_count = (count / 10000).toFixed(1) + "W" } else if (count >= 1000 && count < 10000) {
} collect_count = (count / 1000).toFixed(1) + "K"
$("#aweme_collect_count").html(collect_count); } else {
count = result.statistics.share_count; collect_count = (count / 10000).toFixed(1) + "W"
var share_count; }
if (count < 1000) { $("#aweme_collect_count").html(collect_count);
share_count = count count = result.statistics.share_count;
} else if (count >= 1000 && count < 10000) { var share_count;
share_count = (count / 1000).toFixed(1) + "K" if (count < 1000) {
} else { share_count = count
share_count = (count / 10000).toFixed(1) + "W" } else if (count >= 1000 && count < 10000) {
} share_count = (count / 1000).toFixed(1) + "K"
$("#aweme_share_count").html(share_count); } else {
share_count = (count / 10000).toFixed(1) + "W"
$("#loading").attr("style", "display:none;");//隐藏 loading }
$("#download").attr("style", "display:block;");//显示 download $("#aweme_share_count").html(share_count);
// alert("SUCCESS");
// 执行弹框 $("#icons").attr("style", "display:flex;");//显示 icons
narnSuccess(); $("#icon").attr("style", "display:table-row;");//显示 icon
} else { $("#music").attr("style", "display:inline;");//显示 music
$("#loading").attr("style", "display:none;");//隐藏 loading
$("#download").attr("style", "display:none;");//隐藏 download $("#loading").attr("style", "display:none;");//隐藏 loading
$("#download").attr("style", "display:block;");//显示 download
// 执行弹框 // alert("SUCCESS");
narnFail(); // 执行弹框
} narnSuccess();
; }
},
error: function (xhr, type) { if (result.awemeType === 2) {
$("#loading").attr("style", "display:none;");//隐藏 loading if (result.status === 4) {
$("#download").attr("style", "display:none;");//隐藏 download $("#loading").attr("style", "display:none;");//隐藏 loading
// alert("异常!"); $("#download").attr("style", "display:none;");//隐藏 download
// 执行弹框
// 执行弹框 narnWarn()
narnFail(); } else {
} $("#AwemeOrLive").html("下载直播");
}); $("#awemeType").html("预览直播");
} $("#video").attr("href", removeHttp(result.flv_pull_url0));
$("#pre_video").attr("src", removeHttp(result.flv_pull_url0));
// 右上角弹框
function narnSuccess() { $("#cover").attr("href", removeHttp(result.cover));
naranja().success({ $("#pre_video").attr("poster", result.cover);
title: '解析成功', $("#avatar").attr("src", removeHttp(result.avatar));
text: '请及时下载音视频', $("#avatar").attr("alt", result.nickname);
icon: true, $("#nickname").html(result.nickname);
timeout: 5000, $("#desc").html(result.title);
buttons: []
}) $("#video").attr("style", "display:inline;");//显示 video
} $("#icons").attr("style", "display:none;");//隐藏 icons
$("#icon").attr("style", "display:none;");//隐藏 icon
function narnFail() { $("#music").attr("style", "display:none;");//隐藏 music
naranja().error({
title: '解析失败', $("#loading").attr("style", "display:none;");//隐藏 loading
text: '视频不存在或接口失效', $("#download").attr("style", "display:block;");//显示 download
icon: true, // alert("SUCCESS");
timeout: 5000, // 执行弹框
buttons: [] narnSuccess();
}) }
} }
} else {
$("#loading").attr("style", "display:none;");//隐藏 loading
window.addEventListener('DOMContentLoaded', function () { $("#download").attr("style", "display:none;");//隐藏 download
document.getElementById('view_aweme').addEventListener('click', function () {
var awemeType = document.getElementById("awemeType").innerText; // 执行弹框
narnFail();
if (awemeType === "预览视频") { }
// 调小音量 ;
var videoElement = document.getElementById("pre_video"); },
videoElement.volume = 0.6 error: function (xhr, type) {
/*弹出视频播放层*/ $("#loading").attr("style", "display:none;");//隐藏 loading
$("#show-video").show(); $("#download").attr("style", "display:none;");//隐藏 download
} // alert("异常!");
// 图片查看器
if (awemeType === "预览图集") { // 执行弹框
narnFail();
var viewer = new Viewer(document.getElementById('images'), { }
hidden: function () { });
viewer.destroy(); }
},
}); // 右上角弹框
function narnSuccess() {
// image.click(); naranja().success({
viewer.show(); title: '解析成功',
} text: '请及时下载音视频',
icon: true,
timeout: 5000,
}); buttons: []
/*关闭视频播放层*/ })
$(".video-close").click(function () { }
var videoElement = document.getElementById("pre_video");
videoElement.pause() function narnFail() {
$("#show-video").hide(); naranja().error({
}) title: '解析失败',
}); text: '直播/视频/图集不存在或接口失效',
icon: true,
timeout: 5000,
buttons: []
})
}
function narnWarn() {
naranja().warn({
title: '提示',
text: '直播未开始',
icon: true,
timeout: 5000,
buttons: []
})
}
window.addEventListener('DOMContentLoaded', function () {
document.getElementById('view_aweme').addEventListener('click', function () {
var awemeType = document.getElementById("awemeType").innerText;
if (awemeType === "预览视频") {
// 调小音量
var videoElement = document.getElementById("pre_video");
videoElement.volume = 0.6
/*弹出视频播放层*/
$("#show-video").show();
}
// 图片查看器
if (awemeType === "预览图集") {
var viewer = new Viewer(document.getElementById('images'), {
hidden: function () {
viewer.destroy();
},
});
// image.click();
viewer.show();
}
// 预览直播
if (awemeType === "预览直播") {
if (flvjs.isSupported()) {//检查flvjs能否正常使用
var videoElement = document.getElementById('pre_video');//使用id选择器找到第二步设置的dom元素
var flvPlayer = flvjs.createPlayer({//创建一个新的flv播放器对象
type: 'flv',//类型flv
url: $("#video").attr("href")//flv文件地址
});
flvPlayer.attachMediaElement(videoElement);//将flv视频装载进video元素内
flvPlayer.load();//载入视频
flvPlayer.play();//播放视频,如果不想要自动播放,去掉本行
/*弹出视频播放层*/
$("#show-video").show();
}
}
});
/*关闭视频播放层*/
$(".video-close").click(function () {
var videoElement = document.getElementById("pre_video");
videoElement.pause()
$("#show-video").hide();
})
});
function removeHttp(url) {
if (typeof (url) == 'string') {
url = url.replace(/^https?:/, '');
}
return url;
}

10
static/js/flv.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,170 +1,175 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zhxx"> <html lang="zhxx">
<head> <head>
<title>抖音去水印工具</title> <title>抖音去水印工具</title>
<!-- Meta tag Keywords --> <!-- Meta tag Keywords -->
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="keywords" content="抖音去水印工具"/> <meta name="keywords" content="抖音去水印工具"/>
<meta name="referrer" content="never"> <meta name="referrer" content="never">
<script> <script>
addEventListener("load", function () { addEventListener("load", function () {
setTimeout(hideURLbar, 0); setTimeout(hideURLbar, 0);
}, false); }, false);
function hideURLbar() { function hideURLbar() {
window.scrollTo(0, 1); window.scrollTo(0, 1);
} }
</script> </script>
<!-- //Meta tag Keywords --> <!-- //Meta tag Keywords -->
<!-- /Favicons --> <!-- /Favicons -->
<link href="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/img/favicon.ico" rel="shortcut icon" type="image/x-icon"> <link href="../static/img/favicon.ico" rel="shortcut icon"
<!-- //Favicons --> type="image/x-icon">
<!--/Style-CSS --> <!-- //Favicons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/css/style.css" type="text/css" media="all"/> <!--/Style-CSS -->
<!--//Style-CSS --> <link rel="stylesheet" href="../static/css/style.css" type="text/css"
<!-- font-awesome-icons --> media="all"/>
<link href="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/css/font-awesome.css" rel="stylesheet"> <!--//Style-CSS -->
<!-- //font-awesome-icons --> <!-- font-awesome-icons -->
<!-- naranja 右下角弹框提示 https://github.com/e1016/naranja--> <link href="//lib.baomitu.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/css/naranja.min.css" type="text/css"/> <!-- //font-awesome-icons -->
<!-- //naranja --> <!-- naranja 右下角弹框提示 https://github.com/e1016/naranja-->
<link rel="stylesheet" href="//unpkg.com/naranja@1.0.1/lib/naranja.min.css" type="text/css"/>
<!-- viewerjs 图片查看器 https://github.com/fengyuanchen/viewerjs--> <!-- //naranja -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/css/viewer.min.css" type="text/css"/>
<!-- //viewerjs --> <!-- viewerjs 图片查看器 https://github.com/fengyuanchen/viewerjs-->
</head> <link rel="stylesheet" href="//lib.baomitu.com/viewerjs/1.11.2/viewer.min.css" type="text/css"/>
<!-- //viewerjs -->
<body> </head>
<div class="error-61-mian"> <body>
<div class="wrapper">
<div class="errors-16-top"> <div class="error-61-mian">
<div class="wrapper">
<p style="color:#00c4b6;font-size:32px;">抖音去水印工具</p> <div class="errors-16-top">
<br>
<p>温馨提示: 粘贴分享链接时 无需删除文案 但如果链接正确但解析失败请删掉文案后重试 https://v.douyin.com/kcvMpuN/</p> <p style="color:#00c4b6;font-size:32px;">抖音去水印工具</p>
<p>关于抖音批量下载与去水印工具的更多实现细节请点击: <a href="https://www.imgyh.com/archives/41.html" target="_blank" style="color:#00c4b6;">抖音批量下载与去水印工具</a></p> <br>
<form id="form1" onsubmit="return false" action="#" method="post" class="d-flex error-page-form"> <p>支持视频/图集/直播解析,粘贴视频/图集/直播分享链接时无需删除文案,但如果链接正确但解析失败请删掉文案后重试 https://v.douyin.com/kcvMpuN/</p>
{# 以前需要手动选择 图片 或者 视频 现在加了自动判断#} {# <p>2.支持直播解析,需要网页版直播链接 https://live.douyin.com/343806013144</p>#}
{# <div class="select">#} <p>关于抖音批量下载与去水印工具的更多实现细节请点击: <a href="https://www.imgyh.com/archives/41.html" target="_blank"
{# <select name="awemeType" required="required">#} style="color:#00c4b6;">抖音批量下载与去水印工具</a></p>
{# <option value="0" selected="selected">视频</option>#} <form id="form1" onsubmit="return false" action="#" method="post" class="d-flex error-page-form">
{# <option value="1">图集</option>#} {# 以前需要手动选择 图片 或者 视频 现在加了自动判断#}
{# </select>#} {# <div class="select">#}
{# </div>#} {# <select name="awemeType" required="required">#}
{# <option value="0" selected="selected">视频</option>#}
<input type="text" placeholder="抖音视频分享地址,复制后粘贴到此处" name="share_link" required="required"> {# <option value="1">图集</option>#}
<button type="reset" onclick="SendAjax()">解析</button> {# </select>#}
</form> {# </div>#}
{# <div class="social-coming-icons">#}
{# <a href="#" title="Facebook" class="footer-fb"><span class="fa fa-facebook"#} <input type="text" placeholder="粘贴视频/图集/直播分享地址" name="share_link" required="required">
{# aria-hidden="true"></span></a>#} <button type="reset" onclick="SendAjax()">解析</button>
{# <a href="#" title="Twitter" class="footer-tw"><span class="fa fa-twitter"#} </form>
{# aria-hidden="true"></span></a>#} {# <div class="social-coming-icons">#}
{# <a href="#" title="Google Plus" class="footer-gg"><span class="fa fa-google-plus"#} {# <a href="#" title="Facebook" class="footer-fb"><span class="fa fa-facebook"#}
{# aria-hidden="true"></span></a>#} {# aria-hidden="true"></span></a>#}
{# <a href="#" title="Linkedin" class="footer-lin"><span class="fa fa-linkedin"#} {# <a href="#" title="Twitter" class="footer-tw"><span class="fa fa-twitter"#}
{# aria-hidden="true"></span></a>#} {# aria-hidden="true"></span></a>#}
{# </div>#} {# <a href="#" title="Google Plus" class="footer-gg"><span class="fa fa-google-plus"#}
{# aria-hidden="true"></span></a>#}
</div> {# <a href="#" title="Linkedin" class="footer-lin"><span class="fa fa-linkedin"#}
<div class="errors-16-mid"> {# aria-hidden="true"></span></a>#}
<div class="loading" id="loading"> {# </div>#}
<div class="shape shape-1"></div>
<div class="shape shape-2"></div> </div>
<div class="shape shape-3"></div> <div class="errors-16-mid">
<div class="shape shape-4"></div> <div class="loading" id="loading">
</div> <div class="shape shape-1"></div>
<div class="shape shape-2"></div>
<div id="download" style="display: none;"> <div class="shape shape-3"></div>
<div class="photo"> <div class="shape shape-4"></div>
<img id="avatar" src="#" class="avatar" alt="avatar"> </div>
<br>
<p id="nickname" class="nickname"></p> <div id="download" style="display: none;">
</div> <div class="photo">
<div class="info"> <img id="avatar" src="#" class="avatar" alt="avatar">
<div class="icons"> <br>
<div class="icon"> <p id="nickname" class="nickname"></p>
<i class="fa fa-heart" style="color:#fd325c;" aria-hidden="true"></i> </div>
<p id="aweme_digg_count"></p> <div class="info">
</div> <div id="icons" class="icons">
<div class="icon"> <div id="icon" class="icon">
<i class="fa fa-comment" style="color:#efeeec;" aria-hidden="true"></i> <i class="fa fa-heart" style="color:#fd325c;" aria-hidden="true"></i>
<p id="aweme_comment_count"></p> <p id="aweme_digg_count"></p>
</div> </div>
<div class="icon"> <div id="icon" class="icon">
<i class="fa fa-star" style="color:#fcb505;" aria-hidden="true"></i> <i class="fa fa-comment" style="color:#efeeec;" aria-hidden="true"></i>
<p id="aweme_collect_count"></p> <p id="aweme_comment_count"></p>
</div> </div>
<div class="icon"> <div id="icon" class="icon">
<i class="fa fa-share" style="color:#e7e8e6;" aria-hidden="true"></i> <i class="fa fa-star" style="color:#fcb505;" aria-hidden="true"></i>
<p id="aweme_share_count"></p> <p id="aweme_collect_count"></p>
</div> </div>
</div> <div id="icon" class="icon">
<br> <i class="fa fa-share" style="color:#e7e8e6;" aria-hidden="true"></i>
<p id="desc"></p> <p id="aweme_share_count"></p>
<br> </div>
<a id="cover" href="#" target="_blank"> </div>
<button type="button" class="btn btn1 "> <br>
<span>下载封面</span> <p id="desc"></p>
</button> <br>
</a> <a id="cover" href="#" target="_blank">
<a id="video" href="#" target="_blank"> <button type="button" class="btn btn1 ">
<button type="button" class="btn btn1"> <span>下载封面</span>
<span>下载视频</span> </button>
</button> </a>
</a> <a id="video" href="#" target="_blank">
<a id="music" href="#" target="_blank"> <button type="button" class="btn btn1">
<button type="button" class="btn btn1"> <span id="AwemeOrLive"></span>
<span>下载音乐</span> </button>
</button> </a>
</a> <a id="music" href="#" target="_blank">
<button id="view_aweme" type="button" class="btn btn1"> <button type="button" class="btn btn1">
<span id="awemeType"></span> <span>下载音乐</span>
</button> </button>
</div> </a>
</div> <button id="view_aweme" type="button" class="btn btn1">
<span id="awemeType"></span>
</div> </button>
</div>
</div> </div>
<div class="copy-right">
<p>Copyright &copy; 2021-2023.我的博客 <a href="https://www.imgyh.com/" target="_blank">GYH's Blog</a> && 项目地址 <a </div>
href="https://github.com/imgyh/douyin" target="_blank">Github</a>
All rights reserved.</p> </div>
</div> <div class="copy-right">
</div> <p>Copyright &copy; 2021-2023.我的博客 <a href="https://www.imgyh.com/" target="_blank">GYH's Blog</a> && 项目地址 <a
href="https://github.com/imgyh/douyin" target="_blank">Github</a>
{# 视频预览效果 https://blog.csdn.net/qq_45140694/article/details/115266928 #} All rights reserved.</p>
<div id="show-video"> </div>
<a class="video-close"> </div>
<span>
<svg t="1614676844098" class="icon" viewBox="0 0 1024 1024" {# 视频预览效果 https://blog.csdn.net/qq_45140694/article/details/115266928 #}
xmlns="http://www.w3.org/2000/svg" p-id="2082" <div id="show-video">
width="30" height="30"> <a class="video-close">
<path d="M591.506286 511.853714l417.133714-416.914285a54.601143 54.601143 0 0 0 0-76.8l-2.267429-2.267429a54.601143 54.601143 0 0 0-76.8 0L512.438857 433.481143 95.305143 15.798857a54.601143 54.601143 0 0 0-76.8 0L16.237714 18.066286a53.577143 53.577143 0 0 0 0 76.8l417.097143 416.987428L16.201143 929.097143a54.601143 54.601143 0 0 0 0 76.8l2.267428 2.267428a54.601143 54.601143 0 0 0 76.8 0l417.170286-417.060571 417.097143 417.097143a54.601143 54.601143 0 0 0 76.8 0l2.267429-2.267429a54.601143 54.601143 0 0 0 0-76.8z" <span>
p-id="2083" fill="#e6e6e6"></path> <svg t="1614676844098" class="icon" viewBox="0 0 1024 1024"
</svg> xmlns="http://www.w3.org/2000/svg" p-id="2082"
</span> width="30" height="30">
</a> <path d="M591.506286 511.853714l417.133714-416.914285a54.601143 54.601143 0 0 0 0-76.8l-2.267429-2.267429a54.601143 54.601143 0 0 0-76.8 0L512.438857 433.481143 95.305143 15.798857a54.601143 54.601143 0 0 0-76.8 0L16.237714 18.066286a53.577143 53.577143 0 0 0 0 76.8l417.097143 416.987428L16.201143 929.097143a54.601143 54.601143 0 0 0 0 76.8l2.267428 2.267428a54.601143 54.601143 0 0 0 76.8 0l417.170286-417.060571 417.097143 417.097143a54.601143 54.601143 0 0 0 76.8 0l2.267429-2.267429a54.601143 54.601143 0 0 0 0-76.8z"
{# https://blog.csdn.net/seeeeeeeeeee/article/details/119981594 #} p-id="2083" fill="#e6e6e6"></path>
<video src="" id="pre_video" controls="controls" poster=""></video> </svg>
</div> </span>
</a>
<div style="display: none"> {# https://blog.csdn.net/seeeeeeeeeee/article/details/119981594 #}
<ul id="images"> <video src="" id="pre_video" controls="controls" poster=""></video>
{# <li><img src="picture-1.jpg" alt="Picture 1"></li>#} </div>
{# <li><img src="picture-2.jpg" alt="Picture 2"></li>#}
{# <li><img src="picture-3.jpg" alt="Picture 3"></li>#} <div style="display: none">
</ul> <ul id="images">
</div> {# <li><img src="picture-1.jpg" alt="Picture 1"></li>#}
{# <li><img src="picture-2.jpg" alt="Picture 2"></li>#}
<script src="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/js/jquery-1.8.2.min.js" type="text/javascript"></script> {# <li><img src="picture-3.jpg" alt="Picture 3"></li>#}
<script src="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/js/naranja.min.js" type="text/javascript"></script> </ul>
<script src="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/js/viewer.min.js" type="text/javascript"></script> </div>
<script src="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/js/custom.js" type="text/javascript"></script>
</body> <script src="//lib.baomitu.com/jquery/1.8.2/jquery.min.js" type="text/javascript"></script>
<script src="//unpkg.com/naranja@1.0.1/lib/naranja.min.js" type="text/javascript"></script>
<script src="//lib.baomitu.com/viewerjs/1.11.2/viewer.min.js" type="text/javascript"></script>
<script src="../static/js/custom.js" type="text/javascript"></script>
<script src="//lib.baomitu.com/flv.js/0.0.2/flv.min.js" type="text/javascript"></script>
</body>
</html> </html>