Data importing on login and registering

Co-authored-by: William Oldham <github@binaryoverload.co.uk>
This commit is contained in:
mrjvs 2023-11-21 21:26:26 +01:00
parent 7a591c82b9
commit fa29da1757
10 changed files with 239 additions and 41 deletions

View File

@ -3,13 +3,33 @@ import { ofetch } from "ofetch";
import { getAuthHeaders } from "@/backend/accounts/auth"; import { getAuthHeaders } from "@/backend/accounts/auth";
import { BookmarkResponse } from "@/backend/accounts/user"; import { BookmarkResponse } from "@/backend/accounts/user";
import { AccountWithToken } from "@/stores/auth"; import { AccountWithToken } from "@/stores/auth";
import { BookmarkMediaItem } from "@/stores/bookmarks";
export interface BookmarkInput { export interface BookmarkMetaInput {
title: string; title: string;
year: number; year: number;
poster?: string; poster?: string;
type: string; type: string;
}
export interface BookmarkInput {
tmdbId: string; tmdbId: string;
meta: BookmarkMetaInput;
}
export function bookmarkMediaToInput(
tmdbId: string,
item: BookmarkMediaItem
): BookmarkInput {
return {
meta: {
title: item.title,
type: item.type,
poster: item.poster,
year: item.year ?? 0,
},
tmdbId,
};
} }
export async function addBookmark( export async function addBookmark(
@ -23,10 +43,7 @@ export async function addBookmark(
method: "POST", method: "POST",
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
baseURL: url, baseURL: url,
body: { body: input,
meta: input,
tmdbId: input.tmdbId,
},
} }
); );
} }

View File

@ -0,0 +1,33 @@
import { ofetch } from "ofetch";
import { getAuthHeaders } from "@/backend/accounts/auth";
import { AccountWithToken } from "@/stores/auth";
import { BookmarkInput } from "./bookmarks";
import { ProgressInput } from "./progress";
export function importProgress(
url: string,
account: AccountWithToken,
progressItems: ProgressInput[]
) {
return ofetch<void>(`/users/${account.userId}/progress/import`, {
method: "PUT",
body: progressItems,
baseURL: url,
headers: getAuthHeaders(account.token),
});
}
export function importBookmarks(
url: string,
account: AccountWithToken,
bookmarks: BookmarkInput[]
) {
return ofetch<void>(`/users/${account.userId}/bookmarks`, {
method: "PUT",
body: bookmarks,
baseURL: url,
headers: getAuthHeaders(account.token),
});
}

View File

@ -3,6 +3,7 @@ import { ofetch } from "ofetch";
import { getAuthHeaders } from "@/backend/accounts/auth"; import { getAuthHeaders } from "@/backend/accounts/auth";
import { ProgressResponse } from "@/backend/accounts/user"; import { ProgressResponse } from "@/backend/accounts/user";
import { AccountWithToken } from "@/stores/auth"; import { AccountWithToken } from "@/stores/auth";
import { ProgressMediaItem, ProgressUpdateItem } from "@/stores/progress";
export interface ProgressInput { export interface ProgressInput {
meta?: { meta?: {
@ -12,14 +13,70 @@ export interface ProgressInput {
type: string; type: string;
}; };
tmdbId: string; tmdbId: string;
watched?: number; watched: number;
duration?: number; duration: number;
seasonId?: string; seasonId?: string;
episodeId?: string; episodeId?: string;
seasonNumber?: number; seasonNumber?: number;
episodeNumber?: number; episodeNumber?: number;
} }
export function progressUpdateItemToInput(
item: ProgressUpdateItem
): ProgressInput {
return {
duration: item.progress?.duration ?? 0,
watched: item.progress?.watched ?? 0,
tmdbId: item.tmdbId,
meta: {
title: item.title ?? "",
type: item.type ?? "",
year: item.year ?? NaN,
poster: item.poster,
},
episodeId: item.episodeId,
seasonId: item.seasonId,
episodeNumber: item.episodeNumber,
seasonNumber: item.seasonNumber,
};
}
export function progressMediaItemToInputs(
tmdbId: string,
item: ProgressMediaItem
): ProgressInput[] {
if (item.type === "show") {
return Object.entries(item.episodes).flatMap(([_, episode]) => ({
duration: item.progress?.duration ?? episode.progress.duration,
watched: item.progress?.watched ?? episode.progress.watched,
tmdbId,
meta: {
title: item.title ?? "",
type: item.type ?? "",
year: item.year ?? NaN,
poster: item.poster,
},
episodeId: episode.id,
seasonId: episode.seasonId,
episodeNumber: episode.number,
seasonNumber: item.seasons[episode.seasonId].number,
}));
}
return [
{
duration: item.progress?.duration ?? 0,
watched: item.progress?.watched ?? 0,
tmdbId,
meta: {
title: item.title ?? "",
type: item.type ?? "",
year: item.year ?? NaN,
poster: item.poster,
},
},
];
}
export async function setProgress( export async function setProgress(
url: string, url: string,
account: AccountWithToken, account: AccountWithToken,

View File

@ -0,0 +1,37 @@
import { ofetch } from "ofetch";
import { getAuthHeaders } from "@/backend/accounts/auth";
import { AccountWithToken } from "@/stores/auth";
export interface SettingsInput {
applicationLanguage?: string;
applicationTheme?: string;
defaultSubtitleLanguage?: string;
}
export interface SettingsResponse {
applicationTheme?: string | null;
applicationLanguage?: string | null;
defaultSubtitleLanguage?: string | null;
}
export function updateSettings(
url: string,
account: AccountWithToken,
settings: SettingsInput
) {
return ofetch<SettingsResponse>(`/users/${account.userId}/settings`, {
method: "PUT",
body: settings,
baseURL: url,
headers: getAuthHeaders(account.token),
});
}
export function getSettings(url: string, account: AccountWithToken) {
return ofetch<SettingsResponse>(`/users/${account.userId}/settings`, {
method: "GET",
baseURL: url,
headers: getAuthHeaders(account.token),
});
}

View File

@ -1,5 +1,6 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { bookmarkMediaToInput } from "@/backend/accounts/bookmarks";
import { import {
bytesToBase64, bytesToBase64,
bytesToBase64Url, bytesToBase64Url,
@ -7,7 +8,9 @@ import {
keysFromMnemonic, keysFromMnemonic,
signChallenge, signChallenge,
} from "@/backend/accounts/crypto"; } from "@/backend/accounts/crypto";
import { importBookmarks, importProgress } from "@/backend/accounts/import";
import { getLoginChallengeToken, loginAccount } from "@/backend/accounts/login"; import { getLoginChallengeToken, loginAccount } from "@/backend/accounts/login";
import { progressMediaItemToInputs } from "@/backend/accounts/progress";
import { import {
getRegisterChallengeToken, getRegisterChallengeToken,
registerAccount, registerAccount,
@ -16,7 +19,9 @@ import { removeSession } from "@/backend/accounts/sessions";
import { getBookmarks, getProgress, getUser } from "@/backend/accounts/user"; import { getBookmarks, getProgress, getUser } from "@/backend/accounts/user";
import { useAuthData } from "@/hooks/auth/useAuthData"; import { useAuthData } from "@/hooks/auth/useAuthData";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { useAuthStore } from "@/stores/auth"; import { AccountWithToken, useAuthStore } from "@/stores/auth";
import { BookmarkMediaItem } from "@/stores/bookmarks";
import { ProgressMediaItem } from "@/stores/progress";
export interface RegistrationData { export interface RegistrationData {
recaptchaToken?: string; recaptchaToken?: string;
@ -69,7 +74,7 @@ export function useAuth() {
const user = await getUser(backendUrl, loginResult.token); const user = await getUser(backendUrl, loginResult.token);
const seedBase64 = bytesToBase64(keys.seed); const seedBase64 = bytesToBase64(keys.seed);
await userDataLogin(loginResult, user.user, user.session, seedBase64); return userDataLogin(loginResult, user.user, user.session, seedBase64);
}, },
[userDataLogin, backendUrl] [userDataLogin, backendUrl]
); );
@ -106,7 +111,7 @@ export function useAuth() {
profile: registerData.userData.profile, profile: registerData.userData.profile,
}); });
await userDataLogin( return userDataLogin(
registerResult, registerResult,
registerResult.user, registerResult.user,
registerResult.session, registerResult.session,
@ -116,6 +121,33 @@ export function useAuth() {
[backendUrl, userDataLogin] [backendUrl, userDataLogin]
); );
const importData = useCallback(
async (
account: AccountWithToken,
progressItems: Record<string, ProgressMediaItem>,
bookmarks: Record<string, BookmarkMediaItem>
) => {
if (
Object.keys(progressItems).length === 0 &&
Object.keys(bookmarks).length === 0
) {
return;
}
const progressInputs = Object.entries(progressItems).flatMap(
([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item)
);
const bookmarkInputs = Object.entries(bookmarks).map(([tmdbId, item]) =>
bookmarkMediaToInput(tmdbId, item)
);
await importProgress(backendUrl, account, progressInputs);
await importBookmarks(backendUrl, account, bookmarkInputs);
},
[backendUrl]
);
const restore = useCallback(async () => { const restore = useCallback(async () => {
if (!currentAccount) { if (!currentAccount) {
return; return;
@ -136,5 +168,6 @@ export function useAuth() {
logout, logout,
register, register,
restore, restore,
importData,
}; };
} }

View File

@ -24,19 +24,21 @@ export function useAuthData() {
const login = useCallback( const login = useCallback(
async ( async (
account: LoginResponse, loginResponse: LoginResponse,
user: UserResponse, user: UserResponse,
session: SessionResponse, session: SessionResponse,
seed: string seed: string
) => { ) => {
setAccount({ const account = {
token: account.token, token: loginResponse.token,
userId: user.id, userId: user.id,
sessionId: account.session.id, sessionId: loginResponse.session.id,
deviceName: session.device, deviceName: session.device,
profile: user.profile, profile: user.profile,
seed, seed,
}); };
setAccount(account);
return account;
}, },
[setAccount] [setAccount]
); );

View File

@ -11,6 +11,8 @@ import {
} from "@/components/layout/LargeCard"; } from "@/components/layout/LargeCard";
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
import { useAuth } from "@/hooks/auth/useAuth"; import { useAuth } from "@/hooks/auth/useAuth";
import { useBookmarkStore } from "@/stores/bookmarks";
import { useProgressStore } from "@/stores/progress";
interface LoginFormPartProps { interface LoginFormPartProps {
onLogin?: () => void; onLogin?: () => void;
@ -19,7 +21,9 @@ interface LoginFormPartProps {
export function LoginFormPart(props: LoginFormPartProps) { export function LoginFormPart(props: LoginFormPartProps) {
const [mnemonic, setMnemonic] = useState(""); const [mnemonic, setMnemonic] = useState("");
const [device, setDevice] = useState(""); const [device, setDevice] = useState("");
const { login, restore } = useAuth(); const { login, restore, importData } = useAuth();
const progressItems = useProgressStore((store) => store.items);
const bookmarkItems = useBookmarkStore((store) => store.bookmarks);
const [result, execute] = useAsyncFn( const [result, execute] = useAsyncFn(
async (inputMnemonic: string, inputdevice: string) => { async (inputMnemonic: string, inputdevice: string) => {
@ -27,14 +31,14 @@ export function LoginFormPart(props: LoginFormPartProps) {
if (!verifyValidMnemonic(inputMnemonic)) if (!verifyValidMnemonic(inputMnemonic))
throw new Error("Invalid or incomplete passphrase"); throw new Error("Invalid or incomplete passphrase");
await login({ const account = await login({
mnemonic: inputMnemonic, mnemonic: inputMnemonic,
userData: { userData: {
device: inputdevice, device: inputdevice,
}, },
}); });
// TODO import (and sort out conflicts) await importData(account, progressItems, bookmarkItems);
await restore(); await restore();

View File

@ -2,6 +2,7 @@ import { useState } from "react";
import { useGoogleReCaptcha } from "react-google-recaptcha-v3"; import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
import { useAsyncFn } from "react-use"; import { useAsyncFn } from "react-use";
import { updateSettings } from "@/backend/accounts/settings";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { import {
@ -11,7 +12,13 @@ import {
} from "@/components/layout/LargeCard"; } from "@/components/layout/LargeCard";
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
import { useAuth } from "@/hooks/auth/useAuth"; import { useAuth } from "@/hooks/auth/useAuth";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart"; import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart";
import { useBookmarkStore } from "@/stores/bookmarks";
import { useLanguageStore } from "@/stores/language";
import { useProgressStore } from "@/stores/progress";
import { useSubtitleStore } from "@/stores/subtitles";
import { useThemeStore } from "@/stores/theme";
interface VerifyPassphraseProps { interface VerifyPassphraseProps {
mnemonic: string | null; mnemonic: string | null;
@ -22,7 +29,17 @@ interface VerifyPassphraseProps {
export function VerifyPassphrase(props: VerifyPassphraseProps) { export function VerifyPassphrase(props: VerifyPassphraseProps) {
const [mnemonic, setMnemonic] = useState(""); const [mnemonic, setMnemonic] = useState("");
const { register, restore } = useAuth(); const { register, restore, importData } = useAuth();
const progressItems = useProgressStore((store) => store.items);
const bookmarkItems = useBookmarkStore((store) => store.bookmarks);
const applicationLanguage = useLanguageStore((store) => store.language);
const defaultSubtitleLanguage = useSubtitleStore(
(store) => store.lastSelectedLanguage
);
const applicationTheme = useThemeStore((store) => store.theme);
const backendUrl = useBackendUrl();
const { executeRecaptcha } = useGoogleReCaptcha(); const { executeRecaptcha } = useGoogleReCaptcha();
@ -42,13 +59,19 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
if (inputMnemonic !== props.mnemonic) if (inputMnemonic !== props.mnemonic)
throw new Error("Passphrase doesn't match"); throw new Error("Passphrase doesn't match");
await register({ const account = await register({
mnemonic: inputMnemonic, mnemonic: inputMnemonic,
userData: props.userData, userData: props.userData,
recaptchaToken, recaptchaToken,
}); });
// TODO import (and sort out conflicts) await importData(account, progressItems, bookmarkItems);
await updateSettings(backendUrl, account, {
applicationLanguage,
defaultSubtitleLanguage: defaultSubtitleLanguage ?? undefined,
applicationTheme: applicationTheme ?? undefined,
});
await restore(); await restore();

View File

@ -27,11 +27,13 @@ async function syncBookmarks(
if (item.action === "add") { if (item.action === "add") {
await addBookmark(url, account, { await addBookmark(url, account, {
meta: {
poster: item.poster, poster: item.poster,
title: item.title ?? "", title: item.title ?? "",
tmdbId: item.tmdbId,
type: item.type ?? "", type: item.type ?? "",
year: item.year ?? NaN, year: item.year ?? NaN,
},
tmdbId: item.tmdbId,
}); });
continue; continue;
} }

View File

@ -1,6 +1,10 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { removeProgress, setProgress } from "@/backend/accounts/progress"; import {
progressUpdateItemToInput,
removeProgress,
setProgress,
} from "@/backend/accounts/progress";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { AccountWithToken, useAuthStore } from "@/stores/auth"; import { AccountWithToken, useAuthStore } from "@/stores/auth";
import { ProgressUpdateItem, useProgressStore } from "@/stores/progress"; import { ProgressUpdateItem, useProgressStore } from "@/stores/progress";
@ -32,21 +36,7 @@ async function syncProgress(
} }
if (item.action === "upsert") { if (item.action === "upsert") {
await setProgress(url, account, { await setProgress(url, account, progressUpdateItemToInput(item));
duration: item.progress?.duration ?? 0,
watched: item.progress?.watched ?? 0,
tmdbId: item.tmdbId,
meta: {
title: item.title ?? "",
type: item.type ?? "",
year: item.year ?? NaN,
poster: item.poster,
},
episodeId: item.episodeId,
seasonId: item.seasonId,
episodeNumber: item.episodeNumber,
seasonNumber: item.seasonNumber,
});
continue; continue;
} }
} catch (err) { } catch (err) {