From 8e31eafc06673ceb46410d88d038943142aa656a Mon Sep 17 00:00:00 2001 From: lobo Date: Wed, 30 Sep 2026 15:59:05 +0200 Subject: [PATCH] feat(import): read the exports of GOG Galaxy, Grouvee, HowLongToBeat and Playnite --- README.md | 7 ++ public/i18n/de.json | 10 ++- public/i18n/en.json | 10 ++- src/components/import/ImportInput.jsx | 13 ++-- src/pages/ImportPage.test.jsx | 20 ++++++ src/test/fixtures/import/galaxy.tsv | 5 ++ src/test/fixtures/import/grouvee.csv | 6 ++ src/test/fixtures/import/hltb.csv | 6 ++ src/test/fixtures/import/playnite.csv | 6 ++ src/utils/importFormats.js | 94 +++++++++++++++++++++++++++ src/utils/importFormats.test.js | 58 +++++++++++++++++ src/utils/imports.js | 52 ++++++++++----- src/utils/imports.test.js | 4 ++ src/utils/platforms.js | 24 +++++++ 14 files changed, 289 insertions(+), 26 deletions(-) create mode 100644 src/test/fixtures/import/galaxy.tsv create mode 100644 src/test/fixtures/import/grouvee.csv create mode 100644 src/test/fixtures/import/hltb.csv create mode 100644 src/test/fixtures/import/playnite.csv create mode 100644 src/utils/importFormats.js create mode 100644 src/utils/importFormats.test.js diff --git a/README.md b/README.md index 44ad382..f21a76a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,13 @@ To Be Played is split into three repositories: progress, and "Try again" for an import that stopped. The review shows the best match, other candidates and "search again", with the platform and status per line. Only sure matches are checked. Earlier imports can be undone. +- **Export files** in the same file field, told apart by their header (`src/utils/importFormats.js`): GOG Galaxy 2.0 + (the community script [GOG-Galaxy-Export-Script](https://github.com/AB1908/GOG-Galaxy-Export-Script), minutes), + Grouvee (Settings, "Export your collection", shelves as status, the wish list is left out), HowLongToBeat (its CSV + export, the lists as status, "Ignored" left out) and Playnite (the add-on "Library Exporter Advanced", completion + status, seconds). Store and platform names become IGDB abbreviations (`platformFromNames`); an unknown one stays + empty and the review preselects. **Unverified:** each format is built from the exporter's public source, not from + a real file; the test files in `src/test/fixtures/import` are made up. - **Platform everywhere**: each game shows its platform ("Steam" for Steam games). The favorite platform from the settings is preselected when a game is on it; otherwise a game on one platform gets that one, and a game on several waits for the user's pick. diff --git a/public/i18n/de.json b/public/i18n/de.json index 5c61f8f..901dd02 100644 --- a/public/i18n/de.json +++ b/public/i18n/de.json @@ -641,7 +641,7 @@ "TEXT_LABEL": "Spieltitel", "TEXT_PLACEHOLDER": "Ein Spiel pro Zeile", "CSV_LABEL": "CSV-Datei", - "CSV_HINT": "Erste Zeile mit Spaltennamen: title (oder name), platform, hours (oder minutes), status.", + "CSV_HINT": "Erste Zeile mit Spaltennamen: title (oder name), platform, hours (oder minutes), status. Exporte von GOG Galaxy, Grouvee, HowLongToBeat und Playnite werden direkt gelesen.", "CSV_NO_TITLE": "Die Datei hat keine Spalte title oder name.", "LINES": "{{count}} Zeilen", "TOO_MANY": "{{count}} Zeilen: höchstens {{max}} pro Import.", @@ -705,7 +705,13 @@ "FAILED": "Der Import ist nach {{count}} Versuchen stehen geblieben.", "RETRY": "Nochmal versuchen", "RETRY_FAILED": "Der Import konnte nicht neu gestartet werden.", - "ATTEMPT": "Versuch {{attempt}}: der letzte Versuch kam nicht weiter." + "ATTEMPT": "Versuch {{attempt}}: der letzte Versuch kam nicht weiter.", + "SOURCE_GALAXY": "GOG Galaxy", + "SOURCE_GROUVEE": "Grouvee", + "SOURCE_HLTB": "HowLongToBeat", + "SOURCE_PLAYNITE": "Playnite", + "SOURCE_BOOKMARKLET": "Store-Seite", + "FORMAT_READ": "Gelesen als: {{format}}" }, "UNSAVED_CHANGES": { "CONFIRM": "Du hast Änderungen, die nicht gespeichert sind. Seite verlassen und die Änderungen verwerfen?" diff --git a/public/i18n/en.json b/public/i18n/en.json index fe67518..69b5e59 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -641,7 +641,7 @@ "TEXT_LABEL": "Game titles", "TEXT_PLACEHOLDER": "One game per line", "CSV_LABEL": "CSV file", - "CSV_HINT": "First row with column names: title (or name), platform, hours (or minutes), status.", + "CSV_HINT": "First row with column names: title (or name), platform, hours (or minutes), status. Exports of GOG Galaxy, Grouvee, HowLongToBeat and Playnite are read as they are.", "CSV_NO_TITLE": "The file has no title or name column.", "LINES": "{{count}} lines", "TOO_MANY": "{{count}} lines: at most {{max}} per import.", @@ -705,7 +705,13 @@ "FAILED": "The import stopped after {{count}} tries.", "RETRY": "Try again", "RETRY_FAILED": "Could not start the import again.", - "ATTEMPT": "Try {{attempt}}: the last try made no progress." + "ATTEMPT": "Try {{attempt}}: the last try made no progress.", + "SOURCE_GALAXY": "GOG Galaxy", + "SOURCE_GROUVEE": "Grouvee", + "SOURCE_HLTB": "HowLongToBeat", + "SOURCE_PLAYNITE": "Playnite", + "SOURCE_BOOKMARKLET": "Store page", + "FORMAT_READ": "Read as: {{format}}" }, "UNSAVED_CHANGES": { "CONFIRM": "You have changes that are not saved. Leave this page and lose them?" diff --git a/src/components/import/ImportInput.jsx b/src/components/import/ImportInput.jsx index ac46c52..8567890 100644 --- a/src/components/import/ImportInput.jsx +++ b/src/components/import/ImportInput.jsx @@ -7,7 +7,8 @@ import {Card} from '@/components/ui/card'; import {Input} from '@/components/ui/input'; import {Textarea} from '@/components/ui/textarea'; import useStartImport from '../../hooks/useStartImport.js'; -import {parseCsv, parseTextList} from '../../utils/imports.js'; +import {readImportFile} from '../../utils/importFormats.js'; +import {parseTextList} from '../../utils/imports.js'; import {MAX_IMPORT_ROWS} from '../../utils/limits.js'; export default function ImportInput({onCreated}) { @@ -19,12 +20,13 @@ export default function ImportInput({onCreated}) { const rows = source === 'TEXT' ? parseTextList(text) : csv?.rows ?? []; const tooMany = rows.length > MAX_IMPORT_ROWS; - const readFile = async (file) => setCsv(file ? parseCsv(await file.text()) : null); + const readFile = async (file) => setCsv(file ? readImportFile(await file.text()) : null); const startImport = useStartImport(); const start = async () => { setBusy(true); - const id = await startImport(source, rows); + // An export names its format, so the history shows where the lines came from. + const id = await startImport(source === 'CSV' ? csv.format : source, rows); setBusy(false); if (id) onCreated(id); }; @@ -43,10 +45,13 @@ export default function ImportInput({onCreated}) { onChange={(e) => setText(e.target.value)}/> ) : (
- readFile(e.target.files?.[0])}/>

{t('IMPORT.CSV_HINT')}

{csv?.error &&

{t(csv.error)}

} + {csv?.rows && csv.format !== 'CSV' && ( +

{t('IMPORT.FORMAT_READ', {format: t(`IMPORT.SOURCE_${csv.format}`)})}

+ )} {csv?.rows?.length > 0 && (