Merge branch 'p-stream:production' into production

This commit is contained in:
zisra 2025-10-31 22:00:37 +08:00 committed by GitHub
commit 2e88b778b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 183 additions and 2 deletions

View file

@ -5,10 +5,62 @@ import { playerStatus } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
import { ProgressItem, useProgressStore } from "@/stores/progress";
function progressIsNotStarted(duration: number, watched: number): boolean {
// too short watch time
if (watched < 20) return true;
return false;
}
function progressIsCompleted(duration: number, watched: number): boolean {
const timeFromEnd = duration - watched;
// too close to the end, is completed
if (timeFromEnd < 60 * 2) return true;
return false;
}
function shouldSaveProgress(
meta: any,
progress: ProgressItem,
existingItems: Record<string, any>,
): boolean {
const { duration, watched } = progress;
// Check if progress is acceptable
const isNotStarted = progressIsNotStarted(duration, watched);
const isCompleted = progressIsCompleted(duration, watched);
const isAcceptable = !isNotStarted && !isCompleted;
// For movies, only save if acceptable
if (meta.type === "movie") {
return isAcceptable;
}
// For shows, save if acceptable OR if season has other watched episodes
if (isAcceptable) return true;
// Check if this season has other episodes with progress
const showItem = existingItems[meta.tmdbId];
if (!showItem || !meta.season) return false;
const seasonEpisodes = Object.values(showItem.episodes).filter(
(episode: any) => episode.seasonId === meta.season.tmdbId,
);
// Check if any other episode in this season has acceptable progress
return seasonEpisodes.some((episode: any) => {
const epProgress = episode.progress;
return (
!progressIsNotStarted(epProgress.duration, epProgress.watched) &&
!progressIsCompleted(epProgress.duration, epProgress.watched)
);
});
}
export function ProgressSaver() {
const meta = usePlayerStore((s) => s.meta);
const progress = usePlayerStore((s) => s.progress);
const updateItem = useProgressStore((s) => s.updateItem);
const progressItems = useProgressStore((s) => s.items);
const status = usePlayerStore((s) => s.status);
const hasPlayedOnce = usePlayerStore((s) => s.mediaPlaying.hasPlayedOnce);
@ -16,6 +68,7 @@ export function ProgressSaver() {
const dataRef = useRef({
updateItem,
progressItems,
meta,
progress,
status,
@ -23,11 +76,12 @@ export function ProgressSaver() {
});
useEffect(() => {
dataRef.current.updateItem = updateItem;
dataRef.current.progressItems = progressItems;
dataRef.current.meta = meta;
dataRef.current.progress = progress;
dataRef.current.status = status;
dataRef.current.hasPlayedOnce = hasPlayedOnce;
}, [updateItem, progress, meta, status, hasPlayedOnce]);
}, [updateItem, progressItems, progress, meta, status, hasPlayedOnce]);
useInterval(() => {
const d = dataRef.current;
@ -47,7 +101,10 @@ export function ProgressSaver() {
duration: progress.duration,
watched: progress.time,
};
if (isDifferent)
if (
isDifferent &&
shouldSaveProgress(d.meta, lastSavedRef.current, d.progressItems)
)
d.updateItem({
meta: d.meta,
progress: lastSavedRef.current,

View file

@ -14,6 +14,7 @@ import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart";
import { BackendTestPart } from "../parts/admin/BackendTestPart";
import { EmbedOrderPart } from "../parts/admin/EmbedOrderPart";
import { ProgressCleanupPart } from "../parts/admin/ProgressCleanupPart";
export function AdminPage() {
const { t } = useTranslation();
@ -50,6 +51,7 @@ export function AdminPage() {
disabledEmbeds={embedOrderState.disabledEmbeds}
setDisabledEmbeds={embedOrderState.setDisabledEmbeds}
/>
<ProgressCleanupPart />
</ThinContainer>
<Transition

View file

@ -0,0 +1,122 @@
import { ofetch } from "ofetch";
import { useState } from "react";
import { useAsyncFn } from "react-use";
import { getAuthHeaders } from "@/backend/accounts/auth";
import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
import { Box } from "@/components/layout/Box";
import { Heading2 } from "@/components/utils/Text";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { AccountWithToken, useAuthStore } from "@/stores/auth";
interface CleanupResponse {
deletedCount: number;
message: string;
}
async function cleanupProgressItems(
backendUrl: string,
account: AccountWithToken,
) {
return ofetch<CleanupResponse>(`/users/${account.userId}/progress/cleanup`, {
method: "DELETE",
headers: getAuthHeaders(account.token),
baseURL: backendUrl,
});
}
export function ProgressCleanupPart() {
const backendUrl = useBackendUrl();
const account = useAuthStore((s) => s.account);
const [status, setStatus] = useState<{
hasRun: boolean;
success: boolean;
errorText: string;
result: CleanupResponse | null;
}>({
hasRun: false,
success: false,
errorText: "",
result: null,
});
const [cleanupState, runCleanup] = useAsyncFn(async () => {
setStatus({
hasRun: false,
success: false,
errorText: "",
result: null,
});
if (!backendUrl || !account) {
return setStatus({
hasRun: true,
success: false,
errorText: "Backend URL or account not available",
result: null,
});
}
try {
const result = await cleanupProgressItems(backendUrl, account);
return setStatus({
hasRun: true,
success: true,
errorText: "",
result,
});
} catch (err) {
console.error("Progress cleanup failed:", err);
return setStatus({
hasRun: true,
success: false,
errorText:
"Failed to clean up progress items. Check console for details.",
result: null,
});
}
}, [backendUrl, account]);
return (
<>
<Heading2>Progress Cleanup</Heading2>
<Box>
<div className="w-full flex gap-6 justify-between items-center">
{!status.hasRun ? (
<p>Remove unwanted progress items from the database</p>
) : status.success ? (
<p className="flex items-center text-md">
<Icon
icon={Icons.CIRCLE_CHECK}
className="text-video-scraping-success mr-2"
/>
Cleanup completed
</p>
) : (
<div>
<p className="text-white font-bold w-full mb-3 flex items-center gap-1">
<Icon
icon={Icons.CIRCLE_EXCLAMATION}
className="text-video-scraping-error mr-2"
/>
Cleanup failed
</p>
<p>{status.errorText}</p>
</div>
)}
<Button
theme="danger"
loading={cleanupState.loading}
className="whitespace-nowrap"
onClick={runCleanup}
disabled={cleanupState.loading}
>
Clean Up Progress
</Button>
</div>
</Box>
</>
);
}