async def _async_browse_media( self, source: str, camera_id: str, event_id: str = None, no_descend: bool = True) -> BrowseMediaSource: """ actual browse after input validation """ event: typings.VodEvent = None cache: typings.MediaSourceCacheEntry = None start_date = None if camera_id and camera_id in self.cache: cache = self.cache[camera_id] if cache and event_id: if "playback_events" in cache and event_id in cache[ "playback_events"]: event = cache["playback_events"][event_id] end_date = event["end"] start_date = event["start"] time = start_date.time() duration = end_date - start_date title = f"{time} {duration}" else: year, *rest = event_id.split("/", 3) month = rest[0] if len(rest) > 0 else None day = rest[1] if len(rest) > 1 else None start_date = dt.datetime.combine( dt.date(int(year), int(month) if month else 1, int(day) if day else 1), dt.time.min, dt_utils.now().tzinfo, ) title = f"{start_date.date()}" path = f"{source}/{camera_id}/{event_id}" else: if cache is None: camera_id = "" title = NAME else: title = cache["name"] path = f"{source}/{camera_id}" media_class = MEDIA_CLASS_DIRECTORY if event is None else MEDIA_CLASS_VIDEO media = BrowseMediaSource( domain=DOMAIN, identifier=path, media_class=media_class, media_content_type=MEDIA_TYPE_VIDEO, title=title, can_play=bool(not event is None and event.get("file")), can_expand=event is None, ) if not event is None and cache.get("playback_thumbnails", False): url = "/api/" + DOMAIN + f"/media_proxy/{camera_id}/{event_id}" # TODO : I cannot find a way to get the current user context at this point # so I will have to leave the view as unauthenticated, as a temporary # security measure, I will add a unique token to the event to limit # "exposure" # url = async_sign_path(self.hass, None, url, dt.timedelta(minutes=30)) if "token" not in event: event["token"] = secrets.token_hex() media.thumbnail = f"{url}?token={parse.quote_plus(event['token'])}" if not media.can_play and not media.can_expand: _LOGGER.debug("Camera %s with event %s without media url found", camera_id, event_id) raise IncompatibleMediaSource if not media.can_expand or no_descend: return media media.children = [] base: ReolinkBase = None if cache is None: for entry_id in self.hass.data[DOMAIN]: entry = self.hass.data[DOMAIN][entry_id] if not isinstance(entry, dict) or not BASE in entry: continue base = entry[BASE] camera_id = base.unique_id cache = self.cache.get(camera_id, None) if cache is None: cache = self.cache[camera_id] = { "entry_id": entry_id, "unique_id": base.unique_id, "playback_events": {}, } cache["name"] = base.name child = await self._async_browse_media(source, camera_id) media.children.append(child) return media base = self.hass.data[DOMAIN][cache["entry_id"]][BASE] # TODO: the cache is one way so over time it can grow and have invalid # records, the code should be expanded to invalidate/expire # entries if base is None: raise BrowseError("Camera does not exist.") if not start_date: if ("playback_day_entries" not in cache or cache.get("playback_months", -1) != base.playback_months): end_date = dt_utils.now() start_date = dt.datetime.combine(end_date.date(), dt.time.min) cache["playback_months"] = base.playback_months if cache["playback_months"] > 1: start_date -= relativedelta.relativedelta( months=int(cache["playback_months"])) entries = cache["playback_day_entries"] = [] search, _ = await base.api.send_search(start_date, end_date, True) if not search is None: for status in search: year = status["year"] month = status["mon"] for day, flag in enumerate(status["table"], start=1): if flag == "1": entries.append(dt.date(year, month, day)) entries.sort() else: entries = cache["playback_day_entries"] for date in cache["playback_day_entries"]: child = await self._async_browse_media( source, camera_id, f"{date.year}/{date.month}/{date.day}") media.children.append(child) return media cache["playback_thumbnails"] = base.playback_thumbnails end_date = dt.datetime.combine(start_date.date(), dt.time.max, start_date.tzinfo) _, files = await base.api.send_search(start_date, end_date) if not files is None: events = cache.setdefault("playback_events", {}) for file in files: dto = file["EndTime"] end_date = dt.datetime( dto["year"], dto["mon"], dto["day"], dto["hour"], dto["min"], dto["sec"], 0, end_date.tzinfo, ) dto = file["StartTime"] start_date = dt.datetime( dto["year"], dto["mon"], dto["day"], dto["hour"], dto["min"], dto["sec"], 0, end_date.tzinfo, ) event_id = str(start_date.timestamp()) event = events.setdefault(event_id, {}) event["start"] = start_date event["end"] = end_date event["file"] = file["name"] child = await self._async_browse_media(source, camera_id, event_id) media.children.append(child) return media
def create_item(title: str, path: str, thumbnail: bool = False): nonlocal self, camera_id, event_id, start_date if not title or not path: if event_id and "/" in event_id: year, *rest = event_id.split("/", 3) month = rest[0] if len(rest) > 0 else None day = rest[1] if len(rest) > 1 else None start_date = dt.datetime.combine( dt.date( int(year), int(month) if month else 1, int(day) if day else 1, ), dt.time.min, dt_utils.now().tzinfo, ) title = f"{start_date.date()}" path = f"{source}/{camera_id}/{event_id}" elif base: title = base.name path = f"{source}/{camera_id}" else: title = self.name path = source + "/" media_class = (MEDIA_CLASS_DIRECTORY if not event_id or "/" in event_id else MEDIA_CLASS_VIDEO) media = BrowseMediaSource( domain=self.domain, identifier=path, media_class=media_class, media_content_type=MEDIA_TYPE_VIDEO, title=title, can_play=not bool(media_class == MEDIA_CLASS_DIRECTORY), can_expand=bool(media_class == MEDIA_CLASS_DIRECTORY), ) if thumbnail: url = THUMBNAIL_URL.format(camera_id=camera_id, event_id=event_id) # cannot do authsign as we are in a websocket and isloated from auth and context # we will continue to use custom tokens # request = current_request.get() # refresh_token_id = request.get(KEY_HASS_REFRESH_TOKEN_ID) # if not refresh_token_id: # _LOGGER.debug("no token? %s", list(request.keys())) # # leave expiration 30 seconds? # media.thumbnail = async_sign_path( # self.hass, refresh_token_id, url, dt.timedelta(seconds=30) # ) media.thumbnail = f"{url}?token={self._short_security_token}" if not media.can_play and not media.can_expand: _LOGGER.debug( "Camera %s with event %s without media url found", camera_id, event_id, ) raise IncompatibleMediaSource return media