Skip to content

Commit 1e88a18

Browse files
authored
Merge pull request #5 from auanasgheps/dev
Dev: bugfixes and enhancements (0.34)
2 parents 7b9cb0f + ef76df6 commit 1e88a18

6 files changed

Lines changed: 228 additions & 31 deletions

File tree

‎.github/workflows/tests.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ name: Tests
22

33
on:
44
push:
5+
paths:
6+
- '**.py'
7+
- '.github/workflows/tests.yml'
58
pull_request:
9+
paths:
10+
- '**.py'
11+
- '.github/workflows/tests.yml'
612

713
jobs:
814
test:

‎README.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
## Overview
55

6-
WhatsApp Media Archiver organises your WhatsApp media into a structured folder hierarchy using metadata from the WhatsApp database (`msgstore.db`). Instead of an unstructured dump, you get a browsable archive sorted by contact or group, year, and direction (Sent/Received), with original message timestamps preserved.
6+
WhatsApp Media Archiver organises your WhatsApp media into a structured folder hierarchy using metadata from the WhatsApp database.
7+
Instead of an unstructured dump, you get a browsable archive sorted by contact or group, year, and direction (Sent/Received), with original message timestamps preserved.
78

89
> ⚠️ **This tool is designed to run on a backup copy of your WhatsApp data — never on live device files.** For safeguard reasons, this script will always make a copy of your data.
910
@@ -21,8 +22,10 @@ WhatsApp Media Archiver organises your WhatsApp media into a structured folder h
2122
- Duplicate media detection across runs — CSV report of files with identical content at multiple archive paths
2223
- Missing media CSV report for manual recovery of old or deleted files
2324
- Dry run mode for safe previewing before a full run
24-
- iOS support — reads directly from an iPhone backup (`--ios_backup`); no third-party extraction tool required
25-
- Encrypted backup support (`msgstore.db.crypt15`) via `wa-crypt-tools`
25+
- Windows, Linux and MacOS are supported to run the script.
26+
- WhatsApp platform: Android and iOS are both supported. No root or jailbreak are required.
27+
- Android: requires a manual copy of your `/Whatsapp/Media/` folder
28+
- iOS: reads directly from an iPhone backup, encrypted backups are supported via `wa-crypt-tools`
2629
- Restore mode — reconstructs the original `WhatsApp/Media/` folder structure from the archive (Android only)
2730

2831
### Supported Media Types
@@ -122,7 +125,7 @@ The script is **safe to re-run** on an existing archive:
122125
- Stickers are not archived in this version.
123126
- **Year folders reflect the local time of the machine running the script**, not UTC. A message sent just after midnight on 1 January will be filed under the new year only if your machine's clock agrees. This is intentional — the archive reflects your local experience of when media was shared.
124127
- **iOS restore mode is not supported.** Restore mode reconstructs the Android `Media/` folder layout, which has no equivalent on iOS. Running `--mode restore` on an iOS archive exits with a clear error.
125-
- **iOS number change tracking is best-effort.** Contacts present in the device address book are consolidated automatically. Contacts not saved to the address book (`ZCONTACTABID = NULL`) appear as separate folders.
128+
- **iOS number change tracking is best-effort.** Contacts present in the device address book are consolidated automatically. Contacts not saved to the address book appear as separate folders.
126129

127130
---
128131

‎docs/prerequisites.md‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,21 @@ Ensure Python 3.10+ is installed.
77

88
This script relies on some packages for Android and iOS.
99

10-
Install the relevant package before your first run.
10+
Install the relevant package before your first run. If you miss any of these, the script will notify you.
1111

12-
For Android encrypted backup decryption, the script uses `wa-crypt-tools`. You are going to use it if you don't have a rooted device.
12+
13+
14+
**Android encrypted backup decryption**
15+
16+
The script uses `wa-crypt-tools`. You are going to use it if you don't have a rooted device.
1317

1418
```bash
1519
pip install wa-crypt-tools
1620
```
1721

18-
For iOS encrypted backup decryption, the script uses `iphone-backup-decrypt`. You are going to use it if you create password protected iOS backups.
22+
**iOS encrypted backup decryption**
23+
24+
The script uses `iphone-backup-decrypt`. You are going to use it if you create password protected iOS backups.
1925

2026
```bash
2127
pip install iphone-backup-decrypt
@@ -24,6 +30,15 @@ pip install iphone-backup-decrypt
2430
> 💡 Windows Users: your Python environment may not have pip in the PATH. If `pip install` doesn't work for you, `use py -m pip install`.
2531
2632

33+
**Timezone feature for Windows**
34+
35+
If you plan to use the `--timezone` argument, Windows requires one additional package. macOS and Linux ship the IANA timezone database as part of the OS; Windows does not.
36+
> ```
37+
> pip install tzdata
38+
> ```
39+
> This is only needed when `--timezone` is used. Normal runs without `--timezone` work on Windows without it.
40+
41+
2742
---
2843
2944
## 2. WhatsApp E2E Encrypted Backup
@@ -53,7 +68,7 @@ When asked, create the end-to-end ecrypted backup.
5368
### Your WhatsApp Media Folder
5469
5570
Before you run the script, you need your WhatsApp (Media) folder on disk - actually, the folder called `WhatsApp` that **contains** the `Media` subfolder.
56-
You need to copy this folder from your phone: you can use [Syncthing](https://syncthing.net/) to ease the process.
71+
You need to copy this folder from your phone: you can use [Syncthing](https://syncthing.net/) to smooth the process.
5772
5873
```
5974
/path/to/WhatsApp/

‎docs/running-the-script.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ usage: wa_media_archiver.py [-h]
1919
[--dry-run]
2020
[--limit N]
2121
[--since DATE]
22+
[--timezone TZ]
2223
```
2324

2425
| Argument | Required | Description |
@@ -37,6 +38,7 @@ usage: wa_media_archiver.py [-h]
3738
| `--dry-run` | No | Simulate the run without copying any files |
3839
| `--limit N` | No | Cap rows returned per chat type (N/2 from groups, N/2 from 1-to-1). Total rows ≤ N. Useful for test runs |
3940
| `--since DATE` | No | Only include messages on or after this date (`YYYY-MM-DD`). Combines freely with `--limit` |
41+
| `--timezone TZ` | No | [IANA timezone name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) for year folder assignment and report timestamps (e.g. `Europe/Rome`, `America/New_York`, `UTC`). Ensures files land in the correct `<YYYY>/` subfolder regardless of where or when the script is run. Also applied to `--since` date interpretation. Defaults to machine local time. **Windows users: requires `pip install tzdata`** |
4042

4143
---
4244

@@ -173,7 +175,7 @@ python3 wa_media_archiver.py \
173175

174176
Restore mode reconstructs the flat `WhatsApp/Media/` folder structure directly inside the archive folder, without needing the original device or database. This is useful when re-importing media into tools that expect the original WhatsApp layout.
175177

176-
> ⚠️ **Android archives only.** iOS media is stored at `Message/Media/...` paths that have no equivalent reconstruction target outside the iPhone backup format. Running restore mode on an iOS archive exits with a clear error.
178+
> ⚠️ **Android archives only.** iOS media is stored at `Message/Media/...` paths that have no equivalent reconstruction target outside the iPhone backup format. Running restore mode on an iOS archive is disallowed.
177179
178180
The reconstructed tree is written to `<output>/Media/`, alongside the existing `Contacts/` and `Groups/` folders. No files are overwritten — identical files already in place are skipped silently.
179181

‎tests/test_wa_media_archiver.py‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,36 @@ def test_known_timestamp(self):
148148
year = wa.get_year(ts_ms)
149149
assert year == "2024"
150150

151+
def test_utc_timezone(self):
152+
pytest.importorskip('zoneinfo', reason="zoneinfo unavailable")
153+
from zoneinfo import ZoneInfo
154+
try:
155+
tz = ZoneInfo('UTC')
156+
except Exception:
157+
pytest.skip("tzdata not installed")
158+
# 2024-12-31 23:30:00 UTC → still 2024 in UTC
159+
ts_ms = 1735688200000 # 2024-12-31 23:36:40 UTC
160+
assert wa.get_year(ts_ms, tz=tz) == "2024"
161+
162+
def test_timezone_crosses_year_boundary(self):
163+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
164+
try:
165+
rome = ZoneInfo('Europe/Rome')
166+
utc = ZoneInfo('UTC')
167+
except ZoneInfoNotFoundError:
168+
pytest.skip("tzdata not installed (required on Windows)")
169+
# 2024-12-31 23:30:00 UTC = 2025-01-01 00:30:00 in UTC+1 (Europe/Rome in winter)
170+
ts_ms = 1735687800000 # 2024-12-31 23:30:00 UTC exactly
171+
assert wa.get_year(ts_ms, tz=rome) == "2025"
172+
assert wa.get_year(ts_ms, tz=utc) == "2024"
173+
174+
def test_no_tz_returns_string(self):
175+
# Without tz, returns a 4-digit year string (value depends on local time, just check format)
176+
ts_ms = 1705276800000
177+
year = wa.get_year(ts_ms)
178+
assert len(year) == 4
179+
assert year.isdigit()
180+
151181

152182
class TestAppendSenderToFilename:
153183
def test_normal_case(self):
@@ -1869,3 +1899,56 @@ def test_failure_raises(self, logger, tmp_path):
18691899
side_effect=subprocess.CalledProcessError(1, 'adb', stderr=b"fail")):
18701900
with pytest.raises(subprocess.CalledProcessError):
18711901
adb.pull_contacts(str(tmp_path), logger=logger)
1902+
1903+
1904+
# ---------------------------------------------------------------------------
1905+
# TestCheckDependencies
1906+
# ---------------------------------------------------------------------------
1907+
1908+
class TestCheckDependencies:
1909+
def _args(self, **kwargs):
1910+
"""Return a minimal namespace; only set the fields under test."""
1911+
defaults = dict(mode=None, e2e_key=None, ios_password=None)
1912+
defaults.update(kwargs)
1913+
import argparse
1914+
return argparse.Namespace(**defaults)
1915+
1916+
def test_no_issues_passes_silently(self, logger):
1917+
args = self._args()
1918+
wa.check_dependencies(args, logger) # must not raise
1919+
1920+
def test_adb_missing_raises(self, logger):
1921+
args = self._args(mode='adb')
1922+
with patch("wa_media_archiver.shutil.which", return_value=None):
1923+
with pytest.raises(SystemExit):
1924+
wa.check_dependencies(args, logger)
1925+
1926+
def test_adb_present_passes(self, logger):
1927+
args = self._args(mode='adb')
1928+
with patch("wa_media_archiver.shutil.which", return_value="/usr/bin/adb"):
1929+
wa.check_dependencies(args, logger) # must not raise
1930+
1931+
def test_wa_crypt_tools_missing_raises(self, logger):
1932+
args = self._args(e2e_key='key.bin')
1933+
with patch("importlib.util.find_spec", return_value=None):
1934+
with pytest.raises(SystemExit):
1935+
wa.check_dependencies(args, logger)
1936+
1937+
def test_iphone_backup_decrypt_missing_raises(self, logger):
1938+
args = self._args(ios_password='secret')
1939+
with patch("importlib.util.find_spec", return_value=None):
1940+
with pytest.raises(SystemExit):
1941+
wa.check_dependencies(args, logger)
1942+
1943+
def test_multiple_missing_reported_together(self, logger, caplog):
1944+
args = self._args(mode='adb', e2e_key='key.bin')
1945+
# Capture what logger.error receives by inspecting the call
1946+
errors = []
1947+
logger.error = lambda msg, *a, **kw: errors.append(msg)
1948+
with patch("wa_media_archiver.shutil.which", return_value=None), \
1949+
patch("importlib.util.find_spec", return_value=None), \
1950+
pytest.raises(SystemExit):
1951+
wa.check_dependencies(args, logger)
1952+
assert len(errors) == 1, "Expected a single combined error message"
1953+
assert "adb" in errors[0]
1954+
assert "wa-crypt-tools" in errors[0]

0 commit comments

Comments
 (0)