-
-
Notifications
You must be signed in to change notification settings - Fork 497
feat(assets): fix animated asset extensions and add size/extension property #3294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 14 commits
ea6cacc
7c3e47e
8b3fcc6
67b0210
9741d78
2f09700
c360151
ee12a06
f186811
fdd2462
f76f4bb
920c9ee
1b2f421
44fbc96
4bed875
69f8b09
ce21811
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -156,51 +156,61 @@ class Asset(AssetMixin): | |
| "_url", | ||
| "_animated", | ||
| "_key", | ||
| "_extension", | ||
| "_size", | ||
| ) | ||
|
|
||
| BASE = "https://cdn.discordapp.com" | ||
|
|
||
| def __init__(self, state, *, url: str, key: str, animated: bool = False): | ||
| def __init__( | ||
| self, | ||
| state, | ||
| *, | ||
| url: str, | ||
| key: str, | ||
| animated: bool = False, | ||
| extension: str | None = MISSING, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why ae you using MISSING here ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for the soundboard because it doesn't have an extension. |
||
| size: int | None = None, | ||
| ): | ||
| self._state = state | ||
| self._url = url | ||
| self._animated = animated | ||
| self._key = key | ||
| self._size = size | ||
|
|
||
| if extension is MISSING: | ||
| extension = "webp" if animated else "png" | ||
| self._extension = extension or "" | ||
|
|
||
| @classmethod | ||
| def _from_default_avatar(cls, state, index: int) -> Asset: | ||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/embed/avatars/{index}.png", | ||
| key=str(index), | ||
| animated=False, | ||
| state, url=f"{cls.BASE}/embed/avatars/{index}", key=str(index), size=1024 | ||
| ) | ||
|
|
||
| @classmethod | ||
| def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset: | ||
| animated = avatar.startswith("a_") | ||
| format = "gif" if animated else "png" | ||
| url = f"{cls.BASE}/avatars/{user_id}/{avatar}" | ||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024", | ||
| key=avatar, | ||
| animated=animated, | ||
| state, url=url, key=avatar, animated=avatar.startswith("a_"), size=1024 | ||
|
Paillat-dev marked this conversation as resolved.
Outdated
|
||
| ) | ||
|
|
||
| @classmethod | ||
| def _from_avatar_decoration( | ||
| cls, state, user_id: int, avatar_decoration: str | ||
| ) -> Asset: | ||
| animated = avatar_decoration.startswith("a_") | ||
| endpoint = ( | ||
| "avatar-decoration-presets" | ||
| # if avatar_decoration.startswith(("v3", "v2")) | ||
| # else f"avatar-decorations/{user_id}" | ||
| ) | ||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/{endpoint}/{avatar_decoration}.png?size=1024", | ||
| url=f"{cls.BASE}/{endpoint}/{avatar_decoration}", | ||
| key=avatar_decoration, | ||
| animated=animated, | ||
| animated=avatar_decoration.startswith("a_"), | ||
| extension="png", | ||
| size=1024, | ||
| ) | ||
|
|
||
| @classmethod | ||
|
|
@@ -225,60 +235,55 @@ def _from_user_primary_guild_tag( | |
| """ | ||
| return cls( | ||
| state, | ||
| url=f"{Asset.BASE}/guild-tag-badges/{identity_guild_id}/{badge_id}.png?size=256", | ||
| url=f"{Asset.BASE}/guild-tag-badges/{identity_guild_id}/{badge_id}", | ||
| key=badge_id, | ||
| animated=False, | ||
| extension="png", | ||
| size=256, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def _from_guild_avatar( | ||
| cls, state, guild_id: int, member_id: int, avatar: str | ||
| ) -> Asset: | ||
| animated = avatar.startswith("a_") | ||
| format = "gif" if animated else "png" | ||
| url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}" | ||
|
SilberGecko6917 marked this conversation as resolved.
Outdated
|
||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.{format}?size=1024", | ||
| key=avatar, | ||
| animated=animated, | ||
| state, url=url, key=avatar, animated=avatar.startswith("a_"), size=1024 | ||
| ) | ||
|
|
||
| @classmethod | ||
| def _from_guild_banner( | ||
| cls, state, guild_id: int, member_id: int, banner: str | ||
| ) -> Asset: | ||
| animated = banner.startswith("a_") | ||
| format = "gif" if animated else "png" | ||
| url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}" | ||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=512", | ||
| key=banner, | ||
| animated=animated, | ||
| state, url=url, key=banner, animated=banner.startswith("a_"), size=512 | ||
| ) | ||
|
|
||
| @classmethod | ||
| def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset: | ||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/{path}-icons/{object_id}/{icon_hash}.png?size=1024", | ||
| url=f"{cls.BASE}/{path}-icons/{object_id}/{icon_hash}", | ||
| key=icon_hash, | ||
| animated=False, | ||
| extension="png", | ||
| size=1024, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def _from_cover_image(cls, state, object_id: int, cover_image_hash: str) -> Asset: | ||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/app-assets/{object_id}/store/{cover_image_hash}.png?size=1024", | ||
| url=f"{cls.BASE}/app-assets/{object_id}/store/{cover_image_hash}", | ||
| key=cover_image_hash, | ||
| animated=False, | ||
| extension="png", | ||
| size=1024, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def _from_collectible( | ||
| cls, state: ConnectionState, asset: str, animated: bool = False | ||
| ) -> Asset: | ||
| name = "static.png" if not animated else "asset.webm" | ||
| name = "static" if not animated else "asset" | ||
|
SilberGecko6917 marked this conversation as resolved.
|
||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/assets/collectibles/{asset}{name}", | ||
|
|
@@ -289,47 +294,41 @@ def _from_collectible( | |
| @classmethod | ||
| def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset: | ||
| animated = False | ||
| format = "png" | ||
| if path == "banners": | ||
| animated = image.startswith("a_") | ||
| format = "gif" if animated else "png" | ||
|
|
||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/{path}/{guild_id}/{image}.{format}?size=1024", | ||
| key=image, | ||
| animated=animated, | ||
| ) | ||
| url = f"{cls.BASE}/{path}/{guild_id}/{image}" | ||
| return cls(state, url=url, key=image, animated=animated, size=1024) | ||
|
|
||
| @classmethod | ||
| def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset: | ||
| animated = icon_hash.startswith("a_") | ||
| format = "gif" if animated else "png" | ||
| url = f"{cls.BASE}/icons/{guild_id}/{icon_hash}" | ||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024", | ||
| url=url, | ||
| key=icon_hash, | ||
| animated=animated, | ||
| animated=icon_hash.startswith("a_"), | ||
| size=1024, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def _from_sticker_banner(cls, state, banner: int) -> Asset: | ||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/app-assets/710982414301790216/store/{banner}.png", | ||
| url=f"{cls.BASE}/app-assets/710982414301790216/store/{banner}", | ||
| key=str(banner), | ||
| animated=False, | ||
| extension="png", | ||
| ) | ||
|
|
||
| @classmethod | ||
| def _from_user_banner(cls, state, user_id: int, banner_hash: str) -> Asset: | ||
| animated = banner_hash.startswith("a_") | ||
| format = "gif" if animated else "png" | ||
| url = f"{cls.BASE}/banners/{user_id}/{banner_hash}" | ||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/banners/{user_id}/{banner_hash}.{format}?size=512", | ||
| url=url, | ||
| key=banner_hash, | ||
| animated=animated, | ||
| animated=banner_hash.startswith("a_"), | ||
| size=512, | ||
| ) | ||
|
|
||
| @classmethod | ||
|
|
@@ -338,9 +337,9 @@ def _from_scheduled_event_image( | |
| ) -> Asset: | ||
| return cls( | ||
| state, | ||
| url=f"{cls.BASE}/guild-events/{event_id}/{cover_hash}.png", | ||
| url=f"{cls.BASE}/guild-events/{event_id}/{cover_hash}", | ||
| key=cover_hash, | ||
| animated=False, | ||
| extension="png", | ||
| ) | ||
|
|
||
| @classmethod | ||
|
|
@@ -349,34 +348,58 @@ def _from_soundboard_sound(cls, state, sound_id: int) -> Asset: | |
| state, | ||
| url=f"{cls.BASE}/soundboard-sounds/{sound_id}", | ||
| key=str(sound_id), | ||
| extension=None, | ||
| ) | ||
|
|
||
| def __str__(self) -> str: | ||
| return self._url | ||
| return self.url | ||
|
|
||
| def __len__(self) -> int: | ||
| return len(self._url) | ||
| return len(self.url) | ||
|
|
||
| def __repr__(self): | ||
| shorten = self._url.replace(self.BASE, "") | ||
| shorten = self.url.replace(self.BASE, "") | ||
| return f"<Asset url={shorten!r}>" | ||
|
|
||
| def __eq__(self, other): | ||
| return isinstance(other, Asset) and self._url == other._url | ||
| return isinstance(other, Asset) and self.url == other.url | ||
|
|
||
| def __hash__(self): | ||
| return hash(self._url) | ||
| return hash(self.url) | ||
|
|
||
| @property | ||
| def url(self) -> str: | ||
| """Returns the underlying URL of the asset.""" | ||
| return self._url | ||
|
|
||
| query: dict[str, str] = {} | ||
| if self._size is not None: | ||
| query["size"] = str(self._size) | ||
| if self._animated and self._extension == "webp": | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this mean that something that has animated to true but extension not to webm it would silently fail to non animated without warning ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we only need
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sending it with a non-animated WebP won't do anything. So it could in theory always set it 🤷
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better not unnecessarily pass something when not needed
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes. please dont pass it if its not supported |
||
| query["animated"] = "true" | ||
|
|
||
| parsed = yarl.URL(self._url) | ||
| if self._extension: | ||
| path, _ = os.path.splitext(parsed.path) | ||
| new_path = f"{path}.{self._extension}" | ||
| else: # for example soundboard | ||
| new_path = parsed.path | ||
| return str(parsed.with_path(new_path).with_query(query)) | ||
|
|
||
| @property | ||
| def key(self) -> str: | ||
| """Returns the identifying key of the asset.""" | ||
| return self._key | ||
|
|
||
| @property | ||
| def extension(self) -> str | None: | ||
| """Returns the file extension of the asset.""" | ||
| return self._extension | ||
|
|
||
| @property | ||
| def size(self) -> int | None: | ||
| """Returns the size of the asset.""" | ||
| return self._size | ||
|
|
||
| def is_animated(self) -> bool: | ||
| """Returns whether the asset is animated.""" | ||
| return self._animated | ||
|
|
@@ -411,39 +434,42 @@ def replace( | |
| InvalidArgument | ||
| An invalid size or format was passed. | ||
| """ | ||
| url = yarl.URL(self._url) | ||
| path, _ = os.path.splitext(url.path) | ||
|
|
||
| extension = self._extension | ||
| new_size = self._size | ||
|
|
||
| if format is not MISSING: | ||
| if self._animated: | ||
| if format not in VALID_ASSET_FORMATS: | ||
| raise InvalidArgument( | ||
| f"format must be one of {VALID_ASSET_FORMATS}" | ||
| ) | ||
| url = url.with_path(f"{path}.{format}") | ||
| extension = format | ||
| elif static_format is MISSING: | ||
| if format not in VALID_STATIC_FORMATS: | ||
| raise InvalidArgument( | ||
| f"format must be one of {VALID_STATIC_FORMATS}" | ||
| ) | ||
| url = url.with_path(f"{path}.{format}") | ||
|
|
||
| extension = format | ||
| if static_format is not MISSING and not self._animated: | ||
| if static_format not in VALID_STATIC_FORMATS: | ||
| raise InvalidArgument( | ||
| f"static_format must be one of {VALID_STATIC_FORMATS}" | ||
| ) | ||
| url = url.with_path(f"{path}.{static_format}") | ||
|
|
||
| extension = static_format | ||
| if size is not MISSING: | ||
| if not utils.valid_icon_size(size): | ||
| raise InvalidArgument("size must be a power of 2 between 16 and 4096") | ||
| url = url.with_query(size=size) | ||
| else: | ||
| url = url.with_query(url.raw_query_string) | ||
|
|
||
| url = str(url) | ||
| return Asset(state=self._state, url=url, key=self._key, animated=self._animated) | ||
| new_size = size | ||
|
|
||
| return Asset( | ||
| state=self._state, | ||
| url=self._url, | ||
| key=self._key, | ||
| animated=self._animated, | ||
| extension=extension, | ||
| size=new_size, | ||
| ) | ||
|
|
||
| def with_size(self, size: int, /) -> Asset: | ||
| """Returns a new asset with the specified size. | ||
|
|
@@ -466,8 +492,14 @@ def with_size(self, size: int, /) -> Asset: | |
| if not utils.valid_icon_size(size): | ||
| raise InvalidArgument("size must be a power of 2 between 16 and 4096") | ||
|
|
||
| url = str(yarl.URL(self._url).with_query(size=size)) | ||
| return Asset(state=self._state, url=url, key=self._key, animated=self._animated) | ||
| return Asset( | ||
| state=self._state, | ||
| url=self._url, | ||
| key=self._key, | ||
| animated=self._animated, | ||
| extension=self._extension, | ||
| size=size, | ||
| ) | ||
|
|
||
| def with_format(self, format: ValidAssetFormatTypes, /) -> Asset: | ||
| """Returns a new asset with the specified format. | ||
|
|
@@ -493,11 +525,14 @@ def with_format(self, format: ValidAssetFormatTypes, /) -> Asset: | |
| raise InvalidArgument(f"format must be one of {VALID_ASSET_FORMATS}") | ||
| elif format not in VALID_STATIC_FORMATS: | ||
| raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}") | ||
|
|
||
| url = yarl.URL(self._url) | ||
| path, _ = os.path.splitext(url.path) | ||
| url = str(url.with_path(f"{path}.{format}").with_query(url.raw_query_string)) | ||
| return Asset(state=self._state, url=url, key=self._key, animated=self._animated) | ||
| return Asset( | ||
| state=self._state, | ||
| url=self._url, | ||
| key=self._key, | ||
| animated=self._animated, | ||
| extension=format, | ||
| size=self._size, | ||
| ) | ||
|
|
||
| def with_static_format(self, format: ValidStaticFormatTypes, /) -> Asset: | ||
| """Returns a new asset with the specified static format. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.