Spaces:
Sleeping
Sleeping
| 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); | |
| } | |