import { txasupabase } from './txasupabase.js'; const getLocalMovies = () => txasupabase.getLocalMovies(); const getDeletedMovies = () => txasupabase.getDeletedMovies(); export { formatImageUrl } from './image.js'; // Hàm định dạng phim cục bộ thành định dạng danh sách rút gọn function mapLocalToListItem(localMovie) { return { _id: localMovie.movie.id || localMovie.movie.slug, name: localMovie.movie.name, slug: localMovie.movie.slug, origin_name: localMovie.movie.origin_name, thumb_url: localMovie.movie.thumb_url, poster_url: localMovie.movie.poster_url, year: localMovie.movie.year, quality: localMovie.movie.quality || 'FHD', lang: localMovie.movie.lang || 'Vietsub', episode_current: localMovie.movie.episode_current || 'Tập 1', type: localMovie.movie.type || 'single', isLocal: true }; } // Trích tên gốc phim (bỏ Phần/Part/Season/SS/EP suffix) function getBaseMovieName(name) { if (!name) return ''; let clean = name; // 1. Xóa các cụm chứa ngoặc đơn hoặc ngoặc vuông chứa Phần/Part/Season (ví dụ: "(Phần 2)", "[Season 3]") clean = clean.replace(/\s*[([\])]*\s*(Phần|Part|Season|SS|EP|Ep|Tập)\s*(\d+|[IVXLCDM]+)\s*[([\])]*/gi, ''); // 2. Xóa các cụm Phần/Part/Season đứng độc lập ở cuối clean = clean.replace(/\s*[-–—|:]?\s*(Phần|Part|Season|SS|EP|Ep|Tập)\s*(\d+|[IVXLCDM]+)\b/gi, ''); // 3. Xóa các số hoặc ký tự số La Mã cô độc ở cuối clean = clean.replace(/\s*[-–—|:]?\s*(\d+|[IVXLCDM]+)$/gi, ''); // 4. Dọn dẹp sạch các dấu ngoặc đơn/ngoặc vuông rỗng còn sót lại ở cuối chuỗi clean = clean.replace(/\s*[([\])]+\s*$/gi, ''); return clean.trim().toLowerCase(); } // Nhóm phim theo tên gốc, giữ lại phần mới nhất cho listing function groupMoviesBySeason(movies) { const groups = new Map(); for (const m of movies) { const base = getBaseMovieName(m.movie.name); if (!base || base.length <= 2) { // Tên quá ngắn, không nhóm groups.set(m.movie.slug, m); continue; } const existing = groups.get(base); if (!existing) { groups.set(base, m); } else { // Giữ phần cập nhật mới nhất const existingTime = new Date(existing.movie.modified?.time || 0).getTime(); const currentTime = new Date(m.movie.modified?.time || 0).getTime(); if (currentTime > existingTime) { groups.set(base, m); } } } return Array.from(groups.values()); } // Hàm lọc và phân trang phim cục bộ // grouped = true: gộp phim cùng tên khác phần (mặc định cho trang công khai) // grouped = false: không gộp (dùng cho trang admin) async function queryLocalMovies(filterFn, page = 1, limit = 12, { grouped = true } = {}) { const localMovies = await getLocalMovies(); const deletedSlugs = await getDeletedMovies(); let list = Object.values(localMovies) .filter(m => !deletedSlugs.includes(m.movie.slug)); if (filterFn) { list = list.filter(filterFn); } // Sắp xếp theo ngày cập nhật mới nhất (modified time) list.sort((a, b) => { const timeA = new Date(a.movie.modified?.time || 0).getTime(); const timeB = new Date(b.movie.modified?.time || 0).getTime(); return timeB - timeA; }); // Nhóm phim theo season/part — chỉ hiển thị phần mới nhất trong listing if (grouped) { list = groupMoviesBySeason(list); } const totalItems = list.length; const totalPages = Math.ceil(totalItems / limit) || 1; const paginated = list.slice((page - 1) * limit, page * limit) .map(mapLocalToListItem); return { items: paginated, pagination: { totalItems, totalItemsPerPage: limit, currentPage: page, totalPages } }; } /** * Lấy danh sách phim mới cập nhật * @param {number} page * @param {{ grouped?: boolean }} options - set grouped: false for admin pages */ export async function getNewestMovies(page = 1, options = {}) { return await queryLocalMovies(null, page, 12, options); } /** * Lấy chi tiết phim theo slug */ export async function getMovieDetail(slug) { const deletedSlugs = await getDeletedMovies(); if (deletedSlugs.includes(slug)) { return null; } const localMovies = await getLocalMovies(); if (localMovies[slug]) { return { status: true, movie: localMovies[slug].movie, episodes: localMovies[slug].episodes || [] }; } return null; } export async function getSeriesMovies(page = 1) { return await queryLocalMovies(m => m.movie.type === 'series', page, 12); } export async function getSingleMovies(page = 1) { return await queryLocalMovies(m => m.movie.type === 'single', page, 12); } export async function getCartoonMovies(page = 1) { return await queryLocalMovies(m => m.movie.type === 'hoat-hinh', page, 12); } export async function getTvShows(page = 1) { return await queryLocalMovies(m => m.movie.type === 'tvshows' || m.movie.type === 'tv-shows', page, 12); } export async function getMoviesByCategory(category, page = 1) { const defaultTypes = { 'phim-bo': 'series', 'phim-le': 'single', 'hoat-hinh': 'hoat-hinh', 'tv-shows': 'tv-shows' }; if (defaultTypes[category]) { return await queryLocalMovies(m => m.movie.type === defaultTypes[category], page, 12); } return await queryLocalMovies(m => m.movie.category?.some(c => c.slug === category), page, 12 ); } export async function getMoviesByCountry(country, page = 1) { return await queryLocalMovies(m => m.movie.country?.some(c => c.slug === country), page, 12 ); } export async function searchMovies(keyword, page = 1, options = {}) { if (!keyword) { return { items: [], pagination: { totalItems: 0, totalItemsPerPage: 12, currentPage: page, totalPages: 1 } }; } const k = keyword.toLowerCase(); return await queryLocalMovies(m => m.movie.name?.toLowerCase().includes(k) || m.movie.origin_name?.toLowerCase().includes(k) || m.movie.slug?.includes(k), page, 12, options ); } export async function getMoviesByTopic(topicSlug, page = 1) { const { TOPICS } = await import('./topics.js'); const topic = TOPICS.find(t => t.slug === topicSlug); if (!topic) { return { items: [], pagination: { totalItems: 0, totalItemsPerPage: 12, currentPage: page, totalPages: 1 } }; } return await queryLocalMovies(topic.filter, page, 12); }