Files
KVideo/lib/utils/episode-manager.ts
T
2025-11-16 10:49:55 +08:00

274 lines
6.1 KiB
TypeScript

/**
* Episode Manager
* Handles episode navigation and URL parameter management
*/
import type { Episode } from '@/lib/types';
/**
* Episode navigation parameters
*/
export interface EpisodeNavParams {
videoId: string | number;
title: string;
source: string;
episodeIndex: number;
url: string;
}
/**
* Build player URL with episode parameters
*/
export function buildPlayerUrl(params: EpisodeNavParams): string {
const searchParams = new URLSearchParams();
searchParams.set('id', params.videoId.toString());
searchParams.set('source', params.source);
searchParams.set('index', params.episodeIndex.toString());
searchParams.set('url', encodeURIComponent(params.url));
searchParams.set('title', encodeURIComponent(params.title));
return `/player?${searchParams.toString()}`;
}
/**
* Parse episode parameters from URL
*/
export function parsePlayerParams(searchParams: URLSearchParams): EpisodeNavParams | null {
const id = searchParams.get('id');
const source = searchParams.get('source');
const indexStr = searchParams.get('index');
const url = searchParams.get('url');
const title = searchParams.get('title');
if (!id || !source || !indexStr || !url) {
return null;
}
return {
videoId: id,
title: title || 'Unknown',
source,
episodeIndex: parseInt(indexStr, 10),
url: decodeURIComponent(url),
};
}
/**
* Navigate to next episode
*/
export function getNextEpisodeParams(
currentParams: EpisodeNavParams,
episodes: Episode[]
): EpisodeNavParams | null {
const nextIndex = currentParams.episodeIndex + 1;
if (nextIndex >= episodes.length) {
return null; // No more episodes
}
const nextEpisode = episodes[nextIndex];
return {
...currentParams,
episodeIndex: nextIndex,
url: nextEpisode.url,
};
}
/**
* Navigate to previous episode
*/
export function getPrevEpisodeParams(
currentParams: EpisodeNavParams,
episodes: Episode[]
): EpisodeNavParams | null {
const prevIndex = currentParams.episodeIndex - 1;
if (prevIndex < 0) {
return null; // Already at first episode
}
const prevEpisode = episodes[prevIndex];
return {
...currentParams,
episodeIndex: prevIndex,
url: prevEpisode.url,
};
}
/**
* Get episode by index
*/
export function getEpisodeByIndex(
episodes: Episode[],
index: number
): Episode | null {
if (index < 0 || index >= episodes.length) {
return null;
}
return episodes[index];
}
/**
* Validate episode index
*/
export function isValidEpisodeIndex(index: number, episodes: Episode[]): boolean {
return index >= 0 && index < episodes.length;
}
/**
* Get episode range for pagination
*/
export function getEpisodeRange(
episodes: Episode[],
currentIndex: number,
rangeSize: number = 10
): Episode[] {
const halfRange = Math.floor(rangeSize / 2);
let start = Math.max(0, currentIndex - halfRange);
let end = Math.min(episodes.length, start + rangeSize);
// Adjust if we're near the end
if (end - start < rangeSize) {
start = Math.max(0, end - rangeSize);
}
return episodes.slice(start, end);
}
/**
* Group episodes into sections
*/
export interface EpisodeSection {
title: string;
episodes: Episode[];
startIndex: number;
endIndex: number;
}
export function groupEpisodesIntoSections(
episodes: Episode[],
sectionSize: number = 20
): EpisodeSection[] {
const sections: EpisodeSection[] = [];
for (let i = 0; i < episodes.length; i += sectionSize) {
const end = Math.min(i + sectionSize, episodes.length);
sections.push({
title: `Episodes ${i + 1}-${end}`,
episodes: episodes.slice(i, end),
startIndex: i,
endIndex: end - 1,
});
}
return sections;
}
/**
* Reverse episode order
*/
export function reverseEpisodes(episodes: Episode[]): Episode[] {
return episodes.map((episode, index) => ({
...episode,
index: episodes.length - 1 - index,
})).reverse();
}
/**
* Search episodes by name
*/
export function searchEpisodes(episodes: Episode[], query: string): Episode[] {
if (!query.trim()) return episodes;
const normalizedQuery = query.toLowerCase();
return episodes.filter(episode =>
episode.name.toLowerCase().includes(normalizedQuery)
);
}
/**
* Get episode progress percentage
*/
export function getEpisodeProgress(
episodeIndex: number,
totalEpisodes: number
): number {
if (totalEpisodes === 0) return 0;
return Math.round(((episodeIndex + 1) / totalEpisodes) * 100);
}
/**
* Format episode name
*/
export function formatEpisodeName(episode: Episode, format: 'short' | 'full' = 'full'): string {
if (format === 'short') {
// Extract episode number if available
const match = episode.name.match(/\d+/);
if (match) {
return `EP ${match[0]}`;
}
return `EP ${episode.index + 1}`;
}
return episode.name || `Episode ${episode.index + 1}`;
}
/**
* Check if episode is watched
*/
export function isEpisodeWatched(
episodeIndex: number,
watchedUpTo: number
): boolean {
return episodeIndex <= watchedUpTo;
}
/**
* Get unwatched episodes count
*/
export function getUnwatchedCount(
episodes: Episode[],
watchedUpTo: number
): number {
return Math.max(0, episodes.length - watchedUpTo - 1);
}
/**
* Episode order preference
*/
const EPISODE_ORDER_KEY = 'kvideo_episode_order';
export function saveEpisodeOrder(order: 'normal' | 'reversed'): void {
if (typeof window === 'undefined') return;
localStorage.setItem(EPISODE_ORDER_KEY, order);
}
export function getEpisodeOrder(): 'normal' | 'reversed' {
if (typeof window === 'undefined') return 'normal';
return (localStorage.getItem(EPISODE_ORDER_KEY) as 'normal' | 'reversed') || 'normal';
}
/**
* Apply episode order preference
*/
export function applyEpisodeOrder(episodes: Episode[]): Episode[] {
const order = getEpisodeOrder();
return order === 'reversed' ? reverseEpisodes(episodes) : episodes;
}
/**
* Toggle episode order
*/
export function toggleEpisodeOrder(): 'normal' | 'reversed' {
const current = getEpisodeOrder();
const newOrder = current === 'normal' ? 'reversed' : 'normal';
saveEpisodeOrder(newOrder);
return newOrder;
}