From 7f0bd9f1d10e3d645a4d5691fdea57597643221f Mon Sep 17 00:00:00 2001 From: Oussema Frikha Date: Fri, 16 Oct 2026 12:00:30 +0400 Subject: [PATCH 1/2] feat: Facebook photo albums (v0.1.3) get_page_albums(profile_id), get_group_albums(group_id) and get_album_items(album_id), sync and async, backed by the three new album endpoints. The SDK now covers all 53 endpoints. --- CHANGELOG.md | 9 +++++ README.md | 7 +++- socialapis/_version.py | 2 +- socialapis/facebook/_client.py | 61 ++++++++++++++++++++++++++++++++++ tests/test_facebook.py | 34 +++++++++++++++++++ 5 files changed, 111 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ad789f..299ffd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.3] — 2026-10-16 + +### Added + +- **Facebook photo albums** (sync and async): `get_page_albums(profile_id)`, + `get_group_albums(group_id)` and `get_album_items(album_id)`, backed by + `/facebook/pages/albums`, `/facebook/groups/albums` and + `/facebook/albums/items`. The SDK now covers all 53 endpoints. + ## [0.1.2] — 2026-10-05 ### Fixed diff --git a/README.md b/README.md index 056e2b8..f6242da 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ This SDK is the **drop-in successor**: | **Pagination** | Generator with edge-case bugs | Cursor-based; API decides page size | | **Error handling** | Generic exceptions | Typed hierarchy (`RateLimitError`, etc.) | | **CI / tests** | Manual against live FB | Recorded HTTP fixtures, Python 3.10–3.13 | -| **Coverage** | Page posts, group posts only | **50 endpoints** across FB + IG | +| **Coverage** | Page posts, group posts only | **53 endpoints** across FB + IG | The trade-off: instead of running a scraper yourself, you make a REST call to our hosted API. **200 calls/month free**, no credit card. Paid plans start at $4.99/mo for 1,500 @@ -94,12 +94,17 @@ calls. - `get_page_posts(page)` — recent posts - `get_page_reels(page)` — short-form videos - `get_page_videos(page)` — long-form videos +- `get_page_albums(profile_id)` — photo albums (numeric page ID from `get_page_id`) **Groups** - `get_group_id(group)` - `get_group_details(group)` → `GroupInfo` (typed model) - `get_group_posts(group)` - `get_group_videos(group_id)` +- `get_group_albums(group_id)` — photo albums + +**Albums** +- `get_album_items(album_id)` — photos and videos in a page or group album **Posts** - `get_post_id(post)` — extract numeric ID from URL diff --git a/socialapis/_version.py b/socialapis/_version.py index 881e249..247510b 100644 --- a/socialapis/_version.py +++ b/socialapis/_version.py @@ -4,4 +4,4 @@ # this manually for a release — use `git tag vX.Y.Z` and the CI does the # rest via hatchling's dynamic-version feature (see pyproject.toml). -__version__ = "0.1.2" +__version__ = "0.1.3" diff --git a/socialapis/facebook/_client.py b/socialapis/facebook/_client.py index ff824ba..0e56f73 100644 --- a/socialapis/facebook/_client.py +++ b/socialapis/facebook/_client.py @@ -222,6 +222,19 @@ def get_page_videos(self, page: str, **kwargs: Any) -> dict[str, Any]: _params(("link", _as_facebook_url(page)), extra=kwargs), ).json() + def get_page_albums(self, profile_id: str, **kwargs: Any) -> dict[str, Any]: + """Return the photo albums of a Facebook Page. + + Backed by ``GET /facebook/pages/albums``. Takes the numeric page ID + (use :meth:`get_page_id` to resolve a URL first). Pass ``limit`` + (8-16) or ``end_cursor`` as keyword arguments. Use + :meth:`get_album_items` for the photos in one album. + """ + return self._get( + "/facebook/pages/albums", + _params(("profile_id", profile_id), extra=kwargs), + ).json() + # ======================================================================= # GROUPS # ======================================================================= @@ -285,6 +298,30 @@ def get_group_videos(self, group_id: str, **kwargs: Any) -> dict[str, Any]: _params(("group_id", group_id), extra=kwargs), ).json() + def get_group_albums(self, group_id: str, **kwargs: Any) -> dict[str, Any]: + """Return the photo albums of a public Facebook Group. + + Backed by ``GET /facebook/groups/albums``. Takes a numeric + ``group_id`` (use :meth:`get_group_id` to resolve a URL first). + Pass ``limit`` (8-16) as a keyword argument. + """ + return self._get( + "/facebook/groups/albums", + _params(("group_id", group_id), extra=kwargs), + ).json() + + def get_album_items(self, album_id: str, **kwargs: Any) -> dict[str, Any]: + """Return the photos and videos inside a Facebook album. + + Backed by ``GET /facebook/albums/items``. Works for page and group + albums; ``album_id`` comes from :meth:`get_page_albums` or + :meth:`get_group_albums`. Pass ``end_cursor`` for the next page. + """ + return self._get( + "/facebook/albums/items", + _params(("album_id", album_id), extra=kwargs), + ).json() + # ======================================================================= # POSTS # ======================================================================= @@ -700,6 +737,14 @@ async def get_page_videos(self, page: str, **kwargs: Any) -> dict[str, Any]: ) ).json() + async def get_page_albums(self, profile_id: str, **kwargs: Any) -> dict[str, Any]: + return ( + await self._get( + "/facebook/pages/albums", + _params(("profile_id", profile_id), extra=kwargs), + ) + ).json() + # ======================================================================= # GROUPS # ======================================================================= @@ -749,6 +794,22 @@ async def get_group_videos(self, group_id: str, **kwargs: Any) -> dict[str, Any] ) ).json() + async def get_group_albums(self, group_id: str, **kwargs: Any) -> dict[str, Any]: + return ( + await self._get( + "/facebook/groups/albums", + _params(("group_id", group_id), extra=kwargs), + ) + ).json() + + async def get_album_items(self, album_id: str, **kwargs: Any) -> dict[str, Any]: + return ( + await self._get( + "/facebook/albums/items", + _params(("album_id", album_id), extra=kwargs), + ) + ).json() + # ======================================================================= # POSTS # ======================================================================= diff --git a/tests/test_facebook.py b/tests/test_facebook.py index efa641d..f275940 100644 --- a/tests/test_facebook.py +++ b/tests/test_facebook.py @@ -307,3 +307,37 @@ async def test_async_search_marketplace_works() -> None: async with AsyncFacebook(api_token="t") as fb: result = await fb.search_marketplace("cars") assert result == {"listings": []} + + +@respx.mock +def test_get_page_albums_passes_profile_id_and_limit() -> None: + route = respx.get("https://api.socialapis.io/facebook/pages/albums").mock( + return_value=httpx.Response(200, json={"albums": []}) + ) + with Facebook(api_token="t") as fb: + fb.get_page_albums("100044395330646", limit=16) + params = route.calls.last.request.url.params + assert params["profile_id"] == "100044395330646" + assert params["limit"] == "16" + + +@respx.mock +def test_get_group_albums_passes_group_id() -> None: + route = respx.get("https://api.socialapis.io/facebook/groups/albums").mock( + return_value=httpx.Response(200, json={"albums": []}) + ) + with Facebook(api_token="t") as fb: + fb.get_group_albums("187988788687356") + assert route.calls.last.request.url.params["group_id"] == "187988788687356" + + +@respx.mock +async def test_async_get_album_items_passes_album_id_and_cursor() -> None: + route = respx.get("https://api.socialapis.io/facebook/albums/items").mock( + return_value=httpx.Response(200, json={"items": []}) + ) + async with AsyncFacebook(api_token="t") as fb: + await fb.get_album_items("826298618859975", end_cursor="abc") + params = route.calls.last.request.url.params + assert params["album_id"] == "826298618859975" + assert params["end_cursor"] == "abc" From 2863b3793e338155764561a009a52ede64509679 Mon Sep 17 00:00:00 2001 From: Oussema Frikha Date: Fri, 16 Oct 2026 12:22:51 +0400 Subject: [PATCH 2/2] fix: get_group_videos sends link, as /facebook/groups/videos requires --- CHANGELOG.md | 7 +++++++ README.md | 2 +- socialapis/facebook/_client.py | 12 ++++++------ tests/test_facebook.py | 12 ++++++++++++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 299ffd0..8afc0aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `/facebook/pages/albums`, `/facebook/groups/albums` and `/facebook/albums/items`. The SDK now covers all 53 endpoints. +### Fixed + +- **`get_group_videos`** sent `group_id`, but `/facebook/groups/videos` + requires `link`, so calls failed. It now takes the group URL, slug or + numeric ID and sends `link` (positional calls keep working; the keyword + argument is renamed from `group_id` to `group`). + ## [0.1.2] — 2026-10-05 ### Fixed diff --git a/README.md b/README.md index f6242da..c52b890 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ calls. - `get_group_id(group)` - `get_group_details(group)` → `GroupInfo` (typed model) - `get_group_posts(group)` -- `get_group_videos(group_id)` +- `get_group_videos(group)` — URL, slug or numeric ID - `get_group_albums(group_id)` — photo albums **Albums** diff --git a/socialapis/facebook/_client.py b/socialapis/facebook/_client.py index 0e56f73..d74e32b 100644 --- a/socialapis/facebook/_client.py +++ b/socialapis/facebook/_client.py @@ -287,15 +287,15 @@ def get_group_posts(self, group: str, **kwargs: Any) -> dict[str, Any]: _params(("link", _as_facebook_group_url(group)), extra=kwargs), ).json() - def get_group_videos(self, group_id: str, **kwargs: Any) -> dict[str, Any]: + def get_group_videos(self, group: str, **kwargs: Any) -> dict[str, Any]: """Return videos posted to a Facebook Group. - Backed by ``GET /facebook/groups/videos``. Takes a numeric - ``group_id`` (use :meth:`get_group_id` to resolve a URL first). + Backed by ``GET /facebook/groups/videos``. ``group`` can be the group + URL, slug or numeric ID. Pass ``end_cursor`` for the next page. """ return self._get( "/facebook/groups/videos", - _params(("group_id", group_id), extra=kwargs), + _params(("link", _as_facebook_group_url(group)), extra=kwargs), ).json() def get_group_albums(self, group_id: str, **kwargs: Any) -> dict[str, Any]: @@ -786,11 +786,11 @@ async def get_group_posts(self, group: str, **kwargs: Any) -> dict[str, Any]: ) ).json() - async def get_group_videos(self, group_id: str, **kwargs: Any) -> dict[str, Any]: + async def get_group_videos(self, group: str, **kwargs: Any) -> dict[str, Any]: return ( await self._get( "/facebook/groups/videos", - _params(("group_id", group_id), extra=kwargs), + _params(("link", _as_facebook_group_url(group)), extra=kwargs), ) ).json() diff --git a/tests/test_facebook.py b/tests/test_facebook.py index f275940..d23f107 100644 --- a/tests/test_facebook.py +++ b/tests/test_facebook.py @@ -341,3 +341,15 @@ async def test_async_get_album_items_passes_album_id_and_cursor() -> None: params = route.calls.last.request.url.params assert params["album_id"] == "826298618859975" assert params["end_cursor"] == "abc" + + +@respx.mock +def test_get_group_videos_sends_link() -> None: + route = respx.get("https://api.socialapis.io/facebook/groups/videos").mock( + return_value=httpx.Response(200, json={"videos": []}) + ) + with Facebook(api_token="t") as fb: + fb.get_group_videos("187988788687356") + params = route.calls.last.request.url.params + assert params["link"] == "https://www.facebook.com/groups/187988788687356" + assert "group_id" not in params