diff --git a/README.md b/README.md
index 9999ce9d..0f9e74d9 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,15 @@
+> **This is `ghunt-fixed`, a fork of [mxrch/GHunt](https://github.com/mxrch/GHunt), modified on 2026-07-31.**
+> The upstream PyPI release (2.3.4) crashes with `KeyError: 'container'` on some accounts because
+> Google's People API responses now sometimes omit `metadata.container`. This fork patches
+> `ghunt/parsers/people.py` to handle that missing key gracefully instead of crashing
+> (see [upstream PR #593](https://github.com/mxrch/GHunt/pull/593), not yet merged/released as of this fork).
+> Still licensed under AGPLv3, same as upstream — see [LICENSE.md](LICENSE.md).
+
+
+
#### 🌐 GHunt Online version : https://osint.industries
#### 🐍 Now Python 3.13 compatible !
diff --git a/ghunt/parsers/people.py b/ghunt/parsers/people.py
index 43554121..99143bb0 100644
--- a/ghunt/parsers/people.py
+++ b/ghunt/parsers/people.py
@@ -60,7 +60,9 @@ async def _scrape(self, as_client: httpx.AsyncClient, photo_data: Dict[str, any]
self.isDefault, self.flathash = await is_default_profile_pic(as_client, self.url)
elif photo_type == "cover_photo":
- self.url = '='.join(photo_data.get("imageUrl").split("=")[:-1])
+ image_url = photo_data.get("imageUrl", "")
+ if image_url:
+ self.url = '='.join(image_url.split("=")[:-1])
if (isDefault := photo_data.get("isDefault")):
self.isDefault = isDefault
else:
@@ -107,8 +109,8 @@ def __init__(self):
def _scrape(self, apps_data, container_name: str):
for app in apps_data:
- if app["metadata"]["container"] == container_name:
- self.apps.append(app["appType"].title())
+ if app.get("metadata", {}).get("container") == container_name:
+ self.apps.append(app.get("appType", "").title())
class PersonContainers(dict):
pass
@@ -129,45 +131,61 @@ async def _scrape(self, as_client: httpx.AsyncClient, person_data: Dict[str, any
self.personId = person_data.get("personId")
if person_data.get("email"):
for email_data in person_data["email"]:
+ if not (container := email_data.get("metadata", {}).get("container")):
+ continue
person_email = PersonEmail()
person_email._scrape(email_data)
- self.emails[email_data["metadata"]["container"]] = person_email
+ self.emails[container] = person_email
if person_data.get("name"):
for name_data in person_data["name"]:
+ if not (container := name_data.get("metadata", {}).get("container")):
+ continue
person_name = PersonName()
person_name._scrape(name_data)
- self.names[name_data["metadata"]["container"]] = person_name
+ self.names[container] = person_name
if person_data.get("readOnlyProfileInfo"):
for profile_data in person_data["readOnlyProfileInfo"]:
+ if not (container := profile_data.get("metadata", {}).get("container")):
+ continue
person_profile = PersonProfileInfo()
person_profile._scrape(profile_data)
- self.profileInfos[profile_data["metadata"]["container"]] = person_profile
-
- if person_data.get("photo"):
- for photo_data in person_data["photo"]:
- person_photo = PersonPhoto()
- await person_photo._scrape(as_client, photo_data, "profile_photo")
- self.profilePhotos[profile_data["metadata"]["container"]] = person_photo
+ self.profileInfos[container] = person_profile
+
+ # Each photo carries its own container in its metadata, independent of
+ # readOnlyProfileInfo's containers, so it's keyed separately rather than
+ # nested in the loop above (that previously mis-attributed every photo
+ # to whichever profile container happened to be current in the outer loop).
+ if person_data.get("photo"):
+ for photo_data in person_data["photo"]:
+ if not (container := photo_data.get("metadata", {}).get("container")):
+ continue
+ person_photo = PersonPhoto()
+ await person_photo._scrape(as_client, photo_data, "profile_photo")
+ self.profilePhotos[container] = person_photo
if (source_ids := person_data.get("metadata", {}).get("identityInfo", {}).get("sourceIds")):
for source_ids_data in source_ids:
+ if not (container := source_ids_data.get("container")):
+ continue
person_source_ids = PersonSourceIds()
person_source_ids._scrape(source_ids_data)
- self.sourceIds[source_ids_data["container"]] = person_source_ids
+ self.sourceIds[container] = person_source_ids
if person_data.get("coverPhoto"):
for cover_photo_data in person_data["coverPhoto"]:
+ if not (container := cover_photo_data.get("metadata", {}).get("container")):
+ continue
person_cover_photo = PersonPhoto()
await person_cover_photo._scrape(as_client, cover_photo_data, "cover_photo")
- container = cover_photo_data.get("metadata", {}).get("container", "unknown")
self.coverPhotos[container] = person_cover_photo
if (apps_data := person_data.get("inAppReachability")):
containers_names = set()
for app_data in person_data["inAppReachability"]:
- containers_names.add(app_data["metadata"]["container"])
+ if (container := app_data.get("metadata", {}).get("container")):
+ containers_names.add(container)
for container_name in containers_names:
person_app_reachability = PersonInAppReachability()