From c1fc326d0f51d05970837ef5d99fae8ce951e395 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 3 Jul 2024 16:39:54 +0530 Subject: [PATCH] Allow import IGDb lists (#898) * feat(backend): add basic igdb importer * feat(frontend): allow igdb imports * docs: add instructions for igdb importing * feat(backend): adapt to new instructions * docs: better wording * fix(backend): allow import custom progress for just started * fix(docs): set progress to 5% * docs: remove useless content --- apps/backend/src/importer/igdb.rs | 74 +++++++++++++++++++ apps/backend/src/importer/mod.rs | 16 +++- apps/backend/src/miscellaneous/resolver.rs | 5 +- ...rd.settings.imports-and-exports._index.tsx | 27 ++++++- docs/content/importing.md | 23 ++++-- libs/database/src/definitions.rs | 1 + libs/generated/src/graphql/backend/graphql.ts | 7 ++ 7 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 apps/backend/src/importer/igdb.rs diff --git a/apps/backend/src/importer/igdb.rs b/apps/backend/src/importer/igdb.rs new file mode 100644 index 0000000000..911e502fcd --- /dev/null +++ b/apps/backend/src/importer/igdb.rs @@ -0,0 +1,74 @@ +use async_graphql::Result; +use csv::Reader; +use database::{MediaLot, MediaSource}; +use itertools::Itertools; +use rust_decimal_macros::dec; +use serde::Deserialize; + +use crate::{ + miscellaneous::DefaultCollection, + models::media::{ + ImportOrExportItemIdentifier, ImportOrExportMediaItem, ImportOrExportMediaItemSeen, + }, +}; + +use super::{DeployIgdbImportInput, ImportFailStep, ImportFailedItem, ImportResult}; + +#[derive(Debug, Deserialize)] +struct Item { + id: String, + game: String, +} + +pub async fn import(input: DeployIgdbImportInput) -> Result { + let lot = MediaLot::VideoGame; + let source = MediaSource::Igdb; + let collection = input.collection; + let mut media = vec![]; + let mut failed_items = vec![]; + let items = Reader::from_path(input.csv_path) + .unwrap() + .deserialize() + .collect_vec(); + let seen_history = if collection == DefaultCollection::Completed.to_string() { + vec![ImportOrExportMediaItemSeen { + ..Default::default() + }] + } else if collection == DefaultCollection::InProgress.to_string() { + vec![ImportOrExportMediaItemSeen { + progress: Some(dec!(5)), + ..Default::default() + }] + } else { + vec![] + }; + for (idx, result) in items.into_iter().enumerate() { + let record: Item = match result { + Ok(r) => r, + Err(e) => { + failed_items.push(ImportFailedItem { + lot: Some(lot), + step: ImportFailStep::InputTransformation, + identifier: idx.to_string(), + error: Some(e.to_string()), + }); + continue; + } + }; + media.push(ImportOrExportMediaItem { + collections: vec![collection.clone()], + internal_identifier: Some(ImportOrExportItemIdentifier::NeedsDetails(record.id)), + lot, + source, + source_id: record.game, + identifier: "".to_string(), + seen_history: seen_history.clone(), + reviews: vec![], + }); + } + Ok(ImportResult { + media, + failed_items, + ..Default::default() + }) +} diff --git a/apps/backend/src/importer/mod.rs b/apps/backend/src/importer/mod.rs index 4ae96ee816..739e76faf6 100644 --- a/apps/backend/src/importer/mod.rs +++ b/apps/backend/src/importer/mod.rs @@ -37,6 +37,7 @@ use crate::{ mod audiobookshelf; mod generic_json; mod goodreads; +mod igdb; mod imdb; mod jellyfin; mod mal; @@ -91,6 +92,13 @@ pub struct DeployStrongAppImportInput { mapping: Vec, } +#[derive(Debug, InputObject, Serialize, Deserialize, Clone)] +pub struct DeployIgdbImportInput { + // The path to the CSV file in the local file system. + csv_path: String, + collection: String, +} + #[derive(Debug, InputObject, Serialize, Deserialize, Clone)] pub struct DeployJsonImportInput { // The file path of the uploaded JSON export. @@ -113,13 +121,14 @@ pub struct DeployUrlAndKeyAndUsernameImportInput { #[derive(Debug, InputObject, Serialize, Deserialize, Clone)] pub struct DeployImportJobInput { pub source: ImportSource, - pub generic_csv: Option, + pub mal: Option, + pub igdb: Option, pub trakt: Option, pub movary: Option, - pub mal: Option, - pub strong_app: Option, pub generic_json: Option, + pub strong_app: Option, pub url_and_key: Option, + pub generic_csv: Option, pub jellyfin: Option, } @@ -291,6 +300,7 @@ impl ImporterService { ) .await .unwrap(), + ImportSource::Igdb => igdb::import(input.igdb.unwrap()).await.unwrap(), ImportSource::Imdb => imdb::import( input.generic_csv.unwrap(), &self diff --git a/apps/backend/src/miscellaneous/resolver.rs b/apps/backend/src/miscellaneous/resolver.rs index 7878e57b31..9053200198 100644 --- a/apps/backend/src/miscellaneous/resolver.rs +++ b/apps/backend/src/miscellaneous/resolver.rs @@ -2460,7 +2460,10 @@ impl MiscellaneousService { tracing::debug!("Progress update finished on = {:?}", finished_on); let (progress, started_on) = if matches!(action, ProgressUpdateAction::JustStarted) { - (dec!(0), Some(Utc::now().date_naive())) + ( + input.progress.unwrap_or(dec!(0)), + Some(Utc::now().date_naive()), + ) } else { (dec!(100), None) }; diff --git a/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx b/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx index e843ad2b7d..755e936bc8 100644 --- a/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx +++ b/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx @@ -51,7 +51,7 @@ import { z } from "zod"; import { confirmWrapper } from "~/components/confirmation"; import events from "~/lib/events"; import { dayjsLib } from "~/lib/generals"; -import { useCoreDetails } from "~/lib/hooks"; +import { useCoreDetails, useUserCollections } from "~/lib/hooks"; import { createToastHeaders, getAuthorizationHeader, @@ -130,6 +130,9 @@ export const action = unstable_defineAction(async ({ request }) => { .with(ImportSource.Jellyfin, async () => ({ jellyfin: processSubmission(formData, jellyfinImportFormSchema), })) + .with(ImportSource.Igdb, async () => ({ + igdb: processSubmission(formData, igdbImportFormSchema), + })) .exhaustive(); await serverGqlService.request( DeployImportJobDocument, @@ -182,6 +185,10 @@ const jellyfinImportFormSchema = usernameImportFormSchema const genericCsvImportFormSchema = z.object({ csvPath: z.string() }); +const igdbImportFormSchema = z + .object({ collection: z.string() }) + .merge(genericCsvImportFormSchema); + const movaryImportFormSchema = z.object({ ratings: z.string(), history: z.string(), @@ -207,6 +214,7 @@ const deployExportForm = z.object({ export default function Page() { const loaderData = useLoaderData(); const coreDetails = useCoreDetails(); + const userCollections = useUserCollections(); const [deployImportSource, setDeployImportSource] = useState(); const fetcher = useFetcher(); @@ -251,6 +259,7 @@ export default function Page() { () => "audiobookshelf", ) .with(ImportSource.Imdb, () => "imdb") + .with(ImportSource.Igdb, () => "igdb") .with(ImportSource.Jellyfin, () => "jellyfin") .with(ImportSource.GenericJson, () => "generic-json") .with(ImportSource.OpenScale, () => "open-scale") @@ -364,6 +373,22 @@ export default function Page() { /> )) + .with(ImportSource.Igdb, () => ( + <> +