Skip to content

Commit

Permalink
Allow import IGDb lists (#898)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
IgnisDa authored Jul 3, 2024
1 parent 46b164c commit c1fc326
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 10 deletions.
74 changes: 74 additions & 0 deletions apps/backend/src/importer/igdb.rs
Original file line number Diff line number Diff line change
@@ -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<ImportResult> {
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()
})
}
16 changes: 13 additions & 3 deletions apps/backend/src/importer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use crate::{
mod audiobookshelf;
mod generic_json;
mod goodreads;
mod igdb;
mod imdb;
mod jellyfin;
mod mal;
Expand Down Expand Up @@ -91,6 +92,13 @@ pub struct DeployStrongAppImportInput {
mapping: Vec<StrongAppImportMapping>,
}

#[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.
Expand All @@ -113,13 +121,14 @@ pub struct DeployUrlAndKeyAndUsernameImportInput {
#[derive(Debug, InputObject, Serialize, Deserialize, Clone)]
pub struct DeployImportJobInput {
pub source: ImportSource,
pub generic_csv: Option<DeployGenericCsvImportInput>,
pub mal: Option<DeployMalImportInput>,
pub igdb: Option<DeployIgdbImportInput>,
pub trakt: Option<DeployTraktImportInput>,
pub movary: Option<DeployMovaryImportInput>,
pub mal: Option<DeployMalImportInput>,
pub strong_app: Option<DeployStrongAppImportInput>,
pub generic_json: Option<DeployJsonImportInput>,
pub strong_app: Option<DeployStrongAppImportInput>,
pub url_and_key: Option<DeployUrlAndKeyImportInput>,
pub generic_csv: Option<DeployGenericCsvImportInput>,
pub jellyfin: Option<DeployUrlAndKeyAndUsernameImportInput>,
}

Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion apps/backend/src/miscellaneous/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand All @@ -207,6 +214,7 @@ const deployExportForm = z.object({
export default function Page() {
const loaderData = useLoaderData<typeof loader>();
const coreDetails = useCoreDetails();
const userCollections = useUserCollections();
const [deployImportSource, setDeployImportSource] = useState<ImportSource>();

const fetcher = useFetcher<typeof action>();
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -364,6 +373,22 @@ export default function Page() {
/>
</>
))
.with(ImportSource.Igdb, () => (
<>
<Select
label="Collection"
required
name="collection"
data={userCollections.map((c) => c.name)}
/>
<FileInput
label="CSV File"
accept=".csv"
required
name="csvPath"
/>
</>
))
.with(ImportSource.Mal, () => (
<>
<FileInput
Expand Down
23 changes: 18 additions & 5 deletions docs/content/importing.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Importing

Importing is meant to be a one-time operation. They are irreversible, i.e., importing from
the same source twice will create duplicates. I recommend you make a database backup before
starting an import. To start, click on "Imports and Exports" link under the "Settings"
section in the sidebar.
the same source twice will create duplicates. I recommend you to make a database backup
before starting an import.

An import can fail at various steps. Ryot creates a report when an import completes/fails.
You can see the reports under "Import History" of the imports page.
Expand All @@ -12,8 +11,6 @@ You can see the reports under "Import History" of the imports page.

- Imports are very difficult to have 100% success rate. Though we try our best,
you might have to manually import some data from your previous provider.
- You can see description of the importing steps by going to `<your instance
url>/backend/graphql`, and then searching for `ImportFailStep` enum in search bar.
- I recommend turning on debug logging for the duration of the import using the
`RUST_LOG=ryot=debug` environment variable. This will help you help you see import
progress in the docker logs.
Expand Down Expand Up @@ -144,6 +141,22 @@ the "Watchlist" collection.
- Go the bottom and click on the "Export this list" button.
- Upload the csv file in the input.

## IGDb

You can import your lists from [IGDb](https://www.igdb.com). Each list has to be imported
separately. A few points to note:

- Importing into the "In Progress" collection will set 5% progress for the items.
- Importing into the "Completed" collection will set 100% progress for the items.
- Import into any other collection will just add the items to the collection.

### Steps

- Login to your account and go to your profile. The default activity lists can be exported
from here. Click on the list you want to export and download it as CSV.
- For your custom lists, please visit the "My Lists" page.
- Upload the CSV file and choose the collection you want to import into.

## Audiobookshelf

The Audiobookshelf importer supports importing all media that have a valid Audible ID or
Expand Down
1 change: 1 addition & 0 deletions libs/database/src/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ pub enum ImportSource {
Audiobookshelf,
GenericJson,
Goodreads,
Igdb,
Imdb,
Jellyfin,
Mal,
Expand Down
7 changes: 7 additions & 0 deletions libs/generated/src/graphql/backend/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,15 @@ export type DeployGenericCsvImportInput = {
csvPath: Scalars['String']['input'];
};

export type DeployIgdbImportInput = {
collection: Scalars['String']['input'];
csvPath: Scalars['String']['input'];
};

export type DeployImportJobInput = {
genericCsv?: InputMaybe<DeployGenericCsvImportInput>;
genericJson?: InputMaybe<DeployJsonImportInput>;
igdb?: InputMaybe<DeployIgdbImportInput>;
jellyfin?: InputMaybe<DeployUrlAndKeyAndUsernameImportInput>;
mal?: InputMaybe<DeployMalImportInput>;
movary?: InputMaybe<DeployMovaryImportInput>;
Expand Down Expand Up @@ -686,6 +692,7 @@ export enum ImportSource {
Audiobookshelf = 'AUDIOBOOKSHELF',
GenericJson = 'GENERIC_JSON',
Goodreads = 'GOODREADS',
Igdb = 'IGDB',
Imdb = 'IMDB',
Jellyfin = 'JELLYFIN',
Mal = 'MAL',
Expand Down

0 comments on commit c1fc326

Please sign in to comment.