Skip to content

Commit 86d74f7

Browse files
authored
Merge pull request #29 from ileodo/feature/feed
[Add] Support downloading pictures from the posts in feed (only liked ones)
2 parents c7f83ea + 079a77e commit 86d74f7

File tree

5 files changed

+129
-7
lines changed

5 files changed

+129
-7
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,15 @@ Options:
8282
FAMLY_PASSWORD env var
8383
--access-token TOKEN Your famly.co access token, can be set via
8484
FAMLY_ACCESS_TOKEN env var
85+
--famly-base-url URL Your famly.co instance baseurl (default:
86+
https://app.famly.co), can be set via
87+
FAMLY_BASE_URL env var
8588
--no-tagged Don't download tagged images
8689
-j, --journey Download images from child Learning Journey
8790
-n, --notes Download images from child notes
8891
-m, --messages Download images from messages
92+
-l, --liked Download images which is liked by the
93+
parents from all posts (in the feed)
8994
-p, --pictures-folder DIRECTORY
9095
Directory to save downloaded pictures, can
9196
be set via FAMLY_PICTURES_FOLDER env var
@@ -94,7 +99,7 @@ Options:
9499
file is encountered
95100
-u, --user-agent User Agent used in Famly requests, can be
96101
set via FAMLY_USER_AGENT env var [default:
97-
famly-fetch/0.2.0]
102+
famly-fetch/0.4.0]
98103
--latitude LAT Latitude for EXIF GPS data, can be set via
99104
LATITUDE env var
100105
--longitude LONG Longitude for EXIF GPS data, can be set via

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "famly-fetch"
7-
version = "0.3.0"
7+
version = "0.4.0"
88
authors = [
99
{ name = "Jacob Bunk Nielsen" },
1010
{ name = "Morten Siebuhr" },
11-
{ name = "iLeoDo" },
12-
{ name = "Tom Vincent" },
11+
{ name = "Tianwei Dong", email = "iLeoDo@Gmail.com" },
12+
{ name = "Tom Vincent" },
1313
]
1414
description = "Fetch your (kid's) images from famly.co"
1515
license-files = ["LICENSE"]

src/famly_fetch/api_client.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@ def get_device_id() -> str:
2424

2525
class ApiClient:
2626
_access_token = None
27-
_base = "https://app.famly.co"
2827

29-
def __init__(self, user_agent: str | None = None, access_token: str | None = None):
28+
def __init__(
29+
self,
30+
base_url: str,
31+
user_agent: str | None = None,
32+
access_token: str | None = None,
33+
):
3034
"""
3135
Initialize the ApiClient.
3236
@@ -37,6 +41,7 @@ def __init__(self, user_agent: str | None = None, access_token: str | None = Non
3741
self._user_agent: str | None = user_agent
3842
self._device_id = get_device_id()
3943
self._access_token = access_token
44+
self._base = base_url
4045

4146
def login(self, email, password):
4247
"""
@@ -159,6 +164,21 @@ def make_api_request(self, method, path, body=None, params=None):
159164
print("Error code: ", e.code)
160165
print("Response body: ", e.read())
161166

167+
def feed(
168+
self,
169+
cursor: str | None = None,
170+
older_than: str | None = None,
171+
limit: int | None = None,
172+
):
173+
params = {}
174+
if cursor:
175+
params["cursor"] = cursor
176+
if older_than:
177+
params["olderThan"] = older_than
178+
if limit:
179+
params["first"] = limit
180+
return self.make_api_request("GET", "/api/feed/feed/feed", params=params)
181+
162182
def me_me_me(self):
163183
"""
164184
Get information about the currently authenticated user.
@@ -172,3 +192,17 @@ def me_me_me(self):
172192
"""
173193

174194
return self.make_api_request("GET", "/api/me/me/me")
195+
196+
def get_relations(self, child_id: str) -> list[dict]:
197+
"""
198+
Get the relations of a given child ID.
199+
200+
Args:
201+
child_id (str): The ID of the child.
202+
203+
Returns:
204+
list[dict]: A list of dictionary representing the relations.
205+
"""
206+
return self.make_api_request(
207+
"GET", "/api/v2/relations", params={"childId": child_id}
208+
)

src/famly_fetch/cli.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,26 @@ def get_version():
3636
metavar="TOKEN",
3737
type=str,
3838
)
39+
@click.option(
40+
"--famly-base-url",
41+
envvar="FAMLY_BASE_URL",
42+
help="Your famly.co instance baseurl (default: https://app.famly.co), can be set via FAMLY_BASE_URL env var",
43+
metavar="URL",
44+
default="https://app.famly.co",
45+
type=str,
46+
)
3947
@click.option("--no-tagged", is_flag=True, help="Don't download tagged images")
4048
@click.option(
4149
"-j", "--journey", is_flag=True, help="Download images from child Learning Journey"
4250
)
4351
@click.option("-n", "--notes", is_flag=True, help="Download images from child notes")
4452
@click.option("-m", "--messages", is_flag=True, help="Download images from messages")
53+
@click.option(
54+
"-l",
55+
"--liked",
56+
is_flag=True,
57+
help="Download images which is liked by the parents from all posts (in the feed)",
58+
)
4559
@click.option(
4660
"-p",
4761
"--pictures-folder",
@@ -123,10 +137,12 @@ def main(
123137
email: str,
124138
password: str,
125139
access_token: str,
140+
famly_base_url: str,
126141
no_tagged: bool,
127142
journey: bool,
128143
notes: bool,
129144
messages: bool,
145+
liked: bool,
130146
pictures_folder: Path,
131147
stop_on_existing: bool,
132148
user_agent: str,
@@ -160,6 +176,7 @@ def main(
160176
famly_downloader = FamlyDownloader(
161177
email=email,
162178
password=password,
179+
famly_base_url=famly_base_url,
163180
pictures_folder=pictures_folder,
164181
stop_on_existing=stop_on_existing,
165182
text_comments=text_comments,
@@ -175,7 +192,9 @@ def main(
175192
famly_downloader.download_images_from_messages()
176193

177194
# Process each child
195+
parent_ids = set()
178196
for child_id, first_name in famly_downloader.get_all_children():
197+
parent_ids |= famly_downloader.get_parents_ids(child_id)
179198
if not no_tagged:
180199
famly_downloader.download_tagged_images(child_id, first_name)
181200
if journey:
@@ -184,6 +203,10 @@ def main(
184203
)
185204
if notes:
186205
famly_downloader.download_images_from_notes(child_id, first_name)
206+
207+
if liked:
208+
famly_downloader.download_images_from_feed(parent_ids)
209+
187210
except Exception as e:
188211
click.secho(f"An exception occurred: {e}", fg="red")
189212

src/famly_fetch/downloader.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def __init__(
3131
self,
3232
email: str,
3333
password: str,
34+
famly_base_url: str,
3435
pictures_folder: Path,
3536
stop_on_existing: bool,
3637
text_comments: bool,
@@ -52,7 +53,9 @@ def __init__(
5253
self.state_file = state_file
5354
self.downloaded_images = self.load_state()
5455

55-
self._apiClient = ApiClient(user_agent=user_agent, access_token=access_token)
56+
self._apiClient = ApiClient(
57+
base_url=famly_base_url, user_agent=user_agent, access_token=access_token
58+
)
5659
if not access_token:
5760
self._apiClient.login(email, password)
5861

@@ -88,6 +91,10 @@ def get_all_children(self):
8891

8992
return all_children
9093

94+
def get_parents_ids(self, child_id: str) -> set[str]:
95+
relations = self._apiClient.get_relations(child_id)
96+
return {x["loginId"] for x in relations if x["loginId"]}
97+
9198
def download_images_from_notes(self, child_id, first_name):
9299
click.secho(
93100
f"Downloading learning journey images for {first_name}...", fg="green"
@@ -254,6 +261,59 @@ def download_images_from_messages(self):
254261

255262
self.save_state()
256263

264+
def download_images_from_feed(self, liked_by_ids: set[str]):
265+
click.secho("Downloading liked images in posts...", fg="green")
266+
267+
cursor = None
268+
older_than = None
269+
while True:
270+
click.echo("Fetching next 10 Posts")
271+
response = self._apiClient.feed(
272+
cursor=cursor, older_than=older_than, limit=10
273+
)
274+
if not response["feedItems"]:
275+
break
276+
last_item = response["feedItems"][-1]
277+
cursor = last_item["feedItemId"]
278+
older_than = last_item["createdDate"]
279+
for feed_item in response["feedItems"]:
280+
if not feed_item["originatorId"].startswith("Post:"):
281+
# not a Post item
282+
continue
283+
create_date = feed_item["createdDate"]
284+
for img_dict in feed_item["images"]:
285+
if not (
286+
img_dict["liked"]
287+
or [
288+
like
289+
for like in img_dict["likes"]
290+
if like["loginId"] in liked_by_ids
291+
]
292+
):
293+
# not liked by parents
294+
continue
295+
img = Image.from_dict(
296+
img_dict,
297+
date_override=create_date,
298+
text_override=feed_item["body"] if self.text_comments else None,
299+
)
300+
click.echo(f" - image {img.img_id} from post at {create_date}")
301+
302+
if img.img_id in self.downloaded_images:
303+
click.secho(
304+
f"Image {img.img_id} already downloaded, {'stopping download' if self.stop_on_existing else 'skipping'}.",
305+
fg="yellow",
306+
)
307+
if self.stop_on_existing:
308+
return
309+
else:
310+
continue
311+
file_path = self.download_file_path(img, "post")
312+
self.fetch_image(img, file_path)
313+
self.mark_as_downloaded(img.img_id)
314+
315+
self.save_state()
316+
257317
def download_file_path(self, img: BaseImage, filename_prefix: str) -> Path:
258318
"""Generate the file path for the downloaded image."""
259319

0 commit comments

Comments
 (0)