fix watch history issues

This commit is contained in:
Pas 2026-01-18 10:28:48 -07:00
parent bd717b6b95
commit 04f5958993
9 changed files with 175 additions and 31 deletions

View file

@ -6,6 +6,7 @@ import { AccountWithToken } from "@/stores/auth";
import { BookmarkInput } from "./bookmarks";
import { ProgressInput } from "./progress";
import { SettingsInput } from "./settings";
import { WatchHistoryInput, watchHistoryItemsToInputs } from "./watchHistory";
export function importProgress(
url: string,
@ -46,6 +47,19 @@ export function importGroupOrder(
});
}
export function importWatchHistory(
url: string,
account: AccountWithToken,
watchHistoryItems: WatchHistoryInput[],
) {
return ofetch<void>(`/users/${account.userId}/watch-history/import`, {
method: "PUT",
body: watchHistoryItems,
baseURL: url,
headers: getAuthHeaders(account.token),
});
}
export function importSettings(
url: string,
account: AccountWithToken,

View file

@ -76,6 +76,14 @@ export function watchHistoryItemToInputs(
};
}
export function watchHistoryItemsToInputs(
watchHistoryItems: Record<string, WatchHistoryItem>,
): WatchHistoryInput[] {
return Object.entries(watchHistoryItems).map(([id, item]) =>
watchHistoryItemToInputs(id, item),
);
}
export async function setWatchHistory(
url: string,
account: AccountWithToken,

View file

@ -179,6 +179,7 @@ export function EpisodeCarousel({
// If watched (>90%), reset to 0%, otherwise set to 100%
const isWatched = percentage > 90;
const shouldMarkWatched = !isWatched;
// Get the poster URL from the mediaPosterUrl prop
const posterUrl = mediaPosterUrl;
@ -203,7 +204,7 @@ export function EpisodeCarousel({
},
},
progress: {
watched: isWatched ? 0 : 60,
watched: shouldMarkWatched ? 60 : 0, // 60 seconds (100%) for watched, 0 for unwatched
duration: 60,
},
});

View file

@ -208,7 +208,8 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
// Get the poster URL from the data
const posterUrl = data.posterUrl;
// Update progress - if watched, set to 0%, otherwise set to 100%
// Update progress - if watched, set to 0%, otherwise set to 100% (completed)
const shouldMarkWatched = !isMovieWatched;
updateItem({
meta: {
tmdbId: data.id.toString(),
@ -220,7 +221,7 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
poster: posterUrl,
},
progress: {
watched: isMovieWatched ? 0 : 60, // 60 seconds for "watched"
watched: shouldMarkWatched ? 60 : 0, // 60 seconds (100%) for watched, 0 for unwatched
duration: 60,
},
});

View file

@ -16,7 +16,9 @@ import {
importGroupOrder,
importProgress,
importSettings,
importWatchHistory,
} from "@/backend/accounts/import";
import { watchHistoryItemsToInputs } from "@/backend/accounts/watchHistory";
// import { getLoginChallengeToken, loginAccount } from "@/backend/accounts/login";
import { progressMediaItemToInputs } from "@/backend/accounts/progress";
import {
@ -39,6 +41,7 @@ import { useGroupOrderStore } from "@/stores/groupOrder";
import { usePreferencesStore } from "@/stores/preferences";
import { ProgressMediaItem, useProgressStore } from "@/stores/progress";
import { useSubtitleStore } from "@/stores/subtitles";
import { WatchHistoryItem, useWatchHistoryStore } from "@/stores/watchHistory";
export interface RegistrationData {
recaptchaToken?: string;
@ -63,6 +66,7 @@ export interface LoginData {
export function useMigration() {
const currentAccount = useAuthStore((s) => s.account);
const progress = useProgressStore((s) => s.items);
const watchHistory = useWatchHistoryStore((s) => s.items);
const bookmarks = useBookmarkStore((s) => s.bookmarks);
const groupOrder = useGroupOrderStore((s) => s.groupOrder);
const preferences = usePreferencesStore.getState();
@ -77,11 +81,13 @@ export function useMigration() {
backendUrlInner: string,
account: AccountWithToken,
progressItems: Record<string, ProgressMediaItem>,
watchHistoryItems: Record<string, WatchHistoryItem>,
bookmarkItems: Record<string, BookmarkMediaItem>,
groupOrderItems: string[],
) => {
if (
Object.keys(progressItems).length === 0 &&
Object.keys(watchHistoryItems).length === 0 &&
Object.keys(bookmarkItems).length === 0 &&
groupOrderItems.length === 0
) {
@ -92,12 +98,15 @@ export function useMigration() {
([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item),
);
const watchHistoryInputs = watchHistoryItemsToInputs(watchHistoryItems);
const bookmarkInputs = Object.entries(bookmarkItems).map(
([tmdbId, item]) => bookmarkMediaToInput(tmdbId, item),
);
const importPromises = [
importProgress(backendUrlInner, account, progressInputs),
importWatchHistory(backendUrlInner, account, watchHistoryInputs),
importBookmarks(backendUrlInner, account, bookmarkInputs),
];
@ -177,7 +186,7 @@ export function useMigration() {
bytesToBase64(keys.seed),
);
await importData(backendUrl, account, progress, bookmarks, groupOrder);
await importData(backendUrl, account, progress, watchHistory, bookmarks, groupOrder);
return account;
},
@ -186,6 +195,7 @@ export function useMigration() {
userDataLogin,
bookmarks,
progress,
watchHistory,
groupOrder,
preferences,
subtitleLanguage,

View file

@ -16,6 +16,7 @@ import { useBookmarkStore } from "@/stores/bookmarks";
import { useGroupOrderStore } from "@/stores/groupOrder";
import { useProgressStore } from "@/stores/progress";
import { useSubtitleStore } from "@/stores/subtitles";
import { useWatchHistoryStore } from "@/stores/watchHistory";
export function MigrationDownloadPage() {
const { t } = useTranslation();
@ -23,6 +24,7 @@ export function MigrationDownloadPage() {
const navigate = useNavigate();
const bookmarks = useBookmarkStore((s) => s.bookmarks);
const progress = useProgressStore((s) => s.items);
const watchHistory = useWatchHistoryStore((s) => s.items);
const groupOrder = useGroupOrderStore((s) => s.groupOrder);
// Get data from localStorage directly to ensure we have the persisted data
@ -37,6 +39,7 @@ export function MigrationDownloadPage() {
const persistedBookmarks = getPersistedData("__MW::bookmarks");
const persistedProgress = getPersistedData("__MW::progress");
const persistedWatchHistory = getPersistedData("__MW::watchHistory");
const persistedGroupOrder = getPersistedData("__MW::groupOrder");
const persistedPreferences = getPersistedData("__MW::preferences");
const persistedSubtitles = getPersistedData("__MW::subtitles");
@ -55,6 +58,7 @@ export function MigrationDownloadPage() {
},
bookmarks: persistedBookmarks.bookmarks || bookmarks,
progress: persistedProgress.items || progress,
watchHistory: persistedWatchHistory.items || watchHistory,
groupOrder: persistedGroupOrder.groupOrder || groupOrder,
settings: {
...persistedPreferences,
@ -104,13 +108,15 @@ export function MigrationDownloadPage() {
console.error("Error during data download:", error);
setStatus("error");
}
}, [
}, [
bookmarks,
progress,
watchHistory,
user.account,
groupOrder,
persistedBookmarks,
persistedProgress,
persistedWatchHistory,
persistedGroupOrder,
persistedPreferences,
persistedSubtitles,
@ -138,7 +144,7 @@ export function MigrationDownloadPage() {
<h3 className="font-bold text-white text-lg">
{t("migration.preview.downloadDescription")}
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.CLOCK} className="text-xl" />
@ -166,6 +172,18 @@ export function MigrationDownloadPage() {
</div>
</div>
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.HISTORY} className="text-xl" />
<span className="font-medium">
{t("migration.preview.items.watchHistory")}
</span>
</div>
<div className="text-xl font-bold mt-2">
{Object.keys(persistedWatchHistory.items || watchHistory).length}
</div>
</div>
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.SETTINGS} className="text-xl" />

View file

@ -8,7 +8,9 @@ import {
importGroupOrder,
importProgress,
importSettings,
importWatchHistory,
} from "@/backend/accounts/import";
import { watchHistoryItemsToInputs } from "@/backend/accounts/watchHistory";
import { progressMediaItemToInputs } from "@/backend/accounts/progress";
import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
@ -28,6 +30,7 @@ import { usePreferencesStore } from "@/stores/preferences";
import { ProgressMediaItem, useProgressStore } from "@/stores/progress";
import { useSubtitleStore } from "@/stores/subtitles";
import { useThemeStore } from "@/stores/theme";
import { WatchHistoryItem, useWatchHistoryStore } from "@/stores/watchHistory";
interface UploadedData {
account?: {
@ -40,6 +43,7 @@ interface UploadedData {
};
bookmarks?: Record<string, BookmarkMediaItem>;
progress?: Record<string, ProgressMediaItem>;
watchHistory?: Record<string, WatchHistoryItem>;
groupOrder?: string[];
settings?: any;
theme?: string | null;
@ -55,6 +59,7 @@ export function MigrationUploadPage() {
const fileInputRef = useRef<HTMLInputElement>(null);
const replaceBookmarks = useBookmarkStore((s) => s.replaceBookmarks);
const replaceProgress = useProgressStore((s) => s.replaceItems);
const replaceWatchHistory = useWatchHistoryStore((s) => s.replaceItems);
const setGroupOrder = useGroupOrderStore((s) => s.setGroupOrder);
const preferencesStore = usePreferencesStore();
const subtitleStore = useSubtitleStore();
@ -133,6 +138,37 @@ export function MigrationUploadPage() {
{} as Record<string, ProgressMediaItem>,
)
: undefined,
watchHistory: parsedData.watchHistory
? Object.entries(parsedData.watchHistory).reduce(
(acc, [id, item]: [string, any]) => {
// Ensure type is either "show" or "movie"
if (
typeof item.type === "string" &&
(item.type === "show" || item.type === "movie")
) {
acc[id] = {
title: item.title || "",
poster: item.poster,
type: item.type as "show" | "movie",
year: typeof item.year === "number" ? item.year : undefined,
progress: item.progress,
watchedAt:
typeof item.watchedAt === "number"
? item.watchedAt
: Date.now(),
completed: typeof item.completed === "boolean" ? item.completed : false,
episodeId: item.episodeId,
seasonId: item.seasonId,
seasonNumber: item.seasonNumber,
episodeNumber: item.episodeNumber,
};
}
return acc;
},
{} as Record<string, WatchHistoryItem>,
)
: undefined,
};
setUploadedData(validatedData);
@ -173,6 +209,17 @@ export function MigrationUploadPage() {
);
}
// Import watch history
if (
uploadedData.watchHistory &&
Object.keys(uploadedData.watchHistory).length > 0
) {
const watchHistoryInputs = watchHistoryItemsToInputs(uploadedData.watchHistory);
importPromises.push(
importWatchHistory(backendUrl, user.account, watchHistoryInputs),
);
}
// Import bookmarks
if (
uploadedData.bookmarks &&
@ -234,6 +281,10 @@ export function MigrationUploadPage() {
replaceProgress(uploadedData.progress);
}
if (uploadedData.watchHistory) {
replaceWatchHistory(uploadedData.watchHistory);
}
// Import all data types to backend
try {
await handleBackendImport();
@ -269,6 +320,13 @@ export function MigrationUploadPage() {
);
replaceProgress(uploadedData.progress);
}
if (uploadedData.watchHistory) {
localStorage.setItem(
"__MW::watchHistory",
JSON.stringify({ state: { items: uploadedData.watchHistory } }),
);
replaceWatchHistory(uploadedData.watchHistory);
}
if (uploadedData.groupOrder) {
localStorage.setItem(
"__MW::groupOrder",
@ -550,7 +608,7 @@ export function MigrationUploadPage() {
</Heading2>
<Divider marginClass="my-6 px-8 box-content -mx-8" />
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-3 gap-4">
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.CLOCK} className="text-xl" />
@ -578,6 +636,20 @@ export function MigrationUploadPage() {
: 0}
</div>
</div>
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.HISTORY} className="text-xl" />
<span className="font-medium">
{t("migration.preview.items.watchHistory")}
</span>
</div>
<div className="text-xl font-bold mt-2">
{uploadedData.watchHistory
? Object.keys(uploadedData.watchHistory).length
: 0}
</div>
</div>
</div>
<div className="flex py-6 flex-col space-y-2 items-center justify-center">

View file

@ -141,13 +141,17 @@ export const useProgressStore = create(
duration: 0,
watched: 0,
};
const wasCompleted = item.progress.duration > 0 && item.progress.watched / item.progress.duration > 0.9;
item.progress = { ...progress };
// Update watch history
const completed =
// Update watch history only if becoming completed
const isCompleted =
progress.duration > 0 &&
progress.watched / progress.duration > 0.9;
useWatchHistoryStore.getState().addItem(meta, progress, completed);
if (isCompleted && !wasCompleted) {
useWatchHistoryStore.getState().addItem(meta, progress, true);
}
return;
}
@ -173,12 +177,16 @@ export const useProgressStore = create(
},
};
item.episodes[meta.episode.tmdbId].progress = { ...progress };
const episodeItem = item.episodes[meta.episode.tmdbId];
const wasCompleted = episodeItem.progress.duration > 0 && episodeItem.progress.watched / episodeItem.progress.duration > 0.9;
episodeItem.progress = { ...progress };
// Update watch history
const completed =
// Update watch history only if becoming completed
const isCompleted =
progress.duration > 0 && progress.watched / progress.duration > 0.9;
useWatchHistoryStore.getState().addItem(meta, progress, completed);
if (isCompleted && !wasCompleted) {
useWatchHistoryStore.getState().addItem(meta, progress, true);
}
});
},
clear() {

View file

@ -70,6 +70,16 @@ export const useWatchHistoryStore = create(
updateQueue: [],
addItem(meta, progress, completed) {
set((s) => {
const key = meta.episode
? `${meta.tmdbId}-${meta.episode.tmdbId}`
: meta.tmdbId;
// Only add/update if this is a completion or if the item doesn't exist yet
const existingItem = s.items[key];
const shouldUpdate = !existingItem || (completed && !existingItem.completed);
if (!shouldUpdate) return;
// add to updateQueue
updateId += 1;
s.updateQueue.push({
@ -90,9 +100,6 @@ export const useWatchHistoryStore = create(
});
// add to watch history store
const key = meta.episode
? `${meta.tmdbId}-${meta.episode.tmdbId}`
: meta.tmdbId;
s.items[key] = {
type: meta.type,
title: meta.title,
@ -110,32 +117,37 @@ export const useWatchHistoryStore = create(
},
updateItem(id, progress, completed) {
set((s) => {
if (!s.items[id]) return;
const existingItem = s.items[id];
if (!existingItem) return;
// Only update if this is becoming completed and wasn't completed before
const shouldUpdate = completed && !existingItem.completed;
if (!shouldUpdate) return;
// add to updateQueue
updateId += 1;
const item = s.items[id];
s.updateQueue.push({
tmdbId: item.episodeId ? item.seasonId || id.split("-")[0] : id,
title: item.title,
year: item.year,
poster: item.poster,
type: item.type,
tmdbId: existingItem.episodeId ? existingItem.seasonId || id.split("-")[0] : id,
title: existingItem.title,
year: existingItem.year,
poster: existingItem.poster,
type: existingItem.type,
progress: { ...progress },
watchedAt: Date.now(),
completed,
id: updateId.toString(),
episodeId: item.episodeId,
seasonId: item.seasonId,
seasonNumber: item.seasonNumber,
episodeNumber: item.episodeNumber,
episodeId: existingItem.episodeId,
seasonId: existingItem.seasonId,
seasonNumber: existingItem.seasonNumber,
episodeNumber: existingItem.episodeNumber,
action: "update",
});
// update item
item.progress = { ...progress };
item.watchedAt = Date.now();
item.completed = completed;
existingItem.progress = { ...progress };
existingItem.watchedAt = Date.now();
existingItem.completed = completed;
});
},
removeItem(id) {