Compare commits

...

2 Commits

Author SHA1 Message Date
xRangerDE
1b87a2611e release: 4.6.19 fix public-mode profile avatar (roles instead of broadcasterType)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:43:53 +02:00
xRangerDE
ec48592503 fix: public-mode profile shows letter X — GQL query referenced authed-only field
Root cause of the X-fallback in the new profile header when the app
runs without Twitch credentials ("public mode"): the GQL query in
fetchPublicStreamerProfile asked for `broadcasterType`, which exists
on the AUTHENTICATED Twitch GQL schema but NOT on the public one. The
public endpoint returned `errors[]` with "Cannot query field
broadcasterType on type User", which fetchPublicTwitchGql correctly
treats as a complete failure and returns null. That cascaded:
- avatarUrl stayed empty
- displayName fell back to the lowercase login
- description stayed empty
- partner/affiliate badge never rendered
- the renderer hit the letter-tile fallback path

Reproduced live against gql.twitch.tv with a curl-equivalent: the
exact query worked when broadcasterType was swapped for the public-
schema field roles{isPartner isAffiliate}. xrohat correctly comes
back as Partner, with the full 150x150 avatar URL, real displayName
"xRohat", and 1.25M follower count.

The 4.6.18 data-URL fetch fix is still right (Electron renderer img
loading against the Twitch CDN was its own minor headache) — it just
never got exercised because we never had a URL to fetch in the first
place. With this fix the data-URL path now activates on every
public-mode profile load, and avatars actually render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:43:53 +02:00
3 changed files with 17 additions and 7 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.6.18", "version": "4.6.19",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.6.18", "version": "4.6.19",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.6.18", "version": "4.6.19",
"description": "Twitch VOD Manager - Download Twitch VODs easily", "description": "Twitch VOD Manager - Download Twitch VODs easily",
"main": "dist/main.js", "main": "dist/main.js",
"author": "xRangerDE", "author": "xRangerDE",

View File

@ -2393,7 +2393,7 @@ interface PublicProfileQueryResult {
displayName: string; displayName: string;
description: string | null; description: string | null;
profileImageURL: string | null; profileImageURL: string | null;
broadcasterType: string | null; roles?: { isPartner: boolean; isAffiliate: boolean } | null;
followers?: { totalCount: number } | null; followers?: { totalCount: number } | null;
} | null; } | null;
} }
@ -2405,6 +2405,13 @@ async function fetchPublicStreamerProfile(login: string): Promise<{
broadcasterType: '' | 'partner' | 'affiliate'; broadcasterType: '' | 'partner' | 'affiliate';
followerCount: number | null; followerCount: number | null;
} | null> { } | null> {
// The public (unauthenticated) GQL schema does NOT expose
// `broadcasterType` directly — querying it returns an errors[] response
// which the upstream helper treats as a complete failure (null data),
// which in turn left the avatar empty and the user's whole profile
// fell through to the fallback letter tile. Use `roles{isPartner
// isAffiliate}` instead, which the public schema does expose, and
// derive broadcasterType locally.
const data = await fetchPublicTwitchGql<PublicProfileQueryResult>( const data = await fetchPublicTwitchGql<PublicProfileQueryResult>(
`query($login: String!) { `query($login: String!) {
user(login: $login) { user(login: $login) {
@ -2413,19 +2420,22 @@ async function fetchPublicStreamerProfile(login: string): Promise<{
displayName displayName
description description
profileImageURL(width: 150) profileImageURL(width: 150)
broadcasterType roles { isPartner isAffiliate }
followers { totalCount } followers { totalCount }
} }
}`, }`,
{ login } { login }
); );
if (!data?.user) return null; if (!data?.user) return null;
const bt = (data.user.broadcasterType || '').toLowerCase(); const roles = data.user.roles;
const broadcasterType: '' | 'partner' | 'affiliate' = roles?.isPartner
? 'partner'
: (roles?.isAffiliate ? 'affiliate' : '');
return { return {
displayName: data.user.displayName || login, displayName: data.user.displayName || login,
avatarUrl: data.user.profileImageURL || '', avatarUrl: data.user.profileImageURL || '',
description: data.user.description || '', description: data.user.description || '',
broadcasterType: (bt === 'partner' || bt === 'affiliate') ? bt : '', broadcasterType,
followerCount: typeof data.user.followers?.totalCount === 'number' ? data.user.followers.totalCount : null followerCount: typeof data.user.followers?.totalCount === 'number' ? data.user.followers.totalCount : null
}; };
} }