增加emby格式sxxexx分组功能
This commit is contained in:
@@ -96,7 +96,68 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
onShowToast,
|
onShowToast,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pageCount = Math.ceil(totalEpisodes / episodesPerPage);
|
|
||||||
|
const parseSxxExxTitle = useCallback((title?: string) => {
|
||||||
|
const match = title?.match(/[Ss](\d+)[Ee](\d{1,4}(?:\.\d+)?)/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seasonNumber = Number.parseInt(match[1], 10);
|
||||||
|
if (!Number.isFinite(seasonNumber)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { season: seasonNumber };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const episodeGroupsAsc = useMemo(() => {
|
||||||
|
const sxxexxMatchCount = episodes_titles.reduce((count, title) => {
|
||||||
|
return count + (parseSxxExxTitle(title) ? 1 : 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
if (sxxexxMatchCount >= 2) {
|
||||||
|
const seasons = new Map<number, number[]>();
|
||||||
|
const otherEpisodes: number[] = [];
|
||||||
|
|
||||||
|
for (let episodeNumber = 1; episodeNumber <= totalEpisodes; episodeNumber += 1) {
|
||||||
|
const parsed = parseSxxExxTitle(episodes_titles?.[episodeNumber - 1]);
|
||||||
|
if (!parsed) {
|
||||||
|
otherEpisodes.push(episodeNumber);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const episodes = seasons.get(parsed.season) ?? [];
|
||||||
|
episodes.push(episodeNumber);
|
||||||
|
seasons.set(parsed.season, episodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seasonGroups = Array.from(seasons.entries())
|
||||||
|
.sort(([seasonA], [seasonB]) => seasonA - seasonB)
|
||||||
|
.map(([season, episodes]) => ({
|
||||||
|
label: `S${String(season).padStart(2, '0')}`,
|
||||||
|
episodes,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (otherEpisodes.length > 0) {
|
||||||
|
seasonGroups.push({ label: '其他', episodes: otherEpisodes });
|
||||||
|
}
|
||||||
|
|
||||||
|
return seasonGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageCount = Math.ceil(totalEpisodes / episodesPerPage);
|
||||||
|
return Array.from({ length: pageCount }, (_, i) => {
|
||||||
|
const start = i * episodesPerPage + 1;
|
||||||
|
const end = Math.min(start + episodesPerPage - 1, totalEpisodes);
|
||||||
|
return {
|
||||||
|
label: `${start}-${end}`,
|
||||||
|
episodes: Array.from({ length: end - start + 1 }, (_, idx) => start + idx),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [episodesPerPage, episodes_titles, parseSxxExxTitle, totalEpisodes]);
|
||||||
|
|
||||||
|
const pageCount = episodeGroupsAsc.length;
|
||||||
|
|
||||||
// 存储每个源的视频信息
|
// 存储每个源的视频信息
|
||||||
const [videoInfoMap, setVideoInfoMap] = useState<Map<string, VideoInfo>>(
|
const [videoInfoMap, setVideoInfoMap] = useState<Map<string, VideoInfo>>(
|
||||||
@@ -205,8 +266,11 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
}
|
}
|
||||||
}, [isRoomMember, activeTab]);
|
}, [isRoomMember, activeTab]);
|
||||||
|
|
||||||
// 当前分页索引(0 开始)
|
// 当前分组索引(0 开始)
|
||||||
const initialPage = Math.floor((value - 1) / episodesPerPage);
|
const initialPage = Math.max(
|
||||||
|
0,
|
||||||
|
episodeGroupsAsc.findIndex((group) => group.episodes.includes(value))
|
||||||
|
);
|
||||||
const [currentPage, setCurrentPage] = useState<number>(initialPage);
|
const [currentPage, setCurrentPage] = useState<number>(initialPage);
|
||||||
|
|
||||||
// 是否倒序显示
|
// 是否倒序显示
|
||||||
@@ -264,6 +328,17 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
return currentPage;
|
return currentPage;
|
||||||
}, [currentPage, descending, pageCount]);
|
}, [currentPage, descending, pageCount]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentEpisode = Math.max(1, Math.min(value, totalEpisodes));
|
||||||
|
const nextPage = episodeGroupsAsc.findIndex((group) =>
|
||||||
|
group.episodes.includes(currentEpisode)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextPage >= 0) {
|
||||||
|
setCurrentPage(nextPage);
|
||||||
|
}
|
||||||
|
}, [episodeGroupsAsc, totalEpisodes, value]);
|
||||||
|
|
||||||
// 获取视频信息的函数 - 移除 attemptedSources 依赖避免不必要的重新创建
|
// 获取视频信息的函数 - 移除 attemptedSources 依赖避免不必要的重新创建
|
||||||
const getVideoInfo = useCallback(async (source: SearchResult) => {
|
const getVideoInfo = useCallback(async (source: SearchResult) => {
|
||||||
const sourceKey = `${source.source}-${source.id}`;
|
const sourceKey = `${source.source}-${source.id}`;
|
||||||
@@ -444,25 +519,11 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
prevBackgroundLoadingRef.current = backgroundSourcesLoading;
|
prevBackgroundLoadingRef.current = backgroundSourcesLoading;
|
||||||
}, [backgroundSourcesLoading, activeTab, availableSources, getVideoInfo, optimizationEnabled, initialTestingCompleted, currentSource]);
|
}, [backgroundSourcesLoading, activeTab, availableSources, getVideoInfo, optimizationEnabled, initialTestingCompleted, currentSource]);
|
||||||
|
|
||||||
// 升序分页标签
|
// 根据 descending 状态决定分组标签的排序和内容
|
||||||
const categoriesAsc = useMemo(() => {
|
|
||||||
return Array.from({ length: pageCount }, (_, i) => {
|
|
||||||
const start = i * episodesPerPage + 1;
|
|
||||||
const end = Math.min(start + episodesPerPage - 1, totalEpisodes);
|
|
||||||
return { start, end };
|
|
||||||
});
|
|
||||||
}, [pageCount, episodesPerPage, totalEpisodes]);
|
|
||||||
|
|
||||||
// 根据 descending 状态决定分页标签的排序和内容
|
|
||||||
const categories = useMemo(() => {
|
const categories = useMemo(() => {
|
||||||
if (descending) {
|
const groups = descending ? [...episodeGroupsAsc].reverse() : episodeGroupsAsc;
|
||||||
// 倒序时,label 也倒序显示
|
return groups.map((group) => group.label);
|
||||||
return [...categoriesAsc]
|
}, [episodeGroupsAsc, descending]);
|
||||||
.reverse()
|
|
||||||
.map(({ start, end }) => `${end}-${start}`);
|
|
||||||
}
|
|
||||||
return categoriesAsc.map(({ start, end }) => `${start}-${end}`);
|
|
||||||
}, [categoriesAsc, descending]);
|
|
||||||
|
|
||||||
const categoryContainerRef = useRef<HTMLDivElement>(null);
|
const categoryContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||||
@@ -624,11 +685,10 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
[getVideoInfo]
|
[getVideoInfo]
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentStart = currentPage * episodesPerPage + 1;
|
const currentEpisodeGroup = episodeGroupsAsc[currentPage] ?? {
|
||||||
const currentEnd = Math.min(
|
label: '',
|
||||||
currentStart + episodesPerPage - 1,
|
episodes: [],
|
||||||
totalEpisodes
|
};
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='md:ml-2 px-4 py-0 h-full rounded-xl bg-black/10 dark:bg-white/5 flex flex-col border border-white/0 dark:border-white/30 overflow-hidden'>
|
<div className='md:ml-2 px-4 py-0 h-full rounded-xl bg-black/10 dark:bg-white/5 flex flex-col border border-white/0 dark:border-white/30 overflow-hidden'>
|
||||||
@@ -765,10 +825,9 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
{/* 集数网格 */}
|
{/* 集数网格 */}
|
||||||
<div className='flex flex-wrap gap-3 overflow-y-auto flex-1 content-start pb-4'>
|
<div className='flex flex-wrap gap-3 overflow-y-auto flex-1 content-start pb-4'>
|
||||||
{(() => {
|
{(() => {
|
||||||
const len = currentEnd - currentStart + 1;
|
const episodes = descending
|
||||||
const episodes = Array.from({ length: len }, (_, i) =>
|
? [...currentEpisodeGroup.episodes].reverse()
|
||||||
descending ? currentEnd - i : currentStart + i
|
: currentEpisodeGroup.episodes;
|
||||||
);
|
|
||||||
// 过滤掉被屏蔽的集数,但保持原有索引
|
// 过滤掉被屏蔽的集数,但保持原有索引
|
||||||
return episodes
|
return episodes
|
||||||
.filter(episodeNumber => !isEpisodeFiltered(episodeNumber))
|
.filter(episodeNumber => !isEpisodeFiltered(episodeNumber))
|
||||||
@@ -802,12 +861,10 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
if (title.match(/^OVA\s+\d+/i)) {
|
if (title.match(/^OVA\s+\d+/i)) {
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
// 如果匹配 S01E01 格式,提取并返回
|
// 如果匹配 S01E01 格式,只显示集数部分(去掉 SxxE)
|
||||||
const sxxexxMatch = title.match(/[Ss](\d+)[Ee](\d{1,4}(?:\.\d+)?)/);
|
const sxxexxMatch = title.match(/[Ss]\d+[Ee](\d{1,4}(?:\.\d+)?)/);
|
||||||
if (sxxexxMatch) {
|
if (sxxexxMatch) {
|
||||||
const season = sxxexxMatch[1].padStart(2, '0');
|
return sxxexxMatch[1];
|
||||||
const episode = sxxexxMatch[2];
|
|
||||||
return `S${season}E${episode}`;
|
|
||||||
}
|
}
|
||||||
// 如果匹配"第X集"、"第X话"、"X集"、"X话"格式,提取中间的数字(支持小数)
|
// 如果匹配"第X集"、"第X话"、"X集"、"X话"格式,提取中间的数字(支持小数)
|
||||||
const match = title.match(/(?:第)?(\d+(?:\.\d+)?)(?:集|话)/);
|
const match = title.match(/(?:第)?(\d+(?:\.\d+)?)(?:集|话)/);
|
||||||
|
|||||||
Reference in New Issue
Block a user