From 1d839799e10fb60ea94deac630b03c220e38a544 Mon Sep 17 00:00:00 2001 From: Alexis MARQUIS Date: Sun, 11 Aug 2024 18:24:08 +0200 Subject: [PATCH] Fix downloading shared photos from shared album (#344) * 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 --- .flake8 | 2 +- src/synology_dsm/api/photos/__init__.py | 83 +++++++++++++------ src/synology_dsm/api/photos/model.py | 4 + src/synology_dsm/synology_dsm.py | 5 +- tests/__init__.py | 6 +- tests/api_data/dsm_7/__init__.py | 4 +- .../dsm_7/core/const_7_core_external_usb.py | 2 +- tests/api_data/dsm_7/photos/const_7_photo.py | 47 +++++++++++ tests/test_synology_dsm_7.py | 32 ++++++- 9 files changed, 153 insertions(+), 32 deletions(-) diff --git a/.flake8 b/.flake8 index c2e9783c..caca3c6e 100644 --- a/.flake8 +++ b/.flake8 @@ -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 diff --git a/src/synology_dsm/api/photos/__init__.py b/src/synology_dsm/api/photos/__init__.py index 953bc97c..83ada142 100644 --- a/src/synology_dsm/api/photos/__init__.py +++ b/src/synology_dsm/api/photos/__init__.py @@ -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] = [] @@ -62,6 +69,7 @@ def _raw_data_to_items( item["additional"]["thumbnail"]["cache_key"], size, item["owner_user_id"] == 0, + passphrase, ) ) return items @@ -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 @@ -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 @@ -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 @@ -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, ) diff --git a/src/synology_dsm/api/photos/model.py b/src/synology_dsm/api/photos/model.py index 40985cf3..2896c1ee 100644 --- a/src/synology_dsm/api/photos/model.py +++ b/src/synology_dsm/api/photos/model.py @@ -1,5 +1,7 @@ """Data models for Synology Photos Module.""" +from __future__ import annotations + from dataclasses import dataclass @@ -10,6 +12,7 @@ class SynoPhotosAlbum: album_id: int name: str item_count: int + passphrase: str | None @dataclass @@ -23,3 +26,4 @@ class SynoPhotosItem: thumbnail_cache_key: str thumbnail_size: str is_shared: bool + passphrase: str | None diff --git a/src/synology_dsm/synology_dsm.py b/src/synology_dsm/synology_dsm.py index 8b48c573..d7c17ca0 100644 --- a/src/synology_dsm/synology_dsm.py +++ b/src/synology_dsm/synology_dsm.py @@ -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() diff --git a/tests/__init__.py b/tests/__init__.py index d4acbe01..89e0b969 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -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 ( @@ -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 diff --git a/tests/api_data/dsm_7/__init__.py b/tests/api_data/dsm_7/__init__.py index 037a5c65..5d2770d2 100644 --- a/tests/api_data/dsm_7/__init__.py +++ b/tests/api_data/dsm_7/__init__.py @@ -1,4 +1,4 @@ -"""DSM 6 datas.""" +"""DSM 7 datas.""" from .const_7_api_auth import ( DSM_7_AUTH_LOGIN, @@ -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, ) @@ -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", ] diff --git a/tests/api_data/dsm_7/core/const_7_core_external_usb.py b/tests/api_data/dsm_7/core/const_7_core_external_usb.py index 31b282dd..aeaaa098 100644 --- a/tests/api_data/dsm_7/core/const_7_core_external_usb.py +++ b/tests/api_data/dsm_7/core/const_7_core_external_usb.py @@ -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": []}, diff --git a/tests/api_data/dsm_7/photos/const_7_photo.py b/tests/api_data/dsm_7/photos/const_7_photo.py index e3c78d33..2ae4486e 100644 --- a/tests/api_data/dsm_7/photos/const_7_photo.py +++ b/tests/api_data/dsm_7/photos/const_7_photo.py @@ -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, @@ -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": { diff --git a/tests/test_synology_dsm_7.py b/tests/test_synology_dsm_7.py index d3ffef7c..2c3bade4 100644 --- a/tests/test_synology_dsm_7.py +++ b/tests/test_synology_dsm_7.py @@ -179,13 +179,21 @@ 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 @@ -193,12 +201,17 @@ async def test_photos(self, dsm_7): 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 @@ -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