├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE
├── README.md
├── app
└── DataBase
│ ├── __init__.py
│ ├── micro_msg.py
│ ├── misc.py
│ ├── msg.py
│ └── sns.py
├── decrypter
├── db_decrypt.py
├── image_decrypt.py
└── video_decrypt.py
├── doc
├── manual_guide.md
└── pic
│ ├── 中文问号.png
│ ├── 主界面.png
│ ├── 四幅图.png
│ ├── 展开朋友.png
│ ├── 打开搜一搜.png
│ ├── 打开朋友圈.png
│ ├── 朋友圈页面.png
│ ├── 点击完成.png
│ ├── 点击搜索.png
│ └── 限定时间.png
├── entity
├── comment.py
├── contact.py
└── moment_msg.py
├── exporter
├── avatar_exporter.py
├── emoji_exporter.py
├── html_exporter.py
├── image_exporter.py
└── video_exporter.py
├── gui
├── __init__.py
├── auto_scroll_guide.py
├── auto_scrolls_single_guide.py
├── gui.py
├── listbox_with_search.py
└── tool_tip.py
├── helper
├── auto_scroll.py
└── auto_scroll_single.py
├── log.py
├── main.py
├── main.spec
├── requirements.txt
├── resource
├── auto_gui
│ ├── 1366
│ │ ├── complete.png
│ │ ├── friends.png
│ │ ├── moments_tab.png
│ │ └── search_button.png
│ ├── 1600
│ │ ├── complete.png
│ │ ├── friends.png
│ │ ├── moments_tab.png
│ │ └── search_button.png
│ ├── 1920
│ │ ├── complete.png
│ │ ├── friends.png
│ │ ├── moments_tab.png
│ │ └── search_button.png
│ ├── 2560_100
│ │ ├── complete.png
│ │ ├── friends.png
│ │ ├── moments_tab.png
│ │ └── search_button.png
│ ├── 2560_125
│ │ ├── complete.png
│ │ ├── friends.png
│ │ ├── moments_tab.png
│ │ └── search_button.png
│ ├── 2560_175
│ │ ├── complete.png
│ │ ├── friends.png
│ │ ├── moments_tab.png
│ │ └── search_button.png
│ ├── complete.png
│ ├── friends.png
│ ├── moments_tab.png
│ └── search_button.png
├── ffmpeg.exe
├── ffprobe.exe
├── gui_pictures
│ ├── open_moments_guide -原始.png
│ ├── open_moments_guide.png
│ ├── open_search_guide-原始.png
│ └── open_search_guide.png
├── template.html
└── template
│ ├── css
│ ├── bootstrap.min.css
│ └── index.css
│ ├── icons
│ ├── comment-downarrow.svg
│ ├── comment-uparrow.svg
│ └── massage.svg
│ └── js
│ ├── bootstrap.min.js
│ ├── index.js
│ └── jquery-2.1.1.min.js
└── test.py
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3 |
4 | name: 打包发布
5 |
6 | on:
7 | workflow_dispatch:
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | pyinstaller-build:
14 | runs-on: windows-latest
15 | steps:
16 | - name: Create Executable
17 | uses: sayyid5416/pyinstaller@v1
18 | with:
19 | python_ver: '3.11'
20 | requirements: 'requirements.txt'
21 | spec: 'main.spec'
22 | upload_exe_with_name: 'wechat_moments'
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | log/
3 | build/
4 | dist/
5 | output/
6 | app/DataBase/Msg
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [技术爬爬虾(B站 抖音 Youtube同名)]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
WechatMoments
2 |
3 | # 微信朋友圈导出工具
4 |
5 | # 一、项目介绍
6 |
7 |
8 | ## 0. 求助
9 | 四月底微信对朋友圈图片进行了加密,如果知道如何解密的大佬请指点一下。(帮忙Issue或者PR)
10 | 通过抓包可知朋友圈图片是这样一个请求,现在问题是请求过来的数据不知道是怎么加密的
11 | http://shmmsns.qpic.cn/mmsns/uGxMq1C4wvppcjBbyweK796GtT1hH3LGISYajZ2v7C11XhHk5icyDUXcWNSPk2MooeIa8Es5hXP0/0?idx=1&token=WSEN6qDsKwV8A02w3onOGQYfxnkibdqSOkmHhZGNB4DFumlE9p1vp0e0xjHoXlbbXRzwnQia6X5t3Annc4oqTuDg
12 |
13 | 加密后字节数与原图片一致,可能是某种流式数据加密。key可能是11079841251888681493。
14 |
15 | ## 1. 项目简介
16 |
17 | * [WechatMoments](https://github.com/tech-shrimp/WechatMoments)是一款运行在Windows上的,备份导出朋友圈为html的工具
18 | * 作者:[技术爬爬虾](https://space.bilibili.com/316183842)(B站 抖音 Youtube同名)更多有趣实用项目请关注
19 | * 开源许可: Apache License
20 | * 分发,宣传,二次开发等请注明原作者
21 |
22 | ## 2. 使用说明
23 | * ### [视频演示-Bilibili](https://www.bilibili.com/video/BV1qq421A7aF/)
24 | * (1) 安装[Windows版微信](https://pc.weixin.qq.com/),并且登陆
25 | * (2) 在[Releases](https://github.com/tech-shrimp/WechatMoments/releases)下载压缩包wechat_moments.zip
26 | * (3) 解压文件夹(路径不要包含中文)
27 | * (4) 管理员身份运行wechat_moments.exe,并按提示操作
28 | * (5) 如发生异常,重启微信,重启软件
29 |
30 | # 二. 详细介绍
31 |
32 | ## 1. 核心功能
33 |
34 | * 导出微信朋友圈数据为HTML
35 | * 可以下载图片/视频离线查看,永久保存
36 | * 可以根据联系人,朋友圈时间进行过滤导出
37 | * 强依赖微信Windows客户端,只提供windows版本
38 | * 只测试过python3.11+Win10/Win11,其他环境随缘
39 | * 软件只能导出在电脑微信**浏览过**的朋友圈记录
40 | * 浏览朋友圈方法,参考[电脑微信浏览朋友圈](/doc/manual_guide.md)
41 | * 
42 |
43 | ## 2. 已知问题
44 |
45 | * 视频下载不稳定,视频可能不全(如有解决方案欢迎PR)
46 | * 只能开小号导出自己朋友圈(如有解决方案欢迎PR)
47 | * HTML页面比较原始
48 | * 自动浏览朋友圈的功能不稳定(如有解决方案欢迎PR)
49 |
50 |
51 | ## 3. 常见问题与解决方法
52 | 问:图片为什么无法导出
53 | - 答:2024年5月微信对图片进行了加密,2024年后的朋友圈数据清先浏览朋友圈,点开图片缓存到本地,再使用此工具才能导出图片。
54 |
55 | 问:怎么导出自己朋友圈
56 | - 答:登陆另外一个账户搜自己,详见[电脑微信浏览朋友圈](/doc/manual_guide.md)
57 | - 目前没有其他方案,主要我不知道怎么在电脑端查看自己的朋友圈,如有解决方案欢迎PR
58 |
59 | 问:为什么导出的数据不全?
60 | - 答:软件只能导出在电脑微信**浏览过**的朋友圈记录,未浏览过的无法导出。
61 |
62 | 问:怎么在电脑微信浏览朋友圈?
63 | - 答:软件提供了两种自动浏览朋友圈的方法,第一种浏览全部,缺点是最多只能刷到前100天。第二种浏览单个朋友,没有时间限制。
64 |
65 | 问:自动浏览单个朋友功能失效!
66 | - 答:可以手动操作,也可以替换图片提高成功率,详见文档[电脑微信浏览朋友圈](/doc/manual_guide.md)
67 |
68 | 问:为什么视频没法播放?
69 | - 答:请使用Chrome浏览器打开html文件。或者勾选视频转码,获得更多浏览器的兼容性。
70 |
71 | 问:能不能导出聊天记录?
72 | - 答:导出聊天记录请使用这个软件[https://github.com/LC044/WeChatMsg](https://github.com/LC044/WeChatMsg)
73 |
74 |
75 | ## 4. 更新计划
76 |
77 | * 导出点赞,评论等
78 | * HTML网页功能增强,过滤排序等功能
79 | * 支持更多的朋友圈格式(音乐分享等)
80 | * 其他导出格式(Word, PDF等)
81 | * 佛系开发 随缘更新
82 |
83 | ## 5. 问题反馈
84 |
85 | * 请直接提issue,或发送邮箱techshrimp@163.com
86 | * 请附上日志与软件截图,日志地址log\xxxx-xx-xx-output.log
87 |
88 | ## 6. 二次开发
89 |
90 | * Python环境: Python3.11
91 | * 安装依赖: pip install requirements.txt
92 | * 启动: python main.py
93 | * 编译为可执行文件: 使用Github Action(.github/workflows/main.yml)
94 | * 微信数据库解密见项目:[https://github.com/xaoyaoo/PyWxDump](https://github.com/xaoyaoo/PyWxDump)
95 |
96 | # 三、免责声明
97 |
98 | ### 1. 使用目的
99 |
100 | * 本项目仅供学习交流使用,无收费项目,不用于盈利,**请勿用于非法用途**,否则后果自负。
101 | * 本项目只能导出**自己有权查看**的朋友圈数据,无其他越权功能。
102 | * 用户理解并同意,任何违反法律法规、侵犯他人合法权益的行为,均与本项目及其开发者无关,后果由用户自行承担。
103 | * 禁止利用本项目的相关技术从事非法测试或渗透,禁止利用本项目的相关代码或相关技术从事任何非法工作
104 |
105 |
106 | ### 2. 操作规范
107 |
108 | * 本项目仅允许在授权情况下对朋友圈进行备份与查看,严禁用于非法目的,否则自行承担所有相关责任;用户如因违反此规定而引发的任何法律责任,将由用户自行承担,与本项目及其开发者无关。
109 | * 严禁用于窃取他人隐私,否则自行承担所有相关责任。
110 |
111 | ### 3. 免责声明接受
112 |
113 | * 下载、保存、进一步浏览源代码或者下载安装、编译使用本程序,表示你同意本警告,并承诺遵守它。
114 |
115 | # 四、致谢
116 |
117 | * PC微信工具:[https://github.com/xaoyaoo/PyWxDump](https://github.com/xaoyaoo/PyWxDump)
118 | * 留痕(聊天导出工具):[https://github.com/LC044/WeChatMsg](https://github.com/LC044/WeChatMsg)
119 |
120 | # 五、捐赠
121 |
122 | 如有帮助,请帮忙给B站视频点赞充电
123 | [技术爬爬虾](https://space.bilibili.com/316183842)
--------------------------------------------------------------------------------
/app/DataBase/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | @File : __init__.py.py
4 | @Author : Shuaikang Zhou
5 | @Time : 2023/1/5 0:10
6 | @IDE : Pycharm
7 | @Version : Python3.10
8 | @comment : ···
9 | """
10 | from .micro_msg import MicroMsg
11 | from .misc import Misc
12 | from .msg import Msg
13 | from .sns import Sns
14 |
15 | misc_db = Misc()
16 | msg_db = Msg()
17 | micro_msg_db = MicroMsg()
18 | sns_db = Sns()
19 |
20 |
21 | __all__ = ['misc_db', 'micro_msg_db', 'msg_db', "sns_db"]
22 |
--------------------------------------------------------------------------------
/app/DataBase/micro_msg.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | import sqlite3
3 | import threading
4 |
5 | lock = threading.Lock()
6 | db_path = "./app/Database/Msg/MicroMsg.db"
7 |
8 |
9 | def singleton(cls):
10 | _instance = {}
11 |
12 | def inner():
13 | if cls not in _instance:
14 | _instance[cls] = cls()
15 | return _instance[cls]
16 |
17 | return inner
18 |
19 |
20 | def is_database_exist():
21 | return os.path.exists(db_path)
22 |
23 | @singleton
24 | class MicroMsg:
25 | def __init__(self):
26 | self.DB = None
27 | self.cursor = None
28 | self.open_flag = False
29 | self.init_database()
30 |
31 | def init_database(self):
32 | if not self.open_flag:
33 | if os.path.exists(db_path):
34 | self.DB = sqlite3.connect(db_path, check_same_thread=False)
35 | # '''创建游标'''
36 | self.cursor = self.DB.cursor()
37 | self.open_flag = True
38 | if lock.locked():
39 | lock.release()
40 |
41 | def get_contact(self):
42 | if not self.open_flag:
43 | return None
44 | try:
45 | lock.acquire(True)
46 | sql = '''SELECT UserName, Alias, Type, Remark, NickName, PYInitial, RemarkPYInitial, ContactHeadImgUrl.smallHeadImgUrl, ContactHeadImgUrl.bigHeadImgUrl,ExTraBuf,COALESCE(ContactLabel.LabelName, 'None') AS labelName
47 | FROM Contact
48 | INNER JOIN ContactHeadImgUrl ON Contact.UserName = ContactHeadImgUrl.usrName
49 | LEFT JOIN ContactLabel ON Contact.LabelIDList = ContactLabel.LabelId
50 | WHERE (Type!=4 AND VerifyFlag=0)
51 | AND NickName != ''
52 | ORDER BY
53 | CASE
54 | WHEN RemarkPYInitial = '' THEN PYInitial
55 | ELSE RemarkPYInitial
56 | END ASC
57 | '''
58 | self.cursor.execute(sql)
59 | result = self.cursor.fetchall()
60 | except sqlite3.OperationalError:
61 | # 解决ContactLabel表不存在的问题
62 | # lock.acquire(True)
63 | sql = '''
64 | SELECT UserName, Alias, Type, Remark, NickName, PYInitial, RemarkPYInitial, ContactHeadImgUrl.smallHeadImgUrl, ContactHeadImgUrl.bigHeadImgUrl,ExTraBuf,"None"
65 | FROM Contact
66 | INNER JOIN ContactHeadImgUrl ON Contact.UserName = ContactHeadImgUrl.usrName
67 | WHERE (Type!=4 AND VerifyFlag=0)
68 | AND NickName != ''
69 | ORDER BY
70 | CASE
71 | WHEN RemarkPYInitial = '' THEN PYInitial
72 | ELSE RemarkPYInitial
73 | END ASC
74 | '''
75 | self.cursor.execute(sql)
76 | result = self.cursor.fetchall()
77 | finally:
78 | lock.release()
79 | from app.DataBase import msg_db
80 | return msg_db.get_contact(result)
81 |
82 | def get_contact_by_username(self, username: object) -> object:
83 | if not self.open_flag:
84 | return None
85 | try:
86 | lock.acquire(True)
87 | sql = '''
88 | SELECT UserName, Alias, Type, Remark, NickName, PYInitial, RemarkPYInitial, ContactHeadImgUrl.smallHeadImgUrl, ContactHeadImgUrl.bigHeadImgUrl,ExTraBuf,ContactLabel.LabelName
89 | FROM Contact
90 | INNER JOIN ContactHeadImgUrl ON Contact.UserName = ContactHeadImgUrl.usrName
91 | LEFT JOIN ContactLabel ON Contact.LabelIDList = ContactLabel.LabelId
92 | WHERE UserName = ?
93 | '''
94 | self.cursor.execute(sql, [username])
95 | result = self.cursor.fetchone()
96 | except sqlite3.OperationalError:
97 | # 解决ContactLabel表不存在的问题
98 | # lock.acquire(True)
99 | sql = '''
100 | SELECT UserName, Alias, Type, Remark, NickName, PYInitial, RemarkPYInitial, ContactHeadImgUrl.smallHeadImgUrl, ContactHeadImgUrl.bigHeadImgUrl,ExTraBuf,"None"
101 | FROM Contact
102 | INNER JOIN ContactHeadImgUrl ON Contact.UserName = ContactHeadImgUrl.usrName
103 | WHERE UserName = ?
104 | '''
105 | self.cursor.execute(sql, [username])
106 | result = self.cursor.fetchone()
107 | finally:
108 | lock.release()
109 |
110 | return result
111 |
112 | def get_chatroom_info(self, chatroomname):
113 | '''
114 | 获取群聊信息
115 | '''
116 | if not self.open_flag:
117 | return None
118 | try:
119 | lock.acquire(True)
120 | sql = '''SELECT ChatRoomName, RoomData FROM ChatRoom WHERE ChatRoomName = ?'''
121 | self.cursor.execute(sql, [chatroomname])
122 | result = self.cursor.fetchone()
123 | finally:
124 | lock.release()
125 | return result
126 |
127 | def close(self):
128 | if self.open_flag:
129 | try:
130 | lock.acquire(True)
131 | self.open_flag = False
132 | self.DB.close()
133 | finally:
134 | lock.release()
135 |
136 | def __del__(self):
137 | self.close()
138 |
--------------------------------------------------------------------------------
/app/DataBase/misc.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | import sqlite3
3 | import threading
4 |
5 | lock = threading.Lock()
6 | DB = None
7 | cursor = None
8 | db_path = "./app/Database/Msg/Misc.db"
9 |
10 |
11 | # db_path = './Msg/Misc.db'
12 |
13 |
14 | def singleton(cls):
15 | _instance = {}
16 |
17 | def inner():
18 | if cls not in _instance:
19 | _instance[cls] = cls()
20 | return _instance[cls]
21 |
22 | return inner
23 |
24 |
25 | @singleton
26 | class Misc:
27 | def __init__(self):
28 | self.DB = None
29 | self.cursor = None
30 | self.open_flag = False
31 | self.init_database()
32 |
33 | def init_database(self):
34 | if not self.open_flag:
35 | if os.path.exists(db_path):
36 | self.DB = sqlite3.connect(db_path, check_same_thread=False)
37 | # '''创建游标'''
38 | self.cursor = self.DB.cursor()
39 | self.open_flag = True
40 | if lock.locked():
41 | lock.release()
42 |
43 | def get_avatar_buffer(self, userName):
44 | if not self.open_flag:
45 | return None
46 | sql = '''
47 | select smallHeadBuf
48 | from ContactHeadImg1
49 | where usrName=?;
50 | '''
51 | if not self.open_flag:
52 | self.init_database()
53 | try:
54 | lock.acquire(True)
55 | self.cursor.execute(sql, [userName])
56 | result = self.cursor.fetchall()
57 | if result:
58 | return result[0][0]
59 | finally:
60 | lock.release()
61 | return None
62 |
63 | def close(self):
64 | if self.open_flag:
65 | try:
66 | lock.acquire(True)
67 | self.open_flag = False
68 | self.DB.close()
69 | finally:
70 | lock.release()
71 |
72 | def __del__(self):
73 | self.close()
74 |
75 |
--------------------------------------------------------------------------------
/app/DataBase/msg.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | import random
3 | import sqlite3
4 | import threading
5 | import traceback
6 | db_path = "./app/Database/Msg/MSG.db"
7 | lock = threading.Lock()
8 |
9 |
10 | def is_database_exist():
11 | return os.path.exists(db_path)
12 |
13 |
14 | def singleton(cls):
15 | _instance = {}
16 |
17 | def inner():
18 | if cls not in _instance:
19 | _instance[cls] = cls()
20 | return _instance[cls]
21 |
22 | return inner
23 | @singleton
24 | class Msg:
25 | def __init__(self):
26 | self.DB = None
27 | self.cursor = None
28 | self.open_flag = False
29 | self.init_database()
30 |
31 | def init_database(self, path=None):
32 | global db_path
33 | if not self.open_flag:
34 | if path:
35 | db_path = path
36 | if os.path.exists(db_path):
37 | self.DB = sqlite3.connect(db_path, check_same_thread=False)
38 | # '''创建游标'''
39 | self.cursor = self.DB.cursor()
40 | self.open_flag = True
41 | if lock.locked():
42 | lock.release()
43 |
44 | def get_contact(self, contacts):
45 | """这里查了一遍聊天记录,根据聊天记录最后一条按时间
46 | 对联系人进行排序
47 | """
48 | if not self.open_flag:
49 | return None
50 | try:
51 | lock.acquire(True)
52 | sql = '''select StrTalker, MAX(CreateTime) from MSG group by StrTalker'''
53 | self.cursor.execute(sql)
54 | res = self.cursor.fetchall()
55 | finally:
56 | lock.release()
57 | res = {StrTalker: CreateTime for StrTalker, CreateTime in res}
58 | contacts = [list(cur_contact) for cur_contact in contacts]
59 | for i, cur_contact in enumerate(contacts):
60 | if cur_contact[0] in res:
61 | contacts[i].append(res[cur_contact[0]])
62 | else:
63 | contacts[i].append(0)
64 | contacts.sort(key=lambda cur_contact: cur_contact[-1], reverse=True)
65 | return contacts
66 |
67 | def get_messages_calendar(self, username_):
68 | sql = '''
69 | SELECT strftime('%Y-%m-%d',CreateTime,'unixepoch','localtime') as days
70 | from (
71 | SELECT MsgSvrID, CreateTime
72 | FROM MSG
73 | WHERE StrTalker = ?
74 | ORDER BY CreateTime
75 | )
76 | group by days
77 | '''
78 | if not self.open_flag:
79 | print('数据库未就绪')
80 | return None
81 | try:
82 | lock.acquire(True)
83 | self.cursor.execute(sql, [username_])
84 | result = self.cursor.fetchall()
85 | finally:
86 | lock.release()
87 | return [date[0] for date in result]
88 |
89 | def close(self):
90 | if self.open_flag:
91 | try:
92 | lock.acquire(True)
93 | self.open_flag = False
94 | self.DB.close()
95 | finally:
96 | lock.release()
97 |
98 | def __del__(self):
99 | self.close()
100 |
101 |
--------------------------------------------------------------------------------
/app/DataBase/sns.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | import random
3 | import sqlite3
4 | import threading
5 | import traceback
6 | from typing import Optional
7 |
8 | db_path = "./app/Database/Msg/Sns.db"
9 | lock = threading.Lock()
10 |
11 |
12 | def is_database_exist():
13 | return os.path.exists(db_path)
14 |
15 |
16 | def singleton(cls):
17 | _instance = {}
18 |
19 | def inner():
20 | if cls not in _instance:
21 | _instance[cls] = cls()
22 | return _instance[cls]
23 |
24 | return inner
25 |
26 | @singleton
27 | class Sns:
28 | def __init__(self):
29 | self.DB = None
30 | self.cursor = None
31 | self.open_flag = False
32 | self.init_database()
33 |
34 | def init_database(self, path=None):
35 | global db_path
36 | if not self.open_flag:
37 | if path:
38 | db_path = path
39 | if os.path.exists(db_path):
40 | self.DB = sqlite3.connect(db_path, check_same_thread=False)
41 | # '''创建游标'''
42 | self.cursor = self.DB.cursor()
43 | self.open_flag = True
44 | if lock.locked():
45 | lock.release()
46 |
47 | def get_messages_in_time(self, start_time, end_time):
48 | if not self.open_flag:
49 | return None
50 | try:
51 | lock.acquire(True)
52 | sql = '''select UserName, Content, FeedId from FeedsV20 where CreateTime>=?
53 | and CreateTime<=? order by CreateTime desc'''
54 | self.cursor.execute(sql, [start_time, end_time])
55 | res = self.cursor.fetchall()
56 | finally:
57 | lock.release()
58 |
59 | return res
60 |
61 | def get_comment_by_feed_id(self, feed_id):
62 | if not self.open_flag:
63 | return None
64 | try:
65 | lock.acquire(True)
66 | sql = '''select FromUserName, CommentType, Content from CommentV20 where FeedId=?
67 | order by CreateTime desc'''
68 | self.cursor.execute(sql, [feed_id])
69 | res = self.cursor.fetchall()
70 | finally:
71 | lock.release()
72 | return res
73 |
74 | def get_cover_url(self) -> Optional[str]:
75 | if not self.open_flag:
76 | return None
77 | try:
78 | lock.acquire(True)
79 | sql = '''select StrValue from SnsConfigV20 where Key="6" '''
80 | self.cursor.execute(sql)
81 | result = self.cursor.fetchall()
82 | if result:
83 | return result[0][0]
84 | finally:
85 | lock.release()
86 | return None
87 |
88 |
89 | def close(self):
90 | if self.open_flag:
91 | try:
92 | lock.acquire(True)
93 | self.open_flag = False
94 | self.DB.close()
95 | finally:
96 | lock.release()
97 |
98 | def __del__(self):
99 | self.close()
100 |
101 |
102 | if __name__ == '__main__':
103 | pass
104 |
--------------------------------------------------------------------------------
/decrypter/db_decrypt.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sqlite3
3 | import tkinter
4 | import traceback
5 |
6 | from pywxdump import decrypt
7 | from log import LOG
8 |
9 |
10 | class DatabaseDecrypter:
11 |
12 | def __init__(self, gui: 'Gui', db_path, key):
13 | self.db_path = db_path
14 | self.key = key
15 | self.gui = gui
16 | # 指定需要解密的数据库
17 | self.db_list = ["MicroMsg.db", "Misc.db", "MSG.db", "Sns.db"]
18 | self.db_list.extend([f"MSG{i}.db" for i in range(0, 50)])
19 |
20 | def merge_databases(self, source_paths, target_path):
21 | # 创建目标数据库连接
22 | target_conn = sqlite3.connect(target_path)
23 | target_cursor = target_conn.cursor()
24 | try:
25 | # 开始事务
26 | target_conn.execute("BEGIN;")
27 | for i, source_path in enumerate(source_paths):
28 | if not os.path.exists(source_path):
29 | continue
30 | db = sqlite3.connect(source_path)
31 | db.text_factory = str
32 | cursor = db.cursor()
33 | try:
34 | sql = '''
35 | SELECT TalkerId,MsgsvrID,Type,SubType,IsSender,CreateTime,Sequence,StrTalker,StrContent,DisplayContent,BytesExtra,CompressContent
36 | FROM MSG;
37 | '''
38 | cursor.execute(sql)
39 | result = cursor.fetchall()
40 | # 附加源数据库
41 | target_cursor.executemany(
42 | "INSERT INTO MSG "
43 | "(TalkerId,MsgsvrID,Type,SubType,IsSender,CreateTime,Sequence,StrTalker,StrContent,DisplayContent,"
44 | "BytesExtra,CompressContent)"
45 | "VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
46 | result)
47 | except:
48 | LOG.error(f'{source_path}数据库合并错误:\n{traceback.format_exc()}')
49 | cursor.close()
50 | db.close()
51 | # 提交事务
52 | target_conn.execute("COMMIT;")
53 |
54 | except Exception as e:
55 | # 发生异常时回滚事务
56 | target_conn.execute("ROLLBACK;")
57 | raise e
58 |
59 | finally:
60 | # 关闭目标数据库连接
61 | target_conn.close()
62 |
63 | def decrypt(self):
64 |
65 | output_dir = 'app/DataBase/Msg'
66 | os.makedirs(output_dir, exist_ok=True)
67 | tasks = []
68 | if os.path.exists(self.db_path):
69 | for root, dirs, files in os.walk(self.db_path):
70 | for file in files:
71 | if '.db' == file[-3:] and file in self.db_list:
72 | in_path = os.path.join(root, file)
73 | output_path = os.path.join(output_dir, file)
74 | tasks.append([self.key, in_path, output_path])
75 | for i, task in enumerate(tasks):
76 | flag, result = decrypt(*task)
77 | if not flag:
78 | LOG.error(result)
79 | progress = round((i+1) / len(tasks) * 100)
80 | self.gui.update_decrypt_progressbar(progress)
81 |
82 | target_database = "app/DataBase/Msg/MSG.db"
83 | # 源数据库文件列表
84 | source_databases = [f"app/DataBase/Msg/MSG{i}.db" for i in range(0, 50)]
85 | import shutil
86 | if os.path.exists(target_database):
87 | os.remove(target_database)
88 | try:
89 | shutil.copy2("app/DataBase/Msg/MSG0.db", target_database) # 使用一个数据库文件作为模板
90 | except FileNotFoundError:
91 | LOG.error(traceback.format_exc())
92 | # 合并数据库
93 | try:
94 | self.merge_databases(source_databases, target_database)
95 | except FileNotFoundError:
96 | LOG.error(traceback.format_exc())
97 | LOG.error("数据库不存在\n请检查微信版本是否为最新")
98 |
99 | # 解密完成 放开下一步按钮
100 | self.gui.decrypt_note_text.set("复制成功,请点击下一步")
101 | self.gui.next_step_button.config(state=tkinter.NORMAL)
102 |
103 |
104 |
--------------------------------------------------------------------------------
/decrypter/image_decrypt.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import os
3 | import shutil
4 | import subprocess
5 | import sys
6 | import traceback
7 | from datetime import date
8 | from pathlib import Path
9 | import filetype
10 |
11 | import log
12 |
13 |
14 | class ImageDecrypter:
15 |
16 | def __init__(self, gui: 'Gui', file_path):
17 | self.file_path = file_path
18 | self.gui = gui
19 | self.sns_cache_path = file_path + "/FileStorage/Sns/Cache"
20 |
21 | @staticmethod
22 | def get_output_path(dir_name, md5, duration):
23 | if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
24 | # 这是到_internal文件夹
25 | resource_dir = getattr(sys, '_MEIPASS')
26 | # 获取_internal上一级文件夹再拼接
27 | return os.path.join(os.path.dirname(resource_dir), 'output', dir_name, 'videos', f'{md5}_{duration}.mp4')
28 | else:
29 | return os.path.join(os.getcwd(), 'output', dir_name, 'videos', f'{md5}_{duration}.mp4')
30 |
31 | @staticmethod
32 | def calculate_md5(file_path):
33 | with open(file_path, "rb") as f:
34 | file_content = f.read()
35 | return hashlib.md5(file_content).hexdigest()
36 |
37 | @staticmethod
38 | def get_all_month_between_dates(start_date, end_date) -> list[str]:
39 | result = []
40 | current_date = start_date
41 | while current_date <= end_date:
42 | # 打印当前日期的年份和月份
43 | result.append(current_date.strftime("%Y-%m"))
44 | year = current_date.year + (current_date.month // 12)
45 | month = current_date.month % 12 + 1
46 | # 更新current_date到下个月的第一天
47 | current_date = date(year, month, 1)
48 | return result
49 |
50 | @staticmethod
51 | def decode(magic, buf):
52 | return bytearray([b ^ magic for b in list(buf)])
53 |
54 | @staticmethod
55 | def guess_image_encoding_magic(buf):
56 | header_code, check_code = 0xff, 0xd8
57 | # 微信图片加密方法对字节逐一“异或”,即 源文件^magic(未知数)=加密后文件
58 | # 已知jpg的头字节是0xff,将0xff与加密文件的头字节做异或运算求解magic码
59 | magic = header_code ^ list(buf)[0] if buf else 0x00
60 | # 尝试使用magic码解密,如果第二字节符合jpg特质,则图片解密成功
61 | _, code = ImageDecrypter.decode(magic, buf[:2])
62 | if check_code == code:
63 | return magic
64 |
65 | def decrypt_images(self, exporter, start_date, end_date, dir_name) -> None:
66 | """将图片文件从缓存中复制出来,重命名为{主图字节数}_{缩略图字节数}.jpg
67 | duration单位为秒
68 | """
69 | months = self.get_all_month_between_dates(start_date, end_date)
70 |
71 | total_files = 0
72 | processed_files = 0
73 | for month in months:
74 | source_dir = self.sns_cache_path + "/" + month
75 | total_files = total_files + len(list(Path(source_dir).rglob('*')))
76 |
77 | for month in months:
78 | source_dir = self.sns_cache_path + "/" + month
79 | for file in Path(source_dir).rglob('*'):
80 | # 排除缩略图
81 | if not exporter.stop_flag and file.is_file() and not file.name.endswith('_t'):
82 | try:
83 | with open(file, 'rb') as f:
84 | buff = bytearray(f.read())
85 | magic = self.guess_image_encoding_magic(buff)
86 | if magic:
87 | os.makedirs(f"output/{dir_name}/images/{month}/", exist_ok=True)
88 | os.makedirs(f"output/{dir_name}/thumbs/{month}/", exist_ok=True)
89 | main_file_size = file.stat().st_size
90 | thumb_file_size = 0
91 | # 找到对应缩略图
92 | thumb_file = Path(f'{source_dir}/{file.name}_t')
93 | if thumb_file.exists():
94 | thumb_file_size = thumb_file.stat().st_size
95 | # 读缩略图加密
96 | with open(thumb_file, 'rb') as f:
97 | thumb_buff = bytearray(f.read())
98 |
99 | # 写缩略图
100 | thumb_destination = (f"output/{dir_name}/thumbs/{month}/"
101 | f"{main_file_size}_{thumb_file_size}.jpg")
102 | with open(thumb_destination, 'wb') as f:
103 | new_thumb_buff = self.decode(magic, thumb_buff)
104 | f.write(new_thumb_buff)
105 |
106 | destination = (f"output/{dir_name}/images/{month}/"
107 | f"{main_file_size}_{thumb_file_size}.jpg")
108 | with open(destination, 'wb') as f:
109 | new_buf = self.decode(magic, buff)
110 | f.write(new_buf)
111 | except Exception:
112 | traceback.print_exc()
113 | processed_files = processed_files + 1
114 | # 15%的进度作为处理图片使用
115 | progress = round(processed_files / total_files * 15)
116 | self.gui.update_export_progressbar(progress)
117 |
--------------------------------------------------------------------------------
/decrypter/video_decrypt.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import os
3 | import shutil
4 | import subprocess
5 | import sys
6 | import traceback
7 | from datetime import date
8 | from pathlib import Path
9 | import filetype
10 |
11 | import log
12 |
13 |
14 | class VideoDecrypter:
15 |
16 | def __init__(self, gui: 'Gui', file_path):
17 | self.file_path = file_path
18 | self.gui = gui
19 | self.sns_cache_path = file_path + "/FileStorage/Sns/Cache"
20 |
21 | @staticmethod
22 | def get_ffmpeg_path():
23 | if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
24 | # 这是到_internal文件夹
25 | resource_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__)))
26 | # 获取_internal上一级再拼接
27 | return os.path.join(os.path.dirname(resource_dir), 'resource', 'ffmpeg.exe')
28 | else:
29 | return os.path.join(os.getcwd(), 'resource', 'ffmpeg.exe')
30 |
31 | @staticmethod
32 | def get_ffprobe_path():
33 | if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
34 | # 这是到_internal文件夹
35 | resource_dir = getattr(sys, '_MEIPASS')
36 | # 获取_internal上一级文件夹再拼接
37 | return os.path.join(os.path.dirname(resource_dir), 'resource', 'ffprobe.exe')
38 | else:
39 | return os.path.join(os.getcwd(), 'resource', 'ffprobe.exe')
40 |
41 | @staticmethod
42 | def get_output_path(dir_name, md5, duration):
43 | if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
44 | # 这是到_internal文件夹
45 | resource_dir = getattr(sys, '_MEIPASS')
46 | # 获取_internal上一级文件夹再拼接
47 | return os.path.join(os.path.dirname(resource_dir), 'output', dir_name, 'videos', f'{md5}_{duration}.mp4')
48 | else:
49 | return os.path.join(os.getcwd(), 'output', dir_name, 'videos', f'{md5}_{duration}.mp4')
50 |
51 | @staticmethod
52 | def calculate_md5(file_path):
53 | with open(file_path, "rb") as f:
54 | file_content = f.read()
55 | return hashlib.md5(file_content).hexdigest()
56 |
57 | @staticmethod
58 | def get_all_month_between_dates(start_date, end_date) -> list[str]:
59 | result = []
60 | current_date = start_date
61 | while current_date <= end_date:
62 | # 打印当前日期的年份和月份
63 | result.append(current_date.strftime("%Y-%m"))
64 | year = current_date.year + (current_date.month // 12)
65 | month = current_date.month % 12 + 1
66 | # 更新current_date到下个月的第一天
67 | current_date = date(year, month, 1)
68 | return result
69 |
70 | def get_video_duration(self, video_path) ->float:
71 | """获取视频时长"""
72 | ffprobe_path = self.get_ffprobe_path()
73 | if not os.path.exists(ffprobe_path):
74 | log.LOG.error("Wrong ffprobe path:"+ffprobe_path)
75 | return 0
76 | ffprobe_cmd = f'"{ffprobe_path}" -i "{video_path}" -show_entries format=duration -v quiet -of csv="p=0"'
77 | p = subprocess.Popen(
78 | ffprobe_cmd,
79 | stdout=subprocess.PIPE,
80 | stderr=subprocess.PIPE,
81 | shell=True)
82 | print(ffprobe_cmd)
83 | out, err = p.communicate()
84 | if len(str(err, 'gbk')) > 0:
85 | print(f"subprocess 执行结果:out:{out} err:{str(err, 'gbk')}")
86 | return 0
87 | if len(str(out, 'gbk')) == 0:
88 | return 0
89 | return float(out)
90 |
91 | def decrypt_videos(self, exporter, start_date, end_date, dir_name, convert_video) -> None:
92 | """将视频文件从缓存中复制出来,重命名为{md5}_{duration}.mp4
93 | duration单位为秒
94 | """
95 | months = self.get_all_month_between_dates(start_date, end_date)
96 |
97 | total_files = 0
98 | processed_files = 0
99 | for month in months:
100 | source_dir = self.sns_cache_path + "/" + month
101 | total_files = total_files + len(list(Path(source_dir).rglob('*')))
102 |
103 | for month in months:
104 | source_dir = self.sns_cache_path + "/" + month
105 | for file in Path(source_dir).rglob('*'):
106 | if not exporter.stop_flag:
107 | try:
108 | file_type = filetype.guess(file.resolve())
109 | if file_type and file_type.extension == "mp4":
110 | print("Process Video: "+str(file.resolve()))
111 | md5 = self.calculate_md5(file.resolve())
112 | print("video md5: "+md5)
113 | duration = self.get_video_duration(str(file.resolve()))
114 | print("video duration: " + str(duration))
115 | # 是否需要将视频转码
116 | if convert_video:
117 | input_path = str(file.resolve())
118 | ffmpeg_path = self.get_ffmpeg_path()
119 | output_path = self.get_output_path(dir_name, md5, duration)
120 | print("ffmpeg_path: " + str(ffmpeg_path))
121 | if os.path.exists(ffmpeg_path):
122 | cmd = f'''"{ffmpeg_path}" -loglevel quiet -i "{input_path}" -c:v libx264 "{output_path}"'''
123 | print("ffmpeg_path cmd:" + cmd)
124 | subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
125 | else:
126 | shutil.copy(file.resolve(), f"output/{dir_name}/videos/{md5}_{duration}.mp4")
127 | except Exception:
128 | traceback.print_exc()
129 | processed_files = processed_files + 1
130 | # 15%的进度作为处理视频使用 + 15%(处理图像)
131 | progress = round(processed_files / total_files * 15 + 15)
132 | self.gui.update_export_progressbar(progress)
133 |
--------------------------------------------------------------------------------
/doc/manual_guide.md:
--------------------------------------------------------------------------------
1 | # 电脑微信浏览朋友圈
2 | ## 一、浏览全部朋友圈
3 | 打开微信朋友圈页面进行浏览。缺点是只能浏览最近100天的记录。
4 | 
5 | ## 二、浏览单个朋友
6 | * 此方法没有最大天数限制
7 | * 打开搜一搜
8 | 
9 | * 点击朋友圈选项卡
10 | 
11 | * 输入数字1,点击搜索
12 | 
13 | * 展开朋友下拉框
14 | 
15 | * 搜索一个朋友点击完成
16 | 
17 | * 输入一个**中文**的问号 ? 再次点击搜索
18 | 
19 | * 搜一搜有**最大展示数量限制**,如果展示不全,可以限定一下搜索时间
20 | 
21 |
22 |
23 | ## 三、提高自动化操作成功率
24 | 打开项目 resource/auto_gui文件夹,有四个图片
25 | 
26 | 程序根据这四个图片定位鼠标位置,进行自动化操作。
27 | 识图成功率与电脑的分辨率,缩放比例有很大关系。
28 | 将这四个图片替换成自己电脑的按钮截图,可以极大提高自动化操作成功率
--------------------------------------------------------------------------------
/doc/pic/中文问号.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/doc/pic/中文问号.png
--------------------------------------------------------------------------------
/doc/pic/主界面.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/doc/pic/主界面.png
--------------------------------------------------------------------------------
/doc/pic/四幅图.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/doc/pic/四幅图.png
--------------------------------------------------------------------------------
/doc/pic/展开朋友.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/doc/pic/展开朋友.png
--------------------------------------------------------------------------------
/doc/pic/打开搜一搜.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/doc/pic/打开搜一搜.png
--------------------------------------------------------------------------------
/doc/pic/打开朋友圈.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/doc/pic/打开朋友圈.png
--------------------------------------------------------------------------------
/doc/pic/朋友圈页面.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/doc/pic/朋友圈页面.png
--------------------------------------------------------------------------------
/doc/pic/点击完成.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/doc/pic/点击完成.png
--------------------------------------------------------------------------------
/doc/pic/点击搜索.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/doc/pic/点击搜索.png
--------------------------------------------------------------------------------
/doc/pic/限定时间.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/doc/pic/限定时间.png
--------------------------------------------------------------------------------
/entity/comment.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass
5 | class Comment:
6 | from_user_name: str
7 | comment_type: int
8 | content: str
9 |
--------------------------------------------------------------------------------
/entity/contact.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass
5 | class Contact:
6 | userName: str
7 | alias: str
8 | type: int
9 | remark: str
10 | nickName: str
11 | pYInitial: str
12 | remarkPYInitial: str
13 | smallHeadImgUrl: str
14 | bigHeadImgUrl: str
15 | exTraBuf: str
16 | labelName: str
17 | latestTalkTime: int
--------------------------------------------------------------------------------
/entity/moment_msg.py:
--------------------------------------------------------------------------------
1 | import json
2 | from dataclasses import dataclass, field
3 | from typing import Optional, List
4 | from datetime import datetime, timezone, timedelta
5 |
6 | import xmltodict
7 | from dataclasses_json import dataclass_json, config
8 |
9 |
10 | @dataclass_json
11 | @dataclass
12 | class Location:
13 | poiName: str = field(metadata=config(field_name="@poiName"), default="")
14 | longitude: str = field(metadata=config(field_name="@longitude"), default="")
15 | latitude: str = field(metadata=config(field_name="@latitude"), default="")
16 | country: str = field(metadata=config(field_name="@country"), default="")
17 |
18 |
19 | @dataclass_json
20 | @dataclass
21 | class Url:
22 | type: str = field(metadata=config(field_name="@type"))
23 | text: str = field(metadata=config(field_name="#text"), default="")
24 | md5: str = field(metadata=config(field_name="@md5"), default="")
25 | token: str = field(metadata=config(field_name="@token"), default="")
26 | enc_idx: str = field(metadata=config(field_name="@enc_idx"), default="")
27 |
28 | @dataclass_json
29 | @dataclass
30 | class Thumb:
31 | type: str = field(metadata=config(field_name="@type"))
32 | text: str = field(metadata=config(field_name="#text"))
33 | token: str = field(metadata=config(field_name="@token"), default="")
34 | enc_idx: str = field(metadata=config(field_name="@enc_idx"), default="")
35 |
36 |
37 | @dataclass_json
38 | @dataclass
39 | class Media:
40 | type: Optional[str] = None
41 | id: Optional[str] = None
42 | url: Optional[Url] | str = None
43 | thumb: Optional[Thumb] = None
44 | thumbUrl: Optional[str] = None
45 | videoDuration: Optional[str] = None
46 | title: Optional[str] = None
47 | description: Optional[str] = None
48 |
49 | @dataclass_json
50 | @dataclass
51 | class MediaList:
52 | media: list[Media]
53 |
54 |
55 |
56 | @dataclass_json
57 | @dataclass
58 | class FinderFeed:
59 | feedType: Optional[str] = ""
60 | nickname: Optional[str] = ""
61 | desc: Optional[str] = ""
62 | mediaList: Optional[MediaList] = None
63 |
64 | @dataclass_json
65 | @dataclass
66 | class ContentObject:
67 | contentStyle: int
68 | contentUrl: Optional[str] = ""
69 | title: Optional[str] = ""
70 | description: Optional[str] = ""
71 | mediaList: Optional[MediaList] = None
72 | # 视频号消息
73 | finderFeed: Optional[FinderFeed] = None
74 |
75 | @dataclass_json
76 | @dataclass
77 | class TimelineObject:
78 | username: str
79 | location: Location
80 | ContentObject: ContentObject
81 | createTime: int
82 | contentDesc: Optional[str] = ""
83 |
84 | @property
85 | def create_date(self):
86 | dt = datetime.fromtimestamp(self.createTime, timezone.utc)
87 | # 转换为北京时间(UTC+8)
88 | beijing_timezone = timezone(timedelta(hours=8))
89 | date = dt.astimezone(beijing_timezone).date()
90 | return date
91 | @property
92 | def create_time(self)->str:
93 | dt = datetime.fromtimestamp(self.createTime, timezone.utc)
94 | # 转换为北京时间(UTC+8)
95 | beijing_timezone = timezone(timedelta(hours=8))
96 | time_formatted = dt.astimezone(beijing_timezone).strftime('%Y-%m-%d %H:%M:%S')
97 | return time_formatted
98 | @property
99 | def create_year_month(self)->str:
100 | dt = datetime.fromtimestamp(self.createTime, timezone.utc)
101 | # 转换为北京时间(UTC+8)
102 | beijing_timezone = timezone(timedelta(hours=8))
103 | time_formatted = dt.astimezone(beijing_timezone).strftime('%Y-%m')
104 | return time_formatted
105 |
106 |
107 | @dataclass_json
108 | @dataclass
109 | class MomentMsg:
110 | timelineObject: TimelineObject = field(metadata=config(field_name="TimelineObject"))
111 |
112 |
113 |
114 |
115 | def test():
116 |
117 | xml = """
118 | """
119 | msg_dict = xmltodict.parse(xml.replace(chr(10), '').replace(chr(9), ''), force_list={'media'})
120 | msg_json = json.dumps(msg_dict, sort_keys=False, indent=2)
121 | momentMsg = MomentMsg.from_json(msg_json)
122 | print(momentMsg)
123 |
124 | def test_time_convert():
125 | time = 1706592456
126 | dt = datetime.fromtimestamp(time, timezone.utc)
127 | # 转换为北京时间(UTC+8)
128 | beijing_timezone = timezone(timedelta(hours=8))
129 | beijing_time = dt.astimezone(beijing_timezone).strftime('%Y-%m-%d %H:%M:%S')
130 | print(beijing_time)
131 |
132 | if __name__ == "__main__":
133 | test()
134 |
--------------------------------------------------------------------------------
/exporter/avatar_exporter.py:
--------------------------------------------------------------------------------
1 | import io
2 | import os
3 | from pathlib import Path
4 |
5 | from PIL import Image
6 |
7 |
8 | class AvatarExporter:
9 | def __init__(self, dir_name: str):
10 | self.dir_name = dir_name
11 | # 头像是否已保存好 Key: userName value: True/False
12 | self._saved_map = {}
13 | if not os.path.exists(f'output/{self.dir_name}/avatars/'):
14 | os.mkdir(f'output/{self.dir_name}/avatars/')
15 |
16 | def get_avatar_path(self, userName) -> str:
17 | if userName in self._saved_map:
18 | return f'avatars/{userName}.png'
19 |
20 | from app.DataBase import misc_db
21 | blob_data = misc_db.get_avatar_buffer(userName)
22 | self._saved_map[userName] = True
23 | if blob_data:
24 | image = Image.open(io.BytesIO(blob_data))
25 | image.save(f'output/{self.dir_name}/avatars/{userName}.png', 'PNG')
26 | return f'avatars/{userName}.png'
27 | else:
28 | return f'icons/empty-avatar.jpg'
29 |
--------------------------------------------------------------------------------
/exporter/emoji_exporter.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | class EmojiExporter:
5 | def __init__(self):
6 | pass
7 |
8 | @staticmethod
9 | def replace_emoji(text: str):
10 | replacement_rules = [
11 | {
12 | "pattern": re.compile(r'\[微笑\]'),
13 | "replacement": ' '
14 | },
15 | {
16 | "pattern": re.compile(r'\[发呆\]'),
17 | "replacement": ' '
18 | },
19 | {
20 | "pattern": re.compile(r'\[撇嘴\]'),
21 | "replacement": ' '
22 | },
23 | {
24 | "pattern": re.compile(r'\[色\]'),
25 | "replacement": ' '
26 | },
27 | {
28 | "pattern": re.compile(r'\[发呆\]'),
29 | "replacement": ' '
30 | },
31 | {
32 | "pattern": re.compile(r'\[得意\]'),
33 | "replacement": ' '
34 | },
35 | {
36 | "pattern": re.compile(r'\[流泪\]'),
37 | "replacement": ' '
38 | },
39 | {
40 | "pattern": re.compile(r'\[害羞\]'),
41 | "replacement": ' '
42 | },
43 | {
44 | "pattern": re.compile(r'\[闭嘴\]'),
45 | "replacement": ' '
46 | },
47 | {
48 | "pattern": re.compile(r'\[睡\]'),
49 | "replacement": ' '
50 | },
51 | {
52 | "pattern": re.compile(r'\[大哭\]'),
53 | "replacement": ' '
54 | },
55 | {
56 | "pattern": re.compile(r'\[尴尬\]'),
57 | "replacement": ' '
58 | },
59 | {
60 | "pattern": re.compile(r'\[发怒\]'),
61 | "replacement": ' '
62 | },
63 | {
64 | "pattern": re.compile(r'\[调皮\]'),
65 | "replacement": ' '
66 | },
67 | {
68 | "pattern": re.compile(r'\[呲牙\]'),
69 | "replacement": ' '
70 | },
71 | {
72 | "pattern": re.compile(r'\[惊讶\]'),
73 | "replacement": ' '
74 | },
75 | {
76 | "pattern": re.compile(r'\[难过\]'),
77 | "replacement": ' '
78 | },
79 | {
80 | "pattern": re.compile(r'\[抓狂\]'),
81 | "replacement": ' '
82 | },
83 | {
84 | "pattern": re.compile(r'\[吐\]'),
85 | "replacement": ' '
86 | },
87 | {
88 | "pattern": re.compile(r'\[偷笑\]'),
89 | "replacement": ' '
90 | },
91 | {
92 | "pattern": re.compile(r'\[愉快\]'),
93 | "replacement": ' '
94 | },
95 | {
96 | "pattern": re.compile(r'\[白眼\]'),
97 | "replacement": ' '
98 | },
99 | {
100 | "pattern": re.compile(r'\[傲慢\]'),
101 | "replacement": ' '
102 | },
103 | {
104 | "pattern": re.compile(r'\[困\]'),
105 | "replacement": ' '
106 | },
107 | {
108 | "pattern": re.compile(r'\[惊恐\]'),
109 | "replacement": ' '
110 | },
111 | {
112 | "pattern": re.compile(r'\[憨笑\]'),
113 | "replacement": ' '
114 | },
115 | {
116 | "pattern": re.compile(r'\[悠闲\]'),
117 | "replacement": ' '
118 | },
119 | {
120 | "pattern": re.compile(r'\[咒骂\]'),
121 | "replacement": ' '
122 | },
123 | {
124 | "pattern": re.compile(r'\[疑问\]'),
125 | "replacement": ' '
126 | },
127 | {
128 | "pattern": re.compile(r'\[嘘\]'),
129 | "replacement": ' '
130 | },
131 | {
132 | "pattern": re.compile(r'\[晕\]'),
133 | "replacement": ' '
134 | },
135 | {
136 | "pattern": re.compile(r'\[衰\]'),
137 | "replacement": ' '
138 | },
139 | {
140 | "pattern": re.compile(r'\[骷髅\]'),
141 | "replacement": ' '
142 | },
143 | {
144 | "pattern": re.compile(r'\[敲打\]'),
145 | "replacement": ' '
146 | },
147 | {
148 | "pattern": re.compile(r'\[再见\]'),
149 | "replacement": ' '
150 | },
151 | {
152 | "pattern": re.compile(r'\[擦汗\]'),
153 | "replacement": ' '
154 | },
155 | {
156 | "pattern": re.compile(r'\[抠鼻\]'),
157 | "replacement": ' '
158 | },
159 | {
160 | "pattern": re.compile(r'\[鼓掌\]'),
161 | "replacement": ' '
162 | },
163 | {
164 | "pattern": re.compile(r'\[坏笑\]'),
165 | "replacement": ' '
166 | },
167 | {
168 | "pattern": re.compile(r'\[右哼哼\]'),
169 | "replacement": ' '
170 | },
171 | {
172 | "pattern": re.compile(r'\[鄙视\]'),
173 | "replacement": ' '
174 | },
175 | {
176 | "pattern": re.compile(r'\[委屈\]'),
177 | "replacement": ' '
178 | },
179 | {
180 | "pattern": re.compile(r'\[快哭了\]'),
181 | "replacement": ' '
182 | },
183 | {
184 | "pattern": re.compile(r'\[阴险\]'),
185 | "replacement": ' '
186 | },
187 | {
188 | "pattern": re.compile(r'\[亲亲\]'),
189 | "replacement": ' '
190 | },
191 | {
192 | "pattern": re.compile(r'\[可怜\]'),
193 | "replacement": ' '
194 | },
195 | {
196 | "pattern": re.compile(r'\[Whimper\]'),
197 | "replacement": ' '
198 | },
199 | {
200 | "pattern": re.compile(r'\[笑脸\]'),
201 | "replacement": ' '
202 | },
203 | {
204 | "pattern": re.compile(r'\[生病\]'),
205 | "replacement": ' '
206 | },
207 | {
208 | "pattern": re.compile(r'\[脸红\]'),
209 | "replacement": ' '
210 | },
211 | {
212 | "pattern": re.compile(r'\[破涕为笑\]'),
213 | "replacement": ' '
214 | },
215 | {
216 | "pattern": re.compile(r'\[恐惧\]'),
217 | "replacement": ' '
218 | },
219 | {
220 | "pattern": re.compile(r'\[失望\]'),
221 | "replacement": ' '
222 | },
223 | {
224 | "pattern": re.compile(r'\[无语\]'),
225 | "replacement": ' '
226 | },
227 | {
228 | "pattern": re.compile(r'\[嘿哈\]'),
229 | "replacement": ' '
230 | },
231 | {
232 | "pattern": re.compile(r'\[捂脸\]'),
233 | "replacement": ' '
234 | },
235 | {
236 | "pattern": re.compile(r'\[奸笑\]'),
237 | "replacement": ' '
238 | },
239 | {
240 | "pattern": re.compile(r'\[机智\]'),
241 | "replacement": ' '
242 | },
243 | {
244 | "pattern": re.compile(r'\[皱眉\]'),
245 | "replacement": ' '
246 | },
247 | {
248 | "pattern": re.compile(r'\[耶\]'),
249 | "replacement": ' '
250 | },
251 | {
252 | "pattern": re.compile(r'\[吃瓜\]'),
253 | "replacement": ' '
254 | },
255 | {
256 | "pattern": re.compile(r'\[加油\]'),
257 | "replacement": ' '
258 | },
259 | {
260 | "pattern": re.compile(r'\[汗\]'),
261 | "replacement": ' '
262 | },
263 | {
264 | "pattern": re.compile(r'\[天啊\]'),
265 | "replacement": ' '
266 | },
267 | {
268 | "pattern": re.compile(r'\[Emm\]'),
269 | "replacement": ' '
270 | },
271 | {
272 | "pattern": re.compile(r'\[社会社会\]'),
273 | "replacement": ' '
274 | },
275 | {
276 | "pattern": re.compile(r'\[旺柴\]'),
277 | "replacement": ' '
278 | },
279 | {
280 | "pattern": re.compile(r'\[好的\]'),
281 | "replacement": ' '
282 | },
283 | {
284 | "pattern": re.compile(r'\[打脸\]'),
285 | "replacement": ' '
286 | },
287 | {
288 | "pattern": re.compile(r'\[哇\]'),
289 | "replacement": ' '
290 | },
291 | {
292 | "pattern": re.compile(r'\[翻白眼\]'),
293 | "replacement": ' '
294 | },
295 | {
296 | "pattern": re.compile(r'\[666\]'),
297 | "replacement": ' '
298 | },
299 | {
300 | "pattern": re.compile(r'\[让我看看\]'),
301 | "replacement": ' '
302 | },
303 | {
304 | "pattern": re.compile(r'\[叹气\]'),
305 | "replacement": ' '
306 | },
307 | {
308 | "pattern": re.compile(r'\[苦涩\]'),
309 | "replacement": ' '
310 | },
311 | {
312 | "pattern": re.compile(r'\[難受\]'),
313 | "replacement": ' '
314 | },
315 | {
316 | "pattern": re.compile(r'\[裂开\]'),
317 | "replacement": ' '
318 | },
319 | {
320 | "pattern": re.compile(r'\[嘴唇\]'),
321 | "replacement": ' '
322 | },
323 | {
324 | "pattern": re.compile(r'\[爱心\]'),
325 | "replacement": ' '
326 | },
327 | {
328 | "pattern": re.compile(r'\[心碎\]'),
329 | "replacement": ' '
330 | },
331 | {
332 | "pattern": re.compile(r'\[拥抱\]'),
333 | "replacement": ' '
334 | },
335 | {
336 | "pattern": re.compile(r'\[强\]'),
337 | "replacement": ' '
338 | },
339 | {
340 | "pattern": re.compile(r'\[弱\]'),
341 | "replacement": ' '
342 | },
343 | {
344 | "pattern": re.compile(r'\[握手\]'),
345 | "replacement": ' '
346 | },
347 | {
348 | "pattern": re.compile(r'\[胜利\]'),
349 | "replacement": ' '
350 | },
351 | {
352 | "pattern": re.compile(r'\[抱拳\]'),
353 | "replacement": ' '
354 | },
355 | {
356 | "pattern": re.compile(r'\[勾引\]'),
357 | "replacement": ' '
358 | },
359 | {
360 | "pattern": re.compile(r'\[拳头\]'),
361 | "replacement": ' '
362 | },
363 | {
364 | "pattern": re.compile(r'\[OK\]'),
365 | "replacement": ' '
366 | },
367 | {
368 | "pattern": re.compile(r'\[合十\]'),
369 | "replacement": ' '
370 | },
371 | {
372 | "pattern": re.compile(r'\[啤酒\]'),
373 | "replacement": ' '
374 | },
375 | {
376 | "pattern": re.compile(r'\[咖啡]\]'),
377 | "replacement": ' '
378 | },
379 | {
380 | "pattern": re.compile(r'\[蛋糕\]'),
381 | "replacement": ' '
382 | },
383 | {
384 | "pattern": re.compile(r'\[玫瑰\]'),
385 | "replacement": ' '
386 | },
387 | {
388 | "pattern": re.compile(r'\[凋谢\]'),
389 | "replacement": ' '
390 | },
391 | {
392 | "pattern": re.compile(r'\[菜刀\]'),
393 | "replacement": ' '
394 | },
395 | {
396 | "pattern": re.compile(r'\[炸弹\]'),
397 | "replacement": ' '
398 | },
399 | {
400 | "pattern": re.compile(r'\[便便\]'),
401 | "replacement": ' '
402 | },
403 | {
404 | "pattern": re.compile(r'\[月亮\]'),
405 | "replacement": ' '
406 | },
407 | {
408 | "pattern": re.compile(r'\[太阳\]'),
409 | "replacement": ' '
410 | },
411 | {
412 | "pattern": re.compile(r'\[庆 祝\]'),
413 | "replacement": ' '
414 | },
415 | {
416 | "pattern": re.compile(r'\[礼物\]'),
417 | "replacement": ' '
418 | },
419 | {
420 | "pattern": re.compile(r'\[红包\]'),
421 | "replacement": ' '
422 | },
423 | {
424 | "pattern": re.compile(r'\[發\]'),
425 | "replacement": ' '
426 | },
427 | {
428 | "pattern": re.compile(r'\[福\]'),
429 | "replacement": ' '
430 | },
431 | {
432 | "pattern": re.compile(r'\[烟花\]'),
433 | "replacement": ' '
434 | },
435 | {
436 | "pattern": re.compile(r'\[爆竹\]'),
437 | "replacement": ' '
438 | },
439 | {
440 | "pattern": re.compile(r'\[猪头\]'),
441 | "replacement": ' '
442 | },
443 | {
444 | "pattern": re.compile(r'\[跳跳\]'),
445 | "replacement": ' '
446 | },
447 | {
448 | "pattern": re.compile(r'\[发抖\]'),
449 | "replacement": ' '
450 | },
451 | {
452 | "pattern": re.compile(r'\[转圈\]'),
453 | "replacement": ' '
454 | }]
455 |
456 | for rule in replacement_rules:
457 | pattern = rule.get("pattern")
458 | text = re.sub(pattern, rule.get("replacement"), text)
459 | return text
--------------------------------------------------------------------------------
/exporter/html_exporter.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import json
3 | import shutil
4 | import threading
5 | import time
6 | from typing import Tuple
7 |
8 | import xmltodict
9 |
10 | from entity.comment import Comment
11 | from entity.contact import Contact
12 | from exporter.avatar_exporter import AvatarExporter
13 | from exporter.emoji_exporter import EmojiExporter
14 | from exporter.image_exporter import ImageExporter
15 | from exporter.video_exporter import VideoExporter
16 | from log import LOG
17 | from entity.moment_msg import MomentMsg
18 | from pathlib import Path
19 |
20 |
21 | def get_img_div_css(size: int) -> str:
22 | if size == 1:
23 | return 'width:10rem; overflow:hidden'
24 | else:
25 | return 'width:19rem; overflow:hidden'
26 |
27 |
28 | def get_img_css(size: int) -> str:
29 | """object-fit: cover; 预览图居中裁剪
30 | cursor:pointer; 手形鼠标
31 | """
32 | img_style = "object-fit:cover;cursor:pointer;"
33 | if size == 1:
34 | return f'width:10rem;height:10rem;{img_style}'
35 | elif size == 2:
36 | return f'width:8rem;height:8rem;float:left;margin-bottom:0.2rem;margin-right:0.2rem;{img_style}'
37 | elif size == 4:
38 | return f'width:8rem;height:8rem;float:left;margin-bottom:0.2rem;margin-right:0.2rem;{img_style}'
39 | else:
40 | return f'width:5rem;height:5rem;float:left;margin-bottom:0.2rem;margin-right:0.2rem;{img_style}'
41 |
42 |
43 | def is_music_msg(msg: MomentMsg) -> bool:
44 | """判断一个msg是否为音乐分享
45 | """
46 | if msg.timelineObject.ContentObject and msg.timelineObject.ContentObject.mediaList and msg.timelineObject.ContentObject.mediaList.media:
47 | media = msg.timelineObject.ContentObject.mediaList.media[0]
48 | if media.type == '5':
49 | return True
50 | return False
51 |
52 |
53 | def get_music_info(msg: MomentMsg) -> Tuple[str, str, str]:
54 | """获取音乐标题,演唱者,音乐源
55 | """
56 | title = ""
57 | musician = ""
58 | src = ""
59 | if msg.timelineObject.ContentObject and msg.timelineObject.ContentObject.mediaList and msg.timelineObject.ContentObject.mediaList.media:
60 | media = msg.timelineObject.ContentObject.mediaList.media[0]
61 | title = media.title
62 | musician = media.description
63 | if media.url:
64 | src = media.url.text
65 | return title, musician, src
66 |
67 |
68 | class HtmlExporter(threading.Thread):
69 |
70 | def __init__(self, gui: 'Gui', dir_name: str, contacts_map: dict[str, Contact], begin_date: datetime.date,
71 | end_date: datetime.date, convert_video: int):
72 | self.dir_name = dir_name
73 | if Path(f"output/{self.dir_name}").exists():
74 | shutil.rmtree(f"output/{self.dir_name}")
75 | shutil.copytree("resource/template/", f"output/{self.dir_name}")
76 |
77 | self.gui = gui
78 | self.avatar_exporter = AvatarExporter(dir_name)
79 | self.image_exporter = ImageExporter(dir_name)
80 | self.video_exporter = VideoExporter(dir_name)
81 | self.html_head = None
82 | self.html_end = None
83 | self.file = None
84 | self.contacts_map = contacts_map
85 | self.begin_date = begin_date
86 | self.end_date = end_date
87 | self.convert_video = convert_video
88 | self.stop_flag = False
89 | super().__init__()
90 |
91 | def run(self) -> None:
92 |
93 | with open(f"resource/template.html", encoding='utf-8') as f:
94 | content = f.read()
95 | self.html_head, self.html_end = content.split('/*内容分割线*/')
96 | self.file = open(f"output/{self.dir_name}/index.html", 'w', encoding='utf-8')
97 |
98 | if self.gui.account_info and self.gui.account_info.get('wxid'):
99 | self.avatar_exporter.get_avatar_path(self.gui.account_info.get('wxid'))
100 | self.html_head = self.html_head.replace("{my_wxid}", f"{self.gui.account_info.get('wxid')}")
101 | from app.DataBase import micro_msg_db
102 | my_info = micro_msg_db.get_contact_by_username(self.gui.account_info.get('wxid'))
103 | self.html_head = self.html_head.replace("{my_name}", f"{my_info[4]}")
104 |
105 | from app.DataBase import sns_db
106 | cover_url = sns_db.get_cover_url()
107 | if cover_url:
108 | cover_path = self.image_exporter.save_image((cover_url, "", ""), 'image')
109 | self.html_head = self.html_head.replace("{cover_path}", cover_path)
110 |
111 | self.file.write(self.html_head)
112 | # 加一天
113 | end_date = self.end_date + datetime.timedelta(days=1)
114 | begin_time = time.mktime(
115 | datetime.datetime(self.begin_date.year, self.begin_date.month, self.begin_date.day).timetuple())
116 | end_time = time.mktime(datetime.datetime(end_date.year, end_date.month, end_date.day).timetuple())
117 |
118 | self.gui.image_decrypter.decrypt_images(self, self.begin_date, end_date, self.dir_name)
119 | self.gui.video_decrypter.decrypt_videos(self, self.begin_date, end_date, self.dir_name, self.convert_video)
120 |
121 |
122 | message_datas = sns_db.get_messages_in_time(begin_time, end_time)
123 | for index, message_data in enumerate(message_datas):
124 | if not self.stop_flag:
125 | if message_data[0] in self.contacts_map:
126 | comments_datas = sns_db.get_comment_by_feed_id(message_data[2])
127 | comments: list[Comment] = []
128 | for c in comments_datas:
129 | contact = Comment(c[0], c[1], c[2])
130 | comments.append(contact)
131 | self.export_msg(message_data[1], comments, self.contacts_map)
132 | # 更新进度条 前30%视频处理 后70%其他处理
133 | progress = round(index / len(message_datas) * 70)
134 | self.gui.update_export_progressbar(30 + progress)
135 | self.gui.update_export_progressbar(100)
136 | self.finish_file()
137 | self.gui.export_succeed()
138 |
139 | def stop(self) -> None:
140 | self.stop_flag = True
141 |
142 | def export_msg(self, message: str, comments: list[Comment], contacts_map: dict[str, Contact]) -> None:
143 |
144 | LOG.info(message)
145 | # force_list: 强制要求转media为list
146 | msg_dict = xmltodict.parse(message, force_list={'media'})
147 | msg_json = json.dumps(msg_dict)
148 | msg = MomentMsg.from_json(msg_json)
149 |
150 | # 微信ID
151 | username = msg.timelineObject.username
152 | # 头像路径
153 | avatar_path = self.avatar_exporter.get_avatar_path(username)
154 |
155 | contact = contacts_map.get(username)
156 | # 备注, 或用户名
157 | remark = contact.remark if contact.remark else contact.nickName
158 |
159 | # 朋友圈图片
160 | images = self.image_exporter.get_images(msg)
161 |
162 | # 朋友圈视频
163 | videos = self.video_exporter.get_videos(msg)
164 |
165 | # 样式 3:链接样式
166 | content_style = msg.timelineObject.ContentObject.contentStyle
167 |
168 | html = ' \n'
169 | html += '
\n'
170 | html += '
\n'
171 | html += f'
\n'
172 | html += '
\n'
173 | html += '
\n'
174 | html += '
\n'
249 | html += '
\n'
250 | self.file.write(html)
251 |
252 | def finish_file(self):
253 | self.file.write(self.html_end)
254 | self.file.close()
255 |
--------------------------------------------------------------------------------
/exporter/image_exporter.py:
--------------------------------------------------------------------------------
1 | import os
2 | from io import BytesIO
3 | from pathlib import Path
4 | from typing import Tuple, Optional
5 | from PIL import Image
6 | from entity.moment_msg import MomentMsg, Media
7 | import requests
8 | import uuid
9 |
10 |
11 | class ImageExporter:
12 | def __init__(self, dir_name: str):
13 | self.dir_name = dir_name
14 | if not os.path.exists(f'output/{self.dir_name}/thumbs/'):
15 | os.mkdir(f'output/{self.dir_name}/thumbs/')
16 | if not os.path.exists(f'output/{self.dir_name}/images/'):
17 | os.mkdir(f'output/{self.dir_name}/images/')
18 |
19 | @staticmethod
20 | def get_image(link: tuple) -> bytes:
21 | """ 向微信服务器请求图片
22 | """
23 | url, idx, token = link
24 | # 如果需要传递token
25 | if idx and token:
26 | url = f'{url}?idx={idx}&token={token}'
27 | response = requests.get(url)
28 | if response.ok:
29 | return response.content
30 |
31 | def save_image(self, link: tuple, img_type: str) -> str:
32 | """ 下载图片
33 | """
34 | file_name = uuid.uuid4()
35 | if not (img_type == 'image' or img_type == 'thumb'):
36 | raise Exception("img_type 参数非法")
37 | content = self.get_image(link)
38 | if content:
39 | with open(f'output/{self.dir_name}/{img_type}s/{file_name}.jpg', 'wb') as file:
40 | file.write(content)
41 | return f'{img_type}s/{file_name}.jpg'
42 |
43 | @staticmethod
44 | def get_image_thumb_and_url(media_item, content_style:int) -> Tuple[Tuple, Tuple]:
45 | """ 获取图片的缩略图与大图的链接
46 | """
47 | thumb = None
48 | url = None
49 | # 普通图片
50 | if media_item.type == "2":
51 | thumb = (media_item.thumb.text, media_item.thumb.enc_idx, media_item.thumb.token)
52 | url = (media_item.url.text, media_item.url.enc_idx, media_item.url.token)
53 | # 微信音乐
54 | if media_item.type == "5":
55 | thumb = (media_item.thumb.text, "", "")
56 | url = (media_item.thumb.text, "", "")
57 | # 超链接类型
58 | if content_style == 3:
59 | thumb = (media_item.thumb.text, "", "")
60 | url = (media_item.thumb.text, "", "")
61 |
62 | return thumb, url
63 |
64 | def get_images(self, msg: MomentMsg) -> list[Tuple]:
65 | """ 获取一条朋友圈的全部图像, 返回值是一个元组列表
66 | [(缩略图路径,原图路径),(缩略图路径,原图路径)]
67 | """
68 | results = []
69 | if not msg.timelineObject.ContentObject.mediaList:
70 | return results
71 |
72 | media = msg.timelineObject.ContentObject.mediaList.media
73 | for media_item in media:
74 | thumb, url = self.get_image_thumb_and_url(media_item, msg.timelineObject.ContentObject.contentStyle)
75 | if thumb and url:
76 | thumb_path = None
77 | image_path = None
78 | # 主图内容
79 | image_content = self.get_image(url)
80 | # 如果拿不到主图数据
81 | if not image_content:
82 | continue
83 | # 如果在腾讯服务器获取到jpg图片
84 | if image_content[:2] == b'\xff\xd8':
85 | file_name = uuid.uuid4()
86 | with open(f'output/{self.dir_name}/images/{file_name}.jpg', 'wb') as file:
87 | file.write(image_content)
88 | image_path = f'images/{file_name}.jpg'
89 | # 缩略图内容
90 | thumb_content = self.get_image(thumb)
91 | file_name = uuid.uuid4()
92 | with open(f'output/{self.dir_name}/thumbs/{file_name}.jpg', 'wb') as file:
93 | file.write(thumb_content)
94 | thumb_path = f'thumbs/{file_name}.jpg'
95 | # 如果图片已加密,进入缓存图片中匹配
96 | else:
97 | # 获取2024-06格式的时间
98 | month = msg.timelineObject.create_year_month
99 | image_content = self.get_image(url)
100 | thumb_content = self.get_image(thumb)
101 | # 从缓存里找文件
102 | image_file = Path((f"output/{self.dir_name}/images/{month}/"
103 | f"{len(image_content)}_{len(thumb_content)}.jpg"))
104 | thumb_file = Path((f"output/{self.dir_name}/thumbs/{month}/"
105 | f"{len(image_content)}_{len(thumb_content)}.jpg"))
106 | if image_file.exists():
107 | image_path = image_file.resolve()
108 | if thumb_file.exists():
109 | thumb_path = thumb_file.resolve()
110 |
111 | if thumb_path and image_path:
112 | results.append((thumb_path, image_path))
113 |
114 | return results
115 |
116 | def get_finder_images(self, msg: MomentMsg) -> Optional[str]:
117 | """ 获取视频号的封面图
118 | """
119 | results = None
120 | if not msg.timelineObject.ContentObject.finderFeed:
121 | return results
122 |
123 | if not msg.timelineObject.ContentObject.finderFeed.mediaList:
124 | return results
125 |
126 | media = msg.timelineObject.ContentObject.finderFeed.mediaList.media
127 | for media_item in media:
128 | thumb_path = self.save_image((media_item.thumbUrl, "", ""), 'thumb')
129 | return thumb_path
130 |
--------------------------------------------------------------------------------
/exporter/video_exporter.py:
--------------------------------------------------------------------------------
1 | import math
2 | import os
3 | import re
4 | from pathlib import Path
5 | from entity.moment_msg import MomentMsg, Media
6 |
7 |
8 | class VideoExporter:
9 | def __init__(self, dir_name):
10 | self.dir_name = dir_name
11 | if not os.path.exists(f'output/{self.dir_name}/videos/'):
12 | os.mkdir(f'output/{self.dir_name}/videos/')
13 |
14 |
15 | def find_video_by_md5(self, md5):
16 | """
17 | 使用MD5匹配视频
18 | """
19 | folder_path = Path(f'output/{self.dir_name}/videos/')
20 | pattern = re.compile(r'^(.*?)(?=_)')
21 |
22 | for file_path in folder_path.iterdir():
23 | match = pattern.search(file_path.name)
24 | if match:
25 | filename_md5 = match.group()
26 | if filename_md5 == md5:
27 | return file_path.name
28 |
29 | def find_video_by_duration(self, duration):
30 | """
31 | 使用视频时长匹配视频
32 | """
33 | folder_path = Path(f'output/{self.dir_name}/videos/')
34 | pattern = re.compile(r'_([0-9.]+)\.mp4')
35 |
36 | for file_path in folder_path.iterdir():
37 | match = pattern.search(file_path.name)
38 | if match:
39 | filename_duration = float(match.group(1))
40 | if math.isclose(filename_duration, duration, abs_tol=0.005):
41 | return file_path.name
42 |
43 | def get_videos(self, msg: MomentMsg) -> list[str]:
44 | """ 获取一条朋友圈的全部视频, 返回值是一个文件路径列表
45 | """
46 | results = []
47 | if not msg.timelineObject.ContentObject.mediaList:
48 | return results
49 |
50 | media = msg.timelineObject.ContentObject.mediaList.media
51 | for media_item in media:
52 | if media_item.type == "6":
53 | duration = media_item.videoDuration
54 | rounded_duration = round(float(duration), 2)
55 | # 先用MD5匹配缓存中的视频
56 | # 如果找不到使用视频时长再次匹配
57 | video = self.find_video_by_md5(media_item.url.md5)
58 | if video:
59 | results.append(f'videos/{video}')
60 | else:
61 | video = self.find_video_by_duration(rounded_duration)
62 | if video:
63 | results.append(f'videos/{video}')
64 |
65 | return results
66 |
--------------------------------------------------------------------------------
/gui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/gui/__init__.py
--------------------------------------------------------------------------------
/gui/auto_scroll_guide.py:
--------------------------------------------------------------------------------
1 | import tkinter
2 | import tkinter.ttk
3 |
4 | import win32gui
5 |
6 | from entity.contact import Contact
7 | from helper.auto_scroll import AutoScroll
8 |
9 |
10 | class AutoScrollGuide:
11 |
12 | def __init__(self, root):
13 | self.flood_moments_note = None
14 | self.auto_thread = None
15 | self.frame = tkinter.LabelFrame(root)
16 |
17 | self.open_moments_guide = tkinter.Label(self.frame, text="请打开朋友圈窗口")
18 | self.open_moments_guide.pack()
19 |
20 | image = tkinter.PhotoImage(file='resource/gui_pictures/open_moments_guide.png')
21 | self.open_moments_guide_image = tkinter.Label(self.frame, image=image)
22 | self.open_moments_guide_image.image = image
23 | self.open_moments_guide_image.pack()
24 |
25 | self.auto_scroll_button_text = tkinter.StringVar()
26 | self.auto_scroll_button_text.set("开始")
27 |
28 | self.auto_scroll_button = tkinter.ttk.Button(self.frame, textvariable=self.auto_scroll_button_text,
29 | command=self.switch_auto_scroll)
30 | self.auto_scroll_button.pack(pady=5)
31 |
32 | def switch_auto_scroll(self):
33 |
34 | if self.auto_thread is None:
35 | moments_hwnd = win32gui.FindWindow("SnsWnd", '朋友圈')
36 | if moments_hwnd != 0:
37 | self.auto_thread = AutoScroll(self, moments_hwnd)
38 | self.flood_moments_note = tkinter.Label(self.frame, text="正在自动读取朋友圈数据......."
39 | "\n可将窗口最小化,后台自动执行"
40 | "\n可随时查看进度,可随时停止")
41 | self.flood_moments_note.pack()
42 | self.auto_thread.start()
43 | self.auto_scroll_button_text.set("停止")
44 | else:
45 | pass
46 | else:
47 | if self.auto_thread.scrolling:
48 | self.auto_scroll_button_text.set("继续")
49 | self.auto_thread.set_scrolling(False)
50 | else:
51 | self.auto_scroll_button_text.set("停止")
52 | self.auto_thread.set_scrolling(True)
53 |
--------------------------------------------------------------------------------
/gui/auto_scrolls_single_guide.py:
--------------------------------------------------------------------------------
1 | import time
2 | import tkinter
3 | import tkinter.ttk
4 |
5 | import win32con
6 | import win32gui
7 |
8 | from helper.auto_scroll_single import AutoScrollSingle
9 |
10 |
11 | class AutoScrollSingleGuide:
12 |
13 | def __init__(self, root):
14 | self.working_note = None
15 | self.auto_thread = None
16 | self.frame = tkinter.LabelFrame(root)
17 |
18 | self.guide = tkinter.Label(self.frame, text="请打开搜一搜窗口\n点击开始后不要操作键鼠")
19 | self.guide.pack()
20 |
21 | self.search_username = tkinter.Entry(self.frame, width=12)
22 | self.search_username.insert(0, '请输入好友昵称')
23 | self.search_username.config(fg='grey')
24 | self.search_username.bind('', self.on_search_username_click)
25 | self.search_username.pack()
26 |
27 | image = tkinter.PhotoImage(file='resource/gui_pictures/open_search_guide.png')
28 | self.guide_image = tkinter.Label(self.frame, image=image)
29 | self.guide_image.image = image
30 | self.guide_image.pack()
31 |
32 | self.button_text = tkinter.StringVar()
33 | self.button_text.set("开始")
34 |
35 | self.button = tkinter.ttk.Button(self.frame, textvariable=self.button_text,
36 | command=self.switch_auto_scroll_single)
37 | self.button.pack(pady=5)
38 |
39 | def on_search_username_click(self, event):
40 | if self.search_username.get() == '请输入好友昵称':
41 | self.search_username.delete(0, tkinter.END)
42 | self.search_username.config(fg='black')
43 |
44 | def switch_auto_scroll_single(self):
45 |
46 | search_hwnd = win32gui.FindWindow('Chrome_WidgetWin_0', '微信')
47 | wechat_hwnd = win32gui.FindWindow('WeChatMainWndForPC', '微信')
48 |
49 | search_username = self.search_username.get()
50 |
51 | if self.auto_thread is None:
52 | if search_username == '请输入好友昵称' or search_username == '':
53 | self.search_username.config(fg='red')
54 | return
55 | if search_hwnd != 0 and wechat_hwnd != 0:
56 |
57 | self.auto_thread = AutoScrollSingle(self, search_hwnd, search_username)
58 | self.working_note = tkinter.Label(self.frame, text="正在自动读取朋友圈数据......."
59 | "\n请不要遮挡搜一搜窗口")
60 |
61 |
62 | self.working_note.pack()
63 | self.auto_thread.start()
64 | self.button_text.set("停止")
65 | else:
66 | pass
67 | else:
68 | if self.auto_thread.scrolling:
69 | self.button_text.set("开始")
70 | self.auto_thread.set_scrolling(False)
71 | self.auto_thread = None
72 | else:
73 | self.button_text.set("停止")
74 | self.switch_auto_scroll_single()
75 |
--------------------------------------------------------------------------------
/gui/gui.py:
--------------------------------------------------------------------------------
1 | import os
2 | import tkinter
3 | import tkinter.font
4 | import tkinter.ttk
5 | from datetime import datetime, timedelta
6 | from pathlib import Path
7 | from typing import Optional
8 | import tkcalendar
9 | from decrypter.db_decrypt import DatabaseDecrypter
10 | from decrypter.image_decrypt import ImageDecrypter
11 | from decrypter.video_decrypt import VideoDecrypter
12 | from gui.auto_scroll_guide import AutoScrollGuide
13 | from gui.auto_scrolls_single_guide import AutoScrollSingleGuide
14 | from gui.tool_tip import ToolTip
15 | from entity.contact import Contact
16 | from exporter.html_exporter import HtmlExporter
17 | from gui.listbox_with_search import ListboxWithSearch
18 |
19 |
20 | class Gui:
21 | def __init__(self):
22 |
23 | self.restart_note1 = None
24 | self.restart_note2 = None
25 | self.auto_scroll_single_guide = None
26 | self.auto_scroll_guide = None
27 | self.auto_scroll_frame = None
28 | self.search_username = None
29 | self.open_search_guide_image = None
30 | self.open_search_guide = None
31 | self.auto_scroll_button_single = None
32 | self.auto_scroll_button_single_text = None
33 | self.convert_video = None
34 | self.convert_video_var = None
35 | self.html_exporter_thread = None
36 | self.confirm_button_text = None
37 | self.succeed_label_2 = None
38 | self.succeed_label = None
39 | self.auto_scroll_button_text = None
40 | self.warning_label = None
41 | self.root = None
42 | self.waiting_label = None
43 | self.listbox = None
44 | self.begin_calendar = None
45 | self.end_calendar = None
46 | self.end_calendar_label = None
47 | self.begin_calendar_label = None
48 | self.confirm_button = None
49 | self.decrypt_progressbar = None
50 | self.export_progressbar = None
51 | self.next_step_button = None
52 | self.decrypter = None
53 | self.auto_scroll_button = None
54 | self.auto_scrolling_thread = None
55 | self.decrypt_note = None
56 | self.decrypt_note_text = None
57 | self.account_info = None
58 | self.video_decrypter = None
59 | self.image_decrypter = None
60 | self.export_dir_name = None
61 | self.exporting = False
62 | # 1: 自动滚动数据 2: 解密数据库 3: 导出
63 | self.page_stage = 1
64 |
65 | def run_gui(self):
66 | self.root = tkinter.Tk()
67 | self.root.geometry('650x650')
68 | self.root.title('朋友圈导出')
69 |
70 | self.waiting_label = tkinter.ttk.Label(self.root, text="正在连接微信....",
71 | font=("微软雅黑", 16), anchor='center')
72 | self.waiting_label.place(relx=0.5, rely=0.05, anchor='center')
73 |
74 | self.root.mainloop()
75 |
76 | def wechat_logged_in(self, account_info):
77 |
78 | self.account_info = account_info
79 | self.waiting_label.config(text="微信已登录")
80 |
81 | self.auto_scroll_button_text = tkinter.StringVar()
82 | self.auto_scroll_button_text.set("自动浏览全部朋友圈")
83 | self.auto_scroll_button = tkinter.ttk.Button(self.root, textvariable=self.auto_scroll_button_text,
84 | command=self.open_auto_scroll_guide)
85 | self.auto_scroll_button.place(relx=0.35, rely=0.15, anchor='center')
86 |
87 | self.auto_scroll_button_single_text = tkinter.StringVar()
88 | self.auto_scroll_button_single_text.set("自动浏览单个朋友")
89 | self.auto_scroll_button_single = tkinter.ttk.Button(self.root, textvariable=self.auto_scroll_button_single_text,
90 | command=self.switch_auto_scroll_single)
91 | self.auto_scroll_button_single.place(relx=0.655, rely=0.15, anchor='center')
92 |
93 | self.next_step_button = tkinter.ttk.Button(self.root, text="下一步", command=self.next_step)
94 | self.next_step_button.place(relx=0.65, rely=0.8)
95 |
96 |
97 |
98 | def open_auto_scroll_guide(self):
99 |
100 | if self.auto_scroll_single_guide and self.auto_scroll_single_guide.frame:
101 | self.auto_scroll_single_guide.frame.place_forget()
102 |
103 | self.auto_scroll_guide = AutoScrollGuide(self.root)
104 | self.auto_scroll_guide.frame.place(relx=0.5, rely=0.5, anchor='center')
105 |
106 | def switch_auto_scroll_single(self):
107 |
108 | if self.auto_scroll_guide and self.auto_scroll_guide.frame:
109 | self.auto_scroll_guide.frame.place_forget()
110 |
111 | self.auto_scroll_single_guide = AutoScrollSingleGuide(self.root)
112 | self.auto_scroll_single_guide.frame.place(relx=0.5, rely=0.5, anchor='center')
113 |
114 |
115 | def next_step(self):
116 |
117 | if self.page_stage == 1:
118 |
119 | if self.auto_scroll_guide and self.auto_scroll_guide.auto_thread:
120 | self.auto_scroll_guide.auto_thread.set_scrolling(False)
121 |
122 | if self.auto_scroll_single_guide and self.auto_scroll_single_guide.auto_thread:
123 | self.auto_scroll_guide.auto_thread.set_scrolling(False)
124 |
125 | self.auto_scroll_button.place_forget()
126 | self.auto_scroll_button_single.place_forget()
127 |
128 | if self.auto_scroll_guide and self.auto_scroll_guide.frame:
129 | self.auto_scroll_guide.frame.place_forget()
130 |
131 | if self.auto_scroll_single_guide and self.auto_scroll_single_guide.frame:
132 | self.auto_scroll_single_guide.frame.place_forget()
133 |
134 | self.restart_note1 = tkinter.Label(self.root, text="请关闭微信客户端", fg="red")
135 | self.restart_note1.place(relx=0.5, rely=0.2, anchor='center')
136 | self.restart_note2 = tkinter.Label(self.root, text="然后点击下一步")
137 | self.restart_note2.place(relx=0.5, rely=0.3, anchor='center')
138 |
139 | if self.page_stage == 2:
140 |
141 | self.restart_note1.place_forget()
142 | self.restart_note2.place_forget()
143 | self.waiting_label.place_forget()
144 |
145 | self.decrypter = DatabaseDecrypter(self, self.account_info.get("filePath"), self.account_info.get("key"))
146 |
147 | self.decrypt_note_text = tkinter.StringVar()
148 | self.decrypt_note_text.set("正在复制数据.....")
149 |
150 | self.decrypt_note = tkinter.Label(self.root, textvariable=self.decrypt_note_text)
151 | self.decrypt_note.place(relx=0.5, rely=0.2, anchor='center')
152 | self.decrypt_progressbar = tkinter.ttk.Progressbar(self.root)
153 | self.decrypt_progressbar.place(relx=0.5, rely=0.3, anchor='center')
154 |
155 | # 进度值最大值
156 | self.decrypt_progressbar['maximum'] = 100
157 | # 进度值初始值
158 | self.decrypt_progressbar['value'] = 0
159 | # 解密过程禁用下一步按钮
160 | self.next_step_button.config(state=tkinter.DISABLED)
161 | self.decrypter.decrypt()
162 | if self.page_stage == 3:
163 | self.decrypt_note.place_forget()
164 | self.decrypt_progressbar.place_forget()
165 | self.init_export_page()
166 | # 不再有下一步按钮
167 | self.next_step_button.place_forget()
168 | # 初始化视频导出器
169 | self.video_decrypter = VideoDecrypter(self, self.account_info.get("filePath"))
170 | # 初始化图片导出器
171 | self.image_decrypter = ImageDecrypter(self, self.account_info.get("filePath"))
172 |
173 |
174 | self.page_stage = self.page_stage + 1
175 |
176 | def init_export_page(self):
177 |
178 | from app.DataBase import micro_msg_db
179 | contact_datas = micro_msg_db.get_contact()
180 |
181 | contacts = []
182 | for c in contact_datas:
183 | contact = Contact(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], c[11])
184 | contacts.append(contact)
185 |
186 | def validate_contact(this_contact: Contact):
187 | c_type = this_contact.type
188 | user_name: str = this_contact.userName
189 |
190 | # 不是其他号码
191 | is_misc_account = c_type == 1 or c_type == 33 or c_type == 513
192 | # 不是公众号
193 | is_gh_account = user_name.startswith("gh_")
194 | # 不是聊天群
195 | is_chatroom = user_name.endswith("@chatroom")
196 | # 不是文件传输助手
197 | is_filehelper = this_contact.userName == "filehelper"
198 | return (not is_misc_account) and (not is_gh_account) and (not is_chatroom) and (not is_filehelper)
199 |
200 | filtered = filter(validate_contact, contacts)
201 | contacts = list(filtered)
202 |
203 | self.listbox = ListboxWithSearch(self.root, contacts)
204 | self.listbox.frame.place(relx=0.05, rely=0.03)
205 |
206 | self.begin_calendar_label = tkinter.ttk.Label(text="开始日期")
207 | self.begin_calendar_label.place(relx=0.65, rely=0.15)
208 |
209 | # 默认开始时间是100天前
210 | current_date = datetime.now()
211 | half_year_ago = current_date - timedelta(days=100)
212 | self.begin_calendar = tkcalendar.DateEntry(master=self.root, locale="zh_CN", year=half_year_ago.year,
213 | month=half_year_ago.month, day=half_year_ago.day,
214 | maxdate=datetime.now())
215 | self.begin_calendar.place(relx=0.65, rely=0.2)
216 |
217 | self.end_calendar_label = tkinter.ttk.Label(text="截止日期")
218 | self.end_calendar_label.place(relx=0.65, rely=0.25)
219 | self.end_calendar = tkcalendar.DateEntry(master=self.root, locale="zh_CN", maxdate=datetime.now())
220 | self.end_calendar.place(relx=0.65, rely=0.3)
221 |
222 |
223 | self.convert_video_var = tkinter.IntVar(value=0)
224 | self.convert_video = tkinter.ttk.Checkbutton(self.root, text='视频转码', variable=self.convert_video_var)
225 | self.convert_video.place(relx=0.65, rely=0.45)
226 | ToolTip(self.convert_video,
227 | "视频原始格式为H265,只支持\nChrome浏览器播放,勾选后\n将视频转码为H264,支持大\n部分浏览器,但导出速度变慢")
228 |
229 | self.confirm_button_text = tkinter.StringVar()
230 | self.confirm_button_text.set("开始导出")
231 |
232 | self.confirm_button = tkinter.ttk.Button(self.root, textvariable=self.confirm_button_text,
233 | command=self.confirm_export)
234 | self.confirm_button.place(relx=0.65, rely=0.6)
235 |
236 | # 导出成功的提示
237 | self.succeed_label = tkinter.Label(self.root, text="导出结束")
238 | self.succeed_label_2 = tkinter.Label(self.root, text="打开文件夹", fg="#0000FF", cursor="hand2")
239 | self.succeed_label_2.bind("", self.open_target_folder)
240 |
241 | # 进度条
242 | self.export_progressbar = tkinter.ttk.Progressbar(self.root, length=150)
243 |
244 | def confirm_export(self):
245 |
246 | if self.html_exporter_thread and not self.html_exporter_thread.stop_flag:
247 | self.html_exporter_thread.stop()
248 | else:
249 | if not self.warning_label:
250 | self.warning_label = tkinter.Label(self.root, fg="red")
251 | self.warning_label.place(relx=0.65, rely=0.55)
252 |
253 | self.warning_label.config(text="")
254 | contacts = self.listbox.get_contacts()
255 | if not contacts:
256 | self.warning_label.config(text=f"请选择至少一个联系人")
257 | return
258 |
259 | if self.begin_calendar.get_date() > self.end_calendar.get_date():
260 | self.warning_label.config(text=f"开始时间必须小于截止时间")
261 | return
262 |
263 | self.export_progressbar.place(relx=0.64, rely=0.68)
264 | # 进度值最大值
265 | self.export_progressbar['maximum'] = 100
266 | # 进度值初始值
267 | self.export_progressbar['value'] = 0
268 |
269 | current_time = datetime.now()
270 | self.export_dir_name = current_time.strftime("%Y_%m_%d_%H%M%S")
271 | contact_map = {contact.userName: contact for contact in contacts}
272 |
273 | self.confirm_button_text.set("停止导出")
274 | self.succeed_label.place_forget()
275 | self.succeed_label_2.place_forget()
276 |
277 | # 导出线程
278 | self.html_exporter_thread = HtmlExporter(self, self.export_dir_name, contact_map,
279 | self.begin_calendar.get_date(), self.end_calendar.get_date(),
280 | self.convert_video_var.get())
281 | self.html_exporter_thread.start()
282 |
283 | def update_decrypt_progressbar(self, progress):
284 | self.decrypt_progressbar['value'] = progress
285 | self.root.update()
286 |
287 | def update_export_progressbar(self, progress):
288 | self.export_progressbar['value'] = progress
289 | self.root.update()
290 |
291 | def export_succeed(self):
292 | self.confirm_button_text.set("开始导出")
293 | self.succeed_label.place(relx=0.64, rely=0.75)
294 | self.succeed_label_2.place(relx=0.76, rely=0.75)
295 |
296 | def open_target_folder(self, event):
297 | folder_path = Path(f"output/{self.export_dir_name}/")
298 | # 转换为绝对路径
299 | absolute_path = folder_path.resolve()
300 | os.startfile(absolute_path)
301 |
--------------------------------------------------------------------------------
/gui/listbox_with_search.py:
--------------------------------------------------------------------------------
1 | import tkinter as tk
2 | from entity.contact import Contact
3 |
4 |
5 | class ListboxWithSearch:
6 |
7 | def __init__(self, root, contacts: list[Contact]):
8 |
9 | # key index(在控件里的编号) value Contact
10 | self.index_contact_map = {}
11 |
12 | self.frame = tk.LabelFrame(root, text="请选择导出联系人")
13 | self.tool_frame = tk.Frame(self.frame)
14 | self.tool_frame.pack()
15 |
16 | self.search_label = tk.Label(self.tool_frame, text="搜索:")
17 | self.search_label.pack(side='left')
18 |
19 | self.re = tk.Entry(self.tool_frame, width=10)
20 | self.re.pack(side='left')
21 | self.re.bind("", self.filter)
22 |
23 | self.select_all_button = tk.Button(self.tool_frame, text="全选", command=self.select_all)
24 | self.select_all_button.pack(side='left', padx="10")
25 |
26 | self.invert_select_button = tk.Button(self.tool_frame, text="反选", command=self.invert_select)
27 | self.invert_select_button.pack(side='left', padx="2")
28 |
29 | self.scrollbar = tk.Scrollbar(self.frame)
30 | self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
31 |
32 | self.lb = tk.Listbox(self.frame, selectmode='multiple', height=20, width=30, exportselection=False,
33 | yscrollcommand=self.scrollbar.set)
34 | self.scrollbar.config(command=self.lb.yview)
35 |
36 | self.lb.bind('<>', self.on_select)
37 |
38 | self.lb.pack()
39 | self.contacts = contacts
40 |
41 |
42 | for index, contact in enumerate(contacts):
43 | text = f'{contact.nickName}({contact.remark})' if contact.remark else f'{contact.nickName}'
44 | self.lb.insert(index, text)
45 | self.index_contact_map[index] = contact
46 |
47 | def select_all(self, event=None):
48 | for index in self.index_contact_map.keys():
49 | self.lb.select_set(index)
50 | self.on_select()
51 |
52 | def on_select(self, event=None):
53 | selection = self.lb.curselection()
54 | self.frame.config(text=f"已选择{len(selection)}个联系人")
55 |
56 | def invert_select(self, event=None):
57 | """反选"""
58 | selected = self.lb.curselection()
59 | for index in self.index_contact_map.keys():
60 | if index in selected:
61 | self.lb.selection_clear(index)
62 | else:
63 | self.lb.select_set(index)
64 | self.on_select()
65 |
66 | def get_contacts(self, event=None):
67 | contacts = []
68 | selected = self.lb.curselection()
69 | for index in selected:
70 | contacts.append(self.index_contact_map.get(index))
71 | return contacts
72 |
73 | def filter(self, event=None):
74 | p = self.re.get()
75 | if p:
76 | for index, contact in self.index_contact_map.items():
77 | text = f'{contact.nickName}({contact.remark})' if contact.remark else f'{contact.nickName}'
78 | if p in text:
79 | self.lb.yview(index)
80 | break
81 |
--------------------------------------------------------------------------------
/gui/tool_tip.py:
--------------------------------------------------------------------------------
1 | import tkinter as tk
2 | from tkinter import ttk
3 |
4 | class ToolTip:
5 | def __init__(self, widget, text):
6 | self.widget = widget
7 | self.text = text
8 | self.tooltip = None
9 | self.widget.bind("", self.show_tooltip)
10 | self.widget.bind("", self.hide_tooltip)
11 |
12 | def show_tooltip(self, event=None):
13 | x = y = 0
14 | x, y, _, _ = self.widget.bbox("insert")
15 | x += self.widget.winfo_rootx() + 25
16 | y += self.widget.winfo_rooty() + 25
17 | self.tooltip = tk.Toplevel(self.widget)
18 | self.tooltip.wm_overrideredirect(True)
19 | self.tooltip.wm_geometry(f"+{x}+{y}")
20 | label = tk.Label(self.tooltip, text=self.text, background="#ffffe0", relief="solid", borderwidth=1)
21 | label.pack(ipadx=1)
22 |
23 | def hide_tooltip(self, event=None):
24 | if self.tooltip:
25 | self.tooltip.destroy()
26 | self.tooltip = None
--------------------------------------------------------------------------------
/helper/auto_scroll.py:
--------------------------------------------------------------------------------
1 | import random
2 | import time
3 | import threading
4 | import pywintypes
5 | import win32api
6 | import win32con
7 | import win32gui
8 |
9 |
10 | class AutoScroll(threading.Thread):
11 |
12 | def __init__(self, gui, moments_hwnd):
13 | self.gui = gui
14 | self.moments_hwnd = moments_hwnd
15 | self.scrolling = False
16 | super().__init__()
17 |
18 | def run(self) -> None:
19 | self.scrolling = True
20 | while True:
21 | if self.scrolling:
22 | try:
23 | rect = win32gui.GetWindowRect(self.moments_hwnd)
24 | x = (rect[0] + rect[2]) // 2
25 | y = (rect[1] + rect[3]) // 2
26 | notch = 10
27 | win32api.SendMessage(self.moments_hwnd, win32con.WM_MOUSEWHEEL,
28 | win32api.MAKELONG(0, -120 * notch), win32api.MAKELONG(x, y))
29 |
30 | self.gui.flood_moments_note.pack()
31 | random_sleep = random.uniform(0.7, 0.8)
32 | time.sleep(random_sleep)
33 | except pywintypes.error as e:
34 | self.moments_hwnd = win32gui.FindWindow("SnsWnd", '朋友圈')
35 | self.gui.flood_moments_note.pack_forget()
36 | time.sleep(1)
37 | else:
38 | self.gui.flood_moments_note.pack_forget()
39 | time.sleep(1)
40 |
41 | def set_scrolling(self, scrolling: bool) -> None:
42 | self.scrolling = scrolling
--------------------------------------------------------------------------------
/helper/auto_scroll_single.py:
--------------------------------------------------------------------------------
1 | import math
2 | import threading
3 | import time
4 | import traceback
5 | import pyautogui
6 | import pyperclip
7 | import win32con
8 | import win32gui
9 | from retry import retry
10 | from win32api import GetSystemMetrics
11 |
12 | import log
13 |
14 |
15 | class AutoScrollSingle(threading.Thread):
16 |
17 | def __init__(self, gui, search_hwnd, friend_name):
18 | self.gui = gui
19 | self.search_hwnd = search_hwnd
20 | self.friend_name = friend_name
21 | self.scrolling = False
22 | self.resolutions = ['', '1920', '1600', '2560_125', '2560_175', '2560_100', '1366']
23 | super().__init__()
24 |
25 | @retry(tries=5, delay=2)
26 | def find_moments_tab(self):
27 | result = None
28 | for resolution in self.resolutions:
29 | try:
30 | result = pyautogui.locateCenterOnScreen(f'resource/auto_gui/{resolution}/moments_tab.png',
31 | grayscale=True, confidence=0.8)
32 | break
33 | except Exception as e:
34 | log.LOG.warn("Can't find_moments_tab in resolution: " + resolution)
35 | pass
36 |
37 | if result is None:
38 | raise Exception("Can 't find_moments_tab")
39 |
40 | return result
41 |
42 | @retry(tries=5, delay=2)
43 | def find_search_button(self):
44 |
45 | element = None
46 | for resolution in self.resolutions:
47 | try:
48 | element = pyautogui.locateOnScreen(f'resource/auto_gui/{resolution}/search_button.png',
49 | grayscale=True, confidence=0.8)
50 | break
51 | except Exception as e:
52 | log.LOG.warn("Can't find_moments_tab in resolution: " + resolution)
53 | pass
54 |
55 | if element is None:
56 | raise Exception("Can 't search_button")
57 |
58 | return element
59 |
60 | @retry(tries=5, delay=2)
61 | def find_friends(self):
62 | result = None
63 |
64 | for resolution in self.resolutions:
65 | try:
66 | result = pyautogui.locateCenterOnScreen(f'resource/auto_gui/{resolution}/friends.png',
67 | grayscale=True, confidence=0.8)
68 | break
69 | except Exception as e:
70 | log.LOG.warn("Can't find_friends in resolution: " + resolution)
71 | pass
72 |
73 | if result is None:
74 | raise Exception("Can 't find_friends")
75 |
76 | return result
77 |
78 | @retry(tries=5, delay=2)
79 | def find_complete(self):
80 |
81 | result = None
82 |
83 | for resolution in self.resolutions:
84 | try:
85 | result = pyautogui.locateCenterOnScreen(f'resource/auto_gui/{resolution}/complete.png',
86 | grayscale=True, confidence=0.8)
87 | break
88 | except Exception as e:
89 | log.LOG.warn("Can't find_complete in resolution: " + resolution)
90 | pass
91 |
92 | if result is None:
93 | raise Exception("Can 't find_complete")
94 |
95 | return result
96 |
97 | def run(self) -> None:
98 | self.scrolling = True
99 |
100 | try:
101 | search_hwnd = win32gui.FindWindow('Chrome_WidgetWin_0', '微信')
102 | wechat_hwnd = win32gui.FindWindow('WeChatMainWndForPC', '微信')
103 |
104 | # 先把微信主窗口放置前台
105 | win32gui.SetForegroundWindow(wechat_hwnd)
106 | win32gui.ShowWindow(wechat_hwnd, win32con.SW_SHOWNORMAL)
107 | win32gui.SetWindowPos(wechat_hwnd, None, 100, 100, 0, 0, win32con.SWP_NOSIZE)
108 | time.sleep(0.3)
109 | # 先把搜一搜窗口放前台
110 | win32gui.SetForegroundWindow(search_hwnd)
111 | win32gui.ShowWindow(search_hwnd, win32con.SW_SHOWNORMAL)
112 | win32gui.SetWindowPos(search_hwnd, None, 50, 50, 0, 0, win32con.SWP_NOSIZE)
113 |
114 |
115 | # 点击朋友圈三个字
116 | x, y = self.find_moments_tab()
117 | pyautogui.click(x, y)
118 | time.sleep(0.1)
119 |
120 | # 点击搜索按钮左侧
121 | element = self.find_search_button()
122 | pyautogui.click(element.left - 100, element.top + element.height / 2)
123 | time.sleep(0.25)
124 |
125 | # 输入字符
126 | pyautogui.write('1')
127 | time.sleep(0.25)
128 |
129 | # 搜索
130 | pyautogui.click(element.left + element.width / 2, element.top + element.height / 2)
131 | time.sleep(1.5)
132 |
133 | # 展开朋友
134 | x, y = self.find_friends()
135 | pyautogui.click(x, y)
136 | time.sleep(0.5)
137 |
138 | # 搜索好友
139 | pyperclip.copy(self.friend_name)
140 | time.sleep(0.25)
141 | pyautogui.hotkey('ctrl', 'v')
142 | time.sleep(0.5)
143 |
144 | # 回车
145 | pyautogui.press('enter')
146 | time.sleep(0.5)
147 |
148 | # 点击完成
149 | x, y = self.find_complete()
150 | pyautogui.click(x, y)
151 | time.sleep(0.25)
152 |
153 | # 点击搜索按钮左侧
154 | element = self.find_search_button()
155 | pyautogui.click(element.left - 100, element.top + element.height / 2)
156 | time.sleep(0.25)
157 |
158 | pyautogui.press('backspace')
159 | time.sleep(0.1)
160 | pyautogui.press('backspace')
161 | time.sleep(0.25)
162 | pyperclip.copy('?')
163 | time.sleep(0.25)
164 | pyautogui.hotkey('ctrl', 'v')
165 | time.sleep(0.25)
166 |
167 |
168 |
169 | element = self.find_search_button()
170 | pyautogui.click(element.left + element.width / 2, element.top + element.height / 2)
171 | time.sleep(1.0)
172 |
173 | while self.scrolling:
174 |
175 | element = self.find_search_button()
176 | right_bottom = (element.left + element.width, element.top + element.height + 300)
177 | pyautogui.scroll(-120)
178 | pyautogui.click(right_bottom)
179 | time.sleep(0.2)
180 |
181 | search_hwnd = win32gui.FindWindow('Chrome_WidgetWin_0', '微信')
182 | moments_hwnd = win32gui.FindWindow('SnsWnd', '朋友圈')
183 |
184 | if search_hwnd and moments_hwnd:
185 | # 调整位置朋友圈不要遮挡
186 | width = GetSystemMetrics(0)
187 | win32gui.SetWindowPos(moments_hwnd, None, 50, 50, 0, 0, win32con.SWP_NOSIZE)
188 | win32gui.SetWindowPos(search_hwnd, None, 50, 50, 0, 0, win32con.SWP_NOSIZE)
189 |
190 |
191 | except Exception:
192 | traceback.print_exc()
193 |
194 | def set_scrolling(self, scrolling: bool) -> None:
195 | self.scrolling = scrolling
196 | if not self.scrolling:
197 | self.gui.working_note.pack_forget()
198 | if self.scrolling and self.gui.working_note:
199 | self.gui.working_note.pack()
200 |
--------------------------------------------------------------------------------
/log.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import sys
4 | import time
5 |
6 | filename = time.strftime("%Y-%m-%d", time.localtime(time.time()))
7 |
8 | try:
9 | if not os.path.exists('log'):
10 | os.mkdir('log')
11 | log_file = f'log/{filename}-log.log'
12 | console_file = f'log/{filename}-output.log'
13 | except:
14 | log_file = f'{filename}-log.log'
15 | console_file = f'{filename}-output.log'
16 |
17 | if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
18 | # pyinstaller 输出日志到文件
19 | f = open(console_file, 'a')
20 | sys.stdout = f
21 | sys.stderr = f
22 |
23 | file_handler = logging.FileHandler(log_file, encoding='utf-8')
24 | logging.basicConfig(level='DEBUG', format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
25 | logging.getLogger().addHandler(file_handler)
26 | LOG = logging.getLogger("WechatMoments")
27 |
28 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import threading
2 | import tkinter
3 | import traceback
4 | from time import sleep
5 | from pywxdump import read_info
6 |
7 | def main():
8 | from gui.gui import Gui
9 |
10 | gui = Gui()
11 | gui_thread = threading.Thread(target=gui.run_gui)
12 | gui_thread.start()
13 |
14 | info = ""
15 | while True:
16 | try:
17 | info = read_info(None, is_logging=True)
18 | except:
19 | traceback.print_exc()
20 | # 如果解密失败,读取到报错信息
21 | if isinstance(info, str):
22 | gui.waiting_label.config(text="请启动微信....")
23 | sleep(0.5)
24 | elif isinstance(info, list) and info[0].get("key") == "None":
25 | gui.waiting_label.config(text="请登陆微信....")
26 | sleep(0.5)
27 | else:
28 | break
29 |
30 | gui.wechat_logged_in(info[0])
31 |
32 |
33 | if __name__ == "__main__":
34 | main()
35 |
--------------------------------------------------------------------------------
/main.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 |
3 |
4 | a = Analysis(
5 | ['main.py'],
6 | pathex=[],
7 | binaries=[],
8 | hiddenimports=['babel.numbers'],
9 | hookspath=[],
10 | hooksconfig={},
11 | runtime_hooks=[],
12 | excludes=[],
13 | noarchive=False,
14 | )
15 | pyz = PYZ(a.pure)
16 |
17 | exe = EXE(
18 | pyz,
19 | a.scripts,
20 | [],
21 | exclude_binaries=True,
22 | name='wechat_moments',
23 | debug=False,
24 | bootloader_ignore_signals=False,
25 | strip=False,
26 | upx=True,
27 | console=False,
28 | disable_windowed_traceback=False,
29 | argv_emulation=False,
30 | target_arch=None,
31 | codesign_identity=None,
32 | entitlements_file=None
33 | )
34 | coll = COLLECT(
35 | exe,
36 | a.binaries,
37 | a.datas,
38 | strip=False,
39 | upx=True,
40 | upx_exclude=[],
41 | name='wechat_moments',
42 | )
43 |
44 | # 将资源文件夹拷贝出来
45 | import shutil
46 | shutil.copytree('resource', f'{DISTPATH}/wechat_moments/resource')
47 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pywxdump~=2.4.44
2 | xmltodict~=0.13.0
3 | dataclasses_json
4 | pillow~=10.2.0
5 | tkcalendar~=1.6.1
6 | pywin32
7 | requests~=2.31.0
8 | fileType
9 | pyautogui
10 | opencv-python
11 | retry
--------------------------------------------------------------------------------
/resource/auto_gui/1366/complete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/1366/complete.png
--------------------------------------------------------------------------------
/resource/auto_gui/1366/friends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/1366/friends.png
--------------------------------------------------------------------------------
/resource/auto_gui/1366/moments_tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/1366/moments_tab.png
--------------------------------------------------------------------------------
/resource/auto_gui/1366/search_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/1366/search_button.png
--------------------------------------------------------------------------------
/resource/auto_gui/1600/complete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/1600/complete.png
--------------------------------------------------------------------------------
/resource/auto_gui/1600/friends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/1600/friends.png
--------------------------------------------------------------------------------
/resource/auto_gui/1600/moments_tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/1600/moments_tab.png
--------------------------------------------------------------------------------
/resource/auto_gui/1600/search_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/1600/search_button.png
--------------------------------------------------------------------------------
/resource/auto_gui/1920/complete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/1920/complete.png
--------------------------------------------------------------------------------
/resource/auto_gui/1920/friends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/1920/friends.png
--------------------------------------------------------------------------------
/resource/auto_gui/1920/moments_tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/1920/moments_tab.png
--------------------------------------------------------------------------------
/resource/auto_gui/1920/search_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/1920/search_button.png
--------------------------------------------------------------------------------
/resource/auto_gui/2560_100/complete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/2560_100/complete.png
--------------------------------------------------------------------------------
/resource/auto_gui/2560_100/friends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/2560_100/friends.png
--------------------------------------------------------------------------------
/resource/auto_gui/2560_100/moments_tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/2560_100/moments_tab.png
--------------------------------------------------------------------------------
/resource/auto_gui/2560_100/search_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/2560_100/search_button.png
--------------------------------------------------------------------------------
/resource/auto_gui/2560_125/complete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/2560_125/complete.png
--------------------------------------------------------------------------------
/resource/auto_gui/2560_125/friends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/2560_125/friends.png
--------------------------------------------------------------------------------
/resource/auto_gui/2560_125/moments_tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/2560_125/moments_tab.png
--------------------------------------------------------------------------------
/resource/auto_gui/2560_125/search_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/2560_125/search_button.png
--------------------------------------------------------------------------------
/resource/auto_gui/2560_175/complete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/2560_175/complete.png
--------------------------------------------------------------------------------
/resource/auto_gui/2560_175/friends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/2560_175/friends.png
--------------------------------------------------------------------------------
/resource/auto_gui/2560_175/moments_tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/2560_175/moments_tab.png
--------------------------------------------------------------------------------
/resource/auto_gui/2560_175/search_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/2560_175/search_button.png
--------------------------------------------------------------------------------
/resource/auto_gui/complete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/complete.png
--------------------------------------------------------------------------------
/resource/auto_gui/friends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/friends.png
--------------------------------------------------------------------------------
/resource/auto_gui/moments_tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/moments_tab.png
--------------------------------------------------------------------------------
/resource/auto_gui/search_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/auto_gui/search_button.png
--------------------------------------------------------------------------------
/resource/ffmpeg.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/ffmpeg.exe
--------------------------------------------------------------------------------
/resource/ffprobe.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/ffprobe.exe
--------------------------------------------------------------------------------
/resource/gui_pictures/open_moments_guide -原始.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/gui_pictures/open_moments_guide -原始.png
--------------------------------------------------------------------------------
/resource/gui_pictures/open_moments_guide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/gui_pictures/open_moments_guide.png
--------------------------------------------------------------------------------
/resource/gui_pictures/open_search_guide-原始.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/gui_pictures/open_search_guide-原始.png
--------------------------------------------------------------------------------
/resource/gui_pictures/open_search_guide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tech-shrimp/WechatMoments/958c33900d87dccfb9b6d8d57ea8765fe4da0522/resource/gui_pictures/open_search_guide.png
--------------------------------------------------------------------------------
/resource/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | X
40 |
41 | 视频号请到微信搜索观看
42 |
43 |
44 |
45 |
46 |
47 |
48 |
53 |
54 |
55 |
56 |
57 |
{my_name}
58 |
59 |
60 |
61 |
62 |
63 |
64 | /*内容分割线*/
65 |
已显示全部内容
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/resource/template/css/index.css:
--------------------------------------------------------------------------------
1 | *{margin: 0;padding: 0;}
2 |
3 |
4 | @media screen and (max-width:414px ) {
5 | html{font-size:8px ;}
6 | }
7 | @media screen and (min-width:415px ) and (max-width:878px) {
8 | html{font-size:16px ;}
9 | }
10 | @media screen and (min-width:879px ){
11 | html{font-size:24px ;}
12 | }
13 |
14 | P{margin-bottom:0.4rem;}
15 |
16 | .header{
17 | max-width: 800px;
18 | margin: 0 auto;
19 | width:100% ;
20 | background-color: #EDEDED;
21 | color:black;
22 | overflow: hidden;
23 | font-size:1rem ;
24 | padding:0.2rem 0;
25 | position:fixed;
26 | z-index:100;
27 | }
28 |
29 | body {
30 | max-width: 800px;
31 | margin: 0 auto;
32 | }
33 |
34 |
35 | .cover_container .cover_img{
36 | width:100%;
37 | height:25rem;
38 | object-fit:cover
39 | }
40 |
41 | .cover_container{
42 | position: relative;
43 | padding-top:0.2rem;
44 | }
45 |
46 | .avatar{height: 3.2rem;position: absolute ;bottom: -1rem;right: 0.8rem;}
47 | .avatar h4{float:left ;color:#FFFFFF;margin-top:1rem ;font-size:1rem ;margin-right:1rem ; text-shadow:1px 1px 0 #000 ;}
48 | .usr_img_box{width:3rem ;height:3rem ;overflow: hidden;border-radius:0.3rem;}
49 | .user_logo{width:100%;height: 100%;}
50 |
51 | .text_box{width:100% ;overflow: hidden;}
52 | .logo01_box{width:2.5rem ;height:2.5rem ;overflow: hidden;border-radius:0.3rem;}
53 | .logo01_box img{width:100% ;height: 100%;}
54 | .item{border-bottom:1px solid whitesmoke ;margin-top:1rem ;}
55 | .p1{font-size:0.9rem ;color:#2e4f7a ;font-weight: bold;}
56 | .p2{font-size:0.8rem ;color:#000000 ;width:95% ;}
57 | .logo01_box{margin-left:0.5rem ;}
58 | .xs8{padding-left:0.8rem ;}
59 |
60 | .out_link {background-color:#F7F7F7 ;font-size:0.7rem ;width:95% ;color:#41454D ;overflow: hidden; cursor:pointer;}
61 | .out_link img{width:2.5rem ;height:2.5rem ;margin:0.5rem 0.5rem ;float: left;}
62 |
63 | .music_link {background-color:#F7F7F7 ;font-size:0.7rem ;width:95% ;color:#41454D ;overflow: hidden; cursor:pointer; display: flex; flex-direction:column}
64 | .music_des {width:100%;display: flex;}
65 | .music_title_musician {width:100%;margin-left: 1.5rem;display: flex;flex-direction:column}
66 | .music_title {font-size:1.0rem;margin-top: 0.5rem;}
67 | .music_musician {font-size:0.85rem;margin-top: 0.5rem;}
68 | .music_link img{width:4rem ;height:4rem ;margin:0.2rem 0.2rem ;float: left;}
69 | .music_audio {height: 1.5rem; width: 95%;}
70 |
71 |
72 | .text{float: left;margin-top: 1.1rem;width:80% ;}
73 | .pl{margin-top:0.5rem ;font-size:0.6rem ;color:#80858c ;clear: both;}
74 | .pl span{display:inline-block ;width:2rem ;}
75 |
76 | .pls{width:100% ;overflow:hidden ;color: #41454D;font-size:0.8rem ;}
77 | .pls span{color:#5BAFFF ;}
78 | .pls p{width:95% ;}
79 |
80 | .text_02{margin-top: 0.9rem;}
81 | .dele{display: inline-block;margin-left:2rem ;width:1rem ;height: auto;cursor:pointer ;}
82 | .up .down{color:#ACB1B7;font-size:0.6rem ;cursor:pointer ;}
83 | .pls img{width:1rem ;height:auto ;}
84 | .end{text-align:center ;margin:0.6rem 0 1rem 0 ;font-size:0.9rem ;color:#ACB1B7 ;}
85 |
86 |
87 | #fullSizeOverlay {
88 | display: none;
89 | position: fixed;
90 | top: 0;
91 | left: 0;
92 | width: 100%;
93 | height: 100%;
94 | background-color: rgba(0, 0, 0, 0.8);
95 | z-index: 9999;
96 | text-align: center;
97 | }
98 |
99 | #fullSizeImage {
100 | max-width: 85%;
101 | max-height: 85%;
102 | margin-top: 3%;
103 | }
104 |
105 | .time{font-size:0.6rem ;color:#ACB1B7 ;margin-top:0.5rem ;}
106 | .location{font-size:0.6rem ;color:#2e4f7a;margin-top:0.5rem ;}
107 |
108 | .emoji_img {
109 | max-width: 1.0rem;
110 | max-height: 1.0rem;
111 | padding-bottom: 0.15rem;
112 | }
113 |
114 | .alert {
115 | position: relative;
116 | top: 10;
117 | left: 0;
118 | width: auto;
119 | height: auto;
120 | padding: 10px;
121 | margin: 10px;
122 | line-height: 1.8;
123 | border-radius: 5px;
124 | cursor: hand;
125 | cursor: pointer;
126 | font-family: sans-serif;
127 | font-weight: 400;
128 | }
129 |
130 | .alertCheckbox {
131 | display: none;
132 | }
133 |
134 | :checked + .alert {
135 | display: none;
136 | }
137 |
138 | .alertText {
139 | display: table;
140 | margin: 0 auto;
141 | text-align: center;
142 | font-size: 16px;
143 | }
144 |
145 | .alertClose {
146 | float: right;
147 | padding-left: 10px;
148 | font-size: 16px;
149 | margin-bottom:15px;
150 | }
151 |
152 |
153 | .clear {
154 | clear: both;
155 | }
156 |
157 | .info {
158 | background-color: #EEE;
159 | border: 1px solid #DDD;
160 | color: #999;
161 | }
162 |
163 | #warningOverlay {
164 | display: none;
165 | position: fixed;
166 | top: 70%;
167 | left: 30%;
168 | width: 40%;
169 | z-index: 9999;
170 | text-align: center;
171 | }
--------------------------------------------------------------------------------
/resource/template/icons/comment-downarrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | enter-arrow
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/resource/template/icons/comment-uparrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | enter-arrow
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/resource/template/icons/massage.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | massage
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/resource/template/js/bootstrap.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.3.2 (http://getbootstrap.com)
3 | * Copyright 2011-2015 Twitter, Inc.
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.2",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.2",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.2",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a(this.options.trigger).filter('[href="#'+b.id+'"], [data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.2",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0,trigger:'[data-toggle="collapse"]'},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":a.extend({},e.data(),{trigger:this});c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.2",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('
').insertAfter(a(this)).on("click",b);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger("shown.bs.dropdown",h)}return!1}},g.prototype.keydown=function(b){if(/(38|40|27|32)/.test(b.which)&&!/input|textarea/i.test(b.target.tagName)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var e=c(d),g=e.hasClass("open");if(!g&&27!=b.which||g&&27==b.which)return 27==b.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.divider):visible a",i=e.find('[role="menu"]'+h+', [role="listbox"]'+h);if(i.length){var j=i.index(b.target);38==b.which&&j>0&&j--,40==b.which&&j ').prependTo(this.$element).on("click.dismiss.bs.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),f&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;f?this.$backdrop.one("bsTransitionEnd",b).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):b()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var g=function(){d.removeBackdrop(),b&&b()};a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",g).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):g()}else b&&b()},c.prototype.handleUpdate=function(){this.options.backdrop&&this.adjustBackdrop(),this.adjustDialog()},c.prototype.adjustBackdrop=function(){this.$backdrop.css("height",0).css("height",this.$element[0].scrollHeight)},c.prototype.adjustDialog=function(){var a=this.$element[0].scrollHeight>document.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){this.bodyIsOverflowing=document.body.scrollHeight>document.documentElement.clientHeight,this.scrollbarWidth=this.measureScrollbar()},c.prototype.setScrollbar=function(){var a=parseInt(this.$body.css("padding-right")||0,10);this.bodyIsOverflowing&&this.$body.css("padding-right",a+this.scrollbarWidth)},c.prototype.resetScrollbar=function(){this.$body.css("padding-right","")},c.prototype.measureScrollbar=function(){var a=document.createElement("div");a.className="modal-scrollbar-measure",this.$body.append(a);var b=a.offsetWidth-a.clientWidth;return this.$body[0].removeChild(a),b};var d=a.fn.modal;a.fn.modal=b,a.fn.modal.Constructor=c,a.fn.modal.noConflict=function(){return a.fn.modal=d,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(c){var d=a(this),e=d.attr("href"),f=a(d.attr("data-target")||e&&e.replace(/.*(?=#[^\s]+$)/,"")),g=f.data("bs.modal")?"toggle":a.extend({remote:!/#/.test(e)&&e},f.data(),d.data());d.is("a")&&c.preventDefault(),f.one("show.bs.modal",function(a){a.isDefaultPrevented()||f.one("hidden.bs.modal",function(){d.is(":visible")&&d.trigger("focus")})}),b.call(f,g,this)})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof b&&b;(e||"destroy"!=b)&&(e||d.data("bs.tooltip",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};c.VERSION="3.3.2",c.TRANSITION_DURATION=150,c.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(this.options.viewport.selector||this.options.viewport);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c&&c.$tip&&c.$tip.is(":visible")?void(c.hoverState="in"):(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.options.container?a(this.options.container):this.$element.parent(),p=this.getPosition(o);h="bottom"==h&&k.bottom+m>p.bottom?"top":"top"==h&&k.top-mp.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.width&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type)})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||"destroy"!=b)&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.2",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},c.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){var e=a.proxy(this.process,this);this.$body=a("body"),this.$scrollElement=a(a(c).is("body")?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",e),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.2",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b="offset",c=0;a.isWindow(this.$scrollElement[0])||(b="position",c=this.$scrollElement.scrollTop()),this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight();var d=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[b]().top+c,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){d.offsets.push(this[0]),d.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,this.clear();var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")},b.prototype.clear=function(){a(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var d=a.fn.scrollspy;a.fn.scrollspy=c,a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=d,this},a(window).on("load.bs.scrollspy.data-api",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);c.call(b,b.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new c(this)),"string"==typeof b&&e[b]()})}var c=function(b){this.element=a(b)};c.VERSION="3.3.2",c.TRANSITION_DURATION=150,c.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a"),f=a.Event("hide.bs.tab",{relatedTarget:b[0]}),g=a.Event("show.bs.tab",{relatedTarget:e[0]});if(e.trigger(f),b.trigger(g),!g.isDefaultPrevented()&&!f.isDefaultPrevented()){var h=a(d);this.activate(b.closest("li"),c),this.activate(h,h.parent(),function(){e.trigger({type:"hidden.bs.tab",relatedTarget:b[0]}),b.trigger({type:"shown.bs.tab",relatedTarget:e[0]})})}}},c.prototype.activate=function(b,d,e){function f(){g.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()
7 | }var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=this.unpin=this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.2",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=a("body").height();"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery);
--------------------------------------------------------------------------------
/resource/template/js/index.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 |
3 |
4 | })
5 |
6 |
7 |
8 | function replaceEmoji(text) {
9 | // 定义替换规则,可以根据需要添加更多规则
10 | var replacementRules = [
11 | {
12 | pattern: /\[微笑\]/g,
13 | replacement: ' '
14 | },
15 | {
16 | pattern: /\[发呆\]/g,
17 | replacement: ' '
18 | },
19 | {
20 | pattern: /\[撇嘴\]/g,
21 | replacement: ' '
22 | },
23 | {
24 | pattern: /\[色\]/g,
25 | replacement: ' '
26 | },
27 | {
28 | pattern: /\[发呆\]/g,
29 | replacement: ' '
30 | },
31 | {
32 | pattern: /\[得意\]/g,
33 | replacement: ' '
34 | },
35 | {
36 | pattern: /\[流泪\]/g,
37 | replacement: ' '
38 | },
39 | {
40 | pattern: /\[害羞\]/g,
41 | replacement: ' '
42 | },
43 | {
44 | pattern: /\[闭嘴\]/g,
45 | replacement: ' '
46 | },
47 | {
48 | pattern: /\[睡\]/g,
49 | replacement: ' '
50 | },
51 | {
52 | pattern: /\[大哭\]/g,
53 | replacement: ' '
54 | },
55 | {
56 | pattern: /\[尴尬\]/g,
57 | replacement: ' '
58 | },
59 | {
60 | pattern: /\[发怒\]/g,
61 | replacement: ' '
62 | },
63 | {
64 | pattern: /\[调皮\]/g,
65 | replacement: ' '
66 | },
67 | {
68 | pattern: /\[呲牙\]/g,
69 | replacement: ' '
70 | },
71 | {
72 | pattern: /\[惊讶\]/g,
73 | replacement: ' '
74 | },
75 | {
76 | pattern: /\[难过\]/g,
77 | replacement: ' '
78 | },
79 | {
80 | pattern: /\[抓狂\]/g,
81 | replacement: ' '
82 | },
83 | {
84 | pattern: /\[吐\]/g,
85 | replacement: ' '
86 | },
87 | {
88 | pattern: /\[偷笑\]/g,
89 | replacement: ' '
90 | },
91 | {
92 | pattern: /\[愉快\]/g,
93 | replacement: ' '
94 | },
95 | {
96 | pattern: /\[白眼\]/g,
97 | replacement: ' '
98 | },
99 | {
100 | pattern: /\[傲慢\]/g,
101 | replacement: ' '
102 | },
103 | {
104 | pattern: /\[困\]/g,
105 | replacement: ' '
106 | },
107 | {
108 | pattern: /\[惊恐\]/g,
109 | replacement: ' '
110 | },
111 | {
112 | pattern: /\[憨笑\]/g,
113 | replacement: ' '
114 | },
115 | {
116 | pattern: /\[悠闲\]/g,
117 | replacement: ' '
118 | },
119 | {
120 | pattern: /\[咒骂\]/g,
121 | replacement: ' '
122 | },
123 | {
124 | pattern: /\[疑问\]/g,
125 | replacement: ' '
126 | },
127 | {
128 | pattern: /\[嘘\]/g,
129 | replacement: ' '
130 | },
131 | {
132 | pattern: /\[晕\]/g,
133 | replacement: ' '
134 | },
135 | {
136 | pattern: /\[衰\]/g,
137 | replacement: ' '
138 | },
139 | {
140 | pattern: /\[骷髅\]/g,
141 | replacement: ' '
142 | },
143 | {
144 | pattern: /\[敲打\]/g,
145 | replacement: ' '
146 | },
147 | {
148 | pattern: /\[再见\]/g,
149 | replacement: ' '
150 | },
151 | {
152 | pattern: /\[擦汗\]/g,
153 | replacement: ' '
154 | },
155 | {
156 | pattern: /\[抠鼻\]/g,
157 | replacement: ' '
158 | },
159 | {
160 | pattern: /\[鼓掌\]/g,
161 | replacement: ' '
162 | },
163 | {
164 | pattern: /\[坏笑\]/g,
165 | replacement: ' '
166 | },
167 | {
168 | pattern: /\[右哼哼\]/g,
169 | replacement: ' '
170 | },
171 | {
172 | pattern: /\[鄙视\]/g,
173 | replacement: ' '
174 | },
175 | {
176 | pattern: /\[委屈\]/g,
177 | replacement: ' '
178 | },
179 | {
180 | pattern: /\[快哭了\]/g,
181 | replacement: ' '
182 | },
183 | {
184 | pattern: /\[阴险\]/g,
185 | replacement: ' '
186 | },
187 | {
188 | pattern: /\[亲亲\]/g,
189 | replacement: ' '
190 | },
191 | {
192 | pattern: /\[可怜\]/g,
193 | replacement: ' '
194 | },
195 | {
196 | pattern: /\[Whimper\]/g,
197 | replacement: ' '
198 | },
199 | {
200 | pattern: /\[笑脸\]/g,
201 | replacement: ' '
202 | },
203 | {
204 | pattern: /\[生病\]/g,
205 | replacement: ' '
206 | },
207 | {
208 | pattern: /\[脸红\]/g,
209 | replacement: ' '
210 | },
211 | {
212 | pattern: /\[破涕为笑\]/g,
213 | replacement: ' '
214 | },
215 | {
216 | pattern: /\[恐惧\]/g,
217 | replacement: ' '
218 | },
219 | {
220 | pattern: /\[失望\]/g,
221 | replacement: ' '
222 | },
223 | {
224 | pattern: /\[无语\]/g,
225 | replacement: ' '
226 | },
227 | {
228 | pattern: /\[嘿哈\]/g,
229 | replacement: ' '
230 | },
231 | {
232 | pattern: /\[捂脸\]/g,
233 | replacement: ' '
234 | },
235 | {
236 | pattern: /\[奸笑\]/g,
237 | replacement: ' '
238 | },
239 | {
240 | pattern: /\[机智\]/g,
241 | replacement: ' '
242 | },
243 | {
244 | pattern: /\[皱眉\]/g,
245 | replacement: ' '
246 | },
247 | {
248 | pattern: /\[耶\]/g,
249 | replacement: ' '
250 | },
251 | {
252 | pattern: /\[吃瓜\]/g,
253 | replacement: ' '
254 | },
255 | {
256 | pattern: /\[加油\]/g,
257 | replacement: ' '
258 | },
259 | {
260 | pattern: /\[汗\]/g,
261 | replacement: ' '
262 | },
263 | {
264 | pattern: /\[天啊\]/g,
265 | replacement: ' '
266 | },
267 | {
268 | pattern: /\[Emm\]/g,
269 | replacement: ' '
270 | },
271 | {
272 | pattern: /\[社会社会\]/g,
273 | replacement: ' '
274 | },
275 | {
276 | pattern: /\[旺柴\]/g,
277 | replacement: ' '
278 | },
279 | {
280 | pattern: /\[好的\]/g,
281 | replacement: ' '
282 | },
283 | {
284 | pattern: /\[打脸\]/g,
285 | replacement: ' '
286 | },
287 | {
288 | pattern: /\[哇\]/g,
289 | replacement: ' '
290 | },
291 | {
292 | pattern: /\[翻白眼\]/g,
293 | replacement: ' '
294 | },
295 | {
296 | pattern: /\[666\]/g,
297 | replacement: ' '
298 | },
299 | {
300 | pattern: /\[让我看看\]/g,
301 | replacement: ' '
302 | },
303 | {
304 | pattern: /\[叹气\]/g,
305 | replacement: ' '
306 | },
307 | {
308 | pattern: /\[苦涩\]/g,
309 | replacement: ' '
310 | },
311 | {
312 | pattern: /\[難受\]/g,
313 | replacement: ' '
314 | },
315 | {
316 | pattern: /\[裂开\]/g,
317 | replacement: ' '
318 | },
319 | {
320 | pattern: /\[嘴唇\]/g,
321 | replacement: ' '
322 | },
323 | {
324 | pattern: /\[爱心\]/g,
325 | replacement: ' '
326 | },
327 | {
328 | pattern: /\[心碎\]/g,
329 | replacement: ' '
330 | },
331 | {
332 | pattern: /\[拥抱\]/g,
333 | replacement: ' '
334 | },
335 | {
336 | pattern: /\[强\]/g,
337 | replacement: ' '
338 | },
339 | {
340 | pattern: /\[弱\]/g,
341 | replacement: ' '
342 | },
343 | {
344 | pattern: /\[握手\]/g,
345 | replacement: ' '
346 | },
347 | {
348 | pattern: /\[胜利\]/g,
349 | replacement: ' '
350 | },
351 | {
352 | pattern: /\[抱拳\]/g,
353 | replacement: ' '
354 | },
355 | {
356 | pattern: /\[勾引\]/g,
357 | replacement: ' '
358 | },
359 | {
360 | pattern: /\[拳头\]/g,
361 | replacement: ' '
362 | },
363 | {
364 | pattern: /\[OK\]/g,
365 | replacement: ' '
366 | },
367 | {
368 | pattern: /\[合十\]/g,
369 | replacement: ' '
370 | },
371 | {
372 | pattern: /\[啤酒\]/g,
373 | replacement: ' '
374 | },
375 | {
376 | pattern: /\[咖啡]\]/g,
377 | replacement: ' '
378 | },
379 | {
380 | pattern: /\[蛋糕\]/g,
381 | replacement: ' '
382 | },
383 | {
384 | pattern: /\[玫瑰\]/g,
385 | replacement: ' '
386 | },
387 | {
388 | pattern: /\[凋谢\]/g,
389 | replacement: ' '
390 | },
391 | {
392 | pattern: /\[菜刀\]/g,
393 | replacement: ' '
394 | },
395 | {
396 | pattern: /\[炸弹\]/g,
397 | replacement: ' '
398 | },
399 | {
400 | pattern: /\[便便\]/g,
401 | replacement: ' '
402 | },
403 | {
404 | pattern: /\[月亮\]/g,
405 | replacement: ' '
406 | },
407 | {
408 | pattern: /\[太阳\]/g,
409 | replacement: ' '
410 | },
411 | {
412 | pattern: /\[庆 祝\]/g,
413 | replacement: ' '
414 | },
415 | {
416 | pattern: /\[礼物\]/g,
417 | replacement: ' '
418 | },
419 | {
420 | pattern: /\[红包\]/g,
421 | replacement: ' '
422 | },
423 | {
424 | pattern: /\[發\]/g,
425 | replacement: ' '
426 | },
427 | {
428 | pattern: /\[福\]/g,
429 | replacement: ' '
430 | },
431 | {
432 | pattern: /\[烟花\]/g,
433 | replacement: ' '
434 | },
435 | {
436 | pattern: /\[爆竹\]/g,
437 | replacement: ' '
438 | },
439 | {
440 | pattern: /\[猪头\]/g,
441 | replacement: ' '
442 | },
443 | {
444 | pattern: /\[跳跳\]/g,
445 | replacement: ' '
446 | },
447 | {
448 | pattern: /\[发抖\]/g,
449 | replacement: ' '
450 | },
451 | {
452 | pattern: /\[转圈\]/g,
453 | replacement: ' '
454 | }
455 | ];
456 |
457 | // 循环遍历替换规则
458 | for (var i = 0; i < replacementRules.length; i++) {
459 | var rule = replacementRules[i];
460 | text = text.replace(rule.pattern, rule.replacement);
461 | }
462 | return text;
463 | }
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from decrypter.image_decrypt import ImageDecrypter
4 | from decrypter.video_decrypt import VideoDecrypter
5 | import threading
6 | from time import sleep
7 | from pywxdump import read_info
8 | from gui.gui import Gui
9 |
10 |
11 | def stage_3():
12 | gui = Gui()
13 | gui_thread = threading.Thread(target=gui.run_gui)
14 | gui_thread.start()
15 | gui.init_export_page()
16 |
17 | gui.begin_calendar.set_date(datetime.date(2024, 5, 6))
18 | gui.end_calendar.set_date(datetime.date(2024, 5, 6))
19 |
20 | # 后台读取微信信息
21 | # 请等待完全接入微信再进行UI操作
22 | while True:
23 | sleep(0.5)
24 | result = read_info(None, is_logging=True)
25 | # 如果解密失败,读取到报错信息
26 | if isinstance(result, str):
27 | gui.waiting_label.config(text="请启动微信....")
28 | pass
29 | elif isinstance(result, list) and result[0].get("key") == "None":
30 | gui.waiting_label.config(text="请登陆微信....")
31 | else:
32 | gui.account_info = result[0]
33 | gui.waiting_label.config(text="微信已登录")
34 | # 初始化视频导出器
35 | gui.video_decrypter = VideoDecrypter(gui, gui.account_info.get("filePath"))
36 | gui.image_decrypter = ImageDecrypter(gui, gui.account_info.get("filePath"))
37 | gui.waiting_label.place_forget()
38 | break
39 |
40 | if __name__ == "__main__":
41 | stage_3()
42 |
--------------------------------------------------------------------------------