bookmark syncing

This commit is contained in:
mrjvs 2023-11-19 20:03:35 +01:00
parent fa990d16b2
commit ab4d72ed1a
6 changed files with 215 additions and 3 deletions

View File

@ -0,0 +1,46 @@
import { ofetch } from "ofetch";
import { getAuthHeaders } from "@/backend/accounts/auth";
import { BookmarkResponse } from "@/backend/accounts/user";
import { AccountWithToken } from "@/stores/auth";
export interface BookmarkInput {
title: string;
year: number;
poster?: string;
type: string;
tmdbId: string;
}
export async function addBookmark(
url: string,
account: AccountWithToken,
input: BookmarkInput
) {
return ofetch<BookmarkResponse>(
`/users/${account.userId}/bookmarks/${input.tmdbId}`,
{
method: "POST",
headers: getAuthHeaders(account.token),
baseURL: url,
body: {
meta: input,
},
}
);
}
export async function removeBookmark(
url: string,
account: AccountWithToken,
id: string
) {
return ofetch<{ tmdbId: string }>(
`/users/${account.userId}/bookmarks/${id}`,
{
method: "DELETE",
headers: getAuthHeaders(account.token),
baseURL: url,
}
);
}

View File

@ -130,6 +130,7 @@ export function scrapePartsToProviderMetric(
export function useReportProviders() { export function useReportProviders() {
const report = useCallback((items: ProviderMetric[]) => { const report = useCallback((items: ProviderMetric[]) => {
if (items.length === 0) return;
reportProviders(items); reportProviders(items);
}, []); }, []);

View File

@ -1,4 +1,8 @@
import "core-js/stable"; import "core-js/stable";
import "./stores/__old/imports";
import "@/setup/ga";
import "@/setup/index.css";
import React, { Suspense } from "react"; import React, { Suspense } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
@ -13,13 +17,11 @@ import { MigrationPart } from "@/pages/parts/migrations/MigrationPart";
import App from "@/setup/App"; import App from "@/setup/App";
import { conf } from "@/setup/config"; import { conf } from "@/setup/config";
import i18n from "@/setup/i18n"; import i18n from "@/setup/i18n";
import "@/setup/ga"; import { BookmarkSyncer } from "@/stores/bookmarks/BookmarkSyncer";
import "@/setup/index.css";
import { useLanguageStore } from "@/stores/language"; import { useLanguageStore } from "@/stores/language";
import { useThemeStore } from "@/stores/theme"; import { useThemeStore } from "@/stores/theme";
import { initializeChromecast } from "./setup/chromecast"; import { initializeChromecast } from "./setup/chromecast";
import "./stores/__old/imports";
import { initializeOldStores } from "./stores/__old/migrations"; import { initializeOldStores } from "./stores/__old/migrations";
// initialize // initialize
@ -77,6 +79,7 @@ ReactDOM.render(
<HelmetProvider> <HelmetProvider>
<Suspense fallback={<LoadingScreen type="lazy" />}> <Suspense fallback={<LoadingScreen type="lazy" />}>
<ThemeProvider> <ThemeProvider>
<BookmarkSyncer />
<TheRouter> <TheRouter>
<MigrationRunner /> <MigrationRunner />
</TheRouter> </TheRouter>

View File

@ -0,0 +1,78 @@
import { useEffect } from "react";
import { addBookmark, removeBookmark } from "@/backend/accounts/bookmarks";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { AccountWithToken, useAuthStore } from "@/stores/auth";
import { BookmarkUpdateItem, useBookmarkStore } from "@/stores/bookmarks";
const syncIntervalMs = 5 * 1000;
async function syncBookmarks(
items: BookmarkUpdateItem[],
finish: (id: string) => void,
url: string,
account: AccountWithToken | null
) {
for (const item of items) {
// complete it beforehand so it doesn't get handled while in progress
finish(item.id);
if (!account) return; // not logged in, dont sync to server
try {
if (item.action === "delete") {
await removeBookmark(url, account, item.tmdbId);
continue;
}
if (item.action === "add") {
await addBookmark(url, account, {
poster: item.poster,
title: item.title ?? "",
tmdbId: item.tmdbId,
type: item.type ?? "",
year: item.year ?? NaN,
});
continue;
}
} catch (err) {
console.error(
`Failed to sync bookmark: ${item.tmdbId} - ${item.action}`,
err
);
}
}
}
export function BookmarkSyncer() {
const clearUpdateQueue = useBookmarkStore((s) => s.clearUpdateQueue);
const removeUpdateItem = useBookmarkStore((s) => s.removeUpdateItem);
const url = useBackendUrl();
// when booting for the first time, clear update queue.
// we dont want to process persisted update items
useEffect(() => {
clearUpdateQueue();
}, [clearUpdateQueue]);
useEffect(() => {
const interval = setInterval(() => {
(async () => {
const state = useBookmarkStore.getState();
const user = useAuthStore.getState();
await syncBookmarks(
state.updateQueue,
removeUpdateItem,
url,
user.account
);
})();
}, syncIntervalMs);
return () => {
clearInterval(interval);
};
}, [removeUpdateItem, url]);
return null;
}

View File

@ -12,25 +12,59 @@ export interface BookmarkMediaItem {
updatedAt: number; updatedAt: number;
} }
export interface BookmarkUpdateItem {
tmdbId: string;
title?: string;
year?: number;
id: string;
poster?: string;
type?: "show" | "movie";
action: "delete" | "add";
}
export interface BookmarkStore { export interface BookmarkStore {
bookmarks: Record<string, BookmarkMediaItem>; bookmarks: Record<string, BookmarkMediaItem>;
updateQueue: BookmarkUpdateItem[];
addBookmark(meta: PlayerMeta): void; addBookmark(meta: PlayerMeta): void;
removeBookmark(id: string): void; removeBookmark(id: string): void;
replaceBookmarks(items: Record<string, BookmarkMediaItem>): void; replaceBookmarks(items: Record<string, BookmarkMediaItem>): void;
clear(): void; clear(): void;
clearUpdateQueue(): void;
removeUpdateItem(id: string): void;
} }
let updateId = 0;
export const useBookmarkStore = create( export const useBookmarkStore = create(
persist( persist(
immer<BookmarkStore>((set) => ({ immer<BookmarkStore>((set) => ({
bookmarks: {}, bookmarks: {},
updateQueue: [],
removeBookmark(id) { removeBookmark(id) {
set((s) => { set((s) => {
updateId += 1;
s.updateQueue.push({
id: updateId.toString(),
action: "delete",
tmdbId: id,
});
delete s.bookmarks[id]; delete s.bookmarks[id];
}); });
}, },
addBookmark(meta) { addBookmark(meta) {
set((s) => { set((s) => {
updateId += 1;
s.updateQueue.push({
id: updateId.toString(),
action: "add",
tmdbId: meta.tmdbId,
type: meta.type,
title: meta.title,
year: meta.releaseYear,
poster: meta.poster,
});
s.bookmarks[meta.tmdbId] = { s.bookmarks[meta.tmdbId] = {
type: meta.type, type: meta.type,
title: meta.title, title: meta.title,
@ -48,6 +82,16 @@ export const useBookmarkStore = create(
clear() { clear() {
this.replaceBookmarks({}); this.replaceBookmarks({});
}, },
clearUpdateQueue() {
set((s) => {
s.updateQueue = [];
});
},
removeUpdateItem(id: string) {
set((s) => {
s.updateQueue = [...s.updateQueue.filter((v) => v.id !== id)];
});
},
})), })),
{ {
name: "__MW::bookmarks", name: "__MW::bookmarks",

View File

@ -35,6 +35,19 @@ export interface ProgressMediaItem {
episodes: Record<string, ProgressEpisodeItem>; episodes: Record<string, ProgressEpisodeItem>;
} }
export interface ProgressUpdateItem {
title?: string;
year?: number;
poster?: string;
type?: "show" | "movie";
progress?: ProgressItem;
tmdbId: string;
id: string;
episodeId?: string;
seasonId?: string;
action: "upsert" | "delete";
}
export interface UpdateItemOptions { export interface UpdateItemOptions {
meta: PlayerMeta; meta: PlayerMeta;
progress: ProgressItem; progress: ProgressItem;
@ -42,18 +55,29 @@ export interface UpdateItemOptions {
export interface ProgressStore { export interface ProgressStore {
items: Record<string, ProgressMediaItem>; items: Record<string, ProgressMediaItem>;
updateQueue: ProgressUpdateItem[];
updateItem(ops: UpdateItemOptions): void; updateItem(ops: UpdateItemOptions): void;
removeItem(id: string): void; removeItem(id: string): void;
replaceItems(items: Record<string, ProgressMediaItem>): void; replaceItems(items: Record<string, ProgressMediaItem>): void;
clear(): void; clear(): void;
} }
let updateId = 0;
export const useProgressStore = create( export const useProgressStore = create(
persist( persist(
immer<ProgressStore>((set) => ({ immer<ProgressStore>((set) => ({
items: {}, items: {},
updateQueue: [],
removeItem(id) { removeItem(id) {
set((s) => { set((s) => {
updateId += 1;
s.updateQueue.push({
id: updateId.toString(),
action: "delete",
tmdbId: id,
});
delete s.items[id]; delete s.items[id];
}); });
}, },
@ -64,6 +88,22 @@ export const useProgressStore = create(
}, },
updateItem({ meta, progress }) { updateItem({ meta, progress }) {
set((s) => { set((s) => {
// add to updateQueue
updateId += 1;
s.updateQueue.push({
tmdbId: meta.tmdbId,
title: meta.title,
year: meta.releaseYear,
poster: meta.poster,
type: meta.type,
progress: { ...progress },
id: updateId.toString(),
episodeId: meta.episode?.tmdbId,
seasonId: meta.season?.tmdbId,
action: "upsert",
});
// add to progress store
if (!s.items[meta.tmdbId]) if (!s.items[meta.tmdbId])
s.items[meta.tmdbId] = { s.items[meta.tmdbId] = {
type: meta.type, type: meta.type,