Skip to content

Commit

Permalink
Fix downloading shared photos from shared album (#344)
Browse files Browse the repository at this point in the history
* Handle application/octet-stream content type

* Typos

* Fix download/thumbnail shared photos from shared albums

* Add tests

* Query shared albums not owned by user

* Fixes for pre-commit hooks

* Requested changes

* Review changes
  • Loading branch information
alexismarquis authored Aug 11, 2024
1 parent 704504a commit 1d83979
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 32 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ max-line-length = 80
max-complexity = 10
docstring-convention = google
per-file-ignores =
tests/*:S101
tests/*:S101,S105
tests/**/const_*.py:B950
src/synology_dsm/const.py:B950
83 changes: 57 additions & 26 deletions src/synology_dsm/api/photos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,26 @@ async def get_albums(
"""Get a list of all albums."""
albums: list[SynoPhotosAlbum] = []
raw_data = await self._dsm.get(
self.BROWSE_ALBUMS_API_KEY, "list", {"offset": offset, "limit": limit}
self.BROWSE_ALBUMS_API_KEY,
"list",
{"offset": offset, "limit": limit, "category": "normal_share_with_me"},
)
if not isinstance(raw_data, dict) or (data := raw_data.get("data")) is None:
return None

for album in data["list"]:
albums.append(
SynoPhotosAlbum(album["id"], album["name"], album["item_count"])
SynoPhotosAlbum(
album["id"],
album["name"],
album["item_count"],
album["passphrase"] if album["passphrase"] else None,
)
)
return albums

def _raw_data_to_items(
self, raw_data: bytes | dict | str
self, raw_data: bytes | dict | str, passphrase: str | None = None
) -> list[SynoPhotosItem] | None:
"""Parse the raw data response to a list of photo items."""
items: list[SynoPhotosItem] = []
Expand All @@ -62,6 +69,7 @@ def _raw_data_to_items(
item["additional"]["thumbnail"]["cache_key"],
size,
item["owner_user_id"] == 0,
passphrase,
)
)
return items
Expand All @@ -70,17 +78,22 @@ async def get_items_from_album(
self, album: SynoPhotosAlbum, offset: int = 0, limit: int = 100
) -> list[SynoPhotosItem] | None:
"""Get a list of all items from given album."""
params = {
"offset": offset,
"limit": limit,
"additional": '["thumbnail"]',
}
if album.passphrase:
params["passphrase"] = album.passphrase
else:
params["album_id"] = album.album_id

raw_data = await self._dsm.get(
self.BROWSE_ITEM_API_KEY,
"list",
{
"album_id": album.album_id,
"offset": offset,
"limit": limit,
"additional": '["thumbnail"]',
},
params,
)
return self._raw_data_to_items(raw_data)
return self._raw_data_to_items(raw_data, album.passphrase)

async def get_items_from_shared_space(
self, offset: int = 0, limit: int = 100
Expand Down Expand Up @@ -118,13 +131,19 @@ async def download_item(self, item: SynoPhotosItem) -> bytes | None:
download_api = self.DOWNLOAD_API_KEY
if item.is_shared:
download_api = self.DOWNLOAD_FOTOTEAM_API_KEY

params = {
"unit_id": f"[{item.item_id}]",
"cache_key": item.thumbnail_cache_key,
}

if item.passphrase:
params["passphrase"] = item.passphrase

raw_data = await self._dsm.get(
download_api,
"download",
{
"unit_id": f"[{item.item_id}]",
"cache_key": item.thumbnail_cache_key,
},
params,
)
if isinstance(raw_data, bytes):
return raw_data
Expand All @@ -135,15 +154,21 @@ async def download_item_thumbnail(self, item: SynoPhotosItem) -> bytes | None:
download_api = self.THUMBNAIL_API_KEY
if item.is_shared:
download_api = self.THUMBNAIL_FOTOTEAM_API_KEY

params = {
"id": item.item_id,
"cache_key": item.thumbnail_cache_key,
"size": item.thumbnail_size,
"type": "unit",
}

if item.passphrase:
params["passphrase"] = item.passphrase

raw_data = await self._dsm.get(
download_api,
"get",
{
"id": item.item_id,
"cache_key": item.thumbnail_cache_key,
"size": item.thumbnail_size,
"type": "unit",
},
params,
)
if isinstance(raw_data, bytes):
return raw_data
Expand All @@ -154,13 +179,19 @@ async def get_item_thumbnail_url(self, item: SynoPhotosItem) -> str:
download_api = self.THUMBNAIL_API_KEY
if item.is_shared:
download_api = self.THUMBNAIL_FOTOTEAM_API_KEY

params = {
"id": item.item_id,
"cache_key": item.thumbnail_cache_key,
"size": item.thumbnail_size,
"type": "unit",
}

if item.passphrase:
params["passphrase"] = item.passphrase

return await self._dsm.generate_url(
download_api,
"get",
{
"id": item.item_id,
"cache_key": item.thumbnail_cache_key,
"size": item.thumbnail_size,
"type": "unit",
},
params,
)
4 changes: 4 additions & 0 deletions src/synology_dsm/api/photos/model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Data models for Synology Photos Module."""

from __future__ import annotations

from dataclasses import dataclass


Expand All @@ -10,6 +12,7 @@ class SynoPhotosAlbum:
album_id: int
name: str
item_count: int
passphrase: str | None


@dataclass
Expand All @@ -23,3 +26,4 @@ class SynoPhotosItem:
thumbnail_cache_key: str
thumbnail_size: str
is_shared: bool
passphrase: str | None
5 changes: 4 additions & 1 deletion src/synology_dsm/synology_dsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,10 @@ async def _execute_request(
]:
return dict(await response.json(content_type=content_type))

if content_type.startswith("image"):
if (
content_type == "application/octet-stream"
or content_type.startswith("image")
):
return await response.read()

return await response.text()
Expand Down
6 changes: 5 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
DSM_7_FOTO_ALBUMS,
DSM_7_FOTO_ITEMS,
DSM_7_FOTO_ITEMS_SEARCHED,
DSM_7_FOTO_ITEMS_SHARED_ALBUM,
DSM_7_FOTO_SHARED_ITEMS,
)
from .const import (
Expand Down Expand Up @@ -285,7 +286,10 @@ async def _execute_request(self, method, url, params, **kwargs):
return DSM_7_FOTO_ALBUMS

if SynoPhotos.BROWSE_ITEM_API_KEY in url:
return DSM_7_FOTO_ITEMS
if "passphrase" in url:
return DSM_7_FOTO_ITEMS_SHARED_ALBUM
else:
return DSM_7_FOTO_ITEMS

if SynoPhotos.SEARCH_API_KEY in url:
return DSM_7_FOTO_ITEMS_SEARCHED
Expand Down
4 changes: 3 additions & 1 deletion tests/api_data/dsm_7/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""DSM 6 datas."""
"""DSM 7 datas."""

from .const_7_api_auth import (
DSM_7_AUTH_LOGIN,
Expand All @@ -15,6 +15,7 @@
DSM_7_FOTO_ALBUMS,
DSM_7_FOTO_ITEMS,
DSM_7_FOTO_ITEMS_SEARCHED,
DSM_7_FOTO_ITEMS_SHARED_ALBUM,
DSM_7_FOTO_SHARED_ITEMS,
)

Expand All @@ -29,6 +30,7 @@
"DSM_7_DSM_INFORMATION",
"DSM_7_FOTO_ALBUMS",
"DSM_7_FOTO_ITEMS",
"DSM_7_FOTO_ITEMS_SHARED_ALBUM",
"DSM_7_FOTO_ITEMS_SEARCHED",
"DSM_7_FOTO_SHARED_ITEMS",
]
2 changes: 1 addition & 1 deletion tests/api_data/dsm_7/core/const_7_core_external_usb.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""DSM 6 SYNO.Core.ExternalDevice.Storage.USB data."""
"""DSM 7 SYNO.Core.ExternalDevice.Storage.USB data."""

DSM_7_CORE_EXTERNAL_USB_DS1821_PLUS_NO_EXTERNAL_USB = {
"data": {"devices": []},
Expand Down
47 changes: 47 additions & 0 deletions tests/api_data/dsm_7/photos/const_7_photo.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@
"type": "normal",
"version": 195694,
},
{
"cant_migrate_condition": {},
"condition": {},
"create_time": 1718658534,
"end_time": 1719075481,
"freeze_album": False,
"id": 3,
"item_count": 1,
"name": "Album3",
"owner_user_id": 7,
"passphrase": "NiXlv1i2N",
"shared": False,
"sort_by": "default",
"sort_direction": "default",
"start_time": 1659724703,
"temporary_shared": False,
"type": "normal",
"version": 102886,
},
]
},
"success": True,
Expand Down Expand Up @@ -111,6 +130,34 @@
},
}

DSM_7_FOTO_ITEMS_SHARED_ALBUM = {
"success": True,
"data": {
"list": [
{
"id": 29807,
"filename": "20221115_185645.jpg",
"filesize": 2644859,
"time": 1668538602,
"indexed_time": 1668564550862,
"owner_user_id": 7,
"folder_id": 597,
"type": "photo",
"additional": {
"thumbnail": {
"m": "ready",
"xl": "ready",
"preview": "broken",
"sm": "ready",
"cache_key": "29810_1668560967",
"unit_id": 29807,
}
},
},
]
},
}

DSM_7_FOTO_SHARED_ITEMS = {
"success": True,
"data": {
Expand Down
32 changes: 31 additions & 1 deletion tests/test_synology_dsm_7.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,26 +179,39 @@ async def test_photos(self, dsm_7):
albums = await dsm_7.photos.get_albums()

assert albums
assert len(albums) == 2
assert len(albums) == 3
assert albums[0].album_id == 4
assert albums[0].name == "Album1"
assert albums[0].item_count == 3
assert albums[0].passphrase is None

assert albums[1].album_id == 1
assert albums[1].name == "Album2"
assert albums[1].item_count == 1
assert albums[1].passphrase is None

assert albums[2].album_id == 3
assert albums[2].name == "Album3"
assert albums[2].item_count == 1
assert albums[2].passphrase == "NiXlv1i2N"

items = await dsm_7.photos.get_items_from_album(albums[0])
assert items
assert len(items) == 3
assert items[0].file_name == "20221115_185642.jpg"
assert items[0].thumbnail_cache_key == "29807_1668560967"
assert items[0].thumbnail_size == "xl"
assert items[0].passphrase is None

assert items[1].file_name == "20221115_185643.jpg"
assert items[1].thumbnail_cache_key == "29808_1668560967"
assert items[1].thumbnail_size == "m"
assert items[1].passphrase is None

assert items[2].file_name == "20221115_185644.jpg"
assert items[2].thumbnail_cache_key == "29809_1668560967"
assert items[2].thumbnail_size == "sm"
assert items[2].passphrase is None

thumb_url = await dsm_7.photos.get_item_thumbnail_url(items[0])
assert thumb_url
Expand All @@ -218,6 +231,23 @@ async def test_photos(self, dsm_7):
"&_sid=session_id&SynoToken=Sy%C3%B10_T0k%E2%82%AC%C3%B1"
)

items = await dsm_7.photos.get_items_from_album(albums[2])
assert items
assert len(items) == 1
assert items[0].file_name == "20221115_185645.jpg"
assert items[0].thumbnail_cache_key == "29810_1668560967"
assert items[0].thumbnail_size == "xl"
assert items[0].passphrase == "NiXlv1i2N"

thumb_url = await dsm_7.photos.get_item_thumbnail_url(items[0])
assert thumb_url
assert thumb_url == (
"https://nas.mywebsite.me:443/webapi/entry.cgi?"
"id=29807&cache_key=29810_1668560967&size=xl&type=unit"
"&passphrase=NiXlv1i2N&api=SYNO.Foto.Thumbnail&version=2&method=get"
"&_sid=session_id&SynoToken=Sy%C3%B10_T0k%E2%82%AC%C3%B1"
)

items = await dsm_7.photos.get_items_from_search(albums[0])
assert items
assert len(items) == 2
Expand Down

0 comments on commit 1d83979

Please sign in to comment.