Skip to content

Commit

Permalink
ryot import
Browse files Browse the repository at this point in the history
  • Loading branch information
bonukai committed Dec 21, 2024
1 parent a16e468 commit 2710959
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 8 deletions.
25 changes: 19 additions & 6 deletions client/src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { UpcomingPage } from './pages/UpcomingPage';
import { trpc } from './utils/trpc';
import { Trans } from '@lingui/macro';
import { Img, Poster } from './components/Poster';
import { ImportFormRyotPage } from './pages/import/ImportFormRyot.tsx';

const elementWithParamsFactory = (
fn: (params: Readonly<Params<string>>) => React.JSX.Element
Expand All @@ -56,15 +57,23 @@ const TestPage = () => {
}

return (
<div className='flex gap-3'>
<div className='w-80'>
<div className="flex gap-3">
<div className="w-80">
<Poster mediaItem={details.data} width={640} />
</div>
<div className='w-80'>
<Img alt='poster' aspectRatio='2/3' src='/api/v1/img/get?id=c8ea90e8481a78091fe48406aea535a2&width=640' />
<div className="w-80">
<Img
alt="poster"
aspectRatio="2/3"
src="/api/v1/img/get?id=c8ea90e8481a78091fe48406aea535a2&width=640"
/>
</div>
<div className='w-80'>
<Img alt='poster' aspectRatio='2/3' src='/api/v1/img/get?id=13213&width=640' />
<div className="w-80">
<Img
alt="poster"
aspectRatio="2/3"
src="/api/v1/img/get?id=13213&width=640"
/>
</div>
</div>
);
Expand Down Expand Up @@ -142,6 +151,10 @@ export const router = createBrowserRouter([
path: '/import/trakt',
element: <ImportFromTraktPage />,
},
{
path: '/import/ryot',
element: <ImportFormRyotPage />,
},
{
path: '/import/mediatracker',
element: <ImportFormMediaTrackerPage />,
Expand Down
Binary file modified client/src/assets/ryot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions client/src/pages/ImportPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Button } from '../components/Button';
import {
floxLogo,
goodreadsLogo,
ryotLogo,
simklLogo,
traktLogoBlack,
} from '../components/Logos';
Expand All @@ -24,6 +25,7 @@ export const ImportPage: FC = () => {
<ImportLinkComponent path="trakt" imgSrc={traktLogoBlack} />
<ImportLinkComponent path="flox" imgSrc={floxLogo} />
<ImportLinkComponent path="simkl" imgSrc={simklLogo} />
<ImportLinkComponent path="ryot" imgSrc={ryotLogo} />
<ImportLinkComponent path="mediatracker" text="backup" />
</div>
</>
Expand Down
10 changes: 9 additions & 1 deletion client/src/pages/import/ImportFormMediaTrackerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ export const ImportFormMediaTrackerPage: FC = () => {
<ImportFormFilePage
source="MediaTracker"
fileType="application/json"
itemTypes={['movie', 'tv', 'episode', 'season']}
itemTypes={[
'movie',
'tv',
'episode',
'season',
'audiobook',
'book',
'video_game',
]}
instructions={<div>{/* <Trans></Trans> */}</div>}
/>
</>
Expand Down
27 changes: 27 additions & 0 deletions client/src/pages/import/ImportFormRyot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { FC } from 'react';
import { ImportFormFilePage } from '../ImportFromFilePage';
import { Trans } from '@lingui/macro';
import { MainTitle } from '../../components/MainTitle';

export const ImportFormRyotPage: FC = () => {
return (
<>
<MainTitle elements={[<Trans>Import</Trans>, <Trans>from Ryot</Trans>]} />

<ImportFormFilePage
source="Ryot"
fileType="application/json"
itemTypes={[
'movie',
'tv',
'episode',
'season',
'audiobook',
'book',
'video_game',
]}
instructions={<div></div>}
/>
</>
);
};
2 changes: 1 addition & 1 deletion src/entity/mediaItemModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const mediaItemModelSchema = z.object({
narrators: z.string().nullish(),
platform: z.string().nullish(),
language: z.string().nullish(),
numberOfPages: z.number().nullish(),
numberOfPages: z.coerce.number().nullish(),
audibleCountryCode: audibleCountryCodeSchema.nullish(),
needsDetails: z.coerce.boolean().nullish(),
upcomingEpisodeId: z.number().nullish(),
Expand Down
200 changes: 200 additions & 0 deletions src/import/ryotImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import _ from 'lodash';
import { z } from 'zod';

import {
ImportDataType,
ImportListItem,
ImportRatingItem,
ImportSeenHistoryItem,
ImportWatchlistItem,
} from '../repository/importRepository.js';
import { parseISO } from 'date-fns';
import { itemTypeSchema } from '../entity/mediaItemModel.js';

export const ryotImport = {
map(json: string): ImportDataType {
const { media } = ryotExportSchema.parse(JSON.parse(json));

const lists = _.uniq(
media
.filter((item) => item.collections.length > 0)
.flatMap((item) => item.collections)
.filter((collection) => collection !== 'Watchlist')
);

return {
seenHistory: media
.filter((item) => item.seen_history.length > 0)
.flatMap((item) =>
item.seen_history.map((history) => {
const isEpisode =
item.lot === 'tv' &&
typeof history.show_season_number === 'number' &&
typeof history.show_episode_number === 'number';

return <ImportSeenHistoryItem>{
itemType: isEpisode ? 'episode' : item.lot,
title: item.source_id,
...toExternalId(item.source, item.identifier),
seenAt: history.ended_on,
episode: isEpisode
? {
seasonNumber: history.show_season_number,
episodeNumber: history.show_episode_number,
}
: undefined,
};
})
),
ratings: media
.filter((item) => item.reviews.length > 0)
.map((item) => {
const review = item.reviews.at(-1)!;

const isEpisode =
item.lot === 'tv' &&
typeof review.show_season_number === 'number' &&
typeof review.show_episode_number === 'number';

return <ImportRatingItem>{
itemType: isEpisode ? 'episode' : item.lot,
title: item.source_id,
...toExternalId(item.source, item.identifier),
rating:
typeof review.rating === 'number'
? review.rating / 10
: undefined,
episode: isEpisode
? {
seasonNumber: review.show_season_number,
episodeNumber: review.show_episode_number,
}
: undefined,
};
}),
watchlist: media
.filter((item) => item.collections.includes('Watchlist'))
.filter((item) => itemTypeSchema.options.includes(item.lot as any))
.map(
(item) =>
<ImportWatchlistItem>{
itemType: item.lot,
title: item.source_id,
...toExternalId(item.source, item.identifier),
}
),
lists: lists.map(
(listName) =>
<ImportListItem>{
name: listName,
items: media
.filter((item) => item.collections.includes(listName))
.map((item) => ({
itemType: item.lot,
title: item.source_id,
...toExternalId(item.source, item.identifier),
})),
}
),
};
},
};

const ryotExportSchema = z.object({
media: z.array(
z.object({
lot: z
.enum([
'movie',
'book',
'anime',
'show',
'video_game',
'podcast',
'visual_novel',
'manga',
'audio_book',
])
.transform((value) => {
if (value === 'show') {
return 'tv';
} else if (value === 'audio_book') {
return 'audiobook';
}
return value;
}),
source_id: z.string(),
source: z.enum([
'tmdb',
'custom',
'itunes',
'anilist',
'igdb',
'vndb',
'openlibrary',
'google_books',
'mal',
'manga_updates',
'audible',
]),
identifier: z.string(),
seen_history: z.array(
z.object({
progress: z.coerce.number(),
ended_on: z
.string()
.transform((value) => parseISO(value))
.optional(),
show_season_number: z.number().optional(),
show_episode_number: z.number().optional(),
provider_watched_on: z.string().optional(),
})
),
reviews: z.array(
z.object({
review: z.object({
visibility: z.string(),
date: z.string().transform((value) => parseISO(value)),
spoiler: z.boolean(),
}),
comments: z
.array(
z.object({
id: z.string(),
text: z.string(),
user: z.object({ id: z.string(), name: z.string() }),
liked_by: z.array(z.string()),
created_on: z.string(),
})
)
.optional(),
rating: z.coerce.number().optional(),
show_season_number: z.number().optional(),
show_episode_number: z.number().optional(),
anime_episode_number: z.number().optional(),
})
),
collections: z.array(z.string()),
})
),
});

const toExternalId = (source: string, identifier: string) => {
if (source === 'tmdb') {
return { tmdbId: parseInt(identifier) };
} else if (source === 'imdb') {
return { igdbId: identifier };
} else if (source === 'tvmaze') {
return { igdbId: parseInt(identifier) };
} else if (source === 'trakt') {
return { igdbId: parseInt(identifier) };
} else if (source === 'goodreads') {
return { igdbId: parseInt(identifier) };
} else if (source === 'igdb') {
return { igdbId: parseInt(identifier) };
} else if (source === 'openlibrary') {
return { openlibraryId: identifier };
} else if (source === 'audible') {
return { audibleId: identifier };
}
};
4 changes: 4 additions & 0 deletions src/routers/importRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import {
ImportState,
} from '../repository/importRepository.js';
import { protectedProcedure, router } from '../router.js';
import { ryotImport } from '../import/ryotImport.js';

const importSourceSchema = z.enum([
'Flox',
'TraktTv',
'Goodreads',
'Simkl',
'MediaTracker',
'Ryot',
]);

export type ImportSource = z.infer<typeof importSourceSchema>;
Expand Down Expand Up @@ -134,5 +136,7 @@ const mapImportData = async (source: ImportSource, data: string) => {
return simklImport.map(data);
case 'MediaTracker':
return backupImport.map(data);
case 'Ryot':
return ryotImport.map(data);
}
};

0 comments on commit 2710959

Please sign in to comment.