lk21
lk21 movie + drama lengkap
4 Juni 20261882 file
lk21.jsjavascript · 242 baris
/** * Lk21 movie + drama * base: https://tv10.lk21official.cc/ * Creator: ShanMolvyr * Jangan Hapus Kreator woi ^_^ * * * Note: cek https://snippet.vyr.my.id/shanmolvyr/lk21/README.md * Sumber: https://whatsapp.com/channel/0029VbB4Kw8EFeXfeExaXc3Q */const axios = require('axios');const cheerio = require('cheerio');const DOMAINS = { lk21: 'https://tv10.lk21official.cc', nontondrama: 'https://tv4.nontondrama.my',};const HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'Accept-Language': 'id-ID,id;q=0.9,en-US;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Cache-Control': 'no-cache',};async function fetchPage(url, referer) { const res = await axios.get(url, { headers: { ...HEADERS, Referer: referer || url }, timeout: 15000, maxRedirects: 5, }); return cheerio.load(res.data);}function parseList($) { const results = []; $('article').each((_, el) => { const $el = $(el); const $a = $el.find('figure a').first(); const href = $a.attr('href') || ''; if (!href) return; const title = $el.find('h3.poster-title, h2.poster-title').first().text().trim() || $a.attr('title') || ''; const poster = $el.find('source[type="image/jpeg"]').attr('srcset') || $el.find('img').attr('data-src') || $el.find('img').attr('src') || ''; const year = $el.find('span.year').text().trim() || ''; const quality = $el.find('span.label').text().trim() || ''; const rating = $el.find('span[itemprop="ratingValue"]').text().trim() || ''; const episode = $el.find('span.episode strong').text().trim() || ''; const duration = $el.find('span.duration').text().trim() || ''; results.push({ title, href, poster, year, quality, rating, episode, duration }); }); return results;}function getYear($) { let year = ''; $('script[type="application/ld+json"]').each((_, el) => { if (year) return; const m = ($(el).html() || '').match(/"datePublished":\s*"(\d{4})/); if (m) year = m[1]; }); if (!year) $('.tag-list a[href*="/year/"]').each((_, el) => { year = $(el).text().trim(); }); return year;}function getTags($) { const tags = []; $('.tag-list .tag a').each((_, el) => tags.push({ label: $(el).text().trim(), href: $(el).attr('href') || '' })); return { genre: tags.filter(t => t.href.includes('/genre/')).map(t => t.label), country: tags.filter(t => t.href.includes('/country/')).map(t => t.label), };}function parseMovieDetail($) { const title = $('h1').first().text().trim(); const rating = ($('.info-tag span strong').first().text().trim()).replace(/[^\d.]/g, ''); const infoSpans = []; $('.info-tag span').each((_, el) => { const t = $(el).text().trim(); if (t) infoSpans.push(t); }); const { genre, country } = getTags($); const synopsis = $('[data-full]').first().attr('data-full') || ''; const poster = $('meta[property="og:image"]').attr('content') || ''; const servers = []; const seen = new Set(); $('[data-server]').each((_, el) => { const server = $(el).attr('data-server'), url = $(el).attr('data-url'); if (server && url && !seen.has(server)) { seen.add(server); servers.push({ server, url }); } }); return { title, rating, quality: infoSpans[1] || '', resolution: infoSpans[2] || '', duration: infoSpans[3] || '', year: getYear($), genre, country, synopsis, poster, servers };}function parseSeriesDetail($) { const title = $('h1').first().text().trim(); const rating = ($('.info-tag span strong').first().text().trim()).replace(/[^\d.]/g, ''); const infoSpans = []; $('.info-tag span').each((_, el) => { const t = $(el).text().trim(); if (t) infoSpans.push(t); }); const { genre, country } = getTags($); const synopsis = $('[data-full]').first().attr('data-full') || ''; const poster = $('meta[property="og:image"]').attr('content') || ''; let episodes = []; $('script').each((_, el) => { const txt = $(el).html() || ''; const m = txt.match(/^\s*(\{"1":\[.*\].*\})\s*$/); if (m) { try { const data = JSON.parse(m[1]); Object.values(data).forEach(season => season.forEach(ep => { episodes.push({ episode: ep.episode_no, season: ep.s, title: ep.title, slug: ep.slug, href: `/${ep.slug}` }); })); } catch (_) {} } }); if (episodes.length === 0) { $('.episode-list a').each((_, el) => { const href = $(el).attr('href') || '', label = $(el).text().trim(); if (href && href.includes('episode')) episodes.push({ label, href }); }); } return { title, rating, airDate: infoSpans[1] || '', type: infoSpans[2] || '', status: infoSpans[3] || '', year: getYear($), genre, country, synopsis, poster, episodes };}function parseEpisodeWatch($) { const title = $('h1').first().text().trim(); let meta = {}; $('script').each((_, el) => { const txt = $(el).html() || ''; const m = txt.match(/\{[^<]*"current_eps"[^<]*\}/); if (m) { try { meta = JSON.parse(m[0]); } catch (_) {} } }); const servers = []; const seen = new Set(); $('[data-server]').each((_, el) => { const server = $(el).attr('data-server'), url = $(el).attr('data-url'); if (server && url && !seen.has(server)) { seen.add(server); servers.push({ server, url }); } }); let prevEp = null; const nextEp = meta.next ? `/${meta.next}` : null; if (meta.current_eps > 1 && meta.slug) { prevEp = `/${meta.slug.replace(/-episode-\d+-/, `-episode-${meta.current_eps - 1}-`)}`; } return { title, season: meta.current_season || null, episode: meta.current_eps || null, totalEps: meta.total_eps || null, rating: meta.rating || null, poster: meta.poster || null, seriesSlug: meta.slug || null, servers, prevEp, nextEp };}const api = { async home() { const $ = await fetchPage(`${DOMAINS.lk21}/`); return { source: DOMAINS.lk21, data: parseList($) }; }, async latest(page = 1) { const url = page > 1 ? `${DOMAINS.lk21}/latest/page/${page}/` : `${DOMAINS.lk21}/latest/`; const $ = await fetchPage(url); return { page, data: parseList($) }; }, async search(q, page = 1) { const b = `${DOMAINS.lk21}/search/`; const url = page > 1 ? `${b}page/${page}/?s=${encodeURIComponent(q)}` : `${b}?s=${encodeURIComponent(q)}`; const $ = await fetchPage(url); return { query: q, page, data: parseList($) }; }, async genre(genre, page = 1) { const b = `${DOMAINS.lk21}/genre/${genre}/`; const url = page > 1 ? `${b}page/${page}/` : b; const $ = await fetchPage(url); return { genre, page, data: parseList($) }; }, async detail(slug) { const url = `${DOMAINS.lk21}/${slug}/`; const $ = await fetchPage(url, DOMAINS.lk21); const h1 = $('h1').first().text().toLowerCase(); if (h1.includes('dialihkan') || h1.includes('nontondrama')) { const sUrl = `${DOMAINS.nontondrama}/${slug}/`; const $s = await fetchPage(sUrl, DOMAINS.nontondrama); return { url: sUrl, type: 'series', data: parseSeriesDetail($s) }; } return { url, type: 'movie', data: parseMovieDetail($) }; }, async seriesHome() { const $ = await fetchPage(`${DOMAINS.nontondrama}/`); return { source: DOMAINS.nontondrama, data: parseList($) }; }, async seriesLatest(page = 1) { const url = page > 1 ? `${DOMAINS.nontondrama}/latest-series/page/${page}/` : `${DOMAINS.nontondrama}/latest-series/`; const $ = await fetchPage(url); return { page, data: parseList($) }; }, async seriesSearch(q, page = 1) { const b = `${DOMAINS.nontondrama}/search/`; const url = page > 1 ? `${b}page/${page}/?s=${encodeURIComponent(q)}` : `${b}?s=${encodeURIComponent(q)}`; const $ = await fetchPage(url); return { query: q, page, data: parseList($) }; }, async seriesGenre(genre, page = 1) { const b = `${DOMAINS.nontondrama}/genre/${genre}/`; const url = page > 1 ? `${b}page/${page}/` : b; const $ = await fetchPage(url); return { genre, page, data: parseList($) }; }, async seriesDetail(slug) { const url = `${DOMAINS.nontondrama}/${slug}/`; const $ = await fetchPage(url, DOMAINS.nontondrama); const ogType = $('meta[property="og:type"]').attr('content') || ''; const ogUrl = $('meta[property="og:url"]').attr('content') || ''; const isMovie = ogType !== 'series' && (ogUrl.includes('d21.team') || ogUrl.includes('lk21official') || !ogUrl.includes('nontondrama')); if (isMovie) { const mUrl = `${DOMAINS.lk21}/${slug}/`; const $m = await fetchPage(mUrl, DOMAINS.lk21); return { url: mUrl, type: 'movie', data: parseMovieDetail($m) }; } return { url, type: 'series', data: parseSeriesDetail($) }; }, async seriesWatch(slug) { const url = `${DOMAINS.nontondrama}/${slug}/`; const $ = await fetchPage(url, DOMAINS.nontondrama); return { url, data: parseEpisodeWatch($) }; }, async searchAll(q) { const [a, b] = await Promise.allSettled([api.search(q), api.seriesSearch(q)]); return { query: q, movies: a.status === 'fulfilled' ? a.value : { error: a.reason?.message }, series: b.status === 'fulfilled' ? b.value : { error: b.reason?.message } }; },};function startServer() { const express = require('express'); const cors = require('cors'); const app = express(); const PORT = process.env.PORT || 3000; app.use(cors()); app.use(express.json()); const h = (fn) => async (req, res) => { try { res.json({ ok: true, ...(await fn(req)) }); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } }; app.get('/', (_, res) => res.json({ name: 'lk21-api', v: '1.0.0', by: '@SanVyr', endpoints: ['GET /movie/home','GET /movie/latest?page=','GET /movie/search?q=&page=','GET /movie/genre/:genre?page=','GET /movie/detail?slug= (auto-detect movie/series)','GET /series/home','GET /series/latest?page=','GET /series/search?q=&page=','GET /series/genre/:genre?page=','GET /series/detail?slug=','GET /series/watch?slug=','GET /search?q='] })); app.get('/movie/home', h(() => api.home())); app.get('/movie/latest', h(r => api.latest(+r.query.page || 1))); app.get('/movie/search', h(r => { if (!r.query.q) throw new Error('?q= required'); return api.search(r.query.q, +r.query.page || 1); })); app.get('/movie/genre/:genre', h(r => api.genre(r.params.genre, +r.query.page || 1))); app.get('/movie/detail', h(r => { if (!r.query.slug) throw new Error('?slug= required'); return api.detail(r.query.slug); })); app.get('/series/home', h(() => api.seriesHome())); app.get('/series/latest', h(r => api.seriesLatest(+r.query.page || 1))); app.get('/series/search', h(r => { if (!r.query.q) throw new Error('?q= required'); return api.seriesSearch(r.query.q, +r.query.page || 1); })); app.get('/series/genre/:genre', h(r => api.seriesGenre(r.params.genre, +r.query.page || 1))); app.get('/series/detail', h(r => { if (!r.query.slug) throw new Error('?slug= required'); return api.seriesDetail(r.query.slug); })); app.get('/series/watch', h(r => { if (!r.query.slug) throw new Error('?slug= required'); return api.seriesWatch(r.query.slug); })); app.get('/search', h(r => { if (!r.query.q) throw new Error('?q= required'); return api.searchAll(r.query.q); })); app.use((_, res) => res.status(404).json({ ok: false, error: 'Not found' })); app.listen(PORT, () => console.error(`lk21-api :${PORT}`));}async function cli() { const [,, cmd, ...args] = process.argv; function parseQueryPage(args) { const lastIsPage = args.length > 1 && /^\d+$/.test(args[args.length - 1]); return { query: (lastIsPage ? args.slice(0, -1) : args).join(' '), page: lastIsPage ? +args[args.length - 1] : 1 }; } const commands = { 'home': () => api.home(), 'latest': () => api.latest(+args[0] || 1), 'search': () => { const { query, page } = parseQueryPage(args); return api.search(query, page); }, 'genre': () => api.genre(args[0], +args[1] || 1), 'detail': () => api.detail(args[0]), 'series:home': () => api.seriesHome(), 'series:latest': () => api.seriesLatest(+args[0] || 1), 'series:search': () => { const { query, page } = parseQueryPage(args); return api.seriesSearch(query, page); }, 'series:genre': () => api.seriesGenre(args[0], +args[1] || 1), 'series:detail': () => api.seriesDetail(args[0]), 'series:watch': () => api.seriesWatch(args[0]), 'search:all': () => { const { query } = parseQueryPage(args); return api.searchAll(query); }, 'server': () => { startServer(); return null; }, }; if (!cmd || !commands[cmd]) { process.exit(1); } const result = await commands[cmd](); if (result !== null) console.log(JSON.stringify({ ok: true, ...result }, null, 2));}cli().catch(err => { console.log(JSON.stringify({ ok: false, error: err.message }, null, 2)); process.exit(1); });README.mdmarkdown · 46 baris
Komentar
Masuk untuk ikut berkomentar.
Belum ada komentar.