12 Commits

Author SHA1 Message Date
ef244edd1a fix: lint 2025-10-29 21:03:19 +11:00
b083c1e00a Merge branch 'develop' into redistributable 2025-10-29 20:59:07 +11:00
5f2cb75666 Merge branch 'develop' into redistributable 2025-10-29 20:57:31 +11:00
2a23f4d14c Fix lints 2025-10-24 09:33:39 +11:00
b20d355527 Improve igdb metadata fetching (#257)
* improve igdb metadata fetching

    * Make sure to get images with reasonable resolution.
      By default the url igdb returns is in "t_thumb" size,
      an image of size 90x90, which is good only for the icon,
      but bad for pretty much else. This commit will make sure
      covers will be of size "t_cover_big", artworks of 1080p
      height (i.e. "t_1080p") and logos will have their original
      size ("t_original"). Maybe "t_logo_med" is more appropriate?

    * Fetch screenshots as well.

    * Use a separate image for icon and for cover.
      icon needs to be a square, and can be of low
      resolution, so the "t_thmb" size is more appropriate
      for him.

    * If there is a storyline for a game use it as a short
      description.

* IDGB -> IGDB

* use the longer text between storyline and description for description

---------

Co-authored-by: udifogiel <udifogiel@proton.me>
2025-10-24 09:25:54 +11:00
fa9620eac1 Use 7zip for archive backend (#264)
* feat: use 7zip for archive backend

* fix: lint
2025-10-13 13:02:27 +11:00
a201b62c04 chore(deps): bump axios from 1.11.0 to 1.12.0 (#246)
Bumps [axios](https://github.com/axios/axios) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 11:36:59 +11:00
9bf164ab77 chore(deps): bump tar-fs from 2.1.3 to 2.1.4 (#256)
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.3 to 2.1.4.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v2.1.3...v2.1.4)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 2.1.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 11:36:31 +11:00
97c6f3490c Add store sort options (#238) (#261)
This commit adds the option
to sort store items by name,
and to choose the sort order.

Co-authored-by: udifogiel <udifogiel@proton.me>
2025-10-13 11:20:48 +11:00
f5cb856d3d Carousel UI improvements (#258)
* make carousel pagination clickable

* make carousel in game pages wrap around

* make items in store fit the row when the filter menu is visible

---------

Co-authored-by: udifogiel <udifogiel@proton.me>
2025-10-13 11:18:52 +11:00
f331661c14 working commit 2025-10-02 17:45:54 +10:00
67de1f6c02 Add Steam metadata provider (#232) (#250)
* feat(metadata): add Steam metadata provider (#232)

* style(steam): remove emojis from log messages
2025-09-21 10:43:35 +10:00
27 changed files with 1505 additions and 228 deletions

View File

@ -4,9 +4,10 @@
v-for="(_, i) in amount"
:key="i"
:class="[
carousel.currentSlide == i ? 'bg-blue-600 w-6' : 'bg-zinc-700 w-3',
carousel.currentSlide === i ? 'bg-blue-600 w-6' : 'bg-zinc-700 w-3',
'transition-all cursor-pointer h-2 rounded-full',
]"
@click="slideTo(i)"
/>
</div>
</template>
@ -18,8 +19,8 @@ const carousel = inject(injectCarousel)!;
const amount = carousel.maxSlide - carousel.minSlide + 1;
// function slideTo(index: number) {
// const offsetIndex = index + carousel.minSlide;
// carousel.nav.slideTo(offsetIndex);
// }
function slideTo(index: number) {
const offsetIndex = index + carousel.minSlide;
carousel.nav.slideTo(offsetIndex);
}
</script>

View File

@ -1,3 +1,12 @@
<i18n>
{
"en": {
"↓": "↓",
"↑": "↑"
}
}
</i18n>
<template>
<div>
<div>
@ -176,9 +185,12 @@
active ? 'bg-zinc-900 outline-hidden' : '',
'w-full text-left block px-4 py-2 text-sm',
]"
@click="() => (currentSort = option.param)"
@click.prevent="handleSortClick(option, $event)"
>
{{ option.name }}
<span v-if="currentSort === option.param">
{{ sortOrder === "asc" ? $t("↑") : $t("↓") }}
</span>
</button>
</MenuItem>
</div>
@ -298,7 +310,7 @@
<div
v-if="games?.length ?? 0 > 0"
ref="product-grid"
class="relative lg:col-span-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-7 gap-4"
class="relative lg:col-span-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4"
>
<!-- Your content -->
<GamePanel
@ -397,8 +409,13 @@ const sorts: Array<StoreSortOption> = [
name: "Recently Added",
param: "recent",
},
{
name: "Name",
param: "name",
},
];
const currentSort = ref(sorts[0].param);
const sortOrder = ref<"asc" | "desc">("desc");
const options: Array<StoreFilterOption> = [
...(tags.length > 0
@ -474,7 +491,7 @@ async function updateGames(query: string, resetGames: boolean) {
results: Array<SerializeObject<GameModel>>;
count: number;
}>(
`/api/v1/store?take=50&skip=${resetGames ? 0 : games.value?.length || 0}&sort=${currentSort.value}${query ? "&" + query : ""}`,
`/api/v1/store?take=50&skip=${resetGames ? 0 : games.value?.length || 0}&sort=${currentSort.value}&order=${sortOrder.value}${query ? "&" + query : ""}`,
);
if (resetGames) {
games.value = newValues.results;
@ -491,6 +508,19 @@ watch(filterQuery, (newUrl) => {
watch(currentSort, (_) => {
updateGames(filterQuery.value, true);
});
watch(sortOrder, (_) => {
updateGames(filterQuery.value, true);
});
await updateGames(filterQuery.value, true);
function handleSortClick(option: StoreSortOption, event: MouseEvent) {
event.stopPropagation();
if (currentSort.value === option.param) {
sortOrder.value = sortOrder.value === "asc" ? "desc" : "asc";
} else {
currentSort.value = option.param;
sortOrder.value = option.param === "name" ? "asc" : "desc";
}
}
</script>

View File

@ -134,34 +134,41 @@
</div>
</div>
<!-- setup mode -->
<SwitchGroup as="div" class="max-w-lg flex items-center justify-between">
<span class="flex flex-grow flex-col">
<SwitchLabel
as="span"
class="text-sm font-medium leading-6 text-zinc-100"
passive
>{{ $t("library.admin.import.version.setupMode") }}</SwitchLabel
<fieldset class="max-w-lg">
<legend class="text-sm/6 font-semibold text-white">
Select an import mode
</legend>
<div class="mt-2 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
<label
v-for="mode in setupModes"
:key="mode.id"
:aria-label="mode.title"
:aria-description="mode.description"
class="cursor-pointer group relative flex rounded-lg border border-white/10 bg-zinc-800/50 p-4 has-checked:bg-blue-500/10 has-checked:outline-2 has-checked:-outline-offset-2 has-checked:outline-blue-500 has-focus-visible:outline-3 has-focus-visible:-outline-offset-1 has-disabled:bg-gray-800 has-disabled:opacity-25"
>
<SwitchDescription as="span" class="text-sm text-zinc-400">{{
$t("library.admin.import.version.setupModeDesc")
}}</SwitchDescription>
</span>
<Switch
v-model="versionSettings.onlySetup"
:class="[
versionSettings.onlySetup ? 'bg-blue-600' : 'bg-zinc-800',
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2',
]"
>
<span
aria-hidden="true"
:class="[
versionSettings.onlySetup ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
]"
<input
type="radio"
name="mode"
:value="mode.id"
:checked="versionSettings.onlySetup === mode.value"
class="absolute inset-0 appearance-none opacity-0 focus:outline-none"
@click="versionSettings.onlySetup = mode.value"
/>
</Switch>
</SwitchGroup>
<div class="flex-1">
<span class="block text-sm font-medium text-white">{{
mode.title
}}</span>
<span class="mt-1 block text-xs text-zinc-400">{{
mode.description
}}</span>
</div>
<CheckCircleIcon
class="invisible size-5 text-blue-500 group-has-checked:visible"
aria-hidden="true"
/>
</label>
</div>
</fieldset>
<!-- launch commands -->
<div class="relative max-w-3xl">
<label
@ -462,10 +469,14 @@ import {
} from "@headlessui/vue";
import { XCircleIcon } from "@heroicons/vue/16/solid";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { PlusIcon, TrashIcon } from "@heroicons/vue/24/outline";
import {
CheckCircleIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/vue/24/outline";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/solid";
import type { SerializeObject } from "nitropack";
import type { ImportVersion } from "~~/server/api/v1/admin/import/version/index.post";
import type { ImportGameVersion } from "~~/server/api/v1/admin/import/version/index.post";
definePageMeta({
layout: "admin",
@ -481,9 +492,10 @@ const versions = await $dropFetch(
const userPlatforms = await useAdminPlatforms();
const allPlatforms = renderPlatforms(userPlatforms);
const currentlySelectedVersion = ref(-1);
const versionSettings = ref<Partial<typeof ImportVersion.infer>>({
id: gameId,
const versionSettings = ref<Partial<ImportGameVersion>>({
launches: [],
onlySetup: false,
});
const versionGuesses =
@ -540,7 +552,7 @@ async function updateCurrentlySelectedVersion(value: number) {
const options = await $dropFetch(
`/api/v1/admin/import/version/preload?id=${encodeURIComponent(
gameId,
)}&version=${encodeURIComponent(version)}`,
)}&version=${encodeURIComponent(version)}&mode=game`,
);
versionGuesses.value = options.map((e) => ({
...e,
@ -556,6 +568,7 @@ async function startImport() {
body: {
id: gameId,
version: versions[currentlySelectedVersion.value],
mode: "game",
...versionSettings.value,
},
});
@ -572,4 +585,26 @@ function startImport_wrapper() {
importLoading.value = false;
});
}
const setupModes: Array<{
id: string;
value: boolean;
title: string;
description: string;
}> = [
{
id: "portable",
value: false,
title: "Portable",
description:
"This mode is for games that are designed to be launched directly from the install directory. Drop works best with these.",
},
{
id: "setup",
value: true,
title: "Installer",
description:
"Also known as 'setup-only', this mode is for installers that modify the system directly, and install to directories like Program Files.",
},
];
</script>

View File

@ -1 +1 @@
<template></template>
<template><div /></template>

View File

@ -72,7 +72,7 @@
{{ $t("store.images") }}
</h2>
<div class="relative">
<VueCarousel :items-to-show="1">
<VueCarousel :items-to-show="1" :wrap-around="true">
<VueSlide
v-for="image in game.mImageCarouselObjectIds"
:key="image"

View File

@ -190,7 +190,7 @@
{{ game.mShortDescription }}
</p>
<div class="mt-6 py-4 rounded">
<VueCarousel :items-to-show="1">
<VueCarousel :items-to-show="1" :wrap-around="true">
<VueSlide
v-for="image in game.mImageCarouselObjectIds"
:key="image"

View File

@ -19,7 +19,7 @@ export default withNuxt([
},
],
"@intlify/vue-i18n/no-missing-keys": "error",
"vue/multi-word-component-names": "ignore",
"vue/multi-word-component-names": "off",
},
settings: {
"vue-i18n": {

View File

@ -263,6 +263,7 @@ export default defineNuxtConfig({
"https://www.giantbomb.com",
"https://images.pcgamingwiki.com",
"https://images.igdb.com",
"https://*.steamstatic.com",
],
},
strictTransportSecurity: false,

View File

@ -21,7 +21,7 @@
},
"dependencies": {
"@discordapp/twemoji": "^16.0.1",
"@drop-oss/droplet": "3.0.1",
"@drop-oss/droplet": "3.2.0",
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@nuxt/fonts": "^0.11.0",
@ -32,7 +32,7 @@
"@vueuse/nuxt": "13.6.0",
"argon2": "^0.43.0",
"arktype": "^2.1.10",
"axios": "^1.7.7",
"axios": "^1.12.0",
"bcryptjs": "^3.0.2",
"cheerio": "^1.0.0",
"cookie-es": "^2.0.0",

159
pnpm-lock.yaml generated
View File

@ -12,8 +12,8 @@ importers:
specifier: ^16.0.1
version: 16.0.1
'@drop-oss/droplet':
specifier: 3.0.1
version: 3.0.1
specifier: 3.2.0
version: 3.2.0
'@headlessui/vue':
specifier: ^1.7.23
version: 1.7.23(vue@3.5.21(typescript@5.9.2))
@ -45,7 +45,7 @@ importers:
specifier: ^2.1.10
version: 2.1.22
axios:
specifier: ^1.7.7
specifier: ^1.12.0
version: 1.12.2
bcryptjs:
specifier: ^3.0.2
@ -387,74 +387,83 @@ packages:
'@discordapp/twemoji@16.0.1':
resolution: {integrity: sha512-figLiBWzjS5cyrAjLaGjM8AAaowO3qvK8rg5bA2dElB4qsaPMvBVlFDMO2d3x+nC1igt7kgWH4dvNmvvUHUF8w==}
'@drop-oss/droplet-darwin-arm64@3.0.1':
resolution: {integrity: sha512-LXe8vsXUBL96boI78H6oXpSaPVwF4cCwJ5l/QVtsOWMebNo6gk9wICDZ+5IoR/Ol32t1a1lk+DjbD1zeGenPxg==}
'@drop-oss/droplet-darwin-arm64@3.2.0':
resolution: {integrity: sha512-dH/vRFxuLjOzYBBvDG140wKcx4LmFxBJ5iTjZrWzV641wiRjx8B38niWXuqZ2ZADkCL4muOvgRGFJ4W1N/j6jQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@drop-oss/droplet-darwin-universal@3.0.1':
resolution: {integrity: sha512-Mf2gjC24u6s8djV/3slZvwdr4+h0qBu2OYXBUSDfR4H/VJwV5TstnWVKF+U8d1hjmHE9eLO8elbGNnpQmSoTOQ==}
'@drop-oss/droplet-darwin-universal@3.2.0':
resolution: {integrity: sha512-k7Xhzs2mXrQcm3SLhLNDBkUaCWqtbQ6dyme1ubsG9PZEcvv25T//8CNVFEsHVZTKqj5nF41iSh4Wz1Qn6VxkVw==}
engines: {node: '>= 10'}
os: [darwin]
'@drop-oss/droplet-darwin-x64@3.0.1':
resolution: {integrity: sha512-4IIDl/E+hzZ2Vt9m4FMPlZEXwj1EwE6qXyUidACK6TTFqpjLpsEHKuhv1FOxGyJ8qkvagtyPCc+cs1TxoZD6FA==}
'@drop-oss/droplet-darwin-x64@3.2.0':
resolution: {integrity: sha512-GvRwQrtcC1Dq6YyXxBGSFj+WasnIa1dk9t2lCaR9OQdh3qp2did21o2poo1Sgdjg+mI2lUdgZ6w0yXJlL1vl+A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@drop-oss/droplet-linux-arm64-gnu@3.0.1':
resolution: {integrity: sha512-klGvlLf1xSMT3iYsIAaBbmbir1ZJWtcVyOMUlsfc1lkJ8mgyB+PrW4BsnYj7Pp4G34n7WsOChjC8TdJDBBuBWg==}
'@drop-oss/droplet-linux-arm64-gnu@3.2.0':
resolution: {integrity: sha512-ZqH0xTEeSeJF77vy8rZDxHEV8JMaN0khdg6ptpnbBfc56J5jt6wS3NlHK8M0ZVlDqqZnXMS1vUO0b6rfmQodKw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@drop-oss/droplet-linux-arm64-musl@3.0.1':
resolution: {integrity: sha512-oOjvGETlrJGC1RlNhUoVS9N89Rn/0DqBauVz3BBFjJTKSd5jU3/gLzwgmfkKDGVEU5lyGPAn2WQroiESEG9wdA==}
'@drop-oss/droplet-linux-arm64-musl@3.2.0':
resolution: {integrity: sha512-TTw44PggYfp3RJkvNhXH89duuuvONEA8c8oRBCzCczRf3hDnbzCQLaB1UlnIlESsJZXXiFSDIBV2/0kkpB+Ukg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@drop-oss/droplet-linux-riscv64-gnu@3.0.1':
resolution: {integrity: sha512-Zf3gUsWq9Hqb275MOi7PJDhmJz7Qa/Y1XMen880bxPaOeDFqFOoKUxUr2/qv1MYp6tT3zO27NprGsHirYWqsyA==}
'@drop-oss/droplet-linux-riscv64-gnu@3.2.0':
resolution: {integrity: sha512-Ee/PfkoG8pm/9C3LFXJleIi5N8V5cK+44p+iDaneAo6gj5k67zYzuga3mJVswTgd3fncG1cw+xPqBl4PUWc1pg==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
'@drop-oss/droplet-linux-x64-gnu@3.0.1':
resolution: {integrity: sha512-sskblycJdtNJVnRHjPHhwHkQUfQNaDIWDzXOzEaBPOcDKqYA7od7VMDAseqBkrKDn7l8bBUtRXFAipdsO8hffw==}
'@drop-oss/droplet-linux-x64-gnu@3.2.0':
resolution: {integrity: sha512-L2M/MEoe5Y74MTtzpEWHIvdyRSPLgM1WLzpb/xRNCWe8d6FcUFDgdMlbd6rDj5t4Q6JEzyMIHUciVRaYIv+ShA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@drop-oss/droplet-linux-x64-musl@3.0.1':
resolution: {integrity: sha512-lh+1M6UAf5+ET1/ZEFRsB3shFHjkT/9Ql9akr/vyUue91TWPmP71meqVkCugWDhP6lxBt56jg2VVrJfmPAsK6w==}
'@drop-oss/droplet-linux-x64-musl@3.2.0':
resolution: {integrity: sha512-F/uQUAHWbhiiAtoyKHQHPgjG7jJd8pQX6sCgdf5ufCdwFLvHEdu9pO0qN+xpzaACceIKX4Vip0vUwQwEzYhAKA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@drop-oss/droplet-win32-arm64-msvc@3.0.1':
resolution: {integrity: sha512-caQDPoDNJyyJXUEijw+hGTy0wmCrW5efTqBwnvMcQ282EOilg1d5WeJ31pfEcuLYF4MK1t9uaLcG6jZ9YLtzEQ==}
'@drop-oss/droplet-win32-arm64-msvc@3.2.0':
resolution: {integrity: sha512-x7i1KKL8vQGcXbKIyH56LCEdQxLKNEk/KFjuD/YGrbBJ/+Q+fh46hLK+Sx4I/HzPHecd5g3xc2kVgO7+DgjhYA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@drop-oss/droplet-win32-x64-msvc@3.0.1':
resolution: {integrity: sha512-bp8KwewF/T3JkVeJWkg86U3b0cGQD9i8k92x6HYPtnF5nLPAb2UIUEJgmYYFNPFe36RECBV7PIIG0ujdT1ELQw==}
'@drop-oss/droplet-win32-x64-msvc@3.2.0':
resolution: {integrity: sha512-lC8a456IQ0ArzX40IlStolV4GIdl26xF9PikcuQ9r+n4VDqWSHb8A0Wwj87leU3QdoMu+Y2nlA1QHKgpVSEuoQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@drop-oss/droplet@3.0.1':
resolution: {integrity: sha512-YhtgpwNqEHO8R03yf9Xb5LXuaLWkQvY+2lxOD1PwzpGI5V9PKlDE+x1IJBmdBF5bDPDGk9MxQidGtnYQuAEBEA==}
'@drop-oss/droplet@3.2.0':
resolution: {integrity: sha512-+3zw3MPriMrj8HlKAq2VTlXEPOXN0homusjmQcBRzVx7GjtGvb5Y9YIHs16qfn8zdTEDi5twrtsUBQYkVjU2bQ==}
engines: {node: '>= 10'}
'@emnapi/core@1.5.0':
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
'@emnapi/core@1.4.5':
resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
'@emnapi/runtime@1.5.0':
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
'@emnapi/core@1.6.0':
resolution: {integrity: sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==}
'@emnapi/runtime@1.4.5':
resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
'@emnapi/runtime@1.6.0':
resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==}
'@emnapi/wasi-threads@1.0.4':
resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==}
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
@ -851,8 +860,8 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
'@napi-rs/wasm-runtime@1.0.5':
resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==}
'@napi-rs/wasm-runtime@1.0.7':
resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@ -1804,6 +1813,9 @@ packages:
'@twemoji/parser@16.0.0':
resolution: {integrity: sha512-jmuIjkp3OIaEemwMy3sArBwZSuZkRqmueGwRe2Zk4cFzbUJISFBJSZLDUUBNIgq3c+nY49ideYN2OiII6JUqwA==}
'@tybys/wasm-util@0.10.0':
resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@ -5802,56 +5814,72 @@ snapshots:
jsonfile: 5.0.0
universalify: 0.1.2
'@drop-oss/droplet-darwin-arm64@3.0.1':
'@drop-oss/droplet-darwin-arm64@3.2.0':
optional: true
'@drop-oss/droplet-darwin-universal@3.0.1':
'@drop-oss/droplet-darwin-universal@3.2.0':
optional: true
'@drop-oss/droplet-darwin-x64@3.0.1':
'@drop-oss/droplet-darwin-x64@3.2.0':
optional: true
'@drop-oss/droplet-linux-arm64-gnu@3.0.1':
'@drop-oss/droplet-linux-arm64-gnu@3.2.0':
optional: true
'@drop-oss/droplet-linux-arm64-musl@3.0.1':
'@drop-oss/droplet-linux-arm64-musl@3.2.0':
optional: true
'@drop-oss/droplet-linux-riscv64-gnu@3.0.1':
'@drop-oss/droplet-linux-riscv64-gnu@3.2.0':
optional: true
'@drop-oss/droplet-linux-x64-gnu@3.0.1':
'@drop-oss/droplet-linux-x64-gnu@3.2.0':
optional: true
'@drop-oss/droplet-linux-x64-musl@3.0.1':
'@drop-oss/droplet-linux-x64-musl@3.2.0':
optional: true
'@drop-oss/droplet-win32-arm64-msvc@3.0.1':
'@drop-oss/droplet-win32-arm64-msvc@3.2.0':
optional: true
'@drop-oss/droplet-win32-x64-msvc@3.0.1':
'@drop-oss/droplet-win32-x64-msvc@3.2.0':
optional: true
'@drop-oss/droplet@3.0.1':
'@drop-oss/droplet@3.2.0':
optionalDependencies:
'@drop-oss/droplet-darwin-arm64': 3.0.1
'@drop-oss/droplet-darwin-universal': 3.0.1
'@drop-oss/droplet-darwin-x64': 3.0.1
'@drop-oss/droplet-linux-arm64-gnu': 3.0.1
'@drop-oss/droplet-linux-arm64-musl': 3.0.1
'@drop-oss/droplet-linux-riscv64-gnu': 3.0.1
'@drop-oss/droplet-linux-x64-gnu': 3.0.1
'@drop-oss/droplet-linux-x64-musl': 3.0.1
'@drop-oss/droplet-win32-arm64-msvc': 3.0.1
'@drop-oss/droplet-win32-x64-msvc': 3.0.1
'@drop-oss/droplet-darwin-arm64': 3.2.0
'@drop-oss/droplet-darwin-universal': 3.2.0
'@drop-oss/droplet-darwin-x64': 3.2.0
'@drop-oss/droplet-linux-arm64-gnu': 3.2.0
'@drop-oss/droplet-linux-arm64-musl': 3.2.0
'@drop-oss/droplet-linux-riscv64-gnu': 3.2.0
'@drop-oss/droplet-linux-x64-gnu': 3.2.0
'@drop-oss/droplet-linux-x64-musl': 3.2.0
'@drop-oss/droplet-win32-arm64-msvc': 3.2.0
'@drop-oss/droplet-win32-x64-msvc': 3.2.0
'@emnapi/core@1.5.0':
'@emnapi/core@1.4.5':
dependencies:
'@emnapi/wasi-threads': 1.0.4
tslib: 2.8.1
optional: true
'@emnapi/core@1.6.0':
dependencies:
'@emnapi/wasi-threads': 1.1.0
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.5.0':
'@emnapi/runtime@1.4.5':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.6.0':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.0.4':
dependencies:
tslib: 2.8.1
optional: true
@ -6228,15 +6256,15 @@ snapshots:
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.5.0
'@emnapi/runtime': 1.5.0
'@tybys/wasm-util': 0.10.1
'@emnapi/core': 1.4.5
'@emnapi/runtime': 1.4.5
'@tybys/wasm-util': 0.10.0
optional: true
'@napi-rs/wasm-runtime@1.0.5':
'@napi-rs/wasm-runtime@1.0.7':
dependencies:
'@emnapi/core': 1.5.0
'@emnapi/runtime': 1.5.0
'@emnapi/core': 1.6.0
'@emnapi/runtime': 1.6.0
'@tybys/wasm-util': 0.10.1
optional: true
@ -6722,7 +6750,7 @@ snapshots:
'@oxc-minify/binding-wasm32-wasi@0.87.0':
dependencies:
'@napi-rs/wasm-runtime': 1.0.5
'@napi-rs/wasm-runtime': 1.0.7
optional: true
'@oxc-minify/binding-win32-arm64-msvc@0.87.0':
@ -6807,7 +6835,7 @@ snapshots:
'@oxc-parser/binding-wasm32-wasi@0.87.0':
dependencies:
'@napi-rs/wasm-runtime': 1.0.5
'@napi-rs/wasm-runtime': 1.0.7
optional: true
'@oxc-parser/binding-win32-arm64-msvc@0.70.0':
@ -6870,7 +6898,7 @@ snapshots:
'@oxc-transform/binding-wasm32-wasi@0.87.0':
dependencies:
'@napi-rs/wasm-runtime': 1.0.5
'@napi-rs/wasm-runtime': 1.0.7
optional: true
'@oxc-transform/binding-win32-arm64-msvc@0.87.0':
@ -7258,6 +7286,11 @@ snapshots:
'@twemoji/parser@16.0.0': {}
'@tybys/wasm-util@0.10.0':
dependencies:
tslib: 2.8.1
optional: true
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1

View File

@ -1,4 +1,8 @@
onlyBuiltDependencies:
- '@prisma/client'
- '@prisma/engines'
- '@tailwindcss/oxide'
- esbuild
- prisma
shamefullyHoist: true

View File

@ -0,0 +1,8 @@
-- AlterEnum
ALTER TYPE "MetadataSource" ADD VALUE 'Steam';
-- DropIndex
DROP INDEX "GameTag_name_idx";
-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));

View File

@ -0,0 +1,41 @@
/*
Warnings:
- A unique constraint covering the columns `[installRId]` on the table `LaunchOption` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[uninstallRId]` on the table `LaunchOption` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[installId]` on the table `RedistVersion` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[uninstallId]` on the table `RedistVersion` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "public"."GameTag_name_idx";
-- AlterTable
ALTER TABLE "public"."LaunchOption" ADD COLUMN "installRId" TEXT,
ADD COLUMN "uninstallRId" TEXT;
-- AlterTable
ALTER TABLE "public"."RedistVersion" ADD COLUMN "installId" TEXT,
ADD COLUMN "onlySetup" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "uninstallId" TEXT;
-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
-- CreateIndex
CREATE UNIQUE INDEX "LaunchOption_installRId_key" ON "public"."LaunchOption"("installRId");
-- CreateIndex
CREATE UNIQUE INDEX "LaunchOption_uninstallRId_key" ON "public"."LaunchOption"("uninstallRId");
-- CreateIndex
CREATE UNIQUE INDEX "RedistVersion_installId_key" ON "public"."RedistVersion"("installId");
-- CreateIndex
CREATE UNIQUE INDEX "RedistVersion_uninstallId_key" ON "public"."RedistVersion"("uninstallId");
-- AddForeignKey
ALTER TABLE "public"."RedistVersion" ADD CONSTRAINT "RedistVersion_installId_fkey" FOREIGN KEY ("installId") REFERENCES "public"."LaunchOption"("launchId") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."RedistVersion" ADD CONSTRAINT "RedistVersion_uninstallId_fkey" FOREIGN KEY ("uninstallId") REFERENCES "public"."LaunchOption"("launchId") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,15 @@
/*
Warnings:
- Added the required column `versionIndex` to the `RedistVersion` table without a default value. This is not possible if the table is not empty.
*/
-- DropIndex
DROP INDEX "public"."GameTag_name_idx";
-- AlterTable
ALTER TABLE "public"."RedistVersion" ADD COLUMN "delta" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "versionIndex" INTEGER NOT NULL;
-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));

View File

@ -49,6 +49,11 @@ model LaunchOption {
uninstallGId String? @unique
uninstallGVersion GameVersion? @relation(name: "uninstall")
installRId String? @unique
installRVersion RedistVersion? @relation(name: "install_redist")
uninstallRId String? @unique
uninstallRVersion RedistVersion? @relation(name: "uninstall_redist")
name String
description String
@ -125,8 +130,17 @@ model RedistVersion {
versionId String @id
version Version @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade)
installId String? @unique
install LaunchOption? @relation(name: "install_redist", fields: [installId], references: [launchId])
uninstallId String? @unique
uninstall LaunchOption? @relation(name: "uninstall_redist", fields: [uninstallId], references: [launchId])
onlySetup Boolean @default(false)
launches LaunchOption[]
versionIndex Int
delta Boolean @default(false)
gameDependees GameVersion[]
dlcDependees DLCVersion[]

View File

@ -5,6 +5,7 @@ enum MetadataSource {
IGDB
Metacritic
OpenCritic
Steam
}
model Game {

View File

@ -1,9 +1,7 @@
import { type } from "arktype";
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
import aclManager from "~~/server/internal/acls";
import prisma from "~~/server/internal/db/database";
import libraryManager from "~~/server/internal/library";
import { convertIDToLink } from "~~/server/internal/platform/link";
export const LaunchCommands = type({
name: "string > 0",
@ -12,14 +10,18 @@ export const LaunchCommands = type({
launchArgs: "string = ''",
}).array();
export const ImportVersion = type({
const ImportVersionBase = type({
id: "string",
version: "string",
name: "string?",
platform: "string",
onlySetup: "boolean = false",
delta: "boolean = false",
});
const ImportGameVersion = type({
mode: "'game'",
onlySetup: "boolean = false",
umuId: "string = ''",
install: "string?",
@ -27,7 +29,26 @@ export const ImportVersion = type({
launches: LaunchCommands,
uninstall: "string?",
uninstallArgs: "string?",
}).configure(throwingArktype);
});
const ImportRedistVersion = type({
mode: "'redist'",
install: "string?",
installArgs: "string?",
launches: LaunchCommands,
uninstall: "string?",
uninstallArgs: "string?",
});
export const ImportVersion = ImportVersionBase.and(
ImportGameVersion.or(ImportRedistVersion),
).configure(throwingArktype);
export type ImportGameVersion = typeof ImportVersionBase.infer &
typeof ImportGameVersion.infer;
export type ImportRedistVersion = typeof ImportVersionBase.infer &
typeof ImportRedistVersion.infer;
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:version:new"]);
@ -35,48 +56,10 @@ export default defineEventHandler(async (h3) => {
const body = await readDropValidatedBody(h3, ImportVersion);
const platform = await convertIDToLink(body.platform);
if (!platform)
throw createError({ statusCode: 400, message: "Invalid platform." });
if (body.delta) {
const validOverlayVersions = await prisma.gameVersion.count({
where: {
version: {
gameId: body.id,
},
delta: false,
platform,
},
});
if (validOverlayVersions == 0)
throw createError({
statusCode: 400,
message:
"Update mode requires a pre-existing version for this platform.",
});
}
if (body.onlySetup) {
if (!body.install)
throw createError({
statusCode: 400,
message: 'Install required in "setup mode".',
});
} else {
if (!body.delta && body.launches.length == 0)
throw createError({
statusCode: 400,
message:
"At least one launch command is required for non-delta versions",
});
}
// startup & delta require more complex checking logic
const taskId = await libraryManager.importVersion(
body.id,
body.version,
"game",
body,
);
if (!taskId)

View File

@ -1,22 +1,26 @@
import { ArkErrors, type } from "arktype";
import aclManager from "~~/server/internal/acls";
import libraryManager from "~~/server/internal/library";
import libraryManager, { VersionImportModes } from "~~/server/internal/library";
export const PreloadQuery = type({
id: "string",
version: "string",
mode: type.enumerated(...VersionImportModes),
});
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:version:read"]);
if (!allowed) throw createError({ statusCode: 403 });
const query = await getQuery(h3);
const gameId = query.id?.toString();
const versionName = query.version?.toString();
if (!gameId || !versionName)
throw createError({
statusCode: 400,
message: "Missing id or version in request params",
});
const rawQuery = await getQuery(h3);
const query = PreloadQuery(rawQuery);
if (query instanceof ArkErrors)
throw createError({ statusCode: 400, message: query.summary });
const preload = await libraryManager.fetchUnimportedVersionInformation(
gameId,
versionName,
query.id,
query.mode,
query.version,
);
if (!preload)
throw createError({

View File

@ -2,7 +2,7 @@ import { type } from "arktype";
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
import aclManager from "~~/server/internal/acls";
import taskHandler from "~~/server/internal/tasks";
import { TASK_GROUPS, type TaskGroup } from "~~/server/internal/tasks/group";
import { TASK_GROUPS } from "~~/server/internal/tasks/group";
const StartTask = type({
taskGroup: type.enumerated(...TASK_GROUPS),

View File

@ -1,5 +1,5 @@
import { type } from "arktype";
import { ClientCapabilities } from "~~/prisma/client/enums";
import type { ClientCapabilities } from "~~/prisma/client/enums";
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
import type {
CapabilityConfiguration,
@ -7,7 +7,7 @@ import type {
import capabilityManager, {
validCapabilities,
} from "~~/server/internal/clients/capabilities";
import clientHandler, { AuthMode, AuthModes } from "~~/server/internal/clients/handler";
import clientHandler, { AuthModes } from "~~/server/internal/clients/handler";
import { parsePlatform } from "~~/server/internal/utils/parseplatform";
const ClientAuthInitiate = type({

View File

@ -1,9 +1,7 @@
import { type } from "arktype";
import { ClientCapabilities } from "~~/prisma/client/enums";
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
import capabilityManager, {
validCapabilities,
} from "~~/server/internal/clients/capabilities";
import capabilityManager from "~~/server/internal/clients/capabilities";
import { defineClientEventHandler } from "~~/server/internal/clients/event-handler";
import notificationSystem from "~~/server/internal/notifications";

View File

@ -17,7 +17,8 @@ const StoreRead = type({
company: "string?",
companyActions: "string = 'published,developed'",
sort: "'default' | 'newest' | 'recent' = 'default'",
sort: "'default' | 'newest' | 'recent' | 'name' = 'default'",
order: "'asc' | 'desc' = 'desc'",
});
export default defineEventHandler(async (h3) => {
@ -101,10 +102,13 @@ export default defineEventHandler(async (h3) => {
switch (options.sort) {
case "default":
case "newest":
sort.mReleased = "desc";
sort.mReleased = options.order;
break;
case "recent":
sort.created = "desc";
sort.created = options.order;
break;
case "name":
sort.mName = options.order;
break;
}

View File

@ -18,7 +18,10 @@ import type {
GameVersionCreateInput,
LaunchOptionCreateManyInput,
VersionCreateArgs,
VersionWhereInput,
} from "~~/prisma/client/models";
import type { PlatformLink } from "~~/prisma/client/client";
import { convertIDToLink } from "../platform/link";
export const VersionImportModes = ["game", "redist"] as const;
export type VersionImportMode = (typeof VersionImportModes)[number];
@ -216,7 +219,17 @@ class LibraryManager {
return await this.fetchLibraryObjectWithStatus(redists);
}
private async fetchLibraryPath(id: string, mode: VersionImportMode) {
private async fetchLibraryPath(
id: string,
mode: VersionImportMode,
platform?: PlatformLink,
): Promise<
| [
{ mName: string; libraryId: string; libraryPath: string } | null,
VersionWhereInput,
]
| undefined
> {
switch (mode) {
case "game":
return [
@ -224,8 +237,8 @@ class LibraryManager {
where: { id },
select: { mName: true, libraryId: true, libraryPath: true },
}),
{ gameId: id },
] as const;
{ gameId: id, gameVersions: { some: { platform } } },
];
case "redist":
return [
await prisma.redist.findUnique({
@ -233,7 +246,7 @@ class LibraryManager {
select: { mName: true, libraryId: true, libraryPath: true },
}),
{ redistId: id },
] as const;
];
}
return undefined;
}
@ -241,11 +254,10 @@ class LibraryManager {
private createVersionOptions(
id: string,
currentIndex: number,
mode: VersionImportMode,
metadata: typeof ImportVersion.infer,
): Partial<VersionCreateArgs["data"]> {
switch (mode) {
case "game":
switch (metadata.mode) {
case "game": {
const installCreator = {
install: {
create: {
@ -303,6 +315,7 @@ class LibraryManager {
},
},
};
}
case "redist":
return {};
}
@ -310,18 +323,20 @@ class LibraryManager {
/**
* Fetches recommendations and extra data about the version. Doesn't actually check if it's been imported.
* @param gameId
* @param versionName
* @param id
* @param version
* @returns
*/
async fetchUnimportedVersionInformation(gameId: string, versionName: string) {
const game = await prisma.game.findUnique({
where: { id: gameId },
select: { libraryPath: true, libraryId: true, mName: true },
});
if (!game || !game.libraryId) return undefined;
async fetchUnimportedVersionInformation(
id: string,
mode: VersionImportMode,
version: string,
) {
const value = await this.fetchLibraryPath(id, mode);
if (!value?.[0] || !value[0].libraryId) return undefined;
const [libraryDetails] = value;
const library = this.libraries.get(game.libraryId);
const library = this.libraries.get(libraryDetails.libraryId);
if (!library) return undefined;
const userPlatforms = await prisma.userPlatform.findMany({});
@ -354,7 +369,10 @@ class LibraryManager {
match: number;
}> = [];
const files = await library.versionReaddir(game.libraryPath, versionName);
const files = await library.versionReaddir(
libraryDetails.libraryPath,
version,
);
for (const filename of files) {
const basename = path.basename(filename);
const dotLocation = filename.lastIndexOf(".");
@ -363,7 +381,7 @@ class LibraryManager {
for (const [platform, checkExts] of Object.entries(fileExts)) {
for (const checkExt of checkExts) {
if (checkExt != ext) continue;
const fuzzyValue = fuzzy(basename, game.mName);
const fuzzyValue = fuzzy(basename, libraryDetails.mName);
options.push({
filename,
platform,
@ -404,17 +422,56 @@ class LibraryManager {
async importVersion(
id: string,
version: string,
mode: VersionImportMode,
metadata: typeof ImportVersion.infer,
) {
const taskId = createVersionImportTaskId(id, version);
const value = await this.fetchLibraryPath(id, mode);
if (!value || !value[0]) return undefined;
if (metadata.mode === "game") {
if (metadata.onlySetup) {
if (!metadata.install)
throw createError({
statusCode: 400,
message: "An install command is required in only-setup mode.",
});
} else {
if (!metadata.delta && metadata.launches.length == 0)
throw createError({
statusCode: 400,
message:
"At least one launch command is required in non-delta, non-setup mode.",
});
}
}
const platform = await convertIDToLink(metadata.platform);
if (!platform)
throw createError({ statusCode: 400, message: "Invalid platform." });
const value = await this.fetchLibraryPath(id, metadata.mode, platform);
if (!value || !value[0])
throw createError({
statusCode: 400,
message: `${metadata.mode} not found.`,
});
const [libraryDetails, idFilter] = value;
const library = this.libraries.get(libraryDetails.libraryId);
if (!library) return undefined;
if (!library)
throw createError({
statusCode: 500,
message: "Library not found but exists in database?",
});
const currentIndex = await prisma.version.count({
where: { ...idFilter },
});
if (metadata.delta && currentIndex == 0)
throw createError({
statusCode: 400,
message:
"At least one pre-existing version of the same platform is required for delta mode.",
});
taskHandler.create({
id: taskId,
@ -439,10 +496,6 @@ class LibraryManager {
logger.info("Created manifest successfully!");
const currentIndex = await prisma.version.count({
where: { ...idFilter },
});
// Then, create the database object
await prisma.version.create({
data: {
@ -450,7 +503,7 @@ class LibraryManager {
versionName: metadata.name ?? version,
dropletManifest: manifest,
...libraryManager.createVersionOptions(id, currentIndex, mode, metadata)
...libraryManager.createVersionOptions(id, currentIndex, metadata),
},
});
@ -460,7 +513,7 @@ class LibraryManager {
nonce: `version-create-${id}-${version}`,
title: `'${libraryDetails.mName}' ('${version}') finished importing.`,
description: `Drop finished importing version ${version} for ${libraryDetails.mName}.`,
actions: [`View|/admin/library/${modeToLink[mode]}/${id}`],
actions: [`View|/admin/library/${modeToLink[metadata.mode]}/${id}`],
acls: ["system:import:version:read"],
});

View File

@ -72,7 +72,7 @@ interface IGDBCompanyWebsite extends IGDBItem {
}
interface IGDBCover extends IGDBItem {
url: string;
image_id: string;
}
interface IGDBSearchStub extends IGDBItem {
@ -179,7 +179,7 @@ export class IGDBProvider implements MetadataProvider {
if (response.status !== 200)
throw new Error(
`Error in IDGB \nStatus Code: ${response.status}\n${response.data}`,
`Error in IGDB \nStatus Code: ${response.status}\n${response.data}`,
);
this.accessToken = response.data.access_token;
@ -187,7 +187,7 @@ export class IGDBProvider implements MetadataProvider {
seconds: response.data.expires_in,
});
logger.info("IDGB done authorizing with twitch");
logger.info("IGDB done authorizing with twitch");
}
private async refreshCredentials() {
@ -246,39 +246,47 @@ export class IGDBProvider implements MetadataProvider {
return <T[]>response.data;
}
private async _getMediaInternal(mediaID: IGDBID, type: string) {
private async _getMediaInternal(
mediaID: IGDBID,
type: string,
size: string = "t_thumb",
) {
if (mediaID === undefined)
throw new Error(
`IGDB mediaID when getting item of type ${type} was undefined`,
);
const body = `where id = ${mediaID}; fields url;`;
const body = `where id = ${mediaID}; fields image_id;`;
const response = await this.request<IGDBCover>(type, body);
let result = "";
response.forEach((cover) => {
if (cover.url.startsWith("https:")) {
result = cover.url;
} else {
// twitch *sometimes* provides it in the format "//images.igdb.com"
result = `https:${cover.url}`;
if (!response.length || !response[0].image_id) {
throw new Error(`No image_id found for ${type} with id ${mediaID}`);
}
});
const imageId = response[0].image_id;
const result = `https://images.igdb.com/igdb/image/upload/${size}/${imageId}.jpg`;
return result;
}
private async getCoverURL(id: IGDBID) {
return await this._getMediaInternal(id, "covers");
return await this._getMediaInternal(id, "covers", "t_cover_big");
}
private async getArtworkURL(id: IGDBID) {
return await this._getMediaInternal(id, "artworks");
return await this._getMediaInternal(id, "artworks", "t_1080p");
}
private async getScreenshotURL(id: IGDBID) {
return await this._getMediaInternal(id, "screenshots", "t_1080p");
}
private async getIconURL(id: IGDBID) {
return await this._getMediaInternal(id, "covers", "t_thumb");
}
private async getCompanyLogoURl(id: IGDBID) {
return await this._getMediaInternal(id, "company_logos");
return await this._getMediaInternal(id, "company_logos", "t_original");
}
private trimMessage(msg: string, len: number) {
@ -327,7 +335,7 @@ export class IGDBProvider implements MetadataProvider {
let icon = "";
const cover = response[i].cover;
if (cover !== undefined) {
icon = await this.getCoverURL(cover);
icon = await this.getIconURL(cover);
} else {
icon = "";
}
@ -355,23 +363,26 @@ export class IGDBProvider implements MetadataProvider {
const currentGame = (await this.request<IGDBGameFull>("games", body)).at(0);
if (!currentGame) throw new Error("No game found on IGDB with that id");
context?.logger.info("Using IDGB provider.");
context?.logger.info("Using IGDB provider.");
let iconRaw;
let iconRaw, coverRaw;
const cover = currentGame.cover;
if (cover !== undefined) {
context?.logger.info("Found cover URL, using...");
iconRaw = await this.getCoverURL(cover);
iconRaw = await this.getIconURL(cover);
coverRaw = await this.getCoverURL(cover);
} else {
context?.logger.info("Missing cover URL, using fallback...");
iconRaw = jdenticon.toPng(id, 512);
coverRaw = iconRaw;
}
const icon = createObject(iconRaw);
const coverID = createObject(coverRaw);
let banner;
const images = [icon];
const images = [coverID];
for (const art of currentGame.artworks ?? []) {
const objectId = createObject(await this.getArtworkURL(art));
if (!banner) {
@ -384,6 +395,11 @@ export class IGDBProvider implements MetadataProvider {
banner = createObject(jdenticon.toPng(id, 512));
}
for (const screenshot of currentGame.screenshots ?? []) {
const objectId = createObject(await this.getScreenshotURL(screenshot));
images.push(objectId);
}
context?.progress(20);
const publishers: CompanyModel[] = [];
@ -452,13 +468,25 @@ export class IGDBProvider implements MetadataProvider {
const genres = await this.getGenres(currentGame.genres);
const deck = this.trimMessage(currentGame.summary, 280);
let description = "";
let shortDescription = "";
if (currentGame.summary.length > (currentGame.storyline?.length ?? 0)) {
description = currentGame.summary;
shortDescription = this.trimMessage(
currentGame.storyline ?? currentGame.summary,
280,
);
} else {
description = currentGame.storyline ?? currentGame.summary;
shortDescription = this.trimMessage(currentGame.summary, 280);
}
const metadata = {
id: currentGame.id.toString(),
name: currentGame.name,
shortDescription: deck,
description: currentGame.summary,
shortDescription,
description,
released,
genres,
@ -471,7 +499,7 @@ export class IGDBProvider implements MetadataProvider {
icon,
bannerId: banner,
coverId: icon,
coverId: coverID,
images,
};

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import { defineDropTask } from "..";
// import { defineDropTask } from "..";
/*
export default defineDropTask({

View File

@ -5,11 +5,13 @@ import { GiantBombProvider } from "../internal/metadata/giantbomb";
import { IGDBProvider } from "../internal/metadata/igdb";
import { ManualMetadataProvider } from "../internal/metadata/manual";
import { PCGamingWikiProvider } from "../internal/metadata/pcgamingwiki";
import { logger } from "~~/server/internal/logging";
import { logger } from "../internal/logging";
import { SteamProvider } from "../internal/metadata/steam";
export default defineNitroPlugin(async (_nitro) => {
const metadataProviders = [
GiantBombProvider,
SteamProvider,
PCGamingWikiProvider,
IGDBProvider,
];