Exemplo n.º 1
0
    async def _build_music_library(
            self, library: dict[str, Any],
            include_children: bool) -> BrowseMediaSource:
        """Return a single music library as a browsable media source."""
        library_id = library[ITEM_KEY_ID]
        library_name = library[ITEM_KEY_NAME]

        result = BrowseMediaSource(
            domain=DOMAIN,
            identifier=library_id,
            media_class=MEDIA_CLASS_DIRECTORY,
            media_content_type=MEDIA_TYPE_NONE,
            title=library_name,
            can_play=False,
            can_expand=True,
        )

        if include_children:
            result.children_media_class = MEDIA_CLASS_ARTIST
            result.children = await self._build_artists(
                library_id)  # type: ignore[assignment]
            if not result.children:
                result.children_media_class = MEDIA_CLASS_ALBUM
                result.children = await self._build_albums(
                    library_id)  # type: ignore[assignment]

        return result
Exemplo n.º 2
0
    async def _build_recent(
        self,
        data: ProtectData,
        camera_id: str,
        event_type: SimpleEventType,
        days: int,
        build_children: bool = False,
    ) -> BrowseMediaSource:
        """Build media source for events in relative days."""

        base_id = f"{data.api.bootstrap.nvr.id}:browse:{camera_id}:{event_type.value}"
        title = f"Last {days} Days"
        if days == 1:
            title = "Last 24 Hours"

        source = BrowseMediaSource(
            domain=DOMAIN,
            identifier=f"{base_id}:recent:{days}",
            media_class=MEDIA_CLASS_DIRECTORY,
            media_content_type="video/mp4",
            title=title,
            can_play=False,
            can_expand=True,
            children_media_class=MEDIA_CLASS_VIDEO,
        )

        if not build_children:
            return source

        now = dt_util.now()

        args = {
            "data": data,
            "start": now - timedelta(days=days),
            "end": now,
            "reserve": True,
        }
        if event_type != SimpleEventType.ALL:
            args["event_type"] = get_ufp_event(event_type)

        camera: Camera | None = None
        if camera_id != "all":
            camera = data.api.bootstrap.cameras.get(camera_id)
            args["camera_id"] = camera_id

        events = await self._build_events(**args)  # type: ignore[arg-type]
        source.children = events
        source.title = self._breadcrumb(
            data,
            title,
            camera=camera,
            event_type=event_type,
            count=len(events),
        )
        return source
Exemplo n.º 3
0
    async def _build_month(
        self,
        data: ProtectData,
        camera_id: str,
        event_type: SimpleEventType,
        start: date,
        build_children: bool = False,
    ) -> BrowseMediaSource:
        """Build media source for selectors for a given month."""

        base_id = f"{data.api.bootstrap.nvr.id}:browse:{camera_id}:{event_type.value}"

        title = f"{start.strftime('%B %Y')}"
        source = BrowseMediaSource(
            domain=DOMAIN,
            identifier=f"{base_id}:range:{start.year}:{start.month}",
            media_class=MEDIA_CLASS_DIRECTORY,
            media_content_type=VIDEO_FORMAT,
            title=title,
            can_play=False,
            can_expand=True,
            children_media_class=MEDIA_CLASS_VIDEO,
        )

        if not build_children:
            return source

        month = start.month
        children = [
            self._build_days(data, camera_id, event_type, start, is_all=True)
        ]
        while start.month == month:
            children.append(
                self._build_days(data,
                                 camera_id,
                                 event_type,
                                 start,
                                 is_all=False))
            start = start + timedelta(hours=24)

        camera: Camera | None = None
        if camera_id != "all":
            camera = data.api.bootstrap.cameras.get(camera_id)

        source.children = await asyncio.gather(*children)
        source.title = self._breadcrumb(
            data,
            title,
            camera=camera,
            event_type=event_type,
        )

        return source
Exemplo n.º 4
0
    async def async_browse_media(self,
                                 item: MediaSourceItem) -> BrowseMediaSource:
        """Browse media."""
        dms_data = get_domain_data(self.hass)
        if not dms_data.sources:
            raise BrowseError("No sources have been configured")

        source_id, media_id = _parse_identifier(item)
        LOGGER.debug("Browsing for %s / %s", source_id, media_id)

        if not source_id and len(dms_data.sources) > 1:
            # Browsing the root of dlna_dms with more than one server, return
            # all known servers.
            base = BrowseMediaSource(
                domain=DOMAIN,
                identifier="",
                media_class=MEDIA_CLASS_DIRECTORY,
                media_content_type=MEDIA_TYPE_CHANNELS,
                title=self.name,
                can_play=False,
                can_expand=True,
                children_media_class=MEDIA_CLASS_CHANNEL,
            )

            base.children = [
                BrowseMediaSource(
                    domain=DOMAIN,
                    identifier=
                    f"{source_id}/{PATH_OBJECT_ID_FLAG}{ROOT_OBJECT_ID}",
                    media_class=MEDIA_CLASS_CHANNEL,
                    media_content_type=MEDIA_TYPE_CHANNEL,
                    title=source.name,
                    can_play=False,
                    can_expand=True,
                    thumbnail=source.icon,
                ) for source_id, source in dms_data.sources.items()
            ]

            return base

        if not source_id:
            # No source specified, default to the first registered
            source_id = next(iter(dms_data.sources))

        try:
            source = dms_data.sources[source_id]
        except KeyError as err:
            raise BrowseError(f"Unknown source ID: {source_id}") from err

        return await source.async_browse_media(media_id)
Exemplo n.º 5
0
    def _build_item_response(self,
                             iteminfo: ItemInfo,
                             path: Path,
                             is_child=False):
        mime_type, _ = mimetypes.guess_type(str(path))
        is_file = path.is_file()
        is_dir = path.is_dir()

        # Make sure it's a file or directory
        if not is_file and not is_dir:
            return None

        # Check that it's a media file
        if is_file and (not mime_type
                        or mime_type.split("/")[0] not in MEDIA_MIME_TYPES):
            return None

        title = path.name
        if is_dir:
            title += "/"

        media_class = MEDIA_CLASS_MAP.get(
            mime_type and mime_type.split("/")[0], MEDIA_CLASS_DIRECTORY)

        media = BrowseMediaSource(
            domain=DOMAIN,
            identifier=
            f"{iteminfo.entry_id}/{path.relative_to(iteminfo.target_dir)}",
            media_class=media_class,
            media_content_type=mime_type or "",
            title=title,
            can_play=is_file,
            can_expand=is_dir,
        )

        if is_file or is_child:
            return media

        # Append first level children
        media.children = []
        for child_path in path.iterdir():
            child = self._build_item_response(iteminfo, child_path, True)
            if child:
                media.children.append(child)

        # Sort children showing directories first, then by name
        media.children.sort(key=lambda child: (child.can_play, child.title))

        return media
Exemplo n.º 6
0
    async def _build_events_type(
        self,
        data: ProtectData,
        camera_id: str,
        event_type: SimpleEventType,
        build_children: bool = False,
    ) -> BrowseMediaSource:
        """Build folder media source for a selectors for a given event type."""

        base_id = f"{data.api.bootstrap.nvr.id}:browse:{camera_id}:{event_type.value}"

        title = EVENT_NAME_MAP[event_type].title()
        source = BrowseMediaSource(
            domain=DOMAIN,
            identifier=base_id,
            media_class=MEDIA_CLASS_DIRECTORY,
            media_content_type=VIDEO_FORMAT,
            title=title,
            can_play=False,
            can_expand=True,
            children_media_class=MEDIA_CLASS_VIDEO,
        )

        if not build_children or data.api.bootstrap.recording_start is None:
            return source

        children = [
            self._build_recent(data, camera_id, event_type, 1),
            self._build_recent(data, camera_id, event_type, 7),
            self._build_recent(data, camera_id, event_type, 30),
        ]

        start, end = _get_start_end(self.hass,
                                    data.api.bootstrap.recording_start)
        while end > start:
            children.append(
                self._build_month(data, camera_id, event_type, end.date()))
            end = (end - timedelta(days=1)).replace(day=1)

        camera: Camera | None = None
        if camera_id != "all":
            camera = data.api.bootstrap.cameras.get(camera_id)
        source.children = await asyncio.gather(*children)
        source.title = self._breadcrumb(data, title, camera=camera)

        return source
Exemplo n.º 7
0
    async def _build_libraries(self) -> BrowseMediaSource:
        """Return all supported libraries the user has access to as media sources."""
        base = BrowseMediaSource(
            domain=DOMAIN,
            identifier=None,
            media_class=MEDIA_CLASS_DIRECTORY,
            media_content_type=MEDIA_TYPE_NONE,
            title=self.name,
            can_play=False,
            can_expand=True,
            children_media_class=MEDIA_CLASS_DIRECTORY,
        )

        libraries = await self._get_libraries()

        base.children = []

        for library in libraries:
            base.children.append(await self._build_library(library, False))

        return base
Exemplo n.º 8
0
    async def async_browse_media(
            self,
            item: MediaSourceItem,
            media_types: tuple[str] = MEDIA_MIME_TYPES) -> BrowseMediaSource:

        # root node for motion_frontend media
        # add a child for each configured server
        if item.identifier is None:
            base = BrowseMediaSource(
                domain=DOMAIN,
                identifier="",
                media_class=MEDIA_CLASS_DIRECTORY,
                media_content_type=None,
                title=self.name,
                can_play=False,
                can_expand=True,
                children_media_class=MEDIA_CLASS_DIRECTORY,
            )

            base.children = [
                BrowseMediaSource(
                    domain=DOMAIN,
                    identifier=entry_id,
                    media_class=MEDIA_CLASS_DIRECTORY,
                    media_content_type=None,
                    title=api.client.unique_id,
                    can_play=False,
                    can_expand=True,
                ) for entry_id, api in self.hass.data[DOMAIN].items()
            ]

            return base

        try:
            iteminfo: ItemInfo = self.async_parse_identifier(item)
        except Unresolvable as err:
            raise BrowseError(str(err)) from err

        return await self.hass.async_add_executor_job(self._browse_media,
                                                      iteminfo)
Exemplo n.º 9
0
    async def _build_movie_library(
        self, library: dict[str, Any], include_children: bool
    ) -> BrowseMediaSource:
        """Return a single movie library as a browsable media source."""
        library_id = library[ITEM_KEY_ID]
        library_name = library[ITEM_KEY_NAME]

        result = BrowseMediaSource(
            domain=DOMAIN,
            identifier=library_id,
            media_class=MEDIA_CLASS_DIRECTORY,
            media_content_type=MEDIA_TYPE_NONE,
            title=library_name,
            can_play=False,
            can_expand=True,
        )

        if include_children:
            result.children_media_class = MEDIA_CLASS_MOVIE
            result.children = await self._build_movies(library_id)

        return result
Exemplo n.º 10
0
    async def _build_album(self, album: dict[str, Any],
                           include_children: bool) -> BrowseMediaSource:
        """Return a single album as a browsable media source."""
        album_id = album[ITEM_KEY_ID]
        album_title = album[ITEM_KEY_NAME]
        thumbnail_url = self._get_thumbnail_url(album)

        result = BrowseMediaSource(
            domain=DOMAIN,
            identifier=album_id,
            media_class=MEDIA_CLASS_ALBUM,
            media_content_type=MEDIA_TYPE_NONE,
            title=album_title,
            can_play=False,
            can_expand=True,
            thumbnail=thumbnail_url,
        )

        if include_children:
            result.children_media_class = MEDIA_CLASS_TRACK
            result.children = await self._build_tracks(
                album_id)  # type: ignore[assignment]

        return result
Exemplo n.º 11
0
    async def _build_artist(
        self, artist: dict[str, Any], include_children: bool
    ) -> BrowseMediaSource:
        """Return a single artist as a browsable media source."""
        artist_id = artist[ITEM_KEY_ID]
        artist_name = artist[ITEM_KEY_NAME]
        thumbnail_url = self._get_thumbnail_url(artist)

        result = BrowseMediaSource(
            domain=DOMAIN,
            identifier=artist_id,
            media_class=MEDIA_CLASS_ARTIST,
            media_content_type=MEDIA_TYPE_NONE,
            title=artist_name,
            can_play=False,
            can_expand=True,
            thumbnail=thumbnail_url,
        )

        if include_children:
            result.children_media_class = MEDIA_CLASS_ALBUM
            result.children = await self._build_albums(artist_id)

        return result
Exemplo n.º 12
0
    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
Exemplo n.º 13
0
    def _build_item_response(
        self, source: str, camera_id: str, event_id: int | None = None
    ) -> BrowseMediaSource:
        if event_id and event_id in self.events[camera_id]:
            created = dt.datetime.fromtimestamp(event_id)
            if self.events[camera_id][event_id]["type"] == "outdoor":
                thumbnail = (
                    self.events[camera_id][event_id]["event_list"][0]
                    .get("snapshot", {})
                    .get("url")
                )
                message = remove_html_tags(
                    self.events[camera_id][event_id]["event_list"][0]["message"]
                )
            else:
                thumbnail = (
                    self.events[camera_id][event_id].get("snapshot", {}).get("url")
                )
                message = remove_html_tags(self.events[camera_id][event_id]["message"])
            title = f"{created} - {message}"
        else:
            title = self.hass.data[DOMAIN][DATA_CAMERAS].get(camera_id, MANUFACTURER)
            thumbnail = None

        if event_id:
            path = f"{source}/{camera_id}/{event_id}"
        else:
            path = f"{source}/{camera_id}"

        media_class = MEDIA_CLASS_DIRECTORY if event_id 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(
                event_id and self.events[camera_id][event_id].get("media_url")
            ),
            can_expand=event_id is None,
            thumbnail=thumbnail,
        )

        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:
            return media

        media.children = []
        # Append first level children
        if not camera_id:
            for cid in self.events:
                child = self._build_item_response(source, cid)
                if child:
                    media.children.append(child)
        else:
            for eid in self.events[camera_id]:
                try:
                    child = self._build_item_response(source, camera_id, eid)
                except IncompatibleMediaSource:
                    continue
                if child:
                    media.children.append(child)

        return media
Exemplo n.º 14
0
    async def _build_camera(self,
                            data: ProtectData,
                            camera_id: str,
                            build_children: bool = False) -> BrowseMediaSource:
        """Build media source for selectors for a UniFi Protect camera."""

        name = "All Cameras"
        is_doorbell = data.api.bootstrap.has_doorbell
        has_smart = data.api.bootstrap.has_smart_detections
        camera: Camera | None = None
        if camera_id != "all":
            camera = data.api.bootstrap.cameras.get(camera_id)
            if camera is None:
                raise BrowseError(f"Unknown Camera ID: {camera_id}")
            name = camera.name or camera.market_name or camera.type
            is_doorbell = camera.feature_flags.has_chime
            has_smart = camera.feature_flags.has_smart_detect

        thumbnail_url: str | None = None
        if camera is not None:
            thumbnail_url = await self._get_camera_thumbnail_url(camera)
        source = BrowseMediaSource(
            domain=DOMAIN,
            identifier=f"{data.api.bootstrap.nvr.id}:browse:{camera_id}",
            media_class=MEDIA_CLASS_DIRECTORY,
            media_content_type=VIDEO_FORMAT,
            title=name,
            can_play=False,
            can_expand=True,
            thumbnail=thumbnail_url,
            children_media_class=MEDIA_CLASS_VIDEO,
        )

        if not build_children:
            return source

        source.children = [
            await self._build_events_type(data, camera_id,
                                          SimpleEventType.MOTION),
        ]

        if is_doorbell:
            source.children.insert(
                0,
                await self._build_events_type(data, camera_id,
                                              SimpleEventType.RING),
            )

        if has_smart:
            source.children.append(await self._build_events_type(
                data, camera_id, SimpleEventType.SMART))

        if is_doorbell or has_smart:
            source.children.insert(
                0,
                await self._build_events_type(data, camera_id,
                                              SimpleEventType.ALL),
            )

        source.title = self._breadcrumb(data, name)

        return source
Exemplo n.º 15
0
    async def _build_days(
        self,
        data: ProtectData,
        camera_id: str,
        event_type: SimpleEventType,
        start: date,
        is_all: bool = True,
        build_children: bool = False,
    ) -> BrowseMediaSource:
        """Build media source for events for a given day or whole month."""

        base_id = f"{data.api.bootstrap.nvr.id}:browse:{camera_id}:{event_type.value}"

        if is_all:
            title = "Whole Month"
            identifier = f"{base_id}:range:{start.year}:{start.month}:all"
        else:
            title = f"{start.strftime('%x')}"
            identifier = f"{base_id}:range:{start.year}:{start.month}:{start.day}"
        source = BrowseMediaSource(
            domain=DOMAIN,
            identifier=identifier,
            media_class=MEDIA_CLASS_DIRECTORY,
            media_content_type=VIDEO_FORMAT,
            title=title,
            can_play=False,
            can_expand=True,
            children_media_class=MEDIA_CLASS_VIDEO,
        )

        if not build_children:
            return source

        start_dt = datetime(
            year=start.year,
            month=start.month,
            day=start.day,
            hour=0,
            minute=0,
            second=0,
            tzinfo=dt_util.DEFAULT_TIME_ZONE,
        )
        if is_all:
            if start_dt.month < 12:
                end_dt = start_dt.replace(month=start_dt.month + 1)
            else:
                end_dt = start_dt.replace(year=start_dt.year + 1, month=1)
        else:
            end_dt = start_dt + timedelta(hours=24)

        args = {
            "data": data,
            "start": start_dt,
            "end": end_dt,
            "reserve": False,
        }
        if event_type != SimpleEventType.ALL:
            args["event_type"] = get_ufp_event(event_type)

        camera: Camera | None = None
        if camera_id != "all":
            camera = data.api.bootstrap.cameras.get(camera_id)
            args["camera_id"] = camera_id

        title = f"{start.strftime('%B %Y')} > {title}"
        events = await self._build_events(**args)  # type: ignore[arg-type]
        source.children = events
        source.title = self._breadcrumb(
            data,
            title,
            camera=camera,
            event_type=event_type,
            count=len(events),
        )

        return source