diff --git a/blinkpy/api.py b/blinkpy/api.py index 77833601..0fa5bb3f 100644 --- a/blinkpy/api.py +++ b/blinkpy/api.py @@ -410,7 +410,7 @@ async def http_get( :param is_retry: Is this part of a re-auth attempt? """ _LOGGER.debug("Making GET request to %s", url) - response = await blink.auth.query( + return await blink.auth.query( url=url, headers=blink.auth.header, reqtype="get", @@ -418,7 +418,6 @@ async def http_get( json_resp=json, is_retry=is_retry, ) - return response async def http_post(blink, url, is_retry=False, data=None, json=True, timeout=TIMEOUT): diff --git a/blinkpy/camera.py b/blinkpy/camera.py index 9c73b0ea..80d61d38 100644 --- a/blinkpy/camera.py +++ b/blinkpy/camera.py @@ -2,12 +2,12 @@ import copy import string import os -from aiofiles import open import logging import datetime from json import dumps -import aiohttp import traceback +import aiohttp +from aiofiles import open from requests.compat import urljoin from blinkpy import api from blinkpy.helpers.constants import TIMEOUT_MEDIA @@ -148,7 +148,7 @@ async def async_set_night_vision(self, value): product_type=self.product_type, data=data, ) - if res.status == 200: + if res and res.status == 200: return await res.json() return None @@ -186,14 +186,13 @@ async def get_video_clip(self, url=None): if not url: _LOGGER.warning(f"Video clip URL not available: self.clip={url}") return None - response = await api.http_get( + return await api.http_get( self.sync.blink, url=url, stream=True, json=False, timeout=TIMEOUT_MEDIA, ) - return response async def snap_picture(self): """Take a picture with camera to create a new thumbnail.""" @@ -326,12 +325,12 @@ def timest(record): if new_thumbnail is not None and (update_cached_image or force_cache): response = await self.get_media() - if response.status == 200: + if response and response.status == 200: self._cached_image = await response.read() if clip_addr is not None and (update_cached_video or force_cache): response = await self.get_media(media_type="video") - if response.status == 200: + if response and response.status == 200: self._cached_video = await response.read() # Don't let the recent clips list grow without bound. @@ -374,7 +373,7 @@ async def image_to_file(self, path): """ _LOGGER.debug("Writing image from %s to %s", self.name, path) response = await self.get_media() - if response.status == 200: + if response and response.status == 200: async with open(path, "wb") as imgfile: await imgfile.write(await response.read()) else: @@ -418,7 +417,7 @@ async def save_recent_clips( path = os.path.join(output_dir, file_name) _LOGGER.debug(f"Saving {clip_addr} to {path}") media = await self.get_video_clip(clip_addr) - if media.status == 200: + if media and media.status == 200: async with open(path, "wb") as clip_file: await clip_file.write(await media.read()) num_saved += 1 diff --git a/blinkpy/helpers/util.py b/blinkpy/helpers/util.py index 1c72a348..749a0cb3 100644 --- a/blinkpy/helpers/util.py +++ b/blinkpy/helpers/util.py @@ -159,7 +159,7 @@ async def throttle_method(): @wraps(method) def wrapper(*args, **kwargs): """Wrap that checks for throttling.""" - force = kwargs.get("force", False) + force = kwargs.pop("force", False) now = int(time.time()) last_call_delta = now - self.last_call if force or last_call_delta > self.throttle_time: diff --git a/blinkpy/sync_module.py b/blinkpy/sync_module.py index 4555682b..cc801dc3 100644 --- a/blinkpy/sync_module.py +++ b/blinkpy/sync_module.py @@ -4,6 +4,7 @@ import datetime import traceback import asyncio +import aiofiles from sortedcontainers import SortedSet from requests.structures import CaseInsensitiveDict from blinkpy import api @@ -217,8 +218,10 @@ def get_unique_info(self, name): async def get_events(self, **kwargs): """Retrieve events from server.""" - kwargs.pop("force", False) - response = await api.request_sync_events(self.blink, self.network_id) + force = kwargs.pop("force", False) + response = await api.request_sync_events( + self.blink, self.network_id, force=force + ) try: return response["event"] except (TypeError, KeyError): @@ -370,7 +373,6 @@ async def check_new_videos(self): self._local_storage["last_manifest_read"] = last_manifest_read _LOGGER.debug(f"Updated last_manifest_read to {last_manifest_read}") _LOGGER.debug(f"Last clip time was {last_clip_time}") - # We want to keep the last record when no new motion was detected. for camera in self.cameras.keys(): # Check if there are no new records, indicating motion. @@ -681,6 +683,46 @@ async def prepare_download(self, blink, max_retries=4): await asyncio.sleep(seconds) return response + async def delete_video(self, blink, max_retries=4) -> bool: + """Delete video from sync module.""" + delete_url = blink.urls.base_url + self.url() + delete_url = delete_url.replace("request", "delete") + + for retry in range(max_retries): + delete = await api.http_post( + blink, delete_url, json=False + ) # Delete the video + if delete.status == 200: + return True + seconds = backoff_seconds(retry=retry, default_time=3) + _LOGGER.debug("[retry=%d] Retrying in %d seconds", retry + 1, seconds) + await asyncio.sleep(seconds) + return False + + async def download_video(self, blink, file_name, max_retries=4) -> bool: + """Download a previously prepared video from sync module.""" + for retry in range(max_retries): + url = blink.urls.base_url + self.url() + video = await api.http_get(blink, url, json=False) + if video.status == 200: + async with aiofiles.open(file_name, "wb") as vidfile: + await vidfile.write(await video.read()) # download the video + return True + seconds = backoff_seconds(retry=retry, default_time=3) + _LOGGER.debug( + "[retry=%d] Retrying in %d seconds: %s", retry + 1, seconds, url + ) + await asyncio.sleep(seconds) + return False + + async def download_video_delete(self, blink, file_name, max_retries=4) -> bool: + """Initiate upload of media item from the sync module to Blink cloud servers then download to local filesystem and delete from sync.""" + if await self.prepare_download(blink): + if await self.download_video(blink, file_name): + if await self.delete_video(blink): + return True + return False + def __repr__(self): """Create string representation.""" return ( diff --git a/blinksync/blinksync.py b/blinksync/blinksync.py index 4b255a1c..346c649f 100644 --- a/blinksync/blinksync.py +++ b/blinksync/blinksync.py @@ -5,10 +5,11 @@ import aiohttp import sys from sortedcontainers import SortedSet -from forms import LoginDialog, VideosForm, DELAY,CLOSE, DELETE, DOWNLOAD, REFRESH +from forms import LoginDialog, VideosForm, DELAY, CLOSE, DELETE, DOWNLOAD, REFRESH from blinkpy.blinkpy import Blink, BlinkSyncModule from blinkpy.auth import Auth + async def main(): """Main loop for blink test.""" session = aiohttp.ClientSession() @@ -20,8 +21,8 @@ async def main(): path = dlg.GetPath() else: sys.exit(0) - - with open(f"{path}/blink.json", "rt",encoding='ascii') as j: + + with open(f"{path}/blink.json", "rt", encoding="ascii") as j: blink.auth = Auth(json.loads(j.read()), session=session) except (StopIteration, FileNotFoundError): @@ -33,6 +34,9 @@ async def main(): userpass, session=session, ) + await blink.save(f"{path}/blink.json") + else: + sys.exit(0) with wx.BusyInfo("Blink is Working....") as working: cursor = wx.BusyCursor() if await blink.start(): @@ -44,7 +48,9 @@ async def main(): print(f"Sync :{blink.networks}") if len(blink.networks) == 0: exit() - my_sync: BlinkSyncModule = blink.sync[blink.networks[list(blink.networks)[0]]['name']] + my_sync: BlinkSyncModule = blink.sync[ + blink.networks[list(blink.networks)[0]]["name"] + ] cursor = None working = None @@ -55,7 +61,7 @@ async def main(): print(name) print(camera.attributes) - my_sync._local_storage['manifest'] = SortedSet() + my_sync._local_storage["manifest"] = SortedSet() await my_sync.refresh() if my_sync.local_storage and my_sync.local_storage_manifest_ready: print("Manifest is ready") @@ -80,7 +86,7 @@ async def main(): continue # Download and delete all videos from sync module for item in reversed(manifest): - if item.id in frame.ItemList: + if item.id in frame.ItemList: if button == DOWNLOAD: await item.prepare_download(blink) await item.download_video( @@ -94,12 +100,10 @@ async def main(): working = None frame = None await session.close() - + # Run the program if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) loop = asyncio.get_event_loop() loop.run_until_complete(main()) - - diff --git a/tests/test_sync_module.py b/tests/test_sync_module.py index 8b2a3d55..e07b83d2 100644 --- a/tests/test_sync_module.py +++ b/tests/test_sync_module.py @@ -2,6 +2,7 @@ import datetime from unittest import IsolatedAsyncioTestCase from unittest import mock +import aiofiles from blinkpy.blinkpy import Blink from blinkpy.helpers.util import BlinkURLHandler, to_alphanumeric from blinkpy.sync_module import ( @@ -12,6 +13,7 @@ ) from blinkpy.camera import BlinkCamera from tests.test_blink_functions import MockCamera +import tests.mock_responses as mresp @mock.patch("blinkpy.auth.Auth.query") @@ -528,3 +530,88 @@ async def test_local_storage_media_item(self, mock_resp): self.assertEquals( await item.prepare_download(blink, max_retries=1), {"network_id": 123456} ) + + with mock.patch("blinkpy.api.http_post", return_value=""): + self.assertIsNone(await item2.prepare_download(blink, max_retries=0)) + + async def test_poll_local_storage_manifest(self, mock_resp): + """Test incorrect response.""" + with mock.patch("blinkpy.api.request_local_storage_manifest", return_value=""): + self.assertIsNone( + await self.blink.sync["test"].poll_local_storage_manifest(max_retries=0) + ) + + async def test_delete_video(self, mock_resp): + """Test item delete.""" + blink: Blink = Blink(motion_interval=0, session=mock.AsyncMock()) + blink.last_refresh = 0 + blink.urls = BlinkURLHandler("test") + item = LocalStorageMediaItem( + "1234", + "Backdoor", + datetime.datetime.utcnow().isoformat(), + "432", + " manifest_id", + "url", + ) + mock_resp.return_value = mresp.MockResponse({"status": 200}, 200) + self.assertTrue(await item.delete_video(blink)) + + mock_resp.return_value = mresp.MockResponse({"status": 400}, 400) + self.assertFalse(await item.delete_video(blink, 1)) + + async def test_download_video(self, mock_resp): + """Test item download.""" + blink: Blink = Blink(motion_interval=0, session=mock.AsyncMock()) + blink.last_refresh = 0 + blink.urls = BlinkURLHandler("test") + item = LocalStorageMediaItem( + "1234", + "Backdoor", + datetime.datetime.utcnow().isoformat(), + "432", + " manifest_id", + "url", + ) + mock_file = mock.MagicMock() + aiofiles.threadpool.wrap.register(mock.MagicMock)( + lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase( + *args, **kwargs + ) + ) + with mock.patch("aiofiles.threadpool.sync_open", return_value=mock_file): + mock_resp.return_value = mresp.MockResponse({"status": 200}, 200) + self.assertTrue(await item.download_video(blink, "filename.mp4")) + + mock_resp.return_value = mresp.MockResponse({"status": 400}, 400) + self.assertFalse(await item.download_video(blink, "filename.mp4", 1)) + + @mock.patch("blinkpy.sync_module.LocalStorageMediaItem.download_video") + @mock.patch("blinkpy.sync_module.LocalStorageMediaItem.delete_video") + @mock.patch("blinkpy.sync_module.LocalStorageMediaItem.prepare_download") + async def test_download_delete(self, mock_prepdl, mock_del, mock_dl, mock_resp): + """Test download and delete.""" + blink: Blink = Blink(motion_interval=0, session=mock.AsyncMock()) + blink.last_refresh = 0 + blink.urls = BlinkURLHandler("test") + item = LocalStorageMediaItem( + "1234", + "Backdoor", + datetime.datetime.utcnow().isoformat(), + "432", + " manifest_id", + "url", + ) + + self.assertTrue(await item.download_video_delete(self.blink, "filename.mp4")) + + mock_prepdl.return_value = False + self.assertFalse(await item.download_video_delete(self.blink, "filename.mp4")) + + mock_prepdl.return_value = mock.AsyncMock() + mock_del.return_value = False + self.assertFalse(await item.download_video_delete(self.blink, "filename.mp4")) + + mock_del.return_value = mock.AsyncMock() + mock_dl.return_value = False + self.assertFalse(await item.download_video_delete(self.blink, "filename.mp4"))