diff --git a/src/backend/accounts/bookmarks.ts b/src/backend/accounts/bookmarks.ts new file mode 100644 index 00000000..1872dcd5 --- /dev/null +++ b/src/backend/accounts/bookmarks.ts @@ -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( + `/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, + } + ); +} diff --git a/src/backend/helpers/report.ts b/src/backend/helpers/report.ts index 31505c10..867ffaa0 100644 --- a/src/backend/helpers/report.ts +++ b/src/backend/helpers/report.ts @@ -130,6 +130,7 @@ export function scrapePartsToProviderMetric( export function useReportProviders() { const report = useCallback((items: ProviderMetric[]) => { + if (items.length === 0) return; reportProviders(items); }, []); diff --git a/src/index.tsx b/src/index.tsx index 293dc9a5..08567b27 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,8 @@ import "core-js/stable"; +import "./stores/__old/imports"; +import "@/setup/ga"; +import "@/setup/index.css"; + import React, { Suspense } from "react"; import type { ReactNode } from "react"; import ReactDOM from "react-dom"; @@ -13,13 +17,11 @@ import { MigrationPart } from "@/pages/parts/migrations/MigrationPart"; import App from "@/setup/App"; import { conf } from "@/setup/config"; import i18n from "@/setup/i18n"; -import "@/setup/ga"; -import "@/setup/index.css"; +import { BookmarkSyncer } from "@/stores/bookmarks/BookmarkSyncer"; import { useLanguageStore } from "@/stores/language"; import { useThemeStore } from "@/stores/theme"; import { initializeChromecast } from "./setup/chromecast"; -import "./stores/__old/imports"; import { initializeOldStores } from "./stores/__old/migrations"; // initialize @@ -77,6 +79,7 @@ ReactDOM.render( }> + diff --git a/src/stores/bookmarks/BookmarkSyncer.tsx b/src/stores/bookmarks/BookmarkSyncer.tsx new file mode 100644 index 00000000..7455aa47 --- /dev/null +++ b/src/stores/bookmarks/BookmarkSyncer.tsx @@ -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; +} diff --git a/src/stores/bookmarks/index.ts b/src/stores/bookmarks/index.ts index a4929ac8..b3825a10 100644 --- a/src/stores/bookmarks/index.ts +++ b/src/stores/bookmarks/index.ts @@ -12,25 +12,59 @@ export interface BookmarkMediaItem { updatedAt: number; } +export interface BookmarkUpdateItem { + tmdbId: string; + title?: string; + year?: number; + id: string; + poster?: string; + type?: "show" | "movie"; + action: "delete" | "add"; +} + export interface BookmarkStore { bookmarks: Record; + updateQueue: BookmarkUpdateItem[]; addBookmark(meta: PlayerMeta): void; removeBookmark(id: string): void; replaceBookmarks(items: Record): void; clear(): void; + clearUpdateQueue(): void; + removeUpdateItem(id: string): void; } +let updateId = 0; + export const useBookmarkStore = create( persist( immer((set) => ({ bookmarks: {}, + updateQueue: [], removeBookmark(id) { set((s) => { + updateId += 1; + s.updateQueue.push({ + id: updateId.toString(), + action: "delete", + tmdbId: id, + }); + delete s.bookmarks[id]; }); }, addBookmark(meta) { 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] = { type: meta.type, title: meta.title, @@ -48,6 +82,16 @@ export const useBookmarkStore = create( clear() { this.replaceBookmarks({}); }, + clearUpdateQueue() { + set((s) => { + s.updateQueue = []; + }); + }, + removeUpdateItem(id: string) { + set((s) => { + s.updateQueue = [...s.updateQueue.filter((v) => v.id !== id)]; + }); + }, })), { name: "__MW::bookmarks", diff --git a/src/stores/progress/index.ts b/src/stores/progress/index.ts index 6c076f9d..905e2201 100644 --- a/src/stores/progress/index.ts +++ b/src/stores/progress/index.ts @@ -35,6 +35,19 @@ export interface ProgressMediaItem { episodes: Record; } +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 { meta: PlayerMeta; progress: ProgressItem; @@ -42,18 +55,29 @@ export interface UpdateItemOptions { export interface ProgressStore { items: Record; + updateQueue: ProgressUpdateItem[]; updateItem(ops: UpdateItemOptions): void; removeItem(id: string): void; replaceItems(items: Record): void; clear(): void; } +let updateId = 0; + export const useProgressStore = create( persist( immer((set) => ({ items: {}, + updateQueue: [], removeItem(id) { set((s) => { + updateId += 1; + s.updateQueue.push({ + id: updateId.toString(), + action: "delete", + tmdbId: id, + }); + delete s.items[id]; }); }, @@ -64,6 +88,22 @@ export const useProgressStore = create( }, updateItem({ meta, progress }) { 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]) s.items[meta.tmdbId] = { type: meta.type,