Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Heroic is built with Web Technologies:
- Download custom Wine and Proton versions [Linux]
- Access to Epic and GOG stores directly from Heroic
- Search for the game on ProtonDB for compatibility information [Linux]
- Show ProtonDB and Steam Deck compatibility information [Linux]
- Sync installed games with an existing Epic Games Store installation
- Sync saves with the cloud
- Custom Theming Support
Expand Down
2 changes: 2 additions & 0 deletions public/locales/en/gamepage.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@
"installedInfo": "Installed Information",
"installedPlatform": "Installed Platform",
"path": "Install Path",
"protondb-compatibility-info": "Proton Compatibility Tier",
"size": "Size",
"steamdeck-compatibility-info": "SteamDeck Compatibility",
"syncsaves": "Sync Saves",
"version": "Version"
},
Expand Down
84 changes: 77 additions & 7 deletions src/backend/wiki_game_info/__tests__/wiki_game_info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
AppleGamingWikiInfo,
WikiInfo,
PCGamingWikiInfo,
ProtonDBCompatibilityInfo
ProtonDBCompatibilityInfo,
SteamDeckComp,
SteamInfo
} from 'common/types'
import { wikiGameInfoStore } from '../electronStore'
import { getWikiGameInfo } from '../wiki_game_info'
Expand All @@ -13,16 +15,13 @@ import * as AppleGamingWiki from '../applegamingwiki/utils'
import * as HowLongToBeat from '../howlongtobeat/utils'
import * as GamesDB from '../gamesdb/utils'
import * as ProtonDB from '../protondb/utils'
import * as SteamDeck from '../steamdeck/utils'
import { logError } from '../../logger/logger'

jest.mock('electron-store')
jest.mock('../../logger/logfile')
jest.mock('../../logger/logger')
jest.mock('../../constants', () => {
return {
isMac: true
}
})
import * as mockConstants from '../../constants'
const currentTime = new Date()
jest.useFakeTimers().setSystemTime(currentTime)

Expand All @@ -43,8 +42,13 @@ describe('getWikiGameInfo', () => {
const mockProtonDB = jest
.spyOn(ProtonDB, 'getInfoFromProtonDB')
.mockResolvedValue(testProtonDBInfo)
const mockSteamDeck = jest
.spyOn(SteamDeck, 'getSteamDeckComp')
.mockResolvedValue(testSteamCompat)

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

const result = await getWikiGameInfo('The Witcher 3', '1234', 'gog')
expect(result).toStrictEqual(testExtraGameInfo)
Expand All @@ -53,12 +57,15 @@ describe('getWikiGameInfo', () => {
expect(mockHowLongToBeat).not.toBeCalled()
expect(mockGamesDB).not.toBeCalled()
expect(mockProtonDB).not.toBeCalled()
expect(mockSteamDeck).not.toBeCalled()
})

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

Object.defineProperty(mockConstants, 'isMac', { value: true })
Object.defineProperty(mockConstants, 'isLinux', { value: true })
const mockPCGamingWiki = jest
.spyOn(PCGamingWiki, 'getInfoFromPCGamingWiki')
.mockResolvedValue(testPCGamingWikiInfo)
Expand All @@ -74,6 +81,9 @@ describe('getWikiGameInfo', () => {
const mockProtonDB = jest
.spyOn(ProtonDB, 'getInfoFromProtonDB')
.mockResolvedValue(testProtonDBInfo)
const mockSteamDeck = jest
.spyOn(SteamDeck, 'getSteamDeckComp')
.mockResolvedValue(testSteamCompat)

wikiGameInfoStore.set('The Witcher 3', {
...testExtraGameInfo,
Expand All @@ -87,8 +97,50 @@ describe('getWikiGameInfo', () => {
expect(mockHowLongToBeat).toBeCalled()
expect(mockGamesDB).toBeCalled()
expect(mockProtonDB).toBeCalled()
expect(mockProtonDB).toBeCalledWith('100')
expect(mockSteamDeck).toBeCalled()
expect(mockSteamDeck).toBeCalledWith('100')
})

test('cached data outdated - not mac not linux', async () => {
const oneMonthAgo = new Date(testExtraGameInfo.timestampLastFetch)
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1)

Object.defineProperty(mockConstants, 'isMac', { value: false })
Object.defineProperty(mockConstants, 'isLinux', { value: false })
const mockPCGamingWiki = jest
.spyOn(PCGamingWiki, 'getInfoFromPCGamingWiki')
.mockResolvedValue(testPCGamingWikiInfo)
const mockAppleGamingWiki = jest
.spyOn(AppleGamingWiki, 'getInfoFromAppleGamingWiki')
.mockResolvedValue(testAppleGamingWikiInfo)
const mockHowLongToBeat = jest
.spyOn(HowLongToBeat, 'getHowLongToBeat')
.mockResolvedValue(testHowLongToBeat)
const mockGamesDB = jest
.spyOn(GamesDB, 'getInfoFromGamesDB')
.mockResolvedValue(testGamesDBInfo)
const mockProtonDB = jest
.spyOn(ProtonDB, 'getInfoFromProtonDB')
.mockResolvedValue(null)
const mockSteamDeck = jest
.spyOn(SteamDeck, 'getSteamDeckComp')
.mockResolvedValue(null)

wikiGameInfoStore.set('The Witcher 3', {
...testExtraGameInfo,
timestampLastFetch: oneMonthAgo.toString()
})

const result = await getWikiGameInfo('The Witcher 3', '1234', 'gog')
expect(result).toStrictEqual(testExtraGameInfoNoMac)
expect(mockPCGamingWiki).toBeCalled()
expect(mockAppleGamingWiki).not.toBeCalled()
expect(mockHowLongToBeat).toBeCalled()
expect(mockGamesDB).toBeCalled()
expect(mockProtonDB).not.toBeCalled()
expect(mockSteamDeck).not.toBeCalled()
})
test('catches throws', async () => {
jest
.spyOn(PCGamingWiki, 'getInfoFromPCGamingWiki')
Expand Down Expand Up @@ -154,11 +206,29 @@ const testProtonDBInfo = {
level: 'platinum'
} as ProtonDBCompatibilityInfo

const testSteamCompat = {
category: 1
} as SteamDeckComp

const testSteamInfo = {
compatibilityLevel: testProtonDBInfo.level,
steamDeckCatagory: testSteamCompat.category
} as SteamInfo

const testExtraGameInfo = {
timestampLastFetch: currentTime.toString(),
pcgamingwiki: testPCGamingWikiInfo,
applegamingwiki: testAppleGamingWikiInfo,
howlongtobeat: testHowLongToBeat,
gamesdb: testGamesDBInfo,
protondb: testProtonDBInfo
steamInfo: testSteamInfo
} as WikiInfo

const testExtraGameInfoNoMac = {
timestampLastFetch: currentTime.toString(),
pcgamingwiki: testPCGamingWikiInfo,
applegamingwiki: null,
howlongtobeat: testHowLongToBeat,
gamesdb: testGamesDBInfo,
steamInfo: null
} as WikiInfo
5 changes: 3 additions & 2 deletions src/backend/wiki_game_info/protondb/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import axios, { AxiosError } from 'axios'
import { logDebug, logError, LogPrefix } from 'backend/logger/logger'

export async function getInfoFromProtonDB(
steamID: string
steamID: string | undefined
): Promise<ProtonDBCompatibilityInfo | null> {
if (steamID === '') {
if (!steamID) {
logDebug('No SteamID, not getting ProtonDB info')
return null
}

const url = `https://www.protondb.com/api/v1/reports/summaries/${steamID}.json`

const response = await axios
Expand Down
52 changes: 52 additions & 0 deletions src/backend/wiki_game_info/steamdeck/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { logError } from 'backend/logger/logger'
import { getSteamDeckComp } from '../utils'
import axios, { AxiosError } from 'axios'

jest.mock('backend/logger/logfile')
jest.mock('backend/logger/logger')

describe('getSteamDeckComp', () => {
test('fetches successfully via steamid', async () => {
const mockAxios = jest.spyOn(axios, 'get').mockResolvedValue({
data: { results: { resolved_category: 1 } }
})

const result = await getSteamDeckComp('1234')
expect(result).toStrictEqual(testProtonDBInfo)
expect(mockAxios).toBeCalled()
})
test('api change', async () => {
const mockAxios = jest.spyOn(axios, 'get').mockResolvedValue({
data: { results: { tierLevel: 'gold' } }
})

const result = await getSteamDeckComp('1234')
expect(result).toStrictEqual(null)
expect(mockAxios).toBeCalled()
})
test('does not find game', async () => {
const mockAxios = jest
.spyOn(axios, 'get')
.mockRejectedValue(<AxiosError>new Error('not found'))

const result = await getSteamDeckComp('1234')
expect(result).toBeNull()
expect(mockAxios).toBeCalled()
expect(logError).toBeCalledWith(
['Was not able to get Stem Deck data for 1234', undefined],
'ExtraGameInfo'
)
})

test('no SteamID', async () => {
const mockAxios = jest.spyOn(axios, 'get')

const result = await getSteamDeckComp('')
expect(result).toBeNull()
expect(mockAxios).not.toBeCalled()
})
})

const testProtonDBInfo = {
category: 1
}
39 changes: 39 additions & 0 deletions src/backend/wiki_game_info/steamdeck/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { SteamDeckComp } from 'common/types'
import axios, { AxiosError } from 'axios'
import { logDebug, logError, LogPrefix } from 'backend/logger/logger'

export async function getSteamDeckComp(
steamID: string | undefined
): Promise<SteamDeckComp | null> {
if (!steamID) {
logDebug('No SteamID, not getting Stem Deck info')
return null
}
const url = `https://store.steampowered.com/saleaction/ajaxgetdeckappcompatibilityreport?nAppID=${steamID}`

const response = await axios
.get(url, { headers: {} })
.catch((error: AxiosError) => {
logError(
[
`Was not able to get Stem Deck data for ${steamID}`,
error.response?.data.error_description
],
LogPrefix.ExtraGameInfo
)
return null
})

if (!response) {
logDebug('No response when getting Stem Deck info')
return null
}
const resp_str = JSON.stringify(response.data)
logDebug(`SteamDeck data for ${steamID} ${resp_str}`)

if (!Number.isFinite(response.data?.results?.resolved_category)) {
logError('No resolved_category in response, API changed?')
return null
}
return { category: response.data.results.resolved_category }
}
26 changes: 20 additions & 6 deletions src/backend/wiki_game_info/wiki_game_info.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { getInfoFromGamesDB } from 'backend/wiki_game_info/gamesdb/utils'
import { getInfoFromProtonDB } from 'backend/wiki_game_info/protondb/utils'
import { getSteamDeckComp } from 'backend/wiki_game_info/steamdeck/utils'
import { wikiGameInfoStore } from './electronStore'
import { removeSpecialcharacters } from '../utils'
import { Runner, WikiInfo } from 'common/types'
import { Runner, SteamInfo, WikiInfo } from 'common/types'
import { logError, logInfo, LogPrefix } from '../logger/logger'
import { getInfoFromAppleGamingWiki } from './applegamingwiki/utils'
import { getHowLongToBeat } from './howlongtobeat/utils'
import { getInfoFromPCGamingWiki } from './pcgamingwiki/utils'
import { isMac } from '../constants'
import { isMac, isLinux } from '../constants'

export async function getWikiGameInfo(
title: string,
Expand Down Expand Up @@ -49,16 +50,29 @@ export async function getWikiGameInfo(
isMac ? getInfoFromAppleGamingWiki(title) : null
])

const protondb = await getInfoFromProtonDB(
gamesdb?.steamID ? gamesdb.steamID : ''
)
let steamInfo = null
if (isLinux) {
const steamID = pcgamingwiki?.steamID ?? gamesdb?.steamID
const [protondb, steamdeck] = await Promise.all([
getInfoFromProtonDB(steamID),
getSteamDeckComp(steamID)
])

if (protondb || steamdeck) {
steamInfo = {
compatibilityLevel: protondb?.level,
steamDeckCatagory: steamdeck?.category
} as SteamInfo
}
}

const wikiGameInfo = {
timestampLastFetch: Date(),
pcgamingwiki,
applegamingwiki,
howlongtobeat,
gamesdb,
protondb
steamInfo
}

wikiGameInfoStore.set(title, wikiGameInfo)
Expand Down
11 changes: 10 additions & 1 deletion src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,13 +603,22 @@ export interface ProtonDBCompatibilityInfo {
level: string
}

export interface SteamDeckComp {
category: number
}

export interface SteamInfo {
compatibilityLevel: string | null
steamDeckCatagory: number | null
}

export interface WikiInfo {
timestampLastFetch: string
pcgamingwiki: PCGamingWikiInfo | null
applegamingwiki: AppleGamingWikiInfo | null
howlongtobeat: HowLongToBeatEntry | null
gamesdb: GamesDBInfo | null
protondb: ProtonDBCompatibilityInfo | null
steamInfo: SteamInfo | null
}

/**
Expand Down
Loading