├── .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 | 
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
--------------------------------------------------------------------------------