mirror of
https://github.com/p-stream/p-stream.git
synced 2026-08-12 02:01:53 +00:00
Merge branch 'p-stream:production' into production
This commit is contained in:
commit
2e88b778b3
3 changed files with 183 additions and 2 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
122
src/pages/parts/admin/ProgressCleanupPart.tsx
Normal file
122
src/pages/parts/admin/ProgressCleanupPart.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue