chat-export 0.9.2__tar.gz → 0.9.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {chat_export-0.9.2/chat_export.egg-info → chat_export-0.9.4}/PKG-INFO +71 -26
- {chat_export-0.9.2 → chat_export-0.9.4}/README.md +252 -207
- {chat_export-0.9.2 → chat_export-0.9.4/chat_export.egg-info}/PKG-INFO +71 -26
- {chat_export-0.9.2 → chat_export-0.9.4}/main.py +243 -27
- {chat_export-0.9.2 → chat_export-0.9.4}/pyproject.toml +1 -1
- {chat_export-0.9.2 → chat_export-0.9.4}/LICENSE +0 -0
- {chat_export-0.9.2 → chat_export-0.9.4}/MANIFEST.in +0 -0
- {chat_export-0.9.2 → chat_export-0.9.4}/chat_export.egg-info/SOURCES.txt +0 -0
- {chat_export-0.9.2 → chat_export-0.9.4}/chat_export.egg-info/dependency_links.txt +0 -0
- {chat_export-0.9.2 → chat_export-0.9.4}/chat_export.egg-info/entry_points.txt +0 -0
- {chat_export-0.9.2 → chat_export-0.9.4}/chat_export.egg-info/requires.txt +0 -0
- {chat_export-0.9.2 → chat_export-0.9.4}/chat_export.egg-info/top_level.txt +0 -0
- {chat_export-0.9.2 → chat_export-0.9.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chat-export
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: Convert WhatsApp chat exports to HTML format
|
|
5
5
|
Author-email: matlon GmbH <software@matlon.ch>
|
|
6
6
|
License: MIT License
|
|
@@ -60,7 +60,7 @@ Dynamic: license-file
|
|
|
60
60
|
This tool converts a WhatsApp chat export into two HTML formats: one with inline media (such as images, videos, and audio files) and a compact version with media links.
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
Video Tutorial (still refers to the previously used tool name instead of `chat-export
|
|
63
|
+
Video Tutorial (still refers to the previously used tool name instead of `chat-export`, doesn't cover new features like the non-interactive CLI mode yet):
|
|
64
64
|
[](https://www.youtube.com/watch?v=s1dMO8pjkC8)"
|
|
65
65
|
|
|
66
66
|
## Binary Release Downloads
|
|
@@ -101,7 +101,28 @@ You can for example save it on Google Drive or Dropbox or send it to yourself wi
|
|
|
101
101
|
|
|
102
102
|
3. **Install and Run the Tool:**
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
### Option 1: Download Binary Executables: Easy for non-technical users
|
|
105
|
+
|
|
106
|
+
If you don't know what Python is and just want to use the tool without having to deal with technical details, you can download the binary executables.
|
|
107
|
+
|
|
108
|
+
- For Windows, download [chat-export.exe](https://github.com/mtln/chat-export/releases/latest/download/chat-export.exe).
|
|
109
|
+
|
|
110
|
+
Because the .exe is unsigned, you will see a warning, but after clicking on "More Information" you should have the option to run it anyway. An EV code signing certificate for Windows costs a lot. If I get enough donations, I will buy one, so I can release signed exe files.
|
|
111
|
+
|
|
112
|
+
- For Mac, download [chat-export-installer.pkg](https://github.com/mtln/chat-export/releases/latest/download/chat-export-installer.pkg)
|
|
113
|
+
|
|
114
|
+
Once installed on Mac:
|
|
115
|
+
|
|
116
|
+
1. Open the **Terminal** app (e.g. by typing "terminal" in Spotlight Search).
|
|
117
|
+
2. Type: `chat-export`
|
|
118
|
+
|
|
119
|
+
This will start the chat export tool from anywhere in the terminal.
|
|
120
|
+
|
|
121
|
+
To uninstall `chat-export` from Mac, open the Terminal and run: `sudo rm /usr/local/bin/chat-export`
|
|
122
|
+
|
|
123
|
+
The executables have been built with pyinstaller.
|
|
124
|
+
|
|
125
|
+
### Option 2: Install via pip
|
|
105
126
|
|
|
106
127
|
If you have Python installed, you can install chat-export as a command-line tool:
|
|
107
128
|
```
|
|
@@ -122,43 +143,41 @@ You can for example save it on Google Drive or Dropbox or send it to yourself wi
|
|
|
122
143
|
pip install chat-export[windows]
|
|
123
144
|
```
|
|
124
145
|
|
|
125
|
-
|
|
126
|
-
|
|
146
|
+
|
|
127
147
|
*Note: Without these optional dependencies, the tool will fall back to command-line input for file selection or use tkinter (if available) for file dialogs. If you're planning to use the tool in non-interactive mode, you don't need to install these dependencies.*
|
|
128
148
|
|
|
129
149
|
After installation, you can run it from anywhere in your terminal:
|
|
130
|
-
|
|
150
|
+
|
|
131
151
|
```
|
|
132
152
|
chat-export
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
or
|
|
156
|
+
```
|
|
133
157
|
chat-export --help
|
|
134
158
|
```
|
|
135
|
-
|
|
159
|
+
|
|
136
160
|
To uninstall:
|
|
137
161
|
```
|
|
138
162
|
pip uninstall chat-export
|
|
139
163
|
```
|
|
140
164
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
- For Windows, download [chat-export.exe](https://github.com/mtln/chat-export/releases/latest/download/chat-export.exe).
|
|
144
|
-
|
|
145
|
-
Because the .exe is unsigned, you will see a warning, but after clicking on "More Information" you should have the option to run it anyway. An EV code signing certificate for Windows costs a lot. If I get enough donations, I will buy one, so I can release signed exe files.
|
|
146
|
-
|
|
147
|
-
- For Mac, download [chat-export-installer.pkg](https://github.com/mtln/chat-export/releases/latest/download/chat-export-installer.pkg)
|
|
148
|
-
|
|
149
|
-
Once installed on Mac:
|
|
150
|
-
1. Open the **Terminal** app (e.g. by typing "terminal" in Spotlight Search).
|
|
151
|
-
2. Type: `chat-export`
|
|
152
|
-
|
|
153
|
-
This will start the chat export tool from anywhere in the terminal.
|
|
165
|
+
#### Using uv tool instead of pip
|
|
166
|
+
If you have uv installed, you can use `uv tool install` instead of `pip install`. This is perfect for tools like chat-export because it will install the dependencies in a virtual environment and not in the global environment - while still making the tool available globally in your shell / path.
|
|
154
167
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
|
|
169
|
+
`uv tool install chat-export[windows]`
|
|
170
|
+
or
|
|
171
|
+
`uv tool install chat-export[macos]`
|
|
172
|
+
or just
|
|
173
|
+
`uv tool install chat-export`
|
|
174
|
+
uninstall with:
|
|
175
|
+
`uv tool uninstall chat-export`
|
|
176
|
+
|
|
158
177
|
|
|
159
|
-
|
|
178
|
+
###Option 3: Run Directly (No Installation)
|
|
160
179
|
|
|
161
|
-
If Python is installed on your Windows, Mac or Linux computer, run the tool directly (no installation required) with the following command:
|
|
180
|
+
If Python is installed on your Windows, Mac or Linux computer, run the tool directly (no pip installation required, no venv required, no additional dependencies required because it's just vanilla standard lib Python in a single scriptfile, always run the latest up to date version directly from GitHub) with the following command:
|
|
162
181
|
```
|
|
163
182
|
python -c "import urllib.request; exec(urllib.request.urlopen('https://raw.githubusercontent.com/mtln/chat-export/refs/heads/main/main.py').read().decode())"
|
|
164
183
|
```
|
|
@@ -167,11 +186,14 @@ You can for example save it on Google Drive or Dropbox or send it to yourself wi
|
|
|
167
186
|
python3 -c "import urllib.request; exec(urllib.request.urlopen('https://raw.githubusercontent.com/mtln/chat-export/refs/heads/main/main.py').read().decode())"
|
|
168
187
|
```
|
|
169
188
|
|
|
170
|
-
|
|
189
|
+
## Usage
|
|
171
190
|
|
|
172
191
|
The tool supports both interactive and non-interactive modes:
|
|
173
192
|
|
|
174
193
|
### Interactive Mode (Default)
|
|
194
|
+
|
|
195
|
+
Just run `chat-export` in your terminal.
|
|
196
|
+
|
|
175
197
|
**CLI Parameters:**
|
|
176
198
|
|
|
177
199
|
`-o, --output-dir`: Base directory where the chat folder will be created (optional, default: current directory)
|
|
@@ -186,6 +208,22 @@ The tool supports both interactive and non-interactive modes:
|
|
|
186
208
|
|
|
187
209
|
For automation, scripting, or when you know all parameters in advance, you can use the non-interactive CLI mode. This mode processes the chat without any prompts and is perfect for batch processing or integration into other tools.
|
|
188
210
|
|
|
211
|
+
#### Media Embedding Options
|
|
212
|
+
|
|
213
|
+
The tool offers two approaches for handling media files:
|
|
214
|
+
|
|
215
|
+
**Default Behavior (External Media Files):**
|
|
216
|
+
- Creates a folder structure with HTML files and a separate `media/` folder
|
|
217
|
+
- Media files are extracted and linked to from the HTML
|
|
218
|
+
- Results in multiple files that need to be kept together
|
|
219
|
+
- Smaller HTML files, but requires managing multiple files
|
|
220
|
+
|
|
221
|
+
**Embedded Media (`--embed-media` option):**
|
|
222
|
+
- Creates a single self-contained HTML file with all media embedded as base64
|
|
223
|
+
- No external media files are created or needed
|
|
224
|
+
- Perfect for sharing, archiving, or when you want everything in one file
|
|
225
|
+
- Larger HTML file size, but completely portable
|
|
226
|
+
|
|
189
227
|
**Basic Usage:**
|
|
190
228
|
```
|
|
191
229
|
chat-export -n -z "path/to/chat.zip" -p "Your Name"
|
|
@@ -199,6 +237,7 @@ chat-export -n -z "path/to/chat.zip" -p "Your Name"
|
|
|
199
237
|
- `--from-date`: Optional start date for filtering (formats: DD.MM.YYYY, MM/DD/YYYY, DD.MM.YY, MM/DD/YY)
|
|
200
238
|
- `--until-date`: Optional end date for filtering
|
|
201
239
|
- `-o, --output-dir`: Base directory where the chat folder will be created (default: current directory)
|
|
240
|
+
- `--embed-media`: Embed media files as base64 in HTML instead of linking to external files (optional)
|
|
202
241
|
|
|
203
242
|
|
|
204
243
|
**Examples:**
|
|
@@ -227,9 +266,15 @@ Windows paths (important: no trailing backslash):
|
|
|
227
266
|
chat-export -n -z "chat.zip" -p "Your Name" -o "C:\temp"
|
|
228
267
|
```
|
|
229
268
|
|
|
269
|
+
With embedded media (creates a single self-contained HTML file):
|
|
270
|
+
```
|
|
271
|
+
chat-export -n -z "chat.zip" -p "Your Name" --embed-media
|
|
272
|
+
```
|
|
273
|
+
|
|
230
274
|
**Important Notes:**
|
|
231
275
|
- The participant name must match exactly as it appears in the chat (case-sensitive)
|
|
232
276
|
- If the participant name is not found, the tool will display all available participants and exit
|
|
277
|
+
- When using `--embed-media`, media files are encoded as base64 and embedded directly in the HTML, creating a single self-contained file that doesn't require external media files
|
|
233
278
|
|
|
234
279
|
|
|
235
280
|
* When printing an HTML page, most web browsers are set by default to exclude background colors to save ink or toner. If you want to include them, you need to enable background graphics in your browser settings. See the section below for instructions.
|
|
@@ -1,207 +1,252 @@
|
|
|
1
|
-
# chat-export: Convert WhatsApp Chats to HTML
|
|
2
|
-
|
|
3
|
-
This tool converts a WhatsApp chat export into two HTML formats: one with inline media (such as images, videos, and audio files) and a compact version with media links.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Video Tutorial (still refers to the previously used tool name instead of `chat-export
|
|
7
|
-
[](https://www.youtube.com/watch?v=s1dMO8pjkC8)"
|
|
8
|
-
|
|
9
|
-
## Binary Release Downloads
|
|
10
|
-
* Windows: [chat-export.exe](https://github.com/mtln/chat-export/releases/latest/download/chat-export.exe)
|
|
11
|
-
* Mac: [chat-export-installer.pkg](https://github.com/mtln/chat-export/releases/latest/download/chat-export-installer.pkg)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## Why is this useful?
|
|
15
|
-
The HTML export is:
|
|
16
|
-
|
|
17
|
-
* **Printable:** You can print the chat or save as a PDF.
|
|
18
|
-
* **Searchable:** You can search for specific messages in the browser.
|
|
19
|
-
* **Shareable:** You can share the chat with others, but make sure to get consent from all other participants first.
|
|
20
|
-
* **Durable:** You can keep the chat as a record for years to come. Apps come and go. Plain HTML is here to stay.
|
|
21
|
-
* **Automatable:** With the non-interactive CLI mode, you can script batch processing of multiple chat exports or integrate the tool into other workflows.
|
|
22
|
-
|
|
23
|
-
Maybe you want to:
|
|
24
|
-
|
|
25
|
-
* keep a record of a conversation with a former loved one, friend, or business partner. You want to delete the chat from your phone, but you don’t want to lose the memories.
|
|
26
|
-
* save a chat with important information, such as addresses, phone numbers, or other data.
|
|
27
|
-
* keep a chat with a person who has passed away.
|
|
28
|
-
* export just an excerpt of a chat from a specific date range.
|
|
29
|
-
* archive a chat before freeing up space on your phone by deleting photos, videos, and other documents that were part of the chat.
|
|
30
|
-
* ask another chat participant who still has a complete version of the chat (including all pictures) on his or her phone to send you a chat export, so you can convert and archive it.
|
|
31
|
-
* batch process multiple chat exports automatically using the CLI mode.
|
|
32
|
-
* integrate chat conversion into backup or archival scripts.
|
|
33
|
-
* process chat exports on headless servers or in automated workflows.
|
|
34
|
-
|
|
35
|
-
Furthermore, the tool is [open-source](https://github.com/mtln/chat-export) and runs offline on your computer, so you can be sure that your data is not being sent to any server.
|
|
36
|
-
And by the way, it’s free! If you find it useful, you can [donate](https://donate.stripe.com/3csfZLaIj5JE6dO4gg).
|
|
37
|
-
|
|
38
|
-
## Instructions
|
|
39
|
-
|
|
40
|
-
1. **Export the Chat:** Begin by exporting a chat from the WhatsApp app, preferably with media included. For detailed instructions, refer to [WhatsApp FAQ](https://faq.whatsapp.com/search?helpref=search&query=%20export%20chat).
|
|
41
|
-
You can for example save it on Google Drive or Dropbox or send it to yourself with WhatsApp.
|
|
42
|
-
|
|
43
|
-
2. **Transfer the File:** Move the exported ZIP file to your computer. If you have sent it to yourself with WhatsApp, you can download it with WhatsApp Web or with the WhatsApp App on your computer. Delete the WhatsApp message afterwards to save space.
|
|
44
|
-
|
|
45
|
-
3. **Install and Run the Tool:**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
If you
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
pip
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
1
|
+
# chat-export: Convert WhatsApp Chats to HTML
|
|
2
|
+
|
|
3
|
+
This tool converts a WhatsApp chat export into two HTML formats: one with inline media (such as images, videos, and audio files) and a compact version with media links.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Video Tutorial (still refers to the previously used tool name instead of `chat-export`, doesn't cover new features like the non-interactive CLI mode yet):
|
|
7
|
+
[](https://www.youtube.com/watch?v=s1dMO8pjkC8)"
|
|
8
|
+
|
|
9
|
+
## Binary Release Downloads
|
|
10
|
+
* Windows: [chat-export.exe](https://github.com/mtln/chat-export/releases/latest/download/chat-export.exe)
|
|
11
|
+
* Mac: [chat-export-installer.pkg](https://github.com/mtln/chat-export/releases/latest/download/chat-export-installer.pkg)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## Why is this useful?
|
|
15
|
+
The HTML export is:
|
|
16
|
+
|
|
17
|
+
* **Printable:** You can print the chat or save as a PDF.
|
|
18
|
+
* **Searchable:** You can search for specific messages in the browser.
|
|
19
|
+
* **Shareable:** You can share the chat with others, but make sure to get consent from all other participants first.
|
|
20
|
+
* **Durable:** You can keep the chat as a record for years to come. Apps come and go. Plain HTML is here to stay.
|
|
21
|
+
* **Automatable:** With the non-interactive CLI mode, you can script batch processing of multiple chat exports or integrate the tool into other workflows.
|
|
22
|
+
|
|
23
|
+
Maybe you want to:
|
|
24
|
+
|
|
25
|
+
* keep a record of a conversation with a former loved one, friend, or business partner. You want to delete the chat from your phone, but you don’t want to lose the memories.
|
|
26
|
+
* save a chat with important information, such as addresses, phone numbers, or other data.
|
|
27
|
+
* keep a chat with a person who has passed away.
|
|
28
|
+
* export just an excerpt of a chat from a specific date range.
|
|
29
|
+
* archive a chat before freeing up space on your phone by deleting photos, videos, and other documents that were part of the chat.
|
|
30
|
+
* ask another chat participant who still has a complete version of the chat (including all pictures) on his or her phone to send you a chat export, so you can convert and archive it.
|
|
31
|
+
* batch process multiple chat exports automatically using the CLI mode.
|
|
32
|
+
* integrate chat conversion into backup or archival scripts.
|
|
33
|
+
* process chat exports on headless servers or in automated workflows.
|
|
34
|
+
|
|
35
|
+
Furthermore, the tool is [open-source](https://github.com/mtln/chat-export) and runs offline on your computer, so you can be sure that your data is not being sent to any server.
|
|
36
|
+
And by the way, it’s free! If you find it useful, you can [donate](https://donate.stripe.com/3csfZLaIj5JE6dO4gg).
|
|
37
|
+
|
|
38
|
+
## Instructions
|
|
39
|
+
|
|
40
|
+
1. **Export the Chat:** Begin by exporting a chat from the WhatsApp app, preferably with media included. For detailed instructions, refer to [WhatsApp FAQ](https://faq.whatsapp.com/search?helpref=search&query=%20export%20chat).
|
|
41
|
+
You can for example save it on Google Drive or Dropbox or send it to yourself with WhatsApp.
|
|
42
|
+
|
|
43
|
+
2. **Transfer the File:** Move the exported ZIP file to your computer. If you have sent it to yourself with WhatsApp, you can download it with WhatsApp Web or with the WhatsApp App on your computer. Delete the WhatsApp message afterwards to save space.
|
|
44
|
+
|
|
45
|
+
3. **Install and Run the Tool:**
|
|
46
|
+
|
|
47
|
+
### Option 1: Download Binary Executables: Easy for non-technical users
|
|
48
|
+
|
|
49
|
+
If you don't know what Python is and just want to use the tool without having to deal with technical details, you can download the binary executables.
|
|
50
|
+
|
|
51
|
+
- For Windows, download [chat-export.exe](https://github.com/mtln/chat-export/releases/latest/download/chat-export.exe).
|
|
52
|
+
|
|
53
|
+
Because the .exe is unsigned, you will see a warning, but after clicking on "More Information" you should have the option to run it anyway. An EV code signing certificate for Windows costs a lot. If I get enough donations, I will buy one, so I can release signed exe files.
|
|
54
|
+
|
|
55
|
+
- For Mac, download [chat-export-installer.pkg](https://github.com/mtln/chat-export/releases/latest/download/chat-export-installer.pkg)
|
|
56
|
+
|
|
57
|
+
Once installed on Mac:
|
|
58
|
+
|
|
59
|
+
1. Open the **Terminal** app (e.g. by typing "terminal" in Spotlight Search).
|
|
60
|
+
2. Type: `chat-export`
|
|
61
|
+
|
|
62
|
+
This will start the chat export tool from anywhere in the terminal.
|
|
63
|
+
|
|
64
|
+
To uninstall `chat-export` from Mac, open the Terminal and run: `sudo rm /usr/local/bin/chat-export`
|
|
65
|
+
|
|
66
|
+
The executables have been built with pyinstaller.
|
|
67
|
+
|
|
68
|
+
### Option 2: Install via pip
|
|
69
|
+
|
|
70
|
+
If you have Python installed, you can install chat-export as a command-line tool:
|
|
71
|
+
```
|
|
72
|
+
pip install chat-export
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Optional: Install with platform-specific dependencies for native file dialogs**
|
|
76
|
+
|
|
77
|
+
For better user experience with native file picker dialogs, you can install platform-specific dependencies:
|
|
78
|
+
|
|
79
|
+
**On macOS:**
|
|
80
|
+
```
|
|
81
|
+
pip install chat-export[macos]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**On Windows:**
|
|
85
|
+
```
|
|
86
|
+
pip install chat-export[windows]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
*Note: Without these optional dependencies, the tool will fall back to command-line input for file selection or use tkinter (if available) for file dialogs. If you're planning to use the tool in non-interactive mode, you don't need to install these dependencies.*
|
|
91
|
+
|
|
92
|
+
After installation, you can run it from anywhere in your terminal:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
chat-export
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
or
|
|
99
|
+
```
|
|
100
|
+
chat-export --help
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
To uninstall:
|
|
104
|
+
```
|
|
105
|
+
pip uninstall chat-export
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Using uv tool instead of pip
|
|
109
|
+
If you have uv installed, you can use `uv tool install` instead of `pip install`. This is perfect for tools like chat-export because it will install the dependencies in a virtual environment and not in the global environment - while still making the tool available globally in your shell / path.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
`uv tool install chat-export[windows]`
|
|
113
|
+
or
|
|
114
|
+
`uv tool install chat-export[macos]`
|
|
115
|
+
or just
|
|
116
|
+
`uv tool install chat-export`
|
|
117
|
+
uninstall with:
|
|
118
|
+
`uv tool uninstall chat-export`
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
###Option 3: Run Directly (No Installation)
|
|
122
|
+
|
|
123
|
+
If Python is installed on your Windows, Mac or Linux computer, run the tool directly (no pip installation required, no venv required, no additional dependencies required because it's just vanilla standard lib Python in a single scriptfile, always run the latest up to date version directly from GitHub) with the following command:
|
|
124
|
+
```
|
|
125
|
+
python -c "import urllib.request; exec(urllib.request.urlopen('https://raw.githubusercontent.com/mtln/chat-export/refs/heads/main/main.py').read().decode())"
|
|
126
|
+
```
|
|
127
|
+
or
|
|
128
|
+
```
|
|
129
|
+
python3 -c "import urllib.request; exec(urllib.request.urlopen('https://raw.githubusercontent.com/mtln/chat-export/refs/heads/main/main.py').read().decode())"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Usage
|
|
133
|
+
|
|
134
|
+
The tool supports both interactive and non-interactive modes:
|
|
135
|
+
|
|
136
|
+
### Interactive Mode (Default)
|
|
137
|
+
|
|
138
|
+
Just run `chat-export` in your terminal.
|
|
139
|
+
|
|
140
|
+
**CLI Parameters:**
|
|
141
|
+
|
|
142
|
+
`-o, --output-dir`: Base directory where the chat folder will be created (optional, default: current directory)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
* After starting the tool, a file picker dialog will open. Select the ZIP file of the chat export you want to convert. If your installation does not support file dialogs, you will be prompted for the path to the ZIP file.
|
|
146
|
+
* You can enter start and end dates to export only a specific date range. If you leave the fields empty, the entire chat will be exported. **If the terminal window doesn't accept your keyboard input, click with your mouse right after the colon in `Enter the number corresponding to your name:`** to set the focus to the terminal window.
|
|
147
|
+
* A list of chat participants will appear. Select your name so that your messages are displayed in green chat bubbles, just like on WhatsApp.
|
|
148
|
+
* Once the conversion completes, you can choose to open the HTML files immediately in your browser (just hit enter). From there, you can save the chat as a PDF or print it if needed.
|
|
149
|
+
|
|
150
|
+
### Command Line Interface (Non-Interactive Mode)
|
|
151
|
+
|
|
152
|
+
For automation, scripting, or when you know all parameters in advance, you can use the non-interactive CLI mode. This mode processes the chat without any prompts and is perfect for batch processing or integration into other tools.
|
|
153
|
+
|
|
154
|
+
#### Media Embedding Options
|
|
155
|
+
|
|
156
|
+
The tool offers two approaches for handling media files:
|
|
157
|
+
|
|
158
|
+
**Default Behavior (External Media Files):**
|
|
159
|
+
- Creates a folder structure with HTML files and a separate `media/` folder
|
|
160
|
+
- Media files are extracted and linked to from the HTML
|
|
161
|
+
- Results in multiple files that need to be kept together
|
|
162
|
+
- Smaller HTML files, but requires managing multiple files
|
|
163
|
+
|
|
164
|
+
**Embedded Media (`--embed-media` option):**
|
|
165
|
+
- Creates a single self-contained HTML file with all media embedded as base64
|
|
166
|
+
- No external media files are created or needed
|
|
167
|
+
- Perfect for sharing, archiving, or when you want everything in one file
|
|
168
|
+
- Larger HTML file size, but completely portable
|
|
169
|
+
|
|
170
|
+
**Basic Usage:**
|
|
171
|
+
```
|
|
172
|
+
chat-export -n -z "path/to/chat.zip" -p "Your Name"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**CLI Parameters:**
|
|
176
|
+
|
|
177
|
+
- `-n, --non-interactive`: Enable non-interactive mode (required)
|
|
178
|
+
- `-z, --zip-file`: Path to WhatsApp chat export ZIP file (required)
|
|
179
|
+
- `-p, --participant`: Your name exactly as it appears in the chat (required)
|
|
180
|
+
- `--from-date`: Optional start date for filtering (formats: DD.MM.YYYY, MM/DD/YYYY, DD.MM.YY, MM/DD/YY)
|
|
181
|
+
- `--until-date`: Optional end date for filtering
|
|
182
|
+
- `-o, --output-dir`: Base directory where the chat folder will be created (default: current directory)
|
|
183
|
+
- `--embed-media`: Embed media files as base64 in HTML instead of linking to external files (optional)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
**Examples:**
|
|
187
|
+
|
|
188
|
+
Basic conversion:
|
|
189
|
+
```
|
|
190
|
+
chat-export -n -z "WhatsApp Chat with John.zip" -p "Your Name"
|
|
191
|
+
```
|
|
192
|
+
With absolute path to ZIP file (Windows):
|
|
193
|
+
```
|
|
194
|
+
chat-export -n -z "c:\temp\WhatsApp Chat with John.zip" -p "Your Name"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
With date filtering:
|
|
198
|
+
```
|
|
199
|
+
chat-export -n -z "chat.zip" -p "Your Name" --from-date "01.01.2024" --until-date "31.12.2024"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Custom output directory (creates chat folder in /tmp instead of current directory):
|
|
203
|
+
```
|
|
204
|
+
chat-export -n -z "chat.zip" -p "Your Name" -o "/tmp"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Windows paths (important: no trailing backslash):
|
|
208
|
+
```
|
|
209
|
+
chat-export -n -z "chat.zip" -p "Your Name" -o "C:\temp"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
With embedded media (creates a single self-contained HTML file):
|
|
213
|
+
```
|
|
214
|
+
chat-export -n -z "chat.zip" -p "Your Name" --embed-media
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Important Notes:**
|
|
218
|
+
- The participant name must match exactly as it appears in the chat (case-sensitive)
|
|
219
|
+
- If the participant name is not found, the tool will display all available participants and exit
|
|
220
|
+
- When using `--embed-media`, media files are encoded as base64 and embedded directly in the HTML, creating a single self-contained file that doesn't require external media files
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
* When printing an HTML page, most web browsers are set by default to exclude background colors to save ink or toner. If you want to include them, you need to enable background graphics in your browser settings. See the section below for instructions.
|
|
224
|
+
* **In Google Chrome**: Go to `Print` → `More settings` → Check `Background graphics`.
|
|
225
|
+
* **In Mozilla Firefox**: Go to `File` → `Print` → `Page Setup` → Check `Print Background (colors & images)`.
|
|
226
|
+
* **In Microsoft Edge**: Go to `Print` → `More settings` → Check `Background graphics`.
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
## Supported languages
|
|
230
|
+
WhatsApp chat exports vary depending on your phone’s system language.
|
|
231
|
+
|
|
232
|
+
chat-export has been tested with many languages and regional settings.
|
|
233
|
+
|
|
234
|
+
If it doesn't work for your language, please let me know.
|
|
235
|
+
|
|
236
|
+
## Supported Operating Systems
|
|
237
|
+
|
|
238
|
+
* Windows
|
|
239
|
+
* Mac
|
|
240
|
+
* Linux
|
|
241
|
+
|
|
242
|
+
## Limitations
|
|
243
|
+
There is information in chats, that is not included in the export.zip created by WhatsApp. As a consequence, this information cannot be rendered by chat-export.
|
|
244
|
+
|
|
245
|
+
This includes:
|
|
246
|
+
|
|
247
|
+
* Reactions (thumbs up and other emojis that are not a stand-alone message, but created by long tapping a previous message)
|
|
248
|
+
* Message relations (in WhatsApp, you can right-swipe a previous message and then write a message in relation to this)
|
|
249
|
+
* Chat group metadata (e.g. list of all members, admins, ...)
|
|
250
|
+
|
|
251
|
+
## Disclaimer
|
|
252
|
+
chat-export comes with no warranty. Use it responsibly and respect the privacy of other chat participants. The tool is not affiliated with WhatsApp or Meta.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chat-export
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: Convert WhatsApp chat exports to HTML format
|
|
5
5
|
Author-email: matlon GmbH <software@matlon.ch>
|
|
6
6
|
License: MIT License
|
|
@@ -60,7 +60,7 @@ Dynamic: license-file
|
|
|
60
60
|
This tool converts a WhatsApp chat export into two HTML formats: one with inline media (such as images, videos, and audio files) and a compact version with media links.
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
Video Tutorial (still refers to the previously used tool name instead of `chat-export
|
|
63
|
+
Video Tutorial (still refers to the previously used tool name instead of `chat-export`, doesn't cover new features like the non-interactive CLI mode yet):
|
|
64
64
|
[](https://www.youtube.com/watch?v=s1dMO8pjkC8)"
|
|
65
65
|
|
|
66
66
|
## Binary Release Downloads
|
|
@@ -101,7 +101,28 @@ You can for example save it on Google Drive or Dropbox or send it to yourself wi
|
|
|
101
101
|
|
|
102
102
|
3. **Install and Run the Tool:**
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
### Option 1: Download Binary Executables: Easy for non-technical users
|
|
105
|
+
|
|
106
|
+
If you don't know what Python is and just want to use the tool without having to deal with technical details, you can download the binary executables.
|
|
107
|
+
|
|
108
|
+
- For Windows, download [chat-export.exe](https://github.com/mtln/chat-export/releases/latest/download/chat-export.exe).
|
|
109
|
+
|
|
110
|
+
Because the .exe is unsigned, you will see a warning, but after clicking on "More Information" you should have the option to run it anyway. An EV code signing certificate for Windows costs a lot. If I get enough donations, I will buy one, so I can release signed exe files.
|
|
111
|
+
|
|
112
|
+
- For Mac, download [chat-export-installer.pkg](https://github.com/mtln/chat-export/releases/latest/download/chat-export-installer.pkg)
|
|
113
|
+
|
|
114
|
+
Once installed on Mac:
|
|
115
|
+
|
|
116
|
+
1. Open the **Terminal** app (e.g. by typing "terminal" in Spotlight Search).
|
|
117
|
+
2. Type: `chat-export`
|
|
118
|
+
|
|
119
|
+
This will start the chat export tool from anywhere in the terminal.
|
|
120
|
+
|
|
121
|
+
To uninstall `chat-export` from Mac, open the Terminal and run: `sudo rm /usr/local/bin/chat-export`
|
|
122
|
+
|
|
123
|
+
The executables have been built with pyinstaller.
|
|
124
|
+
|
|
125
|
+
### Option 2: Install via pip
|
|
105
126
|
|
|
106
127
|
If you have Python installed, you can install chat-export as a command-line tool:
|
|
107
128
|
```
|
|
@@ -122,43 +143,41 @@ You can for example save it on Google Drive or Dropbox or send it to yourself wi
|
|
|
122
143
|
pip install chat-export[windows]
|
|
123
144
|
```
|
|
124
145
|
|
|
125
|
-
|
|
126
|
-
|
|
146
|
+
|
|
127
147
|
*Note: Without these optional dependencies, the tool will fall back to command-line input for file selection or use tkinter (if available) for file dialogs. If you're planning to use the tool in non-interactive mode, you don't need to install these dependencies.*
|
|
128
148
|
|
|
129
149
|
After installation, you can run it from anywhere in your terminal:
|
|
130
|
-
|
|
150
|
+
|
|
131
151
|
```
|
|
132
152
|
chat-export
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
or
|
|
156
|
+
```
|
|
133
157
|
chat-export --help
|
|
134
158
|
```
|
|
135
|
-
|
|
159
|
+
|
|
136
160
|
To uninstall:
|
|
137
161
|
```
|
|
138
162
|
pip uninstall chat-export
|
|
139
163
|
```
|
|
140
164
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
- For Windows, download [chat-export.exe](https://github.com/mtln/chat-export/releases/latest/download/chat-export.exe).
|
|
144
|
-
|
|
145
|
-
Because the .exe is unsigned, you will see a warning, but after clicking on "More Information" you should have the option to run it anyway. An EV code signing certificate for Windows costs a lot. If I get enough donations, I will buy one, so I can release signed exe files.
|
|
146
|
-
|
|
147
|
-
- For Mac, download [chat-export-installer.pkg](https://github.com/mtln/chat-export/releases/latest/download/chat-export-installer.pkg)
|
|
148
|
-
|
|
149
|
-
Once installed on Mac:
|
|
150
|
-
1. Open the **Terminal** app (e.g. by typing "terminal" in Spotlight Search).
|
|
151
|
-
2. Type: `chat-export`
|
|
152
|
-
|
|
153
|
-
This will start the chat export tool from anywhere in the terminal.
|
|
165
|
+
#### Using uv tool instead of pip
|
|
166
|
+
If you have uv installed, you can use `uv tool install` instead of `pip install`. This is perfect for tools like chat-export because it will install the dependencies in a virtual environment and not in the global environment - while still making the tool available globally in your shell / path.
|
|
154
167
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
|
|
169
|
+
`uv tool install chat-export[windows]`
|
|
170
|
+
or
|
|
171
|
+
`uv tool install chat-export[macos]`
|
|
172
|
+
or just
|
|
173
|
+
`uv tool install chat-export`
|
|
174
|
+
uninstall with:
|
|
175
|
+
`uv tool uninstall chat-export`
|
|
176
|
+
|
|
158
177
|
|
|
159
|
-
|
|
178
|
+
###Option 3: Run Directly (No Installation)
|
|
160
179
|
|
|
161
|
-
If Python is installed on your Windows, Mac or Linux computer, run the tool directly (no installation required) with the following command:
|
|
180
|
+
If Python is installed on your Windows, Mac or Linux computer, run the tool directly (no pip installation required, no venv required, no additional dependencies required because it's just vanilla standard lib Python in a single scriptfile, always run the latest up to date version directly from GitHub) with the following command:
|
|
162
181
|
```
|
|
163
182
|
python -c "import urllib.request; exec(urllib.request.urlopen('https://raw.githubusercontent.com/mtln/chat-export/refs/heads/main/main.py').read().decode())"
|
|
164
183
|
```
|
|
@@ -167,11 +186,14 @@ You can for example save it on Google Drive or Dropbox or send it to yourself wi
|
|
|
167
186
|
python3 -c "import urllib.request; exec(urllib.request.urlopen('https://raw.githubusercontent.com/mtln/chat-export/refs/heads/main/main.py').read().decode())"
|
|
168
187
|
```
|
|
169
188
|
|
|
170
|
-
|
|
189
|
+
## Usage
|
|
171
190
|
|
|
172
191
|
The tool supports both interactive and non-interactive modes:
|
|
173
192
|
|
|
174
193
|
### Interactive Mode (Default)
|
|
194
|
+
|
|
195
|
+
Just run `chat-export` in your terminal.
|
|
196
|
+
|
|
175
197
|
**CLI Parameters:**
|
|
176
198
|
|
|
177
199
|
`-o, --output-dir`: Base directory where the chat folder will be created (optional, default: current directory)
|
|
@@ -186,6 +208,22 @@ The tool supports both interactive and non-interactive modes:
|
|
|
186
208
|
|
|
187
209
|
For automation, scripting, or when you know all parameters in advance, you can use the non-interactive CLI mode. This mode processes the chat without any prompts and is perfect for batch processing or integration into other tools.
|
|
188
210
|
|
|
211
|
+
#### Media Embedding Options
|
|
212
|
+
|
|
213
|
+
The tool offers two approaches for handling media files:
|
|
214
|
+
|
|
215
|
+
**Default Behavior (External Media Files):**
|
|
216
|
+
- Creates a folder structure with HTML files and a separate `media/` folder
|
|
217
|
+
- Media files are extracted and linked to from the HTML
|
|
218
|
+
- Results in multiple files that need to be kept together
|
|
219
|
+
- Smaller HTML files, but requires managing multiple files
|
|
220
|
+
|
|
221
|
+
**Embedded Media (`--embed-media` option):**
|
|
222
|
+
- Creates a single self-contained HTML file with all media embedded as base64
|
|
223
|
+
- No external media files are created or needed
|
|
224
|
+
- Perfect for sharing, archiving, or when you want everything in one file
|
|
225
|
+
- Larger HTML file size, but completely portable
|
|
226
|
+
|
|
189
227
|
**Basic Usage:**
|
|
190
228
|
```
|
|
191
229
|
chat-export -n -z "path/to/chat.zip" -p "Your Name"
|
|
@@ -199,6 +237,7 @@ chat-export -n -z "path/to/chat.zip" -p "Your Name"
|
|
|
199
237
|
- `--from-date`: Optional start date for filtering (formats: DD.MM.YYYY, MM/DD/YYYY, DD.MM.YY, MM/DD/YY)
|
|
200
238
|
- `--until-date`: Optional end date for filtering
|
|
201
239
|
- `-o, --output-dir`: Base directory where the chat folder will be created (default: current directory)
|
|
240
|
+
- `--embed-media`: Embed media files as base64 in HTML instead of linking to external files (optional)
|
|
202
241
|
|
|
203
242
|
|
|
204
243
|
**Examples:**
|
|
@@ -227,9 +266,15 @@ Windows paths (important: no trailing backslash):
|
|
|
227
266
|
chat-export -n -z "chat.zip" -p "Your Name" -o "C:\temp"
|
|
228
267
|
```
|
|
229
268
|
|
|
269
|
+
With embedded media (creates a single self-contained HTML file):
|
|
270
|
+
```
|
|
271
|
+
chat-export -n -z "chat.zip" -p "Your Name" --embed-media
|
|
272
|
+
```
|
|
273
|
+
|
|
230
274
|
**Important Notes:**
|
|
231
275
|
- The participant name must match exactly as it appears in the chat (case-sensitive)
|
|
232
276
|
- If the participant name is not found, the tool will display all available participants and exit
|
|
277
|
+
- When using `--embed-media`, media files are encoded as base64 and embedded directly in the HTML, creating a single self-contained file that doesn't require external media files
|
|
233
278
|
|
|
234
279
|
|
|
235
280
|
* When printing an HTML page, most web browsers are set by default to exclude background colors to save ink or toner. If you want to include them, you need to enable background graphics in your browser settings. See the section below for instructions.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import base64
|
|
2
3
|
import difflib
|
|
3
4
|
import os
|
|
4
5
|
import sys
|
|
@@ -286,7 +287,7 @@ def windows_file_picker():
|
|
|
286
287
|
|
|
287
288
|
|
|
288
289
|
|
|
289
|
-
version = "0.9.
|
|
290
|
+
version = "0.9.4"
|
|
290
291
|
|
|
291
292
|
donate_link = "https://donate.stripe.com/3csfZLaIj5JE6dO4gg"
|
|
292
293
|
|
|
@@ -355,6 +356,10 @@ Examples:
|
|
|
355
356
|
type=str,
|
|
356
357
|
help='Base directory where the ZIP-derived output folder will be created (default: current working directory)')
|
|
357
358
|
|
|
359
|
+
parser.add_argument('--embed-media',
|
|
360
|
+
action='store_true',
|
|
361
|
+
help='Embed media files as base64 in HTML instead of linking to external files')
|
|
362
|
+
|
|
358
363
|
args = parser.parse_args()
|
|
359
364
|
|
|
360
365
|
# Validate non-interactive mode requirements
|
|
@@ -455,6 +460,44 @@ class MessageParser:
|
|
|
455
460
|
except (ValueError, AttributeError):
|
|
456
461
|
return None
|
|
457
462
|
|
|
463
|
+
@staticmethod
|
|
464
|
+
def trim_zero_widths(text: str) -> str:
|
|
465
|
+
"""Trim harmless zero-width characters from the start and end of a string.
|
|
466
|
+
|
|
467
|
+
Safe to strip: ZWSP, ZWNJ, ZWJ, BOM.
|
|
468
|
+
Does NOT strip directional controls like LRM, RLM, FSI, PDI, etc.
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
SAFE_ZERO_WIDTHS = "\u200B\u200C\u200D\uFEFF" # ZWSP, ZWNJ, ZWJ, BOM
|
|
472
|
+
return text.strip(SAFE_ZERO_WIDTHS).strip()
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@staticmethod
|
|
476
|
+
def mark_invisible_chars(text: str) -> str:
|
|
477
|
+
"""Replace zero-width and directional Unicode control chars with ASCII markers."""
|
|
478
|
+
|
|
479
|
+
replacements = {
|
|
480
|
+
"\u200B": ":ZWSP:", # Zero Width Space
|
|
481
|
+
"\u200C": ":ZWNJ:", # Zero Width Non-Joiner
|
|
482
|
+
"\u200D": ":ZWJ:", # Zero Width Joiner
|
|
483
|
+
"\uFEFF": ":BOM:", # Zero Width No-Break Space / BOM
|
|
484
|
+
|
|
485
|
+
"\u200E": ":LRM:", # Left-to-Right Mark
|
|
486
|
+
"\u200F": ":RLM:", # Right-to-Left Mark
|
|
487
|
+
"\u2066": ":LRI:", # Left-to-Right Isolate
|
|
488
|
+
"\u2067": ":RLI:", # Right-to-Left Isolate
|
|
489
|
+
"\u2068": ":FSI:", # First Strong Isolate
|
|
490
|
+
"\u2069": ":PDI:", # Pop Directional Isolate
|
|
491
|
+
|
|
492
|
+
"\u202A": ":LRE:", # Left-to-Right Embedding
|
|
493
|
+
"\u202B": ":RLE:", # Right-to-Left Embedding
|
|
494
|
+
"\u202C": ":PDF:", # Pop Directional Formatting
|
|
495
|
+
"\u202D": ":LRO:", # Left-to-Right Override
|
|
496
|
+
"\u202E": ":RLO:", # Right-to-Left Override
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return "".join(replacements.get(ch, ch) for ch in text)
|
|
500
|
+
|
|
458
501
|
def get_senders(self, chat_content):
|
|
459
502
|
"""Extract all unique senders from chat content."""
|
|
460
503
|
senders = set()
|
|
@@ -463,6 +506,8 @@ class MessageParser:
|
|
|
463
506
|
match = pattern.match(line)
|
|
464
507
|
if match:
|
|
465
508
|
sender = match.group(2)
|
|
509
|
+
sender = self.trim_zero_widths(sender)
|
|
510
|
+
sender = self.mark_invisible_chars(sender)
|
|
466
511
|
senders.add(sender)
|
|
467
512
|
return sorted(list(senders))
|
|
468
513
|
|
|
@@ -585,6 +630,8 @@ class MessageParser:
|
|
|
585
630
|
|
|
586
631
|
if match:
|
|
587
632
|
timestamp, sender, content = match.groups()
|
|
633
|
+
sender = self.trim_zero_widths(sender)
|
|
634
|
+
sender = self.mark_invisible_chars(sender)
|
|
588
635
|
messages.append(Message.create_with_context(
|
|
589
636
|
timestamp=timestamp,
|
|
590
637
|
sender=sender,
|
|
@@ -610,16 +657,25 @@ class MessageParser:
|
|
|
610
657
|
class HTMLRenderer(Renderer):
|
|
611
658
|
"""Renders messages to HTML format."""
|
|
612
659
|
|
|
613
|
-
def __init__(self, output_dir, has_media=False):
|
|
660
|
+
def __init__(self, output_dir, has_media=False, embed_media=False, zip_path=None):
|
|
614
661
|
super().__init__(output_dir)
|
|
615
662
|
self.has_media = has_media
|
|
663
|
+
self.embed_media = embed_media
|
|
664
|
+
self.zip_path = zip_path
|
|
616
665
|
self.html_filename = 'chat.html'
|
|
617
666
|
self.html_filename_media_linked = 'chat_media_linked.html'
|
|
667
|
+
if embed_media:
|
|
668
|
+
# replace .zip with .html
|
|
669
|
+
self.html_filename = Path(self.zip_path).stem + '.html'
|
|
670
|
+
self.html_filename_media_linked = None
|
|
618
671
|
self.attachments_to_extract = set()
|
|
619
672
|
|
|
620
673
|
def get_generated_files(self) -> list[Path]:
|
|
621
674
|
"""Get the generated files."""
|
|
622
|
-
|
|
675
|
+
result = [Path(self.output_dir, self.html_filename)]
|
|
676
|
+
if self.html_filename_media_linked:
|
|
677
|
+
result.append(Path(self.output_dir, self.html_filename_media_linked))
|
|
678
|
+
return result
|
|
623
679
|
|
|
624
680
|
def get_css_styles(self):
|
|
625
681
|
"""Return the CSS styles for the HTML output."""
|
|
@@ -654,6 +710,30 @@ class HTMLRenderer(Renderer):
|
|
|
654
710
|
border-radius: 5px;
|
|
655
711
|
margin: 5px 0;
|
|
656
712
|
}
|
|
713
|
+
.text-file {
|
|
714
|
+
border: 1px solid #ddd;
|
|
715
|
+
border-radius: 5px;
|
|
716
|
+
padding: 10px;
|
|
717
|
+
background-color: #f9f9f9;
|
|
718
|
+
margin: 5px 0;
|
|
719
|
+
}
|
|
720
|
+
.text-file h4 {
|
|
721
|
+
margin: 0 0 10px 0;
|
|
722
|
+
color: #333;
|
|
723
|
+
font-size: 0.9em;
|
|
724
|
+
}
|
|
725
|
+
.text-content {
|
|
726
|
+
background-color: #fff;
|
|
727
|
+
border: 1px solid #e0e0e0;
|
|
728
|
+
border-radius: 3px;
|
|
729
|
+
padding: 10px;
|
|
730
|
+
font-family: 'Courier New', monospace;
|
|
731
|
+
font-size: 0.8em;
|
|
732
|
+
max-height: 300px;
|
|
733
|
+
overflow-y: auto;
|
|
734
|
+
white-space: pre-wrap;
|
|
735
|
+
word-wrap: break-word;
|
|
736
|
+
}
|
|
657
737
|
.timestamp {
|
|
658
738
|
color: #667781;
|
|
659
739
|
font-size: 0.75em;
|
|
@@ -711,6 +791,83 @@ class HTMLRenderer(Renderer):
|
|
|
711
791
|
</body>
|
|
712
792
|
</html>"""
|
|
713
793
|
|
|
794
|
+
def get_mime_type(self, filename):
|
|
795
|
+
"""Get MIME type based on file extension."""
|
|
796
|
+
ext = filename.lower()
|
|
797
|
+
if ext.endswith(('.jpg', '.jpeg')):
|
|
798
|
+
return 'image/jpeg'
|
|
799
|
+
elif ext.endswith('.png'):
|
|
800
|
+
return 'image/png'
|
|
801
|
+
elif ext.endswith('.gif'):
|
|
802
|
+
return 'image/gif'
|
|
803
|
+
elif ext.endswith('.webp'):
|
|
804
|
+
return 'image/webp'
|
|
805
|
+
elif ext.endswith('.mp4'):
|
|
806
|
+
return 'video/mp4'
|
|
807
|
+
elif ext.endswith('.opus'):
|
|
808
|
+
return 'audio/ogg'
|
|
809
|
+
elif ext.endswith('.mp3'):
|
|
810
|
+
return 'audio/mpeg'
|
|
811
|
+
elif ext.endswith('.wav'):
|
|
812
|
+
return 'audio/wav'
|
|
813
|
+
elif ext.endswith('.m4a'):
|
|
814
|
+
return 'audio/mp4'
|
|
815
|
+
elif ext.endswith('.pdf'):
|
|
816
|
+
return 'application/pdf'
|
|
817
|
+
elif ext.endswith(('.doc', '.docx')):
|
|
818
|
+
return 'application/msword'
|
|
819
|
+
elif ext.endswith(('.xls', '.xlsx')):
|
|
820
|
+
return 'application/vnd.ms-excel'
|
|
821
|
+
elif ext.endswith(('.ppt', '.pptx')):
|
|
822
|
+
return 'application/vnd.ms-powerpoint'
|
|
823
|
+
elif ext.endswith('.txt'):
|
|
824
|
+
return 'text/plain'
|
|
825
|
+
elif ext.endswith('.rtf'):
|
|
826
|
+
return 'application/rtf'
|
|
827
|
+
elif ext.endswith('.zip'):
|
|
828
|
+
return 'application/zip'
|
|
829
|
+
elif ext.endswith('.rar'):
|
|
830
|
+
return 'application/x-rar-compressed'
|
|
831
|
+
elif ext.endswith('.7z'):
|
|
832
|
+
return 'application/x-7z-compressed'
|
|
833
|
+
elif ext.endswith('.tar'):
|
|
834
|
+
return 'application/x-tar'
|
|
835
|
+
elif ext.endswith('.gz'):
|
|
836
|
+
return 'application/gzip'
|
|
837
|
+
elif ext.endswith('.csv'):
|
|
838
|
+
return 'text/csv'
|
|
839
|
+
elif ext.endswith('.json'):
|
|
840
|
+
return 'application/json'
|
|
841
|
+
elif ext.endswith('.xml'):
|
|
842
|
+
return 'application/xml'
|
|
843
|
+
elif ext.endswith('.html'):
|
|
844
|
+
return 'text/html'
|
|
845
|
+
elif ext.endswith('.css'):
|
|
846
|
+
return 'text/css'
|
|
847
|
+
elif ext.endswith('.js'):
|
|
848
|
+
return 'application/javascript'
|
|
849
|
+
elif ext.endswith('.py'):
|
|
850
|
+
return 'text/x-python'
|
|
851
|
+
elif ext.endswith('.java'):
|
|
852
|
+
return 'text/x-java-source'
|
|
853
|
+
elif ext.endswith(('.cpp', '.c', '.h')):
|
|
854
|
+
return 'text/x-c'
|
|
855
|
+
else:
|
|
856
|
+
return 'application/octet-stream'
|
|
857
|
+
|
|
858
|
+
def encode_media_to_base64(self, attachment_name):
|
|
859
|
+
"""Read media file from zip and encode to base64."""
|
|
860
|
+
try:
|
|
861
|
+
with zipfile.ZipFile(self.zip_path, 'r') as zip_ref:
|
|
862
|
+
with zip_ref.open(attachment_name) as media_file:
|
|
863
|
+
media_data = media_file.read()
|
|
864
|
+
base64_data = base64.b64encode(media_data).decode('utf-8')
|
|
865
|
+
mime_type = self.get_mime_type(attachment_name)
|
|
866
|
+
return f"data:{mime_type};base64,{base64_data}"
|
|
867
|
+
except Exception as e:
|
|
868
|
+
print(f"Warning: Could not encode {attachment_name} to base64: {e}")
|
|
869
|
+
return None
|
|
870
|
+
|
|
714
871
|
def render_media_element(self, attachment_name, is_media_linked=False):
|
|
715
872
|
"""Render a media element based on its file extension."""
|
|
716
873
|
media_path = f"./media/{attachment_name}"
|
|
@@ -721,6 +878,32 @@ class HTMLRenderer(Renderer):
|
|
|
721
878
|
|
|
722
879
|
# Render media inline in main version
|
|
723
880
|
ext = attachment_name.lower()
|
|
881
|
+
|
|
882
|
+
# If embed_media is enabled, try to encode as base64
|
|
883
|
+
if self.embed_media and self.zip_path:
|
|
884
|
+
base64_data = self.encode_media_to_base64(attachment_name)
|
|
885
|
+
if base64_data:
|
|
886
|
+
# Images
|
|
887
|
+
if ext.endswith(('.jpg', '.jpeg', '.png', '.webp', '.gif')):
|
|
888
|
+
return f'<img class="media" src="{base64_data}"><br>'
|
|
889
|
+
# Videos
|
|
890
|
+
elif ext.endswith('.mp4'):
|
|
891
|
+
return f'<video class="media" controls><source src="{base64_data}" type="video/mp4"></video><br>'
|
|
892
|
+
# Audio files
|
|
893
|
+
elif ext.endswith('.opus'):
|
|
894
|
+
return f'<audio class="media" controls><source src="{base64_data}" type="audio/ogg"></audio><br>'
|
|
895
|
+
elif ext.endswith('.wav'):
|
|
896
|
+
return f'<audio class="media" controls><source src="{base64_data}" type="audio/wav"></audio><br>'
|
|
897
|
+
elif ext.endswith('.mp3'):
|
|
898
|
+
return f'<audio class="media" controls><source src="{base64_data}" type="audio/mpeg"></audio><br>'
|
|
899
|
+
elif ext.endswith('.m4a'):
|
|
900
|
+
return f'<audio class="media" controls><source src="{base64_data}" type="audio/mp4"></audio><br>'
|
|
901
|
+
|
|
902
|
+
# Other documents - provide download link with base64 data
|
|
903
|
+
else:
|
|
904
|
+
return f'<a href="{base64_data}" download="{attachment_name}">📎 {attachment_name}</a><br>'
|
|
905
|
+
|
|
906
|
+
# Fallback to file references
|
|
724
907
|
if ext.endswith(('.jpg', '.jpeg', '.png', '.webp', '.gif')):
|
|
725
908
|
return f'<img class="media" src="{media_path}"><br>'
|
|
726
909
|
elif ext.endswith('.mp4'):
|
|
@@ -796,7 +979,11 @@ class HTMLRenderer(Renderer):
|
|
|
796
979
|
self.chat = chat
|
|
797
980
|
# Prepare file paths
|
|
798
981
|
main_html_path = os.path.join(self.output_dir, self.html_filename)
|
|
799
|
-
|
|
982
|
+
if self.html_filename_media_linked:
|
|
983
|
+
media_linked_html_path = os.path.join(self.output_dir, self.html_filename_media_linked)
|
|
984
|
+
else:
|
|
985
|
+
# temp file
|
|
986
|
+
media_linked_html_path = "_temp.tmp"
|
|
800
987
|
|
|
801
988
|
# Open both files for writing
|
|
802
989
|
with open(main_html_path, 'w', encoding='utf-8') as main_f, \
|
|
@@ -833,11 +1020,14 @@ class HTMLRenderer(Renderer):
|
|
|
833
1020
|
main_f.write(footer)
|
|
834
1021
|
media_f.write(footer)
|
|
835
1022
|
|
|
1023
|
+
if self.embed_media:
|
|
1024
|
+
os.remove(media_linked_html_path)
|
|
1025
|
+
|
|
836
1026
|
return self.attachments_to_extract
|
|
837
1027
|
|
|
838
1028
|
|
|
839
1029
|
class ChatExport:
|
|
840
|
-
def __init__(self, zip_path, from_date=None, until_date=None, participant_name=None, base_output_dir=None):
|
|
1030
|
+
def __init__(self, zip_path, from_date=None, until_date=None, participant_name=None, base_output_dir=None, embed_media=False):
|
|
841
1031
|
# Validate zip file existence
|
|
842
1032
|
if not os.path.exists(zip_path):
|
|
843
1033
|
raise FileNotFoundError(f"Could not find the file: {zip_path}\nPlease check if the file path is correct.")
|
|
@@ -851,15 +1041,22 @@ class ChatExport:
|
|
|
851
1041
|
self.from_date = from_date
|
|
852
1042
|
self.until_date = until_date
|
|
853
1043
|
self.participant_name = participant_name
|
|
1044
|
+
self.embed_media = embed_media
|
|
854
1045
|
|
|
855
1046
|
# Set up output directory: base_output_dir/zip_filename or just zip_filename
|
|
856
1047
|
zip_stem = Path(zip_path).stem
|
|
857
|
-
if
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1048
|
+
if self.embed_media:
|
|
1049
|
+
if base_output_dir:
|
|
1050
|
+
self.output_dir = parse_path(base_output_dir)
|
|
1051
|
+
else:
|
|
1052
|
+
self.output_dir = ""
|
|
861
1053
|
else:
|
|
862
|
-
|
|
1054
|
+
if base_output_dir:
|
|
1055
|
+
# Normalize the base output directory path (handle Windows paths, quotes, etc.)
|
|
1056
|
+
normalized_base_dir = parse_path(base_output_dir)
|
|
1057
|
+
self.output_dir = os.path.join(normalized_base_dir, zip_stem)
|
|
1058
|
+
else:
|
|
1059
|
+
self.output_dir = zip_stem
|
|
863
1060
|
self.media_dir = os.path.join(self.output_dir, "media")
|
|
864
1061
|
|
|
865
1062
|
self.own_name = participant_name
|
|
@@ -930,7 +1127,9 @@ class ChatExport:
|
|
|
930
1127
|
# Setup renderer
|
|
931
1128
|
self.renderer = HTMLRenderer(
|
|
932
1129
|
output_dir=self.output_dir,
|
|
933
|
-
has_media=self.has_media
|
|
1130
|
+
has_media=self.has_media,
|
|
1131
|
+
embed_media=self.embed_media,
|
|
1132
|
+
zip_path=self.zip_path
|
|
934
1133
|
)
|
|
935
1134
|
|
|
936
1135
|
@staticmethod
|
|
@@ -969,13 +1168,18 @@ class ChatExport:
|
|
|
969
1168
|
# Get the base name of the zip file without extension
|
|
970
1169
|
zip_base_name = Path(self.zip_path).stem
|
|
971
1170
|
|
|
972
|
-
if
|
|
973
|
-
|
|
974
|
-
|
|
1171
|
+
# safety: clean only if it's a subfolder with zip_base_name
|
|
1172
|
+
if os.path.exists(self.output_dir) and str(self.output_dir).endswith(zip_base_name):
|
|
1173
|
+
print(f"Cleaning existing directory: {self.output_dir}")
|
|
1174
|
+
shutil.rmtree(self.output_dir)
|
|
1175
|
+
|
|
1176
|
+
os.makedirs(self.output_dir, exist_ok=True)
|
|
975
1177
|
|
|
1178
|
+
# if not embed_media, create media directory
|
|
1179
|
+
if not self.embed_media:
|
|
976
1180
|
# Create fresh output directories
|
|
977
|
-
|
|
978
|
-
|
|
1181
|
+
os.makedirs(self.media_dir)
|
|
1182
|
+
|
|
979
1183
|
|
|
980
1184
|
|
|
981
1185
|
|
|
@@ -1004,7 +1208,9 @@ class ChatExport:
|
|
|
1004
1208
|
print(f"ZIP file is an {'iOS' if self.is_ios else 'Android'} export with media/attachments, '{chat_file}' is the chat text file.")
|
|
1005
1209
|
else:
|
|
1006
1210
|
print(f"ZIP file is an {'iOS' if self.is_ios else 'Android'} export without media/attachments, '{chat_file}' is the chat text file.")
|
|
1007
|
-
|
|
1211
|
+
# delete if exists
|
|
1212
|
+
if os.path.exists(self.media_dir):
|
|
1213
|
+
shutil.rmtree(self.media_dir)
|
|
1008
1214
|
# Setup modular components now that we know the platform and media status
|
|
1009
1215
|
self.setup_modular_components()
|
|
1010
1216
|
|
|
@@ -1047,7 +1253,7 @@ class ChatExport:
|
|
|
1047
1253
|
# Render messages using the new HTMLRenderer
|
|
1048
1254
|
attachments_to_extract = self.renderer.render(chat)
|
|
1049
1255
|
|
|
1050
|
-
if self.has_media:
|
|
1256
|
+
if self.has_media and not self.embed_media:
|
|
1051
1257
|
print("Extracting attachments/media...")
|
|
1052
1258
|
# extract attachments of rendered messages
|
|
1053
1259
|
with zipfile.ZipFile(self.zip_path, 'r') as zip_ref:
|
|
@@ -1055,6 +1261,8 @@ class ChatExport:
|
|
|
1055
1261
|
for file in zip_ref.namelist():
|
|
1056
1262
|
if file in attachments_to_extract:
|
|
1057
1263
|
zip_ref.extract(file, self.media_dir)
|
|
1264
|
+
elif self.has_media and self.embed_media:
|
|
1265
|
+
print("Media will be embedded as base64 in HTML (no file extraction needed)")
|
|
1058
1266
|
processing_end_time = time.time()
|
|
1059
1267
|
print(f"Processing took {processing_end_time - processing_start_time:.3f} seconds")
|
|
1060
1268
|
|
|
@@ -1081,13 +1289,16 @@ class ChatExport:
|
|
|
1081
1289
|
# Get the base name of the zip file without extension
|
|
1082
1290
|
zip_base_name = Path(self.zip_path).stem
|
|
1083
1291
|
|
|
1084
|
-
if
|
|
1292
|
+
# safety: clean only if it's a subfolder with zip_base_name
|
|
1293
|
+
if os.path.exists(self.output_dir) and str(self.output_dir).endswith(zip_base_name):
|
|
1085
1294
|
print(f"Cleaning existing directory: {self.output_dir}")
|
|
1086
1295
|
shutil.rmtree(self.output_dir)
|
|
1296
|
+
|
|
1297
|
+
os.makedirs(self.output_dir, exist_ok=True)
|
|
1087
1298
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1299
|
+
if not self.embed_media:
|
|
1300
|
+
# Create fresh output directories
|
|
1301
|
+
os.makedirs(self.media_dir)
|
|
1091
1302
|
|
|
1092
1303
|
# Validate that it's a proper ZIP file
|
|
1093
1304
|
try:
|
|
@@ -1120,7 +1331,10 @@ class ChatExport:
|
|
|
1120
1331
|
print(f"ZIP file is an {'iOS' if self.is_ios else 'Android'} export with media/attachments, '{chat_file}' is the chat text file.")
|
|
1121
1332
|
else:
|
|
1122
1333
|
print(f"ZIP file is an {'iOS' if self.is_ios else 'Android'} export without media/attachments, '{chat_file}' is the chat text file.")
|
|
1123
|
-
|
|
1334
|
+
# delete if exists
|
|
1335
|
+
if os.path.exists(self.media_dir):
|
|
1336
|
+
shutil.rmtree(self.media_dir)
|
|
1337
|
+
|
|
1124
1338
|
|
|
1125
1339
|
# Setup modular components now that we know the platform and media status
|
|
1126
1340
|
self.setup_modular_components()
|
|
@@ -1150,7 +1364,7 @@ class ChatExport:
|
|
|
1150
1364
|
# Render messages using the new HTMLRenderer
|
|
1151
1365
|
attachments_to_extract = self.renderer.render(chat)
|
|
1152
1366
|
|
|
1153
|
-
if self.has_media:
|
|
1367
|
+
if self.has_media and not self.embed_media:
|
|
1154
1368
|
print("Extracting attachments/media...")
|
|
1155
1369
|
# extract attachments of rendered messages
|
|
1156
1370
|
with zipfile.ZipFile(self.zip_path, 'r') as zip_ref:
|
|
@@ -1158,6 +1372,8 @@ class ChatExport:
|
|
|
1158
1372
|
for file in zip_ref.namelist():
|
|
1159
1373
|
if file in attachments_to_extract:
|
|
1160
1374
|
zip_ref.extract(file, self.media_dir)
|
|
1375
|
+
elif self.has_media and self.embed_media:
|
|
1376
|
+
print("Media will be embedded as base64 in HTML (no file extraction needed)")
|
|
1161
1377
|
processing_end_time = time.time()
|
|
1162
1378
|
print(f"Processing took {processing_end_time - processing_start_time:.3f} seconds")
|
|
1163
1379
|
|
|
@@ -1170,7 +1386,7 @@ def check_tkinter_availability():
|
|
|
1170
1386
|
root = tk.Tk()
|
|
1171
1387
|
root.destroy()
|
|
1172
1388
|
return True
|
|
1173
|
-
except Exception
|
|
1389
|
+
except Exception:
|
|
1174
1390
|
print("Tkinter is not available on your system. Using prompt input instead of file picker dialog.")
|
|
1175
1391
|
return False
|
|
1176
1392
|
|
|
@@ -1225,7 +1441,7 @@ def main():
|
|
|
1225
1441
|
from_date = args.from_date if args.from_date else None
|
|
1226
1442
|
until_date = args.until_date if args.until_date else None
|
|
1227
1443
|
|
|
1228
|
-
chat_export = ChatExport(args.zip_file, from_date, until_date, args.participant, args.output_dir)
|
|
1444
|
+
chat_export = ChatExport(args.zip_file, from_date, until_date, args.participant, args.output_dir, args.embed_media)
|
|
1229
1445
|
chat_export.process_chat_non_interactive()
|
|
1230
1446
|
print(f'Written: {", ".join([str(p.absolute()) for p in chat_export.renderer.get_generated_files()])}')
|
|
1231
1447
|
print("Done.")
|
|
@@ -1253,7 +1469,7 @@ def main():
|
|
|
1253
1469
|
if not selected_zip_file:
|
|
1254
1470
|
raise FileNotFoundError("No file selected.")
|
|
1255
1471
|
print(f"Processing selected file: {selected_zip_file}...")
|
|
1256
|
-
chat_export = ChatExport(selected_zip_file, base_output_dir=args.output_dir)
|
|
1472
|
+
chat_export = ChatExport(selected_zip_file, base_output_dir=args.output_dir, embed_media=args.embed_media)
|
|
1257
1473
|
chat_export.process_chat()
|
|
1258
1474
|
print(f'Written: {", ".join([str(p.absolute()) for p in chat_export.renderer.get_generated_files()])}')
|
|
1259
1475
|
print("Done.")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|