Use content ids for episode progress keys
This commit is contained in:
+127
-29
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { AlertCircle, Cloud, Heart, Loader2, Router, Sparkles, X } from 'lucide-react';
|
import { AlertCircle, Cloud, Heart, Loader2, Router, Sparkles, X } from 'lucide-react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
||||||
import {
|
import {
|
||||||
@@ -633,12 +633,29 @@ function PlayPageClient() {
|
|||||||
// 搜索所需信息
|
// 搜索所需信息
|
||||||
const [searchTitle] = useState(searchParams.get('stitle') || '');
|
const [searchTitle] = useState(searchParams.get('stitle') || '');
|
||||||
const [searchType] = useState(searchParams.get('stype') || '');
|
const [searchType] = useState(searchParams.get('stype') || '');
|
||||||
const [episodeProgressContentKey] = useState(() =>
|
const [initialEpisodeProgressTitle] = useState(
|
||||||
buildEpisodeProgressContentKey({
|
searchTitle || searchParams.get('title') || ''
|
||||||
title: searchTitle || searchParams.get('title') || '',
|
);
|
||||||
year: searchParams.get('year') || '',
|
const [initialEpisodeProgressYear] = useState(
|
||||||
|
searchParams.get('year') || ''
|
||||||
|
);
|
||||||
|
const episodeProgressContentKey = useMemo(
|
||||||
|
() =>
|
||||||
|
buildEpisodeProgressContentKey({
|
||||||
|
doubanId: videoDoubanId || detail?.douban_id,
|
||||||
|
tmdbId: detail?.tmdb_id,
|
||||||
|
title: initialEpisodeProgressTitle,
|
||||||
|
year: initialEpisodeProgressYear,
|
||||||
|
searchType,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
detail?.douban_id,
|
||||||
|
detail?.tmdb_id,
|
||||||
|
initialEpisodeProgressTitle,
|
||||||
|
initialEpisodeProgressYear,
|
||||||
searchType,
|
searchType,
|
||||||
})
|
videoDoubanId,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 是否需要优选
|
// 是否需要优选
|
||||||
@@ -2719,13 +2736,13 @@ function PlayPageClient() {
|
|||||||
const ensureVideoSource = (video: HTMLVideoElement | null, url: string) => {
|
const ensureVideoSource = (video: HTMLVideoElement | null, url: string) => {
|
||||||
if (!video || !url) return;
|
if (!video || !url) return;
|
||||||
const sources = Array.from(video.getElementsByTagName('source'));
|
const sources = Array.from(video.getElementsByTagName('source'));
|
||||||
|
const isHlsJsActive = !!(video as any).hls;
|
||||||
const isHlsLikeSource =
|
const isHlsLikeSource =
|
||||||
!!(video as any).hls ||
|
|
||||||
/\.m3u8?($|\?)/i.test(url) ||
|
/\.m3u8?($|\?)/i.test(url) ||
|
||||||
url.includes('/api/proxy-m3u8') ||
|
url.includes('/api/proxy-m3u8') ||
|
||||||
url.includes('/api/proxy/vod/m3u8');
|
url.includes('/api/proxy/vod/m3u8');
|
||||||
|
|
||||||
if (isHlsLikeSource) {
|
if (isHlsJsActive && isHlsLikeSource) {
|
||||||
// HLS 由 hls.js 接管时,不能再给 <video> 塞原始 m3u8 source,
|
// HLS 由 hls.js 接管时,不能再给 <video> 塞原始 m3u8 source,
|
||||||
// 否则 Safari 可能切回原生 HLS,和 MSE/hls.js 抢同一个播放器。
|
// 否则 Safari 可能切回原生 HLS,和 MSE/hls.js 抢同一个播放器。
|
||||||
sources.forEach((s) => s.remove());
|
sources.forEach((s) => s.remove());
|
||||||
@@ -3898,6 +3915,13 @@ function PlayPageClient() {
|
|||||||
|
|
||||||
// 加载播放记录
|
// 加载播放记录
|
||||||
try {
|
try {
|
||||||
|
const detailEpisodeProgressContentKey = buildEpisodeProgressContentKey({
|
||||||
|
doubanId: detailData.douban_id,
|
||||||
|
tmdbId: detailData.tmdb_id,
|
||||||
|
title: initialEpisodeProgressTitle,
|
||||||
|
year: initialEpisodeProgressYear,
|
||||||
|
searchType,
|
||||||
|
});
|
||||||
const allRecords = await getAllPlayRecords();
|
const allRecords = await getAllPlayRecords();
|
||||||
const key = generateStorageKey(detailData.source, detailData.id);
|
const key = generateStorageKey(detailData.source, detailData.id);
|
||||||
const record = allRecords[key];
|
const record = allRecords[key];
|
||||||
@@ -3923,7 +3947,7 @@ function PlayPageClient() {
|
|||||||
// 否则使用点击的文件集数,从头开始播放
|
// 否则使用点击的文件集数,从头开始播放
|
||||||
initialIndex = detailData.initialEpisodeIndex;
|
initialIndex = detailData.initialEpisodeIndex;
|
||||||
const localEpisodeTime = loadLocalEpisodeProgress(
|
const localEpisodeTime = loadLocalEpisodeProgress(
|
||||||
episodeProgressContentKey,
|
detailEpisodeProgressContentKey,
|
||||||
initialIndex
|
initialIndex
|
||||||
);
|
);
|
||||||
resumeTimeRef.current = localEpisodeTime;
|
resumeTimeRef.current = localEpisodeTime;
|
||||||
@@ -3942,7 +3966,7 @@ function PlayPageClient() {
|
|||||||
// 使用点击的文件集数
|
// 使用点击的文件集数
|
||||||
initialIndex = detailData.initialEpisodeIndex;
|
initialIndex = detailData.initialEpisodeIndex;
|
||||||
resumeTimeRef.current = loadLocalEpisodeProgress(
|
resumeTimeRef.current = loadLocalEpisodeProgress(
|
||||||
episodeProgressContentKey,
|
detailEpisodeProgressContentKey,
|
||||||
initialIndex
|
initialIndex
|
||||||
);
|
);
|
||||||
console.log('[Play] 没有播放记录,使用点击的文件集数:', initialIndex);
|
console.log('[Play] 没有播放记录,使用点击的文件集数:', initialIndex);
|
||||||
@@ -3950,7 +3974,7 @@ function PlayPageClient() {
|
|||||||
// 默认从第0集开始
|
// 默认从第0集开始
|
||||||
initialIndex = 0;
|
initialIndex = 0;
|
||||||
resumeTimeRef.current = loadLocalEpisodeProgress(
|
resumeTimeRef.current = loadLocalEpisodeProgress(
|
||||||
episodeProgressContentKey,
|
detailEpisodeProgressContentKey,
|
||||||
initialIndex
|
initialIndex
|
||||||
);
|
);
|
||||||
console.log('[Play] 没有播放记录,从第0集开始');
|
console.log('[Play] 没有播放记录,从第0集开始');
|
||||||
@@ -4220,6 +4244,14 @@ function PlayPageClient() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newEpisodeProgressContentKey = buildEpisodeProgressContentKey({
|
||||||
|
doubanId: newDetail.douban_id,
|
||||||
|
tmdbId: newDetail.tmdb_id,
|
||||||
|
title: initialEpisodeProgressTitle,
|
||||||
|
year: initialEpisodeProgressYear,
|
||||||
|
searchType,
|
||||||
|
});
|
||||||
|
|
||||||
// 尝试跳转到当前正在播放的集数
|
// 尝试跳转到当前正在播放的集数
|
||||||
const previousEpisodeIndex = currentEpisodeIndexRef.current;
|
const previousEpisodeIndex = currentEpisodeIndexRef.current;
|
||||||
const previousSource = currentSourceRef.current;
|
const previousSource = currentSourceRef.current;
|
||||||
@@ -4235,7 +4267,7 @@ function PlayPageClient() {
|
|||||||
const resumeTime = isSameEpisodeSwitch
|
const resumeTime = isSameEpisodeSwitch
|
||||||
? await getSourceSwitchResumeTime(previousEpisodeIndex, currentPlayTime)
|
? await getSourceSwitchResumeTime(previousEpisodeIndex, currentPlayTime)
|
||||||
: loadLocalEpisodeProgress(
|
: loadLocalEpisodeProgress(
|
||||||
episodeProgressContentKey,
|
newEpisodeProgressContentKey,
|
||||||
targetIndex
|
targetIndex
|
||||||
);
|
);
|
||||||
resumeTimeRef.current = resumeTime;
|
resumeTimeRef.current = resumeTime;
|
||||||
@@ -4280,7 +4312,7 @@ function PlayPageClient() {
|
|||||||
if (isSameEpisodeSwitch && resumeTime && resumeTime > 1) {
|
if (isSameEpisodeSwitch && resumeTime && resumeTime > 1) {
|
||||||
const currentDuration = artPlayerRef.current?.duration || 0;
|
const currentDuration = artPlayerRef.current?.duration || 0;
|
||||||
saveLocalEpisodeProgress(
|
saveLocalEpisodeProgress(
|
||||||
episodeProgressContentKey,
|
newEpisodeProgressContentKey,
|
||||||
targetIndex,
|
targetIndex,
|
||||||
resumeTime,
|
resumeTime,
|
||||||
currentDuration
|
currentDuration
|
||||||
@@ -5730,6 +5762,29 @@ function PlayPageClient() {
|
|||||||
const Artplayer = ArtplayerModule.default;
|
const Artplayer = ArtplayerModule.default;
|
||||||
const Hls = HlsModule.default;
|
const Hls = HlsModule.default;
|
||||||
const artplayerPluginDanmuku = DanmukuPlugin.default as any;
|
const artplayerPluginDanmuku = DanmukuPlugin.default as any;
|
||||||
|
const playerTimeouts = new Set<number>();
|
||||||
|
const clearTrackedTimeout = (timeoutId: number | null) => {
|
||||||
|
if (timeoutId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
playerTimeouts.delete(timeoutId);
|
||||||
|
};
|
||||||
|
const schedulePlayerTimeout = (callback: () => void, delay: number) => {
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
playerTimeouts.delete(timeoutId);
|
||||||
|
callback();
|
||||||
|
}, delay);
|
||||||
|
playerTimeouts.add(timeoutId);
|
||||||
|
return timeoutId;
|
||||||
|
};
|
||||||
|
const clearPlayerTimeouts = () => {
|
||||||
|
playerTimeouts.forEach((timeoutId) => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
});
|
||||||
|
playerTimeouts.clear();
|
||||||
|
};
|
||||||
|
|
||||||
const syncPlaybackPitch = () => {
|
const syncPlaybackPitch = () => {
|
||||||
if (!isWebkit || !artPlayerRef.current?.video) {
|
if (!isWebkit || !artPlayerRef.current?.video) {
|
||||||
@@ -5749,6 +5804,43 @@ function PlayPageClient() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shouldRescueWebkitHls = (
|
||||||
|
video: HTMLVideoElement & {
|
||||||
|
hls?: {
|
||||||
|
detachMedia?: () => void;
|
||||||
|
attachMedia?: (video: HTMLVideoElement) => void;
|
||||||
|
startLoad?: (startPosition?: number) => void;
|
||||||
|
bufferController?: {
|
||||||
|
mediaSource?: {
|
||||||
|
readyState?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const hls = video.hls;
|
||||||
|
if (!hls) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasBufferedData = false;
|
||||||
|
try {
|
||||||
|
hasBufferedData = video.buffered.length > 0;
|
||||||
|
} catch {
|
||||||
|
hasBufferedData = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video.readyState > 0 || hasBufferedData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSrc = video.currentSrc || video.src || '';
|
||||||
|
const mediaSourceState = hls.bufferController?.mediaSource?.readyState || '';
|
||||||
|
const usingBlobMsePath = currentSrc.startsWith('blob:') && mediaSourceState !== 'closed';
|
||||||
|
|
||||||
|
return !usingBlobMsePath;
|
||||||
|
};
|
||||||
|
|
||||||
const rescueWebkitHlsBootstrap = (
|
const rescueWebkitHlsBootstrap = (
|
||||||
reason: string,
|
reason: string,
|
||||||
retryDelays: number[] = [1500, 3500, 6000]
|
retryDelays: number[] = [1500, 3500, 6000]
|
||||||
@@ -5766,15 +5858,13 @@ function PlayPageClient() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
retryDelays.forEach((delay) => {
|
retryDelays.forEach((delay) => {
|
||||||
window.setTimeout(() => {
|
schedulePlayerTimeout(() => {
|
||||||
if (!artPlayerRef.current || artPlayerRef.current.video !== video) {
|
if (!artPlayerRef.current || artPlayerRef.current.video !== video) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hls = video.hls;
|
const hls = video.hls;
|
||||||
const currentSrc = video.currentSrc || video.src || '';
|
if (!shouldRescueWebkitHls(video)) {
|
||||||
|
|
||||||
if (!hls || currentSrc || video.readyState > 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5962,18 +6052,15 @@ function PlayPageClient() {
|
|||||||
|
|
||||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||||
kickStartHlsPlayback();
|
kickStartHlsPlayback();
|
||||||
});
|
|
||||||
|
|
||||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
|
||||||
if (!isWebkit) {
|
if (!isWebkit) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safari 偶发出现 media 已 attach 但 video.src 仍为空的状态。
|
// Safari 偶发出现 media 已 attach 但 video.src 仍为空的状态。
|
||||||
// 这里延迟自检一次,必要时重新 attach,强制进入 blob: MSE 路径。
|
// 这里延迟自检一次,必要时重新 attach,强制回到 blob: MSE 路径。
|
||||||
window.setTimeout(() => {
|
schedulePlayerTimeout(() => {
|
||||||
const currentSrc = video.currentSrc || video.src || '';
|
if (!shouldRescueWebkitHls(video)) {
|
||||||
if (currentSrc) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5993,9 +6080,8 @@ function PlayPageClient() {
|
|||||||
video.hls = hls;
|
video.hls = hls;
|
||||||
|
|
||||||
if (isWebkit) {
|
if (isWebkit) {
|
||||||
window.setTimeout(() => {
|
schedulePlayerTimeout(() => {
|
||||||
const currentSrc = video.currentSrc || video.src || '';
|
if (!shouldRescueWebkitHls(video)) {
|
||||||
if (currentSrc) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6864,6 +6950,10 @@ function PlayPageClient() {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
artPlayerRef.current.on('destroy', () => {
|
||||||
|
clearPlayerTimeouts();
|
||||||
|
});
|
||||||
|
|
||||||
// 监听播放器事件
|
// 监听播放器事件
|
||||||
artPlayerRef.current.on('ready', async () => {
|
artPlayerRef.current.on('ready', async () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -7753,8 +7843,16 @@ function PlayPageClient() {
|
|||||||
}
|
}
|
||||||
resumeTimeRef.current = null;
|
resumeTimeRef.current = null;
|
||||||
|
|
||||||
setTimeout(() => {
|
schedulePlayerTimeout(() => {
|
||||||
|
if (!artPlayerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const restorePlaybackRate = () => {
|
const restorePlaybackRate = () => {
|
||||||
|
if (!artPlayerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Math.abs(
|
Math.abs(
|
||||||
artPlayerRef.current.playbackRate - lastPlaybackRateRef.current
|
artPlayerRef.current.playbackRate - lastPlaybackRateRef.current
|
||||||
@@ -7781,10 +7879,10 @@ function PlayPageClient() {
|
|||||||
|
|
||||||
if (video.seeking) {
|
if (video.seeking) {
|
||||||
const handleSeeked = () => {
|
const handleSeeked = () => {
|
||||||
window.clearTimeout(seekedTimeout);
|
clearTrackedTimeout(seekedTimeout);
|
||||||
applyRateAfterSeek();
|
applyRateAfterSeek();
|
||||||
};
|
};
|
||||||
const seekedTimeout = window.setTimeout(() => {
|
const seekedTimeout = schedulePlayerTimeout(() => {
|
||||||
video.removeEventListener('seeked', handleSeeked);
|
video.removeEventListener('seeked', handleSeeked);
|
||||||
applyRateAfterSeek();
|
applyRateAfterSeek();
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|||||||
@@ -55,14 +55,18 @@ export default function VirtualScrollableGrid({
|
|||||||
const measureGridRef = useRef<HTMLDivElement>(null);
|
const measureGridRef = useRef<HTMLDivElement>(null);
|
||||||
const childrenRef = useRef(children);
|
const childrenRef = useRef(children);
|
||||||
const rafRef = useRef<number | null>(null);
|
const rafRef = useRef<number | null>(null);
|
||||||
|
const needsMeasureRef = useRef(true);
|
||||||
|
|
||||||
childrenRef.current = children;
|
childrenRef.current = children;
|
||||||
|
|
||||||
const [layout, setLayout] = useState<LayoutMetrics>(() => ({
|
const initialLayout: LayoutMetrics = {
|
||||||
columns: Math.max(1, mobileColumns),
|
columns: Math.max(1, mobileColumns),
|
||||||
rowHeight: DEFAULT_ROW_HEIGHT,
|
rowHeight: DEFAULT_ROW_HEIGHT,
|
||||||
totalRows: Math.ceil(children.length / Math.max(1, mobileColumns)),
|
totalRows: Math.ceil(children.length / Math.max(1, mobileColumns)),
|
||||||
}));
|
};
|
||||||
|
const layoutRef = useRef<LayoutMetrics>(initialLayout);
|
||||||
|
|
||||||
|
const [layout, setLayout] = useState<LayoutMetrics>(() => initialLayout);
|
||||||
const [range, setRange] = useState({ startRow: 0, endRow: 0 });
|
const [range, setRange] = useState({ startRow: 0, endRow: 0 });
|
||||||
|
|
||||||
const computeFallbackColumns = () => {
|
const computeFallbackColumns = () => {
|
||||||
@@ -169,9 +173,23 @@ export default function VirtualScrollableGrid({
|
|||||||
return { startRow: clampedStart, endRow: clampedEnd };
|
return { startRow: clampedStart, endRow: clampedEnd };
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncLayoutAndRange = () => {
|
const syncRange = (nextLayout: LayoutMetrics) => {
|
||||||
const nextLayout = readLayout();
|
|
||||||
const nextRange = computeRange(nextLayout);
|
const nextRange = computeRange(nextLayout);
|
||||||
|
setRange((prev) => {
|
||||||
|
if (
|
||||||
|
prev.startRow === nextRange.startRow &&
|
||||||
|
prev.endRow === nextRange.endRow
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextRange;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncMeasuredLayout = () => {
|
||||||
|
const nextLayout = readLayout();
|
||||||
|
layoutRef.current = nextLayout;
|
||||||
|
|
||||||
setLayout((prev) => {
|
setLayout((prev) => {
|
||||||
if (
|
if (
|
||||||
@@ -185,37 +203,38 @@ export default function VirtualScrollableGrid({
|
|||||||
return nextLayout;
|
return nextLayout;
|
||||||
});
|
});
|
||||||
|
|
||||||
setRange((prev) => {
|
syncRange(nextLayout);
|
||||||
if (
|
|
||||||
prev.startRow === nextRange.startRow &&
|
|
||||||
prev.endRow === nextRange.endRow
|
|
||||||
) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextRange;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const scheduleUpdate = () => {
|
const scheduleUpdate = (measure = false) => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
if (measure) {
|
||||||
|
needsMeasureRef.current = true;
|
||||||
|
}
|
||||||
if (rafRef.current != null) return;
|
if (rafRef.current != null) return;
|
||||||
|
|
||||||
rafRef.current = window.requestAnimationFrame(() => {
|
rafRef.current = window.requestAnimationFrame(() => {
|
||||||
rafRef.current = null;
|
rafRef.current = null;
|
||||||
syncLayoutAndRange();
|
|
||||||
|
if (needsMeasureRef.current) {
|
||||||
|
needsMeasureRef.current = false;
|
||||||
|
syncMeasuredLayout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncRange(layoutRef.current);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scheduleUpdate();
|
scheduleUpdate(true);
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
scheduleUpdate();
|
scheduleUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
scheduleUpdate();
|
scheduleUpdate(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const bodyEl = document.body;
|
const bodyEl = document.body;
|
||||||
@@ -230,7 +249,7 @@ export default function VirtualScrollableGrid({
|
|||||||
let resizeObserver: ResizeObserver | null = null;
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
if (typeof ResizeObserver !== 'undefined') {
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
resizeObserver = new ResizeObserver(() => {
|
resizeObserver = new ResizeObserver(() => {
|
||||||
scheduleUpdate();
|
scheduleUpdate(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const EPISODE_PROGRESS_PREFIX = 'moontv_episode_progress:';
|
const EPISODE_PROGRESS_PREFIX = 'moontv_episode_progress:';
|
||||||
const EPISODE_PROGRESS_MAX_SHOWS = 100;
|
const EPISODE_PROGRESS_MAX_SHOWS = 20;
|
||||||
const EPISODE_PROGRESS_MAX_AGE_MS = 1000 * 60 * 60 * 24 * 120;
|
const EPISODE_PROGRESS_MAX_AGE_MS = 1000 * 60 * 60 * 24 * 120;
|
||||||
|
|
||||||
export interface LocalEpisodeProgressRecord {
|
export interface LocalEpisodeProgressRecord {
|
||||||
@@ -9,6 +9,8 @@ export interface LocalEpisodeProgressRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface EpisodeProgressContentIdentity {
|
interface EpisodeProgressContentIdentity {
|
||||||
|
doubanId?: number | string;
|
||||||
|
tmdbId?: number | string;
|
||||||
title?: string;
|
title?: string;
|
||||||
year?: string;
|
year?: string;
|
||||||
searchType?: string;
|
searchType?: string;
|
||||||
@@ -201,7 +203,21 @@ function normalizeContentTitle(title: string) {
|
|||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildEpisodeProgressContentKey(
|
function normalizeContentIdentityId(value: unknown) {
|
||||||
|
const numericValue = Number(value);
|
||||||
|
if (Number.isFinite(numericValue) && numericValue > 0) {
|
||||||
|
return String(Math.floor(numericValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = String(value || '').trim();
|
||||||
|
if (/^[1-9]\d*$/.test(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLegacyEpisodeProgressContentKey(
|
||||||
identity: EpisodeProgressContentIdentity
|
identity: EpisodeProgressContentIdentity
|
||||||
) {
|
) {
|
||||||
const title = normalizeContentTitle(identity.title || '');
|
const title = normalizeContentTitle(identity.title || '');
|
||||||
@@ -215,6 +231,23 @@ export function buildEpisodeProgressContentKey(
|
|||||||
return `${title}|${year}|${searchType}`;
|
return `${title}|${year}|${searchType}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildEpisodeProgressContentKey(
|
||||||
|
identity: EpisodeProgressContentIdentity
|
||||||
|
) {
|
||||||
|
const doubanId = normalizeContentIdentityId(identity.doubanId);
|
||||||
|
if (doubanId) {
|
||||||
|
return `douban:${doubanId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmdbId = normalizeContentIdentityId(identity.tmdbId);
|
||||||
|
if (tmdbId) {
|
||||||
|
const searchType = String(identity.searchType || '').trim().toLowerCase();
|
||||||
|
return searchType ? `tmdb:${searchType}:${tmdbId}` : `tmdb:${tmdbId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildLegacyEpisodeProgressContentKey(identity);
|
||||||
|
}
|
||||||
|
|
||||||
export function getEpisodeProgressStorageKey(contentKey: string) {
|
export function getEpisodeProgressStorageKey(contentKey: string) {
|
||||||
return `${EPISODE_PROGRESS_PREFIX}${contentKey}`;
|
return `${EPISODE_PROGRESS_PREFIX}${contentKey}`;
|
||||||
}
|
}
|
||||||
@@ -287,6 +320,7 @@ export function saveLocalEpisodeProgress(
|
|||||||
const key = getEpisodeProgressStorageKey(contentKey);
|
const key = getEpisodeProgressStorageKey(contentKey);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const currentStore = readEpisodeProgressStore(contentKey);
|
const currentStore = readEpisodeProgressStore(contentKey);
|
||||||
|
const shouldPruneAfterSave = !currentStore;
|
||||||
const nextStore: LocalEpisodeProgressStore = {
|
const nextStore: LocalEpisodeProgressStore = {
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
episodes: {
|
episodes: {
|
||||||
@@ -303,13 +337,17 @@ export function saveLocalEpisodeProgress(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(key, payload);
|
localStorage.setItem(key, payload);
|
||||||
pruneLocalEpisodeProgressStorage();
|
if (shouldPruneAfterSave) {
|
||||||
|
pruneLocalEpisodeProgressStorage();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!isQuotaExceededError(error)) {
|
if (!isQuotaExceededError(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
pruneLocalEpisodeProgressStorage(Math.max(50, Math.floor(EPISODE_PROGRESS_MAX_SHOWS / 2)));
|
pruneLocalEpisodeProgressStorage(
|
||||||
|
Math.max(10, Math.floor(EPISODE_PROGRESS_MAX_SHOWS / 2))
|
||||||
|
);
|
||||||
localStorage.setItem(key, payload);
|
localStorage.setItem(key, payload);
|
||||||
pruneLocalEpisodeProgressStorage();
|
pruneLocalEpisodeProgressStorage();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user