From 99c7f196b280b238cac63dcb0335358ba22f01fb Mon Sep 17 00:00:00 2001 From: Jacques Yakoub Date: Tue, 13 Aug 2024 22:15:41 +0200 Subject: [PATCH] perf: migration to Sqlite3 (#513) * chore: put information into database (WIP) document database vs relational database * chore: put information into database (WIP) document database vs relational database * chore: put information into database (WIP) * chore: put information into database (WIP) * chore: drop /games sorting features Wasn't used at the end since we remove it from frontend long time ago * chore: put information into database (WIP) * chore: put information into database (WIP) * chore: put information into database (WIP) * chore: put information into database (WIP) * chore: put information into database (WIP) * chore: put information into database (WIP) * chore: put information into database (WIP) * chore: put information into database (WIP) * chore: put information into database (WIP) * chore: put information into database (WIP) * chore: put information into database (WIP) Bug in serie through * chore: put information into database (WIP) * chore: remove null values * chore: use information from database (WIP) * chore: use information from database (WIP) * chore: use information from database (WIP) * chore: use information from database (WIP) * chore: use information from database (WIP) * chore: use information from database (WIP) * chore: use information from database (WIP) * chore: use information from database (WIP) * chore: use information from database (WIP) * chore: bug fixes Hercule playlistId was empty string, not NULL / series_as_json was using the wrong table * chore: use information from database (WIP) * chore: bug fixes * chore: bug fixes * ci: script to automate job * chore: npm outdated --- .github/workflows/refreshJSONFiles.yml | 43 + .vscode/schemas/backlog.json | 41 - .vscode/schemas/games.json | 100 - .vscode/schemas/series.json | 30 - .vscode/schemas/tests.json | 67 - .vscode/settings.json | 26 - GamesPassionFR.db | Bin 0 -> 110592 bytes GamesPassionFR.sqbpro | 1 + README.md | 81 + games_to_sql.mjs | 115 + generate-json-schema.cjs | 51 - generateJsonFiles.mjs | 218 + messages/en.json | 40 +- messages/fr.json | 40 +- package-lock.json | 538 +- package.json | 9 +- src/app/[locale]/backlog/page.tsx | 17 +- .../games/_client/GamesGalleryGrid.tsx | 23 +- src/app/[locale]/planning/page.tsx | 7 +- src/app/[locale]/stats/page.tsx | 54 +- src/app/api/backlog/backlog.json | 1035 ++-- src/app/api/backlog/route.ts | 8 +- src/app/api/games/games.json | 4322 ++++++++--------- src/app/api/games/route.ts | 116 +- src/app/api/genres/genres.json | 82 + src/app/api/genres/route.ts | 20 + src/app/api/planning/planning.json | 232 + src/app/api/planning/route.ts | 24 +- src/app/api/platforms/platforms.json | 26 + src/app/api/platforms/route.ts | 20 + src/app/api/series/route.ts | 73 +- src/app/api/series/series.json | 1120 ++++- src/app/api/stats/route.ts | 134 +- src/app/api/stats/stats.json | 209 + src/app/api/tests/route.ts | 12 +- src/app/api/tests/tests.json | 145 +- src/components/GamesView/GenresSelect.tsx | 27 +- src/components/GamesView/PlatformIcons.tsx | 45 +- src/components/GamesView/PlatformSelect.tsx | 45 +- src/redux/Store.tsx | 8 +- src/redux/features/gamesSlice.tsx | 33 +- src/redux/services/gamesAPI.tsx | 35 +- src/redux/services/genresAPI.tsx | 18 + src/redux/services/planningAPI.tsx | 10 +- src/redux/services/platformsAPI.tsx | 18 + src/redux/services/seriesAPI.tsx | 10 +- src/redux/sharedDefintion.tsx | 50 +- 47 files changed, 5234 insertions(+), 4144 deletions(-) create mode 100644 .github/workflows/refreshJSONFiles.yml delete mode 100644 .vscode/schemas/backlog.json delete mode 100644 .vscode/schemas/games.json delete mode 100644 .vscode/schemas/series.json delete mode 100644 .vscode/schemas/tests.json create mode 100644 GamesPassionFR.db create mode 100644 GamesPassionFR.sqbpro create mode 100644 games_to_sql.mjs delete mode 100644 generate-json-schema.cjs create mode 100644 generateJsonFiles.mjs create mode 100644 src/app/api/genres/genres.json create mode 100644 src/app/api/genres/route.ts create mode 100644 src/app/api/planning/planning.json create mode 100644 src/app/api/platforms/platforms.json create mode 100644 src/app/api/platforms/route.ts create mode 100644 src/app/api/stats/stats.json create mode 100644 src/redux/services/genresAPI.tsx create mode 100644 src/redux/services/platformsAPI.tsx diff --git a/.github/workflows/refreshJSONFiles.yml b/.github/workflows/refreshJSONFiles.yml new file mode 100644 index 00000000..dd8e1aa6 --- /dev/null +++ b/.github/workflows/refreshJSONFiles.yml @@ -0,0 +1,43 @@ +name: Refresh API JSON files + +on: + schedule: + # Each day at 5:00PM + - cron: '0 17 * * *' + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + updater: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: 🛎️ Checkout + uses: actions/checkout@v4 + - name: Setup Node.js ✨ + uses: actions/setup-node@v4.0.2 + with: + node-version: "lts/*" + - name: 💻 Install npm packages + run: npm install better-sqlite3 --no-save + - name: 🤖 Do the thing + run: npm run generate-api-json-files + - name: 💅 Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + title: "[auto-updater] Updated JSON files" + body: | + This is an automated update of JSON files based on the latest data in the database. + + Updated files: + - backlog.json + - games.json + - series.json + - tests.json + - platforms.json + - genres.json + - planning.json + - stats.json + draft: false \ No newline at end of file diff --git a/.vscode/schemas/backlog.json b/.vscode/schemas/backlog.json deleted file mode 100644 index 4a94f2fd..00000000 --- a/.vscode/schemas/backlog.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/RawPayload", - "definitions": { - "RawPayload": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Name of the game" - }, - "platform": { - "$ref": "#/definitions/Platform", - "description": "Platform for that game" - }, - "notes": { - "type": "string", - "description": "Extra notes" - } - }, - "required": [ - "title" - ], - "additionalProperties": false - } - }, - "Platform": { - "type": "string", - "enum": [ - "PC", - "GBA", - "PSP", - "PS1", - "PS2", - "PS3" - ] - } - } -} \ No newline at end of file diff --git a/.vscode/schemas/games.json b/.vscode/schemas/games.json deleted file mode 100644 index fdf5a858..00000000 --- a/.vscode/schemas/games.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/RawPayload", - "definitions": { - "RawPayload": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Title of the game, such as \"Beyond Good & Evil\"" - }, - "platform": { - "$ref": "#/definitions/Platform", - "description": "Platform for that game" - }, - "duration": { - "type": "string", - "description": "Duration of the walkthrough (e.g. \"01:42:13\")" - }, - "genres": { - "type": "array", - "items": { - "$ref": "#/definitions/Genre" - }, - "description": "Genres of the game" - }, - "releaseDate": { - "type": "string", - "description": "When the game was released, such \"01/09/2005\"" - }, - "availableAt": { - "type": "number", - "description": "When to display the game public, such as 20210412 (12/04/2021)" - }, - "endAt": { - "type": "number", - "description": "When to display the game public, such as 20210420 (20/04/2021)" - }, - "coverFile": { - "type": "string", - "description": "Name of the main cover file, such as \"cover.webp\"" - }, - "videoId": { - "type": "string", - "description": "Video ID from Youtube - what you see after \"watch?v=\"" - }, - "playlistId": { - "type": "string", - "description": "Playlist ID from Youtube, what you see after \"playlist?list=\"" - } - }, - "required": [ - "title", - "platform", - "genres", - "releaseDate" - ], - "additionalProperties": false - } - }, - "Platform": { - "type": "string", - "enum": [ - "PC", - "GBA", - "PSP", - "PS1", - "PS2", - "PS3" - ] - }, - "Genre": { - "type": "string", - "enum": [ - "Action", - "Adventure", - "Arcade", - "Board Games", - "Card", - "Casual", - "Educational", - "Family", - "Fighting", - "Indie", - "MMORPG", - "Platformer", - "Puzzle", - "RPG", - "Racing", - "Shooter", - "Simulation", - "Sports", - "Strategy", - "Misc" - ] - } - } -} \ No newline at end of file diff --git a/.vscode/schemas/series.json b/.vscode/schemas/series.json deleted file mode 100644 index 550f9659..00000000 --- a/.vscode/schemas/series.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/RawPayload", - "definitions": { - "RawPayload": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the series" - }, - "games": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of videoId or playlistId for this series" - } - }, - "required": [ - "name", - "games" - ], - "additionalProperties": false - } - } - } -} \ No newline at end of file diff --git a/.vscode/schemas/tests.json b/.vscode/schemas/tests.json deleted file mode 100644 index 896161c1..00000000 --- a/.vscode/schemas/tests.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/RawPayload", - "definitions": { - "RawPayload": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Title of the game, such as \"Beyond Good & Evil\"" - }, - "platform": { - "$ref": "#/definitions/Platform", - "description": "Platform for that game" - }, - "duration": { - "type": "string", - "description": "Duration of the walkthrough (e.g. \"01:42:13\")" - }, - "releaseDate": { - "type": "string", - "description": "When the game was released, such \"01/09/2005\"" - }, - "availableAt": { - "type": "number", - "description": "When to display the game public, such as 20210412 (12/04/2021)" - }, - "endAt": { - "type": "number", - "description": "When to display the game public, such as 20210420 (20/04/2021)" - }, - "coverFile": { - "type": "string", - "description": "Name of the main cover file, such as \"cover.webp\"" - }, - "videoId": { - "type": "string", - "description": "Video ID from Youtube - what you see after \"watch?v=\"" - }, - "playlistId": { - "type": "string", - "description": "Playlist ID from Youtube, what you see after \"playlist?list=\"" - } - }, - "required": [ - "title", - "platform", - "releaseDate" - ], - "additionalProperties": false - } - }, - "Platform": { - "type": "string", - "enum": [ - "PC", - "GBA", - "PSP", - "PS1", - "PS2", - "PS3" - ] - } - } -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 309e4f24..63746894 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,30 +1,4 @@ { - "json.schemas": [ - { - "fileMatch": [ - "**/backlog.json" - ], - "url": "./.vscode/schemas/backlog.json" - }, - { - "fileMatch": [ - "**/games.json" - ], - "url": "./.vscode/schemas/games.json" - }, - { - "fileMatch": [ - "**/series.json" - ], - "url": "./.vscode/schemas/series.json" - }, - { - "fileMatch": [ - "**/tests.json" - ], - "url": "./.vscode/schemas/tests.json" - }, - ], "i18n-ally.localesPaths": [ "messages" ] diff --git a/GamesPassionFR.db b/GamesPassionFR.db new file mode 100644 index 0000000000000000000000000000000000000000..2dfbe767225aeb7dbcfd93699a8a677701e02a83 GIT binary patch literal 110592 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
diff --git a/README.md b/README.md index 4085700b..6585cf0b 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,84 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. + +## Database + +There is a [database](GamesPassionFR.db) for managing video games, their genres, platforms, series, schedules, and associated video content. The [schema](GamesPassionFR.sqbpro) consists of several interrelated tables: games, backlog, games_genres, games_schedules, genres, platforms, series, series_games, and tests. + +### `games` Table + +| Field | Description | Required/Optional | Example | +|--------------|------------------------------------------------------|-------------------|----------------------| +| `id` | Unique identifier for each game (Primary Key). | Required | `1` | +| `videoId` | Unique identifier for an associated video. | Optional | `IOog6QKck6o` | +| `playlistId` | Unique identifier for an associated playlist. | Optional | `PLRfhDHeBTBJ7zlVdm21oc8ndiRGxQJy6F` | +| `title` | The title of the game. | Required | `Harry Potter 3` | +| `releaseDate`| The release date of the game. | Required | `2023-01-01` | +| `duration` | The duration of the game or video content. | Optional | `02:30:00` | +| `platform` | Foreign key referencing the `platforms` table. | Required | `2` | + +### `backlog` Table + +| Field | Description | Required/Optional | Example | +|-----------|-----------------------------------------------------------|-------------------|----------------------| +| `id` | Unique identifier for each backlog entry (Primary Key). | Required | `1` | +| `title` | The title of the game. | Required | `Batman Begins` | +| `platform`| Foreign key referencing the `platforms` table. | Optional | `2` | +| `notes` | Additional notes or comments about the game. | Optional | `Play this first.` | + +### `games_genres` Table + +| Field | Description | Required/Optional | Example | +|---------|-------------------------------------------------------|-------------------|----------------------| +| `game` | Foreign key referencing the `games` table. | Required | `1` | +| `genre` | Foreign key referencing the `genres` table. | Required | `3` | + +### `games_schedules` Table + +| Field | Description | Required/Optional | Example | +|--------------|------------------------------------------------------|-------------------|----------------------| +| `id` | Foreign key referencing the `games` table. | Required | `1` | +| `availableAt`| Date (and time) when the game becomes available. | Optional | `2023-08-01 10:00:00`| +| `endAt` | Date (and time) when the game is no longer available.| Optional | `2023-12-31 23:59:59`| + +### `genres` Table + +| Field | Description | Required/Optional | Example | +|----------|-------------------------------------------------|-------------------|----------------------| +| `id` | Unique identifier for each genre (Primary Key). | Required | `3` | +| `name` | The name of the genre. | Required | `Action` | + +### `platforms` Table + +| Field | Description | Required/Optional | Example | +|----------|----------------------------------------------------|-------------------|----------------------| +| `id` | Unique identifier for each platform (Primary Key). | Required | `2` | +| `name` | The name of the platform. | Required | `PlayStation` | + +## `series` Table + +| Field | Description | Required/Optional | Example | +|----------|--------------------------------------------------|-------------------|----------------------| +| `id` | Unique identifier for each series (Primary Key). | Required | `1` | +| `name` | The name of the series. | Required | `Ratchet & Clank` | + +## `series_games` Table + +| Field | Description | Required/Optional | Example | +|---------|-------------------------------------------------------|-------------------|----------------------| +| `serie` | Foreign key referencing the `series` table. | Required | `1` | +| `game` | Foreign key referencing the `games` table. | Required | `2` | +| `order` | The order of the game within the series. | Optional | `1` | + +## `tests` Table + +| Field | Description | Required/Optional | Example | +|--------------|-------------------------------------------------------|-------------------|----------------------| +| `id` | Unique identifier for each test (Primary Key). | Required | `1` | +| `videoId` | Unique identifier for an associated video. | Optional | `IOog6QKck6o` | +| `playlistId` | Unique identifier for an associated playlist. | Optional | `PLRfhDHeBTBJ7zlVdm21oc8ndiRGxQJy6F` | +| `title` | The title of the game. | Required | `Harry Potter 3` | +| `releaseDate`| The release date of the test. | Required | `2023-01-01` | +| `platform` | Foreign key referencing the `platforms` table. | Required | `2` | +| `duration` | The duration of the test or video content. | Optional | `00:30:00` | \ No newline at end of file diff --git a/games_to_sql.mjs b/games_to_sql.mjs new file mode 100644 index 00000000..a413f120 --- /dev/null +++ b/games_to_sql.mjs @@ -0,0 +1,115 @@ +import { readFile, writeFile } from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// Resolve the path to the JSON file relative to the current file's directory +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const filePath = path.join(__dirname, 'games_to_insert.json'); + +// Mappings for platform and genre names to their respective IDs +const platformMapping = { + "PC": 1, + "GBA": 2, + "PSP": 3, + "PS1": 4, + "PS2": 5, + "PS3": 6 +}; + +const genreMapping = { + "Action": 1, + "Adventure": 2, + "Arcade": 3, + "Board Games": 4, + "Card": 5, + "Casual": 6, + "Educational": 7, + "Family": 8, + "Fighting": 9, + "Indie": 10, + "MMORPG": 11, + "Platformer": 12, + "Puzzle": 13, + "RPG": 14, + "Racing": 15, + "Shooter": 16, + "Simulation": 17, + "Sports": 18, + "Strategy": 19, + "Misc": 20 +}; + +// Helper function to convert releaseDate to SQLite format YYYY-MM-DD +function convertDate(dateStr) { + const [day, month, year] = dateStr.split('/').map(Number); + const date = new Date(year, month - 1, day); + const yyyy = date.getFullYear(); + const mm = (date.getMonth() + 1).toString().padStart(2, '0'); + const dd = date.getDate().toString().padStart(2, '0'); + return `${yyyy}-${mm}-${dd}`; +} + +// Main function to generate and save SQL script +async function generateSqlScript() { + try { + // Load the JSON content + const data = JSON.parse(await readFile(filePath, 'utf-8')); + + // Generating SQL INSERT statements with comments and explanations + const sqlWithComments = []; + + data.forEach((game, index) => { + const gameId = "__YOUR_ID_"; // Assuming this is the auto-increment ID + const title = game.title; + const videoId = game.videoId || null; + const playlistId = game.playlistId || null; + const releaseDate = convertDate(game.releaseDate); + const duration = game.duration || null; + const platformId = platformMapping[game.platform] || null; + + // Insert into games table with comments + sqlWithComments.push( + `-- Insert data for the game '${title}'\n` + + `INSERT INTO games (videoId, playlistId, title, releaseDate, duration, platform) ` + + `VALUES ("${videoId}", "${playlistId}", "${title}", "${releaseDate}", "${duration}", ${platformId}); RETURNING id\n` + ); + + // Insert into games_genres table with comments + game.genres.forEach((genre) => { + const genreId = genreMapping[genre] || null; + if (genreId !== null) { + sqlWithComments.push( + `-- Associate the game '${title}' with the genre '${genre}'\n` + + `INSERT INTO games_genres (game, genre) VALUES (${gameId}, ${genreId});\n` + ); + } + }); + + // Insert into games_schedules table with comments (if available) + const availableAt = game.availableAt; + const endAt = game.endAt || null; + if (availableAt) { + sqlWithComments.push( + `-- Set the availability for the game '${title}'\n` + + `INSERT INTO games_schedules (id, availableAt, endAt) ` + + `VALUES (${gameId}, "${availableAt}", "${endAt}");\n` + ); + } + }); + + // Combine all SQL statements with comments + const finalSqlScript = sqlWithComments.join('\n'); + + // Save the final SQL script to a file + const outputFilePath = path.join(__dirname, 'games_insert_statements.sql'); + await writeFile(outputFilePath, finalSqlScript, 'utf-8'); + + console.log(`SQL script has been saved to ${outputFilePath}`); + } catch (error) { + console.error('Error generating SQL script:', error); + } +} + +// Execute the main function +generateSqlScript(); diff --git a/generate-json-schema.cjs b/generate-json-schema.cjs deleted file mode 100644 index 7a509ccc..00000000 --- a/generate-json-schema.cjs +++ /dev/null @@ -1,51 +0,0 @@ -const tsj = require("ts-json-schema-generator"); -const fs = require("fs"); - -// json file -const files = [ - // backlog.json - { - filename: "backlog.json", - path: "src/app/api/backlog/route.ts", - type: "RawPayload" - }, - // games.json - { - filename: "games.json", - path: "src/app/api/games/route.ts", - type: "RawPayload" - }, - // series.json - { - filename: "series.json", - path: "src/app/api/series/route.ts", - type: "RawPayload" - }, - // tests.json - { - filename: "tests.json", - path: "src/app/api/tests/route.ts", - type: "RawPayload" - }, -] - -// Iterate and generate file -for (let file of files) { - - const { filename, ...rest } = file; - - /** @type {import('ts-json-schema-generator/dist/src/Config').Config} */ - let config = { - ...rest, - tsconfig: "tsconfig.json" - }; - - let outputPath = `.vscode/schemas/${filename}`; - - let schema = tsj.createGenerator(config).createSchema(config.type); - let schemaString = JSON.stringify(schema, null, 2); - fs.writeFile(outputPath, schemaString, (err) => { - if (err) throw err; - }); - -} \ No newline at end of file diff --git a/generateJsonFiles.mjs b/generateJsonFiles.mjs new file mode 100644 index 00000000..30e9ad6d --- /dev/null +++ b/generateJsonFiles.mjs @@ -0,0 +1,218 @@ +import { writeFile } from "fs/promises"; +import Database from 'better-sqlite3'; + +// Params +const databasePath = 'GamesPassionFR.db'; +const FILES = { + "BACKLOG": "src/app/api/backlog/backlog.json", + "GAMES": "src/app/api/games/games.json", + "SERIES": "src/app/api/series/series.json", + "TESTS": "src/app/api/tests/tests.json", + "PLATFORMS": "src/app/api/platforms/platforms.json", + "GENRES": "src/app/api/genres/genres.json", + "PLANNING": "src/app/api/planning/planning.json", + "STATS": "src/app/api/stats/stats.json" +} + +const db = new Database(databasePath, { + readonly: true +}); + +//db.pragma('journal_mode = WAL'); + +// Helper Functions +function stringifyJSON(payload) { + + function parseIfJsonString(value) { + if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } + } + return value; + } + + return JSON.stringify(payload, function(key, value) { + if (value === null) { + // Exclude null values + return undefined; + } + return parseIfJsonString(value); + }, "\t"); +} +function normaliazeDuration(duration) { + + // Turn it into seconds + let totalInSeconds = [ + duration.hours * 3600, + duration.minutes * 60, + duration.seconds + ].reduce( (acc, total) => acc + total, 0); + + // Time to normalize the result + let new_hours = Math.floor(totalInSeconds / 3600); + totalInSeconds %= 3600; + let new_minutes = Math.floor(totalInSeconds / 60); + let new_seconds = totalInSeconds % 60; + + return { + hours: new_hours, + minutes: new_minutes, + seconds : new_seconds + } + +} + + +/** + * Extracts platforms from the database and saves them to a file. + * @param {import('better-sqlite3').Database} db - The database instance + */ +async function extractAndSavePlatforms(db) { + const extractPlatformsStmt = db.prepare("SELECT id, name FROM platforms"); + const platforms = extractPlatformsStmt.all(); + await writeFile( + FILES.PLATFORMS, + stringifyJSON(platforms), + "utf-8" + ); + console.log(`${FILES.PLATFORMS} successfully written`); +} + +/** + * Extracts genres from the database and saves them to a file. + * @param {import('better-sqlite3').Database} db - The database instance + */ +async function extractAndSaveGenres(db) { + const extractGenresStmt = db.prepare("SELECT id, name FROM genres"); + const genres = extractGenresStmt.all(); + await writeFile( + FILES.GENRES, + stringifyJSON(genres), + "utf-8" + ); + console.log(`${FILES.GENRES} successfully written`); +} + +/** + * Extracts backlog from the database and saves them to a file. + * @param {import('better-sqlite3').Database} db - The database instance + */ +async function extractAndSaveBacklog(db) { + const extractBacklogStmt = db.prepare("SELECT id, title, platform, notes FROM backlog"); + const backlog = extractBacklogStmt.all(); + await writeFile( + FILES.BACKLOG, + stringifyJSON(backlog), + "utf-8" + ); + console.log(`${FILES.BACKLOG} successfully written`); +} + +/** + * Extracts planning from the database and saves them to a file. + * @param {import('better-sqlite3').Database} db - The database instance + */ +async function extractAndSavePlanning(db) { + const extractPlanningStmt = db.prepare("SELECT * FROM games_in_future gif INNER JOIN games g ON g.id == gif.id"); + const planning = extractPlanningStmt.all(); + await writeFile( + FILES.PLANNING, + stringifyJSON(planning), + "utf-8" + ); + console.log(`${FILES.PLANNING} successfully written`); +} + +/** + * Extracts games from the database and saves them to a file. + * @param {import('better-sqlite3').Database} db - The database instance + */ +async function extractAndSaveGames(db) { + const extractGamesList = db.prepare("SELECT * FROM games_in_present"); + const gamesList = extractGamesList.all(); + await writeFile( + FILES.GAMES, + stringifyJSON(gamesList), + "utf-8" + ); + console.log(`${FILES.GAMES} successfully written`); +} + +/** + * Extracts series from the database and saves them to a file. + * @param {import('better-sqlite3').Database} db - The database instance + */ +async function extractAndSaveSeries(db) { + const extractSeriesStmt = db.prepare("SELECT * FROM series_as_json"); + const series = extractSeriesStmt.all(); + await writeFile( + FILES.SERIES, + stringifyJSON(series), + "utf-8" + ); + console.log(`${FILES.SERIES} successfully written`); +} + +/** + * Extracts tests from the database and saves them to a file. + * @param {import('better-sqlite3').Database} db - The database instance + */ +async function extractAndSaveTests(db) { + const extractTestsStmt = db.prepare("SELECT title, videoId, playlistId, platform FROM tests"); + const tests = extractTestsStmt.all(); + await writeFile( + FILES.TESTS, + stringifyJSON(tests), + "utf-8" + ); + console.log(`${FILES.TESTS} successfully written`); +} + +/** + * Extracts stats from the database and saves them to a file. + * @param {import('better-sqlite3').Database} db - The database instance + */ +async function extractAndSaveStats(db) { + const genresStats = db.prepare("SELECT * FROM genres_stats").all(); + const platformStats = db.prepare("SELECT * FROM platforms_stats").all(); + const games_total_time = db.prepare("SELECT * FROM games_total_time").get(); + const games_total_time_available = db.prepare("SELECT * FROM games_available_time").get(); + const games_total_time_unavailable = db.prepare("SELECT * FROM games_unavailable_time").get(); + const total_games = db.prepare("SELECT COUNT(*) FROM games").pluck().get(); + const total_game_available = db.prepare("SELECT COUNT(*) FROM games_in_present").pluck().get(); + const total_game_unavailable = db.prepare("SELECT COUNT(*) FROM games_in_future").pluck().get(); + + const result = { + "platforms": platformStats, + "genres": genresStats, + "general": { + "total": total_games, + "total_available": total_game_available, + "total_unavailable": total_game_unavailable, + "channel_start_date": "2014-04-15T17:35:16+00:00", + "total_time": normaliazeDuration(games_total_time), + "total_time_available": normaliazeDuration(games_total_time_available), + "total_time_unavailable": normaliazeDuration(games_total_time_unavailable) + } + } + + await writeFile( + FILES.STATS, + stringifyJSON(result), + "utf-8" + ); + console.log(`${FILES.STATS} successfully written`); +} + +// Operations time +await extractAndSavePlatforms(db) +await extractAndSaveGenres(db) +await extractAndSaveBacklog(db) +await extractAndSavePlanning(db) +await extractAndSaveGames(db) +await extractAndSaveSeries(db) +await extractAndSaveTests(db) +await extractAndSaveStats(db) \ No newline at end of file diff --git a/messages/en.json b/messages/en.json index a214fa7d..67ed80c6 100644 --- a/messages/en.json +++ b/messages/en.json @@ -74,26 +74,26 @@ "title": "Title" }, "gamesGenres": { - "Action": "Action", - "Adventure": "Adventure", - "Arcade": "Arcade", - "Board Games": "Board Games", - "Card": "Card", - "Casual": "Casual", - "Educational": "Educational", - "Family": "Family", - "Fighting": "Fighting", - "Indie": "Indie", - "MMORPG": "MMORPG", - "Platformer": "Platformer", - "Puzzle": "Puzzle", - "RPG": "RPG", - "Racing": "Racing", - "Shooter": "Shooter", - "Simulation": "Simulation", - "Sports": "Sports", - "Strategy": "Strategy", - "Misc": "Misc" + "1": "Action", + "2": "Adventure", + "3": "Arcade", + "4": "Board Games", + "5": "Card", + "6": "Casual", + "7": "Educational", + "8": "Family", + "9": "Fighting", + "10": "Indie", + "11": "MMORPG", + "12": "Platformer", + "13": "Puzzle", + "14": "RPG", + "15": "Racing", + "16": "Shooter", + "17": "Simulation", + "18": "Sports", + "19": "Strategy", + "20": "Misc" } }, "planning": { diff --git a/messages/fr.json b/messages/fr.json index 06316b6d..fdeb2195 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -74,26 +74,26 @@ "title": "Titre" }, "gamesGenres": { - "Action": "Action", - "Adventure": "Aventure", - "Arcade": "Arcade", - "Board Games": "Jeux de société", - "Card": "Cartes", - "Casual": "Casual", - "Educational": "Éducatif", - "Family": "Familial", - "Fighting": "Combat", - "Indie": "Indépendant", - "MMORPG": "MMORPG", - "Platformer": "Plate-formes", - "Puzzle": "Réflexion", - "RPG": "RPG", - "Racing": "Jeu de course", - "Shooter": "Jeu de tir", - "Simulation": "Simulation", - "Sports": "Sports", - "Strategy": "Stratégie", - "Misc": "Autre" + "1": "Action", + "2": "Aventure", + "3": "Arcade", + "4": "Jeux de société", + "5": "Cartes", + "6": "Casual", + "7": "Éducatif", + "8": "Familial", + "9": "Combat", + "10": "Indépendant", + "11": "MMORPG", + "12": "Plate-formes", + "13": "Réflexion", + "14": "RPG", + "15": "Jeu de course", + "16": "Jeu de tir", + "17": "Simulation", + "18": "Sports", + "19": "Stratégie", + "20": "Autre" } }, "planning": { diff --git a/package-lock.json b/package-lock.json index bd30b0a4..3a151cc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,9 +34,9 @@ "typescript": "5.5.4" }, "devDependencies": { - "@inquirer/input": "^2.2.9", - "googleapis": "^140.0.1", - "ts-json-schema-generator": "^2.3.0" + "@inquirer/input": "^2.2.8", + "better-sqlite3": "^11.1.2", + "googleapis": "^140.0.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -920,6 +920,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.0.10.tgz", "integrity": "sha512-TdESOKSVwf6+YWDz8GhS6nKscwzkIyakEzCLJ5Vh6O3Co2ClhCJ0A4MG909MUWfaWdpJm7DE45ii51/2Kat9tA==", "dev": true, + "license": "MIT", "dependencies": { "@inquirer/figures": "^1.0.5", "@inquirer/type": "^1.5.2", @@ -943,13 +944,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@inquirer/core/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -964,6 +967,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -978,6 +982,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.5.tgz", "integrity": "sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -987,6 +992,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.2.9.tgz", "integrity": "sha512-7Z6N+uzkWM7+xsE+3rJdhdG/+mQgejOVqspoW+w0AbSZnL6nq5tGMEVASaYVWbkoSzecABWwmludO2evU3d31g==", "dev": true, + "license": "MIT", "dependencies": { "@inquirer/core": "^9.0.10", "@inquirer/type": "^1.5.2" @@ -1000,6 +1006,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.2.tgz", "integrity": "sha512-w9qFkumYDCNyDZmNQjf/n6qQuvQ4dMC3BJesY4oF+yr0CxR5vxujflAVeIcS6U336uzi9GM0kAfZlLrZ9UTkpA==", "dev": true, + "license": "MIT", "dependencies": { "mute-stream": "^1.0.0" }, @@ -1052,15 +1059,17 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.6.tgz", - "integrity": "sha512-ceNGjoXheH9wbIFa1JHmSc9QVjJUvh18KvHrR4/FkJCSi9HXJ+9ee1kUhCOEFfuxNF8UB6WWVrIUOUgRd70t0A==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", + "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -1086,6 +1095,7 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.16.7", @@ -1187,6 +1197,7 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.16.6", @@ -1268,6 +1279,7 @@ "version": "7.12.1", "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.12.1.tgz", "integrity": "sha512-/l9AslZKoHFfOultD2ehWIxRGuBI0RUwbhVbpAPwNP3ouDbTH0eyWfd5f6KCGhcUbG6xfceiOuTNr7uaPHqKiA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", "@mui/system": "^5.16.5", @@ -1521,6 +1533,7 @@ "version": "2.2.7", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.7.tgz", "integrity": "sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==", + "license": "MIT", "dependencies": { "immer": "^10.0.3", "redux": "^5.0.1", @@ -1618,12 +1631,6 @@ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1634,6 +1641,7 @@ "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -1642,6 +1650,7 @@ "version": "22.2.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.2.0.tgz", "integrity": "sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==", + "license": "MIT", "dependencies": { "undici-types": "~6.13.0" } @@ -1691,7 +1700,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@typescript-eslint/parser": { "version": "7.2.0", @@ -1925,6 +1935,7 @@ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -1940,6 +1951,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -2215,6 +2227,18 @@ ], "license": "MIT" }, + "node_modules/better-sqlite3": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.1.2.tgz", + "integrity": "sha512-gujtFwavWU4MSPT+h9B+4pkvZdyOUkH54zgLdIrMmmmd4ZqiBIrRNBzNzYVFO417xo882uP5HBu4GjOfaSrIQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, "node_modules/bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", @@ -2225,6 +2249,28 @@ "node": "*" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2245,6 +2291,31 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2323,11 +2394,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, "node_modules/cli-spinners": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -2340,6 +2419,7 @@ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, + "license": "ISC", "engines": { "node": ">= 12" } @@ -2394,15 +2474,6 @@ "simple-swizzle": "^0.2.2" } }, - "node_modules/commander": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", - "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", - "dev": true, - "engines": { - "node": ">=18" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2630,6 +2701,32 @@ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2742,6 +2839,16 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.16.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", @@ -3316,6 +3423,16 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3391,6 +3508,13 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3463,6 +3587,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3585,6 +3716,13 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, "node_modules/glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", @@ -3894,6 +4032,27 @@ "node": ">= 14" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -3948,6 +4107,13 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -4411,18 +4577,6 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4574,6 +4728,19 @@ "node": ">=8.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4601,6 +4768,13 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4611,6 +4785,7 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "dev": true, + "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -4632,6 +4807,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true, + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4714,6 +4896,19 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/node-abi": { + "version": "3.65.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz", + "integrity": "sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -4735,15 +4930,6 @@ } } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/notistack": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz", @@ -5072,6 +5258,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5095,6 +5308,17 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5138,6 +5362,32 @@ } ] }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -5237,6 +5487,21 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/recharts": { "version": "2.12.7", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", @@ -5482,15 +5747,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -5646,6 +5902,53 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -5691,6 +5994,16 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5921,6 +6234,36 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5968,74 +6311,6 @@ "typescript": ">=4.2.0" } }, - "node_modules/ts-json-schema-generator": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-2.3.0.tgz", - "integrity": "sha512-t4lBQAwZc0sOJq9LJt3NgbznIcslVnm0JeEMFq8qIRklpMRY8jlYD0YmnRWbqBKANxkby91P1XanSSlSOFpUmg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15", - "commander": "^12.0.0", - "glob": "^10.3.12", - "json5": "^2.2.3", - "normalize-path": "^3.0.0", - "safe-stable-stringify": "^2.4.3", - "tslib": "^2.6.2", - "typescript": "^5.4.5" - }, - "bin": { - "ts-json-schema-generator": "bin/ts-json-schema-generator.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/ts-json-schema-generator/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/ts-json-schema-generator/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ts-json-schema-generator/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -6063,6 +6338,19 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6220,6 +6508,13 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", @@ -6483,6 +6778,7 @@ "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, diff --git a/package.json b/package.json index 00c2b78d..8ce03cba 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "generate-json-schemas": "node generate-json-schema.cjs", "generate-playlist-data-report": "node generate-playlists-stats.mjs", "generate-playlist-csv": "node generate-playlist-csv.mjs", - "find-playlist-ids": "node findPlaylists.mjs" + "find-playlist-ids": "node findPlaylists.mjs", + "generate-api-json-files": "node generateJsonFiles.mjs" }, "dependencies": { "@emotion/react": "^11.13.0", @@ -41,8 +42,8 @@ "typescript": "5.5.4" }, "devDependencies": { - "@inquirer/input": "^2.2.9", - "googleapis": "^140.0.1", - "ts-json-schema-generator": "^2.3.0" + "@inquirer/input": "^2.2.8", + "better-sqlite3": "^11.1.2", + "googleapis": "^140.0.1" } } diff --git a/src/app/[locale]/backlog/page.tsx b/src/app/[locale]/backlog/page.tsx index c0b52dfc..9c0d7c4d 100644 --- a/src/app/[locale]/backlog/page.tsx +++ b/src/app/[locale]/backlog/page.tsx @@ -12,12 +12,7 @@ import { DataGrid, GridToolbar } from '@mui/x-data-grid'; import type { GridRenderCellParams, GridColDef } from '@mui/x-data-grid'; // Platform icons -import iconsSVG from "@/components/GamesView/PlatformIcons"; -import type { Platform } from '@/redux/sharedDefintion'; - -// Icons -import SvgIcon from '@mui/material/SvgIcon'; -import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; +import RenderPlatformIcon from "@/components/GamesView/PlatformIcons"; // Others import Tooltip from '@mui/material/Tooltip'; @@ -51,15 +46,7 @@ export default function BacklogViewer() { field: "platform", headerName: t("columns.platform"), renderCell: (params: GridRenderCellParams) => { - if (params.value !== undefined) { - return ( - - {iconsSVG[params.value as Platform]} - - ); - } else { - return ; - } + return () }, width: 160 }, diff --git a/src/app/[locale]/games/_client/GamesGalleryGrid.tsx b/src/app/[locale]/games/_client/GamesGalleryGrid.tsx index 9dbac334..3c20ff6d 100644 --- a/src/app/[locale]/games/_client/GamesGalleryGrid.tsx +++ b/src/app/[locale]/games/_client/GamesGalleryGrid.tsx @@ -15,21 +15,19 @@ import CardEntry from "@/components/GamesView/CardEntry"; import GamesFilters from "./GamesFilters"; // Types -import type { EnhancedGame } from "@/redux/sharedDefintion"; -import { gamesSorters, gamesFilters } from '@/redux/features/gamesSlice'; +import type { gamesFilters } from '@/redux/features/gamesSlice'; +import type { CardGame } from "@/redux/sharedDefintion"; // To force reset of page when filters / sorters criteria changes type InnerProps = { activeFilters: gamesFilters, - activeSorters: gamesSorters, } // To help react detect change in filters / sorters -function generateKey({activeFilters, activeSorters} : InnerProps) : string { +function generateKey({activeFilters} : InnerProps) : string { return Object .entries({ - activeFilters, - activeSorters + activeFilters }) .toString(); } @@ -41,25 +39,19 @@ export default function GamesGalleryGrid() { (state) => state.games.activeFilters ); - // Active sorters - const activeSorters = useAppSelector( - (state) => state.games.sorters - ); - return (

) } // To force reset of page when filters / sorters criteria changes -function GamesGalleryGridInner({ activeFilters, activeSorters } : InnerProps) { +function GamesGalleryGridInner({ activeFilters } : InnerProps) { // Current page const [page, setPage] = useState(1); @@ -71,13 +63,12 @@ function GamesGalleryGridInner({ activeFilters, activeSorters } : InnerProps) { const LIMIT_PAGE = 16; const { data, isFetching } = useGetGamesQuery({ filters: activeFilters, - sorters: activeSorters, pageSize : LIMIT_PAGE, page: page }); // render row - const renderRow = (game : EnhancedGame) => + const renderRow = (game : CardGame) => ( - - {iconsSVG[params.value as Platform]} - + ), width: 160 }, diff --git a/src/app/[locale]/stats/page.tsx b/src/app/[locale]/stats/page.tsx index 45ec4eaa..80156405 100644 --- a/src/app/[locale]/stats/page.tsx +++ b/src/app/[locale]/stats/page.tsx @@ -44,34 +44,6 @@ import { Legend, } from "recharts"; -// type -import type { Genre as GenreValue } from "@/redux/sharedDefintion"; - -// type -type SortableStat = { - total: number; - total_available: number; - total_unavailable: number; -} - -// To sort stats according their -function sortStats(a : SortableStat, b: SortableStat) : number { - // Sort by total first - if (a.total > b.total) return -1; - if (a.total < b.total) return 1; - - // Then sort by total available - if (a.total_available > b.total_available) return -1; - if (a.total_available < b.total_available) return 1; - - // Then sort by total not yet available - if (a.total_unavailable > b.total_unavailable) return -1; - if (a.total_unavailable < b.total_unavailable) return 1; - - // Default case - return 0; -} - export default function StatsPage() { const t = useTranslations(); const currentColor = useAppSelector((state) => state.themeColor.currentColor); @@ -113,26 +85,22 @@ export default function StatsPage() { } // for genre chart - const genresData = Object - .entries(data.genres) - .map( ([key, value]) => ({ - key: key, - category: t(`gamesLibrary.gamesGenres.${key as GenreValue}`), - ...value - })) - .sort(sortStats); + const genresData = data.genres.map(genre => ({ + key: genre.id, + category: t(`gamesLibrary.gamesGenres.${genre.id}` as any), + ...genre + })) // StackedAreaChart : https://recharts.org/en-US/examples/StackedAreaChart // StackedBarChart : https://recharts.org/en-US/examples/StackedBarChart // for platform chart - const platformsData = Object - .entries(data.platforms) - .map( ([key, value]) => ({ - key: key, - ...value - })) - .sort(sortStats); + const platformsData = + data.platforms + .map(platform => ({ + key: platform.platform, + ...platform + })); // for generals line const generalStats = data.general; diff --git a/src/app/api/backlog/backlog.json b/src/app/api/backlog/backlog.json index 4e2d5ca1..7e486c02 100644 --- a/src/app/api/backlog/backlog.json +++ b/src/app/api/backlog/backlog.json @@ -1,459 +1,578 @@ [ - { - "title": "Beyond: Two Souls", - "platform": "PC" - }, - { - "title": "Super Meat Boy" - }, - { - "title": "Manhunt" - }, - { - "title": "Manhunt 2" - }, - { - "title": "Dead Head Fred", - "platform": "PSP" - }, - { - "title": "Ty, le tigre de Tasmanie", - "notes": "Original (PS2) VS remake (PC) ?" - }, - { - "title": "Ty : Le Tigre de Tasmanie 2 : Opération Sauvetage", - "notes": "Original (PS2) VS remake (PC) ?" - }, - { - "title": "Ty, le tigre de Tasmanie 3 : Nuit du Quinkan", - "notes": "Original (PS2) VS remake (PC) ?" - }, - { - "title": "Ty, le tigre de Tasmanie 4 : Le retour de Opération Sauvetage", - "notes": "Original (PS2) VS remake (PC) ?" - }, - { - "title": "Musashi: Samurai Legend", - "platform": "PS2" - }, - { - "title": "Shadow of the Colossus", - "platform": "PS3" - }, - { - "title": "Okami HD", - "platform": "PS3" - }, - { - "title": "Oni", - "platform": "PS3" - }, - { - "title": "Heavy Rain", - "platform": "PS3" - }, - { - "title": "Crash Twinsanity", - "platform": "PS2" - }, - { - "title": "Crash of the Titans", - "platform": "PS2" - }, - { - "title": "Viewtiful Joe", - "platform": "PS2" - }, - { - "title": "Cold Fear", - "platform": "PS2" - }, - { - "title": "The Suffering : Les Liens qui nous Unissent", - "platform": "PS2" - }, - { - "title": "BloodRayne : Terminal Cut", - "platform": "PC" - }, - { - "title": "BloodRayne 2 : Terminal Cut", - "platform": "PC" - }, - { - "title": "Swords of Destiny", - "platform": "PS3" - }, - { - "title": "Burnout Paradise Remastered" - }, - { - "title": "Trine", - "platform": "PS3" - }, - { - "title": "Trine 2", - "platform": "PS3" - }, - { - "title": "Trine 3 : The Artifacts of Power", - "platform": "PC" - }, - { - "title": "Trine 4 : Melody of Mystery", - "platform": "PC" - }, - { - "title": "Trine 5", - "platform": "PC" - }, - { - "title": "Serious Sam HD: The First Encounter", - "platform": "PC" - }, - { - "title": "Serious Sam HD: The Second Encounter", - "platform": "PC" - }, - { - "title": "Serious Sam 3: BFE", - "platform": "PC" - }, - { - "title": "Serious Sam 4", - "platform": "PC" - }, - { - "title": "God Hand", - "platform": "PS3" - }, - { - "title": "Dead Rising", - "platform": "PC" - }, - { - "title": "Dead Rising 2", - "platform": "PS3" - }, - { - "title": "Dead Rising 3", - "platform": "PS3" - }, - { - "title": "Dead Rising 4", - "platform": "PC" - }, - { - "title": "Psi-Ops : The Mindgate Conspiracy", - "platform": "PS2" - }, - { - "title": "Black", - "platform": "PS2" - }, - { - "title": "Disgaea : Afternoon of Darkness", - "platform": "PSP" - }, - { - "title": "Disgaea 2 : Dark Hero Days", - "platform": "PSP" - }, - { - "title": "TimeSplitters : Future Perfect", - "platform": "PS2" - }, - { - "title": "Kane & Lynch : Dead Men", - "platform": "PS3" - }, - { - "title": "Mark of the Ninja", - "platform": "PC" - }, - { - "title": "The Chronicles of Riddick : Assault on Dark Athena", - "platform": "PS3" - }, - { - "title": "Shadow Complex Remastered", - "platform": "PC" - }, - { - "title": "Kung Fu Panda : Le jeu", - "platform": "PS3" - }, - { - "title": "Shantae Advance : Risky Revolution", - "platform": "PC" - }, - { - "title": "Shantae and the Pirate's Curse", - "platform": "PC" - }, - { - "title": "Shantae and the Seven Sirens", - "platform": "PC" - }, - { - "title": "Shantae : Half-Genie Hero", - "platform": "PC" - }, - { - "title": "Shantae : Risky's Revenge", - "platform": "PC" - }, - { - "title": "Syndicate", - "platform": "PS3" - }, - { - "title": "Wolfenstein", - "platform": "PS3" - }, - { - "title": "Wolfenstein : The New Order", - "platform": "PS3" - }, - { - "title": "Wolfenstein II : The New Colossus", - "platform": "PC" - }, - { - "title": "Rage", - "platform": "PS3" - }, - { - "title": "Driver 76", - "platform": "PSP" - }, - { - "title": "Binary Domain", - "platform": "PS3" - }, - { - "title": "The House of the Dead : Overkill - Extended Cut", - "platform": "PS3" - }, - { - "title": "Dark Sector", - "platform": "PS3" - }, - { - "title": "Scarface : The World is Yours", - "platform": "PC", - "notes": "Scarface Remastered Project 1.1" - }, - { - "title": "Pizza Tower", - "platform": "PC" - }, - { - "title": "Stick It to The Man!", - "platform": "PS3" - }, - { - "title": "Child of Eden", - "platform": "PS3" - }, - { - "title": "Persona 5 Royal", - "platform": "PC" - }, - { - "title": "Yakuza 1 HD", - "platform": "PS3", - "notes": "Yakuza 1 & 2 HD Edition" - }, - { - "title": "Yakuza 2 HD", - "platform": "PS3", - "notes": "Yakuza 1 & 2 HD Edition" - }, - { - "title": "Yakuza 3", - "platform": "PC", - "notes": "The Yakuza Remastered Collection" - }, - { - "title": "Yakuza 4", - "platform": "PC", - "notes": "The Yakuza Remastered Collection" - }, - { - "title": "Yakuza 5", - "platform": "PC", - "notes": "The Yakuza Remastered Collection" - }, - { - "title": "Yakuza 6 : The Song of Life", - "platform": "PC" - }, - { - "title": "Yakuza : Like a Dragon", - "platform": "PC" - }, - { - "title": "Yakuza Zero : The Place of Oath", - "platform": "PS3" - }, - { - "title": "50 Cent - Bulletproof", - "platform": "PS2" - }, - { - "title": "True Crime - New York City", - "platform": "PS2" - }, - { - "title": "Batman Begins", - "platform": "PS2" - }, - { - "title": "Dead to Rights II", - "platform": "PS2" - }, - { - "title": "Shovel Knight", - "platform": "PS3" - }, - { - "title": "Kingdoms of amalur : reckoning", - "platform": "PS3" - }, - { - "title": "The sopranos road to respect", - "platform": "PS2" - }, - { - "title": "Ratatouille", - "platform": "PS3" - }, - { - "title": "State of Emergency", - "platform": "PS2" - }, - { - "title": "Forbidden siren", - "platform": "PS2" - }, - { - "title": "Enchanted Arms", - "platform": "PS3" - }, - { - "title": "The 3rd Birthday", - "platform": "PSP" - }, - { - "title": "Vanquish" - }, - { - "title": "Halo 2", - "platform": "PC" - }, - { - "title": "Halo 4", - "platform": "PC" - }, - { - "title": "Halo Reach", - "platform": "PC" - }, - { - "title": "Grand Theft Auto III" - }, - { - "title": "Grand Theft Auto IV" - }, - { - "title": "Grand Theft Auto V" - }, - { - "title": "Grand Theft Auto : San Andreas" - }, - { - "title": "Grand Theft Auto : Vice City" - }, - { - "title": "Grand Theft Auto : Liberty City Stories" - }, - { - "title": "BioShock", - "platform": "PC", - "notes": "Bioshock : The Collection" - }, - { - "title": "BioShock 2", - "platform": "PC", - "notes": "Bioshock : The Collection" - }, - { - "title": "BioShock Infinite", - "platform": "PC", - "notes": "Bioshock : The Collection" - }, - { - "title": "Dead Space", - "notes": "Dead Space (2023)" - }, - { - "title": "Dead Space 2" - }, - { - "title": "Shadow Of The Tomb Raider", - "platform": "PC" - }, - { - "title": "Tomb Raider: Anniversary" - }, - { - "title": "Tomb Raider", - "platform": "PS3", - "notes": "2013" - }, - { - "title": "Rise Of The Tomb Raider", - "platform": "PC" - }, - { - "title": "Gangs of London", - "platform": "PSP" - }, - { - "title": "Spartan : Total Warrior", - "platform": "PS2" - }, - { - "title": "Crisis Zone", - "platform": "PS2" - }, - { - "title": "Dead Island", - "platform": "PS3" - }, - { - "title": "Blacksite", - "platform": "PS3" - }, - { - "title": "The Club", - "platform": "PS3" - }, - { - "title": "Bionic Commando", - "platform": "PS3" - }, - { - "title": "Rogue Warrior", - "platform": "PS3" - }, - { - "title": "The Darkness", - "platform": "PS3" - }, - { - "title": "Bionicle Heroes", - "notes": "PC vs PS2 ?" - } -] + { + "id": 1, + "title": "Beyond: Two Souls", + "platform": 1 + }, + { + "id": 2, + "title": "Super Meat Boy" + }, + { + "id": 3, + "title": "Manhunt" + }, + { + "id": 4, + "title": "Manhunt 2" + }, + { + "id": 5, + "title": "Dead Head Fred", + "platform": 3 + }, + { + "id": 6, + "title": "Ty, le tigre de Tasmanie", + "platform": 5, + "notes": "Original (PS2) VS remake (PC) ?" + }, + { + "id": 7, + "title": "Ty : Le Tigre de Tasmanie 2 : Opération Sauvetage", + "platform": 5, + "notes": "Original (PS2) VS remake (PC) ?" + }, + { + "id": 8, + "title": "Ty, le tigre de Tasmanie 3 : Nuit du Quinkan", + "platform": 5, + "notes": "Original (PS2) VS remake (PC) ?" + }, + { + "id": 9, + "title": "Ty, le tigre de Tasmanie 4 : Le retour de Opération Sauvetage", + "platform": 5, + "notes": "Original (PS2) VS remake (PC) ?" + }, + { + "id": 10, + "title": "Musashi: Samurai Legend", + "platform": 5 + }, + { + "id": 11, + "title": "Shadow of the Colossus", + "platform": 6 + }, + { + "id": 12, + "title": "Okami HD", + "platform": 6 + }, + { + "id": 13, + "title": "Oni", + "platform": 6 + }, + { + "id": 14, + "title": "Heavy Rain", + "platform": 6 + }, + { + "id": 15, + "title": "Crash Twinsanity", + "platform": 5 + }, + { + "id": 16, + "title": "Crash of the Titans", + "platform": 5 + }, + { + "id": 17, + "title": "Viewtiful Joe", + "platform": 5 + }, + { + "id": 18, + "title": "Cold Fear", + "platform": 5 + }, + { + "id": 19, + "title": "The Suffering : Les Liens qui nous Unissent", + "platform": 5 + }, + { + "id": 20, + "title": "BloodRayne : Terminal Cut", + "platform": 1 + }, + { + "id": 21, + "title": "BloodRayne 2 : Terminal Cut", + "platform": 1 + }, + { + "id": 22, + "title": "Swords of Destiny", + "platform": 6 + }, + { + "id": 23, + "title": "Burnout Paradise Remastered" + }, + { + "id": 24, + "title": "Trine", + "platform": 6 + }, + { + "id": 25, + "title": "Trine 2", + "platform": 6 + }, + { + "id": 26, + "title": "Trine 3 : The Artifacts of Power", + "platform": 1 + }, + { + "id": 27, + "title": "Trine 4 : Melody of Mystery", + "platform": 1 + }, + { + "id": 28, + "title": "Trine 5", + "platform": 1 + }, + { + "id": 29, + "title": "Serious Sam HD: The First Encounter", + "platform": 1 + }, + { + "id": 30, + "title": "Serious Sam HD: The Second Encounter", + "platform": 1 + }, + { + "id": 31, + "title": "Serious Sam 3: BFE", + "platform": 1 + }, + { + "id": 32, + "title": "Serious Sam 4", + "platform": 1 + }, + { + "id": 33, + "title": "God Hand", + "platform": 6 + }, + { + "id": 34, + "title": "Dead Rising", + "platform": 1 + }, + { + "id": 35, + "title": "Dead Rising 2", + "platform": 6 + }, + { + "id": 36, + "title": "Dead Rising 3", + "platform": 6 + }, + { + "id": 37, + "title": "Dead Rising 4", + "platform": 1 + }, + { + "id": 38, + "title": "Psi-Ops : The Mindgate Conspiracy", + "platform": 5 + }, + { + "id": 39, + "title": "Black", + "platform": 5 + }, + { + "id": 40, + "title": "Disgaea : Afternoon of Darkness", + "platform": 3 + }, + { + "id": 41, + "title": "Disgaea 2 : Dark Hero Days", + "platform": 3 + }, + { + "id": 42, + "title": "TimeSplitters : Future Perfect", + "platform": 5 + }, + { + "id": 43, + "title": "Kane & Lynch : Dead Men", + "platform": 6 + }, + { + "id": 44, + "title": "Mark of the Ninja", + "platform": 1 + }, + { + "id": 45, + "title": "The Chronicles of Riddick : Assault on Dark Athena", + "platform": 6 + }, + { + "id": 46, + "title": "Shadow Complex Remastered", + "platform": 1 + }, + { + "id": 47, + "title": "Kung Fu Panda : Le jeu", + "platform": 6 + }, + { + "id": 48, + "title": "Shantae Advance : Risky Revolution", + "platform": 1 + }, + { + "id": 49, + "title": "Shantae and the Pirate's Curse", + "platform": 1 + }, + { + "id": 50, + "title": "Shantae and the Seven Sirens", + "platform": 1 + }, + { + "id": 51, + "title": "Shantae : Half-Genie Hero", + "platform": 1 + }, + { + "id": 52, + "title": "Shantae : Risky's Revenge", + "platform": 1 + }, + { + "id": 53, + "title": "Syndicate", + "platform": 6 + }, + { + "id": 54, + "title": "Wolfenstein", + "platform": 6 + }, + { + "id": 55, + "title": "Wolfenstein : The New Order", + "platform": 6 + }, + { + "id": 56, + "title": "Wolfenstein II : The New Colossus", + "platform": 1 + }, + { + "id": 57, + "title": "Rage", + "platform": 6 + }, + { + "id": 58, + "title": "Driver 76", + "platform": 3 + }, + { + "id": 59, + "title": "Binary Domain", + "platform": 6 + }, + { + "id": 60, + "title": "The House of the Dead : Overkill - Extended Cut", + "platform": 6 + }, + { + "id": 61, + "title": "Dark Sector", + "platform": 6 + }, + { + "id": 62, + "title": "Scarface : The World is Yours", + "platform": 1, + "notes": "Scarface Remastered Project 1.1" + }, + { + "id": 63, + "title": "Pizza Tower", + "platform": 1 + }, + { + "id": 64, + "title": "Stick It to The Man!", + "platform": 6 + }, + { + "id": 65, + "title": "Child of Eden", + "platform": 6 + }, + { + "id": 66, + "title": "Persona 5 Royal", + "platform": 1 + }, + { + "id": 67, + "title": "Yakuza 1 HD", + "platform": 6, + "notes": "Yakuza 1 & 2 HD Edition" + }, + { + "id": 68, + "title": "Yakuza 2 HD", + "platform": 6, + "notes": "Yakuza 1 & 2 HD Edition" + }, + { + "id": 69, + "title": "Yakuza 3", + "platform": 1, + "notes": "The Yakuza Remastered Collection" + }, + { + "id": 70, + "title": "Yakuza 4", + "platform": 1, + "notes": "The Yakuza Remastered Collection" + }, + { + "id": 71, + "title": "Yakuza 5", + "platform": 1, + "notes": "The Yakuza Remastered Collection" + }, + { + "id": 72, + "title": "Yakuza 6 : The Song of Life", + "platform": 1 + }, + { + "id": 73, + "title": "Yakuza : Like a Dragon", + "platform": 1 + }, + { + "id": 74, + "title": "Yakuza Zero : The Place of Oath", + "platform": 6 + }, + { + "id": 75, + "title": "50 Cent - Bulletproof", + "platform": 5 + }, + { + "id": 76, + "title": "True Crime - New York City", + "platform": 5 + }, + { + "id": 77, + "title": "Batman Begins", + "platform": 5 + }, + { + "id": 78, + "title": "Dead to Rights II", + "platform": 5 + }, + { + "id": 79, + "title": "Shovel Knight", + "platform": 6 + }, + { + "id": 80, + "title": "Kingdoms of amalur : reckoning", + "platform": 6 + }, + { + "id": 81, + "title": "The sopranos road to respect", + "platform": 5 + }, + { + "id": 82, + "title": "Ratatouille", + "platform": 6 + }, + { + "id": 83, + "title": "State of Emergency", + "platform": 5 + }, + { + "id": 84, + "title": "Forbidden siren", + "platform": 5 + }, + { + "id": 85, + "title": "Enchanted Arms", + "platform": 6 + }, + { + "id": 86, + "title": "The 3rd Birthday", + "platform": 3 + }, + { + "id": 87, + "title": "Vanquish" + }, + { + "id": 88, + "title": "Halo 2", + "platform": 1 + }, + { + "id": 89, + "title": "Halo 4", + "platform": 1 + }, + { + "id": 90, + "title": "Halo Reach", + "platform": 1 + }, + { + "id": 91, + "title": "Grand Theft Auto III" + }, + { + "id": 92, + "title": "Grand Theft Auto IV" + }, + { + "id": 93, + "title": "Grand Theft Auto V" + }, + { + "id": 94, + "title": "Grand Theft Auto : San Andreas" + }, + { + "id": 95, + "title": "Grand Theft Auto : Vice City" + }, + { + "id": 96, + "title": "Grand Theft Auto : Liberty City Stories" + }, + { + "id": 97, + "title": "BioShock", + "platform": 1, + "notes": "Bioshock : The Collection" + }, + { + "id": 98, + "title": "BioShock 2", + "platform": 1, + "notes": "Bioshock : The Collection" + }, + { + "id": 99, + "title": "BioShock Infinite", + "platform": 1, + "notes": "Bioshock : The Collection" + }, + { + "id": 100, + "title": "Dead Space", + "notes": "Dead Space (2023)" + }, + { + "id": 101, + "title": "Dead Space 2" + }, + { + "id": 102, + "title": "Shadow Of The Tomb Raider", + "platform": 1 + }, + { + "id": 103, + "title": "Tomb Raider: Anniversary" + }, + { + "id": 104, + "title": "Tomb Raider", + "platform": 6, + "notes": "2013" + }, + { + "id": 105, + "title": "Rise Of The Tomb Raider", + "platform": 1 + }, + { + "id": 106, + "title": "Gangs of London", + "platform": 3 + }, + { + "id": 107, + "title": "Spartan : Total Warrior", + "platform": 5 + }, + { + "id": 108, + "title": "Crisis Zone", + "platform": 5 + }, + { + "id": 109, + "title": "Dead Island", + "platform": 6 + }, + { + "id": 110, + "title": "Blacksite", + "platform": 6 + }, + { + "id": 111, + "title": "The Club", + "platform": 6 + }, + { + "id": 112, + "title": "Bionic Commando", + "platform": 6 + }, + { + "id": 113, + "title": "Rogue Warrior", + "platform": 6 + }, + { + "id": 114, + "title": "The Darkness", + "platform": 6 + }, + { + "id": 115, + "title": "Bionicle Heroes", + "notes": "PC vs PS2 ?" + } +] \ No newline at end of file diff --git a/src/app/api/backlog/route.ts b/src/app/api/backlog/route.ts index 0ff6997e..3408173e 100644 --- a/src/app/api/backlog/route.ts +++ b/src/app/api/backlog/route.ts @@ -1,9 +1,5 @@ import { NextResponse } from "next/server"; -import type { - Platform -} from "@/redux/sharedDefintion"; - // An entry of backlog export type BacklogEntry = { /**@description Id */ @@ -11,7 +7,7 @@ export type BacklogEntry = { /** @description Name of the game */ "title": string, /** @description Platform for that game */ - "platform"?: Platform, + "platform"?: number, /** @description Extra notes */ "notes"?: string } @@ -24,7 +20,7 @@ export async function GET() { // Game data const gamesData = (await import("./backlog.json")).default; - const games = gamesData.map( (game, idx) => enhanceGameItem(game as RawBacklogEntry, idx) ); + const games = gamesData.map( (game, idx) => enhanceGameItem(game, idx) ); return NextResponse.json(games, { headers: { diff --git a/src/app/api/games/games.json b/src/app/api/games/games.json index 3256b71c..5e4b36da 100644 --- a/src/app/api/games/games.json +++ b/src/app/api/games/games.json @@ -1,2310 +1,2014 @@ [ - { - "title":"Mafia: Definitive Edition", - "playlistId":"PLRfhDHeBTBJ6SEXdQnTM4OHRH9mDIRocv", - "releaseDate":"25/09/2020", - "duration":"6:59:14", - "genres":[ - "Action", - "Adventure", - "Shooter" - ], - "platform":"PC" - }, - { - "title":"Samurai Jack: Battle Through Time", - "playlistId":"PLRfhDHeBTBJ5PoDOIMqIEoWuC-woSa4iC", - "releaseDate":"21/08/2020", - "duration":"3:51:35", - "genres":[ - "Action", - "Adventure", - "Fighting" - ], - "platform":"PC" - }, - { - "title":"Destroy All Humans! - Remake", - "playlistId":"PLRfhDHeBTBJ76V2inJIW484ZQZZyKKejr", - "releaseDate":"28/07/2020", - "duration":"4:25:44", - "genres":[ - "Action", - "Shooter" - ], - "platform":"PC" - }, - { - "title":"Batman: Arkham Origins", - "playlistId":"PLRfhDHeBTBJ56EHMyvxX6iBKldqvxJHqz", - "releaseDate":"25/10/2013", - "duration":"6:48:06", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC" - }, - { - "title":"Guns Gore & Cannoli", - "playlistId":"PLRfhDHeBTBJ6oiHK7lfTbOQlMWVHecx2I", - "releaseDate":"30/04/2015", - "duration":"2:10:05", - "genres":[ - "Action", - "Shooter", - "Arcade", - "Indie" - ], - "platform":"PC" - }, - { - "title":"Batman: Arkham Asylum", - "playlistId":"PLRfhDHeBTBJ4FHOgX8bYRe8alwgNKIpYv", - "releaseDate":"28/08/2009", - "duration":"5:59:51", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC" - }, - { - "title":"Batman: Arkham Knight", - "playlistId":"PLRfhDHeBTBJ6POjMkqqeCliq5jrCWagcg", - "releaseDate":"23/06/2015", - "duration":"8:33:35", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC" - }, - { - "title":"Batman Arkham City", - "playlistId":"PLRfhDHeBTBJ6RBqsJ0WYOOaL5wohZTbxw", - "releaseDate":"20/10/2011", - "duration":"6:47:41", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC" - }, - { - "title":"Splinter Cell Blacklist", - "playlistId":"PLRfhDHeBTBJ7JOqVieOgK3RrNvDRMC5cV", - "releaseDate":"22/08/2013", - "duration":"6:34:25", - "genres":[ - "Action" - ], - "platform":"PC" - }, - { - "title":"Prince of Persia : Les Sables du temps", - "playlistId":"PLRfhDHeBTBJ53K1nOu8IEXqZltsLtffGO", - "releaseDate":"13/11/2003", - "duration":"5:01:34", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS3" - }, - { - "title":"Prince of Persia : Les Deux Royaumes", - "playlistId":"PLRfhDHeBTBJ6ptBn3Rkq74O50hly24Gqg", - "releaseDate":"01/12/2005", - "duration":"4:39:35", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS3" - }, - { - "title":"God Of War II HD", - "playlistId":"PLRfhDHeBTBJ6zui2RaBzaZ5zjm768Jzum", - "releaseDate":"03/11/2010", - "duration":"6:20:04", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3" - }, - { - "title":"Prince of Persia Epilogue", - "playlistId":"PLRfhDHeBTBJ4hCgXVdIqLpBkTKS9q1JN6", - "releaseDate":"05/03/2009", - "duration":"1:24:13", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS3" - }, - { - "title":"Def Jam: Fight for NY", - "playlistId":"PLRfhDHeBTBJ63YC9KZywNsOoLCgX7EXsO", - "releaseDate":"30/09/2004", - "duration":"4:19:35", - "genres":[ - "Fighting" - ], - "platform":"PS2" - }, - { - "title":"Destroy All Humans! 2", - "playlistId":"PLRfhDHeBTBJ7aGxJBcsSJXKe1l_sRAMPy", - "releaseDate":"27/10/2006", - "duration":"11:22:25", - "genres":[ - "Adventure" - ], - "platform":"PS2" - }, - { - "title":"Da Vinci Code", - "playlistId":"PLRfhDHeBTBJ4LD3P49nfFel3sLPMw7Kl5", - "releaseDate":"19/05/2006", - "duration":"5:49:40", - "genres":[ - "Action", - "Adventure", - "Puzzle" - ], - "platform":"PS2" - }, - { - "title":"Astérix & Obélix XXL", - "playlistId":"PLRfhDHeBTBJ7hPe8RxhK3FTXoqdFNL0BG", - "releaseDate":"21/11/2003", - "duration":"6:21:55", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS2" - }, - { - "title":"Astérix & Obélix XXL 2", - "playlistId":"PLRfhDHeBTBJ6FTJ2LSrdL4MaFM1Pl5nfo", - "releaseDate":"29/11/2018", - "duration":"4:53:56", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC" - }, - { - "title":"Shrek 2 Le Jeu", - "playlistId":"PLRfhDHeBTBJ53mxvj3I9ZwNfn38vQSI3j", - "releaseDate":"18/06/2004", - "duration":"2:24:35", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC" - }, - { - "title":"Sam Pyjam : Héros de la Nuit", - "videoId":"qaIADIQyr0A", - "releaseDate":"04/10/1996", - "duration":"44:11", - "genres":[ - "Adventure", - "Educational" - ], - "platform":"PC" - }, - { - "title":"Pyjama Sam 2 : Héros Météo", - "videoId":"We7-JHU7D7I", - "releaseDate":"02/10/1998", - "duration":"53:45", - "genres":[ - "Adventure", - "Educational" - ], - "platform":"PC" - }, - { - "title":"Pyjama Sam : Héros du Goûter", - "videoId":"Qsq_q65MwhU", - "releaseDate":"22/11/2000", - "duration":"1:07:54", - "genres":[ - "Adventure", - "Educational" - ], - "platform":"PC" - }, - { - "title":"Remember Me", - "playlistId":"PLRfhDHeBTBJ4AFRA0rUuICqeb7QJf8-jO", - "releaseDate":"07/06/2013", - "duration":"6:41:04", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS3" - }, - { - "title":"Prince of Persia : L'Ame du guerrier", - "playlistId":"PLRfhDHeBTBJ5FBZQLKtgH6QsC536I4ggr", - "releaseDate":"02/12/2004", - "duration":"5:52:06", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS3" - }, - { - "title":"Mafia II", - "playlistId":"PLRfhDHeBTBJ5KzX5CFJUfNs_Y8_Xi-dhg", - "releaseDate":"27/08/2010", - "duration":"7:56:28", - "genres":[ - "Action", - "Adventure", - "Shooter" - ], - "platform":"PS3" - }, - { - "title":"Le Parrain : Edition du Don", - "playlistId":"PLRfhDHeBTBJ4OEBaFpx7GHRdCN7tVYhLO", - "releaseDate":"23/03/2007", - "duration":"15:05:17", - "genres":[ - "Action", - "Adventure", - "Shooter" - ], - "platform":"PS3" - }, - { - "title":"Mafia 1", - "playlistId":"PLRfhDHeBTBJ65md00lEyYxa_K8qf8mPbP", - "releaseDate":"13/02/2004", - "duration":"9:00:04", - "genres":[ - "Action", - "Adventure", - "Shooter" - ], - "platform":"PS2" - }, - { - "title":"TMNT 2007 (GBA)", - "playlistId":"PLRfhDHeBTBJ511Df7N0KGWuGY0O6nlmNU", - "releaseDate":"29/03/2007", - "duration":"1:10:46", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"GBA" - }, - { - "title":"Dishonored", - "playlistId":"PLRfhDHeBTBJ7aXxD52b-jsEk7WzKzsdy0", - "releaseDate":"09/10/2012", - "duration":"5:25:12", - "genres":[ - "Action", - "Adventure", - "Shooter", - "RPG" - ], - "platform":"PS3" - }, - { - "title":"Les indestructibles Le Jeu", - "playlistId":"PLRfhDHeBTBJ6kWfGJp4xw769cajM4K9Gi", - "releaseDate":"12/11/2004", - "duration":"3:42:26", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC" - }, - { - "title":"Batman: Rise of Sin Tzu", - "playlistId":"PLRfhDHeBTBJ4BQrJ2mIgTHLpgPVR2XsRb", - "releaseDate":"20/11/2003", - "duration":"4:25:24", - "genres":[ - "Action" - ], - "platform":"PS2" - }, - { - "title":"Secret Agent Clank", - "playlistId":"PLRfhDHeBTBJ70A5fWB1qpafjMvJ2w1IxY", - "releaseDate":"11/07/2008", - "duration":"6:22:48", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PSP" - }, - { - "title":"Ratchet & Clank : La Taille, Ca Compte", - "playlistId":"PLRfhDHeBTBJ5J7kgKwUQGOnru8DZhKi6s", - "releaseDate":"16/05/2007", - "duration":"4:05:03", - "genres":[ - "Action", - "Adventure", - "Platformer", - "Shooter" - ], - "platform":"PSP" - }, - { - "title":"Sonic Heroes", - "playlistId":"PLRfhDHeBTBJ5bAuo9zjPUGnXfzu9Py5Nx", - "releaseDate":"06/02/2004", - "duration":"2:55:04", - "genres":[ - "Arcade", - "Platformer", - "Racing" - ], - "platform":"PS2" - }, - { - "title":"Ratchet & Clank 3", - "playlistId":"PLRfhDHeBTBJ7MKXXCaweIjEpKNA-qtQvu", - "releaseDate":"17/11/2004", - "duration":"6:55:09", - "genres":[ - "Action", - "Adventure", - "Platformer", - "Shooter" - ], - "platform":"PS3" - }, - { - "title":"Les Indestructibles : La Terrible Attaque du Démolisseur", - "playlistId":"PLRfhDHeBTBJ44T1V46UXupwH6GPekErCH", - "releaseDate":"18/11/2005", - "duration":"2:11:24", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC" - }, - { - "title":"Ratchet & Clank 2", - "playlistId":"PLRfhDHeBTBJ5R8zWiWBkY-DBMU2AFb9mu", - "releaseDate":"19/11/2003", - "duration":"7:48:33", - "genres":[ - "Action", - "Adventure", - "Platformer", - "Shooter" - ], - "platform":"PS3" - }, - { - "title":"Ratchet & Clank", - "playlistId":"PLRfhDHeBTBJ56NplVu4F1-uQm5bq7jwvo", - "releaseDate":"08/11/2002", - "duration":"7:03:58", - "genres":[ - "Action", - "Adventure", - "Platformer", - "Shooter" - ], - "platform":"PS3" - }, - { - "title":"Ratchet : Gladiator", - "playlistId":"PLRfhDHeBTBJ7AoUspxANPSVOxoKEC0hRA", - "releaseDate":"23/11/2005", - "duration":"6:07:43", - "genres":[ - "Action", - "Adventure", - "Platformer", - "Shooter" - ], - "platform":"PS3" - }, - { - "title":"God of War Ascension", - "playlistId":"PLRfhDHeBTBJ7oa6cAMnUL_Qec1nkK6Oi6", - "releaseDate":"13/03/2013", - "duration":"6:20:52", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3" - }, - { - "title":"God of War III", - "playlistId":"PLRfhDHeBTBJ7FqF_Z2hOQZE4JP7CmvSdO", - "releaseDate":"17/03/2010", - "duration":"6:46:19", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3" - }, - { - "title":"Darksiders II", - "playlistId":"PLRfhDHeBTBJ6Pp3ukQM6-F8LYtOmXmy9X", - "releaseDate":"21/08/2012", - "duration":"14:35:48", - "genres":[ - "Action", - "Adventure", - "RPG" - ], - "platform":"PS3" - }, - { - "title":"Guns Gore and Cannoli 2", - "playlistId":"PLRfhDHeBTBJ4ivg1J2leFzeuABXobVFNO", - "releaseDate":"03/03/2018", - "duration":"2:31:53", - "genres":[ - "Action", - "Shooter", - "Arcade", - "Indie" - ], - "platform":"PC" - }, - { - "title":"Bully : Scholarship Edition", - "playlistId":"PLRfhDHeBTBJ4l1G59ZVCUuTuZ-btLpPnm", - "releaseDate":"07/03/2008", - "duration":"11:38:17", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC" - }, - { - "title":"Max Payne 2 - The Fall of Max Payne", - "playlistId":"PLRfhDHeBTBJ4ZHSeesVGZle5F3OOA6ERf", - "releaseDate":"24/10/2003", - "duration":"3:35:54", - "genres":[ - "Action", - "Shooter" - ], - "platform":"PC" - }, - { - "title":"Max Payne I", - "playlistId":"PLRfhDHeBTBJ5LaRTqHGdoOWrQM8Uck35J", - "releaseDate":"01/08/2001", - "duration":"4:10:57", - "genres":[ - "Action", - "Shooter" - ], - "platform":"PC" - }, - { - "title":"Pokémon Uranium", - "playlistId":"PLRfhDHeBTBJ6nITizlIWkG5aiqeF8iJeh", - "releaseDate":"06/08/2016", - "duration":"19:13:50", - "genres":[ - "Adventure", - "RPG" - ], - "platform":"PC" - }, - { - "title":"Kirby: Cauchemar au pays des Rêves", - "playlistId":"PLRfhDHeBTBJ5g1jcrLLDl5Azwux73Edsi", - "releaseDate":"26/09/2003", - "duration":"1:38:08", - "genres":[ - "Platformer" - ], - "platform":"GBA" - }, - { - "title":"Splinter Cell", - "playlistId":"PLRfhDHeBTBJ6hq_3hyZpacsJt6ZUJPTTN", - "releaseDate":"28/11/2002", - "duration":"4:34:58", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC" - }, - { - "title":"Darksiders I", - "playlistId":"PLRfhDHeBTBJ61QLZkvjtKvsYx3muPHVjs", - "releaseDate":"24/09/2010", - "duration":"11:59:57", - "genres":[ - "Action", - "Adventure", - "RPG" - ], - "platform":"PC" - }, - { - "title":"God of War: Chains of Olympus", - "playlistId":"PLRfhDHeBTBJ6aLzxXL6JLStPjrxFH7nkf", - "releaseDate":"28/03/2008", - "duration":"3:34:18", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PSP" - }, - { - "title":"God of War: Ghost of Sparta", - "playlistId":"PLRfhDHeBTBJ7VH9EyPcKvQSCo6FbMUE6g", - "releaseDate":"03/11/2010", - "duration":"4:53:25", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PSP" - }, - { - "title":"God of War I", - "playlistId":"PLRfhDHeBTBJ4zvv9b7-EaycdD6x_YtIkj", - "releaseDate":"22/06/2005", - "duration":"7:02:42", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS2" - }, - { - "title":"Total Overdose", - "playlistId":"PLRfhDHeBTBJ6JUtolPUXj2YXNHLOMXbij", - "releaseDate":"16/09/2005", - "duration":"6:54:30", - "genres":[ - "Action", - "Adventure", - "Shooter" - ], - "platform":"PC" - }, - { - "title":"Le parrain II", - "playlistId":"PLRfhDHeBTBJ6ho33uahEjHiIyL5nSi_3O", - "releaseDate":"09/04/2009", - "duration":"8:24:01", - "genres":[ - "Action", - "Adventure", - "Shooter" - ], - "platform":"PC" - }, - { - "title":"Chicken Invaders 5 - Cluck of the dark side", - "playlistId":"PLRfhDHeBTBJ5x3Al4O71diHmivHNIRn2B", - "releaseDate":"13/03/2015", - "duration":"2:13:36", - "genres":[ - "Action", - "Casual", - "Indie" - ], - "platform":"PC" - }, - { - "title":"Spy Fox : Operation Ozone", - "playlistId":"PLRfhDHeBTBJ5VLwcplFFMaHjd5eKC4p2A", - "releaseDate":"30/03/2001", - "duration":"1:09:17", - "genres":[ - "Action", - "Adventure", - "Educational", - "Casual" - ], - "platform":"PC" - }, - { - "title":"Spy Fox : Opération Robot Expo", - "playlistId":"PLRfhDHeBTBJ4AAsZf91kxWWrQEKHae4Wx", - "releaseDate":"20/11/2000", - "duration":"1:12:15", - "genres":[ - "Action", - "Adventure", - "Educational", - "Casual" - ], - "platform":"PC" - }, - { - "title":"Splinter Cell Double Agent", - "playlistId":"PLRfhDHeBTBJ6KR3qS-DYwgEsMsP3hJmoU", - "releaseDate":"19/10/2006", - "duration":"6:45:13", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC" - }, - { - "title":"Rayman 2: The Great Escape", - "playlistId":"PLRfhDHeBTBJ7O8QVtJw10sNcDen6yARFm", - "releaseDate":"07/09/2000", - "duration":"4:18:06", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS1" - }, - { - "title":"Prince of Persia : Les sables oubliés", - "playlistId":"PLRfhDHeBTBJ7EmZWmBJjwVfrNo4c2OGnb", - "releaseDate":"20/04/2010", - "duration":"5:16:11", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC" - }, - { - "title":"Saints Row IV", - "playlistId":"PLRfhDHeBTBJ4QdQseb6rQyYe_lybyI5LB", - "releaseDate":"23/08/2013", - "duration":"20:55:30", - "genres":[ - "Action", - "Shooter" - ], - "platform":"PC" - }, - { - "title":"Psychonauts", - "playlistId":"PLRfhDHeBTBJ6NqC2O9ojqqUv91ARBqUkl", - "releaseDate":"03/02/2006", - "duration":"13:35:45", - "genres":[ - "Action", - "Platformer" - ], - "platform":"PC" - }, - { - "title":"Saints Row Gat out of Hell", - "playlistId":"PLRfhDHeBTBJ6qA11nZAq1HtkkuE98F0iO", - "releaseDate":"23/01/2015", - "duration":"5:45:04", - "genres":[ - "Action", - "Shooter" - ], - "platform":"PC" - }, - { - "title":"XIII - le jeu", - "playlistId":"PLRfhDHeBTBJ67wrk9BvDqa4pGRwq4jO-8", - "releaseDate":"20/11/2003", - "duration":"4:34:35", - "genres":[ - "Action", - "Adventure", - "Shooter" - ], - "platform":"PC" - }, - { - "title":"Saints Row The Third", - "playlistId":"PLRfhDHeBTBJ5BcOJy0BEPvdPbZSfi1P1b", - "releaseDate":"15/11/2011", - "duration":"13:53:03", - "genres":[ - "Action", - "Shooter" - ], - "platform":"PC" - }, - { - "title":"South Park - Le baton de la vérité", - "playlistId":"PLRfhDHeBTBJ6PP1XZO-tM70wPQyPBBGmT", - "releaseDate":"06/03/2014", - "duration":"9:16:21", - "genres":[ - "Adventure", - "RPG" - ], - "platform":"PC" - }, - { - "title":"Prince of Persia 2008", - "playlistId":"PLRfhDHeBTBJ4H78MI_RvItygKp29dg0bc", - "releaseDate":"04/12/2008", - "duration":"10:18:10", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC" - }, - { - "title":"Spy fox : Opération Milkshake", - "playlistId":"PLRfhDHeBTBJ42kEksYsYPnAHmS_7V8525", - "releaseDate":"02/09/1997", - "duration":"1:17:20", - "genres":[ - "Action", - "Adventure", - "Educational", - "Casual" - ], - "platform":"PC" - }, - { - "title":"Astérix & Obélix XXL 2 : Mission Ouifix", - "playlistId":"PLRfhDHeBTBJ6lDdIcNMFKsCzWtItCEcBq", - "releaseDate":"17/11/2006", - "duration":"4:08:28", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PSP" - }, - { - "title":"Ryse : Son of Rome", - "playlistId":"PLRfhDHeBTBJ6cs6uc2X0dzBvaNuF-n49i", - "releaseDate":"22/11/2013", - "duration":"4:52:11", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC", - "availableAt":20201120, - "endAt":20201208 - }, - { - "title":"South Park : L'Annale du Destin", - "playlistId":"PLRfhDHeBTBJ4WXhWDhZCsIp3k6JxR7HpU", - "releaseDate":"22/11/2013", - "duration":"12:25:40", - "genres":[ - "Adventure", - "RPG" - ], - "platform":"PC", - "availableAt":20201209, - "endAt":20201231 - }, - { - "title":"Rayman 2 : Revolution", - "playlistId":"PLRfhDHeBTBJ5iBOO8LgF8L_AvxX91jbaX", - "releaseDate":"14/12/2000", - "duration":"4:59:15", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS2", - "availableAt":20210101, - "endAt":20210124 - }, - { - "title":"Sleeping Dogs", - "playlistId":"PLRfhDHeBTBJ70QO5aD5fbYmlbSdvZdmQO", - "releaseDate":"17/08/2012", - "duration":"7:22:10", - "genres":[ - "Action", - "Adventure", - "Shooter" - ], - "platform":"PC", - "availableAt":20210125, - "endAt":20210221 - }, - { - "title":"Rayman contre les Lapins Crétins", - "playlistId":"PLRfhDHeBTBJ7zlVdm21oc8ndiRGxQJy6F", - "releaseDate":"30/11/2006", - "duration":"3:05:12", - "genres":[ - "Action", - "Adventure", - "Family" - ], - "platform":"PC", - "availableAt":20210222, - "endAt":20210308 - }, - { - "title":"Need for Speed Carbon", - "playlistId":"PLRfhDHeBTBJ6MEuVUHAD2HZETNt5cBCZP", - "releaseDate":"03/11/2006", - "duration":"5:12:32", - "genres":[ - "Action", - "Racing", - "Arcade" - ], - "platform":"PC", - "availableAt":20210309, - "endAt":20210326 - }, - { - "title":"Quantum Break", - "playlistId":"PLRfhDHeBTBJ5kA7HOY4M_5Ki0we95CW_f", - "releaseDate":"05/04/2016", - "duration":"5:29:07", - "genres":[ - "Action", - "Adventure", - "Shooter" - ], - "platform":"PC", - "availableAt":20210329, - "endAt":20210411 - }, - { - "title":"Splinter Cell Conviction", - "playlistId":"PLRfhDHeBTBJ48YBgwV9lobG0Ddo4EN9I3", - "releaseDate":"15/04/2010", - "duration":"4:02:12", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC", - "availableAt":20210412, - "endAt":20210422 - }, - { - "title":"Crash Bandicoot 3 : Warped", - "playlistId":"PLRfhDHeBTBJ4s03iZ1UDggaenmv_YDqzE", - "releaseDate":"10/12/1998", - "duration":"1:57:55", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC", - "availableAt":20210512, - "endAt":20210519 - }, - { - "title":"Crash Bandicoot", - "playlistId":"PLRfhDHeBTBJ6uNb7KhMn-zU-slBk-HPbe", - "releaseDate":"09/11/1996", - "duration":"1:48:31", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC", - "availableAt":20210426, - "endAt":20210503 - }, - { - "title":"Crash Bandicoot 2 : Cortex Strikes Back", - "playlistId":"PLRfhDHeBTBJ7gvlHCEXz7XFPkHVTCZpao", - "releaseDate":"01/12/1997", - "duration":"2:13:19", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC", - "availableAt":20210503, - "endAt":20210510 - }, - { - "title":"Max Payne 3", - "playlistId":"PLRfhDHeBTBJ6qxm-Yidg-RNG6ICJuvll2", - "releaseDate":"18/05/2012", - "duration":"7:01:05", - "genres":[ - "Action", - "Shooter" - ], - "platform":"PC", - "availableAt":20210521, - "endAt":20210621 - }, - { - "title":"Sigi - Un Pet Pour Mélusina", - "videoId":"odnIJiLjqCY", - "releaseDate":"22/12/2017", - "duration":"26:00", - "genres":[ - "Action", - "Platformer" - ], - "platform":"PC" - }, - { - "title":"Hercule", - "videoId":"Z-hI2U2g_j0", - "releaseDate":"01/07/1997", - "duration":"1:08:37", - "genres":[ - "Action", - "Platformer" - ], - "platform":"PS1" - }, - { - "title":"Beyond Good & Evil", - "playlistId":"PLRfhDHeBTBJ7GRN3EnqBEvfki5uAKiw5Z", - "releaseDate":"14/11/2003", - "duration":"5:52:22", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3" - }, - { - "title":"Dragon Ball : Advanced Adventure", - "videoId":"5ZHExfdq9F8", - "releaseDate":"17/06/2005", - "duration":"1:50:53", - "genres":[ - "Action", - "Platformer" - ], - "platform":"GBA", - "availableAt":20210622, - "endAt":20210622 - }, - { - "title":"Avatar : Le Dernier Maître de l'Air", - "videoId":"L7R_LBBOPEk", - "releaseDate":"27/10/2006", - "duration":"2:13:24", - "genres":[ - "Action", - "Adventure", - "RPG", - "Puzzle" - ], - "platform":"GBA", - "availableAt":20210623, - "endAt":20210623 - }, - { - "title":"Hot Pixel", - "videoId":"6UzzEQjYA9c", - "releaseDate":"22/06/2008", - "duration":"27:38", - "genres":[ - "Puzzle" - ], - "platform":"PSP" - }, - { - "title":"Prince of Persia : Les sables oubliés", - "videoId":"lYIH7XkENSw", - "releaseDate":"20/04/2010", - "duration":"3:23:14", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "availableAt":20210624, - "endAt":20210624, - "platform":"PSP" - }, - { - "title":"Shrek Super Slam", - "videoId":"AVwWGO0Zocs", - "releaseDate":"18/11/2005", - "duration":"31:13", - "genres":[ - "Fighting" - ], - "platform":"PS2" - }, - { - "title":"The Legend of Zelda: The Minish Cap", - "playlistId":"PLRfhDHeBTBJ41LQiFCR00rhaLAyMNy7cR", - "releaseDate":"12/11/2004", - "duration":"6:10:03", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"GBA", - "availableAt":20210625, - "endAt":20210715 - }, - { - "title":"Rayman Origins", - "playlistId":"PLRfhDHeBTBJ703-Pyf2BZmJC0dM8Uu4Wc", - "releaseDate":"24/11/2011", - "duration":"4:40:15", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC", - "availableAt":20210716, - "endAt":20210729 - }, - { - "title":"Danny Fantôme : L'Ultime Ennemi", - "videoId":"uDs_7iY6oqw", - "releaseDate":"26/05/2006", - "duration":"52:53", - "genres":[ - "Fighting" - ], - "platform":"GBA", - "availableAt":20210802, - "endAt":20210802 - }, - { - "title":"Ratchet & Clank : Opération Destruction", - "playlistId":"PLRfhDHeBTBJ4uA4ArliLWDLuJgaykBcIY", - "releaseDate":"14/11/2007", - "duration":"6:49:19", - "genres":[ - "Platformer", - "Shooter", - "Adventure", - "Action" - ], - "platform":"PS3", - "availableAt":20210803, - "endAt":20210908 - }, - { - "title":"Harry Potter et la Coupe de Feu", - "playlistId":"PLRfhDHeBTBJ5nRIWD5BCLi7NaQCYc6PpF", - "releaseDate":"10/11/2005", - "duration":"2:17:52", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC", - "availableAt":20210910, - "endAt":20210920 - }, - { - "title":"Pumpkin Jack", - "playlistId":"PLRfhDHeBTBJ4Aam6TaY3Z22rby8dr0TR6", - "releaseDate":"23/10/2020", - "duration":"3:33:36", - "genres":[ - "Platformer", - "Puzzle" - ], - "platform":"PC", - "availableAt":20210922, - "endAt":20211004 - }, - { - "title":"Dingo : Une Journée de Dingue !", - "videoId":"-LX3ae9m_uU", - "releaseDate":"13/12/2001", - "duration":"53:20", - "genres":[ - "Puzzle", - "Educational" - ], - "platform":"PS1", - "availableAt":20211006, - "endAt":20211006 - }, - { - "title":"Destroy All Humans! En route vers Paname !", - "playlistId":"PLRfhDHeBTBJ5PxtGm8Pwm7PNMhPVhIugR", - "releaseDate":"13/02/2009", - "duration":"6:44:54", - "genres":[ - "Shooter", - "Adventure" - ], - "platform":"PS3", - "availableAt":20211008, - "endAt":20211110 - }, - { - "title":"Madagascar", - "playlistId":"PLRfhDHeBTBJ4ifDoD5QUnc2O-bFyHWGDn", - "releaseDate":"17/06/2005", - "duration":"3:24:49", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS2", - "availableAt":20211009, - "endAt":20211026 - }, - { - "title":"Titeuf : Méga-Compet'", - "playlistId":"PLRfhDHeBTBJ54gjV7GzpbGumgw64u7LVP", - "releaseDate":"24/09/2004", - "duration":"1:26:00", - "genres":[ - "Casual", - "Puzzle" - ], - "platform":"PS2", - "availableAt":20211111, - "endAt":20211113 - }, - { - "title":"Jak and Daxter: The Precursor Legacy", - "playlistId":"PLRfhDHeBTBJ5QvrTXXZ64vl8sRngbgkDv", - "releaseDate":"14/12/2001", - "duration":"5:07:21", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS3", - "availableAt":20211115, - "endAt":20211210 - }, - { - "title":"Splinter Cell Chaos Theory HD", - "playlistId":"PLRfhDHeBTBJ7gViU4fNr0LhQvSAWk55T2", - "releaseDate":"10/08/2011", - "duration":"5:22:51", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3", - "availableAt":20211213, - "endAt":20220103 - }, - { - "title":"Jak II : Hors la Loi", - "playlistId":"PLRfhDHeBTBJ5omqALCFYK0zrIANxO8d2y", - "releaseDate":"15/10/2003", - "duration":"6:25:22", - "genres":[ - "Action", - "Adventure", - "Platformer", - "Shooter" - ], - "platform":"PS3", - "availableAt":20220105, - "endAt":20220128 - }, - { - "title":"Splinter Cell: Pandora Tomorrow", - "playlistId":"PLRfhDHeBTBJ7xvQe1Jbs8Kv9ie_uQhdJH", - "releaseDate":"25/03/2004", - "duration":"3:11:52", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3", - "availableAt":20220131, - "endAt":20220216 - }, - { - "title":"Jak 3", - "playlistId":"PLRfhDHeBTBJ5KZm9-LWr5C-Iet5dyGGSc", - "releaseDate":"01/12/2004", - "duration":"6:27:02", - "genres":[ - "Action", - "Adventure", - "Platformer", - "Shooter" - ], - "platform":"PS3", - "availableAt":20220218, - "endAt":20220318 - }, - { - "title":"Crash Bandicoot 4: It's About Time", - "playlistId":"PLRfhDHeBTBJ6AGIBxZa5zvykgQBKSHzd5", - "releaseDate":"26/03/2021", - "duration":"5:34:31", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC", - "availableAt":20220321, - "endAt":20220413 - }, - { - "title":"Dragon Ball Z : L'Héritage de Goku 2", - "playlistId":"PLRfhDHeBTBJ73iy4EC825y_EZ4cmrneem", - "releaseDate":"01/08/2003", - "duration":"4:38:54", - "genres":[ - "Action", - "Adventure", - "RPG" - ], - "platform":"GBA", - "availableAt":20220415, - "endAt":20220426 - }, - { - "title":"Zapper : Le Criquet Ravageur !", - "playlistId":"PLRfhDHeBTBJ63WnP22tTxS7SMXx_SyMDT", - "releaseDate":"07/03/2003", - "duration":"2:45:50", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS2", - "availableAt":20220427, - "endAt":20220501 - }, - { - "title":"DmC: Devil May Cry", - "playlistId":"PLRfhDHeBTBJ6KPgu5IETjWe7YLFYjRzx3", - "releaseDate":"15/01/2013", - "duration":"6:08:09", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC", - "availableAt":20220502, - "endAt":20220524 - }, - { - "title":"DmC Devil May Cry : La Chute de Vergil", - "playlistId":"PLRfhDHeBTBJ7U7XHzOSTBvaVkkd0dOAE8", - "releaseDate":"05/03/2013", - "duration":"1:20:40", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC", - "availableAt":20220525, - "endAt":20220531 - }, - { - "title":"Marc Eckō's Getting Up: Contents Under Pressure", - "playlistId":"PLRfhDHeBTBJ558YL4-DGi1B8jrQhM3uWa", - "releaseDate":"17/06/2006", - "duration":"7:26:19", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS2", - "availableAt":20220704, - "endAt":20220805 - }, - { - "title":"Psychonauts 2", - "playlistId":"PLRfhDHeBTBJ4PRXxtpRjP_o3PUzSQ0-nQ", - "releaseDate":"25/08/2021", - "duration":"10:59:44", - "genres":[ - "Action", - "Platformer" - ], - "platform":"PC" - }, - { - "title":"The Simpsons Hit & Run", - "playlistId":"PLRfhDHeBTBJ4Z99FBhfpJO6T_pe5HymRV", - "releaseDate":"16/09/2003", - "duration":"4:04:20", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC", - "availableAt":20220808, - "endAt":20220822 - }, - { - "title":"Ratchet & Clank : A Crack in Time", - "playlistId":"PLRfhDHeBTBJ5ysRbqhFaSSapvvuEVOu7M", - "releaseDate":"04/11/2009", - "duration":"5:45:00", - "genres":[ - "Action", - "Adventure", - "Platformer", - "Shooter" - ], - "platform":"PS3", - "availableAt":20220601, - "endAt":20220620 - }, - { - "title":"-KLAUS-", - "playlistId":"PLRfhDHeBTBJ7MU5DX4P_oBIRN457ah9lA", - "releaseDate":"19/01/2016", - "duration":"4:32:14", - "genres":[ - "Platformer", - "Puzzle" - ], - "platform":"PC", - "availableAt":20220824, - "endAt":20220905 - }, - { - "title":"Rayman 3 : Hoodlum Havoc HD", - "playlistId":"PLRfhDHeBTBJ7bQV45KKQ7Japc7Z9uLvgJ", - "releaseDate":"21/03/2012", - "duration":"5:27:54", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS3", - "availableAt":20220907, - "endAt":20220926 - }, - { - "title":"Batman Vengeance", - "playlistId":"PLRfhDHeBTBJ53yF5vIimEuhh4JbBQF5l4", - "releaseDate":"15/10/2001", - "duration":"2:32:02", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS2", - "availableAt":20220928, - "endAt":20221007 - }, - { - "title":"Sonic Adventure DX: Director's Cut", - "playlistId":"PLRfhDHeBTBJ5YBw47XwARehnyP6bhSpuJ", - "releaseDate":"06/02/2004", - "duration":"6:25:57", - "genres":[ - "Action", - "Adventure", - "Platformer", - "RPG" - ], - "platform":"PC", - "availableAt":20221010, - "endAt":20221102 - }, - { - "title":"Harry Potter à l'Ecole des Sorciers", - "playlistId":"PLRfhDHeBTBJ6Dbl3hku6SlyybSN86clQ7", - "releaseDate":"16/11/2001", - "duration":"3:36:33", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC", - "availableAt":20221103, - "endAt":20221111 - }, - { - "title":"Harry Potter et la Chambre des Secrets", - "playlistId":"PLRfhDHeBTBJ4zV33TJt9y--cp84eD7ivO", - "releaseDate":"14/11/2002", - "duration":"4:06:59", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC", - "availableAt":20221114, - "endAt":20221123 - }, - { - "title":"Sonic Adventure 2", - "videoId":"a0mr0A8VM9A", - "releaseDate":"19/11/2012", - "duration":"3:50:39", - "genres":[ - "Action", - "Adventure", - "Platformer", - "RPG" - ], - "platform":"PC", - "availableAt":20221125, - "endAt":20221125 - }, - { - "title":"Dead to Rights", - "playlistId":"PLRfhDHeBTBJ7y3xsVkTNRYORp_mX0ag14", - "releaseDate":"03/06/2003", - "duration":"6:19:09", - "genres":[ - "Action", - "Shooter", - "Adventure" - ], - "platform":"PC", - "availableAt":20221201, - "endAt":20221228 - }, - { - "title": "Harry Potter et le Prisonnier d'Azkaban", - "videoId": "IOog6QKck6o", - "releaseDate": "04/06/2004", - "duration": "3:01:32", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PC", - "availableAt":20221230, - "endAt":20221230 - }, - { - "title":"Dead to Rights : Retribution", - "playlistId":"PLRfhDHeBTBJ6sm2gbS9qtdovdSGygP-3f", - "releaseDate":"23/10/2010", - "duration":"5:50:51", - "genres":[ - "Action", - "Shooter", - "Adventure" - ], - "platform":"PS3", - "availableAt":20230101, - "endAt":20230123 - }, - { - "title":"God of War (2018)", - "playlistId":"PLRfhDHeBTBJ4axkTXH5Ty8shlzKlR86Wm", - "releaseDate":"14/02/2022", - "duration":"17:41:07", - "genres":[ - "Action", - "Adventure", - "RPG" - ], - "platform":"PC" - }, - { - "title":"50 Cent: Blood on the Sand", - "playlistId":"PLRfhDHeBTBJ44bSU0cT2G0m4srnTO2gZY", - "releaseDate":"13/02/2009", - "duration":"3:52:13", - "genres":[ - "Action", - "Adventure", - "Shooter" - ], - "platform":"PS3", - "availableAt":20230125, - "endAt":20230204 - }, - { - "title":"Aladdin : La Revanche de Nasira", - "playlistId":"PLRfhDHeBTBJ49XIAdTq1YJ5ozgKjaUM6E", - "releaseDate":"13/12/2000", - "duration":"3:51:25", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS1", - "availableAt":20230206, - "endAt":20230216 - }, - { - "title":"Alan Wake Remastered", - "playlistId":"PLRfhDHeBTBJ6L_uo8nMekfbJRE_ixUG5H", - "releaseDate":"04/10/2021", - "duration":"9:33:04", - "genres":[ - "Action", - "Adventure", - "Shooter" - ], - "platform":"PC", - "availableAt":20230217, - "endAt":20230301 - }, - { - "title":"The Punisher", - "playlistId":"PLRfhDHeBTBJ4ZxkrGCk7a8C-6jXEj4HoT", - "releaseDate":"11/03/2005", - "duration":"5:36:21", - "genres":[ - "Action", - "Shooter", - "Adventure" - ], - "platform":"PC", - "availableAt":20230303, - "endAt":20230403 - }, - { - "title":"Astérix & Obélix : Baffez-les Tous !", - "playlistId":"PLRfhDHeBTBJ5hLNQ3UBKtImQLXkR1bChZ", - "releaseDate":"07/12/2021", - "duration":"5:43:21", - "genres":[ - "Fighting" - ], - "platform":"PC", - "availableAt":20230405, - "endAt":20230417 - }, - { - "title":"Dragon Ball Z : Buu's Fury", - "playlistId":"PLRfhDHeBTBJ6zkB2GAtg_k6_J799AozQz", - "releaseDate":"14/09/2004", - "duration":"4:36:11", - "genres":[ - "Action", - "Adventure", - "RPG" - ], - "platform":"GBA", - "availableAt":20230419, - "endAt":20230512 - }, - { - "title":"Space Invaders Extreme", - "videoId":"6wJFMcIHi8k", - "releaseDate":"03/07/2008", - "duration":"0:30:04", - "genres":[ - "Action", - "Shooter" - ], - "platform":"PSP", - "availableAt":20230515, - "endAt":20230515 - }, - { - "title":"X-Men Origins : Wolverine", - "playlistId":"PLRfhDHeBTBJ6FOWzD8YONj2LoxCxPNJzO", - "releaseDate":"30/04/2009", - "duration":"6:43:09", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PC", - "availableAt":20230517, - "endAt":20230602 - }, - { - "title":"Les Simpson : Le Jeu", - "playlistId":"PLRfhDHeBTBJ6aLNhmxhZdrI2dz0Bavlkh", - "releaseDate":"15/11/2007", - "duration":"4:05:46", - "genres":[ - "Action", - "Adventure", - "RPG" - ], - "platform":"PS3", - "availableAt":20230605, - "endAt":20230626 - }, - { - "title":"Worms Ultimate Mayhem", - "videoId":"Drz_YdmE-yQ", - "releaseDate":"28/09/2011", - "duration":"3:43:44", - "genres":[ - "Action", - "Strategy" - ], - "platform":"PS3", - "availableAt":20230628, - "endAt":20230628 - }, - { - "title":"Rayman Legends", - "playlistId":"PLRfhDHeBTBJ4GoSaMs-BqHumiPqUHU3OX", - "releaseDate":"29/08/2013", - "duration":"4:41:01", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS3", - "availableAt":20230630, - "endAt":20230712 - }, - { - "title":"Yu-Gi-Oh! Les Cartes Sacrées", - "videoId":"NZiAc-mIJuQ", - "releaseDate":"30/01/2004", - "duration":"2:57:48", - "genres":[ - "RPG", - "Card" - ], - "platform":"GBA", - "availableAt":20230714, - "endAt":20230714 - }, - { - "title":"Afro Samurai", - "playlistId":"PLRfhDHeBTBJ6PHF-BH9dFug86rb877o2y", - "releaseDate":"27/03/2009", - "duration":"4:46:22", - "genres":[ - "Action", - "Fighting" - ], - "platform":"PS3", - "availableAt":20230717, - "endAt":20230802 - }, - { - "title":"Yu-Gi-Oh! Reshef le Destructeur", - "videoId":"1vvwPVwbACY", - "releaseDate":"10/09/2004", - "duration":"5:04:19", - "genres":[ - "RPG", - "Card" - ], - "platform":"GBA", - "availableAt":20230804, - "endAt":20230804 - }, - { - "title": "Pursuit Force", - "playlistId": "PLRfhDHeBTBJ47XmS2_upaFjIh_wKhN0XB", - "releaseDate": "23/11/2005", - "duration":"3:06:59", - "genres": [ - "Action", - "Racing" - ], - "platform": "PSP", - "availableAt":20230807, - "endAt":20230816 - }, - { - "title": "Stranglehold", - "playlistId": "PLRfhDHeBTBJ7dIG6eD6CYJh8xu4j_eXyq", - "releaseDate": "14/09/2007", - "duration":"4:00:42", - "genres": [ - "Action", - "Shooter" - ], - "platform": "PC", - "availableAt": 20230818, - "endAt": 20230901 - }, - { - "title": "Metal Slug XX", - "videoId": "0vTdVPo9I_4", - "releaseDate": "25/06/2010", - "duration": "1:10:06", - "genres": [ - "Action" - ], - "platform": "PSP", - "availableAt": 20230904, - "endAt": 20230904 - }, - { - "title": "Jet Li: Rise to Honour", - "playlistId": "PLRfhDHeBTBJ5K0xPqtndkObR9g05hHw5h", - "duration": "3:35:54", - "releaseDate": "07/04/2004", - "genres": [ - "Action", - "Shooter" - ], - "platform": "PS2", - "availableAt": 20230905, - "endAt": 20230922 - }, - { - "title": "Kya : Dark Lineage", - "playlistId": "PLRfhDHeBTBJ54lUeJrodUPuEOohHUdCJj", - "duration": "7:34:50", - "releaseDate": "21/11/2003", - "genres": [ - "Action", - "Adventure", - "Platformer" - ], - "platform": "PS2", - "availableAt": 20230925, - "endAt": 20231018 - }, - { - "title": "Bionicle", - "videoId": "rGnInmyP2s4", - "duration": "1:20:42", - "releaseDate": "30/10/2003", - "genres": [ - "Action", - "Adventure", - "Platformer" - ], - "platform": "PC", - "availableAt": 20231020, - "endAt": 20231020 - }, - { - "title": "Pursuit Force: Extreme Justice", - "playlistId": "PLRfhDHeBTBJ72gRyZBPf23ZfxSlG5h8Em", - "duration": "4:35:22", - "releaseDate": "12/12/2007", - "genres": [ - "Action", - "Racing" - ], - "platform": "PSP", - "availableAt": 20231023, - "endAt": 20231101 - }, - { - "title":"God of War: Chains of Olympus HD", - "playlistId":"PLRfhDHeBTBJ5BASLY5Kgof-1fgRxNVkOX", - "releaseDate":"13/09/2011", - "duration":"3:20:24", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3", - "availableAt": 20231106, - "endAt": 20231115 - }, - { - "title":"God of War I HD", - "playlistId": "PLRfhDHeBTBJ5EYj5y6ByrLTuhqyKV4dpc", - "releaseDate":"03/11/2010", - "duration":"6:48:04", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3", - "availableAt": 20231120, - "endAt": 20231218 - }, - { - "title":"Astérix aux Jeux olympiques", - "playlistId":"PLRfhDHeBTBJ5S4PtZF0WBNaPVYvNDUJun", - "releaseDate":"09/11/2007", - "duration":"3:23:11", - "genres":[ - "Action", - "Adventure", - "Platformer" - ], - "platform":"PS2", - "availableAt": 20240101, - "endAt": 20240119 - }, - { - "title":"God of War: Ghost of Sparta HD", - "playlistId":"PLRfhDHeBTBJ7TJ1MBUMRg588JQd9p-ub9", - "releaseDate":"14/09/2011", - "duration":"4:30:40", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3", - "availableAt": 20240122, - "endAt": 20240223 - }, - { - "title": "Lucky Luke : Tous à l'Ouest", - "videoId": "kDoc1rzc-Zs", - "releaseDate": "07/11/2007", - "duration": "1:21:01", - "genres": [ - "Arcade", - "Casual" - ], - "platform": "PC", - "availableAt": 20240304, - "endAt": 20240304 - }, - { - "title": "Jak and Daxter: The Lost Frontier", - "playlistId": "PLRfhDHeBTBJ5yE1da-5kvYirmxBd7sLEX", - "releaseDate": "18/11/2009", - "duration": "5:16:27", - "genres": [ - "Action", - "Adventure", - "Platformer" - ], - "platform": "PSP", - "availableAt": 20240306, - "endAt": 20240325 - }, - { - "title": "Batman The Telltale Series", - "playlistId": "PLRfhDHeBTBJ6_wunnzhRIaBhtF3aiuS7q", - "releaseDate": "02/08/2016", - "duration": "7:08:15", - "genres": [ - "Action", - "Adventure" - ], - "platform": "PC", - "availableAt": 20240329, - "endAt": 20240408 - }, - { - "title":"Splinter Cell HD", - "playlistId":"PLRfhDHeBTBJ7Do88fiav23gsXrV-0SHn-", - "releaseDate":"10/08/2011", - "duration":"4:40:45", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3", - "availableAt": 20240410, - "endAt": 20240501 - }, - { - "title": "Daxter", - "playlistId": "PLRfhDHeBTBJ5AJN9pWW6v6NQSxnK5tGTV", - "releaseDate": "20/04/2006", - "duration": "5:12:35", - "genres": [ - "Action", - "Adventure", - "Platformer" - ], - "platform": "PSP", - "availableAt": 20240503, - "endAt": 20240603 - }, - { - "title":"Chili Con Carnage", - "playlistId":"PLRfhDHeBTBJ7-gVyU44CdXMekwgbm-sNn", - "releaseDate":"23/02/2007", - "duration":"3:19:49", - "genres":[ - "Action", - "Adventure", - "Shooter" - ], - "platform":"PSP", - "availableAt": 20240610, - "endAt": 20240624 - }, - { - "title": "Robots", - "playlistId": "PLRfhDHeBTBJ56jE5Kb3Wb6vBZZKLgM0dR", - "releaseDate": "01/04/2005", - "duration": "3:28:59", - "genres": [ - "Action", - "Adventure", - "Platformer" - ], - "platform": "PS2", - "availableAt": 20240701, - "endAt": 20240717 - }, - { - "title": "Legend of Kay Anniversary", - "playlistId": "PLRfhDHeBTBJ4wnTcJ3vXDERleACuvRLPp", - "releaseDate": "29/05/2015", - "duration": "9:06:52", - "genres": [ - "Action", - "Adventure", - "Platformer" - ], - "platform": "PS3", - "availableAt": 20240722, - "endAt": 20240826 - }, - { - "title": "Dr. Fetus' Mean Meat Machine", - "playlistId": "PLRfhDHeBTBJ5QMoekvhUMtxdWRNHEMAbD", - "releaseDate": "22/06/2023", - "duration": "5:28:02", - "genres": [ - "Puzzle" - ], - "platform": "PC" - }, - { - "title": "Dante's Inferno", - "playlistId": "PLRfhDHeBTBJ7TcJC0sKdnIpnebpXtemRP", - "releaseDate": "04/10/2010", - "duration": "5:48:59", - "genres": [ - "Action", - "Adventure" - ], - "platform": "PS3", - "availableAt": 20240902, - "endAt": 20240923 - }, - { - "title": "Lollipop Chainsaw", - "playlistId": "PLRfhDHeBTBJ7-G7QCx-nB3Oc-TNmIaXMk", - "releaseDate": "12/06/2012", - "duration": "4:26:45", - "genres": [ - "Action", - "Adventure", - "RPG" - ], - "platform": "PS3", - "availableAt": 20240927, - "endAt": 20241011 - }, - { - "title":"Ratchet & Clank : Quest for Booty", - "playlistId":"PLRfhDHeBTBJ4cSitG0GgcyOxbL54oqvgo", - "releaseDate":"21/08/2008", - "duration":"1:48:22", - "genres":[ - "Action", - "Adventure", - "Platformer", - "Shooter" - ], - "platform":"PS3", - "availableAt":20241015, - "endAt":20241021 - }, - { - "title":"Ratchet & Clank Nexus", - "playlistId":"PLRfhDHeBTBJ5l6LcIoNR1rUTAaGp8WPDf", - "releaseDate":"21/08/2008", - "duration": "3:39:10", - "genres":[ - "Action", - "Adventure", - "Platformer", - "Shooter" - ], - "platform":"PS3", - "availableAt":20241023, - "endAt":20241104 - }, - { - "title": "La Légende de Korra", - "playlistId": "PLRfhDHeBTBJ544INoLqRwd2WxbcFBJxz4", - "releaseDate": "22/10/2014", - "duration": "2:33:20", - "genres":[ - "Action", - "Adventure", - "Platformer", - "Shooter" - ], - "platform":"PS3", - "availableAt":20241106, - "endAt":20241120 - }, - { - "title": "Sly Raccoon", - "playlistId": "PLRfhDHeBTBJ4ZNp1r0Rlsc2yopGcndh3x", - "releaseDate": "07/12/2011", - "duration": "4:05:13", - "genres": [ - "Action", - "Adventure" - ], - "platform": "PS3", - "availableAt":20241122, - "endAt":20241202 - }, - { - "title": "DuckTales Remastered", - "playlistId": "PLRfhDHeBTBJ6PHLRDp5qi_ZCfVy-Lg8OO", - "releaseDate": "14/08/2013", - "duration": "2:34:47", - "genres": [ - "Action", - "Platformer" - ], - "platform": "PS3", - "availableAt":20241209, - "endAt":20241223 - }, - { - "title":"Splinter Cell Double Agent HD", - "playlistId": "PLRfhDHeBTBJ4DjOEslvP3es_dby3reani", - "releaseDate":"27/03/2007", - "duration":"4:06:45", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3", - "availableAt":20241225, - "endAt":20250115 - }, - { - "title": "Enslaved Odyssey to the West", - "playlistId": "PLRfhDHeBTBJ7Mc84qhoazY9gu0EgHPSAn", - "releaseDate": "08/10/2010", - "duration": "6:29:47", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3", - "availableAt":20250117, - "endAt":20250217 - }, - { - "title": "Sly 2 : Association de voleurs", - "playlistId": "PLRfhDHeBTBJ6WKXcBeMcBP7JVfRmbc361", - "releaseDate": "07/12/2011", - "duration": "11:26:08", - "genres": [ - "Action", - "Adventure" - ], - "platform": "PS3", - "availableAt":20250221, - "endAt":20250312 - }, - { - "title": "Batman The Enemy Within", - "playlistId": "PLRfhDHeBTBJ7acyVd3rXz6TsMphHkQKHG", - "releaseDate": "08/08/2017", - "duration": "8:10:21", - "genres": [ - "Action", - "Adventure" - ], - "platform": "PC", - "availableAt": 20250317, - "endAt": 20250326 - }, - { - "title": "Fahrenheit Indigo Prophecy", - "playlistId": "PLRfhDHeBTBJ4Fh7iL-9-0ZEncKHStdgO2", - "releaseDate": "16/09/2005", - "duration": "5:38:37", - "genres": [ - "Action" - ], - "platform": "PS2", - "availableAt":20250328, - "endAt":20250506 - }, - { - "title": "Metal Gear Rising Revengeance", - "playlistId": "PLRfhDHeBTBJ5hcdTpev5IgERHhgFaIDbP", - "releaseDate": "21/02/2013", - "duration": "5:59:03", - "genres": [ - "Action", - "Adventure" - ], - "platform": "PS3", - "availableAt":20250508, - "endAt":20250523 - }, - { - "title": "Ghost Rider", - "playlistId": "PLRfhDHeBTBJ4KQEvirhf9p1o_xuFysva8", - "releaseDate": "16/02/2007", - "duration": "3:29:44", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS2", - "availableAt":20250526, - "endAt":20250702 - }, - { - "title": "Sly 3 : Honor Among Thieves", - "playlistId": "PLRfhDHeBTBJ78Irx7Gb3-f07ApP4Ybvvb", - "releaseDate": "07/12/2011", - "duration": "9:46:28", - "genres": [ - "Action", - "Adventure" - ], - "platform": "PS3", - "availableAt":20250707, - "endAt":20250721 - }, - { - "title": "La Planète au Trésor", - "playlistId": "PLRfhDHeBTBJ5m5_crztm7tu7HwJiRpLSl", - "releaseDate": "13/02/2013", - "duration": "4:54:34", - "genres": [ - "Action", - "Platformer" - ], - "platform": "PS3", - "availableAt":20250725, - "endAt":20250905 - }, - { - "title": "The Warriors", - "playlistId": "PLRfhDHeBTBJ4TZAon0Ty7Z4wLnRtb4D3t", - "releaseDate": "20/03/2013", - "duration": "6:11:20", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3", - "availableAt": 20250908, - "endAt": 20251001 - }, - { - "title": "WET", - "playlistId": "PLRfhDHeBTBJ46OsqsAwhbnzBqCPryYeuV", - "releaseDate": "18/09/2009", - "duration": "4:12:24", - "genres":[ - "Action", - "Adventure" - ], - "platform":"PS3", - "availableAt": 20251006, - "endAt": 20251029 - }, - { - "title": "Contra Operation Galuga", - "playlistId": "PLRfhDHeBTBJ7q8Vq-WzXu8bRhv82qbR2X", - "releaseDate": "21/02/2024", - "duration": "2:04:32", - "genres":[ - "Action", - "Adventure" - ], - "platform": "PC" - }, - { - "title": "Deadpool", - "playlistId": "PLRfhDHeBTBJ4o4JXHAX93JXwXw_lazgqI", - "releaseDate": "28/06/2013", - "duration": "5:23:02", - "genres": [ - "Action", - "Adventure" - ], - "platform": "PC", - "availableAt": 20251031, - "endAt": 20251117 - }, - { - "title": "Sly Cooper : Voleurs à travers le Temps", - "playlistId": "PLRfhDHeBTBJ55wAzjDq3URVbIluuz7NpL", - "releaseDate": "27/03/2013", - "duration": "8:55:44", - "genres": [ - "Action", - "Adventure" - ], - "platform": "PS3", - "availableAt": 20251119, - "endAt": 20251212 - }, - { - "title": "Asura's Wrath", - "playlistId": "PLRfhDHeBTBJ4VxR6N3cG5Jaasb3ovntPQ", - "releaseDate": "24/02/2012", - "duration": "8:49:28", - "genres": [ - "Action", - "Adventure" - ], - "platform": "PS3", - "availableAt": 20251215, - "endAt": 20260123 - }, - { - "title": "Ratchet & Clank : All 4 One", - "playlistId": "PLRfhDHeBTBJ5o809T1bcqRrEC_uoOtT_M", - "releaseDate": "19/10/2011", - "duration": "7:56:23", - "genres": [ - "Action", - "Adventure", - "Platformer", - "Shooter" - ], - "platform": "PS3", - "availableAt": 20260126, - "endAt": 20260216 - } -] + { + "id": 115, + "title": "-KLAUS-", + "playlistId": "PLRfhDHeBTBJ7MU5DX4P_oBIRN457ah9lA", + "releaseDate": "2016-01-19", + "duration": "04:32:14", + "platform": 1, + "genres": [ + 12, + 13 + ] + }, + { + "id": 126, + "title": "50 Cent: Blood on the Sand", + "playlistId": "PLRfhDHeBTBJ44bSU0cT2G0m4srnTO2gZY", + "releaseDate": "2009-02-13", + "duration": "03:52:13", + "platform": 6, + "genres": [ + 1, + 2, + 16 + ] + }, + { + "id": 138, + "title": "Afro Samurai", + "playlistId": "PLRfhDHeBTBJ6PHF-BH9dFug86rb877o2y", + "releaseDate": "2009-03-27", + "duration": "04:46:22", + "platform": 6, + "genres": [ + 1, + 9 + ] + }, + { + "id": 127, + "title": "Aladdin : La Revanche de Nasira", + "playlistId": "PLRfhDHeBTBJ49XIAdTq1YJ5ozgKjaUM6E", + "releaseDate": "2000-12-13", + "duration": "03:51:25", + "platform": 4, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 128, + "title": "Alan Wake Remastered", + "playlistId": "PLRfhDHeBTBJ6L_uo8nMekfbJRE_ixUG5H", + "releaseDate": "2021-10-04", + "duration": "09:33:04", + "platform": 1, + "genres": [ + 1, + 2, + 16 + ] + }, + { + "id": 130, + "title": "Astérix & Obélix : Baffez-les Tous !", + "playlistId": "PLRfhDHeBTBJ5hLNQ3UBKtImQLXkR1bChZ", + "releaseDate": "2021-12-07", + "duration": "05:43:21", + "platform": 1, + "genres": [ + 9 + ] + }, + { + "id": 17, + "title": "Astérix & Obélix XXL", + "playlistId": "PLRfhDHeBTBJ7hPe8RxhK3FTXoqdFNL0BG", + "releaseDate": "2003-11-21", + "duration": "06:21:55", + "platform": 5, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 18, + "title": "Astérix & Obélix XXL 2", + "playlistId": "PLRfhDHeBTBJ6FTJ2LSrdL4MaFM1Pl5nfo", + "releaseDate": "2018-11-29", + "duration": "04:53:56", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 70, + "title": "Astérix & Obélix XXL 2 : Mission Ouifix", + "playlistId": "PLRfhDHeBTBJ6lDdIcNMFKsCzWtItCEcBq", + "releaseDate": "2006-11-17", + "duration": "04:08:28", + "platform": 3, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 149, + "title": "Astérix aux Jeux olympiques", + "playlistId": "PLRfhDHeBTBJ5S4PtZF0WBNaPVYvNDUJun", + "releaseDate": "2007-11-09", + "duration": "03:23:11", + "platform": 5, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 87, + "title": "Avatar : Le Dernier Maître de l'Air", + "videoId": "L7R_LBBOPEk", + "releaseDate": "2006-10-27", + "duration": "02:13:24", + "platform": 2, + "genres": [ + 1, + 2, + 14, + 13 + ] + }, + { + "id": 8, + "title": "Batman Arkham City", + "playlistId": "PLRfhDHeBTBJ6RBqsJ0WYOOaL5wohZTbxw", + "releaseDate": "2011-10-20", + "duration": "06:47:41", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 153, + "title": "Batman The Telltale Series", + "playlistId": "PLRfhDHeBTBJ6_wunnzhRIaBhtF3aiuS7q", + "releaseDate": "2016-08-02", + "duration": "07:08:15", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 117, + "title": "Batman Vengeance", + "playlistId": "PLRfhDHeBTBJ53yF5vIimEuhh4JbBQF5l4", + "releaseDate": "2001-10-15", + "duration": "02:32:02", + "platform": 5, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 6, + "title": "Batman: Arkham Asylum", + "playlistId": "PLRfhDHeBTBJ4FHOgX8bYRe8alwgNKIpYv", + "releaseDate": "2009-08-28", + "duration": "05:59:51", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 7, + "title": "Batman: Arkham Knight", + "playlistId": "PLRfhDHeBTBJ6POjMkqqeCliq5jrCWagcg", + "releaseDate": "2015-06-23", + "duration": "08:33:35", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 4, + "title": "Batman: Arkham Origins", + "playlistId": "PLRfhDHeBTBJ56EHMyvxX6iBKldqvxJHqz", + "releaseDate": "2013-10-25", + "duration": "06:48:06", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 31, + "title": "Batman: Rise of Sin Tzu", + "playlistId": "PLRfhDHeBTBJ4BQrJ2mIgTHLpgPVR2XsRb", + "releaseDate": "2003-11-20", + "duration": "04:25:24", + "platform": 5, + "genres": [ + 1 + ] + }, + { + "id": 85, + "title": "Beyond Good & Evil", + "playlistId": "PLRfhDHeBTBJ7GRN3EnqBEvfki5uAKiw5Z", + "releaseDate": "2003-11-14", + "duration": "05:52:22", + "platform": 6, + "genres": [ + 1, + 2 + ] + }, + { + "id": 145, + "title": "Bionicle", + "videoId": "rGnInmyP2s4", + "releaseDate": "2003-10-30", + "duration": "01:20:42", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 44, + "title": "Bully : Scholarship Edition", + "playlistId": "PLRfhDHeBTBJ4l1G59ZVCUuTuZ-btLpPnm", + "releaseDate": "2008-03-07", + "duration": "11:38:17", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 56, + "title": "Chicken Invaders 5 - Cluck of the dark side", + "playlistId": "PLRfhDHeBTBJ5x3Al4O71diHmivHNIRn2B", + "releaseDate": "2015-03-13", + "duration": "02:13:36", + "platform": 1, + "genres": [ + 1, + 6, + 10 + ] + }, + { + "id": 156, + "title": "Chili Con Carnage", + "playlistId": "PLRfhDHeBTBJ7-gVyU44CdXMekwgbm-sNn", + "releaseDate": "2007-02-23", + "duration": "03:19:49", + "platform": 3, + "genres": [ + 1, + 2, + 16 + ] + }, + { + "id": 178, + "title": "Contra Operation Galuga", + "playlistId": "PLRfhDHeBTBJ7q8Vq-WzXu8bRhv82qbR2X", + "releaseDate": "2024-02-21", + "duration": "02:04:32", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 80, + "title": "Crash Bandicoot", + "playlistId": "PLRfhDHeBTBJ6uNb7KhMn-zU-slBk-HPbe", + "releaseDate": "1996-11-09", + "duration": "01:48:31", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 81, + "title": "Crash Bandicoot 2 : Cortex Strikes Back", + "playlistId": "PLRfhDHeBTBJ7gvlHCEXz7XFPkHVTCZpao", + "releaseDate": "1997-12-01", + "duration": "02:13:19", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 79, + "title": "Crash Bandicoot 3 : Warped", + "playlistId": "PLRfhDHeBTBJ4s03iZ1UDggaenmv_YDqzE", + "releaseDate": "1998-12-10", + "duration": "01:57:55", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 106, + "title": "Crash Bandicoot 4: It's About Time", + "playlistId": "PLRfhDHeBTBJ6AGIBxZa5zvykgQBKSHzd5", + "releaseDate": "2021-03-26", + "duration": "05:34:31", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 16, + "title": "Da Vinci Code", + "playlistId": "PLRfhDHeBTBJ4LD3P49nfFel3sLPMw7Kl5", + "releaseDate": "2006-05-19", + "duration": "05:49:40", + "platform": 5, + "genres": [ + 1, + 2, + 13 + ] + }, + { + "id": 93, + "title": "Danny Fantôme : L'Ultime Ennemi", + "videoId": "uDs_7iY6oqw", + "releaseDate": "2006-05-26", + "duration": "00:52:53", + "platform": 2, + "genres": [ + 9 + ] + }, + { + "id": 50, + "title": "Darksiders I", + "playlistId": "PLRfhDHeBTBJ61QLZkvjtKvsYx3muPHVjs", + "releaseDate": "2010-09-24", + "duration": "11:59:57", + "platform": 1, + "genres": [ + 1, + 2, + 14 + ] + }, + { + "id": 42, + "title": "Darksiders II", + "playlistId": "PLRfhDHeBTBJ6Pp3ukQM6-F8LYtOmXmy9X", + "releaseDate": "2012-08-21", + "duration": "14:35:48", + "platform": 6, + "genres": [ + 1, + 2, + 14 + ] + }, + { + "id": 155, + "title": "Daxter", + "playlistId": "PLRfhDHeBTBJ5AJN9pWW6v6NQSxnK5tGTV", + "releaseDate": "2006-04-20", + "duration": "05:12:35", + "platform": 3, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 122, + "title": "Dead to Rights", + "playlistId": "PLRfhDHeBTBJ7y3xsVkTNRYORp_mX0ag14", + "releaseDate": "2003-06-03", + "duration": "06:19:09", + "platform": 1, + "genres": [ + 1, + 16, + 2 + ] + }, + { + "id": 124, + "title": "Dead to Rights : Retribution", + "playlistId": "PLRfhDHeBTBJ6sm2gbS9qtdovdSGygP-3f", + "releaseDate": "2010-10-23", + "duration": "05:50:51", + "platform": 6, + "genres": [ + 1, + 16, + 2 + ] + }, + { + "id": 14, + "title": "Def Jam: Fight for NY", + "playlistId": "PLRfhDHeBTBJ63YC9KZywNsOoLCgX7EXsO", + "releaseDate": "2004-09-30", + "duration": "04:19:35", + "platform": 5, + "genres": [ + 9 + ] + }, + { + "id": 3, + "title": "Destroy All Humans! - Remake", + "playlistId": "PLRfhDHeBTBJ76V2inJIW484ZQZZyKKejr", + "releaseDate": "2020-07-28", + "duration": "04:25:44", + "platform": 1, + "genres": [ + 1, + 16 + ] + }, + { + "id": 15, + "title": "Destroy All Humans! 2", + "playlistId": "PLRfhDHeBTBJ7aGxJBcsSJXKe1l_sRAMPy", + "releaseDate": "2006-10-27", + "duration": "11:22:25", + "platform": 5, + "genres": [ + 2 + ] + }, + { + "id": 98, + "title": "Destroy All Humans! En route vers Paname !", + "playlistId": "PLRfhDHeBTBJ5PxtGm8Pwm7PNMhPVhIugR", + "releaseDate": "2009-02-13", + "duration": "06:44:54", + "platform": 6, + "genres": [ + 16, + 2 + ] + }, + { + "id": 97, + "title": "Dingo : Une Journée de Dingue !", + "videoId": "-LX3ae9m_uU", + "releaseDate": "2001-12-13", + "duration": "00:53:20", + "platform": 4, + "genres": [ + 13, + 7 + ] + }, + { + "id": 29, + "title": "Dishonored", + "playlistId": "PLRfhDHeBTBJ7aXxD52b-jsEk7WzKzsdy0", + "releaseDate": "2012-10-09", + "duration": "05:25:12", + "platform": 6, + "genres": [ + 1, + 2, + 16, + 14 + ] + }, + { + "id": 110, + "title": "DmC Devil May Cry : La Chute de Vergil", + "playlistId": "PLRfhDHeBTBJ7U7XHzOSTBvaVkkd0dOAE8", + "releaseDate": "2013-03-05", + "duration": "01:20:40", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 109, + "title": "DmC: Devil May Cry", + "playlistId": "PLRfhDHeBTBJ6KPgu5IETjWe7YLFYjRzx3", + "releaseDate": "2013-01-15", + "duration": "06:08:09", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 159, + "title": "Dr. Fetus' Mean Meat Machine", + "playlistId": "PLRfhDHeBTBJ5QMoekvhUMtxdWRNHEMAbD", + "releaseDate": "2023-06-22", + "duration": "05:28:02", + "platform": 1, + "genres": [ + 13 + ] + }, + { + "id": 86, + "title": "Dragon Ball : Advanced Adventure", + "videoId": "5ZHExfdq9F8", + "releaseDate": "2005-06-17", + "duration": "01:50:53", + "platform": 2, + "genres": [ + 1, + 12 + ] + }, + { + "id": 131, + "title": "Dragon Ball Z : Buu's Fury", + "playlistId": "PLRfhDHeBTBJ6zkB2GAtg_k6_J799AozQz", + "releaseDate": "2004-09-14", + "duration": "04:36:11", + "platform": 2, + "genres": [ + 1, + 2, + 14 + ] + }, + { + "id": 107, + "title": "Dragon Ball Z : L'Héritage de Goku 2", + "playlistId": "PLRfhDHeBTBJ73iy4EC825y_EZ4cmrneem", + "releaseDate": "2003-08-01", + "duration": "04:38:54", + "platform": 2, + "genres": [ + 1, + 2, + 14 + ] + }, + { + "id": 12, + "title": "God Of War II HD", + "playlistId": "PLRfhDHeBTBJ6zui2RaBzaZ5zjm768Jzum", + "releaseDate": "2010-11-03", + "duration": "06:20:04", + "platform": 6, + "genres": [ + 1, + 2 + ] + }, + { + "id": 125, + "title": "God of War (2018)", + "playlistId": "PLRfhDHeBTBJ4axkTXH5Ty8shlzKlR86Wm", + "releaseDate": "2022-02-14", + "duration": "17:41:07", + "platform": 1, + "genres": [ + 1, + 2, + 14 + ] + }, + { + "id": 40, + "title": "God of War Ascension", + "playlistId": "PLRfhDHeBTBJ7oa6cAMnUL_Qec1nkK6Oi6", + "releaseDate": "2013-03-13", + "duration": "06:20:52", + "platform": 6, + "genres": [ + 1, + 2 + ] + }, + { + "id": 53, + "title": "God of War I", + "playlistId": "PLRfhDHeBTBJ4zvv9b7-EaycdD6x_YtIkj", + "releaseDate": "2005-06-22", + "duration": "07:02:42", + "platform": 5, + "genres": [ + 1, + 2 + ] + }, + { + "id": 148, + "title": "God of War I HD", + "playlistId": "PLRfhDHeBTBJ5EYj5y6ByrLTuhqyKV4dpc", + "releaseDate": "2010-11-03", + "duration": "06:48:04", + "platform": 6, + "genres": [ + 1, + 2 + ] + }, + { + "id": 41, + "title": "God of War III", + "playlistId": "PLRfhDHeBTBJ7FqF_Z2hOQZE4JP7CmvSdO", + "releaseDate": "2010-03-17", + "duration": "06:46:19", + "platform": 6, + "genres": [ + 1, + 2 + ] + }, + { + "id": 51, + "title": "God of War: Chains of Olympus", + "playlistId": "PLRfhDHeBTBJ6aLzxXL6JLStPjrxFH7nkf", + "releaseDate": "2008-03-28", + "duration": "03:34:18", + "platform": 3, + "genres": [ + 1, + 2 + ] + }, + { + "id": 147, + "title": "God of War: Chains of Olympus HD", + "playlistId": "PLRfhDHeBTBJ5BASLY5Kgof-1fgRxNVkOX", + "releaseDate": "2011-09-13", + "duration": "03:20:24", + "platform": 6, + "genres": [ + 1, + 2 + ] + }, + { + "id": 52, + "title": "God of War: Ghost of Sparta", + "playlistId": "PLRfhDHeBTBJ7VH9EyPcKvQSCo6FbMUE6g", + "releaseDate": "2010-11-03", + "duration": "04:53:25", + "platform": 3, + "genres": [ + 1, + 2 + ] + }, + { + "id": 150, + "title": "God of War: Ghost of Sparta HD", + "playlistId": "PLRfhDHeBTBJ7TJ1MBUMRg588JQd9p-ub9", + "releaseDate": "2011-09-14", + "duration": "04:30:40", + "platform": 6, + "genres": [ + 1, + 2 + ] + }, + { + "id": 5, + "title": "Guns Gore & Cannoli", + "playlistId": "PLRfhDHeBTBJ6oiHK7lfTbOQlMWVHecx2I", + "releaseDate": "2015-04-30", + "duration": "02:10:05", + "platform": 1, + "genres": [ + 1, + 16, + 3, + 10 + ] + }, + { + "id": 43, + "title": "Guns Gore and Cannoli 2", + "playlistId": "PLRfhDHeBTBJ4ivg1J2leFzeuABXobVFNO", + "releaseDate": "2018-03-03", + "duration": "02:31:53", + "platform": 1, + "genres": [ + 1, + 16, + 3, + 10 + ] + }, + { + "id": 120, + "title": "Harry Potter et la Chambre des Secrets", + "playlistId": "PLRfhDHeBTBJ4zV33TJt9y--cp84eD7ivO", + "releaseDate": "2002-11-14", + "duration": "04:06:59", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 95, + "title": "Harry Potter et la Coupe de Feu", + "playlistId": "PLRfhDHeBTBJ5nRIWD5BCLi7NaQCYc6PpF", + "releaseDate": "2005-11-10", + "duration": "02:17:52", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 123, + "title": "Harry Potter et le Prisonnier d'Azkaban", + "videoId": "IOog6QKck6o", + "releaseDate": "2004-06-04", + "duration": "03:01:32", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 119, + "title": "Harry Potter à l'Ecole des Sorciers", + "playlistId": "PLRfhDHeBTBJ6Dbl3hku6SlyybSN86clQ7", + "releaseDate": "2001-11-16", + "duration": "03:36:33", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 84, + "title": "Hercule", + "videoId": "Z-hI2U2g_j0", + "releaseDate": "1997-07-01", + "duration": "01:08:37", + "platform": 4, + "genres": [ + 1, + 12 + ] + }, + { + "id": 88, + "title": "Hot Pixel", + "videoId": "6UzzEQjYA9c", + "releaseDate": "2008-06-22", + "duration": "00:27:38", + "platform": 3, + "genres": [ + 13 + ] + }, + { + "id": 105, + "title": "Jak 3", + "playlistId": "PLRfhDHeBTBJ5KZm9-LWr5C-Iet5dyGGSc", + "releaseDate": "2004-12-01", + "duration": "06:27:02", + "platform": 6, + "genres": [ + 1, + 2, + 12, + 16 + ] + }, + { + "id": 103, + "title": "Jak II : Hors la Loi", + "playlistId": "PLRfhDHeBTBJ5omqALCFYK0zrIANxO8d2y", + "releaseDate": "2003-10-15", + "duration": "06:25:22", + "platform": 6, + "genres": [ + 1, + 2, + 12, + 16 + ] + }, + { + "id": 152, + "title": "Jak and Daxter: The Lost Frontier", + "playlistId": "PLRfhDHeBTBJ5yE1da-5kvYirmxBd7sLEX", + "releaseDate": "2009-11-18", + "duration": "05:16:27", + "platform": 3, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 101, + "title": "Jak and Daxter: The Precursor Legacy", + "playlistId": "PLRfhDHeBTBJ5QvrTXXZ64vl8sRngbgkDv", + "releaseDate": "2001-12-14", + "duration": "05:07:21", + "platform": 6, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 143, + "title": "Jet Li: Rise to Honour", + "playlistId": "PLRfhDHeBTBJ5K0xPqtndkObR9g05hHw5h", + "releaseDate": "2004-04-07", + "duration": "03:35:54", + "platform": 5, + "genres": [ + 1, + 16 + ] + }, + { + "id": 48, + "title": "Kirby: Cauchemar au pays des Rêves", + "playlistId": "PLRfhDHeBTBJ5g1jcrLLDl5Azwux73Edsi", + "releaseDate": "2003-09-26", + "duration": "01:38:08", + "platform": 2, + "genres": [ + 12 + ] + }, + { + "id": 144, + "title": "Kya : Dark Lineage", + "playlistId": "PLRfhDHeBTBJ54lUeJrodUPuEOohHUdCJj", + "releaseDate": "2003-11-21", + "duration": "07:34:50", + "platform": 5, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 26, + "title": "Le Parrain : Edition du Don", + "playlistId": "PLRfhDHeBTBJ4OEBaFpx7GHRdCN7tVYhLO", + "releaseDate": "2007-03-23", + "duration": "15:05:17", + "platform": 6, + "genres": [ + 1, + 2, + 16 + ] + }, + { + "id": 55, + "title": "Le parrain II", + "playlistId": "PLRfhDHeBTBJ6ho33uahEjHiIyL5nSi_3O", + "releaseDate": "2009-04-09", + "duration": "08:24:01", + "platform": 1, + "genres": [ + 1, + 2, + 16 + ] + }, + { + "id": 158, + "title": "Legend of Kay Anniversary", + "playlistId": "PLRfhDHeBTBJ4wnTcJ3vXDERleACuvRLPp", + "releaseDate": "2015-05-29", + "duration": "09:06:52", + "platform": 6, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 36, + "title": "Les Indestructibles : La Terrible Attaque du Démolisseur", + "playlistId": "PLRfhDHeBTBJ44T1V46UXupwH6GPekErCH", + "releaseDate": "2005-11-18", + "duration": "02:11:24", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 134, + "title": "Les Simpson : Le Jeu", + "playlistId": "PLRfhDHeBTBJ6aLNhmxhZdrI2dz0Bavlkh", + "releaseDate": "2007-11-15", + "duration": "04:05:46", + "platform": 6, + "genres": [ + 1, + 2, + 14 + ] + }, + { + "id": 30, + "title": "Les indestructibles Le Jeu", + "playlistId": "PLRfhDHeBTBJ6kWfGJp4xw769cajM4K9Gi", + "releaseDate": "2004-11-12", + "duration": "03:42:26", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 151, + "title": "Lucky Luke : Tous à l'Ouest", + "videoId": "kDoc1rzc-Zs", + "releaseDate": "2007-11-07", + "duration": "01:21:01", + "platform": 1, + "genres": [ + 3, + 6 + ] + }, + { + "id": 99, + "title": "Madagascar", + "playlistId": "PLRfhDHeBTBJ4ifDoD5QUnc2O-bFyHWGDn", + "releaseDate": "2005-06-17", + "duration": "03:24:49", + "platform": 5, + "genres": [ + 1, + 2 + ] + }, + { + "id": 27, + "title": "Mafia 1", + "playlistId": "PLRfhDHeBTBJ65md00lEyYxa_K8qf8mPbP", + "releaseDate": "2004-02-13", + "duration": "09:00:04", + "platform": 5, + "genres": [ + 1, + 2, + 16 + ] + }, + { + "id": 25, + "title": "Mafia II", + "playlistId": "PLRfhDHeBTBJ5KzX5CFJUfNs_Y8_Xi-dhg", + "releaseDate": "2010-08-27", + "duration": "07:56:28", + "platform": 6, + "genres": [ + 1, + 2, + 16 + ] + }, + { + "id": 1, + "title": "Mafia: Definitive Edition", + "playlistId": "PLRfhDHeBTBJ6SEXdQnTM4OHRH9mDIRocv", + "releaseDate": "2020-09-25", + "duration": "06:59:14", + "platform": 1, + "genres": [ + 1, + 2, + 16 + ] + }, + { + "id": 111, + "title": "Marc Eckō's Getting Up: Contents Under Pressure", + "playlistId": "PLRfhDHeBTBJ558YL4-DGi1B8jrQhM3uWa", + "releaseDate": "2006-06-17", + "duration": "07:26:19", + "platform": 5, + "genres": [ + 1, + 2 + ] + }, + { + "id": 45, + "title": "Max Payne 2 - The Fall of Max Payne", + "playlistId": "PLRfhDHeBTBJ4ZHSeesVGZle5F3OOA6ERf", + "releaseDate": "2003-10-24", + "duration": "03:35:54", + "platform": 1, + "genres": [ + 1, + 16 + ] + }, + { + "id": 82, + "title": "Max Payne 3", + "playlistId": "PLRfhDHeBTBJ6qxm-Yidg-RNG6ICJuvll2", + "releaseDate": "2012-05-18", + "duration": "07:01:05", + "platform": 1, + "genres": [ + 1, + 16 + ] + }, + { + "id": 46, + "title": "Max Payne I", + "playlistId": "PLRfhDHeBTBJ5LaRTqHGdoOWrQM8Uck35J", + "releaseDate": "2001-08-01", + "duration": "04:10:57", + "platform": 1, + "genres": [ + 1, + 16 + ] + }, + { + "id": 142, + "title": "Metal Slug XX", + "videoId": "0vTdVPo9I_4", + "releaseDate": "2010-06-25", + "duration": "01:10:06", + "platform": 3, + "genres": [ + 1 + ] + }, + { + "id": 76, + "title": "Need for Speed Carbon", + "playlistId": "PLRfhDHeBTBJ6MEuVUHAD2HZETNt5cBCZP", + "releaseDate": "2006-11-03", + "duration": "05:12:32", + "platform": 1, + "genres": [ + 1, + 15, + 3 + ] + }, + { + "id": 47, + "title": "Pokémon Uranium", + "playlistId": "PLRfhDHeBTBJ6nITizlIWkG5aiqeF8iJeh", + "releaseDate": "2016-08-06", + "duration": "19:13:50", + "platform": 1, + "genres": [ + 2, + 14 + ] + }, + { + "id": 68, + "title": "Prince of Persia 2008", + "playlistId": "PLRfhDHeBTBJ4H78MI_RvItygKp29dg0bc", + "releaseDate": "2008-12-04", + "duration": "10:18:10", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 24, + "title": "Prince of Persia : L'Ame du guerrier", + "playlistId": "PLRfhDHeBTBJ5FBZQLKtgH6QsC536I4ggr", + "releaseDate": "2004-12-02", + "duration": "05:52:06", + "platform": 6, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 11, + "title": "Prince of Persia : Les Deux Royaumes", + "playlistId": "PLRfhDHeBTBJ6ptBn3Rkq74O50hly24Gqg", + "releaseDate": "2005-12-01", + "duration": "04:39:35", + "platform": 6, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 10, + "title": "Prince of Persia : Les Sables du temps", + "playlistId": "PLRfhDHeBTBJ53K1nOu8IEXqZltsLtffGO", + "releaseDate": "2003-11-13", + "duration": "05:01:34", + "platform": 6, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 89, + "title": "Prince of Persia : Les sables oubliés", + "videoId": "lYIH7XkENSw", + "releaseDate": "2010-04-20", + "duration": "03:23:14", + "platform": 3, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 61, + "title": "Prince of Persia : Les sables oubliés", + "playlistId": "PLRfhDHeBTBJ7EmZWmBJjwVfrNo4c2OGnb", + "releaseDate": "2010-04-20", + "duration": "05:16:11", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 13, + "title": "Prince of Persia Epilogue", + "playlistId": "PLRfhDHeBTBJ4hCgXVdIqLpBkTKS9q1JN6", + "releaseDate": "2009-03-05", + "duration": "01:24:13", + "platform": 6, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 63, + "title": "Psychonauts", + "playlistId": "PLRfhDHeBTBJ6NqC2O9ojqqUv91ARBqUkl", + "releaseDate": "2006-02-03", + "duration": "13:35:45", + "platform": 1, + "genres": [ + 1, + 12 + ] + }, + { + "id": 112, + "title": "Psychonauts 2", + "playlistId": "PLRfhDHeBTBJ4PRXxtpRjP_o3PUzSQ0-nQ", + "releaseDate": "2021-08-25", + "duration": "10:59:44", + "platform": 1, + "genres": [ + 1, + 12 + ] + }, + { + "id": 96, + "title": "Pumpkin Jack", + "playlistId": "PLRfhDHeBTBJ4Aam6TaY3Z22rby8dr0TR6", + "releaseDate": "2020-10-23", + "duration": "03:33:36", + "platform": 1, + "genres": [ + 12, + 13 + ] + }, + { + "id": 140, + "title": "Pursuit Force", + "playlistId": "PLRfhDHeBTBJ47XmS2_upaFjIh_wKhN0XB", + "releaseDate": "2005-11-23", + "duration": "03:06:59", + "platform": 3, + "genres": [ + 1, + 15 + ] + }, + { + "id": 146, + "title": "Pursuit Force: Extreme Justice", + "playlistId": "PLRfhDHeBTBJ72gRyZBPf23ZfxSlG5h8Em", + "releaseDate": "2007-12-12", + "duration": "04:35:22", + "platform": 3, + "genres": [ + 1, + 15 + ] + }, + { + "id": 21, + "title": "Pyjama Sam 2 : Héros Météo", + "videoId": "We7-JHU7D7I", + "releaseDate": "1998-10-02", + "duration": "00:53:45", + "platform": 1, + "genres": [ + 2, + 7 + ] + }, + { + "id": 22, + "title": "Pyjama Sam : Héros du Goûter", + "videoId": "Qsq_q65MwhU", + "releaseDate": "2000-11-22", + "duration": "01:07:54", + "platform": 1, + "genres": [ + 2, + 7 + ] + }, + { + "id": 77, + "title": "Quantum Break", + "playlistId": "PLRfhDHeBTBJ5kA7HOY4M_5Ki0we95CW_f", + "releaseDate": "2016-04-05", + "duration": "05:29:07", + "platform": 1, + "genres": [ + 1, + 2, + 16 + ] + }, + { + "id": 38, + "title": "Ratchet & Clank", + "playlistId": "PLRfhDHeBTBJ56NplVu4F1-uQm5bq7jwvo", + "releaseDate": "2002-11-08", + "duration": "07:03:58", + "platform": 6, + "genres": [ + 1, + 2, + 12, + 16 + ] + }, + { + "id": 37, + "title": "Ratchet & Clank 2", + "playlistId": "PLRfhDHeBTBJ5R8zWiWBkY-DBMU2AFb9mu", + "releaseDate": "2003-11-19", + "duration": "07:48:33", + "platform": 6, + "genres": [ + 1, + 2, + 12, + 16 + ] + }, + { + "id": 35, + "title": "Ratchet & Clank 3", + "playlistId": "PLRfhDHeBTBJ7MKXXCaweIjEpKNA-qtQvu", + "releaseDate": "2004-11-17", + "duration": "06:55:09", + "platform": 6, + "genres": [ + 1, + 2, + 12, + 16 + ] + }, + { + "id": 114, + "title": "Ratchet & Clank : A Crack in Time", + "playlistId": "PLRfhDHeBTBJ5ysRbqhFaSSapvvuEVOu7M", + "releaseDate": "2009-11-04", + "duration": "05:45:00", + "platform": 6, + "genres": [ + 1, + 2, + 12, + 16 + ] + }, + { + "id": 33, + "title": "Ratchet & Clank : La Taille, Ca Compte", + "playlistId": "PLRfhDHeBTBJ5J7kgKwUQGOnru8DZhKi6s", + "releaseDate": "2007-05-16", + "duration": "04:05:03", + "platform": 3, + "genres": [ + 1, + 2, + 12, + 16 + ] + }, + { + "id": 94, + "title": "Ratchet & Clank : Opération Destruction", + "playlistId": "PLRfhDHeBTBJ4uA4ArliLWDLuJgaykBcIY", + "releaseDate": "2007-11-14", + "duration": "06:49:19", + "platform": 6, + "genres": [ + 12, + 16, + 2, + 1 + ] + }, + { + "id": 39, + "title": "Ratchet : Gladiator", + "playlistId": "PLRfhDHeBTBJ7AoUspxANPSVOxoKEC0hRA", + "releaseDate": "2005-11-23", + "duration": "06:07:43", + "platform": 6, + "genres": [ + 1, + 2, + 12, + 16 + ] + }, + { + "id": 73, + "title": "Rayman 2 : Revolution", + "playlistId": "PLRfhDHeBTBJ5iBOO8LgF8L_AvxX91jbaX", + "releaseDate": "2000-12-14", + "duration": "04:59:15", + "platform": 5, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 60, + "title": "Rayman 2: The Great Escape", + "playlistId": "PLRfhDHeBTBJ7O8QVtJw10sNcDen6yARFm", + "releaseDate": "2000-09-07", + "duration": "04:18:06", + "platform": 4, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 116, + "title": "Rayman 3 : Hoodlum Havoc HD", + "playlistId": "PLRfhDHeBTBJ7bQV45KKQ7Japc7Z9uLvgJ", + "releaseDate": "2012-03-21", + "duration": "05:27:54", + "platform": 6, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 136, + "title": "Rayman Legends", + "playlistId": "PLRfhDHeBTBJ4GoSaMs-BqHumiPqUHU3OX", + "releaseDate": "2013-08-29", + "duration": "04:41:01", + "platform": 6, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 92, + "title": "Rayman Origins", + "playlistId": "PLRfhDHeBTBJ703-Pyf2BZmJC0dM8Uu4Wc", + "releaseDate": "2011-11-24", + "duration": "04:40:15", + "platform": 1, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 75, + "title": "Rayman contre les Lapins Crétins", + "playlistId": "PLRfhDHeBTBJ7zlVdm21oc8ndiRGxQJy6F", + "releaseDate": "2006-11-30", + "duration": "03:05:12", + "platform": 1, + "genres": [ + 1, + 2, + 8 + ] + }, + { + "id": 23, + "title": "Remember Me", + "playlistId": "PLRfhDHeBTBJ4AFRA0rUuICqeb7QJf8-jO", + "releaseDate": "2013-06-07", + "duration": "06:41:04", + "platform": 6, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 157, + "title": "Robots", + "playlistId": "PLRfhDHeBTBJ56jE5Kb3Wb6vBZZKLgM0dR", + "releaseDate": "2005-04-01", + "duration": "03:28:59", + "platform": 5, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 71, + "title": "Ryse : Son of Rome", + "playlistId": "PLRfhDHeBTBJ6cs6uc2X0dzBvaNuF-n49i", + "releaseDate": "2013-11-22", + "duration": "04:52:11", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 64, + "title": "Saints Row Gat out of Hell", + "playlistId": "PLRfhDHeBTBJ6qA11nZAq1HtkkuE98F0iO", + "releaseDate": "2015-01-23", + "duration": "05:45:04", + "platform": 1, + "genres": [ + 1, + 16 + ] + }, + { + "id": 62, + "title": "Saints Row IV", + "playlistId": "PLRfhDHeBTBJ4QdQseb6rQyYe_lybyI5LB", + "releaseDate": "2013-08-23", + "duration": "20:55:30", + "platform": 1, + "genres": [ + 1, + 16 + ] + }, + { + "id": 66, + "title": "Saints Row The Third", + "playlistId": "PLRfhDHeBTBJ5BcOJy0BEPvdPbZSfi1P1b", + "releaseDate": "2011-11-15", + "duration": "13:53:03", + "platform": 1, + "genres": [ + 1, + 16 + ] + }, + { + "id": 20, + "title": "Sam Pyjam : Héros de la Nuit", + "videoId": "qaIADIQyr0A", + "releaseDate": "1996-10-04", + "duration": "00:44:11", + "platform": 1, + "genres": [ + 2, + 7 + ] + }, + { + "id": 2, + "title": "Samurai Jack: Battle Through Time", + "playlistId": "PLRfhDHeBTBJ5PoDOIMqIEoWuC-woSa4iC", + "releaseDate": "2020-08-21", + "duration": "03:51:35", + "platform": 1, + "genres": [ + 1, + 2, + 9 + ] + }, + { + "id": 32, + "title": "Secret Agent Clank", + "playlistId": "PLRfhDHeBTBJ70A5fWB1qpafjMvJ2w1IxY", + "releaseDate": "2008-07-11", + "duration": "06:22:48", + "platform": 3, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 19, + "title": "Shrek 2 Le Jeu", + "playlistId": "PLRfhDHeBTBJ53mxvj3I9ZwNfn38vQSI3j", + "releaseDate": "2004-06-18", + "duration": "02:24:35", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 90, + "title": "Shrek Super Slam", + "videoId": "AVwWGO0Zocs", + "releaseDate": "2005-11-18", + "duration": "00:31:13", + "platform": 5, + "genres": [ + 9 + ] + }, + { + "id": 83, + "title": "Sigi - Un Pet Pour Mélusina", + "videoId": "odnIJiLjqCY", + "releaseDate": "2017-12-22", + "duration": "00:26:00", + "platform": 1, + "genres": [ + 1, + 12 + ] + }, + { + "id": 74, + "title": "Sleeping Dogs", + "playlistId": "PLRfhDHeBTBJ70QO5aD5fbYmlbSdvZdmQO", + "releaseDate": "2012-08-17", + "duration": "07:22:10", + "platform": 1, + "genres": [ + 1, + 2, + 16 + ] + }, + { + "id": 121, + "title": "Sonic Adventure 2", + "videoId": "a0mr0A8VM9A", + "releaseDate": "2012-11-19", + "duration": "03:50:39", + "platform": 1, + "genres": [ + 1, + 2, + 12, + 14 + ] + }, + { + "id": 118, + "title": "Sonic Adventure DX: Director's Cut", + "playlistId": "PLRfhDHeBTBJ5YBw47XwARehnyP6bhSpuJ", + "releaseDate": "2004-02-06", + "duration": "06:25:57", + "platform": 1, + "genres": [ + 1, + 2, + 12, + 14 + ] + }, + { + "id": 34, + "title": "Sonic Heroes", + "playlistId": "PLRfhDHeBTBJ5bAuo9zjPUGnXfzu9Py5Nx", + "releaseDate": "2004-02-06", + "duration": "02:55:04", + "platform": 5, + "genres": [ + 3, + 12, + 15 + ] + }, + { + "id": 67, + "title": "South Park - Le baton de la vérité", + "playlistId": "PLRfhDHeBTBJ6PP1XZO-tM70wPQyPBBGmT", + "releaseDate": "2014-03-06", + "duration": "09:16:21", + "platform": 1, + "genres": [ + 2, + 14 + ] + }, + { + "id": 72, + "title": "South Park : L'Annale du Destin", + "playlistId": "PLRfhDHeBTBJ4WXhWDhZCsIp3k6JxR7HpU", + "releaseDate": "2013-11-22", + "duration": "12:25:40", + "platform": 1, + "genres": [ + 2, + 14 + ] + }, + { + "id": 132, + "title": "Space Invaders Extreme", + "videoId": "6wJFMcIHi8k", + "releaseDate": "2008-07-03", + "duration": "00:30:04", + "platform": 3, + "genres": [ + 1, + 16 + ] + }, + { + "id": 49, + "title": "Splinter Cell", + "playlistId": "PLRfhDHeBTBJ6hq_3hyZpacsJt6ZUJPTTN", + "releaseDate": "2002-11-28", + "duration": "04:34:58", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 9, + "title": "Splinter Cell Blacklist", + "playlistId": "PLRfhDHeBTBJ7JOqVieOgK3RrNvDRMC5cV", + "releaseDate": "2013-08-22", + "duration": "06:34:25", + "platform": 1, + "genres": [ + 1 + ] + }, + { + "id": 102, + "title": "Splinter Cell Chaos Theory HD", + "playlistId": "PLRfhDHeBTBJ7gViU4fNr0LhQvSAWk55T2", + "releaseDate": "2011-08-10", + "duration": "05:22:51", + "platform": 6, + "genres": [ + 1, + 2 + ] + }, + { + "id": 78, + "title": "Splinter Cell Conviction", + "playlistId": "PLRfhDHeBTBJ48YBgwV9lobG0Ddo4EN9I3", + "releaseDate": "2010-04-15", + "duration": "04:02:12", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 59, + "title": "Splinter Cell Double Agent", + "playlistId": "PLRfhDHeBTBJ6KR3qS-DYwgEsMsP3hJmoU", + "releaseDate": "2006-10-19", + "duration": "06:45:13", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 154, + "title": "Splinter Cell HD", + "playlistId": "PLRfhDHeBTBJ7Do88fiav23gsXrV-0SHn-", + "releaseDate": "2011-08-10", + "duration": "04:40:45", + "platform": 6, + "genres": [ + 1, + 2 + ] + }, + { + "id": 104, + "title": "Splinter Cell: Pandora Tomorrow", + "playlistId": "PLRfhDHeBTBJ7xvQe1Jbs8Kv9ie_uQhdJH", + "releaseDate": "2004-03-25", + "duration": "03:11:52", + "platform": 6, + "genres": [ + 1, + 2 + ] + }, + { + "id": 57, + "title": "Spy Fox : Operation Ozone", + "playlistId": "PLRfhDHeBTBJ5VLwcplFFMaHjd5eKC4p2A", + "releaseDate": "2001-03-30", + "duration": "01:09:17", + "platform": 1, + "genres": [ + 1, + 2, + 7, + 6 + ] + }, + { + "id": 58, + "title": "Spy Fox : Opération Robot Expo", + "playlistId": "PLRfhDHeBTBJ4AAsZf91kxWWrQEKHae4Wx", + "releaseDate": "2000-11-20", + "duration": "01:12:15", + "platform": 1, + "genres": [ + 1, + 2, + 7, + 6 + ] + }, + { + "id": 69, + "title": "Spy fox : Opération Milkshake", + "playlistId": "PLRfhDHeBTBJ42kEksYsYPnAHmS_7V8525", + "releaseDate": "1997-09-02", + "duration": "01:17:20", + "platform": 1, + "genres": [ + 1, + 2, + 7, + 6 + ] + }, + { + "id": 141, + "title": "Stranglehold", + "playlistId": "PLRfhDHeBTBJ7dIG6eD6CYJh8xu4j_eXyq", + "releaseDate": "2007-09-14", + "duration": "04:00:42", + "platform": 1, + "genres": [ + 1, + 16 + ] + }, + { + "id": 28, + "title": "TMNT 2007 (GBA)", + "playlistId": "PLRfhDHeBTBJ511Df7N0KGWuGY0O6nlmNU", + "releaseDate": "2007-03-29", + "duration": "01:10:46", + "platform": 2, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 91, + "title": "The Legend of Zelda: The Minish Cap", + "playlistId": "PLRfhDHeBTBJ41LQiFCR00rhaLAyMNy7cR", + "releaseDate": "2004-11-12", + "duration": "06:10:03", + "platform": 2, + "genres": [ + 1, + 2, + 12 + ] + }, + { + "id": 129, + "title": "The Punisher", + "playlistId": "PLRfhDHeBTBJ4ZxkrGCk7a8C-6jXEj4HoT", + "releaseDate": "2005-03-11", + "duration": "05:36:21", + "platform": 1, + "genres": [ + 1, + 16, + 2 + ] + }, + { + "id": 113, + "title": "The Simpsons Hit & Run", + "playlistId": "PLRfhDHeBTBJ4Z99FBhfpJO6T_pe5HymRV", + "releaseDate": "2003-09-16", + "duration": "04:04:20", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 100, + "title": "Titeuf : Méga-Compet'", + "playlistId": "PLRfhDHeBTBJ54gjV7GzpbGumgw64u7LVP", + "releaseDate": "2004-09-24", + "duration": "01:26:00", + "platform": 5, + "genres": [ + 6, + 13 + ] + }, + { + "id": 54, + "title": "Total Overdose", + "playlistId": "PLRfhDHeBTBJ6JUtolPUXj2YXNHLOMXbij", + "releaseDate": "2005-09-16", + "duration": "06:54:30", + "platform": 1, + "genres": [ + 1, + 2, + 16 + ] + }, + { + "id": 135, + "title": "Worms Ultimate Mayhem", + "videoId": "Drz_YdmE-yQ", + "releaseDate": "2011-09-28", + "duration": "03:43:44", + "platform": 6, + "genres": [ + 1, + 19 + ] + }, + { + "id": 133, + "title": "X-Men Origins : Wolverine", + "playlistId": "PLRfhDHeBTBJ6FOWzD8YONj2LoxCxPNJzO", + "releaseDate": "2009-04-30", + "duration": "06:43:09", + "platform": 1, + "genres": [ + 1, + 2 + ] + }, + { + "id": 65, + "title": "XIII - le jeu", + "playlistId": "PLRfhDHeBTBJ67wrk9BvDqa4pGRwq4jO-8", + "releaseDate": "2003-11-20", + "duration": "04:34:35", + "platform": 1, + "genres": [ + 1, + 2, + 16 + ] + }, + { + "id": 137, + "title": "Yu-Gi-Oh! Les Cartes Sacrées", + "videoId": "NZiAc-mIJuQ", + "releaseDate": "2004-01-30", + "duration": "02:57:48", + "platform": 2, + "genres": [ + 14, + 5 + ] + }, + { + "id": 139, + "title": "Yu-Gi-Oh! Reshef le Destructeur", + "videoId": "1vvwPVwbACY", + "releaseDate": "2004-09-10", + "duration": "05:04:19", + "platform": 2, + "genres": [ + 14, + 5 + ] + }, + { + "id": 108, + "title": "Zapper : Le Criquet Ravageur !", + "playlistId": "PLRfhDHeBTBJ63WnP22tTxS7SMXx_SyMDT", + "releaseDate": "2003-03-07", + "duration": "02:45:50", + "platform": 5, + "genres": [ + 1, + 2, + 12 + ] + } +] \ No newline at end of file diff --git a/src/app/api/games/route.ts b/src/app/api/games/route.ts index e9131c2f..0229beb7 100644 --- a/src/app/api/games/route.ts +++ b/src/app/api/games/route.ts @@ -3,12 +3,10 @@ import Fuse from 'fuse.js' import type { BasicGame, - EnhancedGame, + CardGame, BasicVideo, BasicPlaylist, YTUrlType, - Platform, - Genre } from "@/redux/sharedDefintion"; // Extract criteria from request into something useful for me @@ -24,24 +22,15 @@ const SIZES = [ // Types type gamesFilters = { - platform?: Platform, + platform?: number, title?: string, - genres?: Genre[] + genres?: number[] } -const sortCriterias = ["name", "releaseDate", "duration"] as const; -type sortCriteria = typeof sortCriterias[number]; -type gamesSorters = [ - sortCriteria, - "ASC" | "DESC" -][]; - // Request parameters type RequestParams = { // filter criteria filters: gamesFilters, - // sort results - sorters: gamesSorters, // page size // If equal to -1, it means full result pageSize: number @@ -49,18 +38,13 @@ type RequestParams = { page: number // include previous page result ? includePreviousPagesResult: boolean - // From which date produces the resultset ? - // Format : "YYYYMMDD" , example "20240520" - dateAsInteger?: number } export type ResponseBody = { // the games we are looking for - items: EnhancedGame[], + items: CardGame[], // Filter criteria used filters: gamesFilters, - // sort results used - sorters: gamesSorters, // Number of result matching criteria total_items: number, // Number of page available @@ -103,11 +87,6 @@ function generateResponse(params : RequestParams, gamesData: RawPayload): Respon const filtered_games = gamesData .filter(game => { - // hide not yet public games on channel - if (game?.availableAt !== undefined && params.dateAsInteger !== undefined && game?.availableAt > params?.dateAsInteger) { - return false; - } - // hide not matching platforms if (params.filters.platform !== undefined && game.platform !== params.filters.platform) { return false; @@ -132,13 +111,12 @@ function generateResponse(params : RequestParams, gamesData: RawPayload): Respon total_pages: Math.ceil(results.length / params.pageSize), page: params.page, pageSize: params.pageSize, - filters: params.filters, - sorters: params.sorters + filters: params.filters } } // Return subset and sorted resultset -function sortedAndFilteredResultset(params : RequestParams, games: RawPayload) : EnhancedGame[] { +function sortedAndFilteredResultset(params : RequestParams, games: RawPayload) : CardGame[] { // Bound for result const [startOffset, endOffset] = (params.includePreviousPagesResult) @@ -146,55 +124,9 @@ function sortedAndFilteredResultset(params : RequestParams, games: RawPayload) : : [ (params.pageSize - 1) * params.page, params.pageSize * params.page]; // No sort criteria, return the filtered list only - if (params.sorters.length === 0) { - return ((params.pageSize === -1) ? games : games.slice(startOffset, endOffset)).map(enhanceGameItem); - } - - // At least one criteria for sort - const gamesData = games - .map(enhanceGameItem) - .sort(sortFunction(params)); - - // filtered resultset ? - return (params.pageSize === -1) ? gamesData : gamesData.slice(startOffset, endOffset); + return ((params.pageSize === -1) ? games : games.slice(startOffset, endOffset)).map(enhanceGameItem); } -function sortFunction(params : RequestParams) { - return (a : EnhancedGame, b : EnhancedGame) => { - for(let [field, order] of params.sorters) { - - let comparatorResult = 0; - - switch(field) { - case "releaseDate": - comparatorResult = (order === "ASC") - ? sortByReleaseDateASC(a.releaseDate, b.releaseDate) - : -sortByReleaseDateASC(a.releaseDate, b.releaseDate) - break; - case "duration": - comparatorResult = (order === "ASC") - ? sortByDurationASC(a.durationAsInt, b.durationAsInt) - : -sortByDurationASC(a.durationAsInt, b.durationAsInt) - break; - default: - comparatorResult = (order === "ASC") - ? sortByNameASC(a.title, b.title) - : -sortByNameASC(a.title, b.title) - } - - if (comparatorResult !== 0) { - return comparatorResult; - } - } - return 0; - } -} - -// Sort function -const sortByNameASC = (a : string, b : string) => new Intl.Collator().compare(a, b); -const sortByDurationASC = (a : number, b : number) => (a < b) ? -1 : (a > b ? 1 : 0); -const sortByReleaseDateASC = (a : number, b : number) => (a < b) ? -1 : (a > b ? 1 : 0); - // Convert input parameters to my structures function extractParameters(params: URLSearchParams): RequestParams { @@ -203,47 +135,29 @@ function extractParameters(params: URLSearchParams): RequestParams { // 1. platform if (params.has("selected_platform")) { - filters["platform"] = params.get("selected_platform")! as any + filters["platform"] = parseInt(params.get("selected_platform") as string); } // 2. title if (params.has("selected_title")) { - filters["title"] = params.get("selected_title")! + filters["title"] = params.get("selected_title") as string; } // 3. genres if (params.has("selected_genres")) { - filters["genres"] = params.getAll("selected_genres") as Genre[] + filters["genres"] = params.getAll("selected_genres").map(parseInt); } - // sorters - let sortCriteria = params.getAll("sortCriteria"); - let sortOrders = params.getAll("sortOrder"); - - let sorters : gamesSorters = sortCriteria - .filter(criteria => sortCriterias.includes(criteria as any)) - .map( (criteria, index) => { - - let order = (index < sortOrders.length) ? sortOrders[index] : undefined; - - return [ - criteria as sortCriteria, - (order === "ASC" || order === "DESC") ? order : "ASC" - ] - }); - return { page: parseInt(params.get("page") || "1"), pageSize: parseInt(params.get("pageSize") || "16"), - dateAsInteger: parseInt(params.get("dateAsInteger") || "0"), filters: filters, - sorters: sorters, includePreviousPagesResult: (params.has("includePreviousPagesResult")) ? !!params.get("includePreviousPagesResult") : false } } // Return an enhanced payload for a single game -function enhanceGameItem(game: rawEntry): EnhancedGame { +function enhanceGameItem(game: rawEntry): CardGame { const id = (game as BasicPlaylist).playlistId ?? (game as BasicVideo).videoId; const base_url = ( @@ -256,13 +170,7 @@ function enhanceGameItem(game: rawEntry): EnhancedGame { id, imagePath: `/covers/${id}/${ game?.coverFile ?? "cover.webp" }`, sizes: SIZES.join(", "), - releaseDate: game.releaseDate - .split("/") - .reduce( (acc : number, curr : string, idx : number) => acc + (parseInt(curr) * Math.pow(100, idx)), 0), url: base_url, - url_type: ("playlistId" in game) ? "PLAYLIST" : "VIDEO" as YTUrlType, - durationAsInt: (game.duration) - ? Number(game.duration.replaceAll(":", "")) - : 0 + url_type: ("playlistId" in game) ? "PLAYLIST" : "VIDEO" as YTUrlType }); } \ No newline at end of file diff --git a/src/app/api/genres/genres.json b/src/app/api/genres/genres.json new file mode 100644 index 00000000..c293b62f --- /dev/null +++ b/src/app/api/genres/genres.json @@ -0,0 +1,82 @@ +[ + { + "id": 1, + "name": "Action" + }, + { + "id": 2, + "name": "Adventure" + }, + { + "id": 3, + "name": "Arcade" + }, + { + "id": 4, + "name": "Board Games" + }, + { + "id": 5, + "name": "Card" + }, + { + "id": 6, + "name": "Casual" + }, + { + "id": 7, + "name": "Educational" + }, + { + "id": 8, + "name": "Family" + }, + { + "id": 9, + "name": "Fighting" + }, + { + "id": 10, + "name": "Indie" + }, + { + "id": 11, + "name": "MMORPG" + }, + { + "id": 12, + "name": "Platformer" + }, + { + "id": 13, + "name": "Puzzle" + }, + { + "id": 14, + "name": "RPG" + }, + { + "id": 15, + "name": "Racing" + }, + { + "id": 16, + "name": "Shooter" + }, + { + "id": 17, + "name": "Simulation" + }, + { + "id": 18, + "name": "Sports" + }, + { + "id": 19, + "name": "Strategy" + }, + { + "id": 20, + "name": "Misc" + } +] \ No newline at end of file diff --git a/src/app/api/genres/route.ts b/src/app/api/genres/route.ts new file mode 100644 index 00000000..ef149e6a --- /dev/null +++ b/src/app/api/genres/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from "next/server"; + +export type Genre = { + // identifier + id: number; + // untraslated name + name: string; +} +export type GenreResponse = Genre[]; + +export async function GET() { + + const genresData = (await import("./genres.json")).default; + + return NextResponse.json(genresData, { + headers: { + "Cache-Control": "public, max-age=86400, must-revalidate" + } + }); +} \ No newline at end of file diff --git a/src/app/api/planning/planning.json b/src/app/api/planning/planning.json new file mode 100644 index 00000000..2b966f94 --- /dev/null +++ b/src/app/api/planning/planning.json @@ -0,0 +1,232 @@ +[ + { + "id": 158, + "availableAt": "2024-07-22", + "endAt": "2024-08-26", + "playlistId": "PLRfhDHeBTBJ4wnTcJ3vXDERleACuvRLPp", + "title": "Legend of Kay Anniversary", + "releaseDate": "2015-05-29", + "duration": "09:06:52", + "platform": 6 + }, + { + "id": 160, + "availableAt": "2024-09-02", + "endAt": "2024-09-23", + "playlistId": "PLRfhDHeBTBJ7TcJC0sKdnIpnebpXtemRP", + "title": "Dante's Inferno", + "releaseDate": "2010-10-04", + "duration": "05:48:59", + "platform": 6 + }, + { + "id": 161, + "availableAt": "2024-09-27", + "endAt": "2024-10-11", + "playlistId": "PLRfhDHeBTBJ7-G7QCx-nB3Oc-TNmIaXMk", + "title": "Lollipop Chainsaw", + "releaseDate": "2012-06-12", + "duration": "04:26:45", + "platform": 6 + }, + { + "id": 162, + "availableAt": "2024-10-15", + "endAt": "2024-10-21", + "playlistId": "PLRfhDHeBTBJ4cSitG0GgcyOxbL54oqvgo", + "title": "Ratchet & Clank : Quest for Booty", + "releaseDate": "2008-08-21", + "duration": "01:48:22", + "platform": 6 + }, + { + "id": 163, + "availableAt": "2024-10-23", + "endAt": "2024-11-04", + "playlistId": "PLRfhDHeBTBJ5l6LcIoNR1rUTAaGp8WPDf", + "title": "Ratchet & Clank Nexus", + "releaseDate": "2008-08-21", + "duration": "03:39:10", + "platform": 6 + }, + { + "id": 164, + "availableAt": "2024-11-06", + "endAt": "2024-11-20", + "playlistId": "PLRfhDHeBTBJ544INoLqRwd2WxbcFBJxz4", + "title": "La Légende de Korra", + "releaseDate": "2014-10-22", + "duration": "02:33:20", + "platform": 6 + }, + { + "id": 165, + "availableAt": "2024-11-22", + "endAt": "2024-12-02", + "playlistId": "PLRfhDHeBTBJ4ZNp1r0Rlsc2yopGcndh3x", + "title": "Sly Raccoon", + "releaseDate": "2011-12-07", + "duration": "04:05:13", + "platform": 6 + }, + { + "id": 166, + "availableAt": "2024-12-09", + "endAt": "2024-12-23", + "playlistId": "PLRfhDHeBTBJ6PHLRDp5qi_ZCfVy-Lg8OO", + "title": "DuckTales Remastered", + "releaseDate": "2013-08-14", + "duration": "02:34:47", + "platform": 6 + }, + { + "id": 167, + "availableAt": "2024-12-25", + "endAt": "2025-01-15", + "playlistId": "PLRfhDHeBTBJ4DjOEslvP3es_dby3reani", + "title": "Splinter Cell Double Agent HD", + "releaseDate": "2007-03-27", + "duration": "04:06:45", + "platform": 6 + }, + { + "id": 168, + "availableAt": "2025-01-17", + "endAt": "2025-02-17", + "playlistId": "PLRfhDHeBTBJ7Mc84qhoazY9gu0EgHPSAn", + "title": "Enslaved Odyssey to the West", + "releaseDate": "2010-10-08", + "duration": "06:29:47", + "platform": 6 + }, + { + "id": 169, + "availableAt": "2025-02-21", + "endAt": "2025-03-12", + "playlistId": "PLRfhDHeBTBJ6WKXcBeMcBP7JVfRmbc361", + "title": "Sly 2 : Association de voleurs", + "releaseDate": "2011-12-07", + "duration": "11:26:08", + "platform": 6 + }, + { + "id": 170, + "availableAt": "2025-03-17", + "endAt": "2025-03-26", + "playlistId": "PLRfhDHeBTBJ7acyVd3rXz6TsMphHkQKHG", + "title": "Batman The Enemy Within", + "releaseDate": "2017-08-08", + "duration": "08:10:21", + "platform": 1 + }, + { + "id": 171, + "availableAt": "2025-03-28", + "endAt": "2025-05-06", + "playlistId": "PLRfhDHeBTBJ4Fh7iL-9-0ZEncKHStdgO2", + "title": "Fahrenheit Indigo Prophecy", + "releaseDate": "2005-09-16", + "duration": "05:38:37", + "platform": 5 + }, + { + "id": 172, + "availableAt": "2025-05-08", + "endAt": "2025-05-23", + "playlistId": "PLRfhDHeBTBJ5hcdTpev5IgERHhgFaIDbP", + "title": "Metal Gear Rising Revengeance", + "releaseDate": "2013-02-21", + "duration": "05:59:03", + "platform": 6 + }, + { + "id": 173, + "availableAt": "2025-05-26", + "endAt": "2025-07-02", + "playlistId": "PLRfhDHeBTBJ4KQEvirhf9p1o_xuFysva8", + "title": "Ghost Rider", + "releaseDate": "2007-02-16", + "duration": "03:29:44", + "platform": 5 + }, + { + "id": 174, + "availableAt": "2025-07-07", + "endAt": "2025-07-21", + "playlistId": "PLRfhDHeBTBJ78Irx7Gb3-f07ApP4Ybvvb", + "title": "Sly 3 : Honor Among Thieves", + "releaseDate": "2011-12-07", + "duration": "09:46:28", + "platform": 6 + }, + { + "id": 175, + "availableAt": "2025-07-25", + "endAt": "2025-09-05", + "playlistId": "PLRfhDHeBTBJ5m5_crztm7tu7HwJiRpLSl", + "title": "La Planète au Trésor", + "releaseDate": "2013-02-13", + "duration": "04:54:34", + "platform": 6 + }, + { + "id": 176, + "availableAt": "2025-09-08", + "endAt": "2025-10-01", + "playlistId": "PLRfhDHeBTBJ4TZAon0Ty7Z4wLnRtb4D3t", + "title": "The Warriors", + "releaseDate": "2013-03-20", + "duration": "06:11:20", + "platform": 6 + }, + { + "id": 177, + "availableAt": "2025-10-06", + "endAt": "2025-10-29", + "playlistId": "PLRfhDHeBTBJ46OsqsAwhbnzBqCPryYeuV", + "title": "WET", + "releaseDate": "2009-09-18", + "duration": "04:12:24", + "platform": 6 + }, + { + "id": 179, + "availableAt": "2025-10-31", + "endAt": "2025-11-17", + "playlistId": "PLRfhDHeBTBJ4o4JXHAX93JXwXw_lazgqI", + "title": "Deadpool", + "releaseDate": "2013-06-28", + "duration": "05:23:02", + "platform": 1 + }, + { + "id": 180, + "availableAt": "2025-11-19", + "endAt": "2025-12-12", + "playlistId": "PLRfhDHeBTBJ55wAzjDq3URVbIluuz7NpL", + "title": "Sly Cooper : Voleurs à travers le Temps", + "releaseDate": "2013-03-27", + "duration": "08:55:44", + "platform": 6 + }, + { + "id": 181, + "availableAt": "2025-12-15", + "endAt": "2026-01-23", + "playlistId": "PLRfhDHeBTBJ4VxR6N3cG5Jaasb3ovntPQ", + "title": "Asura's Wrath", + "releaseDate": "2012-02-24", + "duration": "08:49:28", + "platform": 6 + }, + { + "id": 182, + "availableAt": "2026-01-26", + "endAt": "2026-02-16", + "playlistId": "PLRfhDHeBTBJ5o809T1bcqRrEC_uoOtT_M", + "title": "Ratchet & Clank : All 4 One", + "releaseDate": "2011-10-19", + "duration": "07:56:23", + "platform": 6 + } +] \ No newline at end of file diff --git a/src/app/api/planning/route.ts b/src/app/api/planning/route.ts index 2dc09339..feb4f6fd 100644 --- a/src/app/api/planning/route.ts +++ b/src/app/api/planning/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import type { BasicGame, BasicPlaylist, BasicVideo } from "@/redux/sharedDefintion"; +type rawEntry = Omit; export type planningEntry = Omit & { /** @description Still in progress or finished ? */ status: "RECORDED" | "PENDING"; @@ -10,22 +11,12 @@ export type planningEntry = Omit (min !== undefined && max === undefined) || (max !== undefined && elem <= max); - const games = gamesData.filter(game => should_be_displayed(dateAsInteger, game.availableAt as number | undefined, game.endAt as number | undefined)) + const games = (await import("./planning.json")).default; - return NextResponse.json(games.map(game => enhanceGameItem(game as BasicGame)), { + return NextResponse.json(games.map(game => enhanceGameItem(game)), { headers: { "Cache-Control": "public, max-age=86400, must-revalidate" } @@ -33,18 +24,17 @@ export async function GET(request: Request) { } // Turn "YYYY...MMDD" to int -function turnDateToInt(value: number | string | undefined) { +function turnDateToInt(value: string | undefined) { if (value) { - const { year, month, day } = (/(?\d{4,})(?\d{2})(?\d{2})/.exec(value.toString()) as any).groups; // TODO one day, remove that & let PlanningColumn do the job - return new Date(+year, Number(month) - 1, +day).getTime(); + return new Date(value).getTime(); } else { return undefined; } } // Return an enhanced payload for a single game -function enhanceGameItem(game: BasicGame): planningEntry { +function enhanceGameItem(game: rawEntry): planningEntry { return { id: (game as BasicPlaylist).playlistId ?? (game as BasicVideo).videoId, title: game.title, diff --git a/src/app/api/platforms/platforms.json b/src/app/api/platforms/platforms.json new file mode 100644 index 00000000..a5944eb3 --- /dev/null +++ b/src/app/api/platforms/platforms.json @@ -0,0 +1,26 @@ +[ + { + "id": 1, + "name": "PC" + }, + { + "id": 2, + "name": "GBA" + }, + { + "id": 3, + "name": "PSP" + }, + { + "id": 4, + "name": "PS1" + }, + { + "id": 5, + "name": "PS2" + }, + { + "id": 6, + "name": "PS3" + } +] \ No newline at end of file diff --git a/src/app/api/platforms/route.ts b/src/app/api/platforms/route.ts new file mode 100644 index 00000000..92956eb8 --- /dev/null +++ b/src/app/api/platforms/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from "next/server"; + +export type Platform_Entry = { + // Identifier + id: number; + // Database name, not translated by default + name: string; +} +export type PlatformsResponse = Platform_Entry[]; + +export async function GET() { + + const platformsData = (await import("./platforms.json")).default; + + return NextResponse.json(platformsData, { + headers: { + "Cache-Control": "public, max-age=86400, must-revalidate" + } + }); +} \ No newline at end of file diff --git a/src/app/api/series/route.ts b/src/app/api/series/route.ts index ac095f91..3e37a4a3 100644 --- a/src/app/api/series/route.ts +++ b/src/app/api/series/route.ts @@ -1,56 +1,30 @@ import { NextResponse } from "next/server"; -import type { BasicGame, BasicPlaylist, BasicVideo, EnhancedGame, YTUrlType } from "@/redux/sharedDefintion"; - -export type serieType = { - name: string, - items: EnhancedGame[] -}; +import type { BasicVideo, CardGame, YTUrlType } from "@/redux/sharedDefintion"; +type rawGame = Omit; type rawEntry = { /** @description Name of the series */ name: string; /** @description List of videoId or playlistId for this series */ - games: string[] + items: rawGame[] } export type RawPayload = rawEntry[]; -export async function GET(request: Request) { - - // Get query parameters - const { searchParams } = new URL(request.url); - - // Get wanted date - const dateAsInteger = parseInt( searchParams.get("dateAsInteger") || "0"); +export type serieType = { + name: string, + items: CardGame[] +}; - // Fetch games data - const gamesData = (await import("@/app/api/games/games.json")).default; - const games = generateGamesResponse(gamesData as BasicGame[], dateAsInteger) +export async function GET() { // Fetch series data const seriesData = (await import("./series.json")).default; - // Convert array to { "id": Game } - let games_dictionary = games - .reduce( (acc : {[id: string]: EnhancedGame}, game : EnhancedGame) => { - acc[game.id] = game; - return acc; - }, {}) + const series : serieType[] = seriesData.map(serie => ({ + name: serie.name, + items: fromRawGamesToCardGames(serie.items as rawGame[]) + }) ) - const sortByNameASC = (a : serieType, b : serieType) => new Intl.Collator().compare(a.name, b.name); - - let series = seriesData - .map(serie => { - return { - "name": serie.name, - "items": serie - .games - .map( (gameId) => games_dictionary[gameId]) - .filter(game => game !== undefined) - } - }) - .filter(serie => serie.items.length > 1) - .sort(sortByNameASC); - return NextResponse.json(series, { headers: { "Cache-Control": "public, max-age=86400, must-revalidate" @@ -58,22 +32,12 @@ export async function GET(request: Request) { }); } -function generateGamesResponse(gamesData : BasicGame[], integerDate : number) : EnhancedGame[]{ +function fromRawGamesToCardGames(gamesData : rawGame[]) : CardGame[]{ return gamesData - .filter(game => { - - // hide not yet public games on channel - if (game?.availableAt !== undefined && game?.availableAt > integerDate) { - return false; - } - - // Either it is a valid game - return true; - }) .map(game => { - const id = (game as BasicPlaylist).playlistId ?? (game as BasicVideo).videoId; + const id = (game as any).playlistId as string ?? (game as any).videoId as string; const base_url = ( ("playlistId" in game) ? "https://www.youtube.com/playlist?list=" @@ -83,15 +47,10 @@ function generateGamesResponse(gamesData : BasicGame[], integerDate : number) : return { ...game, id, - imagePath: `/covers/${id}/${ game?.coverFile ?? "cover.webp" }`, + genres: [], + imagePath: `/covers/${id}/cover.webp`, url: base_url, url_type: ("playlistId" in game) ? "PLAYLIST" : "VIDEO" as YTUrlType, - releaseDate: game.releaseDate - .split("/") - .reduce( (acc : number, curr : string, idx : number) => acc + (parseInt(curr) * Math.pow(100, idx)), 0), - durationAsInt: (game.duration) - ? Number(game.duration.replaceAll(":", "")) - : 0 } }); } \ No newline at end of file diff --git a/src/app/api/series/series.json b/src/app/api/series/series.json index b3e4c470..588c07b2 100644 --- a/src/app/api/series/series.json +++ b/src/app/api/series/series.json @@ -1,259 +1,863 @@ [ - { - "name": "Astérix & Obélix", - "games": [ - "PLRfhDHeBTBJ7hPe8RxhK3FTXoqdFNL0BG", - "PLRfhDHeBTBJ6lDdIcNMFKsCzWtItCEcBq", - "PLRfhDHeBTBJ6FTJ2LSrdL4MaFM1Pl5nfo", - "PLRfhDHeBTBJ5S4PtZF0WBNaPVYvNDUJun", - "PLRfhDHeBTBJ5hLNQ3UBKtImQLXkR1bChZ" - ] - }, - { - "name": "Batman", - "games": [ - "PLRfhDHeBTBJ53yF5vIimEuhh4JbBQF5l4", - "PLRfhDHeBTBJ4BQrJ2mIgTHLpgPVR2XsRb", - "PLRfhDHeBTBJ4FHOgX8bYRe8alwgNKIpYv", - "PLRfhDHeBTBJ6RBqsJ0WYOOaL5wohZTbxw", - "PLRfhDHeBTBJ56EHMyvxX6iBKldqvxJHqz", - "PLRfhDHeBTBJ6POjMkqqeCliq5jrCWagcg", - "PLRfhDHeBTBJ6_wunnzhRIaBhtF3aiuS7q", - "PLRfhDHeBTBJ7acyVd3rXz6TsMphHkQKHG" - ] - }, - { - "name": "Crash Bandicoot", - "games": [ - "PLRfhDHeBTBJ6uNb7KhMn-zU-slBk-HPbe", - "PLRfhDHeBTBJ7gvlHCEXz7XFPkHVTCZpao", - "PLRfhDHeBTBJ4s03iZ1UDggaenmv_YDqzE", - "PLRfhDHeBTBJ6AGIBxZa5zvykgQBKSHzd5" - ] - }, - { - "name": "Darksiders", - "games": [ - "PLRfhDHeBTBJ61QLZkvjtKvsYx3muPHVjs", - "PLRfhDHeBTBJ6Pp3ukQM6-F8LYtOmXmy9X" - ] - }, - { - "name": "Dead to Rights", - "games": [ - "PLRfhDHeBTBJ6sm2gbS9qtdovdSGygP-3f", - "PLRfhDHeBTBJ7y3xsVkTNRYORp_mX0ag14" - ] - }, - { - "name": "Destroy All Humans!", - "games": [ - "PLRfhDHeBTBJ7aGxJBcsSJXKe1l_sRAMPy", - "PLRfhDHeBTBJ5PxtGm8Pwm7PNMhPVhIugR", - "PLRfhDHeBTBJ76V2inJIW484ZQZZyKKejr" - ] - }, - { - "name": "Dragon Ball", - "games": [ - "5ZHExfdq9F8", - "PLRfhDHeBTBJ73iy4EC825y_EZ4cmrneem", - "PLRfhDHeBTBJ6zkB2GAtg_k6_J799AozQz" - ] - }, - { - "name": "God of War", - "games": [ - "PLRfhDHeBTBJ7oa6cAMnUL_Qec1nkK6Oi6", - "PLRfhDHeBTBJ6aLzxXL6JLStPjrxFH7nkf", - "PLRfhDHeBTBJ5BASLY5Kgof-1fgRxNVkOX", - "PLRfhDHeBTBJ4zvv9b7-EaycdD6x_YtIkj", - "PLRfhDHeBTBJ5EYj5y6ByrLTuhqyKV4dpc", - "PLRfhDHeBTBJ7VH9EyPcKvQSCo6FbMUE6g", - "PLRfhDHeBTBJ6zui2RaBzaZ5zjm768Jzum", - "PLRfhDHeBTBJ7FqF_Z2hOQZE4JP7CmvSdO", - "PLRfhDHeBTBJ4axkTXH5Ty8shlzKlR86Wm" - ] - }, - { - "name": "Guns Gore & Cannoli", - "games": [ - "PLRfhDHeBTBJ6oiHK7lfTbOQlMWVHecx2I", - "PLRfhDHeBTBJ4ivg1J2leFzeuABXobVFNO" - ] - }, - { - "name": "Harry Potter", - "games": [ - "PLRfhDHeBTBJ6Dbl3hku6SlyybSN86clQ7", - "PLRfhDHeBTBJ4zV33TJt9y--cp84eD7ivO", - "IOog6QKck6o", - "PLRfhDHeBTBJ5nRIWD5BCLi7NaQCYc6PpF" - ] - }, - { - "name": "Jak and Daxter", - "games": [ - "PLRfhDHeBTBJ5QvrTXXZ64vl8sRngbgkDv", - "PLRfhDHeBTBJ5omqALCFYK0zrIANxO8d2y", - "PLRfhDHeBTBJ5KZm9-LWr5C-Iet5dyGGSc", - "PLRfhDHeBTBJ5yE1da-5kvYirmxBd7sLEX" - ] - }, - { - "name": "Le Parrain", - "games": [ - "PLRfhDHeBTBJ4OEBaFpx7GHRdCN7tVYhLO", - "PLRfhDHeBTBJ6ho33uahEjHiIyL5nSi_3O" - ] - }, - { - "name": "Les indestructibles", - "games": [ - "PLRfhDHeBTBJ6kWfGJp4xw769cajM4K9Gi", - "PLRfhDHeBTBJ44T1V46UXupwH6GPekErCH" - ] - }, - { - "name": "Les Simpson", - "games": [ - "PLRfhDHeBTBJ4Z99FBhfpJO6T_pe5HymRV", - "PLRfhDHeBTBJ6aLNhmxhZdrI2dz0Bavlkh" - ] - }, - { - "name": "Mafia", - "games": [ - "PLRfhDHeBTBJ6SEXdQnTM4OHRH9mDIRocv", - "PLRfhDHeBTBJ65md00lEyYxa_K8qf8mPbP", - "PLRfhDHeBTBJ5KzX5CFJUfNs_Y8_Xi-dhg" - ] - }, - { - "name": "Max Payne", - "games": [ - "PLRfhDHeBTBJ5LaRTqHGdoOWrQM8Uck35J", - "PLRfhDHeBTBJ4ZHSeesVGZle5F3OOA6ERf", - "PLRfhDHeBTBJ6qxm-Yidg-RNG6ICJuvll2" - ] - }, - { - "name": "Prince of Persia", - "games": [ - "PLRfhDHeBTBJ53K1nOu8IEXqZltsLtffGO", - "lYIH7XkENSw", - "PLRfhDHeBTBJ7EmZWmBJjwVfrNo4c2OGnb", - "PLRfhDHeBTBJ5FBZQLKtgH6QsC536I4ggr", - "PLRfhDHeBTBJ6ptBn3Rkq74O50hly24Gqg", - "PLRfhDHeBTBJ4H78MI_RvItygKp29dg0bc", - "PLRfhDHeBTBJ4hCgXVdIqLpBkTKS9q1JN6" - ] - }, - { - "name": "Psychonauts", - "games": [ - "PLRfhDHeBTBJ6NqC2O9ojqqUv91ARBqUkl", - "PLRfhDHeBTBJ4PRXxtpRjP_o3PUzSQ0-nQ" - ] - }, - { - "name": "Pursuit Force", - "games": [ - "PLRfhDHeBTBJ47XmS2_upaFjIh_wKhN0XB", - "PLRfhDHeBTBJ72gRyZBPf23ZfxSlG5h8Em" - ] - }, - { - "name": "Pyjama Sam", - "games": [ - "qaIADIQyr0A", - "We7-JHU7D7I", - "Qsq_q65MwhU" - ] - }, - { - "name": "Ratchet & Clank", - "games": [ - "PLRfhDHeBTBJ56NplVu4F1-uQm5bq7jwvo", - "PLRfhDHeBTBJ5R8zWiWBkY-DBMU2AFb9mu", - "PLRfhDHeBTBJ7MKXXCaweIjEpKNA-qtQvu", - "PLRfhDHeBTBJ7AoUspxANPSVOxoKEC0hRA", - "PLRfhDHeBTBJ5J7kgKwUQGOnru8DZhKi6s", - "PLRfhDHeBTBJ70A5fWB1qpafjMvJ2w1IxY", - "PLRfhDHeBTBJ4uA4ArliLWDLuJgaykBcIY", - "PLRfhDHeBTBJ4cSitG0GgcyOxbL54oqvgo", - "PLRfhDHeBTBJ5ysRbqhFaSSapvvuEVOu7M", - "PLRfhDHeBTBJ5o809T1bcqRrEC_uoOtT_M", - "PLRfhDHeBTBJ5l6LcIoNR1rUTAaGp8WPDf" - ] - }, - { - "name": "Rayman", - "games": [ - "PLRfhDHeBTBJ7O8QVtJw10sNcDen6yARFm", - "PLRfhDHeBTBJ5iBOO8LgF8L_AvxX91jbaX", - "PLRfhDHeBTBJ7bQV45KKQ7Japc7Z9uLvgJ", - "PLRfhDHeBTBJ7zlVdm21oc8ndiRGxQJy6F", - "PLRfhDHeBTBJ703-Pyf2BZmJC0dM8Uu4Wc", - "PLRfhDHeBTBJ4GoSaMs-BqHumiPqUHU3OX" - ] - }, - { - "name": "Saints Row", - "games": [ - "PLRfhDHeBTBJ5BcOJy0BEPvdPbZSfi1P1b", - "PLRfhDHeBTBJ4QdQseb6rQyYe_lybyI5LB", - "PLRfhDHeBTBJ6qA11nZAq1HtkkuE98F0iO" - ] - }, - { - "name": "Shrek", - "games": [ - "PLRfhDHeBTBJ53mxvj3I9ZwNfn38vQSI3j", - "AVwWGO0Zocs" - ] - }, - { - "name": "Sonic", - "games": [ - "PLRfhDHeBTBJ5YBw47XwARehnyP6bhSpuJ", - "a0mr0A8VM9A", - "PLRfhDHeBTBJ5bAuo9zjPUGnXfzu9Py5Nx" - ] - }, - { - "name": "South Park", - "games": [ - "PLRfhDHeBTBJ6PP1XZO-tM70wPQyPBBGmT", - "PLRfhDHeBTBJ4WXhWDhZCsIp3k6JxR7HpU" - ] - }, - { - "name": "Splinter Cell", - "games": [ - "PLRfhDHeBTBJ7Do88fiav23gsXrV-0SHn-", - "PLRfhDHeBTBJ7xvQe1Jbs8Kv9ie_uQhdJH", - "PLRfhDHeBTBJ7gViU4fNr0LhQvSAWk55T2", - "PLRfhDHeBTBJ4DjOEslvP3es_dby3reani", - "PLRfhDHeBTBJ48YBgwV9lobG0Ddo4EN9I3", - "PLRfhDHeBTBJ7JOqVieOgK3RrNvDRMC5cV" - ] - }, - { - "name": "Spy Fox", - "games": [ - "PLRfhDHeBTBJ42kEksYsYPnAHmS_7V8525", - "PLRfhDHeBTBJ4AAsZf91kxWWrQEKHae4Wx", - "PLRfhDHeBTBJ5VLwcplFFMaHjd5eKC4p2A" - ] - }, - { - "name": "Sly Cooper", - "games": [ - "PLRfhDHeBTBJ4ZNp1r0Rlsc2yopGcndh3x", - "PLRfhDHeBTBJ6WKXcBeMcBP7JVfRmbc361", - "PLRfhDHeBTBJ78Irx7Gb3-f07ApP4Ybvvb", - "PLRfhDHeBTBJ55wAzjDq3URVbIluuz7NpL" - ] - } -] + { + "name": "Astérix & Obélix", + "items": [ + { + "id": 17, + "title": "Astérix & Obélix XXL", + "playlistId": "PLRfhDHeBTBJ7hPe8RxhK3FTXoqdFNL0BG", + "duration": "06:21:55", + "platform": 5 + }, + { + "id": 18, + "title": "Astérix & Obélix XXL 2", + "playlistId": "PLRfhDHeBTBJ6FTJ2LSrdL4MaFM1Pl5nfo", + "duration": "04:53:56", + "platform": 1 + }, + { + "id": 70, + "title": "Astérix & Obélix XXL 2 : Mission Ouifix", + "playlistId": "PLRfhDHeBTBJ6lDdIcNMFKsCzWtItCEcBq", + "duration": "04:08:28", + "platform": 3 + }, + { + "id": 130, + "title": "Astérix & Obélix : Baffez-les Tous !", + "playlistId": "PLRfhDHeBTBJ5hLNQ3UBKtImQLXkR1bChZ", + "duration": "05:43:21", + "platform": 1 + }, + { + "id": 149, + "title": "Astérix aux Jeux olympiques", + "playlistId": "PLRfhDHeBTBJ5S4PtZF0WBNaPVYvNDUJun", + "duration": "03:23:11", + "platform": 5 + } + ] + }, + { + "name": "Batman", + "items": [ + { + "id": 4, + "title": "Batman: Arkham Origins", + "playlistId": "PLRfhDHeBTBJ56EHMyvxX6iBKldqvxJHqz", + "duration": "06:48:06", + "platform": 1 + }, + { + "id": 6, + "title": "Batman: Arkham Asylum", + "playlistId": "PLRfhDHeBTBJ4FHOgX8bYRe8alwgNKIpYv", + "duration": "05:59:51", + "platform": 1 + }, + { + "id": 7, + "title": "Batman: Arkham Knight", + "playlistId": "PLRfhDHeBTBJ6POjMkqqeCliq5jrCWagcg", + "duration": "08:33:35", + "platform": 1 + }, + { + "id": 8, + "title": "Batman Arkham City", + "playlistId": "PLRfhDHeBTBJ6RBqsJ0WYOOaL5wohZTbxw", + "duration": "06:47:41", + "platform": 1 + }, + { + "id": 31, + "title": "Batman: Rise of Sin Tzu", + "playlistId": "PLRfhDHeBTBJ4BQrJ2mIgTHLpgPVR2XsRb", + "duration": "04:25:24", + "platform": 5 + }, + { + "id": 117, + "title": "Batman Vengeance", + "playlistId": "PLRfhDHeBTBJ53yF5vIimEuhh4JbBQF5l4", + "duration": "02:32:02", + "platform": 5 + }, + { + "id": 153, + "title": "Batman The Telltale Series", + "playlistId": "PLRfhDHeBTBJ6_wunnzhRIaBhtF3aiuS7q", + "duration": "07:08:15", + "platform": 1 + } + ] + }, + { + "name": "Crash Bandicoot", + "items": [ + { + "id": 79, + "title": "Crash Bandicoot 3 : Warped", + "playlistId": "PLRfhDHeBTBJ4s03iZ1UDggaenmv_YDqzE", + "duration": "01:57:55", + "platform": 1 + }, + { + "id": 80, + "title": "Crash Bandicoot", + "playlistId": "PLRfhDHeBTBJ6uNb7KhMn-zU-slBk-HPbe", + "duration": "01:48:31", + "platform": 1 + }, + { + "id": 81, + "title": "Crash Bandicoot 2 : Cortex Strikes Back", + "playlistId": "PLRfhDHeBTBJ7gvlHCEXz7XFPkHVTCZpao", + "duration": "02:13:19", + "platform": 1 + }, + { + "id": 106, + "title": "Crash Bandicoot 4: It's About Time", + "playlistId": "PLRfhDHeBTBJ6AGIBxZa5zvykgQBKSHzd5", + "duration": "05:34:31", + "platform": 1 + } + ] + }, + { + "name": "Darksiders", + "items": [ + { + "id": 42, + "title": "Darksiders II", + "playlistId": "PLRfhDHeBTBJ6Pp3ukQM6-F8LYtOmXmy9X", + "duration": "14:35:48", + "platform": 6 + }, + { + "id": 50, + "title": "Darksiders I", + "playlistId": "PLRfhDHeBTBJ61QLZkvjtKvsYx3muPHVjs", + "duration": "11:59:57", + "platform": 1 + } + ] + }, + { + "name": "Dead to Rights", + "items": [ + { + "id": 122, + "title": "Dead to Rights", + "playlistId": "PLRfhDHeBTBJ7y3xsVkTNRYORp_mX0ag14", + "duration": "06:19:09", + "platform": 1 + }, + { + "id": 124, + "title": "Dead to Rights : Retribution", + "playlistId": "PLRfhDHeBTBJ6sm2gbS9qtdovdSGygP-3f", + "duration": "05:50:51", + "platform": 6 + } + ] + }, + { + "name": "Destroy All Humans!", + "items": [ + { + "id": 3, + "title": "Destroy All Humans! - Remake", + "playlistId": "PLRfhDHeBTBJ76V2inJIW484ZQZZyKKejr", + "duration": "04:25:44", + "platform": 1 + }, + { + "id": 15, + "title": "Destroy All Humans! 2", + "playlistId": "PLRfhDHeBTBJ7aGxJBcsSJXKe1l_sRAMPy", + "duration": "11:22:25", + "platform": 5 + }, + { + "id": 98, + "title": "Destroy All Humans! En route vers Paname !", + "playlistId": "PLRfhDHeBTBJ5PxtGm8Pwm7PNMhPVhIugR", + "duration": "06:44:54", + "platform": 6 + } + ] + }, + { + "name": "Dragon Ball", + "items": [ + { + "id": 86, + "title": "Dragon Ball : Advanced Adventure", + "videoId": "5ZHExfdq9F8", + "duration": "01:50:53", + "platform": 2 + }, + { + "id": 107, + "title": "Dragon Ball Z : L'Héritage de Goku 2", + "playlistId": "PLRfhDHeBTBJ73iy4EC825y_EZ4cmrneem", + "duration": "04:38:54", + "platform": 2 + }, + { + "id": 131, + "title": "Dragon Ball Z : Buu's Fury", + "playlistId": "PLRfhDHeBTBJ6zkB2GAtg_k6_J799AozQz", + "duration": "04:36:11", + "platform": 2 + } + ] + }, + { + "name": "God of War", + "items": [ + { + "id": 12, + "title": "God Of War II HD", + "playlistId": "PLRfhDHeBTBJ6zui2RaBzaZ5zjm768Jzum", + "duration": "06:20:04", + "platform": 6 + }, + { + "id": 40, + "title": "God of War Ascension", + "playlistId": "PLRfhDHeBTBJ7oa6cAMnUL_Qec1nkK6Oi6", + "duration": "06:20:52", + "platform": 6 + }, + { + "id": 41, + "title": "God of War III", + "playlistId": "PLRfhDHeBTBJ7FqF_Z2hOQZE4JP7CmvSdO", + "duration": "06:46:19", + "platform": 6 + }, + { + "id": 51, + "title": "God of War: Chains of Olympus", + "playlistId": "PLRfhDHeBTBJ6aLzxXL6JLStPjrxFH7nkf", + "duration": "03:34:18", + "platform": 3 + }, + { + "id": 52, + "title": "God of War: Ghost of Sparta", + "playlistId": "PLRfhDHeBTBJ7VH9EyPcKvQSCo6FbMUE6g", + "duration": "04:53:25", + "platform": 3 + }, + { + "id": 53, + "title": "God of War I", + "playlistId": "PLRfhDHeBTBJ4zvv9b7-EaycdD6x_YtIkj", + "duration": "07:02:42", + "platform": 5 + }, + { + "id": 125, + "title": "God of War (2018)", + "playlistId": "PLRfhDHeBTBJ4axkTXH5Ty8shlzKlR86Wm", + "duration": "17:41:07", + "platform": 1 + }, + { + "id": 147, + "title": "God of War: Chains of Olympus HD", + "playlistId": "PLRfhDHeBTBJ5BASLY5Kgof-1fgRxNVkOX", + "duration": "03:20:24", + "platform": 6 + }, + { + "id": 148, + "title": "God of War I HD", + "playlistId": "PLRfhDHeBTBJ5EYj5y6ByrLTuhqyKV4dpc", + "duration": "06:48:04", + "platform": 6 + } + ] + }, + { + "name": "Guns Gore & Cannoli", + "items": [ + { + "id": 5, + "title": "Guns Gore & Cannoli", + "playlistId": "PLRfhDHeBTBJ6oiHK7lfTbOQlMWVHecx2I", + "duration": "02:10:05", + "platform": 1 + }, + { + "id": 43, + "title": "Guns Gore and Cannoli 2", + "playlistId": "PLRfhDHeBTBJ4ivg1J2leFzeuABXobVFNO", + "duration": "02:31:53", + "platform": 1 + } + ] + }, + { + "name": "Harry Potter", + "items": [ + { + "id": 95, + "title": "Harry Potter et la Coupe de Feu", + "playlistId": "PLRfhDHeBTBJ5nRIWD5BCLi7NaQCYc6PpF", + "duration": "02:17:52", + "platform": 1 + }, + { + "id": 119, + "title": "Harry Potter à l'Ecole des Sorciers", + "playlistId": "PLRfhDHeBTBJ6Dbl3hku6SlyybSN86clQ7", + "duration": "03:36:33", + "platform": 1 + }, + { + "id": 120, + "title": "Harry Potter et la Chambre des Secrets", + "playlistId": "PLRfhDHeBTBJ4zV33TJt9y--cp84eD7ivO", + "duration": "04:06:59", + "platform": 1 + }, + { + "id": 123, + "title": "Harry Potter et le Prisonnier d'Azkaban", + "videoId": "IOog6QKck6o", + "duration": "03:01:32", + "platform": 1 + } + ] + }, + { + "name": "Jak and Daxter", + "items": [ + { + "id": 101, + "title": "Jak and Daxter: The Precursor Legacy", + "playlistId": "PLRfhDHeBTBJ5QvrTXXZ64vl8sRngbgkDv", + "duration": "05:07:21", + "platform": 6 + }, + { + "id": 103, + "title": "Jak II : Hors la Loi", + "playlistId": "PLRfhDHeBTBJ5omqALCFYK0zrIANxO8d2y", + "duration": "06:25:22", + "platform": 6 + }, + { + "id": 105, + "title": "Jak 3", + "playlistId": "PLRfhDHeBTBJ5KZm9-LWr5C-Iet5dyGGSc", + "duration": "06:27:02", + "platform": 6 + }, + { + "id": 152, + "title": "Jak and Daxter: The Lost Frontier", + "playlistId": "PLRfhDHeBTBJ5yE1da-5kvYirmxBd7sLEX", + "duration": "05:16:27", + "platform": 3 + } + ] + }, + { + "name": "Le Parrain", + "items": [ + { + "id": 26, + "title": "Le Parrain : Edition du Don", + "playlistId": "PLRfhDHeBTBJ4OEBaFpx7GHRdCN7tVYhLO", + "duration": "15:05:17", + "platform": 6 + }, + { + "id": 55, + "title": "Le parrain II", + "playlistId": "PLRfhDHeBTBJ6ho33uahEjHiIyL5nSi_3O", + "duration": "08:24:01", + "platform": 1 + } + ] + }, + { + "name": "Les indestructibles", + "items": [ + { + "id": 30, + "title": "Les indestructibles Le Jeu", + "playlistId": "PLRfhDHeBTBJ6kWfGJp4xw769cajM4K9Gi", + "duration": "03:42:26", + "platform": 1 + }, + { + "id": 36, + "title": "Les Indestructibles : La Terrible Attaque du Démolisseur", + "playlistId": "PLRfhDHeBTBJ44T1V46UXupwH6GPekErCH", + "duration": "02:11:24", + "platform": 1 + } + ] + }, + { + "name": "Les Simpson", + "items": [ + { + "id": 113, + "title": "The Simpsons Hit & Run", + "playlistId": "PLRfhDHeBTBJ4Z99FBhfpJO6T_pe5HymRV", + "duration": "04:04:20", + "platform": 1 + }, + { + "id": 134, + "title": "Les Simpson : Le Jeu", + "playlistId": "PLRfhDHeBTBJ6aLNhmxhZdrI2dz0Bavlkh", + "duration": "04:05:46", + "platform": 6 + } + ] + }, + { + "name": "Mafia", + "items": [ + { + "id": 1, + "title": "Mafia: Definitive Edition", + "playlistId": "PLRfhDHeBTBJ6SEXdQnTM4OHRH9mDIRocv", + "duration": "06:59:14", + "platform": 1 + }, + { + "id": 25, + "title": "Mafia II", + "playlistId": "PLRfhDHeBTBJ5KzX5CFJUfNs_Y8_Xi-dhg", + "duration": "07:56:28", + "platform": 6 + }, + { + "id": 27, + "title": "Mafia 1", + "playlistId": "PLRfhDHeBTBJ65md00lEyYxa_K8qf8mPbP", + "duration": "09:00:04", + "platform": 5 + } + ] + }, + { + "name": "Max Payne", + "items": [ + { + "id": 45, + "title": "Max Payne 2 - The Fall of Max Payne", + "playlistId": "PLRfhDHeBTBJ4ZHSeesVGZle5F3OOA6ERf", + "duration": "03:35:54", + "platform": 1 + }, + { + "id": 46, + "title": "Max Payne I", + "playlistId": "PLRfhDHeBTBJ5LaRTqHGdoOWrQM8Uck35J", + "duration": "04:10:57", + "platform": 1 + }, + { + "id": 82, + "title": "Max Payne 3", + "playlistId": "PLRfhDHeBTBJ6qxm-Yidg-RNG6ICJuvll2", + "duration": "07:01:05", + "platform": 1 + } + ] + }, + { + "name": "Prince of Persia", + "items": [ + { + "id": 10, + "title": "Prince of Persia : Les Sables du temps", + "playlistId": "PLRfhDHeBTBJ53K1nOu8IEXqZltsLtffGO", + "duration": "05:01:34", + "platform": 6 + }, + { + "id": 11, + "title": "Prince of Persia : Les Deux Royaumes", + "playlistId": "PLRfhDHeBTBJ6ptBn3Rkq74O50hly24Gqg", + "duration": "04:39:35", + "platform": 6 + }, + { + "id": 13, + "title": "Prince of Persia Epilogue", + "playlistId": "PLRfhDHeBTBJ4hCgXVdIqLpBkTKS9q1JN6", + "duration": "01:24:13", + "platform": 6 + }, + { + "id": 24, + "title": "Prince of Persia : L'Ame du guerrier", + "playlistId": "PLRfhDHeBTBJ5FBZQLKtgH6QsC536I4ggr", + "duration": "05:52:06", + "platform": 6 + }, + { + "id": 61, + "title": "Prince of Persia : Les sables oubliés", + "playlistId": "PLRfhDHeBTBJ7EmZWmBJjwVfrNo4c2OGnb", + "duration": "05:16:11", + "platform": 1 + }, + { + "id": 68, + "title": "Prince of Persia 2008", + "playlistId": "PLRfhDHeBTBJ4H78MI_RvItygKp29dg0bc", + "duration": "10:18:10", + "platform": 1 + }, + { + "id": 89, + "title": "Prince of Persia : Les sables oubliés", + "videoId": "lYIH7XkENSw", + "duration": "03:23:14", + "platform": 3 + } + ] + }, + { + "name": "Psychonauts", + "items": [ + { + "id": 63, + "title": "Psychonauts", + "playlistId": "PLRfhDHeBTBJ6NqC2O9ojqqUv91ARBqUkl", + "duration": "13:35:45", + "platform": 1 + }, + { + "id": 112, + "title": "Psychonauts 2", + "playlistId": "PLRfhDHeBTBJ4PRXxtpRjP_o3PUzSQ0-nQ", + "duration": "10:59:44", + "platform": 1 + } + ] + }, + { + "name": "Pursuit Force", + "items": [ + { + "id": 140, + "title": "Pursuit Force", + "playlistId": "PLRfhDHeBTBJ47XmS2_upaFjIh_wKhN0XB", + "duration": "03:06:59", + "platform": 3 + }, + { + "id": 146, + "title": "Pursuit Force: Extreme Justice", + "playlistId": "PLRfhDHeBTBJ72gRyZBPf23ZfxSlG5h8Em", + "duration": "04:35:22", + "platform": 3 + } + ] + }, + { + "name": "Pyjama Sam", + "items": [ + { + "id": 20, + "title": "Sam Pyjam : Héros de la Nuit", + "videoId": "qaIADIQyr0A", + "duration": "00:44:11", + "platform": 1 + }, + { + "id": 21, + "title": "Pyjama Sam 2 : Héros Météo", + "videoId": "We7-JHU7D7I", + "duration": "00:53:45", + "platform": 1 + }, + { + "id": 22, + "title": "Pyjama Sam : Héros du Goûter", + "videoId": "Qsq_q65MwhU", + "duration": "01:07:54", + "platform": 1 + } + ] + }, + { + "name": "Ratchet & Clank", + "items": [ + { + "id": 32, + "title": "Secret Agent Clank", + "playlistId": "PLRfhDHeBTBJ70A5fWB1qpafjMvJ2w1IxY", + "duration": "06:22:48", + "platform": 3 + }, + { + "id": 33, + "title": "Ratchet & Clank : La Taille, Ca Compte", + "playlistId": "PLRfhDHeBTBJ5J7kgKwUQGOnru8DZhKi6s", + "duration": "04:05:03", + "platform": 3 + }, + { + "id": 35, + "title": "Ratchet & Clank 3", + "playlistId": "PLRfhDHeBTBJ7MKXXCaweIjEpKNA-qtQvu", + "duration": "06:55:09", + "platform": 6 + }, + { + "id": 37, + "title": "Ratchet & Clank 2", + "playlistId": "PLRfhDHeBTBJ5R8zWiWBkY-DBMU2AFb9mu", + "duration": "07:48:33", + "platform": 6 + }, + { + "id": 38, + "title": "Ratchet & Clank", + "playlistId": "PLRfhDHeBTBJ56NplVu4F1-uQm5bq7jwvo", + "duration": "07:03:58", + "platform": 6 + }, + { + "id": 39, + "title": "Ratchet : Gladiator", + "playlistId": "PLRfhDHeBTBJ7AoUspxANPSVOxoKEC0hRA", + "duration": "06:07:43", + "platform": 6 + }, + { + "id": 94, + "title": "Ratchet & Clank : Opération Destruction", + "playlistId": "PLRfhDHeBTBJ4uA4ArliLWDLuJgaykBcIY", + "duration": "06:49:19", + "platform": 6 + }, + { + "id": 114, + "title": "Ratchet & Clank : A Crack in Time", + "playlistId": "PLRfhDHeBTBJ5ysRbqhFaSSapvvuEVOu7M", + "duration": "05:45:00", + "platform": 6 + } + ] + }, + { + "name": "Rayman", + "items": [ + { + "id": 60, + "title": "Rayman 2: The Great Escape", + "playlistId": "PLRfhDHeBTBJ7O8QVtJw10sNcDen6yARFm", + "duration": "04:18:06", + "platform": 4 + }, + { + "id": 73, + "title": "Rayman 2 : Revolution", + "playlistId": "PLRfhDHeBTBJ5iBOO8LgF8L_AvxX91jbaX", + "duration": "04:59:15", + "platform": 5 + }, + { + "id": 75, + "title": "Rayman contre les Lapins Crétins", + "playlistId": "PLRfhDHeBTBJ7zlVdm21oc8ndiRGxQJy6F", + "duration": "03:05:12", + "platform": 1 + }, + { + "id": 92, + "title": "Rayman Origins", + "playlistId": "PLRfhDHeBTBJ703-Pyf2BZmJC0dM8Uu4Wc", + "duration": "04:40:15", + "platform": 1 + }, + { + "id": 116, + "title": "Rayman 3 : Hoodlum Havoc HD", + "playlistId": "PLRfhDHeBTBJ7bQV45KKQ7Japc7Z9uLvgJ", + "duration": "05:27:54", + "platform": 6 + }, + { + "id": 136, + "title": "Rayman Legends", + "playlistId": "PLRfhDHeBTBJ4GoSaMs-BqHumiPqUHU3OX", + "duration": "04:41:01", + "platform": 6 + } + ] + }, + { + "name": "Saints Row", + "items": [ + { + "id": 62, + "title": "Saints Row IV", + "playlistId": "PLRfhDHeBTBJ4QdQseb6rQyYe_lybyI5LB", + "duration": "20:55:30", + "platform": 1 + }, + { + "id": 64, + "title": "Saints Row Gat out of Hell", + "playlistId": "PLRfhDHeBTBJ6qA11nZAq1HtkkuE98F0iO", + "duration": "05:45:04", + "platform": 1 + }, + { + "id": 66, + "title": "Saints Row The Third", + "playlistId": "PLRfhDHeBTBJ5BcOJy0BEPvdPbZSfi1P1b", + "duration": "13:53:03", + "platform": 1 + } + ] + }, + { + "name": "Shrek", + "items": [ + { + "id": 19, + "title": "Shrek 2 Le Jeu", + "playlistId": "PLRfhDHeBTBJ53mxvj3I9ZwNfn38vQSI3j", + "duration": "02:24:35", + "platform": 1 + }, + { + "id": 90, + "title": "Shrek Super Slam", + "videoId": "AVwWGO0Zocs", + "duration": "00:31:13", + "platform": 5 + } + ] + }, + { + "name": "Sonic", + "items": [ + { + "id": 34, + "title": "Sonic Heroes", + "playlistId": "PLRfhDHeBTBJ5bAuo9zjPUGnXfzu9Py5Nx", + "duration": "02:55:04", + "platform": 5 + }, + { + "id": 118, + "title": "Sonic Adventure DX: Director's Cut", + "playlistId": "PLRfhDHeBTBJ5YBw47XwARehnyP6bhSpuJ", + "duration": "06:25:57", + "platform": 1 + }, + { + "id": 121, + "title": "Sonic Adventure 2", + "videoId": "a0mr0A8VM9A", + "duration": "03:50:39", + "platform": 1 + } + ] + }, + { + "name": "South Park", + "items": [ + { + "id": 67, + "title": "South Park - Le baton de la vérité", + "playlistId": "PLRfhDHeBTBJ6PP1XZO-tM70wPQyPBBGmT", + "duration": "09:16:21", + "platform": 1 + }, + { + "id": 72, + "title": "South Park : L'Annale du Destin", + "playlistId": "PLRfhDHeBTBJ4WXhWDhZCsIp3k6JxR7HpU", + "duration": "12:25:40", + "platform": 1 + } + ] + }, + { + "name": "Splinter Cell", + "items": [ + { + "id": 9, + "title": "Splinter Cell Blacklist", + "playlistId": "PLRfhDHeBTBJ7JOqVieOgK3RrNvDRMC5cV", + "duration": "06:34:25", + "platform": 1 + }, + { + "id": 78, + "title": "Splinter Cell Conviction", + "playlistId": "PLRfhDHeBTBJ48YBgwV9lobG0Ddo4EN9I3", + "duration": "04:02:12", + "platform": 1 + }, + { + "id": 102, + "title": "Splinter Cell Chaos Theory HD", + "playlistId": "PLRfhDHeBTBJ7gViU4fNr0LhQvSAWk55T2", + "duration": "05:22:51", + "platform": 6 + }, + { + "id": 104, + "title": "Splinter Cell: Pandora Tomorrow", + "playlistId": "PLRfhDHeBTBJ7xvQe1Jbs8Kv9ie_uQhdJH", + "duration": "03:11:52", + "platform": 6 + }, + { + "id": 154, + "title": "Splinter Cell HD", + "playlistId": "PLRfhDHeBTBJ7Do88fiav23gsXrV-0SHn-", + "duration": "04:40:45", + "platform": 6 + } + ] + }, + { + "name": "Spy Fox", + "items": [ + { + "id": 57, + "title": "Spy Fox : Operation Ozone", + "playlistId": "PLRfhDHeBTBJ5VLwcplFFMaHjd5eKC4p2A", + "duration": "01:09:17", + "platform": 1 + }, + { + "id": 58, + "title": "Spy Fox : Opération Robot Expo", + "playlistId": "PLRfhDHeBTBJ4AAsZf91kxWWrQEKHae4Wx", + "duration": "01:12:15", + "platform": 1 + }, + { + "id": 69, + "title": "Spy fox : Opération Milkshake", + "playlistId": "PLRfhDHeBTBJ42kEksYsYPnAHmS_7V8525", + "duration": "01:17:20", + "platform": 1 + } + ] + } +] \ No newline at end of file diff --git a/src/app/api/stats/route.ts b/src/app/api/stats/route.ts index d3e86800..d6bdc9cf 100644 --- a/src/app/api/stats/route.ts +++ b/src/app/api/stats/route.ts @@ -1,5 +1,4 @@ import { NextResponse } from "next/server"; -import type { BasicGame, Genre, Platform } from "@/redux/sharedDefintion" type statsEntry = { /** @description Number of games for this stat (including not yet available ones) */ @@ -16,12 +15,6 @@ type contentDuration = { seconds: number } -let defaultDuration : contentDuration = { - hours: 0, - minutes: 0, - seconds: 0 -} - // For extraneous properties in "general" type statsGeneral = statsEntry & { // Info can be found in Youtube RSS feed @@ -31,131 +24,32 @@ type statsGeneral = statsEntry & { "total_time_unavailable": contentDuration, } +// For specifc stats +type platformStats = statsEntry & { + id: number, + platform: string +} +type genreStats = statsEntry & { + id: number, + genre: string +} + export type statsProperty = { /** @description Stats about platforms covered */ - platforms: { - [P in Platform]?: statsEntry - }, + platforms: platformStats[], /** @description Stats about genres covered */ - genres: { - [G in Genre]?: statsEntry - }, + genres: genreStats[], /** @description General stats */ general: statsGeneral }; export async function GET() { - const gamesData = (await import("../games/games.json")).default; - - // current date as integer (quicker comparaison) - const currentDate = new Date(); - const integerDate = (currentDate.getFullYear() * 10000) + - ( (currentDate.getMonth() + 1) * 100 ) + - currentDate.getDate(); - - const stats = (gamesData as BasicGame[]).reduce( (acc : statsProperty, game) => { - - let isAlreadyPublic = (game?.availableAt === undefined) || (game?.availableAt <= integerDate); - - // Update platform stats - let updatedPlatform = acc.platforms[game.platform] || { - total: 0, - total_available: 0, - total_unavailable: 0 - }; - - updatedPlatform.total = updatedPlatform.total + 1; - if (isAlreadyPublic) { - updatedPlatform.total_available = updatedPlatform.total_available + 1; - } else { - updatedPlatform.total_unavailable = updatedPlatform.total_unavailable + 1; - } - acc.platforms[game.platform] = updatedPlatform; - - // Update genres stats - game.genres.forEach( (genre) => { - // Update specific genre - let updatedGenre = acc.genres[genre] || { - total: 0, - total_available: 0, - total_unavailable: 0 - }; - updatedGenre.total = updatedGenre.total + 1; - if (isAlreadyPublic) { - updatedGenre.total_available = updatedGenre.total_available + 1; - } else { - updatedGenre.total_unavailable = updatedGenre.total_unavailable + 1; - } - - acc.genres[genre] = updatedGenre; - }); + const statsData = (await import("./stats.json")).default; - // Update general stats - acc.general.total = acc.general.total + 1; - acc.general.total_time = sumTimes(acc.general.total_time, game.duration) - if (isAlreadyPublic) { - acc.general.total_available = acc.general.total_available + 1; - acc.general.total_time_available = sumTimes(acc.general.total_time_available, game.duration) - } else { - acc.general.total_unavailable = acc.general.total_unavailable + 1; - acc.general.total_time_unavailable = sumTimes(acc.general.total_time_unavailable, game.duration) - } - - return acc; - }, { - platforms: {}, - genres: {}, - general: { - total: 0, - total_available: 0, - total_unavailable: 0, - // Info can be found in Youtube RSS feed - channel_start_date: "2014-04-15T17:35:16+00:00", - total_time: defaultDuration, - total_time_available: defaultDuration, - total_time_unavailable: defaultDuration, - } - }); - - return NextResponse.json(stats, { + return NextResponse.json(statsData, { headers: { "Cache-Control": "public, max-age=86400, must-revalidate" } }); -} - -// To compute sum of two times -function sumTimes(currentTotal: contentDuration, gameDuration: string | undefined) : contentDuration { - // If game time isn't specified, then no need to compute - if (gameDuration === undefined){ - return currentTotal; - } else { - let fields = gameDuration.split(":"); - - let hours = Number((fields.length === 3) ? fields[0] : 0); - let minutes = Number((fields.length === 2) ? fields[0] : fields[1]); - let seconds = Number((fields.length === 2) ? fields[1] : fields[2]); - - // Combine them with - let totalInSeconds = [ - (hours + currentTotal.hours) * 3600, - (minutes + currentTotal.minutes) * 60, - (seconds + currentTotal.seconds) - ].reduce( (acc, total) => acc + total, 0); - - // Time to normalize the result - let new_hours = Math.floor(totalInSeconds / 3600); - totalInSeconds %= 3600; - let new_minutes = Math.floor(totalInSeconds / 60); - let new_seconds = totalInSeconds % 60; - - return { - hours: new_hours, - minutes: new_minutes, - seconds : new_seconds - } - - } - } \ No newline at end of file diff --git a/src/app/api/stats/stats.json b/src/app/api/stats/stats.json new file mode 100644 index 00000000..c94a4594 --- /dev/null +++ b/src/app/api/stats/stats.json @@ -0,0 +1,209 @@ +{ + "platforms": [ + { + "id": 1, + "platform": "PC", + "total": 78, + "total_available": 76, + "total_unavailable": 2 + }, + { + "id": 6, + "platform": "PS3", + "total": 55, + "total_available": 37, + "total_unavailable": 19 + }, + { + "id": 5, + "platform": "PS2", + "total": 21, + "total_available": 19, + "total_unavailable": 2 + }, + { + "id": 3, + "platform": "PSP", + "total": 14, + "total_available": 14, + "total_unavailable": 0 + }, + { + "id": 2, + "platform": "GBA", + "total": 10, + "total_available": 10, + "total_unavailable": 0 + }, + { + "id": 4, + "platform": "PS1", + "total": 4, + "total_available": 4, + "total_unavailable": 0 + } + ], + "genres": [ + { + "id": 1, + "genre": "Action", + "total": 159, + "total_available": 137, + "total_unavailable": 23 + }, + { + "id": 2, + "genre": "Adventure", + "total": 138, + "total_available": 119, + "total_unavailable": 20 + }, + { + "id": 12, + "genre": "Platformer", + "total": 66, + "total_available": 60, + "total_unavailable": 7 + }, + { + "id": 16, + "genre": "Shooter", + "total": 42, + "total_available": 38, + "total_unavailable": 4 + }, + { + "id": 14, + "genre": "RPG", + "total": 16, + "total_available": 15, + "total_unavailable": 1 + }, + { + "id": 13, + "genre": "Puzzle", + "total": 8, + "total_available": 8, + "total_unavailable": 0 + }, + { + "id": 7, + "genre": "Educational", + "total": 7, + "total_available": 7, + "total_unavailable": 0 + }, + { + "id": 6, + "genre": "Casual", + "total": 6, + "total_available": 6, + "total_unavailable": 0 + }, + { + "id": 9, + "genre": "Fighting", + "total": 6, + "total_available": 6, + "total_unavailable": 0 + }, + { + "id": 3, + "genre": "Arcade", + "total": 5, + "total_available": 5, + "total_unavailable": 0 + }, + { + "id": 15, + "genre": "Racing", + "total": 4, + "total_available": 4, + "total_unavailable": 0 + }, + { + "id": 10, + "genre": "Indie", + "total": 3, + "total_available": 3, + "total_unavailable": 0 + }, + { + "id": 5, + "genre": "Card", + "total": 2, + "total_available": 2, + "total_unavailable": 0 + }, + { + "id": 8, + "genre": "Family", + "total": 1, + "total_available": 1, + "total_unavailable": 0 + }, + { + "id": 19, + "genre": "Strategy", + "total": 1, + "total_available": 1, + "total_unavailable": 0 + }, + { + "id": 4, + "genre": "Board Games", + "total": 0, + "total_available": 0, + "total_unavailable": 0 + }, + { + "id": 11, + "genre": "MMORPG", + "total": 0, + "total_available": 0, + "total_unavailable": 0 + }, + { + "id": 17, + "genre": "Simulation", + "total": 0, + "total_available": 0, + "total_unavailable": 0 + }, + { + "id": 18, + "genre": "Sports", + "total": 0, + "total_available": 0, + "total_unavailable": 0 + }, + { + "id": 20, + "genre": "Misc", + "total": 0, + "total_available": 0, + "total_unavailable": 0 + } + ], + "general": { + "total": 182, + "total_available": 160, + "total_unavailable": 23, + "channel_start_date": "2014-04-15T17:35:16+00:00", + "total_time": { + "hours": 967, + "minutes": 40, + "seconds": 36 + }, + "total_time_available": { + "hours": 841, + "minutes": 14, + "seconds": 12 + }, + "total_time_unavailable": { + "hours": 135, + "minutes": 33, + "seconds": 16 + } + } +} \ No newline at end of file diff --git a/src/app/api/tests/route.ts b/src/app/api/tests/route.ts index a1f7d033..a790d2c2 100644 --- a/src/app/api/tests/route.ts +++ b/src/app/api/tests/route.ts @@ -32,7 +32,7 @@ export async function GET(request: Request) { // Returns results return NextResponse.json({ - items: games.map(game => enhanceGameItem(game as BasicGame)), + items: games.map(game => enhanceGameItem(game)), total_items: gamesData.length, limit: limit, offset: offset @@ -53,7 +53,7 @@ const SIZES = [ ] // Return an enhanced payload for a single game -function enhanceGameItem(game: BasicGame): CardGame { +function enhanceGameItem(game: rawEntry): CardGame { const id = (game as BasicPlaylist).playlistId ?? (game as BasicVideo).videoId; const base_url = ( @@ -66,13 +66,7 @@ function enhanceGameItem(game: BasicGame): CardGame { id, imagePath: `/testscovers/${id}/${ game?.coverFile ?? "cover.webp" }`, sizes: SIZES.join(", "), - releaseDate: game.releaseDate - .split("/") - .reduce( (acc : number, curr : string, idx : number) => acc + (parseInt(curr) * Math.pow(100, idx)), 0), url: base_url, - url_type: ("playlistId" in game) ? "PLAYLIST" : "VIDEO" as YTUrlType, - durationAsInt: (game.duration) - ? Number(game.duration.replaceAll(":", "")) - : 0 + url_type: ("playlistId" in game) ? "PLAYLIST" : "VIDEO" as YTUrlType }); } \ No newline at end of file diff --git a/src/app/api/tests/tests.json b/src/app/api/tests/tests.json index ec200ffa..99d8a2c6 100644 --- a/src/app/api/tests/tests.json +++ b/src/app/api/tests/tests.json @@ -1,87 +1,62 @@ [ - { - "title": "Nova Drift", - "videoId": "RMgDUMubFsM", - "releaseDate": "18/05/2021", - "platform": "PC", - "duration": "00:13:53" - }, - { - "title": "Metal Commando", - "videoId": "ip-PjOmBOb4", - "releaseDate": "18/11/2020", - "platform": "PC", - "duration": "00:16:45" - }, - { - "title": "Foregone", - "videoId": "RGsuBcF3W4s", - "releaseDate": "13/10/2020", - "platform": "PC", - "duration": "00:25:48" - }, - { - "title": "Damsel", - "videoId": "ghUYugzShf8", - "releaseDate": "19/10/2018", - "platform": "PC", - "duration": "00:35:30" - }, - { - "title": "FURY UNLEASHED", - "videoId": "VI5irWqgEZY", - "releaseDate": "08/05/2020", - "platform": "PC", - "duration": "00:14:26" - }, - { - "title": "Libtard: The Satire Game", - "videoId": "zR4m0LyTDAY", - "releaseDate": "03/11/2020", - "platform": "PC", - "duration": "00:13:35" - }, - { - "title": "The Hero Slayer", - "videoId": "eY5Ll-E8izg", - "releaseDate": "19/02/2020", - "platform": "PC", - "duration": "00:26:30" - }, - { - "title": "Spirit Roots", - "videoId": "92xWnlnXX7A", - "releaseDate": "13/12/2019", - "platform": "PC", - "duration": "01:04:50" - }, - { - "title": "Demon Pit", - "videoId": "2aPqECXPA_E", - "coverFile": "cover.webp", - "releaseDate": "17/10/2019", - "platform": "PC", - "duration": "00:05:01" - }, - { - "title": "ULTRAGOODNESS 2", - "videoId": "D4yPcvrfV2o", - "releaseDate": "02/09/2019", - "platform": "PC", - "duration": "00:40:33" - }, - { - "title": "GOD’S TRIGGER", - "videoId": "cS4AuUMOuiE", - "releaseDate": "18/04/2019", - "platform": "PC", - "duration": "00:49:19" - }, - { - "title": "Creepy Road", - "releaseDate": "18/05/2018", - "platform": "PC", - "playlistId": "PLRfhDHeBTBJ7kgZQ8pv1-OByUypdJLr8M", - "duration": "00:33:32" - } + { + "title": "Nova Drift", + "videoId": "RMgDUMubFsM", + "platform": 1 + }, + { + "title": "Metal Commando", + "videoId": "ip-PjOmBOb4", + "platform": 1 + }, + { + "title": "Foregone", + "videoId": "RGsuBcF3W4s", + "platform": 1 + }, + { + "title": "Damsel", + "videoId": "ghUYugzShf8", + "platform": 1 + }, + { + "title": "FURY UNLEASHED", + "videoId": "VI5irWqgEZY", + "platform": 1 + }, + { + "title": "Libtard: The Satire Game", + "videoId": "zR4m0LyTDAY", + "platform": 1 + }, + { + "title": "The Hero Slayer", + "videoId": "eY5Ll-E8izg", + "platform": 1 + }, + { + "title": "Spirit Roots", + "videoId": "92xWnlnXX7A", + "platform": 1 + }, + { + "title": "Demon Pit", + "videoId": "2aPqECXPA_E", + "platform": 1 + }, + { + "title": "ULTRAGOODNESS 2", + "videoId": "D4yPcvrfV2o", + "platform": 1 + }, + { + "title": "GOD’S TRIGGER", + "videoId": "cS4AuUMOuiE", + "platform": 1 + }, + { + "title": "Creepy Road", + "playlistId": "PLRfhDHeBTBJ7kgZQ8pv1-OByUypdJLr8M", + "platform": 1 + } ] \ No newline at end of file diff --git a/src/components/GamesView/GenresSelect.tsx b/src/components/GamesView/GenresSelect.tsx index 32fc1e7f..654a54ae 100644 --- a/src/components/GamesView/GenresSelect.tsx +++ b/src/components/GamesView/GenresSelect.tsx @@ -8,17 +8,12 @@ import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import Autocomplete from "@mui/material/Autocomplete"; import TextField from '@mui/material/TextField'; +// actions import { filteringByGenre, selectSelectedGenres } from "@/redux/features/gamesSlice"; - -import type { Genre as GenreValue } from "@/redux/sharedDefintion"; -// Each one is also a key for translation -import { genre_list as GENRES } from "@/redux/sharedDefintion"; +import { useGetGenresQuery } from "@/redux/services/genresAPI" // Generate list of values for game genre -type Genre = { - label: string, - id: GenreValue -}; +import type { Genre } from "@/app/api/genres/route" // Genres filter of GamesGallery function GenresSelect() { @@ -27,15 +22,16 @@ function GenresSelect() { const selectedGenres = useAppSelector( (state) => selectSelectedGenres(state) ) + const { data, isFetching } = useGetGenresQuery(); const t = useTranslations("gamesLibrary") - const genre_options : Genre[] = GENRES + const genre_options : Genre[] = (data || []) .map(genre => ({ - label: t(`gamesGenres.${genre}` as const), - id: genre + name: t(`gamesGenres.${genre.id}` as any), + id: genre.id })) .sort( - (a, b) => (a.label < b.label) ? -1 : (a.label > b.label ? 1 : 0) + (a, b) => (a.name < b.name) ? -1 : (a.name > b.name ? 1 : 0) ); return <> @@ -45,14 +41,15 @@ function GenresSelect() { filterSelectedOptions id="select-game-genre" limitTags={3} + loading={isFetching} options={genre_options} - getOptionLabel={(option) => option.label} + getOptionLabel={(option) => option.name} isOptionEqualToValue={(option, value) => Array.isArray(value) ? value.some(v => v.id === option.id) : value.id === option.id } value={selectedGenres.map(genre => ({ - label: t(`gamesGenres.${genre as GenreValue}` as const), - id: genre as GenreValue + name: t(`gamesGenres.${genre}` as any), + id: genre }))} renderInput={(params) => } onChange={(_event, value) => { diff --git a/src/components/GamesView/PlatformIcons.tsx b/src/components/GamesView/PlatformIcons.tsx index dd885689..73f0e7d7 100644 --- a/src/components/GamesView/PlatformIcons.tsx +++ b/src/components/GamesView/PlatformIcons.tsx @@ -1,3 +1,7 @@ +// Icons +import SvgIcon from '@mui/material/SvgIcon'; +import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; + const platformIcons = { "PS1": , "PS2": , @@ -12,4 +16,43 @@ const platformIcons = { , "GBA": } -export default platformIcons; \ No newline at end of file + +const platformsMappings = { + 1: platformIcons["PC"], + 2: platformIcons["GBA"], + 3: platformIcons["PSP"], + 4: platformIcons["PS1"], + 5: platformIcons["PS2"], + 6: platformIcons["PS3"] +} + +type Props = { + identifier: number | undefined, + label?: string +}; + +function RenderPlatformIcon(props: Props) { + + const { identifier, label } = props; + + // If nothing, put nothing + if (identifier === undefined) { + return ; + } + + // If part of mapping, return dedicated icon + if (identifier in platformsMappings) { + // @ts-ignore: Typescript is mad here ^^ + const elem = platformsMappings[identifier]; + return ( + + {elem} + + ) + } + + // By default, put unknwon icon + return ; +} + +export default RenderPlatformIcon; \ No newline at end of file diff --git a/src/components/GamesView/PlatformSelect.tsx b/src/components/GamesView/PlatformSelect.tsx index 0ed2fbad..5647b28f 100644 --- a/src/components/GamesView/PlatformSelect.tsx +++ b/src/components/GamesView/PlatformSelect.tsx @@ -4,68 +4,49 @@ import { useTranslations } from "next-intl"; import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import { filterByPlatform, selectSelectedPlatform } from "@/redux/features/gamesSlice"; +import { useGetPlatformsQuery } from "@/redux/services/platformsAPI"; // React Material UI import Autocomplete from "@mui/material/Autocomplete"; import TextField from '@mui/material/TextField'; -import SvgIcon from '@mui/material/SvgIcon'; // icons -import iconsSVG from "@/components/GamesView/PlatformIcons"; -import type { Platform } from "@/redux/sharedDefintion"; +import RenderPlatformIcon from "@/components/GamesView/PlatformIcons"; -const PLATFORMS = [ - "GBA", - "PC", - "PS1", - "PS2", - "PS3", - "PSP" -]; - -type Platform_Entry = { - label: string; - id: Platform; -} +import type { Platform_Entry } from "@/app/api/platforms/route"; function PlatformSelect() { const t = useTranslations("gamesLibrary.filtersLabels") const dispatch = useAppDispatch(); + const { data, isFetching } = useGetPlatformsQuery(); const selectedPlatform = useAppSelector( (state) => selectSelectedPlatform(state) ) - const options = PLATFORMS - .map(platform => ({ - label: platform, - id: platform as Platform - })) - return ( id="select-game-platform" openOnFocus - options={options} - getOptionLabel={(option) => option.label} + options={data || []} + loading={isFetching} + getOptionLabel={(option) => option.name} isOptionEqualToValue={(option, value) => value.id === option.id} renderInput={(params) => } renderOption={(props, option) => ( -
  • - - {iconsSVG[option.id as Platform]} - - {option.label} +
  • + + {option.name}
  • )} onChange={(_event, value) => { - const platform = (value) ? value.id : ""; + const platform = (value) ? value.id : undefined; dispatch(filterByPlatform(platform)); }} value={ selectedPlatform ? { - id: selectedPlatform as Platform, - label: selectedPlatform + id: selectedPlatform, + name: (data || [] ).find(p => p.id === selectedPlatform)?.name || "" } : null } /> diff --git a/src/redux/Store.tsx b/src/redux/Store.tsx index 2a0f59b0..7ab906e8 100644 --- a/src/redux/Store.tsx +++ b/src/redux/Store.tsx @@ -9,6 +9,8 @@ import { seriesAPI } from "./services/seriesAPI"; import { statsAPI } from "./services/statsAPI"; import { testsAPI } from "./services/testsAPI"; import { backlogAPI } from "./services/backlogAPI"; +import { platformsAPI } from "./services/platformsAPI" +import { genresAPI } from "./services/genresAPI"; /* eslint-disable no-underscore-dangle */ const store = configureStore({ @@ -24,6 +26,8 @@ const store = configureStore({ [statsAPI.reducerPath]: statsAPI.reducer, [testsAPI.reducerPath]: testsAPI.reducer, [backlogAPI.reducerPath]: backlogAPI.reducer, + [platformsAPI.reducerPath]: platformsAPI.reducer, + [genresAPI.reducerPath]: genresAPI.reducer }, // Adding the api middleware enables caching, invalidation, polling, // and other useful features of `rtk-query`. @@ -33,7 +37,9 @@ const store = configureStore({ .concat(seriesAPI.middleware) .concat(statsAPI.middleware) .concat(testsAPI.middleware) - .concat(backlogAPI.middleware), + .concat(backlogAPI.middleware) + .concat(platformsAPI.middleware) + .concat(genresAPI.middleware), }) export default store; // Infer the `RootState` and `AppDispatch` types from the store itself diff --git a/src/redux/features/gamesSlice.tsx b/src/redux/features/gamesSlice.tsx index a29ce7cd..e2b53f41 100644 --- a/src/redux/features/gamesSlice.tsx +++ b/src/redux/features/gamesSlice.tsx @@ -3,37 +3,26 @@ import { createSlice, createSelector } from '@reduxjs/toolkit'; // Types import type { RootState } from "../Store" import type { PayloadAction } from "@reduxjs/toolkit"; -import type { Platform } from "@/redux/sharedDefintion"; -import type { Genre as GenreValue } from "@/redux/sharedDefintion"; - -export type gamesSorters = [ - "name" | "releaseDate" | "duration", - "ASC" | "DESC" -][]; // To compute new filtering function //type gamesFilterKeys = "selected_platform" | "selected_title" | "selected_genres"; export type gamesFilters = ({ value: string, - key: "selected_platform" | "selected_title" + key: "selected_title" } | { - value: string[], + value: number[], key: "selected_genres" +} | { + value: number, + key: "selected_platform" })[]; export interface GamesState { - /** @description sorting */ - sorters: gamesSorters, /** @description current filters applied */ activeFilters: gamesFilters } const initialState: GamesState = { - sorters: [ - ["name", "ASC"], - ["releaseDate", "ASC"], - ["duration", "ASC"] - ], activeFilters: [] }; @@ -43,7 +32,7 @@ const gamesSlice = createSlice({ // Redux Toolkit allows us to write "mutating" logic in reducers. It // doesn't actually mutate the state because it uses the Immer library reducers: { - filteringByGenre(state : GamesState, action: PayloadAction) { + filteringByGenre(state : GamesState, action: PayloadAction) { // If empty, remove filter - if not, add it let newFilters = state.activeFilters.filter(s => s.key !== "selected_genres") as gamesFilters; if (action.payload.length !== 0) { @@ -65,10 +54,10 @@ const gamesSlice = createSlice({ } state.activeFilters = newFilters; }, - filterByPlatform(state : GamesState, action: PayloadAction) { + filterByPlatform(state : GamesState, action: PayloadAction) { // If empty, remove filter - if not, add it let newFilters = state.activeFilters.filter(s => s.key !== "selected_platform") as gamesFilters; - if (action.payload.length !== 0) { + if (action.payload !== undefined) { newFilters.push({ key: "selected_platform", value: action.payload @@ -92,7 +81,7 @@ export const selectSelectedGenres = createSelector( if (!entry) { return []; } else { - return entry.value as string[] + return entry.value as number[] } } ); @@ -105,9 +94,9 @@ export const selectSelectedPlatform = createSelector( (filters) => { let entry = filters.find(s => s.key === "selected_platform"); if (!entry) { - return ""; + return undefined; } else { - return entry.value as string + return entry.value as number } } ); diff --git a/src/redux/services/gamesAPI.tsx b/src/redux/services/gamesAPI.tsx index 22651bd5..0a889d3a 100644 --- a/src/redux/services/gamesAPI.tsx +++ b/src/redux/services/gamesAPI.tsx @@ -1,26 +1,13 @@ // Need to use the React-specific entry point to import createApi import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' -import type { ResponseBody as GamesResponse } from "@/app/api/games/route"; - -type gamesSorters = [ - "name" | "releaseDate" | "duration", - "ASC" | "DESC" -][]; -// To compute new filtering function -//type gamesFilterKeys = "selected_platform" | "selected_title" | "selected_genres"; -type gamesFilters = ({ - value: string, - key: "selected_platform" | "selected_title" -} | { - value: string[], - key: "selected_genres" -})[]; +// Types +import type { ResponseBody as GamesResponse } from "@/app/api/games/route"; +import type { gamesFilters } from "@/redux/features/gamesSlice" // parameters for method type Parameters = { filters: gamesFilters, - sorters: gamesSorters, page: number, pageSize: number, } @@ -30,12 +17,9 @@ type RequestParams = { selected_platform?: string, selected_title?: string, selected_genres?: string[], - sortCriteria?: string[], - sortOrder?: string[], page: number, pageSize: number, includePreviousPagesResult: boolean, - dateAsInteger: number, } const stringifyObject = (object: any) => { @@ -59,12 +43,8 @@ export const gamesAPI = createApi({ baseQuery: fetchBaseQuery({ baseUrl: '/api' }), endpoints: (builder) => ({ getGames: builder.query({ - query: ({ pageSize, page, filters, sorters }) => { + query: ({ pageSize, page, filters }) => { - const currentDate = new Date(); - const integerDate = (currentDate.getFullYear() * 10000) + - ( (currentDate.getMonth() + 1) * 100 ) + - currentDate.getDate(); let parameters : RequestParams = { // page size @@ -74,7 +54,6 @@ export const gamesAPI = createApi({ // Needed for RTK Query to work properly // In the future, I should likely remove that stuff includePreviousPagesResult: true, - dateAsInteger: integerDate, }; // filters parameter @@ -84,12 +63,6 @@ export const gamesAPI = createApi({ } } - // sorters parameter - if (sorters.length > 0) { - parameters["sortCriteria"] = sorters.map(s => s[0]); - parameters["sortOrder"] = sorters.map(s => s[1]); - } - return `/games?${stringifyObject(parameters)}`; }, // Force refresh when offset change diff --git a/src/redux/services/genresAPI.tsx b/src/redux/services/genresAPI.tsx new file mode 100644 index 00000000..9ea00f5c --- /dev/null +++ b/src/redux/services/genresAPI.tsx @@ -0,0 +1,18 @@ +// Need to use the React-specific entry point to import createApi +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import type { GenreResponse } from "@/app/api/genres/route"; + +// Define a service using a base URL and expected endpoints +export const genresAPI = createApi({ + reducerPath: 'genresApi', + baseQuery: fetchBaseQuery({ baseUrl: '/api' }), + endpoints: (builder) => ({ + getGenres: builder.query({ + query: () => "/genres" + }) + }) +}); + +// Export hooks for usage in functional components, which are +// auto-generated based on the defined endpoints +export const { useGetGenresQuery } = genresAPI \ No newline at end of file diff --git a/src/redux/services/planningAPI.tsx b/src/redux/services/planningAPI.tsx index e5eed58a..4d30a193 100644 --- a/src/redux/services/planningAPI.tsx +++ b/src/redux/services/planningAPI.tsx @@ -8,15 +8,7 @@ export const planningAPI = createApi({ baseQuery: fetchBaseQuery({ baseUrl: '/api' }), endpoints: (builder) => ({ getPlanning: builder.query({ - query: () => { - - const currentDate = new Date(); - const integerDate = (currentDate.getFullYear() * 10000) + - ( (currentDate.getMonth() + 1) * 100 ) + - currentDate.getDate(); - - return `/planning?dateAsInteger=${integerDate}` - } + query: () => "/planning" }) }) }); diff --git a/src/redux/services/platformsAPI.tsx b/src/redux/services/platformsAPI.tsx new file mode 100644 index 00000000..43003b50 --- /dev/null +++ b/src/redux/services/platformsAPI.tsx @@ -0,0 +1,18 @@ +// Need to use the React-specific entry point to import createApi +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import type { PlatformsResponse } from "@/app/api/platforms/route"; + +// Define a service using a base URL and expected endpoints +export const platformsAPI = createApi({ + reducerPath: 'platformsApi', + baseQuery: fetchBaseQuery({ baseUrl: '/api' }), + endpoints: (builder) => ({ + getPlatforms: builder.query({ + query: () => "/platforms" + }) + }) +}); + +// Export hooks for usage in functional components, which are +// auto-generated based on the defined endpoints +export const { useGetPlatformsQuery } = platformsAPI \ No newline at end of file diff --git a/src/redux/services/seriesAPI.tsx b/src/redux/services/seriesAPI.tsx index 198d9d97..eb85df7f 100644 --- a/src/redux/services/seriesAPI.tsx +++ b/src/redux/services/seriesAPI.tsx @@ -8,15 +8,7 @@ export const seriesAPI = createApi({ baseQuery: fetchBaseQuery({ baseUrl: '/api' }), endpoints: (builder) => ({ getSeries: builder.query({ - query: () => { - - const currentDate = new Date(); - const integerDate = (currentDate.getFullYear() * 10000) + - ( (currentDate.getMonth() + 1) * 100 ) + - currentDate.getDate(); - - return `/series?dateAsInteger=${integerDate}` - } + query: () => "/series" }) }) }); diff --git a/src/redux/sharedDefintion.tsx b/src/redux/sharedDefintion.tsx index 1a3588b9..b11b63d2 100644 --- a/src/redux/sharedDefintion.tsx +++ b/src/redux/sharedDefintion.tsx @@ -1,29 +1,3 @@ -export type Platform = "PC" | "GBA" | "PSP" | "PS1" | "PS2" | "PS3"; - -export const genre_list = [ - "Action", - "Adventure", - "Arcade", - "Board Games", - "Card", - "Casual", - "Educational", - "Family", - "Fighting", - "Indie", - "MMORPG", - "Platformer", - "Puzzle", - "RPG", - "Racing", - "Shooter", - "Simulation", - "Sports", - "Strategy", - "Misc" -] as const; -export type Genre = typeof genre_list[number]; - // structure used in data/games.json export type BasicEntry = { /** @description Technical identifier for React - by default : playlistId | videoId */ @@ -31,17 +5,17 @@ export type BasicEntry = { /** @description Title of the game, such as "Beyond Good & Evil" */ title: string; /** @description Platform for that game */ - platform: Platform; + platform: number; /** @description Duration of the walkthrough (e.g. "01:42:13") */ duration?: string; /** @description Genres of the game */ - genres: Genre[]; - /** @description When the game was released, such "01/09/2005" */ - releaseDate: string; - /** @description When to display the game public, such as 20210412 (12/04/2021) */ - availableAt?: number; - /** @description When to display the game public, such as 20210420 (20/04/2021) */ - endAt?: number; + genres: number[]; + /** @description When the game was released, such "2005-12-22" */ + releaseDate?: string; + /** @description When to display the game public, such as "2021-12-22" */ + availableAt?: string; + /** @description When to display the game public, such as "2024-07-22" */ + endAt?: string; /** @description Name of the main cover file, such as "cover.webp" */ coverFile?: string; } @@ -73,13 +47,5 @@ interface CardEntry { url_type: YTUrlType; }; -// structured after parsing data/games.json -export interface EnhancedGame extends Omit, CardEntry { - /** @description When the game was released (Date(2020, 02, 02).getTime()) */ - releaseDate: number; - /** @description "duration" into something useful for sorting */ - durationAsInt: number; -}; - // structure used for GameEntry and thus GameDialog export interface CardGame extends Omit, CardEntry {}; \ No newline at end of file