mirror of
https://github.com/p-stream/p-stream.git
synced 2026-08-19 05:35:38 +00:00
fix watch history issues
This commit is contained in:
parent
bd717b6b95
commit
04f5958993
9 changed files with 175 additions and 31 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue