2026-06-28 18:19:09 +07:00
|
|
|
const axios = require('axios')
|
|
|
|
|
const cheerio = require('cheerio')
|
|
|
|
|
const crypto = require('crypto')
|
2026-04-09 00:02:18 +02:00
|
|
|
const dayjs = require('dayjs')
|
|
|
|
|
const utc = require('dayjs/plugin/utc')
|
2026-06-28 18:19:09 +07:00
|
|
|
const doFetch = require('@ntlab/sfetch')
|
|
|
|
|
|
2026-04-09 00:02:18 +02:00
|
|
|
dayjs.extend(utc)
|
2026-06-28 18:19:09 +07:00
|
|
|
|
|
|
|
|
const eventIds = []
|
|
|
|
|
const caches = {}
|
|
|
|
|
const headers = {
|
|
|
|
|
'user-agent':
|
|
|
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
|
|
|
|
|
}
|
2025-09-28 17:55:05 +03:00
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
site: 'tv.mail.ru',
|
|
|
|
|
days: 2,
|
2026-06-28 18:19:09 +07:00
|
|
|
request: {
|
|
|
|
|
cache: {
|
|
|
|
|
ttl: 24 * 60 * 60 * 1000 // 1 day
|
|
|
|
|
},
|
|
|
|
|
headers
|
2025-09-28 17:55:05 +03:00
|
|
|
},
|
2026-06-28 18:19:09 +07:00
|
|
|
async url({ date }) {
|
|
|
|
|
return await followUrl(getUrl(date))
|
|
|
|
|
},
|
|
|
|
|
async parser({ content, channel, date }) {
|
2025-09-28 17:55:05 +03:00
|
|
|
const programs = []
|
2026-06-28 18:19:09 +07:00
|
|
|
if (content) {
|
|
|
|
|
// common utility
|
|
|
|
|
const v = { e: [], d: date, p: 1 }
|
|
|
|
|
const ev = (res, url) => {
|
|
|
|
|
if (typeof res === 'string' || Buffer.isBuffer(res)) {
|
|
|
|
|
res = JSON.parse(res)
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(res?.data?.schedule?.items)) {
|
|
|
|
|
res.data.schedule.items
|
|
|
|
|
.filter(schedule => schedule.id == channel.site_id)
|
|
|
|
|
.forEach(schedule => {
|
|
|
|
|
if (Array.isArray(schedule.events)) {
|
|
|
|
|
schedule.events
|
|
|
|
|
.filter(event => !eventIds.includes(event.id))
|
|
|
|
|
.forEach(event => {
|
|
|
|
|
// use event id as unique filter
|
|
|
|
|
if (!v.e.find(item => item.id === event.id)) {
|
|
|
|
|
v.e.push(event)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
// cache result
|
|
|
|
|
if (url) {
|
|
|
|
|
caches[url] = res
|
|
|
|
|
}
|
2025-09-28 17:55:05 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-28 18:19:09 +07:00
|
|
|
const mu = (next = true, dt = null) => {
|
|
|
|
|
return getUrl(dt ? dt : v.d, next ? ++v.p : v.p)
|
|
|
|
|
}
|
|
|
|
|
const mq = oUrl => ({
|
|
|
|
|
async url(queue) {
|
|
|
|
|
return await followUrl(queue.oUrl)
|
|
|
|
|
},
|
|
|
|
|
oUrl,
|
|
|
|
|
params: module.exports.request
|
2025-09-28 17:55:05 +03:00
|
|
|
})
|
2026-06-28 18:19:09 +07:00
|
|
|
const cr = async (next = true, dt = null) => {
|
|
|
|
|
const oUrl = mu(next, dt)
|
|
|
|
|
if (caches[oUrl]) {
|
|
|
|
|
ev(caches[oUrl])
|
|
|
|
|
} else {
|
|
|
|
|
await doFetch([mq(oUrl)], (queue, res) => {
|
|
|
|
|
ev(res, queue.oUrl)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// add main content
|
|
|
|
|
ev(content)
|
|
|
|
|
// fetch next page until site is found
|
|
|
|
|
await new Promise(resolve => {
|
|
|
|
|
const f = async () => {
|
|
|
|
|
if (v.e.length) {
|
|
|
|
|
resolve()
|
|
|
|
|
} else {
|
|
|
|
|
await cr()
|
|
|
|
|
f()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
f()
|
|
|
|
|
})
|
|
|
|
|
if (v.e.length) {
|
|
|
|
|
date = date.startOf('d')
|
|
|
|
|
v.n = [...v.e]
|
|
|
|
|
v.e = []
|
|
|
|
|
// fetch previous day
|
|
|
|
|
await cr(false, date.subtract(1, 'd'))
|
|
|
|
|
v.e.push(...v.n)
|
|
|
|
|
// process collected events
|
|
|
|
|
const queues = []
|
|
|
|
|
const events = []
|
|
|
|
|
v.e
|
|
|
|
|
.forEach(event => {
|
|
|
|
|
const start = dayjs.utc(event.start_ts * 1000)
|
|
|
|
|
const stop = dayjs.utc(event.stop_ts * 1000)
|
|
|
|
|
if (date.isSame(start, 'd') || (date.isSame(stop, 'd') && stop > date)) {
|
|
|
|
|
eventIds.push(event.id)
|
|
|
|
|
const url = `https://tv.mail.ru${event.url}`
|
|
|
|
|
const evt = {
|
|
|
|
|
program: {
|
|
|
|
|
title: event.name,
|
|
|
|
|
image: event.image,
|
|
|
|
|
start,
|
|
|
|
|
stop
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (caches[url]) {
|
|
|
|
|
evt.detail = caches[url]
|
|
|
|
|
} else {
|
|
|
|
|
queues.push({
|
|
|
|
|
async url(queue) {
|
|
|
|
|
return await followUrl(queue.oUrl)
|
|
|
|
|
},
|
|
|
|
|
oUrl: url,
|
|
|
|
|
params: module.exports.request,
|
|
|
|
|
evt
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
events.push(evt)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
// fetch detailed programs
|
|
|
|
|
if (process.env.EPG_DETAILED_GUIDE) {
|
|
|
|
|
await doFetch(queues, (queue, res) => {
|
|
|
|
|
queue.evt.detail = res
|
|
|
|
|
caches[queue.oUrl] = res
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
// add programs
|
|
|
|
|
programs.push(...events
|
|
|
|
|
.map(evt => ({
|
|
|
|
|
...evt.program,
|
|
|
|
|
...parseProgram(evt.detail)
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-28 17:55:05 +03:00
|
|
|
|
|
|
|
|
return programs
|
|
|
|
|
},
|
|
|
|
|
async channels() {
|
2026-06-28 18:19:09 +07:00
|
|
|
const channels = []
|
|
|
|
|
const args = { date: dayjs(), page: 0 }
|
|
|
|
|
const queues = [getUrl(args.date, ++args.page)]
|
|
|
|
|
await doFetch(queues, (url, res) => {
|
|
|
|
|
if (res?.data?.schedule?.has_next_page) {
|
|
|
|
|
queues.push(getUrl(args.date, ++args.page))
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(res?.data?.schedule?.items)) {
|
|
|
|
|
channels.push(...res.data.schedule.items
|
|
|
|
|
.map(item => ({
|
2025-09-28 17:55:05 +03:00
|
|
|
lang: 'ru',
|
|
|
|
|
name: item.name,
|
|
|
|
|
site_id: item.id
|
2026-06-28 18:19:09 +07:00
|
|
|
}))
|
|
|
|
|
)
|
2025-09-28 17:55:05 +03:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-06-28 18:19:09 +07:00
|
|
|
return channels
|
|
|
|
|
}
|
2025-09-28 17:55:05 +03:00
|
|
|
}
|
|
|
|
|
|
2026-06-28 18:19:09 +07:00
|
|
|
function parseProgram(data) {
|
|
|
|
|
const res = {}
|
|
|
|
|
if (data) {
|
|
|
|
|
const props = {
|
|
|
|
|
title: '.fa7d86047b [data-qa="Title"]',
|
|
|
|
|
sub_title: '.d4a57e7163',
|
|
|
|
|
category: '[data-qa="TagLink"] [data-qa="Text"]',
|
|
|
|
|
description: ['.f9cbc7a035', 'h2'],
|
|
|
|
|
actor: '[data-qa*="TEST_IDS__EVENT_ATTR__actors_link_"]',
|
|
|
|
|
director: '[data-qa*="TEST_IDS__EVENT_ATTR__director_link_"]',
|
|
|
|
|
date: '[data-qa="TEST_IDS__TEVENT_META__start_year"]',
|
|
|
|
|
country: '.ec1bc00977'
|
|
|
|
|
}
|
|
|
|
|
const $ = cheerio.load(data)
|
|
|
|
|
for (const [prop, selector] of Object.entries(props)) {
|
|
|
|
|
const item = $(Array.isArray(selector) ? selector[0] : selector)
|
|
|
|
|
if (item.length) {
|
|
|
|
|
// prune children
|
|
|
|
|
if (Array.isArray(selector)) {
|
|
|
|
|
item.find(selector[1]).remove()
|
|
|
|
|
}
|
|
|
|
|
if (item.length > 1) {
|
|
|
|
|
res[prop] = item.toArray()
|
|
|
|
|
.map(item => $(item).text().trim())
|
|
|
|
|
} else {
|
|
|
|
|
res[prop] = item.text().trim()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-28 17:55:05 +03:00
|
|
|
}
|
|
|
|
|
|
2026-06-28 18:19:09 +07:00
|
|
|
return res
|
2025-09-28 17:55:05 +03:00
|
|
|
}
|
|
|
|
|
|
2026-06-28 18:19:09 +07:00
|
|
|
function getUrl(date, page = 1) {
|
|
|
|
|
page = page > 1 ? `&page=${page}` : ''
|
|
|
|
|
return `https://tv.mail.ru/ajax/service/index/schedule/?region_id=70&channel_type=all&date=${
|
|
|
|
|
date.format('YYYY-MM-DD')
|
|
|
|
|
}&appearance=grid&period=all${
|
|
|
|
|
page
|
|
|
|
|
}`
|
|
|
|
|
}
|
2025-09-28 17:55:05 +03:00
|
|
|
|
2026-06-28 18:19:09 +07:00
|
|
|
async function followUrl(url) {
|
|
|
|
|
const f = async (m = 'head') => {
|
|
|
|
|
await axios[m](url, { headers })
|
|
|
|
|
.then(res => {
|
|
|
|
|
if (res.headers && res.headers['x-challenge'] === 'required') {
|
|
|
|
|
const challengeUrl = new URL(res.request.res.responseUrl)
|
|
|
|
|
if (challengeUrl.searchParams.has('hash429')) {
|
|
|
|
|
const key = crypto.hash('md5', challengeUrl.searchParams.get('hash429'))
|
|
|
|
|
if (challengeUrl.searchParams.has('key')) {
|
|
|
|
|
challengeUrl.searchParams.set('key', key)
|
|
|
|
|
} else {
|
|
|
|
|
challengeUrl.searchParams.append('key', key)
|
|
|
|
|
}
|
|
|
|
|
url = challengeUrl.toString()
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('Unknown challenge page!')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
// @todo: handle captcha on 429
|
|
|
|
|
.catch(console.error)
|
|
|
|
|
|
|
|
|
|
return url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await f()
|
2025-09-28 17:55:05 +03:00
|
|
|
}
|