From ea6caccc60066c8d38dddb94bfcd87bef7bc2cb0 Mon Sep 17 00:00:00 2001 From: SilberGecko6917 Date: Thu, 2 Jul 2026 15:07:25 +0200 Subject: [PATCH 01/11] replace gif with webp --- discord/asset.py | 65 +++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/discord/asset.py b/discord/asset.py index 659d6dd5eb..817828fa8a 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -178,13 +178,11 @@ def _from_default_avatar(cls, state, index: int) -> Asset: @classmethod def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset: animated = avatar.startswith("a_") - format = "gif" if animated else "png" - return cls( - state, - url=f"{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024", - key=avatar, - animated=animated, - ) + if animated: + url = f"{cls.BASE}/avatars/{user_id}/{avatar}.webp?animated=true&size=1024" + else: + url = f"{cls.BASE}/avatars/{user_id}/{avatar}.png?size=1024" + return cls(state, url=url, key=avatar, animated=animated) @classmethod def _from_avatar_decoration( @@ -235,26 +233,22 @@ 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" - return cls( - state, - url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.{format}?size=1024", - key=avatar, - animated=animated, - ) + if animated: + url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.webp?animated=true&size=1024" + else: + url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.png?size=1024" + return cls(state, url=url, key=avatar, animated=animated) @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" - return cls( - state, - url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=512", - key=banner, - animated=animated, - ) + if animated: + url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.webp?animated=true&size=512" + else: + url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.png?size=512" + return cls(state, url=url, key=banner, animated=animated) @classmethod def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset: @@ -292,11 +286,12 @@ def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset format = "png" if path == "banners": animated = image.startswith("a_") - format = "gif" if animated else "png" + format = "webp" if animated else "png" + extra = "&animated=true" if animated else "" return cls( state, - url=f"{cls.BASE}/{path}/{guild_id}/{image}.{format}?size=1024", + url=f"{cls.BASE}/{path}/{guild_id}/{image}.{format}?size=1024{extra}", key=image, animated=animated, ) @@ -304,13 +299,11 @@ def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset @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" - return cls( - state, - url=f"{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024", - key=icon_hash, - animated=animated, - ) + if animated: + url = f"{cls.BASE}/icons/{guild_id}/{icon_hash}.webp?animated=true&size=1024" + else: + url = f"{cls.BASE}/icons/{guild_id}/{icon_hash}.png?size=1024" + return cls(state, url=url, key=icon_hash, animated=animated) @classmethod def _from_sticker_banner(cls, state, banner: int) -> Asset: @@ -324,13 +317,11 @@ def _from_sticker_banner(cls, state, banner: int) -> Asset: @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" - return cls( - state, - url=f"{cls.BASE}/banners/{user_id}/{banner_hash}.{format}?size=512", - key=banner_hash, - animated=animated, - ) + if animated: + url = f"{cls.BASE}/banners/{user_id}/{banner_hash}.webp?animated=true&size=512" + else: + url = f"{cls.BASE}/banners/{user_id}/{banner_hash}.png?size=512" + return cls(state, url=url, key=banner_hash, animated=animated) @classmethod def _from_scheduled_event_image( From 7c3e47e9a6c39c2c8aae76ecd314369fff62c406 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:27:29 +0000 Subject: [PATCH 02/11] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/asset.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/asset.py b/discord/asset.py index 817828fa8a..1983c90916 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -300,7 +300,9 @@ def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset: animated = icon_hash.startswith("a_") if animated: - url = f"{cls.BASE}/icons/{guild_id}/{icon_hash}.webp?animated=true&size=1024" + url = ( + f"{cls.BASE}/icons/{guild_id}/{icon_hash}.webp?animated=true&size=1024" + ) else: url = f"{cls.BASE}/icons/{guild_id}/{icon_hash}.png?size=1024" return cls(state, url=url, key=icon_hash, animated=animated) From 8b3fcc62721799db8df47dd4692bc375d43b6759 Mon Sep 17 00:00:00 2001 From: SilberGecko6917 Date: Thu, 2 Jul 2026 15:29:33 +0200 Subject: [PATCH 03/11] add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7647529eee..331a9618dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ These changes are available on the `master` branch, but have not yet been releas ### Fixed +- Fix animated asset url extension from `.gif` to `.webp` + ([#3294](https://github.com/Pycord-Development/pycord/pull/3294)) - Fix an attribute error in `RoleColours.is_holographic()` when `secondary` or `tertiary` is `None`. ([#3268](https://github.com/Pycord-Development/pycord/pull/3268)) From 67b021037f23a438592138b085c301d28d536908 Mon Sep 17 00:00:00 2001 From: SilberGecko6917 Date: Sat, 4 Jul 2026 20:26:06 +0200 Subject: [PATCH 04/11] add new properties and change some things --- CHANGELOG.md | 2 ++ discord/asset.py | 56 +++++++++++++++++++++++++++--------------------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 331a9618dc..f063feb315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ These changes are available on the `master` branch, but have not yet been releas - Add `RoleColours.HOLOGRAPHIC_PRIMARY`, `RoleColours.HOLOGRAPHIC_SECONDARY`, and `RoleColours.HOLOGRAPHIC_TERTIARY` class constants. ([#3268](https://github.com/Pycord-Development/pycord/pull/3268)) +- Add `Asset.size`, `Asset.extention` properties. + ([#3294](https://github.com/Pycord-Development/pycord/pull/3294)) ### Changed diff --git a/discord/asset.py b/discord/asset.py index 1983c90916..987b8aa041 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -178,10 +178,10 @@ def _from_default_avatar(cls, state, index: int) -> Asset: @classmethod def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset: animated = avatar.startswith("a_") + format = "webp" if animated else "png" + url = f"{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024" if animated: - url = f"{cls.BASE}/avatars/{user_id}/{avatar}.webp?animated=true&size=1024" - else: - url = f"{cls.BASE}/avatars/{user_id}/{avatar}.png?size=1024" + url += "&animated=true" return cls(state, url=url, key=avatar, animated=animated) @classmethod @@ -233,10 +233,10 @@ def _from_guild_avatar( cls, state, guild_id: int, member_id: int, avatar: str ) -> Asset: animated = avatar.startswith("a_") + format = "webp" if animated else "png" + url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.{format}?size=1024" if animated: - url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.webp?animated=true&size=1024" - else: - url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.png?size=1024" + url += "&animated=true" return cls(state, url=url, key=avatar, animated=animated) @classmethod @@ -244,10 +244,10 @@ def _from_guild_banner( cls, state, guild_id: int, member_id: int, banner: str ) -> Asset: animated = banner.startswith("a_") + format = "webp" if animated else "png" + url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=512" if animated: - url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.webp?animated=true&size=512" - else: - url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.png?size=512" + url += "&animated=true" return cls(state, url=url, key=banner, animated=animated) @classmethod @@ -288,23 +288,18 @@ def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset animated = image.startswith("a_") format = "webp" if animated else "png" - extra = "&animated=true" if animated else "" - return cls( - state, - url=f"{cls.BASE}/{path}/{guild_id}/{image}.{format}?size=1024{extra}", - key=image, - animated=animated, - ) + url = f"{cls.BASE}/{path}/{guild_id}/{image}.{format}?size=1024" + if animated: + url += "&animated=true" + return cls(state, url=url, key=image, animated=animated) @classmethod def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset: animated = icon_hash.startswith("a_") + format = "webp" if animated else "png" + url = f"{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024" if animated: - url = ( - f"{cls.BASE}/icons/{guild_id}/{icon_hash}.webp?animated=true&size=1024" - ) - else: - url = f"{cls.BASE}/icons/{guild_id}/{icon_hash}.png?size=1024" + url += "&animated=true" return cls(state, url=url, key=icon_hash, animated=animated) @classmethod @@ -319,10 +314,10 @@ def _from_sticker_banner(cls, state, banner: int) -> Asset: @classmethod def _from_user_banner(cls, state, user_id: int, banner_hash: str) -> Asset: animated = banner_hash.startswith("a_") + format = "webp" if animated else "png" + url = f"{cls.BASE}/banners/{user_id}/{banner_hash}.{format}?size=512" if animated: - url = f"{cls.BASE}/banners/{user_id}/{banner_hash}.webp?animated=true&size=512" - else: - url = f"{cls.BASE}/banners/{user_id}/{banner_hash}.png?size=512" + url += "&animated=true" return cls(state, url=url, key=banner_hash, animated=animated) @classmethod @@ -370,6 +365,19 @@ def key(self) -> str: """Returns the identifying key of the asset.""" return self._key + @property + def extension(self) -> Literal["webp", "png"]: + """Returns the extension of the asset.""" + return "webp" if self._animated else "png" + + @property + def size(self) -> int | None: + """Returns the size of the asset.""" + + query = yarl.URL(self._url).query + size = query.get("size") + return int(size) if size is not None else None + def is_animated(self) -> bool: """Returns whether the asset is animated.""" return self._animated From 9741d789053fc8ad42891d6a5ef064be271a656d Mon Sep 17 00:00:00 2001 From: SilberGecko6917 Date: Sun, 5 Jul 2026 01:00:08 +0200 Subject: [PATCH 05/11] get size and extension in init --- discord/asset.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/discord/asset.py b/discord/asset.py index 987b8aa041..da37589b00 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -156,6 +156,8 @@ class Asset(AssetMixin): "_url", "_animated", "_key", + "_extension", + "_size", ) BASE = "https://cdn.discordapp.com" @@ -166,6 +168,13 @@ def __init__(self, state, *, url: str, key: str, animated: bool = False): self._animated = animated self._key = key + parsed = yarl.URL(url) + _, ext = os.path.splitext(parsed.path) + self._extension = ext.lstrip(".") + + size = parsed.query.get("size") + self._size = int(size) if size is not None else None + @classmethod def _from_default_avatar(cls, state, index: int) -> Asset: return cls( @@ -366,17 +375,14 @@ def key(self) -> str: return self._key @property - def extension(self) -> Literal["webp", "png"]: + def extension(self) -> str: """Returns the extension of the asset.""" - return "webp" if self._animated else "png" + return self._extension @property def size(self) -> int | None: """Returns the size of the asset.""" - - query = yarl.URL(self._url).query - size = query.get("size") - return int(size) if size is not None else None + return self._size def is_animated(self) -> bool: """Returns whether the asset is animated.""" From 2f09700bcff08620913e3771ad17a8190ca8996c Mon Sep 17 00:00:00 2001 From: SilberGecko6917 Date: Sun, 5 Jul 2026 14:59:47 +0200 Subject: [PATCH 06/11] requested changes --- discord/asset.py | 106 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 20 deletions(-) diff --git a/discord/asset.py b/discord/asset.py index da37589b00..3519fa9ec2 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -162,18 +162,22 @@ class Asset(AssetMixin): 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 = None, + size: int | None = None, + ): self._state = state self._url = url self._animated = animated self._key = key - - parsed = yarl.URL(url) - _, ext = os.path.splitext(parsed.path) - self._extension = ext.lstrip(".") - - size = parsed.query.get("size") - self._size = int(size) if size is not None else None + self._extension = extension + self._size = size @classmethod def _from_default_avatar(cls, state, index: int) -> Asset: @@ -182,6 +186,7 @@ def _from_default_avatar(cls, state, index: int) -> Asset: url=f"{cls.BASE}/embed/avatars/{index}.png", key=str(index), animated=False, + extension="png", ) @classmethod @@ -191,7 +196,9 @@ def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset: url = f"{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024" if animated: url += "&animated=true" - return cls(state, url=url, key=avatar, animated=animated) + return cls( + state, url=url, key=avatar, animated=animated, extension=format, size=1024 + ) @classmethod def _from_avatar_decoration( @@ -208,6 +215,8 @@ def _from_avatar_decoration( url=f"{cls.BASE}/{endpoint}/{avatar_decoration}.png?size=1024", key=avatar_decoration, animated=animated, + extension="png", + size=1024, ) @classmethod @@ -235,6 +244,8 @@ def _from_user_primary_guild_tag( url=f"{Asset.BASE}/guild-tag-badges/{identity_guild_id}/{badge_id}.png?size=256", key=badge_id, animated=False, + extension="png", + size=256, ) @classmethod @@ -246,7 +257,9 @@ def _from_guild_avatar( url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.{format}?size=1024" if animated: url += "&animated=true" - return cls(state, url=url, key=avatar, animated=animated) + return cls( + state, url=url, key=avatar, animated=animated, extension=format, size=1024 + ) @classmethod def _from_guild_banner( @@ -257,7 +270,9 @@ def _from_guild_banner( url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=512" if animated: url += "&animated=true" - return cls(state, url=url, key=banner, animated=animated) + return cls( + state, url=url, key=banner, animated=animated, extension=format, size=512 + ) @classmethod def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset: @@ -266,6 +281,8 @@ def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset: url=f"{cls.BASE}/{path}-icons/{object_id}/{icon_hash}.png?size=1024", key=icon_hash, animated=False, + extension="png", + size=1024, ) @classmethod @@ -275,6 +292,8 @@ def _from_cover_image(cls, state, object_id: int, cover_image_hash: str) -> Asse url=f"{cls.BASE}/app-assets/{object_id}/store/{cover_image_hash}.png?size=1024", key=cover_image_hash, animated=False, + extension="png", + size=1024, ) @classmethod @@ -287,6 +306,7 @@ def _from_collectible( url=f"{cls.BASE}/assets/collectibles/{asset}{name}", key=asset, animated=animated, + extension="webm" if animated else "png", ) @classmethod @@ -300,7 +320,9 @@ def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset url = f"{cls.BASE}/{path}/{guild_id}/{image}.{format}?size=1024" if animated: url += "&animated=true" - return cls(state, url=url, key=image, animated=animated) + return cls( + state, url=url, key=image, animated=animated, extension=format, size=1024 + ) @classmethod def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset: @@ -309,7 +331,14 @@ def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset: url = f"{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024" if animated: url += "&animated=true" - return cls(state, url=url, key=icon_hash, animated=animated) + return cls( + state, + url=url, + key=icon_hash, + animated=animated, + extension=format, + size=1024, + ) @classmethod def _from_sticker_banner(cls, state, banner: int) -> Asset: @@ -318,6 +347,7 @@ def _from_sticker_banner(cls, state, banner: int) -> Asset: url=f"{cls.BASE}/app-assets/710982414301790216/store/{banner}.png", key=str(banner), animated=False, + extension="png", ) @classmethod @@ -327,7 +357,14 @@ def _from_user_banner(cls, state, user_id: int, banner_hash: str) -> Asset: url = f"{cls.BASE}/banners/{user_id}/{banner_hash}.{format}?size=512" if animated: url += "&animated=true" - return cls(state, url=url, key=banner_hash, animated=animated) + return cls( + state, + url=url, + key=banner_hash, + animated=animated, + extension=format, + size=512, + ) @classmethod def _from_scheduled_event_image( @@ -338,6 +375,7 @@ def _from_scheduled_event_image( url=f"{cls.BASE}/guild-events/{event_id}/{cover_hash}.png", key=cover_hash, animated=False, + extension="png", ) @classmethod @@ -346,6 +384,8 @@ 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, + size=None, ) def __str__(self) -> str: @@ -375,7 +415,7 @@ def key(self) -> str: return self._key @property - def extension(self) -> str: + def extension(self) -> str | None: """Returns the extension of the asset.""" return self._extension @@ -421,6 +461,9 @@ def replace( 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: @@ -428,29 +471,38 @@ def replace( 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) + new_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) + return Asset( + state=self._state, + url=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. @@ -474,7 +526,14 @@ def with_size(self, size: int, /) -> Asset: 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=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. @@ -504,7 +563,14 @@ def with_format(self, format: ValidAssetFormatTypes, /) -> Asset: 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=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. From c36015134a7ef9dde4c1c9336b2b2a2759abc5bd Mon Sep 17 00:00:00 2001 From: SilberGecko6917 Date: Sun, 5 Jul 2026 15:00:17 +0200 Subject: [PATCH 07/11] fix typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f063feb315..5f3a0567b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ These changes are available on the `master` branch, but have not yet been releas - Add `RoleColours.HOLOGRAPHIC_PRIMARY`, `RoleColours.HOLOGRAPHIC_SECONDARY`, and `RoleColours.HOLOGRAPHIC_TERTIARY` class constants. ([#3268](https://github.com/Pycord-Development/pycord/pull/3268)) -- Add `Asset.size`, `Asset.extention` properties. +- Add `Asset.size`, `Asset.extension` properties. ([#3294](https://github.com/Pycord-Development/pycord/pull/3294)) ### Changed From ee12a060b22563e29a944fbe63f4628616d9da20 Mon Sep 17 00:00:00 2001 From: SilberGecko6917 Date: Sun, 5 Jul 2026 19:04:07 +0200 Subject: [PATCH 08/11] review changes --- CHANGELOG.md | 6 +++--- discord/asset.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f3a0567b1..58c97e9c53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,15 +15,13 @@ These changes are available on the `master` branch, but have not yet been releas - Add `RoleColours.HOLOGRAPHIC_PRIMARY`, `RoleColours.HOLOGRAPHIC_SECONDARY`, and `RoleColours.HOLOGRAPHIC_TERTIARY` class constants. ([#3268](https://github.com/Pycord-Development/pycord/pull/3268)) -- Add `Asset.size`, `Asset.extension` properties. +- Add `Asset.size` and `Asset.extension` properties. ([#3294](https://github.com/Pycord-Development/pycord/pull/3294)) ### Changed ### Fixed -- Fix animated asset url extension from `.gif` to `.webp` - ([#3294](https://github.com/Pycord-Development/pycord/pull/3294)) - Fix an attribute error in `RoleColours.is_holographic()` when `secondary` or `tertiary` is `None`. ([#3268](https://github.com/Pycord-Development/pycord/pull/3268)) @@ -58,6 +56,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#3278](https://github.com/Pycord-Development/pycord/pull/3278)) - Fix `PartialMessage.edit()` to work with `DesignerView`. ([#3237](https://github.com/Pycord-Development/pycord/pull/3237)) +- Fix animated asset url extension from `.gif` to `.webp` + ([#3294](https://github.com/Pycord-Development/pycord/pull/3294)) ### Deprecated diff --git a/discord/asset.py b/discord/asset.py index 3519fa9ec2..5379501386 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -416,7 +416,7 @@ def key(self) -> str: @property def extension(self) -> str | None: - """Returns the extension of the asset.""" + """Returns the file extension of the asset.""" return self._extension @property From 44fbc969ce5cc4841fe7732e66481523aa7ca72e Mon Sep 17 00:00:00 2001 From: SilberGecko6917 Date: Sun, 9 Aug 2026 16:20:14 +0200 Subject: [PATCH 09/11] review changes --- discord/asset.py | 134 +++++++++++++++++------------------------------ 1 file changed, 48 insertions(+), 86 deletions(-) diff --git a/discord/asset.py b/discord/asset.py index 5379501386..c2664c25fa 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -169,42 +169,36 @@ def __init__( url: str, key: str, animated: bool = False, - extension: str | None = None, + extension: str | None = MISSING, size: int | None = None, ): self._state = state self._url = url self._animated = animated self._key = key - self._extension = extension 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, - extension="png", + 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 = "webp" if animated else "png" - url = f"{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024" - if animated: - url += "&animated=true" + url = f"{cls.BASE}/avatars/{user_id}/{avatar}" return cls( - state, url=url, key=avatar, animated=animated, extension=format, size=1024 + state, url=url, key=avatar, animated=avatar.startswith("a_"), size=1024 ) @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")) @@ -212,9 +206,9 @@ def _from_avatar_decoration( ) 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, ) @@ -241,9 +235,8 @@ 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, ) @@ -252,35 +245,26 @@ def _from_user_primary_guild_tag( def _from_guild_avatar( cls, state, guild_id: int, member_id: int, avatar: str ) -> Asset: - animated = avatar.startswith("a_") - format = "webp" if animated else "png" - url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.{format}?size=1024" - if animated: - url += "&animated=true" + url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}" return cls( - state, url=url, key=avatar, animated=animated, extension=format, size=1024 + 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 = "webp" if animated else "png" - url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=512" - if animated: - url += "&animated=true" + url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}" return cls( - state, url=url, key=banner, animated=animated, extension=format, size=512 + 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, ) @@ -289,9 +273,8 @@ def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset: 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, ) @@ -300,43 +283,31 @@ def _from_cover_image(cls, state, object_id: int, cover_image_hash: str) -> Asse 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" return cls( state, url=f"{cls.BASE}/assets/collectibles/{asset}{name}", key=asset, animated=animated, - extension="webm" if animated else "png", ) @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 = "webp" if animated else "png" - url = f"{cls.BASE}/{path}/{guild_id}/{image}.{format}?size=1024" - if animated: - url += "&animated=true" - return cls( - state, url=url, key=image, animated=animated, extension=format, size=1024 - ) + 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 = "webp" if animated else "png" - url = f"{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024" - if animated: - url += "&animated=true" + url = f"{cls.BASE}/icons/{guild_id}/{icon_hash}" return cls( state, url=url, key=icon_hash, - animated=animated, - extension=format, + animated=icon_hash.startswith("a_"), size=1024, ) @@ -344,25 +315,19 @@ def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset: 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 = "webp" if animated else "png" - url = f"{cls.BASE}/banners/{user_id}/{banner_hash}.{format}?size=512" - if animated: - url += "&animated=true" + url = f"{cls.BASE}/banners/{user_id}/{banner_hash}" return cls( state, url=url, key=banner_hash, - animated=animated, - extension=format, + animated=banner_hash.startswith("a_"), size=512, ) @@ -372,9 +337,8 @@ 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", ) @@ -385,29 +349,41 @@ def _from_soundboard_sound(cls, state, sound_id: int) -> Asset: url=f"{cls.BASE}/soundboard-sounds/{sound_id}", key=str(sound_id), extension=None, - size=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"" 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": + 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: @@ -458,8 +434,6 @@ 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 @@ -470,34 +444,27 @@ def replace( 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) new_size = size - else: - url = url.with_query(url.raw_query_string) - url = str(url) return Asset( state=self._state, - url=url, + url=self._url, key=self._key, animated=self._animated, extension=extension, @@ -525,10 +492,9 @@ 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, + url=self._url, key=self._key, animated=self._animated, extension=self._extension, @@ -559,13 +525,9 @@ 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, + url=self._url, key=self._key, animated=self._animated, extension=format, From 4bed87578fae0125b5c2248e834afbbf8d5305a0 Mon Sep 17 00:00:00 2001 From: SilberGecko6917 Date: Mon, 10 Aug 2026 13:57:17 +0200 Subject: [PATCH 10/11] set collectible only to png/webm and cleanup code --- discord/asset.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/discord/asset.py b/discord/asset.py index c2664c25fa..3bc1685541 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -190,9 +190,12 @@ def _from_default_avatar(cls, state, index: int) -> Asset: @classmethod def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset: - url = f"{cls.BASE}/avatars/{user_id}/{avatar}" return cls( - state, url=url, key=avatar, animated=avatar.startswith("a_"), size=1024 + state, + url=f"{cls.BASE}/avatars/{user_id}/{avatar}", + key=avatar, + animated=avatar.startswith("a_"), + size=1024, ) @classmethod @@ -245,18 +248,24 @@ def _from_user_primary_guild_tag( def _from_guild_avatar( cls, state, guild_id: int, member_id: int, avatar: str ) -> Asset: - url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}" return cls( - state, url=url, key=avatar, animated=avatar.startswith("a_"), size=1024 + state, + url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}", + key=avatar, + animated=avatar.startswith("a_"), + size=1024, ) @classmethod def _from_guild_banner( cls, state, guild_id: int, member_id: int, banner: str ) -> Asset: - url = f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}" return cls( - state, url=url, key=banner, animated=banner.startswith("a_"), size=512 + state, + url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}", + key=banner, + animated=banner.startswith("a_"), + size=512, ) @classmethod @@ -284,11 +293,13 @@ def _from_collectible( cls, state: ConnectionState, asset: str, animated: bool = False ) -> Asset: name = "static" if not animated else "asset" + extension = "png" if not animated else "webm" return cls( state, url=f"{cls.BASE}/assets/collectibles/{asset}{name}", key=asset, animated=animated, + extension=extension, ) @classmethod @@ -296,16 +307,19 @@ def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset animated = False if path == "banners": animated = image.startswith("a_") - - url = f"{cls.BASE}/{path}/{guild_id}/{image}" - return cls(state, url=url, key=image, animated=animated, size=1024) + return cls( + state, + url=f"{cls.BASE}/{path}/{guild_id}/{image}", + key=image, + animated=animated, + size=1024, + ) @classmethod def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset: - url = f"{cls.BASE}/icons/{guild_id}/{icon_hash}" return cls( state, - url=url, + url=f"{cls.BASE}/icons/{guild_id}/{icon_hash}", key=icon_hash, animated=icon_hash.startswith("a_"), size=1024, @@ -322,10 +336,9 @@ def _from_sticker_banner(cls, state, banner: int) -> Asset: @classmethod def _from_user_banner(cls, state, user_id: int, banner_hash: str) -> Asset: - url = f"{cls.BASE}/banners/{user_id}/{banner_hash}" return cls( state, - url=url, + url=f"{cls.BASE}/banners/{user_id}/{banner_hash}", key=banner_hash, animated=banner_hash.startswith("a_"), size=512, From 69f8b099de23555e0948ede6fc2d311bc8c1fd32 Mon Sep 17 00:00:00 2001 From: SilberGecko6917 Date: Tue, 11 Aug 2026 14:20:21 +0200 Subject: [PATCH 11/11] update changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc9c911fcb..09f7516247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ These changes are available on the `master` branch, but have not yet been releas - Added `Member.vr_status` property. ([#3328](https://github.com/Pycord-Development/pycord/pull/3328)) +- Added `Asset.size` and `Asset.extension` properties. + ([#3294](https://github.com/Pycord-Development/pycord/pull/3294)) ### Changed @@ -24,6 +26,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#3320](https://github.com/Pycord-Development/pycord/pull/3320)) - Fix `SyntaxWarning` about `return` in a `finally` block raised on Python 3.14+ ([#3332](https://github.com/Pycord-Development/pycord/pull/3334)) +- Fix animated asset URL extension from `.gif` to `.webp`. + ([#3294](https://github.com/Pycord-Development/pycord/pull/3294)) ### Deprecated @@ -36,8 +40,6 @@ These changes are available on the `master` branch, but have not yet been releas - Add `RoleColours.HOLOGRAPHIC_PRIMARY`, `RoleColours.HOLOGRAPHIC_SECONDARY`, and `RoleColours.HOLOGRAPHIC_TERTIARY` class constants. ([#3268](https://github.com/Pycord-Development/pycord/pull/3268)) -- Add `Asset.size` and `Asset.extension` properties. - ([#3294](https://github.com/Pycord-Development/pycord/pull/3294)) ### Fixed @@ -75,8 +77,6 @@ These changes are available on the `master` branch, but have not yet been releas ([#3278](https://github.com/Pycord-Development/pycord/pull/3278)) - Fix `PartialMessage.edit()` to work with `DesignerView`. ([#3237](https://github.com/Pycord-Development/pycord/pull/3237)) -- Fix animated asset url extension from `.gif` to `.webp` - ([#3294](https://github.com/Pycord-Development/pycord/pull/3294)) ## [2.8.0] - 2026-05-18