├── .gitignore ├── GetDanMu.py ├── LICENSE ├── README.md ├── __init__.py ├── basic ├── ass.py └── vars.py ├── config.json ├── methods ├── assbase.py └── sameheight.py ├── pfunc ├── cfunc.py ├── dump_to_ass.py └── request_info.py ├── requirements.txt └── sites ├── iqiyi.py ├── mgtv.py ├── qq.py ├── sohu.py └── youku.py /.gitignore: -------------------------------------------------------------------------------- 1 | # 额外 2 | .vscode/ 3 | releases/ 4 | test/ 5 | *.ass 6 | methods/calc_danmu_pos.py 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | pip-wheel-metadata/ 31 | share/python-wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | *.py,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ -------------------------------------------------------------------------------- /GetDanMu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-04 19:14:39 6 | # 上次编辑时间 : 2020-02-07 19:10:02 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | 10 | import sys 11 | 12 | from argparse import ArgumentParser 13 | 14 | from sites.qq import main as qq 15 | from sites.iqiyi import main as iqiyi 16 | from sites.youku import main as youku 17 | from sites.sohu import main as sohu 18 | from sites.mgtv import main as mgtv 19 | from pfunc.cfunc import check_url_site 20 | 21 | from basic.vars import ALLOW_SITES 22 | 23 | # ------------------------------------------- 24 | # 基本流程 25 | # 1. 根据传入参数确定网站,否则请求输入有关参数或链接。并初始化字幕的基本信息。 26 | # 2. 解析链接得到相关视频/弹幕的参数,以及时长等 27 | # 3. 根据网站对应接口获取全部弹幕 28 | # 4. 转换弹幕 29 | # 5. 写入字幕文件 30 | # ------------------------------------------- 31 | 32 | 33 | def main(): 34 | parser = ArgumentParser(description="视频网站弹幕转换/下载工具,项目地址https://github.com/xhlove/GetDanMu,任何问题请联系vvtoolbox.dev@gmail.com") 35 | parser.add_argument("-f", "--font", default="微软雅黑", help="指定输出字幕字体") 36 | parser.add_argument("-fs", "--font-size", default=28, help="指定输出字幕字体大小") 37 | parser.add_argument("-s", "--site", default="", help=f"使用非url方式下载需指定网站 支持的网站 -> {' '.join(ALLOW_SITES)}") 38 | parser.add_argument("-r", "--range", default="0,720", help="指定弹幕的纵向范围 默认0到720 请用逗号隔开") 39 | parser.add_argument("-cid", "--cid", default="", help="下载cid对应视频的弹幕(腾讯 芒果视频合集)") 40 | parser.add_argument("-vid", "--vid", default="", help="下载vid对应视频的弹幕,支持同时多个vid,需要用逗号隔开") 41 | parser.add_argument("-aid", "--aid", default="", help="下载aid对应视频的弹幕(爱奇艺合集)") 42 | parser.add_argument("-tvid", "--tvid", default="", help="下载tvid对应视频的弹幕,支持同时多个tvid,需要用逗号隔开") 43 | parser.add_argument("-series", "--series", action="store_true", help="尝试通过单集得到合集的全部弹幕") 44 | parser.add_argument("-u", "--url", default="", help="下载视频链接所指向视频的弹幕") 45 | parser.add_argument("-y", "--y", action="store_true", help="默认覆盖原有弹幕而不提示") 46 | args = parser.parse_args() 47 | # print(args.__dict__) 48 | init_args = sys.argv 49 | imode = "command_line" 50 | if init_args.__len__() == 1: 51 | # 双击运行或命令执行exe文件时 传入参数只有exe的路径 52 | # 命令行下执行会传入exe的相对路径(在exe所在路径执行时) 传入完整路径(非exe所在路径下执行) 53 | # 双击运行exe传入完整路径 54 | imode = "non_command_line" 55 | if imode == "non_command_line": 56 | content = input("请输入链接:\n") 57 | check_tip = check_url_site(content) 58 | if check_tip is None: 59 | sys.exit("不支持的网站") 60 | args.url = content 61 | args.site = check_tip 62 | # 要么有url 要么有site和相关参数的组合 63 | if args.url != "": 64 | args.site = check_url_site(args.url) 65 | elif args.site == "": 66 | sys.exit("请传入链接或指定网站+视频相关的参数") 67 | if args.site == "qq": 68 | subtitles = qq(args) 69 | if args.site == "iqiyi": 70 | subtitles = iqiyi(args) 71 | if args.site == "youku": 72 | subtitles = youku(args) 73 | if args.site == "sohu": 74 | subtitles = sohu(args) 75 | if args.site == "mgtv": 76 | subtitles = mgtv(args) 77 | 78 | if __name__ == "__main__": 79 | # 打包 --> pyinstaller GetDanMu.spec 80 | main() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GetDanMu 2 | 3 | [转换/下载各类视频弹幕的工具][1] 4 | 5 | 项目主页:https://github.com/xhlove/GetDanMu 6 | 7 | ## 网站支持 8 | | Site | URL | 单集? | 合集? | 综艺合集? | 支持series? | 9 | | :--: | :-- | :-----: | :-----: | :-----: | :-----: | 10 | | **腾讯视频** | |✓|✓| | 11 | | **爱奇艺** | |✓|✓|✓|✓| 12 | | **优酷** | |✓|✓|✓|✓| 13 | | **搜狐视频** | |✓|✓||| 14 | | **芒果TV** | |✓|✓|✓|✓| 15 | 16 | # 使用示例 17 | - 命令(建议) 18 | 19 | > GetDanMu.exe -s mgtv -r 20,960 -series -u https://www.mgtv.com/b/334727/7452407.html 20 | 21 | - 双击运行 22 | > 提示逻辑有待完善 23 | 24 | - 选项说明 25 | > -f或--font 指定输出字幕字体,默认微软雅黑 26 | > -fs或--font-size 指定输出字幕字体大小,默认28 27 | > -s或--site 使用非url方式下载需指定网站 支持的网站 -> qq iqiyi youku sohu mgtv 28 | > -r或--range 指定弹幕的纵向范围 默认0到720,请用逗号隔开 29 | > -cid或--cid 下载cid对应视频的弹幕(腾讯 芒果视频合集) 30 | > -vid或--vid 下载vid对应视频的弹幕,支持同时多个vid,需要用逗号隔开 31 | > -aid或--aid 下载aid对应视频的弹幕(爱奇艺合集) 32 | > -tvid或--tvid 下载tvid对应视频的弹幕,支持同时多个tvid,需要用逗号隔开 33 | > -series或--series 尝试通过单集得到合集的全部弹幕 默认不使用 34 | > -u或--url 下载视频链接所指向视频的弹幕 35 | > -y或--y 覆盖原有弹幕而不提示 默认不使用 36 | 37 | 38 | - 字体配置文件(可选) 39 | 新建名为`config.json`的文件,内容形式如下: 40 | ```json 41 | { 42 | "fonts_base_folder": "C:/Windows/Fonts", 43 | "fonts": { 44 | "微软雅黑":"msyh.ttc", 45 | "微软雅黑粗体":"msyhbd.ttc", 46 | "微软雅黑细体":"msyhl.ttc" 47 | } 48 | } 49 | ``` 50 | 51 | # 效果示意(字幕与视频不相关) 52 | ![potplayer截屏](http://puui.qpic.cn/vshpic/0/5TLOX3WbgjudEj61IxYZ4tAuf2lFwl-ynf4S5T4sXkdjS9cd_0/0) 53 | [查看使用演示视频点我][2] 54 | 55 | 注意有背景音乐 56 | 57 | 演示是直接使用的python命令,使用exe的话把python GetDanMu.py换成GetDanMu.exe即可 58 | 59 | ## 可能存在的问题 60 | - 下载进度接近100%时暂时没有反应 61 | 62 | 这是因为在全部弹幕获取完后一次性处理所致,对于时间过长和弹幕过多的视频,处理耗时较多,属于正常现象。 63 | - 命令组合未达到预期效果 64 | 65 | 当前的逻辑并不完善,如果出现这种现象请反馈给我。 66 | 67 | # 更新日志 68 | 69 | ## 2020/2/8 70 | - 爱奇艺bug修复 71 | 72 | ## 2020/2/7 73 | - 完善说明 74 | - 爱奇艺支持series选项,并完善地区判断 75 | - 增加字体配置文件,建立字体名称与实际字体文件的映射关系,用于预先设定,方便更准确计算弹幕的分布 76 | - 增加自定义弹幕区间选项,即-r或--range命令 77 | - README完善 78 | 79 | ## 2020/1/28 80 | - 增加芒果TV的支持(支持综艺合集、支持series命令) 81 | - 爱奇艺bug修复 82 | 83 | ## 2020/1/16 84 | - 增加搜狐视频的支持(剧集) 85 | - 改进输入提示(双击运行时) 86 | - 腾讯支持-series设定 87 | 88 | ## 2020/1/11 89 | - 增加优酷弹幕下载,支持合集,支持通过单集直接下载合集弹幕(暂时仅限优酷) 90 | - 改进去重方式 91 | - 优酷的视频id用vid指代,若下载合集请使用连接或通过`-series`选项下载合集弹幕 92 | - 加入下载进度显示,后续可能改进为单行刷新 93 | 94 | ## 2020/1/5 95 | 96 | - 增加了通过链接下载爱奇艺视频弹幕的方法,支持综艺合集。 97 | - 增加通过链接判断网站 98 | 99 | [赞助点此][3] 100 | 101 | [1]: https://blog.weimo.info/archives/431/ 102 | [2]: https://alime-customer-upload-cn-hangzhou.oss-cn-hangzhou.aliyuncs.com/customer-upload/1581073011183_8t14dpgg2bdc.mp4 103 | [3]: https://afdian.net/@vvtoolbox_dev -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xhlove/GetDanMu/6ab886dd8fcacdf9a5ae27c389b7831cc8127abd/__init__.py -------------------------------------------------------------------------------- /basic/ass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-04 19:14:46 6 | # 上次编辑时间 : 2020-02-07 19:21:19 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | 10 | import os 11 | import json 12 | from basic.vars import fonts 13 | 14 | ass_script = """[Script Info] 15 | ; Script generated by N 16 | ScriptType: v4.00+ 17 | PlayResX: 1920 18 | PlayResY: 1080 19 | Aspect Ratio: 1920:1080 20 | Collisions: Normal 21 | WrapStyle: 0 22 | ScaledBorderAndShadow: yes 23 | YCbCr Matrix: TV.601""" 24 | 25 | ass_style_head = """[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding""" 26 | ass_style_default = """Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1""" 27 | ass_style_base = """Style:{font},{font},{font_size},&H00FFFFFF,&H66FFFFFF,&H66000000,&H66000000,0,0,0,0,100,100,0.00,0.00,1,1,0,7,0,0,0,0""" 28 | ass_events_head = """[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text""" 29 | # 基于当前时间范围,在0~1000ms之间停留在(676.571,506.629)处,在1000~3000ms内从位置1300,600移动到360,600(原点在左上) 30 | # ass_baseline = """Dialogue: 0,0:20:08.00,0:20:28.00,Default,,0,0,0,,{\t(1000,3000,\move(1300,600,360,600))\pos(676.571,506.629)}这是字幕内容示意""" 31 | 32 | def get_fonts_info(): 33 | global fonts 34 | fonts_path = r"C:\Windows\Fonts" 35 | if os.path.exists("config.json"): 36 | with open("config.json", "r", encoding="utf-8") as f: 37 | fr = f.read() 38 | try: 39 | config = json.loads(fr) 40 | except Exception as e: 41 | print("get_fonts_info error info ->", e) 42 | else: 43 | fonts_path = config["fonts_base_folder"] 44 | fonts = config["fonts"] 45 | return fonts_path, fonts 46 | 47 | def get_ass_head(font_style_name, font_size): 48 | ass_head = ass_script + "\n\n" + ass_style_head + "\n" + ass_style_base.format(font=font_style_name, font_size=font_size) + "\n\n" + ass_events_head 49 | return ass_head 50 | 51 | def check_font(font): 52 | fonts_path, fonts = get_fonts_info() 53 | maybe_font_path = os.path.join(fonts_path, font) 54 | font_style_name = "微软雅黑" 55 | font_path = os.path.join(fonts_path, fonts[font_style_name]) # 默认 56 | if os.path.exists(font): 57 | # 字体就在当前文件夹 或 完整路径 58 | if os.path.isfile(font): 59 | if os.path.isabs(font): 60 | font_path = font 61 | else: 62 | font_path = os.path.join(os.getcwd(), font) 63 | font_style_name = font[:-os.path.splitext(font)[-1].__len__()] 64 | else: 65 | pass 66 | elif os.path.exists(maybe_font_path): 67 | # 给的是字体文件名 68 | if os.path.isfile(maybe_font_path): 69 | font_path = maybe_font_path 70 | font_style_name = font[:-os.path.splitext(font)[-1].__len__()] 71 | else: 72 | pass 73 | elif fonts.get(font): 74 | # 别名映射 75 | font_path = os.path.join(fonts_path, fonts.get(font)) 76 | font_style_name = font 77 | else: 78 | pass 79 | return font_path, font_style_name -------------------------------------------------------------------------------- /basic/vars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-04 19:14:35 6 | # 上次编辑时间 : 2020-02-07 17:57:05 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | 10 | ALLOW_SITES = ["qq", "iqiyi", "youku", "sohu", "mgtv"] 11 | 12 | qqlive = { 13 | "User-Agent":"qqlive" 14 | } 15 | iqiyiplayer = { 16 | "User-Agent":"Qiyi List Client PC 7.2.102.1343" 17 | } 18 | chrome = { 19 | "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" 20 | } 21 | fonts = { 22 | "微软雅黑":"msyh.ttc", 23 | "微软雅黑粗体":"msyhbd.ttc", 24 | "微软雅黑细体":"msyhl.ttc", 25 | } -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "fonts_base_folder": "C:/Windows/Fonts", 3 | "fonts": { 4 | "微软雅黑":"msyh.ttc", 5 | "微软雅黑粗体":"msyhbd.ttc", 6 | "微软雅黑细体":"msyhl.ttc" 7 | } 8 | } -------------------------------------------------------------------------------- /methods/assbase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-04 19:14:32 6 | # 上次编辑时间: 2020-01-05 14:46:27 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | 10 | from datetime import datetime 11 | from random import randint, choice 12 | 13 | class ASS(object): 14 | 15 | def __init__(self, file_path, get_xy_obj, font="微软雅黑"): 16 | self.font = font 17 | self.get_xy_obj = get_xy_obj 18 | 19 | # 起点位置可以随机在一个区域出现 20 | # 起点位置可以随机在一个区域出现 其他扩展 21 | self.baseline = """Dialogue: 0,{start_time},{end_time},{font},,0,0,0,,{{{move_text}{color}}}{text}""" 22 | self.lines = [] 23 | 24 | def create_new_line(self, comment): 25 | text, color, timepoint = comment 26 | start_time, end_time, show_time = self.set_start_end_time(timepoint) 27 | font = self.set_random_font(line="") 28 | move_text = self.set_start_end_pos(text, show_time) 29 | color = self.set_color(color) 30 | line = self.baseline.format(start_time=start_time, end_time=end_time, font=font, move_text=move_text, color=color, text=text) 31 | self.lines.append(line) 32 | 33 | def set_color(self, color: list): 34 | # \1c&FDA742& 35 | if color.__len__() == 1: 36 | color = "\\1c&{}&".format(color[0].lstrip("#").upper()) 37 | else: 38 | color = "\\1c&{}&".format(choice(color).lstrip("#").upper()) 39 | # color = "\\1c&{}&\\t(0,10000,\\2c&{}&".format(color[0].lstrip("#").upper(), color[1].lstrip("#").upper()) 40 | return color 41 | 42 | def set_start_end_pos(self, text, show_time): 43 | # 考虑不同大小字体下的情况 TODO 44 | # \move(1920,600,360,600) 45 | # min_index = self.get_min_length_used_y() 46 | start_x = 1920 47 | width, height, start_y = self.get_xy_obj.get_xy(text, show_time) 48 | # start_y = self.all_start_y[min_index] 49 | end_x = -(width + randint(0, 30)) 50 | end_y = start_y 51 | move_text = "\\move({},{},{},{})".format(start_x, start_y, end_x, end_y) 52 | # self.update_length_used_y(min_index, text.__len__() * 2) 53 | return move_text 54 | 55 | def set_start_end_time(self, timepoint): 56 | # 40*60*60 fromtimestamp接收的数太小就会出问题 57 | t = 144000 58 | # 记录显示时间 用于计算字幕运动速度 在某刻的位置 最终决定弹幕分布选择 59 | show_time = 15 #randint(10, 20) 60 | st = t + timepoint 61 | et = t + timepoint + show_time 62 | start_time = datetime.fromtimestamp(st).strftime("%H:%M:%S.%f")[1:][:-4] 63 | end_time = datetime.fromtimestamp(et).strftime("%H:%M:%S.%f")[1:][:-4] 64 | return start_time, end_time, show_time 65 | 66 | def set_random_font(self, line=""): 67 | font = self.font 68 | return font -------------------------------------------------------------------------------- /methods/sameheight.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-04 19:14:47 6 | # 上次编辑时间 : 2020-02-07 18:40:42 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | 10 | 11 | from PIL.ImageFont import truetype 12 | 13 | class SameHeight(object): 14 | ''' 15 | # 等高弹幕 --> 矩形分割问题? 16 | ''' 17 | def __init__(self, text, ass_range: str, font_path="msyh.ttc", font_size=14): 18 | self.font = truetype(font=font_path, size=font_size) 19 | self.width, self.height = self.get_danmu_size(text) 20 | self.height_range = [int(n.strip()) for n in ass_range.split(",")] 21 | self.width_range = [0, 1920] 22 | self.lines_start_y = list(range(*(self.height_range + [self.height]))) 23 | self.lines_width_used = [[y, 0] for y in self.lines_start_y] 24 | self.contents = [] 25 | 26 | def get_xy(self, text, show_time): 27 | # 在此之前 务必现将弹幕按时间排序 28 | self.contents.append([text, show_time]) 29 | width, height = self.get_danmu_size(text) 30 | lines_index = self.get_min_width_used() 31 | self.update_width_used(lines_index, width) 32 | start_y = self.lines_start_y[lines_index] 33 | return width, height, start_y 34 | 35 | def get_min_width_used(self): 36 | sorted_width_used = sorted(self.lines_width_used, key=lambda width_used: width_used[1]) 37 | lines_index = self.lines_width_used.index(sorted_width_used[0]) 38 | return lines_index 39 | 40 | def update_width_used(self, index, length): 41 | self.lines_width_used[index][1] += length 42 | 43 | def get_danmu_size(self, text): 44 | # 放在这 不太好 每一次计算都会load下字体 45 | text_width, text_height = self.font.getsize(text) 46 | return text_width + 2, text_height + 2 47 | 48 | 49 | def main(): 50 | text = "测试" 51 | show_time = 13 52 | sh = SameHeight(text, "0,720") 53 | sh.get_xy(text, show_time) 54 | 55 | 56 | if __name__ == "__main__": 57 | main() -------------------------------------------------------------------------------- /pfunc/cfunc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-05 12:45:18 6 | # 上次编辑时间 : 2020-01-16 14:50:34 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | 10 | import hashlib 11 | from urllib.parse import urlparse 12 | 13 | from basic.vars import ALLOW_SITES 14 | 15 | def remove_same_danmu(comments: list): 16 | # 在原有基础上pop会引起索引变化 所以还是采用下面这个方式 17 | contents = [] 18 | for comment in comments: 19 | content, color, timepoint = comment 20 | content = content.replace(" ", "") 21 | if content in contents: 22 | continue 23 | else: 24 | contents.append([content, color, timepoint]) 25 | return contents 26 | 27 | def check_url_site(url): 28 | site = urlparse(url).netloc.split(".")[-2] 29 | if site in ALLOW_SITES: 30 | return site 31 | else: 32 | return None 33 | 34 | def check_url_locale(url): 35 | flag = { 36 | "cn":"zh_cn", 37 | "tw":"zh_tw", 38 | "intl":"intl" 39 | } 40 | if urlparse(url).netloc.split(".")[0] == "tw": 41 | return flag["tw"] 42 | else: 43 | return flag["cn"] 44 | 45 | def yk_msg_sign(msg: str): 46 | return hashlib.new("md5", bytes(msg + "MkmC9SoIw6xCkSKHhJ7b5D2r51kBiREr", "utf-8")).hexdigest() 47 | 48 | def yk_t_sign(token, t, appkey, data): 49 | text = "&".join([token, t, appkey, data]) 50 | return hashlib.new('md5', bytes(text, 'utf-8')).hexdigest() -------------------------------------------------------------------------------- /pfunc/dump_to_ass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-04 19:17:44 6 | # 上次编辑时间 : 2020-02-07 18:17:48 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | import os 10 | 11 | from basic.ass import get_ass_head, check_font 12 | from methods.assbase import ASS 13 | from methods.sameheight import SameHeight 14 | from pfunc.cfunc import remove_same_danmu 15 | 16 | def write_one_video_subtitles(file_path, comments, args): 17 | # 对于合集则每次都都得检查一次 也可以放在上一级 放在这里 考虑后面可能特殊指定字体的情况 18 | font_path, font_style_name = check_font(args.font) 19 | ass_head = get_ass_head(font_style_name, args.font_size) 20 | get_xy_obj = SameHeight("那就写这一句作为初始化测试吧!", args.range, font_path=font_path, font_size=int(args.font_size)) 21 | subtitle = ASS(file_path, get_xy_obj, font=font_style_name) 22 | comments = remove_same_danmu(comments) 23 | for comment in comments: 24 | subtitle.create_new_line(comment) 25 | write_lines_to_file(ass_head, subtitle.lines, file_path) 26 | return comments 27 | 28 | def write_lines_to_file(ass_head, lines, file_path): 29 | with open(file_path, "a+", encoding="utf-8") as f: 30 | f.write(ass_head + "\n") 31 | for line in lines: 32 | f.write(line + "\n") 33 | 34 | def check_file(name, args, fpath=os.getcwd()): 35 | flag = True 36 | file_path = os.path.join(fpath, name + ".ass") 37 | if os.path.isfile(file_path): 38 | if args.y: 39 | os.remove(file_path) 40 | elif args.series: 41 | # 存在重复的 那么直接pass(认为已经下载好了) 42 | flag = False 43 | return flag, file_path 44 | else: 45 | isremove = input("{}已存在,是否覆盖?(y/n):".format(file_path)) 46 | if isremove.strip() == "y": 47 | os.remove(file_path) 48 | else: 49 | flag = False 50 | return flag, file_path 51 | with open(file_path, "wb") as f: 52 | pass 53 | return flag, file_path -------------------------------------------------------------------------------- /pfunc/request_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-04 19:14:43 6 | # 上次编辑时间 : 2020-02-08 21:37:26 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | import re 10 | import json 11 | import requests 12 | 13 | from time import localtime 14 | from pfunc.cfunc import check_url_locale 15 | from basic.vars import qqlive, iqiyiplayer, chrome 16 | 17 | # 放一些仅通过某个id获取另一个/多个id的方法 18 | 19 | #---------------------------------------------qq--------------------------------------------- 20 | 21 | def get_danmu_target_id_by_vid(vid: str): 22 | api_url = "http://bullet.video.qq.com/fcgi-bin/target/regist" 23 | params = { 24 | "otype":"json", 25 | "vid":vid 26 | } 27 | try: 28 | r = requests.get(api_url, params=params, headers=qqlive).content.decode("utf-8") 29 | except Exception as e: 30 | print("target_id requests error info -->", e) 31 | return None 32 | data = json.loads(r.lstrip("QZOutputJson=").rstrip(";")) 33 | target_id = None 34 | if data.get("targetid"): 35 | target_id = data["targetid"] 36 | return target_id 37 | 38 | def get_all_vids_by_column_id(): 39 | # https://s.video.qq.com/get_playsource?id=85603&plat=2&type=4&data_type=3&video_type=10&year=2019&month=&plname=qq&otype=json 40 | # 综艺类型的 41 | pass 42 | 43 | def get_cid_by_vid(vid): 44 | api_url = "http://union.video.qq.com/fcgi-bin/data" 45 | params = { 46 | "tid": "98", 47 | "appid": "10001005", 48 | "appkey": "0d1a9ddd94de871b", 49 | "idlist": vid, 50 | "otype":"json" 51 | } 52 | r = requests.get(api_url, params=params, headers=qqlive).content.decode("utf-8") 53 | data = json.loads(r.lstrip("QZOutputJson=").rstrip(";")) 54 | try: 55 | cid = data["results"][0]["fields"] 56 | except Exception as e: 57 | print("load fields error info -->", e) 58 | return None 59 | if cid.get("sync_cover"): 60 | return cid["sync_cover"] 61 | elif cid.get("cover_list"): 62 | return cid["cover_list"][0] 63 | return 64 | 65 | def get_all_vids_by_cid(cid): 66 | api_url = "http://union.video.qq.com/fcgi-bin/data" 67 | params = { 68 | "tid":"431", 69 | "appid":"10001005", 70 | "appkey":"0d1a9ddd94de871b", 71 | "idlist":cid, 72 | "otype":"json" 73 | } 74 | r = requests.get(api_url, params=params, headers=qqlive).content.decode("utf-8") 75 | data = json.loads(r.lstrip("QZOutputJson=").rstrip(";")) 76 | try: 77 | nomal_ids = json.loads(data["results"][0]["fields"]["nomal_ids"]) 78 | except Exception as e: 79 | print("load nomal_ids error info -->", e) 80 | return None 81 | # F 2是免费 7是会员 0是最新正片之前的预告 4是正片之后的预告 82 | vids = [item["V"] for item in nomal_ids if item["F"] in [2, 7]] 83 | return vids 84 | 85 | #---------------------------------------------qq--------------------------------------------- 86 | 87 | #-------------------------------------------iqiyi-------------------------------------------- 88 | 89 | def get_vinfos(aid, locale="zh_cn"): 90 | api_url = "http://cache.video.iqiyi.com/avlist/{}/0/".format(aid) 91 | if locale != "zh_cn": 92 | api_url += "?locale=" + locale 93 | try: 94 | r = requests.get(api_url, headers=chrome, timeout=5).content.decode("utf-8") 95 | except Exception as e: 96 | print("get_vinfos requests error info -->", e) 97 | return None 98 | data = json.loads(r[len("var videoListC="):]) 99 | try: 100 | vlist = data["data"]["vlist"] 101 | except Exception as e: 102 | print("get_vinfos load vlist error info -->", e) 103 | return None 104 | vinfos = [[v["shortTitle"] + "_" + str(v["timeLength"]), v["timeLength"], v["id"]] for v in vlist] 105 | return vinfos 106 | 107 | def matchit(patterns, text): 108 | ret = None 109 | for pattern in patterns: 110 | match = re.match(pattern, text) 111 | if match: 112 | ret = match.group(1) 113 | break 114 | return ret 115 | 116 | def duration_to_sec(duration: str): 117 | return sum(x * int(t) for x, t in zip([3600, 60, 1][2 - duration.count(":"):], duration.split(":"))) 118 | 119 | def get_year_range(aid, locale="zh_cn"): 120 | # 获取第一个和最新一个视频的年份,生成列表返回,遇到任何错误则返回当前年份 121 | year_start = year_end = localtime().tm_year 122 | api_url = "http://pcw-api.iqiyi.com/album/album/baseinfo/{}".format(aid) 123 | if locale != "zh_cn": 124 | api_url += "?locale=" + locale 125 | try: 126 | r = requests.get(api_url, headers=chrome, timeout=5).content.decode("utf-8") 127 | except Exception as e: 128 | print("error info -->", e) 129 | return list(range(year_start, year_end + 1)) 130 | data = json.loads(r)["data"] 131 | if data.get("firstVideo"): 132 | year_start = int(data["firstVideo"]["period"][:4]) 133 | if data.get("latestVideo"): 134 | year_end = int(data["latestVideo"]["period"][:4]) 135 | return list(range(year_start, year_end + 1)) 136 | 137 | def get_vinfo_by_tvid(tvid, locale="zh_cn", isall=False): 138 | api_url = "https://pcw-api.iqiyi.com/video/video/baseinfo/{}".format(tvid) 139 | if locale != "zh_cn": 140 | api_url += "?locale=" + locale 141 | try: 142 | r = requests.get(api_url, headers=chrome, timeout=5).content.decode("utf-8") 143 | except Exception as e: 144 | print("error info -->", e) 145 | return 146 | data = json.loads(r)["data"] 147 | if data.__class__ != dict: 148 | return None 149 | if isall: 150 | aid = data.get("albumId") 151 | if aid is None: 152 | print("通过单集tvid获取合集aid失败,将只下载单集的弹幕") 153 | locale = check_video_area_by_tvid(tvid) 154 | if locale is None: 155 | locale = "zh_cn" 156 | return get_vinfos(aid, locale=locale) 157 | name = data["name"] 158 | duration = data["durationSec"] 159 | return [[name + "_" + str(duration), duration, tvid]] 160 | 161 | def check_video_area_by_tvid(tvid): 162 | api_url = "https://pcw-api.iqiyi.com/video/video/playervideoinfo?tvid={}".format(tvid) 163 | try: 164 | r = requests.get(api_url, headers=chrome, timeout=5).content.decode("utf-8") 165 | except Exception as e: 166 | print("check_video_area_by_tvid error info -->", e) 167 | return None 168 | data = json.loads(r)["data"] 169 | intl_flag = data["operation_base"]["is_international"] 170 | langs = [item["language"].lower() for item in data["operation_language_base"]] 171 | locale = "zh_cn" 172 | if intl_flag is False and "zh_tw" in langs: 173 | locale = "zh_tw" 174 | return locale 175 | 176 | def get_vinfos_by_year(aid, years: list, cid=6, locale="zh_cn"): 177 | api_url = "https://pcw-api.iqiyi.com/album/source/svlistinfo?cid={}&sourceid={}&timelist={}".format(cid, aid, ",".join([str(_) for _ in years.copy()])) 178 | if locale != "zh_cn": 179 | api_url += "&locale=" + locale 180 | try: 181 | r = requests.get(api_url, headers=chrome, timeout=5).content.decode("utf-8") 182 | except Exception as e: 183 | print("get_vinfos_by_year error info -->", e) 184 | return None 185 | data = json.loads(r)["data"] 186 | vinfos = [] 187 | for year in years: 188 | if year.__class__ != str: 189 | year = str(year) 190 | if data.get(year) is None: 191 | continue 192 | for ep in data[year]: 193 | sec = duration_to_sec(ep["duration"]) 194 | vinfos.append([ep["shortTitle"] + "_" + str(sec), sec, ep["tvId"]]) 195 | return vinfos 196 | 197 | def get_vinfos_by_url(url, isall=False): 198 | locale = check_url_locale(url) 199 | patterns = [".+?/w_(\w+?).html", ".+?/v_(\w+?).html", ".+?/a_(\w+?).html", ".+?/lib/m_(\w+?).html"] 200 | isw, isep, isas, isms = [re.match(pattern, url) for pattern in patterns] 201 | if isw is None and isep is None and isas is None and isms is None: 202 | return None 203 | try: 204 | r = requests.get(url, headers=chrome, timeout=5).content.decode("utf-8") 205 | except Exception as e: 206 | print("get_vinfos_by_url error info -->", e) 207 | return None 208 | cid_patterns = ["[\s\S]+?\.cid.+?(\d+)", "[\s\S]+?cid: \"(\d+)\"", "[\s\S]+?channelID.+?\"(\d+)\""] 209 | cid = matchit(cid_patterns, r) 210 | aid_patterns = ["[\s\S]+?aid:'(\d+)'", "[\s\S]+?albumid=\"(\d+)\"", "[\s\S]+?movlibalbumaid=\"(\d+)\"", "[\s\S]+?data-score-tvid=\"(\d+)\""] 211 | aid = matchit(aid_patterns, r) 212 | tvid_patterns = ["[\s\S]+?\"tvid\":\"(\d+)\"", "[\s\S]+?\['tvid'\].+?\"(\d+)\""] 213 | tvid = matchit(tvid_patterns, r) 214 | if cid is None: 215 | cid = "" 216 | elif cid == "6" and isas or isms:#对于综艺合集需要获取年份 217 | # year_patterns = ["[\s\S]+?datePublished.+?(\d\d\d\d)-\d\d-\d\d", "[\s\S]+?data-year=\"(\d+)\""] 218 | # year = matchit(year_patterns, r) 219 | # if year is None: 220 | # years = [localtime().tm_year] 221 | # else: 222 | # years = [year] 223 | years = get_year_range(aid, locale=locale) 224 | else: 225 | pass#暂时没有其他的情况计划特别处理 226 | 227 | if isep or isw: 228 | if tvid is None: 229 | return 230 | return get_vinfo_by_tvid(tvid, locale=locale, isall=isall) 231 | 232 | if isas or isms: 233 | if aid is None: 234 | return 235 | if cid == "6": 236 | return get_vinfos_by_year(aid, years, locale=locale) 237 | else: 238 | return get_vinfos(aid, locale=locale) 239 | 240 | #-------------------------------------------iqiyi-------------------------------------------- 241 | 242 | #-------------------------------------------youku-------------------------------------------- 243 | 244 | def get_vinfos_by_url_youku(url, isall=False): 245 | vid_patterns = ["[\s\S]+?youku.com/video/id_(/+?)\.html", "[\s\S]+?youku.com/v_show/id_(.+?)\.html"] 246 | video_id = matchit(vid_patterns, url) 247 | show_id_patterns = ["[\s\S]+?youku.com/v_nextstage/id_(/+?)\.html", "[\s\S]+?youku.com/show/id_z(.+?)\.html", "[\s\S]+?youku.com/show_page/id_z(.+?)\.html", "[\s\S]+?youku.com/alipay_video/id_(.+?)\.html"] 248 | show_id = matchit(show_id_patterns, url) 249 | if video_id is None and show_id is None: 250 | return None 251 | if video_id: 252 | return get_vinfos_by_video_id(video_id, isall=isall) 253 | if show_id.__len__() == 20 and show_id == show_id.lower(): 254 | return get_vinfos_by_show_id(show_id) 255 | else: 256 | return get_vinfos_by_video_id(show_id, isall=isall) 257 | 258 | def get_vinfos_by_video_id(video_id, isall=False): 259 | api_url = "https://openapi.youku.com/v2/videos/show.json?client_id=53e6cc67237fc59a&package=com.huawei.hwvplayer.youku&ext=show&video_id={}".format(video_id) 260 | try: 261 | r = requests.get(api_url, headers=chrome, timeout=5).content.decode("utf-8") 262 | except Exception as e: 263 | print("get_vinfos_by_video_id error info -->", e) 264 | return None 265 | data = json.loads(r) 266 | if isall: 267 | show_id = data["show"]["id"] 268 | return get_vinfos_by_show_id(show_id) 269 | duration = 0 270 | if data.get("duration"): 271 | duration = int(float(data["duration"])) 272 | if data.get("title"): 273 | name = data["title"] + "_" + str(duration) 274 | else: 275 | name = "优酷未知" + "_" + str(duration) 276 | vinfo = [name, duration, video_id] 277 | return [vinfo] 278 | 279 | def get_vinfos_by_show_id(show_id): 280 | api_url = "https://openapi.youku.com/v2/shows/videos.json?show_videotype=正片&count=100&client_id=53e6cc67237fc59a&page=1&show_id={}&package=com.huawei.hwvplayer.youku".format(show_id) 281 | try: 282 | r = requests.get(api_url, headers=chrome, timeout=5).content.decode("utf-8") 283 | except Exception as e: 284 | print("get_vinfos_by_show_id error info -->", e) 285 | return None 286 | data = json.loads(r)["videos"] 287 | if data.__len__() == 0: 288 | return None 289 | vinfos = [] 290 | for video in data: 291 | duration = 0 292 | if video.get("duration"): 293 | duration = int(float(video["duration"])) 294 | if video.get("title"): 295 | name = video["title"] + "_" + str(duration) 296 | else: 297 | name = "优酷未知_{}".format(video["id"]) + "_" + str(duration) 298 | vinfos.append([name, duration, video["id"]]) 299 | return vinfos 300 | #-------------------------------------------youku-------------------------------------------- -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.22.0 2 | Pillow==7.0.0 3 | xmltodict==0.12.0 4 | -------------------------------------------------------------------------------- /sites/iqiyi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-04 19:14:41 6 | # 上次编辑时间 : 2020-02-08 21:37:36 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | 10 | import json 11 | import requests 12 | 13 | from zlib import decompress 14 | from xmltodict import parse 15 | 16 | from basic.vars import iqiyiplayer 17 | from pfunc.dump_to_ass import check_file, write_one_video_subtitles 18 | from pfunc.request_info import get_vinfos, get_vinfos_by_url, get_vinfo_by_tvid 19 | 20 | 21 | def get_danmu_by_tvid(name, duration, tvid): 22 | # http://cmts.iqiyi.com/bullet/41/00/10793494100_300_3.z 23 | if tvid.__class__ == int: 24 | tvid = str(tvid) 25 | api_url = "http://cmts.iqiyi.com/bullet/{}/{}/{}_{}_{}.z" 26 | timestamp = 300 27 | index = 0 28 | max_index = duration // timestamp + 1 29 | comments = [] 30 | while index < max_index: 31 | url = api_url.format(tvid[-4:-2], tvid[-2:], tvid, timestamp, index + 1) 32 | # print(url) 33 | try: 34 | r = requests.get(url, headers=iqiyiplayer).content 35 | except Exception as e: 36 | print("error info -->", e) 37 | continue 38 | try: 39 | raw_xml = decompress(bytearray(r), 15+32).decode('utf-8') 40 | except Exception as e: 41 | index += 1 42 | continue 43 | try: 44 | entry = parse(raw_xml)["danmu"]["data"]["entry"] 45 | except Exception as e: 46 | index += 1 47 | continue 48 | # with open("raw_xml.json", "w", encoding="utf-8") as f: 49 | # f.write(json.dumps(parse(raw_xml), ensure_ascii=False, indent=4)) 50 | if entry.__class__ != list: 51 | entry = [entry] 52 | for comment in entry: 53 | if comment.get("list") is None: 54 | continue 55 | bulletInfo = comment["list"]["bulletInfo"] 56 | if bulletInfo.__class__ != list: 57 | bulletInfo = [bulletInfo] 58 | for info in bulletInfo: 59 | color = [info["color"]] 60 | comments.append([info["content"], color, int(comment["int"])]) 61 | print("已下载{:.2f}%".format(index * timestamp * 100 / duration)) 62 | index += 1 63 | comments = sorted(comments, key=lambda _: _[-1]) 64 | return comments 65 | 66 | 67 | def main(args): 68 | vinfos = [] 69 | isall = False 70 | if args.series: 71 | isall = True 72 | if args.tvid: 73 | vi = get_vinfo_by_tvid(args.tvid, isall=isall) 74 | if vi: 75 | vinfos.append(vi) 76 | if args.aid: 77 | vi = get_vinfos(args.aid) 78 | if vi: 79 | vinfos += vi 80 | if args.tvid == "" and args.aid == "" and args.url == "": 81 | args.url = input("请输入iqiyi链接:\n") 82 | if args.url: 83 | vi = get_vinfos_by_url(args.url, isall=isall) 84 | if vi: 85 | vinfos += vi 86 | subtitles = {} 87 | for name, duration, tvid in vinfos: 88 | print(name, "开始下载...") 89 | flag, file_path = check_file(name, args) 90 | if flag is False: 91 | print("跳过{}".format(name)) 92 | continue 93 | comments = get_danmu_by_tvid(name, duration, tvid) 94 | comments = write_one_video_subtitles(file_path, comments, args) 95 | subtitles.update({file_path:comments}) 96 | return subtitles -------------------------------------------------------------------------------- /sites/mgtv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-28 15:55:22 6 | # 上次编辑时间 : 2020-02-07 18:32:05 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | import re 10 | import json 11 | import time 12 | import base64 13 | import requests 14 | from uuid import uuid4 15 | from collections import OrderedDict 16 | 17 | from basic.vars import chrome 18 | from pfunc.request_info import duration_to_sec 19 | from pfunc.dump_to_ass import check_file, write_one_video_subtitles 20 | 21 | pno_params = { 22 | "pad":"1121", 23 | "ipad":"1030" 24 | } 25 | type_params = { 26 | "h5flash":"h5flash", 27 | "padh5":"padh5", 28 | "pch5":"pch5" 29 | } 30 | 31 | def get_danmu_by_vid(vid: str, cid: str, duration: int): 32 | api_url = "https://galaxy.bz.mgtv.com/rdbarrage" 33 | params = OrderedDict({ 34 | "version": "2.0.0", 35 | "vid": vid, 36 | "abroad": "0", 37 | "pid": "", 38 | "os": "", 39 | "uuid": "", 40 | "deviceid": "", 41 | "cid": cid, 42 | "ticket": "", 43 | "time": "0", 44 | "mac": "", 45 | "platform": "0", 46 | "callback": "" 47 | }) 48 | comments = [] 49 | index = 0 50 | max_index = duration // 60 + 1 51 | while index < max_index: 52 | params["time"] = str(index * 60 * 1000) 53 | try: 54 | r = requests.get(api_url, params=params, headers=chrome, timeout=3).content.decode("utf-8") 55 | except Exception as e: 56 | continue 57 | items = json.loads(r)["data"]["items"] 58 | index += 1 59 | if items is None: 60 | continue 61 | for item in items: 62 | comments.append([item["content"], ["ffffff"], int(item["time"] / 1000)]) 63 | print("已下载{:.2f}%".format(index / max_index * 100)) 64 | return comments 65 | 66 | def get_tk2(did): 67 | pno = pno_params["ipad"] 68 | ts = str(int(time.time())) 69 | text = f"did={did}|pno={pno}|ver=0.3.0301|clit={ts}" 70 | tk2 = base64.b64encode(text.encode("utf-8")).decode("utf-8").replace("+", "_").replace("/", "~").replace("=", "-") 71 | return tk2[::-1] 72 | 73 | def get_vinfos_by_cid_or_vid(xid: str, flag="vid"): 74 | api_url = "https://pcweb.api.mgtv.com/episode/list" 75 | params = { 76 | "video_id": xid, 77 | "page": "0", 78 | "size": "25", 79 | "cxid": "", 80 | "version": "5.5.35", 81 | "callback": "", 82 | "_support": "10000000", 83 | "_": str(int(time.time() * 1000)) 84 | } 85 | if flag == "cid": 86 | _ = params.pop("video_id") 87 | params["collection_id"] = xid 88 | page = 1 89 | vinfos = [] 90 | while True: 91 | params["page"] = page 92 | try: 93 | r = requests.get(api_url, params=params, headers=chrome, timeout=3).content.decode("utf-8") 94 | except Exception as e: 95 | continue 96 | data = json.loads(r)["data"] 97 | for ep in data["list"]: 98 | if re.match("\d\d\d\d-\d\d-\d\d", ep["t4"]): 99 | # 综艺的加上日期 100 | name = "{t4}_{t3}_{t2}".format(**ep).replace(" ", "") 101 | else: 102 | name = "{t3}_{t2}".format(**ep).replace(" ", "") 103 | duration = duration_to_sec(ep["time"]) 104 | vinfos.append([name, duration, ep["video_id"], ep["clip_id"]]) 105 | if page < data["count"] // 25 + 1: 106 | page += 1 107 | else: 108 | break 109 | return vinfos 110 | 111 | def get_vinfo_by_vid(vid: str): 112 | api_url = "https://pcweb.api.mgtv.com/player/video" 113 | type_ = type_params["pch5"] 114 | did = uuid4().__str__() 115 | suuid = uuid4().__str__() 116 | params = OrderedDict({ 117 | "did": did, 118 | "suuid": suuid, 119 | "cxid": "", 120 | "tk2": get_tk2(did), 121 | "video_id": vid, 122 | "type": type_, 123 | "_support": "10000000", 124 | "auth_mode": "1", 125 | "callback": "" 126 | }) 127 | try: 128 | r = requests.get(api_url, params=params, headers=chrome, timeout=3).content.decode("utf-8") 129 | except Exception as e: 130 | return 131 | info = json.loads(r)["data"]["info"] 132 | name = "{title}_{series}_{desc}".format(**info).replace(" ", "") 133 | duration = int(info["duration"]) 134 | cid = info["collection_id"] 135 | return [name, duration, vid, cid] 136 | 137 | def get_vinfos_by_url(url: str, isall: bool): 138 | vinfos = [] 139 | # url = https://www.mgtv.com/b/323323/4458375.html 140 | ids = re.match("[\s\S]+?mgtv.com/b/(\d+)/(\d+)\.html", url) 141 | # url = "https://www.mgtv.com/h/333999.html?fpa=se" 142 | cid_v1 = re.match("[\s\S]+?mgtv.com/h/(\d+)\.html", url) 143 | # url = "https://m.mgtv.com/h/333999/0.html" 144 | cid_v2 = re.match("[\s\S]+?mgtv.com/h/(\d+)/\d\.html", url) 145 | if ids is None and cid_v1 is None and cid_v2 is None: 146 | return 147 | if ids and ids.groups().__len__() == 2: 148 | cid, vid = ids.groups() 149 | if isall: 150 | vi = get_vinfos_by_cid_or_vid(vid) 151 | if vi: 152 | vinfos += vi 153 | else: 154 | vinfo = get_vinfo_by_vid(vid) 155 | if vinfo is None: 156 | return 157 | vinfos.append(vinfo) 158 | if cid_v1 or cid_v2: 159 | if cid_v2 is None: 160 | cid = cid_v1.group(1) 161 | else: 162 | cid = cid_v2.group(1) 163 | vi = get_vinfos_by_cid_or_vid(cid, flag="cid") 164 | if vi: 165 | vinfos += vi 166 | return vinfos 167 | 168 | def main(args): 169 | vinfos = [] 170 | isall = False 171 | if args.series: 172 | isall = True 173 | if args.url: 174 | vi = get_vinfos_by_url(args.url, isall) 175 | if vi: 176 | vinfos += vi 177 | if args.vid: 178 | if isall: 179 | vi = get_vinfos_by_cid_or_vid(args.vid) 180 | if vi: 181 | vinfos += vi 182 | else: 183 | vi = get_vinfo_by_vid(args.vid) 184 | if vi: 185 | vinfos.append(vi) 186 | if args.cid: 187 | vi = get_vinfos_by_cid_or_vid(args.cid) 188 | if vi: 189 | vinfos += vi 190 | subtitles = {} 191 | for name, duration, vid, cid in vinfos: 192 | print(name, "开始下载...") 193 | flag, file_path = check_file(name, args) 194 | if flag is False: 195 | print("跳过{}".format(name)) 196 | continue 197 | comments = get_danmu_by_vid(vid, cid, duration) 198 | write_one_video_subtitles(file_path, comments, args) 199 | subtitles.update({file_path:comments}) 200 | print(name, "下载完成!") 201 | return subtitles 202 | 203 | if __name__ == "__main__": 204 | args = object() 205 | args.url = "https://www.mgtv.com/h/333999.html?fpa=se" 206 | main(args) -------------------------------------------------------------------------------- /sites/qq.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-04 19:14:37 6 | # 上次编辑时间 : 2020-01-16 20:04:51 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | 10 | import os 11 | import sys 12 | import json 13 | import requests 14 | 15 | from basic.vars import qqlive 16 | from pfunc.dump_to_ass import check_file, write_one_video_subtitles 17 | from pfunc.request_info import get_cid_by_vid 18 | from pfunc.request_info import get_all_vids_by_cid as get_vids 19 | from pfunc.request_info import get_danmu_target_id_by_vid as get_target_id 20 | 21 | 22 | def get_video_info_by_vid(vids: list): 23 | idlist = ",".join(vids) 24 | api_url = "http://union.video.qq.com/fcgi-bin/data" 25 | params = { 26 | "tid":"98", 27 | "appid":"10001005", 28 | "appkey":"0d1a9ddd94de871b", 29 | "idlist":f"{idlist}", 30 | "otype":"json" 31 | } 32 | try: 33 | r = requests.get(api_url, params=params, headers=qqlive).content.decode("utf-8") 34 | except Exception as e: 35 | print("error info -->", e) 36 | return 37 | data = json.loads(r.lstrip("QZOutputJson=").rstrip(";")) 38 | if not data.get("results"): 39 | return 40 | subkey = ["title", "episode", "langue", "duration"] 41 | vinfos = [] 42 | for index, item in enumerate(data["results"]): 43 | vid = [vids[index]] 44 | values = [str(item["fields"][key]) for key in subkey if item["fields"].get(key) is not None] 45 | name = "_".join(values) 46 | if item["fields"].get("duration"): 47 | duration = int(item["fields"]["duration"]) 48 | else: 49 | duration = 0 50 | # target_id = item["fields"]["targetid"] # 这个target_id一般不是弹幕用的 51 | target_id = get_target_id(vid) 52 | if target_id is None: 53 | continue 54 | vinfos.append([vid, name, duration, target_id]) 55 | # print(vinfos) 56 | return vinfos 57 | 58 | def get_danmu_by_target_id(vid: str, duration: int, target_id, font="微软雅黑", font_size=25): 59 | # timestamp间隔30s 默认从15开始 60 | api_url = "https://mfm.video.qq.com/danmu" 61 | params = { 62 | "otype":"json", 63 | "target_id":"{}&vid={}".format(target_id, vid), 64 | "session_key":"0,0,0", 65 | "timestamp":15 66 | } 67 | # subtitle = ASS(file_path, font=font, font_size=font_size) 68 | comments = [] 69 | while params["timestamp"] < duration: 70 | try: 71 | r = requests.get(api_url, params=params, headers=qqlive).content.decode("utf-8") 72 | except Exception as e: 73 | print("error info -->", e) 74 | continue 75 | try: 76 | danmu = json.loads(r) 77 | except Exception as e: 78 | danmu = json.loads(r, strict=False) 79 | if danmu.get("count") is None: 80 | # timestamp不变 再试一次 81 | continue 82 | danmu_count = danmu["count"] 83 | for comment in danmu["comments"]: 84 | if comment["content_style"]: 85 | style = json.loads(comment["content_style"]) 86 | if style.get("gradient_colors"): 87 | color = style["gradient_colors"] 88 | elif style.get("color"): 89 | color = style["color"] 90 | else: 91 | color = ["ffffff"] 92 | else: 93 | color = ["ffffff"] 94 | comments.append([comment["content"], color, comment["timepoint"]]) 95 | print("已下载{:.2f}%".format(params["timestamp"]*100/duration)) 96 | params["timestamp"] += 30 97 | comments = sorted(comments, key=lambda _: _[-1]) 98 | return comments 99 | 100 | 101 | def get_one_subtitle_by_vinfo(vinfo, font="微软雅黑", font_size=25, args=""): 102 | vid, name, duration, target_id = vinfo 103 | print(name, "开始下载...") 104 | flag, file_path = check_file(name, args) 105 | if flag is False: 106 | print("跳过{}".format(name)) 107 | return 108 | comments = get_danmu_by_target_id(vid, duration, target_id, font=font, font_size=font_size) 109 | # print("{}弹幕下载完成!".format(name)) 110 | return comments, file_path 111 | 112 | def ask_input(url="", isall=False): 113 | if url == "": 114 | url = input("请输入vid/coverid/链接,输入q退出:\n").strip() 115 | if url == "q" or url == "": 116 | sys.exit("已结束") 117 | # https://v.qq.com/x/cover/m441e3rjq9kwpsc/i0025secmkz.html 118 | params = url.replace(".html", "").split("/") 119 | if params[-1].__len__() == 11: 120 | vids = [params[-1]] 121 | if isall: 122 | cid = get_cid_by_vid(params[-1]) 123 | vids += get_vids(cid) 124 | elif params[-1].__len__() == 15: 125 | cid = params[-1] 126 | vids = get_vids(cid) 127 | else: 128 | vid = url.split("vid=")[-1] 129 | if len(vid) != 11: 130 | vid = vid.split("&")[0] 131 | if len(vid) != 11: 132 | sys.exit("没找到vid") 133 | vids = [vid] 134 | return vids 135 | 136 | 137 | def main(args): 138 | vids = [] 139 | isall = False 140 | if args.series: 141 | isall = True 142 | if args.cid and args.cid.__len__() == 15: 143 | vids += get_vids(args.cid) 144 | if args.vid: 145 | if args.vid.strip().__len__() == 11: 146 | vids += [args.vid.strip()] 147 | elif args.vid.strip().__len__() > 11: 148 | vids += [vid for vid in args.vid.strip().replace(" ", "").split(",") if vid.__len__() == 11] 149 | else: 150 | pass 151 | if args.series: 152 | cid = get_cid_by_vid(args.vid) 153 | vids += get_vids(cid) 154 | if args.url: 155 | vids += ask_input(url=args.url, isall=isall) 156 | if args.vid == "" and args.cid == "" and args.url == "": 157 | vids += ask_input(isall=isall) 158 | if vids.__len__() <= 0: 159 | sys.exit("没有任何有效输入") 160 | vids_bak = vids 161 | vids = [] 162 | for vid in vids_bak: 163 | if vid in vids: 164 | continue 165 | else: 166 | vids.append(vid) 167 | vinfos = get_video_info_by_vid(vids) 168 | subtitles = {} 169 | for vinfo in vinfos: 170 | infos = get_one_subtitle_by_vinfo(vinfo, args.font, args.font_size, args=args) 171 | if infos is None: 172 | continue 173 | comments, file_path = infos 174 | comments = write_one_video_subtitles(file_path, comments, args) 175 | subtitles.update({file_path:comments}) 176 | return subtitles 177 | 178 | # if __name__ == "__main__": 179 | # # 打包 --> pyinstaller -F .\qq.py -c -n GetDanMu_qq_1.1 180 | # main() 181 | # # subtitle = ASS() -------------------------------------------------------------------------------- /sites/sohu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-16 17:45:35 6 | # 上次编辑时间 : 2020-02-07 18:43:55 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | import json 10 | import requests 11 | 12 | from basic.vars import chrome 13 | from pfunc.request_info import matchit 14 | from pfunc.dump_to_ass import check_file, write_one_video_subtitles 15 | 16 | def try_decode(content): 17 | flag = False 18 | methods = ["gbk", "utf-8"] 19 | for method in methods: 20 | try: 21 | content_decode = content.decode(method) 22 | except Exception as e: 23 | print("try {} decode method failed.".format(method)) 24 | continue 25 | flag = True 26 | break 27 | if flag is True: 28 | return content_decode 29 | else: 30 | return None 31 | 32 | def get_vinfos_by_url(url: str): 33 | ep_url = matchit(["[\s\S]+?tv.sohu.com/v/(.+?)\.html", "[\s\S]+?tv.sohu.com/(.+?)/(.+?)\.html"], url) 34 | aid_url = matchit(["[\s\S]+?tv.sohu.com/album/.(\d+)\.shtml"], url) 35 | vid_url = matchit(["[\s\S]+?tv.sohu.com/v(\d+)\.shtml"], url) 36 | if ep_url: 37 | try: 38 | r = requests.get(url, headers=chrome, timeout=3).content 39 | except Exception as e: 40 | print(e) 41 | print("get sohu (url -> {}) ep url failed.".format(url)) 42 | return 43 | r_decode = try_decode(r) 44 | if r_decode is None: 45 | print("ep response use decode failed(url -> {}).".format(url)) 46 | return None 47 | vid = matchit(["[\s\S]+?var vid.+?(\d+)"], r_decode) 48 | if vid: 49 | vinfo = get_vinfo_by_vid(vid) 50 | if vinfo is None: 51 | return 52 | else: 53 | return [vinfo] 54 | else: 55 | print("match sohu vid (url -> {}) failed.".format(url)) 56 | return None 57 | if aid_url: 58 | return get_vinfos(aid_url) 59 | if vid_url: 60 | vinfo = get_vinfo_by_vid(vid_url) 61 | if vinfo is None: 62 | return 63 | else: 64 | return [vinfo] 65 | if ep_url is None and aid_url is None and vid_url is None: 66 | # 可能是合集页面 67 | try: 68 | r = requests.get(url, headers=chrome, timeout=3).content 69 | except Exception as e: 70 | print("get sohu (url -> {}) album url failed.".format(url)) 71 | return 72 | r_decode = try_decode(r) 73 | if r_decode is None: 74 | print("album response decode failed(url -> {}).".format(url)) 75 | return None 76 | aid = matchit(["[\s\S]+?var playlistId.+?(\d+)"], r_decode) 77 | if aid: 78 | return get_vinfos(aid) 79 | return 80 | 81 | 82 | def get_vinfos(aid: str): 83 | api_url = "https://pl.hd.sohu.com/videolist" 84 | params = { 85 | "callback": "", 86 | "playlistid": aid, 87 | "o_playlistId": "", 88 | "pianhua": "0", 89 | "pagenum": "1", 90 | "pagesize": "999", 91 | "order": "0", # 0 从小到大 92 | "cnt": "1", 93 | "pageRule": "2", 94 | "withPgcVideo": "0", 95 | "ssl": "0", 96 | "preVideoRule": "3", 97 | "_": "" # 1579167883430 98 | } 99 | try: 100 | r = requests.get(api_url, params=params, headers=chrome, timeout=3).content.decode("gbk") 101 | except Exception as e: 102 | print("get sohu (aid -> {}) videolist failed.".format(aid)) 103 | return None 104 | data = json.loads(r) 105 | if data.get("videos"): 106 | videos = data["videos"] 107 | else: 108 | print("videolist has no videos (aid -> {}).".format(aid)) 109 | return None 110 | vinfos = [[video["name"], int(float(video["playLength"])), video["vid"], aid] for video in videos] 111 | return vinfos 112 | 113 | 114 | def get_vinfo_by_vid(vid: str): 115 | api_url = "https://hot.vrs.sohu.com/vrs_flash.action" 116 | params = { 117 | "vid": vid, 118 | "ver": "31", 119 | "ssl": "1", 120 | "pflag": "pch5" 121 | } 122 | try: 123 | r = requests.get(api_url, params=params, headers=chrome, timeout=3).content.decode("utf-8") 124 | except Exception as e: 125 | print("get sohu (vid -> {}) vinfo failed.".format(vid)) 126 | return None 127 | data = json.loads(r) 128 | if data.get("status") == 1: 129 | aid = "" 130 | if data.get("pid"): 131 | aid = str(data["pid"]) 132 | if data.get("data"): 133 | data = data["data"] 134 | else: 135 | print("vid -> {} vinfo request return no data.".format(vid)) 136 | return 137 | else: 138 | print("vid -> {} vinfo request return error.".format(vid)) 139 | return 140 | return [data["tvName"], int(float(data["totalDuration"])), vid, aid] 141 | 142 | def get_danmu_all_by_vid(vid: str, aid: str, duration: int): 143 | api_url = "https://api.danmu.tv.sohu.com/dmh5/dmListAll" 144 | params = { 145 | "act": "dmlist_v2", 146 | "dct": "1", 147 | "request_from": "h5_js", 148 | "vid": vid, 149 | "page": "1", 150 | "pct": "2", 151 | "from": "PlayerType.SOHU_VRS", 152 | "o": "4", 153 | "aid": aid, 154 | "time_begin": "0", 155 | "time_end": str(duration) 156 | } 157 | try: 158 | r = requests.get(api_url, params=params, headers=chrome, timeout=3).content.decode("utf-8") 159 | except Exception as e: 160 | print("get sohu (vid -> {}) danmu failed.".format(vid)) 161 | return None 162 | data = json.loads(r)["info"]["comments"] 163 | comments = [] 164 | for comment in data: 165 | comments.append([comment["c"], "ffffff", comment["v"]]) 166 | comments = sorted(comments, key=lambda _: _[-1]) 167 | return comments 168 | 169 | def main(args): 170 | vinfos = [] 171 | if args.vid: 172 | vi = get_vinfo_by_vid(args.vid) 173 | if vi: 174 | vinfos.append(vi) 175 | if args.aid: 176 | vi = get_vinfos(args.aid) 177 | if vi: 178 | vinfos += vi 179 | if args.vid == "" and args.aid == "" and args.url == "": 180 | args.url = input("请输入sohu链接:\n") 181 | if args.url: 182 | vi = get_vinfos_by_url(args.url) 183 | if vi: 184 | vinfos += vi 185 | subtitles = {} 186 | for name, duration, vid, aid in vinfos: 187 | print(name, "开始下载...") 188 | flag, file_path = check_file(name, args) 189 | if flag is False: 190 | print("跳过{}".format(name)) 191 | continue 192 | comments = get_danmu_all_by_vid(vid, aid, duration) 193 | if comments is None: 194 | print(name, "弹幕获取失败了,记得重试~(@^_^@)~") 195 | continue 196 | comments = write_one_video_subtitles(file_path, comments, args) 197 | subtitles.update({file_path:comments}) 198 | print(name, "下载完成!") 199 | return subtitles -------------------------------------------------------------------------------- /sites/youku.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # coding=utf-8 3 | ''' 4 | # 作者: weimo 5 | # 创建日期: 2020-01-05 14:52:21 6 | # 上次编辑时间 : 2020-01-16 19:59:08 7 | # 一个人的命运啊,当然要靠自我奋斗,但是... 8 | ''' 9 | import re 10 | import time 11 | import json 12 | import base64 13 | import requests 14 | 15 | from basic.vars import chrome 16 | from pfunc.dump_to_ass import check_file, write_one_video_subtitles 17 | from pfunc.cfunc import yk_msg_sign, yk_t_sign 18 | from pfunc.request_info import get_vinfos_by_show_id, get_vinfos_by_video_id, get_vinfos_by_url_youku 19 | 20 | def get_tk_enc(): 21 | """ 22 | 获取优酷的_m_h5_tk和_m_h5_tk_enc 23 | """ 24 | api_url = "https://acs.youku.com/h5/mtop.com.youku.aplatform.weakget/1.0/?jsv=2.5.1&appKey=24679788" 25 | try: 26 | r = requests.get(api_url, headers=chrome, timeout=5) 27 | except Exception as e: 28 | return 29 | tk_enc = dict(r.cookies) 30 | if tk_enc.get("_m_h5_tk_enc") and tk_enc.get("_m_h5_tk"): 31 | return tk_enc 32 | return 33 | 34 | def get_cna(): 35 | api_url = "https://log.mmstat.com/eg.js" 36 | try: 37 | r = requests.get(api_url, headers=chrome, timeout=5) 38 | except Exception as e: 39 | return 40 | cookies = dict(r.cookies) 41 | if cookies.get("cna"): 42 | return cookies["cna"] 43 | return 44 | 45 | def get_danmu_by_mat(vid, cna, mat: int, comments: list): 46 | api_url = "https://acs.youku.com/h5/mopen.youku.danmu.list/1.0/" 47 | tm = str(int(time.time() * 1000)) 48 | msg = { 49 | "ctime": tm, 50 | "ctype": 10004, 51 | "cver": "v1.0", 52 | "guid": cna, 53 | "mat": mat, 54 | "mcount": 1, 55 | "pid": 0, 56 | "sver": "3.1.0", 57 | "type": 1, 58 | "vid": vid} 59 | msg_b64encode = base64.b64encode(json.dumps(msg, separators=(',', ':')).encode("utf-8")).decode("utf-8") 60 | msg.update({"msg":msg_b64encode}) 61 | msg.update({"sign":yk_msg_sign(msg_b64encode)}) 62 | # 测试发现只要有Cookie的_m_h5_tk和_m_h5_tk_enc就行 63 | tk_enc = get_tk_enc() 64 | if tk_enc is None: 65 | return 66 | headers = { 67 | "Content-Type":"application/x-www-form-urlencoded", 68 | "Cookie":";".join([k + "=" + v for k, v in tk_enc.items()]), 69 | "Referer": "https://v.youku.com" 70 | } 71 | headers.update(chrome) 72 | t = str(int(time.time() * 1000)) 73 | data = json.dumps(msg, separators=(',', ':')) 74 | params = { 75 | "jsv":"2.5.6", 76 | "appKey":"24679788", 77 | "t":t, 78 | "sign":yk_t_sign(tk_enc["_m_h5_tk"][:32], t, "24679788", data), 79 | "api":"mopen.youku.danmu.list", 80 | "v":"1.0", 81 | "type":"originaljson", 82 | "dataType":"jsonp", 83 | "timeout":"20000", 84 | "jsonpIncPrefix":"utility" 85 | } 86 | try: 87 | r = requests.post(api_url, params=params, data={"data":data}, headers=headers, timeout=5).content.decode("utf-8") 88 | except Exception as e: 89 | print("youku danmu request failed.", e) 90 | return "once again" 91 | result = json.loads(json.loads(r)["data"]["result"])["data"]["result"] 92 | for item in result: 93 | comment = item["content"] 94 | c_int = json.loads(item["propertis"])["color"] 95 | if c_int.__class__ == str: 96 | c_int = int(c_int) 97 | color = hex(c_int)[2:].zfill(6) 98 | timepoint = item["playat"] / 1000 99 | comments.append([comment, [color], timepoint]) 100 | return comments 101 | 102 | def main(args): 103 | cna = get_cna() 104 | if cna is None: 105 | # 放前面 免得做无用功 106 | return 107 | isall = False 108 | if args.series: 109 | isall = True 110 | vinfos = [] 111 | if args.url: 112 | vi = get_vinfos_by_url_youku(args.url, isall=isall) 113 | if vi: 114 | vinfos += vi 115 | if args.vid: 116 | vi = get_vinfos_by_video_id(args.vid, isall=isall) 117 | if vi: 118 | vinfos += vi 119 | subtitles = {} 120 | for name, duration, video_id in vinfos: 121 | print(name, "开始下载...") 122 | flag, file_path = check_file(name, args=args) 123 | if flag is False: 124 | print("跳过{}".format(name)) 125 | continue 126 | max_mat = duration // 60 + 1 127 | comments = [] 128 | for mat in range(max_mat): 129 | result = get_danmu_by_mat(video_id, cna, mat + 1, comments) 130 | if result is None: 131 | continue 132 | elif result == "once again": 133 | # 可能改成while好点 134 | result = get_danmu_by_mat(video_id, cna, mat + 1, comments) 135 | if result is None: 136 | continue 137 | comments = result 138 | print("已下载{}/{}".format(mat + 1, max_mat)) 139 | comments = write_one_video_subtitles(file_path, comments, args) 140 | subtitles.update({file_path:comments}) 141 | return subtitles --------------------------------------------------------------------------------