--- /dev/null
+"""Nugs.net musicprovider support for MusicAssistant."""
+
+from __future__ import annotations
+
+from collections.abc import AsyncGenerator
+from datetime import UTC, datetime
+from time import time
+from typing import TYPE_CHECKING, Any
+
+from aiohttp import ClientTimeout
+from music_assistant_models.config_entries import ConfigEntry, ConfigValueType
+from music_assistant_models.enums import (
+ ConfigEntryType,
+ ContentType,
+ ImageType,
+ MediaType,
+ ProviderFeature,
+ StreamType,
+)
+from music_assistant_models.errors import (
+ InvalidDataError,
+ LoginFailed,
+ MediaNotFoundError,
+ ResourceTemporarilyUnavailable,
+)
+from music_assistant_models.media_items import (
+ Album,
+ Artist,
+ AudioFormat,
+ ItemMapping,
+ MediaItemImage,
+ MediaItemMetadata,
+ Playlist,
+ ProviderMapping,
+ Track,
+ UniqueList,
+)
+from music_assistant_models.streamdetails import StreamDetails
+
+from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME
+from music_assistant.helpers.json import json_loads
+from music_assistant.models.music_provider import MusicProvider
+
+if TYPE_CHECKING:
+ from music_assistant_models.config_entries import ProviderConfig
+ from music_assistant_models.provider import ProviderManifest
+
+ from music_assistant.mass import MusicAssistant
+ from music_assistant.models import ProviderInstanceType
+
+
+async def setup(
+ mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
+) -> ProviderInstanceType:
+ """Initialize provider(instance) with given configuration."""
+ prov = NugsProvider(mass, manifest, config)
+ await prov.handle_async_init()
+ return prov
+
+
+async def get_config_entries(
+ mass: MusicAssistant,
+ instance_id: str | None = None,
+ action: str | None = None,
+ values: dict[str, ConfigValueType] | None = None,
+) -> tuple[ConfigEntry, ...]:
+ """
+ Return Config entries to setup this provider.
+
+ instance_id: id of an existing provider instance (None if new instance setup).
+ action: [optional] action key called from config entries UI.
+ values: the (intermediate) raw values for config entries sent with the action.
+ """
+ # ruff: noqa: ARG001
+ return (
+ ConfigEntry(
+ key=CONF_USERNAME,
+ type=ConfigEntryType.STRING,
+ label="Username",
+ required=True,
+ ),
+ ConfigEntry(
+ key=CONF_PASSWORD,
+ type=ConfigEntryType.SECURE_STRING,
+ label="Password",
+ required=True,
+ ),
+ )
+
+
+class NugsProvider(MusicProvider):
+ """Provider implementation for Nugs.net."""
+
+ _auth_token: str | None = None
+ _token_expiry: float = 0
+
+ @property
+ def supported_features(self) -> set[ProviderFeature]:
+ """Return the features supported by this Provider."""
+ return {
+ ProviderFeature.BROWSE,
+ ProviderFeature.LIBRARY_ARTISTS,
+ ProviderFeature.LIBRARY_ALBUMS,
+ ProviderFeature.LIBRARY_PLAYLISTS,
+ ProviderFeature.ARTIST_ALBUMS,
+ }
+
+ async def handle_async_init(self) -> None:
+ """Handle async initialization of the provider."""
+ await self.login()
+
+ async def get_library_artists(self) -> AsyncGenerator[Artist, None]:
+ """Retrieve library artists from nugs.net."""
+ artist_data = await self._get_all_items("stash", "artists/favorite/")
+ for item in artist_data:
+ if item and item["id"]:
+ yield self._parse_artist(item)
+
+ async def get_library_albums(self) -> AsyncGenerator[Album, None]:
+ """Retrieve library albums from the provider."""
+ album_data = await self._get_all_items("stash", "releases/favorite")
+ for item in album_data:
+ if item and item["id"]:
+ yield self._parse_album(item)
+
+ async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]:
+ """Retrieve playlists from the provider."""
+ playlist_data = await self._get_all_items("stash", "playlists/")
+ for item in playlist_data:
+ if item and item["id"]:
+ yield self._parse_playlist(item)
+
+ async def get_artist(self, prov_artist_id: str) -> Artist:
+ """Get artist details by id."""
+ endpoint = f"/releases/recent?limit=1&artistIds={prov_artist_id}"
+ artist_response = await self._get_data("catalog", endpoint)
+ artist_data = artist_response["items"][0]["artist"]
+ return self._parse_artist(artist_data)
+
+ async def get_artist_albums(self, prov_artist_id: str) -> list[Album]:
+ """Get a list of all albums for the given artist."""
+ params = {
+ "artistIds": prov_artist_id,
+ "contentType": "any",
+ }
+ return [
+ self._parse_album(item)
+ for item in await self._get_all_items("catalog", "releases/recent", **params)
+ if (item and item["id"])
+ ]
+
+ async def get_album(self, prov_album_id: str) -> Album:
+ """Get album details by id."""
+ endpoint = f"shows/{prov_album_id}"
+ response = await self._get_data("catalog", endpoint)
+ return self._parse_album(response["Response"])
+
+ async def get_playlist(self, prov_playlist_id: str) -> Playlist:
+ """Get full playlist details by id."""
+ endpoint = f"playlists/{prov_playlist_id}"
+ response = await self._get_data("stash", endpoint)
+ return self._parse_playlist(response["items"])
+
+ async def get_album_tracks(self, prov_album_id: str) -> list[Track]:
+ """Get all album tracks for given album id."""
+ endpoint = f"shows/{prov_album_id}"
+ response = await self._get_data("catalog", endpoint)
+ album_data = response["Response"]
+ artist = await self.get_artist(album_data["artistID"])
+ album = self._get_item_mapping(
+ MediaType.ALBUM, album_data["containerID"], album_data["containerInfo"]
+ )
+ image = f"https://api.livedownloads.com{album_data['img']['url']}"
+ return [
+ self._parse_track(item, artist=artist, album=album, image_url=image)
+ for item in album_data["tracks"]
+ if item["trackID"]
+ ]
+
+ async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]:
+ """Get playlist tracks."""
+ result: list[Track] = []
+ if page > 0:
+ # paging not yet supported
+ return []
+ endpoint = f"/playlists/{prov_playlist_id}/playlist-tracks/all"
+ nugs_result = await self._get_data("stash", endpoint)
+ for index, item in enumerate(nugs_result["items"], 1):
+ track = self._parse_track(item)
+ track.position = index
+ result.append(track)
+ return result
+
+ async def get_stream_details(self, item_id: str, media_type: MediaType) -> StreamDetails:
+ """Return the content details for the given track when it will be streamed."""
+ stream_url = await self._get_stream_url(item_id)
+ return StreamDetails(
+ item_id=item_id,
+ provider=self.lookup_key,
+ audio_format=AudioFormat(
+ content_type=ContentType.UNKNOWN,
+ ),
+ stream_type=StreamType.HTTP,
+ path=stream_url,
+ )
+
+ def _parse_artist(self, artist_obj: dict[str, Any]) -> Artist:
+ """Parse nugs artist object to generic layout."""
+ artist_id = artist_obj.get("artistID") or artist_obj.get("id")
+ artist_name = artist_obj.get("artistName") or artist_obj.get("name")
+ artist = Artist(
+ item_id=str(artist_id),
+ provider=self.lookup_key,
+ name=str(artist_name),
+ provider_mappings={
+ ProviderMapping(
+ item_id=str(artist_id),
+ provider_domain=self.domain,
+ provider_instance=self.instance_id,
+ url=f"https://catalog.nugs.net/api/v1/artists?ids={artist_id}",
+ )
+ },
+ )
+ if artist_obj.get("avatarImage"):
+ artist.metadata.add_image(
+ MediaItemImage(
+ type=ImageType.THUMB,
+ path=artist_obj["avatarImage"]["url"],
+ provider=self.lookup_key,
+ remotely_accessible=True,
+ )
+ )
+ return artist
+
+ def _parse_album(self, album_obj: dict[str, Any]) -> Album:
+ """Parse nugs release/show/album object to generic album layout."""
+ item_id = album_obj.get("releaseId") or album_obj.get("id") or album_obj.get("containerID")
+ title = album_obj.get("title") or album_obj.get("containerInfo")
+ album = Album(
+ item_id=str(item_id),
+ provider=self.lookup_key,
+ name=str(title),
+ # version=album_obj["type"],
+ provider_mappings={
+ ProviderMapping(
+ item_id=str(item_id),
+ provider_domain=self.domain,
+ provider_instance=self.instance_id,
+ )
+ },
+ )
+
+ artist_obj = album_obj.get("artist", False) or {
+ "id": album_obj["artistID"],
+ "name": album_obj["artistName"],
+ }
+ if artist_obj.get("name") and artist_obj.get("id"):
+ album.artists.append(self._parse_artist(artist_obj))
+
+ path: str | None = None
+ if album_obj.get("image"):
+ path = album_obj["image"]["url"]
+ if album_obj.get("img"):
+ path = f"https://api.livedownloads.com{album_obj['img']['url']}"
+ if path:
+ album.metadata.add_image(
+ MediaItemImage(
+ type=ImageType.THUMB,
+ path=path,
+ provider=self.lookup_key,
+ remotely_accessible=True,
+ )
+ )
+ year = album_obj.get("performanceDateYear", False)
+ if not year:
+ date = album_obj.get("performanceDate", False) or album_obj.get(
+ "albumreleaseDate", False
+ )
+ if date:
+ year = date.split("-")[0]
+ if year:
+ album.year = int(year)
+
+ return album
+
+ def _parse_playlist(self, playlist_obj: dict[str, Any]) -> Playlist:
+ """Parse nugs playlist object to generic layout."""
+ return Playlist(
+ item_id=playlist_obj["id"],
+ provider=self.lookup_key,
+ name=playlist_obj["name"],
+ provider_mappings={
+ ProviderMapping(
+ item_id=playlist_obj["id"],
+ provider_domain=self.domain,
+ provider_instance=self.instance_id,
+ )
+ },
+ metadata=MediaItemMetadata(
+ images=UniqueList(
+ [
+ MediaItemImage(
+ type=ImageType.THUMB,
+ path=playlist_obj["imageUrl"],
+ provider=self.lookup_key,
+ remotely_accessible=True,
+ )
+ ]
+ ),
+ ),
+ is_editable=False,
+ )
+
+ def _parse_track(
+ self,
+ track_obj: dict[str, Any],
+ artist: Artist | None = None,
+ album: Album | ItemMapping | None = None,
+ image_url: str | None = None,
+ ) -> Track:
+ """Parse response from inconsistent nugs.net APIs to a Track model object."""
+ track_id = (
+ track_obj.get("trackId") or track_obj.get("trackID") or track_obj.get("trackLabel")
+ )
+ track_name = track_obj.get("name") or track_obj.get("songTitle")
+
+ track = Track(
+ item_id=str(track_id),
+ provider=self.lookup_key,
+ name=str(track_name),
+ provider_mappings={
+ ProviderMapping(
+ item_id=str(track_id),
+ provider_domain=self.domain,
+ provider_instance=self.instance_id,
+ available=True,
+ )
+ },
+ )
+
+ if artist:
+ track.artists.append(artist)
+ if (
+ track_obj.get("artist")
+ and isinstance(track_obj.get("artist"), dict)
+ and track_obj["artist"].get("id")
+ ):
+ track.artists.append(
+ self._get_item_mapping(
+ MediaType.ARTIST, track_obj["artist"]["id"], track_obj["artist"]["name"]
+ )
+ )
+ if not track.artists:
+ msg = "Track is missing artists"
+ raise InvalidDataError(msg)
+
+ if album:
+ track.album = album
+ if image_url is None and track_obj.get("image"):
+ image_url = track_obj["image"]["url"]
+ if image_url:
+ track.metadata.add_image(
+ MediaItemImage(
+ type=ImageType.THUMB,
+ path=image_url,
+ provider=self.lookup_key,
+ remotely_accessible=True,
+ )
+ )
+ duration = track_obj.get("durationSeconds") or track_obj.get("totalRunningTime")
+ if duration:
+ track.duration = int(duration)
+ return track
+
+ async def _get_stream_url(self, item_id: str) -> Any:
+ subscription_info = await self._get_data("subscription", "")
+ dt_start = datetime.strptime(subscription_info["startedAt"], "%m/%d/%Y %H:%M:%S").replace(
+ tzinfo=UTC
+ )
+ dt_end = datetime.strptime(subscription_info["endsAt"], "%m/%d/%Y %H:%M:%S").replace(
+ tzinfo=UTC
+ )
+ user_info = await self._get_data("user", "")
+ url = "https://streamapi.nugs.net/bigriver/subplayer.aspx"
+ timeout = ClientTimeout(total=120)
+ params = {
+ "platformID": -1,
+ "app": 1,
+ "HLS": 1,
+ "orgn": "websdk",
+ "method": "subPlayer",
+ "trackId": item_id,
+ "subCostplanIDAccessList": subscription_info["plan"]["id"],
+ "startDateStamp": int(dt_start.timestamp()),
+ "endDateStamp": int(dt_end.timestamp()),
+ "nn_userID": user_info["userId"],
+ "subscriptionID": subscription_info["legacySubscriptionId"],
+ }
+ async with (
+ self.mass.http_session.get(url, params=params, ssl=True, timeout=timeout) as response,
+ ):
+ response.raise_for_status()
+ content = await response.text()
+ stream = json_loads(content)
+ if not stream.get("streamLink"):
+ raise MediaNotFoundError("No stream found for song %s.", item_id)
+ return stream["streamLink"]
+
+ def _get_item_mapping(self, media_type: MediaType, key: str, name: str) -> ItemMapping:
+ return ItemMapping(
+ media_type=media_type,
+ item_id=key,
+ provider=self.lookup_key,
+ name=name,
+ )
+
+ async def login(self) -> Any:
+ """Login to nugs.net and return the token."""
+ if self._auth_token and (self._token_expiry > time()):
+ return self._auth_token
+ if not self.config.get_value(CONF_USERNAME) or not self.config.get_value(CONF_PASSWORD):
+ msg = "Invalid login credentials"
+ raise LoginFailed(msg)
+ login_data = {
+ "username": self.config.get_value(CONF_USERNAME),
+ "password": self.config.get_value(CONF_PASSWORD),
+ "scope": "offline_access nugsnet:api nugsnet:legacyapi openid profile email",
+ "grant_type": "password",
+ "client_id": "Eg7HuH873H65r5rt325UytR5429",
+ }
+ token = None
+ url = "https://id.nugs.net/connect/token"
+ timeout = ClientTimeout(total=120)
+ async with (
+ self.mass.http_session.post(
+ url, data=login_data, ssl=True, timeout=timeout
+ ) as response,
+ ):
+ # Handle errors
+ if response.status == 401:
+ raise LoginFailed("Invalid Nugs.net username or password")
+ # handle temporary server error
+ if response.status in (502, 503):
+ raise ResourceTemporarilyUnavailable(backoff_time=30)
+ response.raise_for_status()
+ token = await response.json()
+ self._auth_token = token["access_token"]
+ self._token_expiry = time() + token["expires_in"]
+ return token["access_token"]
+
+ async def _get_data(self, nugs_api: str, endpoint: str, **kwargs: Any) -> Any:
+ """Return the requested data from one of various nugs.net API."""
+ headers = {}
+ url: str | None = None
+ timeout = ClientTimeout(total=120)
+ if nugs_api in ("stash", "subscription", "user"):
+ tokeninfo = kwargs.pop("tokeninfo", None)
+ if tokeninfo is None:
+ tokeninfo = await self.login()
+ headers = {"Authorization": f"Bearer {tokeninfo}"}
+ if nugs_api == "catalog":
+ url = f"https://catalog.nugs.net/api/v1/{endpoint}"
+ if nugs_api == "stash":
+ url = f"https://stash.nugs.net/api/v1/me/{endpoint}"
+ if nugs_api == "subscription":
+ url = "https://subscriptions.nugs.net/api/v1/me/subscriptions"
+ if nugs_api == "user":
+ url = "https://stash.nugs.net/api/v1/stash"
+ if not url:
+ raise MediaNotFoundError(f"{nugs_api} not found")
+ async with (
+ self.mass.http_session.get(
+ url, headers=headers, params=kwargs, ssl=True, timeout=timeout
+ ) as response,
+ ):
+ if response.status == 404:
+ raise MediaNotFoundError(f"{url} not found")
+ response.raise_for_status()
+ return await response.json()
+
+ async def _get_all_items(
+ self, nugs_api: str, endpoint: str, **kwargs: Any
+ ) -> list[dict[str, Any]]:
+ limit = 100
+ offset = 0
+ total = 0
+ all_items = []
+ while True:
+ kwargs["limit"] = limit
+ kwargs["offset"] = offset
+ result = await self._get_data(nugs_api, endpoint, **kwargs)
+ total = result["total"]
+ all_items += result["items"]
+ if total <= offset + limit:
+ break
+ offset += limit
+ return all_items
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="200"
+ height="200"
+ viewBox="0 0 200 200"
+ fill="none"
+ version="1.1"
+ id="svg20"
+ sodipodi:docname="icon.svg"
+ inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs20" />
+ <sodipodi:namedview
+ id="namedview20"
+ pagecolor="#ffffff"
+ bordercolor="#111111"
+ borderopacity="1"
+ inkscape:showpageshadow="0"
+ inkscape:pageopacity="0"
+ inkscape:pagecheckerboard="1"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:zoom="1.1748311"
+ inkscape:cx="0"
+ inkscape:cy="3.8303379"
+ inkscape:window-width="1536"
+ inkscape:window-height="792"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg20" />
+ <mask
+ id="mask0_813_105"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="148"
+ height="35">
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M 0,34.4854 H 148 V 0 H 0 Z"
+ fill="#ffffff"
+ id="path1" />
+ </mask>
+ <g
+ mask="url(#mask0_813_105)"
+ id="g20"
+ transform="matrix(5.2340353,0,0,5.2340353,12.93915,9.5049533)"
+ inkscape:transform-center-x="22.832585"
+ inkscape:transform-center-y="-7.2393385">
+ <mask
+ id="mask1_813_105"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="34"
+ height="35">
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M 0,0.204056 H 33.2672 V 34.3754 H 0 Z"
+ fill="#ffffff"
+ id="path2" />
+ </mask>
+ <g
+ mask="url(#mask1_813_105)"
+ id="g3">
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="m 0,17.2903 c 0,9.4363 7.44629,17.0852 16.6326,17.0852 9.1867,0 16.6346,-7.6489 16.6346,-17.0852 C 33.2672,7.8537 25.8193,0.204056 16.6326,0.204056 7.44629,0.204056 0,7.8537 0,17.2903 Z"
+ fill="#ff0000"
+ id="path3" />
+ </g>
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M 26.5848,9.52843 C 26.3701,7.92496 25.4683,6.47596 24.2031,5.65872 22.9674,4.85841 21.0915,5.04043 19.7249,5.17776 19.0467,5.24571 18.3296,5.40018 17.6138,5.6026 16.5045,5.62239 15.2633,6.0854 14.2466,6.41189 13.436,6.6741 12.0526,7.05058 11.368,7.55521 10.8966,7.90353 10.607,8.32532 10.428,8.79261 9.4762,10.4626 9.49825,11.9155 9.38144,14.0789 c -0.04073,0.7462 -1.20347,3.836 0.30196,4.378 0.15674,0.0553 0.3224,0.1024 0.4943,0.1349 0.1803,0.4471 0.4012,0.8758 0.6758,1.2743 0.0272,0.0424 0.0618,0.0775 0.0904,0.1179 0.1549,0.4039 0.0177,0.0094 0.1549,0.4039 0.6153,1.7755 -1.15337,2.2813 1.7476,2.4784 0.8151,0.0551 1.5748,0.028 2.2796,-0.0465 1.8324,-0.0647 3.6454,-0.4599 4.4923,0.9885 0.3574,2.1642 -1.3573,2.6778 -3.4684,3.0324 -1.3189,0.1545 -2.9052,0.2347 -4.8218,0.2347 H 2.99976 l 1.21499,1.6231 h 7.11335 c 11.0603,0 12.4801,-2.8003 13.5049,-7.4089 l 0.0191,-0.0898 V 21.077 c 0.8435,-1.3772 2.9266,-2.5379 2.9916,-4.3646 -0.6691,0.6701 -1.2349,-7.00216 -1.2589,-7.18397 z"
+ fill="#000000"
+ id="path4" />
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="m 23.7345,24.0365 c 0.0194,-0.1188 0.0178,-0.2445 -0.003,-0.3706 -0.0046,0.1231 -0.0046,0.2471 0.003,0.3706 z"
+ fill="#000000"
+ id="path5" />
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="m 9.23315,14.351 c 1.11605,-0.2462 2.30765,-0.5846 3.21845,-1.36 0.4102,-0.3487 1.1479,-1.2533 1.7408,-1.1594 1.0857,0.171 0.4395,1.4708 -0.2296,1.7208 -0.4575,0.1695 -0.9899,0.041 -1.4576,0.1436 -0.461,0.1014 -0.8945,0.3445 -1.3381,0.5177 -0.5541,0.2159 -1.35069,0.6477 -1.93395,0.3261"
+ fill="#ffffff"
+ id="path6" />
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M 15.8761,4.80792 C 15.12,4.97933 14.5892,5.42927 14.2141,6.28019 l 0.0068,0.13182 c 2.2355,-0.11387 6.092,0.91886 7.271,3.4967 l 1.484,-0.71604 C 21.694,6.38772 18.4394,5.12992 15.8761,4.80792 Z"
+ fill="#a8a8a8"
+ id="path7" />
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="m 28.8575,13.3662 c 0,2.6592 -1.954,4.8138 -4.3653,4.8138 -2.4107,0 -4.3645,-2.1546 -4.3645,-4.8138 0,-2.6583 1.9538,-4.81371 4.3645,-4.81371 2.4113,0 4.3653,2.15541 4.3653,4.81371 z"
+ fill="#ffffff"
+ id="path8" />
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="m 24.4935,9.03566 c -2.1473,0 -3.8943,1.94264 -3.8943,4.33044 0,2.3879 1.747,4.3305 3.8943,4.3305 2.1477,0 3.8947,-1.9426 3.8947,-4.3305 0,-2.3878 -1.747,-4.33044 -3.8947,-4.33044 z m 3e-4,9.62734 c -2.6662,0 -4.8351,-2.3763 -4.8351,-5.2971 0,-2.9209 2.1689,-5.2969 4.8351,-5.2969 2.6664,0 4.8357,2.376 4.8357,5.2969 0,2.9208 -2.1693,5.2971 -4.8357,5.2971 z"
+ fill="#000000"
+ id="path9" />
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="m 26.9848,13.4421 c 0,0.8811 -0.6484,1.5961 -1.4474,1.5961 -0.7988,0 -1.4471,-0.715 -1.4471,-1.5961 0,-0.8814 0.6483,-1.5956 1.4471,-1.5956 0.799,0 1.4474,0.7142 1.4474,1.5956 z"
+ fill="#000000"
+ id="path10" />
+ <mask
+ id="mask2_813_105"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="151"
+ height="35">
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="m 0,34.4853 150.714,-0.11 V -9.15527e-5 H 0 Z"
+ fill="#ffffff"
+ id="path15" />
+ </mask>
+ </g>
+</svg>