Skip to content

Commit 0dd5ad5

Browse files
committed
Typing, timestamps, enforce Py 3.10+; Closes #11
1 parent cb36990 commit 0dd5ad5

File tree

7 files changed

+94
-53
lines changed

7 files changed

+94
-53
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# snapmap-archiver
22

3-
A tool written in Python to download all Snapmaps content from a specific location.
3+
A tool written in Python **3.10** to download all Snapmaps content from a specific location.
4+
5+
## Install Python 3.10+
6+
7+
Be sure to check that you're using a version of Python that is 3.10 or above. **This project will not work on Python 3.9 or below!**
48

59
![snapmap-archiver splash](/.github/img/Splash.png)
610

@@ -21,6 +25,7 @@ pip install -r requirements.txt
2125
```sh
2226
snapmap-archiver -o [OUTPUT DIR] -l="[LATITUDE],[LONGITUDE]"
2327
```
28+
2429
Unfortunately you have to use the arbitrary `-l="lat,lon"` rather than just `-l "lat,lon"` when parsing negative numbers as `argsparse` interprets said numbers as extra arguments.
2530

2631
### Optional Arguments
@@ -42,7 +47,7 @@ With `-f` or `--file`, you can specify a file containing a list of line-separate
4247
E.g
4348

4449
```sh
45-
snapmap-archiver -o ~/Desktop/snaps -t ~/Desktop/snaps.txt
50+
snapmap-archiver -o ~/Desktop/snaps -f ~/Desktop/snaps.txt
4651
```
4752

4853
Inside `snaps.txt`:

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[tool.poetry]
22
name = "snapmap-archiver"
3-
version = "2.0.2"
3+
version = "2.1.0"
44
description = "Download all Snapmaps content from a specific location."
55
readme = "README.md"
6-
authors = ["king-millez <[email protected]>"]
6+
authors = ["Miles Greenwark <[email protected]>"]
77
license = "GPL-3.0-or-later"
88

99
[tool.poetry.dependencies]
10-
python = "^3.9"
10+
python = "^3.10"
1111
requests = "^2.25.1"
1212

1313
[tool.poetry.dev-dependencies]

setup.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,26 @@
44
import re
55
from setuptools import setup
66

7-
version = '2.0.2'
7+
version = "2.1.0"
88
with open("README.md", "r") as f:
99
long_descr = f.read()
1010

1111
setup(
12-
name = "snapmap-archiver",
13-
packages = ["snapmap_archiver"],
14-
entry_points = {
15-
"console_scripts": ['snapmap-archiver = snapmap_archiver:main']
16-
},
17-
version = version,
18-
description = "Download all Snapmaps content from a specific location.",
19-
long_description = long_descr,
20-
author = "Miles Greenwark",
21-
author_email = "[email protected]",
22-
url = "https://github.com/king-millez/snapmap-archiver",
12+
name="snapmap-archiver",
13+
packages=["snapmap_archiver"],
14+
entry_points={"console_scripts": ["snapmap-archiver = snapmap_archiver:main"]},
15+
version=version,
16+
description="Download all Snapmaps content from a specific location.",
17+
long_description=long_descr,
18+
author="Miles Greenwark",
19+
author_email="[email protected]",
20+
url="https://github.com/king-millez/snapmap-archiver",
2321
python_requires=">=3.10",
2422
install_requires=[
2523
"certifi",
2624
"chardet",
2725
"idna",
2826
"requests",
2927
"urllib3",
30-
]
31-
)
28+
],
29+
)

snapmap_archiver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import snapmap_archiver
22

33

4-
if __name__ == '__main__':
4+
if __name__ == "__main__":
55
snapmap_archiver.main()

snapmap_archiver/SnapmapArchiver.py

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
11
import os
22
import re
3+
import sys
34
import json
45
import requests
56
from time import sleep
6-
from typing import Iterable
7+
from typing import Iterable, Any
8+
from datetime import datetime
79

8-
from snapmap_archiver.Coordinates import Coordinates
9-
from snapmap_archiver.snap import Snap
10-
11-
12-
MAX_RADIUS = 85_000
13-
ISSUES_URL = "https://github.com/king-millez/snapmap-archiver/issues/new/choose"
10+
from snapmap_archiver.coordinates import Coordinates
11+
from snapmap_archiver.snap import Snap, SnapJSONEncoder
1412

1513

1614
class SnapmapArchiver:
15+
MAX_RADIUS = 85_000
16+
ISSUES_URL = "https://github.com/king-millez/snapmap-archiver/issues/new/choose"
17+
SNAP_PATTERN = re.compile(
18+
r"(?:https?:\/\/map\.snapchat\.com\/ttp\/snap\/)?(W7_(?:[aA-zZ0-9\-_\+]{22})(?:[aA-zZ0-9-_\+]{28})AAAAA[AQ])(?:\/?@-?[0-9]{1,3}\.?[0-9]{0,},-?[0-9]{1,3}\.?[0-9]{0,}(?:,[0-9]{1,3}\.?[0-9]{0,}z))?"
19+
)
20+
1721
def __init__(self, *args, **kwargs) -> None:
22+
if sys.version_info < (3, 10):
23+
raise RuntimeError(
24+
"Python 3.10 or above is required to use snapmap-archiver!"
25+
)
26+
1827
self.write_json = kwargs.get("write_json")
19-
self.all_snaps: dict[str, dict[str, str]] = {}
28+
self.all_snaps: dict[str, Snap] = {}
2029
self.arg_snaps = args
2130
self.coords_list = []
2231
self.radius = 10_000
@@ -27,7 +36,7 @@ def __init__(self, *args, **kwargs) -> None:
2736

2837
if not kwargs["locations"] and not args and not kwargs["input_file"]:
2938
raise ValueError(
30-
"Some sort of input is required; location (-l), input file (--file), and raw Snap IDs are all valid options."
39+
"Some sort of input is required; location (-l), input file (-f), and raw Snap IDs are all valid options."
3140
)
3241

3342
if not kwargs["output_dir"]:
@@ -42,7 +51,9 @@ def __init__(self, *args, **kwargs) -> None:
4251

4352
if kwargs.get("radius"):
4453
self.radius = (
45-
MAX_RADIUS if kwargs["radius"] > MAX_RADIUS else kwargs["radius"]
54+
self.MAX_RADIUS
55+
if kwargs["radius"] > self.MAX_RADIUS
56+
else kwargs["radius"]
4657
)
4758

4859
# Query provided coordinates for Snaps
@@ -55,7 +66,7 @@ def __init__(self, *args, **kwargs) -> None:
5566
if kwargs.get("input_file"):
5667
self.input_file = kwargs["input_file"]
5768

58-
def download_snaps(self, group: Iterable[Snap] | Snap):
69+
def download_snaps(self, group: list[Snap] | Snap):
5970
if isinstance(group, Snap):
6071
group = [group]
6172
for snap in group:
@@ -67,29 +78,31 @@ def download_snaps(self, group: Iterable[Snap] | Snap):
6778
f.write(requests.get(snap.url).content)
6879
print(f" - Downloaded {fpath}.")
6980

70-
def query_snaps(self, snaps: str | Iterable[str]) -> list[Snap | None]:
81+
def query_snaps(self, snaps: str | Iterable[str]) -> list[Snap]:
7182
if isinstance(snaps, str):
7283
snaps = [
7384
snaps
7485
] # The Snap query endpoint can take multiple IDs, so here we can query 1 or more snaps with ease.
7586
to_query = []
7687
for snap_id in snaps:
7788
rgx_match = re.search(
78-
r"(?:https?:\/\/map\.snapchat\.com\/ttp\/snap\/)?(W7_(?:[aA-zZ0-9\-_\+]{22})(?:[aA-zZ0-9-_\+]{28})AAAAA[AQ])(?:\/?@-?[0-9]{1,3}\.?[0-9]{0,},-?[0-9]{1,3}\.?[0-9]{0,}(?:,[0-9]{1,3}\.?[0-9]{0,}z))?",
89+
self.SNAP_PATTERN,
7990
snap_id,
8091
)
8192
if not rgx_match:
8293
print(f"{snap_id} is not a valid Snap URL or ID.")
8394
continue
8495
to_query.append(rgx_match.group(1))
8596
try:
86-
return [
87-
self._parse_snap(snap)
88-
for snap in requests.post(
89-
"https://ms.sc-jpl.com/web/getStoryElements",
90-
json={"snapIds": to_query},
91-
).json()["elements"]
92-
]
97+
retl = []
98+
for snap in requests.post(
99+
"https://ms.sc-jpl.com/web/getStoryElements",
100+
json={"snapIds": to_query},
101+
).json()["elements"]:
102+
s = self._parse_snap(snap)
103+
if s:
104+
retl.append(s)
105+
return retl
93106
except requests.exceptions.JSONDecodeError:
94107
return []
95108

@@ -155,7 +168,7 @@ def main(self):
155168
if self.input_file:
156169
if os.path.isfile(self.input_file):
157170
with open(self.input_file, "r") as f:
158-
to_format = f.read().split("\n")
171+
to_format = [ln for ln in f.read().split("\n") if ln.strip()]
159172
self.download_snaps(self.query_snaps(to_format))
160173
else:
161174
raise FileNotFoundError("Input file does not exist.")
@@ -164,30 +177,50 @@ def main(self):
164177
self.download_snaps(self.query_snaps(self.arg_snaps))
165178

166179
if self.write_json:
167-
with open(os.path.join(self.output_dir, "archive.json"), "w") as f:
168-
f.write(json.dumps(self._transform_index(self.all_snaps), indent=2))
180+
with open(
181+
os.path.join(
182+
self.output_dir, f"archive_{int(datetime.now().timestamp())}.json"
183+
),
184+
"w",
185+
) as f:
186+
f.write(
187+
json.dumps(
188+
self._transform_index(self.all_snaps),
189+
indent=2,
190+
cls=SnapJSONEncoder,
191+
)
192+
)
169193

170194
def _transform_index(self, index: dict[str, Snap]):
171195
return [v for v in index.values()]
172196

173-
def _parse_snap(self, snap: dict):
174-
data_dict = {"create_time": snap["timestamp"], "snap_id": snap["id"]}
197+
def _parse_snap(
198+
self,
199+
snap: dict[
200+
str, Any
201+
], # I don't like the Any type but this dict is so dynamic there isn't much point hinting it accurately.
202+
) -> Snap | None:
203+
data_dict = {
204+
"create_time": round(int(snap["timestamp"]) * 10**-3, 3),
205+
"snap_id": snap["id"],
206+
}
175207
if snap["snapInfo"].get("snapMediaType"):
176208
data_dict["file_type"] = "mp4"
177209
elif snap["snapInfo"].get("streamingMediaInfo"):
178210
data_dict["file_type"] = "jpg"
179211
else:
180212
print(
181-
f'**Unknown Snap type detected!**\n\tID: {snap["id"]}\n\tSnap data: {json.dumps(snap)}\nPlease report this at {ISSUES_URL}\n'
213+
f'**Unknown Snap type detected!**\n\tID: {snap["id"]}\n\tSnap data: {json.dumps(snap)}\nPlease report this at {self.ISSUES_URL}\n'
182214
)
183-
return
215+
return None
184216
url = snap["snapInfo"]["streamingMediaInfo"].get("mediaUrl")
185217
if not url:
186-
return
218+
return None
187219
data_dict["url"] = url
220+
s = Snap(**data_dict)
188221
if not self.all_snaps.get(snap["id"]):
189-
self.all_snaps[snap["id"]] = data_dict
190-
return Snap(**data_dict)
222+
self.all_snaps[snap["id"]] = s
223+
return s
191224

192225
def _get_epoch(self):
193226
epoch_endpoint = requests.post(
@@ -201,7 +234,7 @@ def _get_epoch(self):
201234
return entry["id"]["epoch"]
202235
else:
203236
raise self.MissingEpochError(
204-
f"The API epoch could not be obtained.\n\nPlease report this at {ISSUES_URL}"
237+
f"The API epoch could not be obtained.\n\nPlease report this at {self.ISSUES_URL}"
205238
)
206239

207240
class MissingEpochError(Exception):

snapmap_archiver/__main__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import snapmap_archiver
22

3-
43
if __name__ == "__main__":
54
snapmap_archiver.main()

snapmap_archiver/snap.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from dataclasses import dataclass
1+
import json
2+
from dataclasses import dataclass, asdict
23

34

45
@dataclass
@@ -7,3 +8,8 @@ class Snap:
78
url: str
89
create_time: int
910
file_type: str
11+
12+
13+
class SnapJSONEncoder(json.JSONEncoder):
14+
def default(self, o: Snap):
15+
return asdict(o)

0 commit comments

Comments
 (0)