Limit local episode progress cache
This commit is contained in:
+19
-63
@@ -45,6 +45,11 @@ import {
|
|||||||
saveSkipConfig,
|
saveSkipConfig,
|
||||||
subscribeToDataUpdates,
|
subscribeToDataUpdates,
|
||||||
} from '@/lib/db.client';
|
} from '@/lib/db.client';
|
||||||
|
import {
|
||||||
|
loadLocalEpisodeProgress,
|
||||||
|
pruneLocalEpisodeProgressStorage,
|
||||||
|
saveLocalEpisodeProgress,
|
||||||
|
} from '@/lib/episode-progress';
|
||||||
import { getDoubanDetail } from '@/lib/douban.client';
|
import { getDoubanDetail } from '@/lib/douban.client';
|
||||||
import { getTMDBImageUrl } from '@/lib/tmdb.search';
|
import { getTMDBImageUrl } from '@/lib/tmdb.search';
|
||||||
import { DanmakuFilterConfig, EpisodeFilterConfig, SearchResult } from '@/lib/types';
|
import { DanmakuFilterConfig, EpisodeFilterConfig, SearchResult } from '@/lib/types';
|
||||||
@@ -748,10 +753,11 @@ function PlayPageClient() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否禁用了自动装填弹幕
|
// 检查是否禁用了自动加载弹幕
|
||||||
const disableAutoLoad = localStorage.getItem('disableAutoLoadDanmaku') === 'true';
|
if (isDanmakuAutoLoadDisabled()) {
|
||||||
if (disableAutoLoad) {
|
console.log('[弹幕] 已禁用自动加载弹幕,跳过自动加载');
|
||||||
console.log('[弹幕] 已禁用自动装填弹幕,跳过自动加载');
|
setShowDanmakuSourceSelector(false);
|
||||||
|
setDanmakuLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1564,63 +1570,6 @@ function PlayPageClient() {
|
|||||||
return (window as any).RUNTIME_CONFIG?.DANMAKU_AUTO_LOAD_DEFAULT === false;
|
return (window as any).RUNTIME_CONFIG?.DANMAKU_AUTO_LOAD_DEFAULT === false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEpisodeProgressStorageKey = (
|
|
||||||
source: string,
|
|
||||||
id: string,
|
|
||||||
episodeIndex: number
|
|
||||||
) => `moontv_episode_progress:${source}+${id}:${episodeIndex}`;
|
|
||||||
|
|
||||||
const loadLocalEpisodeProgress = (
|
|
||||||
source: string,
|
|
||||||
id: string,
|
|
||||||
episodeIndex: number
|
|
||||||
): number | null => {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(
|
|
||||||
getEpisodeProgressStorageKey(source, id, episodeIndex)
|
|
||||||
);
|
|
||||||
if (!raw) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = JSON.parse(raw) as { playTime?: number };
|
|
||||||
const playTime = Number(parsed.playTime);
|
|
||||||
return Number.isFinite(playTime) && playTime > 1 ? playTime : null;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[Play] Failed to load local episode progress:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveLocalEpisodeProgress = (
|
|
||||||
source: string,
|
|
||||||
id: string,
|
|
||||||
episodeIndex: number,
|
|
||||||
playTime: number,
|
|
||||||
totalTime: number
|
|
||||||
) => {
|
|
||||||
if (typeof window === 'undefined' || !Number.isFinite(playTime) || playTime <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
localStorage.setItem(
|
|
||||||
getEpisodeProgressStorageKey(source, id, episodeIndex),
|
|
||||||
JSON.stringify({
|
|
||||||
playTime: Math.floor(playTime),
|
|
||||||
totalTime: Math.floor(totalTime),
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[Play] Failed to save local episode progress:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 用于记录是否需要在播放器 ready 后跳转到指定进度
|
// 用于记录是否需要在播放器 ready 后跳转到指定进度
|
||||||
const resumeTimeRef = useRef<number | null>(null);
|
const resumeTimeRef = useRef<number | null>(null);
|
||||||
// 播放记录跳转按钮状态
|
// 播放记录跳转按钮状态
|
||||||
@@ -1642,6 +1591,14 @@ function PlayPageClient() {
|
|||||||
);
|
);
|
||||||
const [backgroundSourcesLoading, setBackgroundSourcesLoading] = useState(false);
|
const [backgroundSourcesLoading, setBackgroundSourcesLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
pruneLocalEpisodeProgressStorage();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Play] Failed to prune local episode progress:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 优选和测速开关
|
// 优选和测速开关
|
||||||
const [optimizationEnabled] = useState<boolean>(() => {
|
const [optimizationEnabled] = useState<boolean>(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -4779,8 +4736,7 @@ function PlayPageClient() {
|
|||||||
const preloadNextEpisodeDanmaku = async () => {
|
const preloadNextEpisodeDanmaku = async () => {
|
||||||
try {
|
try {
|
||||||
if (isDirectPlay) return;
|
if (isDirectPlay) return;
|
||||||
const disableAutoLoad = localStorage.getItem('disableAutoLoadDanmaku') === 'true';
|
if (isDanmakuAutoLoadDisabled()) return;
|
||||||
if (disableAutoLoad) return;
|
|
||||||
|
|
||||||
const title = videoTitleRef.current;
|
const title = videoTitleRef.current;
|
||||||
if (!title) {
|
if (!title) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import React, {
|
|||||||
|
|
||||||
import type { DanmakuComment,DanmakuSelection } from '@/lib/danmaku/types';
|
import type { DanmakuComment,DanmakuSelection } from '@/lib/danmaku/types';
|
||||||
import { generateStorageKey, getCachedPlayRecordsSnapshot } from '@/lib/db.client';
|
import { generateStorageKey, getCachedPlayRecordsSnapshot } from '@/lib/db.client';
|
||||||
|
import { loadLocalEpisodeProgressRecord } from '@/lib/episode-progress';
|
||||||
import { EpisodeFilterConfig,SearchResult } from '@/lib/types';
|
import { EpisodeFilterConfig,SearchResult } from '@/lib/types';
|
||||||
import { getVideoResolutionFromM3u8 } from '@/lib/utils';
|
import { getVideoResolutionFromM3u8 } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -146,13 +147,12 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
|
|
||||||
for (let episodeNumber = 1; episodeNumber <= totalEpisodes; episodeNumber++) {
|
for (let episodeNumber = 1; episodeNumber <= totalEpisodes; episodeNumber++) {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(
|
const record = loadLocalEpisodeProgressRecord(
|
||||||
`moontv_episode_progress:${currentSource}+${currentId}:${episodeNumber - 1}`
|
currentSource,
|
||||||
|
currentId,
|
||||||
|
episodeNumber - 1
|
||||||
);
|
);
|
||||||
if (!raw) continue;
|
if (Number(record?.playTime) > 1) {
|
||||||
|
|
||||||
const parsed = JSON.parse(raw) as { playTime?: number };
|
|
||||||
if (Number(parsed.playTime) > 1) {
|
|
||||||
watched.add(episodeNumber);
|
watched.add(episodeNumber);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
const EPISODE_PROGRESS_PREFIX = 'moontv_episode_progress:';
|
||||||
|
const EPISODE_PROGRESS_MAX_ENTRIES = 200;
|
||||||
|
const EPISODE_PROGRESS_MAX_AGE_MS = 1000 * 60 * 60 * 24 * 120;
|
||||||
|
|
||||||
|
interface LocalEpisodeProgressRecord {
|
||||||
|
playTime: number;
|
||||||
|
totalTime: number;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBrowser() {
|
||||||
|
return typeof window !== 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isQuotaExceededError(error: unknown) {
|
||||||
|
return (
|
||||||
|
error instanceof DOMException &&
|
||||||
|
(error.name === 'QuotaExceededError' || error.name === 'NS_ERROR_DOM_QUOTA_REACHED')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEpisodeProgressRecord(raw: string | null): LocalEpisodeProgressRecord | null {
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as Partial<LocalEpisodeProgressRecord>;
|
||||||
|
const playTime = Number(parsed.playTime);
|
||||||
|
const totalTime = Number(parsed.totalTime);
|
||||||
|
const updatedAt = Number(parsed.updatedAt);
|
||||||
|
|
||||||
|
if (!Number.isFinite(playTime) || playTime <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
playTime,
|
||||||
|
totalTime: Number.isFinite(totalTime) && totalTime >= 0 ? totalTime : 0,
|
||||||
|
updatedAt: Number.isFinite(updatedAt) && updatedAt > 0 ? updatedAt : 0,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectEpisodeProgressEntries() {
|
||||||
|
if (!isBrowser()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries: Array<{ key: string; record: LocalEpisodeProgressRecord }> = [];
|
||||||
|
const keys = Array.from({ length: localStorage.length }, (_, index) =>
|
||||||
|
localStorage.key(index)
|
||||||
|
).filter((key): key is string => Boolean(key));
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!key.startsWith(EPISODE_PROGRESS_PREFIX)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = parseEpisodeProgressRecord(localStorage.getItem(key));
|
||||||
|
if (!record) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push({ key, record });
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEpisodeProgressStorageKey(
|
||||||
|
source: string,
|
||||||
|
id: string,
|
||||||
|
episodeIndex: number
|
||||||
|
) {
|
||||||
|
return `${EPISODE_PROGRESS_PREFIX}${source}+${id}:${episodeIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadLocalEpisodeProgressRecord(
|
||||||
|
source: string,
|
||||||
|
id: string,
|
||||||
|
episodeIndex: number
|
||||||
|
) {
|
||||||
|
if (!isBrowser()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = getEpisodeProgressStorageKey(source, id, episodeIndex);
|
||||||
|
const record = parseEpisodeProgressRecord(localStorage.getItem(key));
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
record.updatedAt > 0 &&
|
||||||
|
Date.now() - record.updatedAt > EPISODE_PROGRESS_MAX_AGE_MS
|
||||||
|
) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadLocalEpisodeProgress(
|
||||||
|
source: string,
|
||||||
|
id: string,
|
||||||
|
episodeIndex: number
|
||||||
|
) {
|
||||||
|
const record = loadLocalEpisodeProgressRecord(source, id, episodeIndex);
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number.isFinite(record.playTime) && record.playTime > 1
|
||||||
|
? Math.floor(record.playTime)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pruneLocalEpisodeProgressStorage(maxEntries = EPISODE_PROGRESS_MAX_ENTRIES) {
|
||||||
|
if (!isBrowser()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const entries = collectEpisodeProgressEntries();
|
||||||
|
|
||||||
|
entries.forEach(({ key, record }) => {
|
||||||
|
if (record.updatedAt > 0 && now - record.updatedAt > EPISODE_PROGRESS_MAX_AGE_MS) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const validEntries = entries
|
||||||
|
.filter(({ record }) => record.updatedAt <= 0 || now - record.updatedAt <= EPISODE_PROGRESS_MAX_AGE_MS)
|
||||||
|
.sort((a, b) => b.record.updatedAt - a.record.updatedAt);
|
||||||
|
|
||||||
|
if (validEntries.length <= maxEntries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
validEntries.slice(maxEntries).forEach(({ key }) => {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveLocalEpisodeProgress(
|
||||||
|
source: string,
|
||||||
|
id: string,
|
||||||
|
episodeIndex: number,
|
||||||
|
playTime: number,
|
||||||
|
totalTime: number
|
||||||
|
) {
|
||||||
|
if (!isBrowser() || !Number.isFinite(playTime) || playTime <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = getEpisodeProgressStorageKey(source, id, episodeIndex);
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
playTime: Math.floor(playTime),
|
||||||
|
totalTime: Number.isFinite(totalTime) && totalTime >= 0 ? Math.floor(totalTime) : 0,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, payload);
|
||||||
|
pruneLocalEpisodeProgressStorage();
|
||||||
|
} catch (error) {
|
||||||
|
if (!isQuotaExceededError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneLocalEpisodeProgressStorage(Math.max(50, Math.floor(EPISODE_PROGRESS_MAX_ENTRIES / 2)));
|
||||||
|
localStorage.setItem(key, payload);
|
||||||
|
pruneLocalEpisodeProgressStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user