Skip to content

Commit d56f232

Browse files
kohendNocccer
andauthored
[UI/UX]: Fetch steam deck compatibility (#2829)
* Get steamdeck comaptibility * Attempt to fix actions * unify steam data * yarn lock * UI support * Get/Show unknown properly * Make compatibility info a link * Get proton and Steam Deck info only on Linux * Fix tests warnings * Design and backend clean up * Added review suggestion --------- Co-authored-by: Nocccer <[email protected]>
1 parent 02ac2a1 commit d56f232

File tree

9 files changed

+318
-21
lines changed

9 files changed

+318
-21
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Heroic is built with Web Technologies:
6161
- Download custom Wine and Proton versions [Linux]
6262
- Access to Epic and GOG stores directly from Heroic
6363
- Search for the game on ProtonDB for compatibility information [Linux]
64+
- Show ProtonDB and Steam Deck compatibility information [Linux]
6465
- Sync installed games with an existing Epic Games Store installation
6566
- Sync saves with the cloud
6667
- Custom Theming Support

public/locales/en/gamepage.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@
118118
"installedInfo": "Installed Information",
119119
"installedPlatform": "Installed Platform",
120120
"path": "Install Path",
121+
"protondb-compatibility-info": "Proton Compatibility Tier",
121122
"size": "Size",
123+
"steamdeck-compatibility-info": "SteamDeck Compatibility",
122124
"syncsaves": "Sync Saves",
123125
"version": "Version"
124126
},

src/backend/wiki_game_info/__tests__/wiki_game_info.test.ts

Lines changed: 124 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
AppleGamingWikiInfo,
55
WikiInfo,
66
PCGamingWikiInfo,
7-
ProtonDBCompatibilityInfo
7+
ProtonDBCompatibilityInfo,
8+
SteamDeckComp,
9+
SteamInfo
810
} from 'common/types'
911
import { wikiGameInfoStore } from '../electronStore'
1012
import { getWikiGameInfo } from '../wiki_game_info'
@@ -13,16 +15,13 @@ import * as AppleGamingWiki from '../applegamingwiki/utils'
1315
import * as HowLongToBeat from '../howlongtobeat/utils'
1416
import * as GamesDB from '../gamesdb/utils'
1517
import * as ProtonDB from '../protondb/utils'
18+
import * as SteamDeck from '../steamdeck/utils'
1619
import { logError } from '../../logger/logger'
1720

1821
jest.mock('electron-store')
1922
jest.mock('../../logger/logfile')
2023
jest.mock('../../logger/logger')
21-
jest.mock('../../constants', () => {
22-
return {
23-
isMac: true
24-
}
25-
})
24+
import * as mockConstants from '../../constants'
2625
const currentTime = new Date()
2726
jest.useFakeTimers().setSystemTime(currentTime)
2827

@@ -43,8 +42,13 @@ describe('getWikiGameInfo', () => {
4342
const mockProtonDB = jest
4443
.spyOn(ProtonDB, 'getInfoFromProtonDB')
4544
.mockResolvedValue(testProtonDBInfo)
45+
const mockSteamDeck = jest
46+
.spyOn(SteamDeck, 'getSteamDeckComp')
47+
.mockResolvedValue(testSteamCompat)
4648

4749
wikiGameInfoStore.set('The Witcher 3', testExtraGameInfo)
50+
Object.defineProperty(mockConstants, 'isMac', { value: true })
51+
Object.defineProperty(mockConstants, 'isLinux', { value: true })
4852

4953
const result = await getWikiGameInfo('The Witcher 3', '1234', 'gog')
5054
expect(result).toStrictEqual(testExtraGameInfo)
@@ -53,12 +57,15 @@ describe('getWikiGameInfo', () => {
5357
expect(mockHowLongToBeat).not.toBeCalled()
5458
expect(mockGamesDB).not.toBeCalled()
5559
expect(mockProtonDB).not.toBeCalled()
60+
expect(mockSteamDeck).not.toBeCalled()
5661
})
5762

5863
test('cached data outdated', async () => {
5964
const oneMonthAgo = new Date(testExtraGameInfo.timestampLastFetch)
6065
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1)
6166

67+
Object.defineProperty(mockConstants, 'isMac', { value: true })
68+
Object.defineProperty(mockConstants, 'isLinux', { value: true })
6269
const mockPCGamingWiki = jest
6370
.spyOn(PCGamingWiki, 'getInfoFromPCGamingWiki')
6471
.mockResolvedValue(testPCGamingWikiInfo)
@@ -74,29 +81,119 @@ describe('getWikiGameInfo', () => {
7481
const mockProtonDB = jest
7582
.spyOn(ProtonDB, 'getInfoFromProtonDB')
7683
.mockResolvedValue(testProtonDBInfo)
84+
const mockSteamDeck = jest
85+
.spyOn(SteamDeck, 'getSteamDeckComp')
86+
.mockResolvedValue(testSteamCompat)
7787

7888
wikiGameInfoStore.set('The Witcher 3', {
7989
...testExtraGameInfo,
8090
timestampLastFetch: oneMonthAgo.toString()
8191
})
8292

83-
const result = await await getWikiGameInfo('The Witcher 3', '1234', 'gog')
93+
const result = await getWikiGameInfo('The Witcher 3', '1234', 'gog')
8494
expect(result).toStrictEqual(testExtraGameInfo)
8595
expect(mockPCGamingWiki).toBeCalled()
8696
expect(mockAppleGamingWiki).toBeCalled()
8797
expect(mockHowLongToBeat).toBeCalled()
8898
expect(mockGamesDB).toBeCalled()
8999
expect(mockProtonDB).toBeCalled()
100+
expect(mockProtonDB).toBeCalledWith('100')
101+
expect(mockSteamDeck).toBeCalled()
102+
expect(mockSteamDeck).toBeCalledWith('100')
90103
})
91104

105+
test('fallback to gamesdb steamID', async () => {
106+
const oneMonthAgo = new Date(testExtraGameInfo.timestampLastFetch)
107+
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1)
108+
109+
Object.defineProperty(mockConstants, 'isMac', { value: true })
110+
Object.defineProperty(mockConstants, 'isLinux', { value: true })
111+
const mockPCGamingWiki = jest
112+
.spyOn(PCGamingWiki, 'getInfoFromPCGamingWiki')
113+
.mockResolvedValue({ ...testPCGamingWikiInfo, steamID: '' })
114+
const mockAppleGamingWiki = jest
115+
.spyOn(AppleGamingWiki, 'getInfoFromAppleGamingWiki')
116+
.mockResolvedValue(testAppleGamingWikiInfo)
117+
const mockHowLongToBeat = jest
118+
.spyOn(HowLongToBeat, 'getHowLongToBeat')
119+
.mockResolvedValue(testHowLongToBeat)
120+
const mockGamesDB = jest
121+
.spyOn(GamesDB, 'getInfoFromGamesDB')
122+
.mockResolvedValue(testGamesDBInfo)
123+
const mockProtonDB = jest
124+
.spyOn(ProtonDB, 'getInfoFromProtonDB')
125+
.mockResolvedValue(testProtonDBInfo)
126+
const mockSteamDeck = jest
127+
.spyOn(SteamDeck, 'getSteamDeckComp')
128+
.mockResolvedValue(testSteamCompat)
129+
130+
wikiGameInfoStore.set('The Witcher 3', {
131+
...testExtraGameInfo,
132+
timestampLastFetch: oneMonthAgo.toString()
133+
})
134+
135+
const result = await getWikiGameInfo('The Witcher 3', '1234', 'gog')
136+
expect(result).toStrictEqual({
137+
...testExtraGameInfo,
138+
pcgamingwiki: { ...testPCGamingWikiInfo, steamID: '' }
139+
})
140+
expect(mockPCGamingWiki).toBeCalled()
141+
expect(mockAppleGamingWiki).toBeCalled()
142+
expect(mockHowLongToBeat).toBeCalled()
143+
expect(mockGamesDB).toBeCalled()
144+
expect(mockProtonDB).toBeCalled()
145+
expect(mockProtonDB).toBeCalledWith('123')
146+
expect(mockSteamDeck).toBeCalled()
147+
expect(mockSteamDeck).toBeCalledWith('123')
148+
})
149+
150+
test('cached data outdated - not mac not linux', async () => {
151+
const oneMonthAgo = new Date(testExtraGameInfo.timestampLastFetch)
152+
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1)
153+
154+
Object.defineProperty(mockConstants, 'isMac', { value: false })
155+
Object.defineProperty(mockConstants, 'isLinux', { value: false })
156+
const mockPCGamingWiki = jest
157+
.spyOn(PCGamingWiki, 'getInfoFromPCGamingWiki')
158+
.mockResolvedValue(testPCGamingWikiInfo)
159+
const mockAppleGamingWiki = jest
160+
.spyOn(AppleGamingWiki, 'getInfoFromAppleGamingWiki')
161+
.mockResolvedValue(testAppleGamingWikiInfo)
162+
const mockHowLongToBeat = jest
163+
.spyOn(HowLongToBeat, 'getHowLongToBeat')
164+
.mockResolvedValue(testHowLongToBeat)
165+
const mockGamesDB = jest
166+
.spyOn(GamesDB, 'getInfoFromGamesDB')
167+
.mockResolvedValue(testGamesDBInfo)
168+
const mockProtonDB = jest
169+
.spyOn(ProtonDB, 'getInfoFromProtonDB')
170+
.mockResolvedValue(null)
171+
const mockSteamDeck = jest
172+
.spyOn(SteamDeck, 'getSteamDeckComp')
173+
.mockResolvedValue(null)
174+
175+
wikiGameInfoStore.set('The Witcher 3', {
176+
...testExtraGameInfo,
177+
timestampLastFetch: oneMonthAgo.toString()
178+
})
179+
180+
const result = await getWikiGameInfo('The Witcher 3', '1234', 'gog')
181+
expect(result).toStrictEqual(testExtraGameInfoNoMac)
182+
expect(mockPCGamingWiki).toBeCalled()
183+
expect(mockAppleGamingWiki).not.toBeCalled()
184+
expect(mockHowLongToBeat).toBeCalled()
185+
expect(mockGamesDB).toBeCalled()
186+
expect(mockProtonDB).not.toBeCalled()
187+
expect(mockSteamDeck).not.toBeCalled()
188+
})
92189
test('catches throws', async () => {
93190
jest
94191
.spyOn(PCGamingWiki, 'getInfoFromPCGamingWiki')
95192
.mockRejectedValueOnce(new Error('Failed'))
96193

97194
wikiGameInfoStore.clear()
98195

99-
const result = await await getWikiGameInfo('The Witcher 3', '1234', 'gog')
196+
const result = await getWikiGameInfo('The Witcher 3', '1234', 'gog')
100197
expect(result).toBeNull()
101198
expect(logError).toBeCalledWith(
102199
[
@@ -154,11 +251,29 @@ const testProtonDBInfo = {
154251
level: 'platinum'
155252
} as ProtonDBCompatibilityInfo
156253

254+
const testSteamCompat = {
255+
category: 1
256+
} as SteamDeckComp
257+
258+
const testSteamInfo = {
259+
compatibilityLevel: testProtonDBInfo.level,
260+
steamDeckCatagory: testSteamCompat.category
261+
} as SteamInfo
262+
157263
const testExtraGameInfo = {
158264
timestampLastFetch: currentTime.toString(),
159265
pcgamingwiki: testPCGamingWikiInfo,
160266
applegamingwiki: testAppleGamingWikiInfo,
161267
howlongtobeat: testHowLongToBeat,
162268
gamesdb: testGamesDBInfo,
163-
protondb: testProtonDBInfo
269+
steamInfo: testSteamInfo
270+
} as WikiInfo
271+
272+
const testExtraGameInfoNoMac = {
273+
timestampLastFetch: currentTime.toString(),
274+
pcgamingwiki: testPCGamingWikiInfo,
275+
applegamingwiki: null,
276+
howlongtobeat: testHowLongToBeat,
277+
gamesdb: testGamesDBInfo,
278+
steamInfo: null
164279
} as WikiInfo

src/backend/wiki_game_info/protondb/utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import axios, { AxiosError } from 'axios'
33
import { logDebug, logError, LogPrefix } from 'backend/logger/logger'
44

55
export async function getInfoFromProtonDB(
6-
steamID: string
6+
steamID: string | undefined
77
): Promise<ProtonDBCompatibilityInfo | null> {
8-
if (steamID === '') {
8+
if (!steamID) {
99
logDebug('No SteamID, not getting ProtonDB info')
1010
return null
1111
}
12+
1213
const url = `https://www.protondb.com/api/v1/reports/summaries/${steamID}.json`
1314

1415
const response = await axios
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { logError } from 'backend/logger/logger'
2+
import { getSteamDeckComp } from '../utils'
3+
import axios, { AxiosError } from 'axios'
4+
5+
jest.mock('backend/logger/logfile')
6+
jest.mock('backend/logger/logger')
7+
8+
describe('getSteamDeckComp', () => {
9+
test('fetches successfully via steamid', async () => {
10+
const mockAxios = jest.spyOn(axios, 'get').mockResolvedValue({
11+
data: { results: { resolved_category: 1 } }
12+
})
13+
14+
const result = await getSteamDeckComp('1234')
15+
expect(result).toStrictEqual(testProtonDBInfo)
16+
expect(mockAxios).toBeCalled()
17+
})
18+
test('api change', async () => {
19+
const mockAxios = jest.spyOn(axios, 'get').mockResolvedValue({
20+
data: { results: { tierLevel: 'gold' } }
21+
})
22+
23+
const result = await getSteamDeckComp('1234')
24+
expect(result).toStrictEqual(null)
25+
expect(mockAxios).toBeCalled()
26+
})
27+
test('does not find game', async () => {
28+
const mockAxios = jest
29+
.spyOn(axios, 'get')
30+
.mockRejectedValue(<AxiosError>new Error('not found'))
31+
32+
const result = await getSteamDeckComp('1234')
33+
expect(result).toBeNull()
34+
expect(mockAxios).toBeCalled()
35+
expect(logError).toBeCalledWith(
36+
['Was not able to get Stem Deck data for 1234', undefined],
37+
'ExtraGameInfo'
38+
)
39+
})
40+
41+
test('no SteamID', async () => {
42+
const mockAxios = jest.spyOn(axios, 'get')
43+
44+
const result = await getSteamDeckComp('')
45+
expect(result).toBeNull()
46+
expect(mockAxios).not.toBeCalled()
47+
})
48+
})
49+
50+
const testProtonDBInfo = {
51+
category: 1
52+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { SteamDeckComp } from 'common/types'
2+
import axios, { AxiosError } from 'axios'
3+
import { logDebug, logError, LogPrefix } from 'backend/logger/logger'
4+
5+
export async function getSteamDeckComp(
6+
steamID: string | undefined
7+
): Promise<SteamDeckComp | null> {
8+
if (!steamID) {
9+
logDebug('No SteamID, not getting Stem Deck info')
10+
return null
11+
}
12+
const url = `https://store.steampowered.com/saleaction/ajaxgetdeckappcompatibilityreport?nAppID=${steamID}`
13+
14+
const response = await axios
15+
.get(url, { headers: {} })
16+
.catch((error: AxiosError) => {
17+
logError(
18+
[
19+
`Was not able to get Stem Deck data for ${steamID}`,
20+
error.response?.data.error_description
21+
],
22+
LogPrefix.ExtraGameInfo
23+
)
24+
return null
25+
})
26+
27+
if (!response) {
28+
logDebug('No response when getting Stem Deck info')
29+
return null
30+
}
31+
const resp_str = JSON.stringify(response.data)
32+
logDebug(`SteamDeck data for ${steamID} ${resp_str}`)
33+
34+
if (!Number.isFinite(response.data?.results?.resolved_category)) {
35+
logError('No resolved_category in response, API changed?')
36+
return null
37+
}
38+
return { category: response.data.results.resolved_category }
39+
}

src/backend/wiki_game_info/wiki_game_info.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { getInfoFromGamesDB } from 'backend/wiki_game_info/gamesdb/utils'
22
import { getInfoFromProtonDB } from 'backend/wiki_game_info/protondb/utils'
3+
import { getSteamDeckComp } from 'backend/wiki_game_info/steamdeck/utils'
34
import { wikiGameInfoStore } from './electronStore'
45
import { removeSpecialcharacters } from '../utils'
5-
import { Runner, WikiInfo } from 'common/types'
6+
import { Runner, SteamInfo, WikiInfo } from 'common/types'
67
import { logError, logInfo, LogPrefix } from '../logger/logger'
78
import { getInfoFromAppleGamingWiki } from './applegamingwiki/utils'
89
import { getHowLongToBeat } from './howlongtobeat/utils'
910
import { getInfoFromPCGamingWiki } from './pcgamingwiki/utils'
10-
import { isMac } from '../constants'
11+
import { isMac, isLinux } from '../constants'
1112

1213
export async function getWikiGameInfo(
1314
title: string,
@@ -49,16 +50,29 @@ export async function getWikiGameInfo(
4950
isMac ? getInfoFromAppleGamingWiki(title) : null
5051
])
5152

52-
const protondb = await getInfoFromProtonDB(
53-
gamesdb?.steamID ? gamesdb.steamID : ''
54-
)
53+
let steamInfo = null
54+
if (isLinux) {
55+
const steamID = pcgamingwiki?.steamID || gamesdb?.steamID
56+
const [protondb, steamdeck] = await Promise.all([
57+
getInfoFromProtonDB(steamID),
58+
getSteamDeckComp(steamID)
59+
])
60+
61+
if (protondb || steamdeck) {
62+
steamInfo = {
63+
compatibilityLevel: protondb?.level,
64+
steamDeckCatagory: steamdeck?.category
65+
} as SteamInfo
66+
}
67+
}
68+
5569
const wikiGameInfo = {
5670
timestampLastFetch: Date(),
5771
pcgamingwiki,
5872
applegamingwiki,
5973
howlongtobeat,
6074
gamesdb,
61-
protondb
75+
steamInfo
6276
}
6377

6478
wikiGameInfoStore.set(title, wikiGameInfo)

0 commit comments

Comments
 (0)