diff --git a/README.md b/README.md index 74fdb923..0ffc1305 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,15 @@ This is a fork of the original repository, containing only my custom plugins and
-🎬 Streaming (9 sources) +🎬 Streaming (10 sources) - [AnimeVietsub](https://animevietsub.bz)[^1] - [AnimeVsub](https://animevsub.app)[^1] -- [NguonC](https://phim.nguonc.com)[^1] -- [HentaiZ](https://hentaiz.hot)[^1] - ~~[AnimeHay](https://animehay.fm)[^1][^2]~~ +- [HentaiZ](https://hentaiz.hot)[^1] +- [KKPhim](https://kkphim.com)[^1] - [Motchill](https://envasion.net)[^1] +- [NguonC](https://phim.nguonc.com)[^1] - [OPhim](https://ophim17.cc)[^1] - [PhimFun](https://phimfun.net)[^1] - [Yêu Anime](https://yeuanime.org)[^1] diff --git a/plugins/index.ts b/plugins/index.ts index ee23a4f2..e6896e46 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -10,24 +10,25 @@ import p_7 from '@plugins/vietnamese/BaoMoi'; import p_8 from '@plugins/vietnamese/DocTruyenLN'; import p_9 from '@plugins/vietnamese/HentaiZ'; import p_10 from '@plugins/vietnamese/JukaNovel'; -import p_11 from '@plugins/vietnamese/LNHako'; -import p_12 from '@plugins/vietnamese/LNKuro'; -import p_13 from '@plugins/vietnamese/Luvevaland'; -import p_14 from '@plugins/vietnamese/MeTruyenCV'; -import p_15 from '@plugins/vietnamese/Motchill'; -import p_16 from '@plugins/vietnamese/MotTruyen'; -import p_17 from '@plugins/vietnamese/NguonC'; -import p_18 from '@plugins/vietnamese/OPhim'; -import p_19 from '@plugins/vietnamese/PhimFun'; -import p_20 from '@plugins/vietnamese/SangTacViet'; -import p_21 from '@plugins/vietnamese/TieuThuyetMang'; -import p_22 from '@plugins/vietnamese/TomatoMTL'; -import p_23 from '@plugins/vietnamese/TruyenCV'; -import p_24 from '@plugins/vietnamese/TruyenFull'; -import p_25 from '@plugins/vietnamese/Valvrareteam'; -import p_26 from '@plugins/vietnamese/WanwanSekai'; -import p_27 from '@plugins/vietnamese/YeuAnime'; -import p_28 from '@plugins/vietnamese/ZumiNovel'; +import p_11 from '@plugins/vietnamese/KKPhim'; +import p_12 from '@plugins/vietnamese/LNHako'; +import p_13 from '@plugins/vietnamese/LNKuro'; +import p_14 from '@plugins/vietnamese/Luvevaland'; +import p_15 from '@plugins/vietnamese/MeTruyenCV'; +import p_16 from '@plugins/vietnamese/Motchill'; +import p_17 from '@plugins/vietnamese/MotTruyen'; +import p_18 from '@plugins/vietnamese/NguonC'; +import p_19 from '@plugins/vietnamese/OPhim'; +import p_20 from '@plugins/vietnamese/PhimFun'; +import p_21 from '@plugins/vietnamese/SangTacViet'; +import p_22 from '@plugins/vietnamese/TieuThuyetMang'; +import p_23 from '@plugins/vietnamese/TomatoMTL'; +import p_24 from '@plugins/vietnamese/TruyenCV'; +import p_25 from '@plugins/vietnamese/TruyenFull'; +import p_26 from '@plugins/vietnamese/Valvrareteam'; +import p_27 from '@plugins/vietnamese/WanwanSekai'; +import p_28 from '@plugins/vietnamese/YeuAnime'; +import p_29 from '@plugins/vietnamese/ZumiNovel'; const PLUGINS: Plugin.PluginBase[] = [ p_0, @@ -59,5 +60,6 @@ const PLUGINS: Plugin.PluginBase[] = [ p_26, p_27, p_28, + p_29, ]; export default PLUGINS; diff --git a/plugins/vietnamese/KKPhim/filters.ts b/plugins/vietnamese/KKPhim/filters.ts new file mode 100644 index 00000000..fd1dd4e2 --- /dev/null +++ b/plugins/vietnamese/KKPhim/filters.ts @@ -0,0 +1,139 @@ +import { type Filters, FilterTypes } from '@libs/filterInputs'; + +export default { + sort_lang: { + type: FilterTypes.Picker, + label: 'Phiên bản', + value: '', + options: [ + { label: 'Tất cả', value: '' }, + { label: 'Phụ đề (Vietsub)', value: 'vietsub' }, + { label: 'Thuyết Minh', value: 'thuyet-minh' }, + { label: 'Lồng Tiếng', value: 'long-tieng' }, + ], + }, + category: { + type: FilterTypes.Picker, + label: 'Thể loại', + value: '', + options: [ + { label: 'Tất cả', value: '' }, + { label: 'Hành Động', value: 'hanh-dong' }, + { label: 'Miền Tây', value: 'mien-tay' }, + { label: 'Trẻ Em', value: 'tre-em' }, + { label: 'Lịch Sử', value: 'lich-su' }, + { label: 'Cổ Trang', value: 'co-trang' }, + { label: 'Chiến Tranh', value: 'chien-tranh' }, + { label: 'Viễn Tưởng', value: 'vien-tuong' }, + { label: 'Kinh Dị', value: 'kinh-di' }, + { label: 'Tài Liệu', value: 'tai-lieu' }, + { label: 'Bí Ẩn', value: 'bi-an' }, + { label: 'Phim 18+', value: 'phim-18' }, + { label: 'Tình Cảm', value: 'tinh-cam' }, + { label: 'Tâm Lý', value: 'tam-ly' }, + { label: 'Thể Thao', value: 'the-thao' }, + { label: 'Phiêu Lưu', value: 'phieu-luu' }, + { label: 'Âm Nhạc', value: 'am-nhac' }, + { label: 'Gia Đình', value: 'gia-dinh' }, + { label: 'Học Đường', value: 'hoc-duong' }, + { label: 'Hài Hước', value: 'hai-huoc' }, + { label: 'Hình Sự', value: 'hinh-su' }, + { label: 'Võ Thuật', value: 'vo-thuat' }, + { label: 'Khoa Học', value: 'khoa-hoc' }, + { label: 'Thần Thoại', value: 'than-thoai' }, + { label: 'Chính Kịch', value: 'chinh-kich' }, + { label: 'Kinh Điển', value: 'kinh-dien' }, + ], + }, + country: { + type: FilterTypes.Picker, + label: 'Quốc gia', + value: '', + options: [ + { label: 'Tất cả', value: '' }, + { label: 'Việt Nam', value: 'viet-nam' }, + { label: 'Trung Quốc', value: 'trung-quoc' }, + { label: 'Thái Lan', value: 'thai-lan' }, + { label: 'Hồng Kông', value: 'hong-kong' }, + { label: 'Pháp', value: 'phap' }, + { label: 'Đức', value: 'duc' }, + { label: 'Hà Lan', value: 'ha-lan' }, + { label: 'Mexico', value: 'mexico' }, + { label: 'Thụy Điển', value: 'thuy-dien' }, + { label: 'Philippines', value: 'philippines' }, + { label: 'Đan Mạch', value: 'dan-mach' }, + { label: 'Thụy Sĩ', value: 'thuy-si' }, + { label: 'Ukraina', value: 'ukraina' }, + { label: 'Hàn Quốc', value: 'han-quoc' }, + { label: 'Âu Mỹ', value: 'au-my' }, + { label: 'Ấn Độ', value: 'an-do' }, + { label: 'Canada', value: 'canada' }, + { label: 'Tây Ban Nha', value: 'tay-ban-nha' }, + { label: 'Indonesia', value: 'indonesia' }, + { label: 'Ba Lan', value: 'ba-lan' }, + { label: 'Malaysia', value: 'malaysia' }, + { label: 'Bồ Đào Nha', value: 'bo-dao-nha' }, + { label: 'UAE', value: 'uae' }, + { label: 'Châu Phi', value: 'chau-phi' }, + { label: 'Ả Rập Xê Út', value: 'a-rap-xe-ut' }, + { label: 'Nhật Bản', value: 'nhat-ban' }, + { label: 'Đài Loan', value: 'dai-loan' }, + { label: 'Anh', value: 'anh' }, + { label: 'Quốc Gia Khác', value: 'quoc-gia-khac' }, + { label: 'Thổ Nhĩ Kỳ', value: 'tho-nhi-ky' }, + { label: 'Nga', value: 'nga' }, + { label: 'Úc', value: 'uc' }, + { label: 'Brazil', value: 'brazil' }, + { label: 'Ý', value: 'y' }, + { label: 'Na Uy', value: 'na-uy' }, + { label: 'Nam Phi', value: 'nam-phi' }, + ], + }, + type: { + type: FilterTypes.Picker, + label: 'Loại phim', + value: '', + options: [ + { label: 'Tất cả', value: '' }, + { label: 'Phim Lẻ', value: 'phim-le' }, + { label: 'Phim Bộ', value: 'phim-bo' }, + { label: 'TV Shows', value: 'tv-shows' }, + { label: 'Hoạt Hình', value: 'hoat-hinh' }, + ], + }, + year: { + type: FilterTypes.Picker, + label: 'Năm sản xuất', + value: '', + options: [ + { label: 'Tất cả', value: '' }, + { label: '2026', value: '2026' }, + { label: '2025', value: '2025' }, + { label: '2024', value: '2024' }, + { label: '2023', value: '2023' }, + { label: '2022', value: '2022' }, + { label: '2021', value: '2021' }, + { label: '2020', value: '2020' }, + { label: '2019', value: '2019' }, + { label: '2018', value: '2018' }, + { label: '2017', value: '2017' }, + { label: '2016', value: '2016' }, + { label: '2015', value: '2015' }, + { label: '2014', value: '2014' }, + { label: '2013', value: '2013' }, + { label: '2012', value: '2012' }, + { label: '2011', value: '2011' }, + { label: '2010', value: '2010' }, + ], + }, + sort_field: { + type: FilterTypes.Picker, + label: 'Sắp xếp', + value: 'modified.time', + options: [ + { label: 'Mới cập nhật', value: 'modified.time' }, + { label: 'Thời gian đăng', value: 'created.time' }, + { label: 'Năm sản xuất', value: 'year' }, + ], + }, +} satisfies Filters; diff --git a/plugins/vietnamese/KKPhim/index.ts b/plugins/vietnamese/KKPhim/index.ts new file mode 100644 index 00000000..15808800 --- /dev/null +++ b/plugins/vietnamese/KKPhim/index.ts @@ -0,0 +1,272 @@ +import { fetchText } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { load as loadCheerio } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +import { encodeHtmlEntities } from '@libs/utils'; +import { isUrlAbsolute } from '@libs/isAbsoluteUrl'; +import { storage } from '@libs/storage'; +import filters from './filters'; + +const SITE = 'https://kkphim.com'; + +class KKPhimPlugin implements Plugin.PluginBase { + id = 'kkphim'; + name = '🎞 KKPhim'; + icon = 'src/vi/kkphim/icon.png'; + site = SITE; + version = '1.0.0'; + customJS = 'src/vi/kkphim/player.js'; + + filters = filters; + + pluginSettings: Plugin.PluginSettings = { + enableAdBlocker: { + value: false, + label: 'Tắt chặn quảng cáo', + type: 'Switch', + }, + }; + + get enableAdBlocker(): boolean { + return storage.get('enableAdBlocker') as boolean; + } + + imageRequestInit: Plugin.ImageRequestInit = { + headers: { + Referer: this.site + '/', + }, + }; + + // ---------- helpers ---------- + private stripImageProxy(url: string): string { + // phimapi.com/image.php?url= + if (url && url.includes('phimapi.com/image.php')) { + try { + const u = new URL(url); + const real = u.searchParams.get('url'); + if (real) return real; + } catch (e) { /* ignore */ } + } + return url; + } + + private urlToPath(url: string): string { + if (!isUrlAbsolute(url)) { + return url; + } else { + const parsed = new URL(url); + return url.slice(parsed.origin.length); + } + } + + private async fetchHTML(url: string): Promise { + return await fetchText(url); + } + + private parseListHtml(html: string): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + const $ = loadCheerio(html); + const seen = new Set(); + + $('table tbody tr').each((_: number, el: any) => { + const $row = $(el); + const $link = $row.find('a[href*="/phim/"]').first(); + const href = $link.attr('href') || ''; + if (!href) return; + const path = this.urlToPath(href); + if (seen.has(path)) return; + seen.add(path); + + const name = $link.find('h3').first().text().trim(); + const cover = this.stripImageProxy($row.find('img').first().attr('src') || defaultCover); + + if (name) { + novels.push({ name, path, cover }); + } + }); + + return novels; + } + + // ---------- popularNovels ---------- + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + const params = new URLSearchParams(); + params.set('page', String(pageNo)); + + if (!showLatestNovels && filters) { + if (filters.sort_lang.value) params.set('sort_lang', filters.sort_lang.value); + if (filters.category.value) params.set('category', filters.category.value); + if (filters.country.value) params.set('country', filters.country.value); + if (filters.type.value) params.set('type', filters.type.value); + if (filters.year.value) params.set('year', filters.year.value); + if (filters.sort_field.value) params.set('sort_field', filters.sort_field.value); + } else { + params.set('sort_field', 'modified.time'); + } + + const url = `${SITE}/duyet-tim?${params.toString()}`; + const html = await this.fetchHTML(url); + return this.parseListHtml(html); + } + + // ---------- searchNovels ---------- + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const params = new URLSearchParams(); + params.set('keyword', searchTerm.trim()); + params.set('page', String(pageNo)); + + const url = `${SITE}/tim-kiem?${params.toString()}`; + const html = await this.fetchHTML(url); + return this.parseListHtml(html); + } + + // ---------- parseNovel ---------- + async parseNovel( + novelPath: string, + ): Promise { + const url = `${SITE}${novelPath}`; + const html = await this.fetchHTML(url); + const $ = loadCheerio(html); + + // Title + const title = $('h1').first().text().trim(); + const subtitle = $('h2').first().text().trim(); + const name = subtitle ? `${title} - ${subtitle}` : title; + + // Cover - from og:image meta tag + const cover = this.stripImageProxy($('meta[property="og:image"]').attr('content') || defaultCover); + + // Description + const summary = $('article').first().text().trim(); + + // Info table + const novel: Plugin.SourceNovel = { + path: novelPath, + name, + cover, + summary, + }; + + $('table tr').each((_: number, el: any) => { + const cells = $(el).find('td'); + if (cells.length < 2) return; + const key = cells.eq(0).text().trim(); + const value = cells.eq(1).text().trim(); + + switch (key) { + case 'Đạo diễn': + case 'Diễn viên': + if (!novel.author) novel.author = value; + break; + case 'Thể loại': + novel.genres = value; + break; + case 'Tình trạng': + if (/hoàn|full|complete|trọn bộ/i.test(value)) { + novel.status = NovelStatus.Completed; + } else if (/đang|ongoing|tập/i.test(value)) { + novel.status = NovelStatus.Ongoing; + } + break; + } + }); + + const chapters: Plugin.ChapterItem[] = []; + let chapterIndex = 0; + + const episodesStart = html.indexOf('var episodes'); + if (episodesStart !== -1) { + const arrStart = html.indexOf('[', episodesStart); + let depth = 0; + let arrEnd = -1; + for (let i = arrStart; i < html.length; i++) { + if (html[i] === '[') depth++; + if (html[i] === ']') depth--; + if (depth === 0) { arrEnd = i; break; } + } + + let episodesData: { + server_name: string; + list: { name: string; slug: string; embed: string; m3u8: string }[]; + }[] = []; + if (arrEnd > arrStart) { + try { + episodesData = JSON.parse(html.substring(arrStart, arrEnd + 1)); + } catch (e) { + // JSON parse failed — episodesData stays empty + } + } + + if (episodesData.length) { + + for (const server of episodesData) { + const serverName = (server.server_name || '').replace(/^#/, '').trim(); + + for (const ep of server.list) { + if (!ep.m3u8) continue; + + chapterIndex++; + const epNum = parseFloat(ep.name.replace(/[^0-9.]/g, '')); + + chapters.push({ + name: ep.name, + path: ep.m3u8, + chapterNumber: Number.isFinite(epNum) ? epNum : chapterIndex, + page: serverName ? serverName + '\u200b' : undefined, + }); + } + } + } + } + + novel.chapters = chapters; + return novel; + } + + // ---------- parsePage ---------- + async parsePage( + novelPath: string, + page: string, + ): Promise { + const novel = await this.parseNovel(novelPath); + return { + chapters: (novel.chapters || []).filter((ch) => ch.page === page), + }; + } + + // ---------- parseChapter ---------- + async parseChapter(chapterPath: string): Promise { + const m3u8Url = chapterPath.startsWith('http') ? chapterPath : ''; + + if (!m3u8Url) { + return '

Không tìm thấy nguồn video cho tập phim này.

'; + } + + const esc = (s: string) => encodeHtmlEntities(s); + return [ + '', + '', + '', + '', + ``, + ].join('\n'); + } + + resolveUrl(path: string, isNovel?: boolean): string { + if (path.startsWith('http')) return path; + if (isNovel) return `${SITE}/phim/${path.replace(/^\/phim\//, '')}`; + return `${SITE}${path}`; + } +} + +export default new KKPhimPlugin(); diff --git a/plugins/vietnamese/KKPhim/webview/index.ts b/plugins/vietnamese/KKPhim/webview/index.ts new file mode 100644 index 00000000..8b3b11e5 --- /dev/null +++ b/plugins/vietnamese/KKPhim/webview/index.ts @@ -0,0 +1,240 @@ +// KKPhim Player - m3u8 playback with ad blocker + +const DISCONTINUITY_TAG = '#EXT-X-DISCONTINUITY'; + +function collapseConsecutiveDiscontinuities(playlist: string): string { + const lines = playlist.split(/\r?\n/); + const normalized: string[] = []; + let previousWasDiscontinuity = false; + + for (const line of lines) { + if (line.trim() === DISCONTINUITY_TAG) { + if (previousWasDiscontinuity) continue; + normalized.push(DISCONTINUITY_TAG); + previousWasDiscontinuity = true; + continue; + } + normalized.push(line); + if (line.trim().length > 0) { + previousWasDiscontinuity = false; + } + } + + while (normalized.length > 0 && normalized[normalized.length - 1].trim().length === 0) { + normalized.pop(); + } + if (normalized.length > 0 && normalized[normalized.length - 1].trim() === DISCONTINUITY_TAG) { + normalized.pop(); + } + + return normalized.join('\n'); +} + +function processDiscontinuityBlocks(playlist: string): string { + const lines = playlist.replace(/\r\n/g, '\n').split('\n'); + const result: string[] = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + if (line.trim() !== DISCONTINUITY_TAG) { + result.push(line); + i++; + continue; + } + + const blockLines: string[] = []; + i++; + + while (i < lines.length && lines[i].trim() !== DISCONTINUITY_TAG) { + blockLines.push(lines[i]); + i++; + } + + if (i < lines.length) i++; // skip closing DISCONTINUITY + + const segmentLines = blockLines.filter((l) => { + const trimmed = l.trim(); + return trimmed.length > 0 && !trimmed.startsWith('#'); + }); + + // Match /v7/, /v8/, convertv7/, convertv8/ etc. (ad segments) + const isAdBlock = segmentLines.some((l) => /\/v\d+\/|convertv\d+\//.test(l)); + + if (!isAdBlock) { + result.push(DISCONTINUITY_TAG); + result.push(...blockLines); + result.push(DISCONTINUITY_TAG); + } else { + // Remove ad block but keep DISCONTINUITY marker for PTS reset + result.push(DISCONTINUITY_TAG); + } + } + + return collapseConsecutiveDiscontinuities(result.join('\n')); +} + +function cleanManifest(manifest: string): string { + const lines = manifest.replace(/\r\n/g, '\n').split('\n'); + const discontinuityIndexes: number[] = []; + + for (let i = 0; i < lines.length; i++) { + if (lines[i].trim() === DISCONTINUITY_TAG) { + discontinuityIndexes.push(i); + } + } + + const removeLine = new Array(lines.length).fill(false); + + for (let index = 0; index < discontinuityIndexes.length - 1; index++) { + const start = discontinuityIndexes[index]; + const end = discontinuityIndexes[index + 1]; + let segmentCount = 0; + let totalDuration = 0; + let shortDurationCount = 0; + + for (let lineIndex = start + 1; lineIndex < end; lineIndex++) { + const line = lines[lineIndex].trim(); + + if (/\.ts($|\?)/i.test(line)) segmentCount++; + + if (line.startsWith('#EXTINF:')) { + const match = line.match(/#EXTINF:([\d.]+),/); + if (!match) continue; + const duration = parseFloat(match[1]); + if (!isFinite(duration)) continue; + totalDuration += duration; + if (duration < 2.5) shortDurationCount++; + } + } + + const averageDuration = segmentCount > 0 ? totalDuration / segmentCount : 0; + const shortDurationRatio = segmentCount > 0 ? shortDurationCount / segmentCount : 0; + + const isAdBlock = + segmentCount >= 8 && + segmentCount <= 20 && + (averageDuration < 3 || shortDurationRatio >= 0.6); + + if (isAdBlock) { + for (let lineIndex = start; lineIndex <= end; lineIndex++) { + removeLine[lineIndex] = true; + } + } + } + + for (let i = 0; i < lines.length; i++) { + if (/^#EXT-X-KEY:METHOD=NONE\b/i.test(lines[i].trim())) removeLine[i] = true; + if (lines[i].trim() === DISCONTINUITY_TAG) removeLine[i] = true; + } + + const cleanedLines: string[] = []; + for (let i = 0; i < lines.length; i++) { + if (!removeLine[i]) { + cleanedLines.push(lines[i].replace(/\/convertv\d+\//g, '/')); + } + } + + return cleanedLines.join('\n').replace(/\n{3,}/g, '\n\n').trim(); +} + +function isContainAds(playlist: string): boolean { + if (/\/v\d+\//.test(playlist)) return true; + if (/convertv\d+\//.test(playlist)) return true; + return false; +} + +function cleanMediaPlaylistText(text: string, baseUrl: string): string { + if (!text.includes('#EXTM3U')) return text; + + // Resolve relative URLs to absolute + const withAbsoluteUrls = text.replace(/^[^#].*$/gm, (line) => { + const trimmed = line.trim(); + if (!trimmed) return line; + if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) return line; + try { + return new URL(trimmed, baseUrl).toString(); + } catch { + return line; + } + }); + + if (!isContainAds(withAbsoluteUrls)) { + return collapseConsecutiveDiscontinuities(withAbsoluteUrls); + } + + return processDiscontinuityBlocks(withAbsoluteUrls); +} + +// ── Main ── +(async function () { + if (!window.LNReaderPlayer) return; + + const container = document.getElementById('kkphim-player-container'); + if (!container) return; + + const m3u8Url = container.getAttribute('data-m3u8') || ''; + const adBlockerDisabled = container.getAttribute('data-ad-blocker') === 'true'; + + if (!m3u8Url) { + window.LNReaderPlayer.log('No m3u8 URL found'); + return; + } + + window.LNReaderPlayer.log('KKPhim player initialized'); + window.LNReaderPlayer.log('Ad blocker: ' + (adBlockerDisabled ? 'OFF' : 'ON')); + + if (adBlockerDisabled) { + window.LNReaderPlayer.log('Playing m3u8 directly (ad blocker off)'); + window.LNReaderPlayer.playHls(m3u8Url); + return; + } + + // ── Ad blocker: fetch, clean, play blob ── + window.LNReaderPlayer.log('Fetching m3u8 for ad cleaning...'); + + try { + const res = await fetch(m3u8Url); + const playlistText = await res.text(); + + // Master playlist — find best quality stream + if (playlistText.includes('#EXT-X-STREAM-INF')) { + window.LNReaderPlayer.log('Master playlist detected, parsing streams...'); + + const lines = playlistText.split('\n'); + let streamUrl = ''; + for (let i = 0; i < lines.length; i++) { + if (lines[i].trim().startsWith('#EXT-X-STREAM-INF')) { + const nextLine = lines[i + 1]?.trim(); + if (nextLine && !nextLine.startsWith('#')) { + streamUrl = new URL(nextLine, m3u8Url).toString(); + break; + } + } + } + + if (streamUrl) { + window.LNReaderPlayer.log('Fetching stream playlist: ' + streamUrl); + const streamRes = await fetch(streamUrl); + const streamPlaylist = await streamRes.text(); + const cleaned = cleanMediaPlaylistText(streamPlaylist, streamUrl); + window.LNReaderPlayer.log('Ad cleaning complete, playing...'); + const blob = new Blob([cleaned], { type: 'application/vnd.apple.mpegurl' }); + const blobUrl = URL.createObjectURL(blob); + window.LNReaderPlayer.playHls(blobUrl); + return; + } + } + + const cleaned = cleanMediaPlaylistText(playlistText, m3u8Url); + window.LNReaderPlayer.log('Ad cleaning complete, playing...'); + const blob = new Blob([cleaned], { type: 'application/vnd.apple.mpegurl' }); + const blobUrl = URL.createObjectURL(blob); + window.LNReaderPlayer.playHls(blobUrl); + } catch (err: any) { + window.LNReaderPlayer.log('Error cleaning ads: ' + err.message); + window.LNReaderPlayer.log('Falling back to direct playback...'); + window.LNReaderPlayer.playHls(m3u8Url); + } +})(); diff --git a/public/static/src/vi/kkphim/icon.png b/public/static/src/vi/kkphim/icon.png new file mode 100644 index 00000000..74a18460 Binary files /dev/null and b/public/static/src/vi/kkphim/icon.png differ